From 911110c6bc4d80cb0452a9789bb0d4c8b4946b64 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Sat, 11 May 2013 16:19:04 -0400 Subject: [PATCH 001/375] Imports djanalytics, and adds URLs --- lms/envs/analyticsserver.py | 262 ++++++++++++++++++++++++++++++++++++ lms/envs/common.py | 7 + lms/urls.py | 8 ++ 3 files changed, 277 insertions(+) create mode 100644 lms/envs/analyticsserver.py diff --git a/lms/envs/analyticsserver.py b/lms/envs/analyticsserver.py new file mode 100644 index 0000000000..e91e935d95 --- /dev/null +++ b/lms/envs/analyticsserver.py @@ -0,0 +1,262 @@ +""" + +This config file is used to host an analytics server. The edX codebase +is fairly monolithic, and expensive to import from within the +analytics framework. It also mixes up Django authentication databases, +etc. The analytics framework is fairly modular, and easy to import +from within edX. With this configuration. + +This should configuration should never be enabled on a production LMS. + +This configuration should also never be used as the main analytics +server. It should only be used as a thin layer to allow access to edX +data in a way that can use the edX libraries. When used in this mode, +it should only be granted access to read replicas of the databases. + +""" +import json + +ROOT_URLCONF = 'lms.urls' + +from .common import * +from logsettings import get_logger_config + +DEBUG = True +TEMPLATE_DEBUG = True + + +MITX_FEATURES['DISABLE_START_DATES'] = True +MITX_FEATURES['ENABLE_SQL_TRACKING_LOGS'] = True +MITX_FEATURES['SUBDOMAIN_COURSE_LISTINGS'] = False # Enable to test subdomains--otherwise, want all courses to show up +MITX_FEATURES['SUBDOMAIN_BRANDING'] = True +MITX_FEATURES['FORCE_UNIVERSITY_DOMAIN'] = None # show all university courses if in dev (ie don't use HTTP_HOST) +MITX_FEATURES['ENABLE_MANUAL_GIT_RELOAD'] = True +MITX_FEATURES['ENABLE_PSYCHOMETRICS'] = False # real-time psychometrics (eg item response theory analysis in instructor dashboard) +MITX_FEATURES['ENABLE_INSTRUCTOR_ANALYTICS'] = True +MITX_FEATURES['RUN_AS_ANALYTICS_SERVER_ENABLED'] = True + +INSTALLED_APPS = INSTALLED_APPS + ( 'djeventstream.httphandler', + 'djcelery', + 'south', + 'djanalytics.core', + 'djanalytics.modulefs', +) + +INSTALLED_ANALYTICS_MODULES = open("../analytics_modules.txt").readlines() +INSTALLED_ANALYTICS_MODULES = [x for x in INSTALLED_ANALYTICS_MODULES if x and len(x)>1] + +DJFS = { 'type' : 'osfs', + 'directory_root' : '/tmp/djfsmodule', + 'url_root' : 'file:///tmp/' + } + +import djcelery + +djcelery.setup_loader() +default_optional_kwargs = ['fs','db','query'] + +WIKI_ENABLED = True + +LOGGING = get_logger_config(ENV_ROOT / "log", + logging_env="dev", + local_loglevel="DEBUG", + dev_env=True, + debug=True) + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': ENV_ROOT / "db" / "mitx.db", + } +} + +CACHES = { + # This is the cache used for most things. + # 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', + }, + + 'mongo_metadata_inheritance': { + 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', + 'LOCATION': '/var/tmp/mongo_metadata_inheritance', + 'TIMEOUT': 300, + 'KEY_FUNCTION': 'util.memcache.safe_key', + } +} + + +XQUEUE_INTERFACE = { + "url": "https://sandbox-xqueue.edx.org", + "django_auth": { + "username": "lms", + "password": "***REMOVED***" + }, + "basic_auth": ('anant', 'agarwal'), +} + +# Make the keyedcache startup warnings go away +CACHE_TIMEOUT = 0 + +# Dummy secret key for dev +SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd' + + +COURSE_LISTINGS = { + 'default': ['BerkeleyX/CS169.1x/2012_Fall', + 'BerkeleyX/CS188.1x/2012_Fall', + 'HarvardX/CS50x/2012', + 'HarvardX/PH207x/2012_Fall', + 'MITx/3.091x/2012_Fall', + 'MITx/6.002x/2012_Fall', + 'MITx/6.00x/2012_Fall'], + 'berkeley': ['BerkeleyX/CS169/fa12', + 'BerkeleyX/CS188/fa12'], + 'harvard': ['HarvardX/CS50x/2012H'], + 'mit': ['MITx/3.091/MIT_2012_Fall'], + 'sjsu': ['MITx/6.002x-EE98/2012_Fall_SJSU'], +} + + +SUBDOMAIN_BRANDING = { + 'sjsu': 'MITx', + 'mit': 'MITx', + 'berkeley': 'BerkeleyX', + 'harvard': 'HarvardX', +} + +# List of `university` landing pages to display, even though they may not +# have an actual course with that org set +VIRTUAL_UNIVERSITIES = [] + +# Organization that contain other organizations +META_UNIVERSITIES = {'UTx': ['UTAustinX']} + +COMMENTS_SERVICE_KEY = "PUT_YOUR_API_KEY_HERE" + +############################## Course static files ########################## +if os.path.isdir(DATA_DIR): + # Add the full course repo if there is no static directory + STATICFILES_DIRS += [ + # TODO (cpennington): When courses are stored in a database, this + # should no longer be added to STATICFILES + (course_dir, DATA_DIR / course_dir) + for course_dir in os.listdir(DATA_DIR) + if (os.path.isdir(DATA_DIR / course_dir) and + not os.path.isdir(DATA_DIR / course_dir / 'static')) + ] + # Otherwise, add only the static directory from the course dir + STATICFILES_DIRS += [ + # TODO (cpennington): When courses are stored in a database, this + # should no longer be added to STATICFILES + (course_dir, DATA_DIR / course_dir / 'static') + for course_dir in os.listdir(DATA_DIR) + if (os.path.isdir(DATA_DIR / course_dir / 'static')) + ] + + +################################# mitx revision string ##################### + +MITX_VERSION_STRING = os.popen('cd %s; git describe' % REPO_ROOT).read().strip() + +################################# Open ended grading config ##################### + +OPEN_ENDED_GRADING_INTERFACE = { + 'url' : 'http://127.0.0.1:3033/', + 'username' : 'lms', + 'password' : 'abcd', + 'staff_grading' : 'staff_grading', + 'peer_grading' : 'peer_grading', + 'grading_controller' : 'grading_controller' +} + +################################ LMS Migration ################################# +MITX_FEATURES['ENABLE_LMS_MIGRATION'] = True +MITX_FEATURES['ACCESS_REQUIRE_STAFF_FOR_COURSE'] = False # require that user be in the staff_* group to be able to enroll +MITX_FEATURES['USE_XQA_SERVER'] = 'http://xqa:server@content-qa.mitx.mit.edu/xqa' + +INSTALLED_APPS += ('lms_migration',) + +LMS_MIGRATION_ALLOWED_IPS = ['127.0.0.1'] + +################################ OpenID Auth ################################# +MITX_FEATURES['AUTH_USE_OPENID'] = True +MITX_FEATURES['AUTH_USE_OPENID_PROVIDER'] = True +MITX_FEATURES['BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH'] = True + +INSTALLED_APPS += ('external_auth',) +INSTALLED_APPS += ('django_openid_auth',) + +OPENID_CREATE_USERS = False +OPENID_UPDATE_DETAILS_FROM_SREG = True +OPENID_SSO_SERVER_URL = 'https://www.google.com/accounts/o8/id' # TODO: accept more endpoints +OPENID_USE_AS_ADMIN_LOGIN = False + +OPENID_PROVIDER_TRUSTED_ROOTS = ['*'] + +################################ MIT Certificates SSL Auth ################################# + +MITX_FEATURES['AUTH_USE_MIT_CERTIFICATES'] = True + +################################ DEBUG TOOLBAR ################################# +INSTALLED_APPS += ('debug_toolbar',) +MIDDLEWARE_CLASSES += ('django_comment_client.utils.QueryCountDebugMiddleware', + 'debug_toolbar.middleware.DebugToolbarMiddleware',) +INTERNAL_IPS = ('127.0.0.1',) + +DEBUG_TOOLBAR_PANELS = ( + 'debug_toolbar.panels.version.VersionDebugPanel', + 'debug_toolbar.panels.timer.TimerDebugPanel', + 'debug_toolbar.panels.settings_vars.SettingsVarsDebugPanel', + 'debug_toolbar.panels.headers.HeaderDebugPanel', + 'debug_toolbar.panels.request_vars.RequestVarsDebugPanel', + 'debug_toolbar.panels.sql.SQLDebugPanel', + 'debug_toolbar.panels.signals.SignalDebugPanel', + 'debug_toolbar.panels.logger.LoggingPanel', + +# Enabling the profiler has a weird bug as of django-debug-toolbar==0.9.4 and +# Django=1.3.1/1.4 where requests to views get duplicated (your method gets +# hit twice). So you can uncomment when you need to diagnose performance +# problems, but you shouldn't leave it on. +# 'debug_toolbar.panels.profiling.ProfilingDebugPanel', +) + +DEBUG_TOOLBAR_CONFIG = { + 'INTERCEPT_REDIRECTS': False +} +############################ FILE UPLOADS (for discussion forums) ############################# +DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' +MEDIA_ROOT = ENV_ROOT / "uploads" +MEDIA_URL = "/static/uploads/" +STATICFILES_DIRS.append(("uploads", MEDIA_ROOT)) +FILE_UPLOAD_TEMP_DIR = ENV_ROOT / "uploads" +FILE_UPLOAD_HANDLERS = ( + 'django.core.files.uploadhandler.MemoryFileUploadHandler', + 'django.core.files.uploadhandler.TemporaryFileUploadHandler', +) + +########################### PIPELINE ################################# + +PIPELINE_SASS_ARGUMENTS = '--debug-info --require {proj_dir}/static/sass/bourbon/lib/bourbon.rb'.format(proj_dir=PROJECT_ROOT) + +########################## PEARSON TESTING ########################### +MITX_FEATURES['ENABLE_PEARSON_LOGIN'] = False + +########################## ANALYTICS TESTING ######################## + +ANALYTICS_SERVER_URL = "http://127.0.0.1:9000/" +ANALYTICS_API_KEY = "" diff --git a/lms/envs/common.py b/lms/envs/common.py index e6d761c070..c6091fa382 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -85,6 +85,13 @@ MITX_FEATURES = { # analytics experiments 'ENABLE_INSTRUCTOR_ANALYTICS': False, + # enable analytics server. + # WARNING: THIS SHOULD ALWAYS BE SET TO FALSE UNDER NORMAL + # LMS OPERATION. See analytics.py for details about what + # this does. + + 'RUN_AS_ANALYTICS_SERVER_ENABLED' : False, + # Flip to True when the YouTube iframe API breaks (again) 'USE_YOUTUBE_OBJECT_API': False, diff --git a/lms/urls.py b/lms/urls.py index b00813a40d..8fffa80586 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -353,6 +353,12 @@ if settings.MITX_FEATURES.get('ENABLE_SQL_TRACKING_LOGS'): url(r'^event_logs/(?P.+)$', 'track.views.view_tracking_log'), ) +if settings.MITX_FEATURES.get('RUN_AS_ANALYTICS_SERVER_ENABLED'): + urlpatterns += ( + url('^', include('djanalytics.core.urls')), + ) + import djanalytics.core.registry + # FoldIt views urlpatterns += ( # The path is hardcoded into their app... @@ -367,3 +373,5 @@ if settings.DEBUG: #Custom error pages handler404 = 'static_template_view.views.render_404' handler500 = 'static_template_view.views.render_500' + + From 21e0d4fe61ae535164b782fca4f5e330e209faae Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Sat, 11 May 2013 21:19:14 -0400 Subject: [PATCH 002/375] Fixed whitespace bug --- lms/envs/analyticsserver.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lms/envs/analyticsserver.py b/lms/envs/analyticsserver.py index e91e935d95..f6a05392d1 100644 --- a/lms/envs/analyticsserver.py +++ b/lms/envs/analyticsserver.py @@ -18,6 +18,11 @@ import json ROOT_URLCONF = 'lms.urls' +import sys +#sys.path.append("/home/pmitros/mitx_all/mitx") +#sys.path.append("/home/pmitros/mitx_all/mitx/edxdataanalytic") +#print ">>>>>>>>>>>", [x for x in sys.path if "mitx_all/mitx" in str(x)] + from .common import * from logsettings import get_logger_config @@ -43,7 +48,7 @@ INSTALLED_APPS = INSTALLED_APPS + ( 'djeventstream.httphandler', ) INSTALLED_ANALYTICS_MODULES = open("../analytics_modules.txt").readlines() -INSTALLED_ANALYTICS_MODULES = [x for x in INSTALLED_ANALYTICS_MODULES if x and len(x)>1] +INSTALLED_ANALYTICS_MODULES = [x.strip() for x in INSTALLED_ANALYTICS_MODULES if x and len(x)>1] DJFS = { 'type' : 'osfs', 'directory_root' : '/tmp/djfsmodule', From 0cc763bc126deffe96b4e35c0a5cee596af6c141 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Sun, 12 May 2013 08:44:48 -0400 Subject: [PATCH 003/375] Removed debug code --- lms/envs/analyticsserver.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/lms/envs/analyticsserver.py b/lms/envs/analyticsserver.py index f6a05392d1..2ec5bf9e3a 100644 --- a/lms/envs/analyticsserver.py +++ b/lms/envs/analyticsserver.py @@ -19,9 +19,6 @@ import json ROOT_URLCONF = 'lms.urls' import sys -#sys.path.append("/home/pmitros/mitx_all/mitx") -#sys.path.append("/home/pmitros/mitx_all/mitx/edxdataanalytic") -#print ">>>>>>>>>>>", [x for x in sys.path if "mitx_all/mitx" in str(x)] from .common import * from logsettings import get_logger_config From 622b53d9526c2e0cd981c3bf7f591a034ae78a58 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Sun, 12 May 2013 08:47:33 -0400 Subject: [PATCH 004/375] Finished docstring --- lms/envs/analyticsserver.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lms/envs/analyticsserver.py b/lms/envs/analyticsserver.py index 2ec5bf9e3a..54b4993232 100644 --- a/lms/envs/analyticsserver.py +++ b/lms/envs/analyticsserver.py @@ -4,7 +4,10 @@ This config file is used to host an analytics server. The edX codebase is fairly monolithic, and expensive to import from within the analytics framework. It also mixes up Django authentication databases, etc. The analytics framework is fairly modular, and easy to import -from within edX. With this configuration. +from within edX. With this configuration, we can import analytics as a +library into the core edX platform, and use it to expose data through +the standard analytics protocols. The main analytics servers can then +use this to pull data out. This should configuration should never be enabled on a production LMS. From dbcef7d7439a6ce43bc407e8b6714c834311d35f Mon Sep 17 00:00:00 2001 From: yarko Date: Fri, 31 May 2013 23:18:38 -0500 Subject: [PATCH 005/375] create-dev-env.sh: if run from repo, set $BASE appropriately 2 changes: [1] If PROJECT_HOME is not set, AND create-dev-env.sh is run from a cloned repo, then set BASE to that repo; else: (and only finally) set BASE to a default of "$HOME/edx_all" [2] if PROJECT_HOME is set, or if this is from a repo, don't add edx_all to the BASE name (no need;) --- scripts/create-dev-env.sh | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/scripts/create-dev-env.sh b/scripts/create-dev-env.sh index 520ce05b5c..a13fec3850 100755 --- a/scripts/create-dev-env.sh +++ b/scripts/create-dev-env.sh @@ -96,13 +96,27 @@ clone_repos() { fi } +set_base_default() { # if PROJECT_HOME not set + # 2 possibilities: this is from cloned repo, or not + # this script is in "./scripts" if a git clone + this_repo=$(cd "${BASH_SOURCE%/*}/.." && pwd) + if [[ "${this_repo##*/}" = "edx-platform" && -d "$this_repo/.git" ]]; then + # set BASE one-up from this_repo; + echo "${this_repo%/*}" + else + echo "$HOME/edx_all" + fi +} + + + ### START PROG=${0##*/} # Adjust this to wherever you'd like to place the codebase -BASE="${PROJECT_HOME:-$HOME}/edx_all" +BASE="${PROJECT_HOME:-$(set_base_default)}" # Use a sensible default (~/.virtualenvs) for your Python virtualenvs # unless you've already got one set up with virtualenvwrapper. From 74866a38b00c2ce5a593dc509dcb23f22febaaf0 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Wed, 29 May 2013 12:55:11 -0400 Subject: [PATCH 006/375] Move parseActions and statics out of evaluator() --- common/lib/calc/calc.py | 144 ++++++++++++++++++++-------------------- 1 file changed, 73 insertions(+), 71 deletions(-) diff --git a/common/lib/calc/calc.py b/common/lib/calc/calc.py index 2ee82e2fb4..0ab02e413b 100644 --- a/common/lib/calc/calc.py +++ b/common/lib/calc/calc.py @@ -37,16 +37,33 @@ default_variables = {'j': numpy.complex(0, 1), 'q': scipy.constants.e } + +ops = {"^": operator.pow, + "*": operator.mul, + "/": operator.truediv, + "+": operator.add, + "-": operator.sub, +} +# We eliminated extreme ones, since they're rarely used, and potentially +# confusing. They may also conflict with variables if we ever allow e.g. +# 5R instead of 5*R +suffixes = {'%': 0.01, 'k': 1e3, 'M': 1e6, 'G': 1e9, + 'T': 1e12, # 'P':1e15,'E':1e18,'Z':1e21,'Y':1e24, + 'c': 1e-2, 'm': 1e-3, 'u': 1e-6, + 'n': 1e-9, 'p': 1e-12} # ,'f':1e-15,'a':1e-18,'z':1e-21,'y':1e-24} + log = logging.getLogger("mitx.courseware.capa") class UndefinedVariable(Exception): - def raiseself(self): - ''' Helper so we can use inside of a lambda ''' - raise self + pass + # unused for now + # def raiseself(self): + # ''' Helper so we can use inside of a lambda ''' + # raise self -general_whitespace = re.compile('[^\w]+') +general_whitespace = re.compile('[^\\w]+') def check_variables(string, variables): @@ -65,13 +82,61 @@ def check_variables(string, variables): for v in possible_variables: if len(v) == 0: continue - if v[0] <= '9' and '0' <= 'v': # Skip things that begin with numbers + if v[0] <= '9' and '0' <= v: # Skip things that begin with numbers continue if v not in variables: bad_variables.append(v) if len(bad_variables) > 0: raise UndefinedVariable(' '.join(bad_variables)) +def lower_dict(d): + return dict([(k.lower(), d[k]) for k in d]) + +def super_float(text): + ''' Like float, but with si extensions. 1k goes to 1000''' + if text[-1] in suffixes: + return float(text[:-1]) * suffixes[text[-1]] + else: + return float(text) + +def number_parse_action(x): # [ '7' ] -> [ 7 ] + return [super_float("".join(x))] + +def exp_parse_action(x): # [ 2 ^ 3 ^ 2 ] -> 512 + x = [e for e in x if isinstance(e, numbers.Number)] # Ignore ^ + x.reverse() + x = reduce(lambda a, b: b ** a, x) + return x + +def parallel(x): # Parallel resistors [ 1 2 ] => 2/3 + # convert from pyparsing.ParseResults, which doesn't support '0 in x' + x = list(x) + if len(x) == 1: + return x[0] + if 0 in x: + return float('nan') + x = [1. / e for e in x if isinstance(e, numbers.Number)] # Ignore || + return 1. / sum(x) + +def sum_parse_action(x): # [ 1 + 2 - 3 ] -> 0 + total = 0.0 + op = ops['+'] + for e in x: + if e in set('+-'): + op = ops[e] + else: + total = op(total, e) + return total + +def prod_parse_action(x): # [ 1 * 2 / 3 ] => 0.66 + prod = 1.0 + op = ops['*'] + for e in x: + if e in set('*/'): + op = ops[e] + else: + prod = op(prod, e) + return prod def evaluator(variables, functions, string, cs=False): ''' @@ -86,12 +151,12 @@ def evaluator(variables, functions, string, cs=False): # log.debug("functions: {0}".format(functions)) # log.debug("string: {0}".format(string)) - def lower_dict(d): - return dict([(k.lower(), d[k]) for k in d]) - all_variables = copy.copy(default_variables) all_functions = copy.copy(default_functions) + def func_parse_action(x): + return [all_functions[x[0]](x[1])] + if not cs: all_variables = lower_dict(all_variables) all_functions = lower_dict(all_functions) @@ -113,69 +178,6 @@ def evaluator(variables, functions, string, cs=False): if string.strip() == "": return float('nan') - ops = {"^": operator.pow, - "*": operator.mul, - "/": operator.truediv, - "+": operator.add, - "-": operator.sub, - } - # We eliminated extreme ones, since they're rarely used, and potentially - # confusing. They may also conflict with variables if we ever allow e.g. - # 5R instead of 5*R - suffixes = {'%': 0.01, 'k': 1e3, 'M': 1e6, 'G': 1e9, - 'T': 1e12, # 'P':1e15,'E':1e18,'Z':1e21,'Y':1e24, - 'c': 1e-2, 'm': 1e-3, 'u': 1e-6, - 'n': 1e-9, 'p': 1e-12} # ,'f':1e-15,'a':1e-18,'z':1e-21,'y':1e-24} - - def super_float(text): - ''' Like float, but with si extensions. 1k goes to 1000''' - if text[-1] in suffixes: - return float(text[:-1]) * suffixes[text[-1]] - else: - return float(text) - - def number_parse_action(x): # [ '7' ] -> [ 7 ] - return [super_float("".join(x))] - - def exp_parse_action(x): # [ 2 ^ 3 ^ 2 ] -> 512 - x = [e for e in x if isinstance(e, numbers.Number)] # Ignore ^ - x.reverse() - x = reduce(lambda a, b: b ** a, x) - return x - - def parallel(x): # Parallel resistors [ 1 2 ] => 2/3 - # convert from pyparsing.ParseResults, which doesn't support '0 in x' - x = list(x) - if len(x) == 1: - return x[0] - if 0 in x: - return float('nan') - x = [1. / e for e in x if isinstance(e, numbers.Number)] # Ignore || - return 1. / sum(x) - - def sum_parse_action(x): # [ 1 + 2 - 3 ] -> 0 - total = 0.0 - op = ops['+'] - for e in x: - if e in set('+-'): - op = ops[e] - else: - total = op(total, e) - return total - - def prod_parse_action(x): # [ 1 * 2 / 3 ] => 0.66 - prod = 1.0 - op = ops['*'] - for e in x: - if e in set('*/'): - op = ops[e] - else: - prod = op(prod, e) - return prod - - def func_parse_action(x): - return [all_functions[x[0]](x[1])] - # SI suffixes and percent number_suffix = reduce(lambda a, b: a | b, map(Literal, suffixes.keys()), NoMatch()) (dot, minus, plus, times, div, lpar, rpar, exp) = map(Literal, ".-+*/()^") From ed45c505a39cf3a8aa094ee6c64591da1c604773 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Wed, 29 May 2013 12:55:51 -0400 Subject: [PATCH 007/375] Simpler pyparsing --- common/lib/calc/calc.py | 48 +++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/common/lib/calc/calc.py b/common/lib/calc/calc.py index 0ab02e413b..64053d6ca5 100644 --- a/common/lib/calc/calc.py +++ b/common/lib/calc/calc.py @@ -8,11 +8,11 @@ import numpy import numbers import scipy.constants -from pyparsing import Word, alphas, nums, oneOf, Literal -from pyparsing import ZeroOrMore, OneOrMore, StringStart -from pyparsing import StringEnd, Optional, Forward -from pyparsing import CaselessLiteral, Group, StringEnd -from pyparsing import NoMatch, stringEnd, alphanums +from pyparsing import Word, nums, Literal +from pyparsing import ZeroOrMore, MatchFirst +from pyparsing import Optional, Forward +from pyparsing import CaselessLiteral +from pyparsing import NoMatch, stringEnd, Suppress, Combine default_functions = {'sin': numpy.sin, 'cos': numpy.cos, @@ -179,17 +179,19 @@ def evaluator(variables, functions, string, cs=False): return float('nan') # SI suffixes and percent - number_suffix = reduce(lambda a, b: a | b, map(Literal, suffixes.keys()), NoMatch()) - (dot, minus, plus, times, div, lpar, rpar, exp) = map(Literal, ".-+*/()^") + number_suffix = MatchFirst([Literal(k) for k in suffixes.keys()]) + plus_minus = Literal('+') | Literal('-') + times_div = Literal('*') | Literal('/') number_part = Word(nums) # 0.33 or 7 or .34 or 16. inner_number = (number_part + Optional("." + Optional(number_part))) | ("." + number_part) + inner_number = Combine(inner_number) # by default pyparsing allows spaces between tokens--this prevents that # 0.33k or -17 - number = (Optional(minus | plus) + inner_number - + Optional(CaselessLiteral("E") + Optional((plus | minus)) + number_part) + number = (inner_number + + Optional(CaselessLiteral("E") + Optional(plus_minus) + number_part) + Optional(number_suffix)) number = number.setParseAction(number_parse_action) # Convert to number @@ -197,40 +199,34 @@ def evaluator(variables, functions, string, cs=False): expr = Forward() factor = Forward() - def sreduce(f, l): - ''' Same as reduce, but handle len 1 and len 0 lists sensibly ''' - if len(l) == 0: - return NoMatch() - if len(l) == 1: - return l[0] - return reduce(f, l) - # Handle variables passed in. E.g. if we have {'R':0.5}, we make the substitution. # Special case for no variables because of how we understand PyParsing is put together if len(all_variables) > 0: # We sort the list so that var names (like "e2") match before # mathematical constants (like "e"). This is kind of a hack. all_variables_keys = sorted(all_variables.keys(), key=len, reverse=True) - varnames = sreduce(lambda x, y: x | y, map(lambda x: CasedLiteral(x), all_variables_keys)) - varnames.setParseAction(lambda x: map(lambda y: all_variables[y], x)) + literal_all_vars = [CasedLiteral(k) for k in all_variables_keys] + varnames = MatchFirst(literal_all_vars) + varnames.setParseAction(lambda x: [all_variables[k] for k in x]) else: varnames = NoMatch() # Same thing for functions. if len(all_functions) > 0: - funcnames = sreduce(lambda x, y: x | y, - map(lambda x: CasedLiteral(x), all_functions.keys())) - function = funcnames + lpar.suppress() + expr + rpar.suppress() + funcnames = MatchFirst([CasedLiteral(k) for k in all_functions.keys()]) + function = funcnames + Suppress("(") + expr + Suppress(")") function.setParseAction(func_parse_action) else: function = NoMatch() - atom = number | function | varnames | lpar + expr + rpar - factor << (atom + ZeroOrMore(exp + atom)).setParseAction(exp_parse_action) # 7^6 + atom = number | function | varnames | Suppress("(") + expr + Suppress(")") + + # Do the following in the correct order to preserve order of operation + factor << (atom + ZeroOrMore("^" + atom)).setParseAction(exp_parse_action) # 7^6 paritem = factor + ZeroOrMore(Literal('||') + factor) # 5k || 4k paritem = paritem.setParseAction(parallel) - term = paritem + ZeroOrMore((times | div) + paritem) # 7 * 5 / 4 - 3 + term = paritem + ZeroOrMore(times_div + paritem) # 7 * 5 / 4 - 3 term = term.setParseAction(prod_parse_action) - expr << Optional((plus | minus)) + term + ZeroOrMore((plus | minus) + term) # -5 + 4 - 3 + expr << Optional(plus_minus) + term + ZeroOrMore(plus_minus + term) # -5 + 4 - 3 expr = expr.setParseAction(sum_parse_action) return (expr + stringEnd).parseString(string)[0] From 72d149caae1c5cd3909b59e850d94cb8ffc95c59 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Wed, 29 May 2013 13:25:48 -0400 Subject: [PATCH 008/375] Add docstrings and comments --- common/lib/calc/calc.py | 81 +++++++++++++++++++++++---- common/lib/capa/capa/responsetypes.py | 1 + 2 files changed, 71 insertions(+), 11 deletions(-) diff --git a/common/lib/calc/calc.py b/common/lib/calc/calc.py index 64053d6ca5..5d0aeb3fd1 100644 --- a/common/lib/calc/calc.py +++ b/common/lib/calc/calc.py @@ -1,3 +1,9 @@ +""" +Parser and evaluator for FormulaResponse and NumericalResponse + +Uses pyparsing to parse. Main function as of now is evaluator(). +""" + import copy import logging import math @@ -56,6 +62,10 @@ log = logging.getLogger("mitx.courseware.capa") class UndefinedVariable(Exception): + """ + Used to indicate the student input of a variable, which was unused by the + instructor. + """ pass # unused for now # def raiseself(self): @@ -67,7 +77,8 @@ general_whitespace = re.compile('[^\\w]+') def check_variables(string, variables): - '''Confirm the only variables in string are defined. + """ + Confirm the only variables in string are defined. Pyparsing uses a left-to-right parser, which makes the more elegant approach pretty hopeless. @@ -76,7 +87,7 @@ def check_variables(string, variables): undefined_variable = achar + Word(alphanums) undefined_variable.setParseAction(lambda x:UndefinedVariable("".join(x)).raiseself()) varnames = varnames | undefined_variable - ''' + """ possible_variables = re.split(general_whitespace, string) # List of all alnums in string bad_variables = list() for v in possible_variables: @@ -90,26 +101,59 @@ def check_variables(string, variables): raise UndefinedVariable(' '.join(bad_variables)) def lower_dict(d): + """ + takes each key in the dict and makes it lowercase, still mapping to the + same value. + + keep in mind that it is possible (but not useful?) to define different + variables that have the same lowercase representation. It would be hard to + tell which is used in the final dict and which isn't. + """ return dict([(k.lower(), d[k]) for k in d]) +# The following few functions define parse actions, which are run on lists of +# results from each parse component. They convert the strings and (previously +# calculated) numbers into the number that component represents. + def super_float(text): - ''' Like float, but with si extensions. 1k goes to 1000''' + """ + Like float, but with si extensions. 1k goes to 1000 + """ if text[-1] in suffixes: return float(text[:-1]) * suffixes[text[-1]] else: return float(text) -def number_parse_action(x): # [ '7' ] -> [ 7 ] +def number_parse_action(x): + """ + Create a float out of its string parts + + e.g. [ '7', '.', '13' ] -> [ 7.13 ] + Calls super_float above + """ return [super_float("".join(x))] -def exp_parse_action(x): # [ 2 ^ 3 ^ 2 ] -> 512 +def exp_parse_action(x): + """ + Take a list of numbers and exponentiate them, right to left + + e.g. [ 3, 2, 3 ] (which is 3^2^3 = 3^(2^3)) -> 6561 + """ x = [e for e in x if isinstance(e, numbers.Number)] # Ignore ^ x.reverse() x = reduce(lambda a, b: b ** a, x) return x -def parallel(x): # Parallel resistors [ 1 2 ] => 2/3 - # convert from pyparsing.ParseResults, which doesn't support '0 in x' +def parallel(x): + """ + Compute numbers according to the parallel resistors operator + + BTW it is commutative. Its formula is given by + out = 1 / (1/in1 + 1/in2 + ...) + e.g. [ 1, 2 ] => 2/3 + + Return NaN if there is a zero among the inputs + """ x = list(x) if len(x) == 1: return x[0] @@ -119,6 +163,13 @@ def parallel(x): # Parallel resistors [ 1 2 ] => 2/3 return 1. / sum(x) def sum_parse_action(x): # [ 1 + 2 - 3 ] -> 0 + """ + Add the inputs + + [ 1, '+', 2, '-', 3 ] -> 0 + + Allow a leading + or - + """ total = 0.0 op = ops['+'] for e in x: @@ -129,6 +180,11 @@ def sum_parse_action(x): # [ 1 + 2 - 3 ] -> 0 return total def prod_parse_action(x): # [ 1 * 2 / 3 ] => 0.66 + """ + Multiply the inputs + + [ 1, '*', 2, '/', 3 ] => 0.66 + """ prod = 1.0 op = ops['*'] for e in x: @@ -139,14 +195,13 @@ def prod_parse_action(x): # [ 1 * 2 / 3 ] => 0.66 return prod def evaluator(variables, functions, string, cs=False): - ''' + """ Evaluate an expression. Variables are passed as a dictionary from string to value. Unary functions are passed as a dictionary from string to function. Variables must be floats. cs: Case sensitive - TODO: Fix it so we can pass integers and complex numbers in variables dict - ''' + """ # log.debug("variables: {0}".format(variables)) # log.debug("functions: {0}".format(functions)) # log.debug("string: {0}".format(string)) @@ -187,7 +242,8 @@ def evaluator(variables, functions, string, cs=False): # 0.33 or 7 or .34 or 16. inner_number = (number_part + Optional("." + Optional(number_part))) | ("." + number_part) - inner_number = Combine(inner_number) # by default pyparsing allows spaces between tokens--this prevents that + # by default pyparsing allows spaces between tokens--Combine prevents that + inner_number = Combine(inner_number) # 0.33k or -17 number = (inner_number @@ -209,6 +265,8 @@ def evaluator(variables, functions, string, cs=False): varnames = MatchFirst(literal_all_vars) varnames.setParseAction(lambda x: [all_variables[k] for k in x]) else: + # all_variables includes DEFAULT_VARIABLES, which isn't empty + # this is unreachable. Get rid of it? varnames = NoMatch() # Same thing for functions. @@ -217,6 +275,7 @@ def evaluator(variables, functions, string, cs=False): function = funcnames + Suppress("(") + expr + Suppress(")") function.setParseAction(func_parse_action) else: + # see note above (this is unreachable) function = NoMatch() atom = number | function | varnames | Suppress("(") + expr + Suppress(")") diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 0fa50079de..314d01e7e8 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1717,6 +1717,7 @@ class FormulaResponse(LoncapaResponse): student_variables = dict() # ranges give numerical ranges for testing for var in ranges: + # TODO: allow specified ranges (i.e. integers and complex numbers) for random variables value = random.uniform(*ranges[var]) instructor_variables[str(var)] = value student_variables[str(var)] = value From 862bb3f8bc34ff14618d92f91c5cbb9dbf458928 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Tue, 4 Jun 2013 11:34:52 -0400 Subject: [PATCH 009/375] Added the beginnings of the navigation tests I still need to refactor the methods but at this point, all tests work --- .../courseware/features/navigation.feature | 27 ++ .../courseware/features/navigation.py | 242 ++++++++++++++++++ 2 files changed, 269 insertions(+) create mode 100644 lms/djangoapps/courseware/features/navigation.feature create mode 100644 lms/djangoapps/courseware/features/navigation.py diff --git a/lms/djangoapps/courseware/features/navigation.feature b/lms/djangoapps/courseware/features/navigation.feature new file mode 100644 index 0000000000..f9cee87c89 --- /dev/null +++ b/lms/djangoapps/courseware/features/navigation.feature @@ -0,0 +1,27 @@ +Feature: Navigate Course + As a student in an edX course + In order to view the course properly + I want to be able to navigate through the content + + Scenario: I can navigate to a section + Given I am viewing a course with multiple sections + When I click on section "2" + Then I see the content of section "2" + + + Scenario: I can navigate to subsections + Given I am viewing a section with multiple subsections + When I click on subsection "2" + Then I see the content of subsection "2" + + Scenario: I can navigate to sequences + Given I am viewing a section with multiple sequences + When I click on sequence "2" + Then I see the content of sequence "2" + + Scenario: I can go back to where I was after I log out and back in + Given I am viewing a course with multiple sections + When I click on section "2" + And I visit the homepage + And I go to the section + Then I should see "You were most recently in Test Section2" somewhere on the page diff --git a/lms/djangoapps/courseware/features/navigation.py b/lms/djangoapps/courseware/features/navigation.py new file mode 100644 index 0000000000..2f7f19f39a --- /dev/null +++ b/lms/djangoapps/courseware/features/navigation.py @@ -0,0 +1,242 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + +from lettuce import world, step +from django.contrib.auth.models import User +from lettuce.django import django_url +from student.models import CourseEnrollment +from common import course_id +from xmodule.modulestore import Location +from problems_setup import PROBLEM_DICT + +TEST_COURSE_ORG = 'edx' +TEST_COURSE_NAME = 'Test Course' +TEST_SECTION_NAME = 'Test Section' +SUBSECTION_2_LOC = None + + +@step(u'I am viewing a course with multiple sections') +def view_course_multiple_sections(step): + # First clear the modulestore so we don't try to recreate + # the same course twice + # This also ensures that the necessary templates are loaded + world.clear_courses() + + # Create the course + # We always use the same org and display name, + # but vary the course identifier (e.g. 600x or 191x) + course = world.CourseFactory.create(org=TEST_COURSE_ORG, + number="model_course", + display_name=TEST_COURSE_NAME) + + # Add a section to the course to contain problems + section1 = world.ItemFactory.create(parent_location=course.location, + display_name=TEST_SECTION_NAME+"1") + + # Add a section to the course to contain problems + section2 = world.ItemFactory.create(parent_location=course.location, + display_name=TEST_SECTION_NAME+"2") + + world.ItemFactory.create(parent_location=section1.location, + template='i4x://edx/templates/sequential/Empty', + display_name=TEST_SECTION_NAME+"1") + + world.ItemFactory.create(parent_location=section2.location, + template='i4x://edx/templates/sequential/Empty', + display_name=TEST_SECTION_NAME+"2") + + add_problem_to_course_section('model_course', 'multiple choice', section=1) + add_problem_to_course_section('model_course', 'drop down', section=2) + + # Create the user + world.create_user('robot') + u = User.objects.get(username='robot') + + # If the user is not already enrolled, enroll the user. + # TODO: change to factory + CourseEnrollment.objects.get_or_create(user=u, course_id=course_id("model_course")) + + world.log_in('robot', 'test') + chapter_name = (TEST_SECTION_NAME+"1").replace(" ", "_") + section_name = chapter_name + url = django_url('/courses/edx/model_course/Test_Course/courseware/%s/%s' % + (chapter_name, section_name)) + + world.browser.visit(url) + + +@step(u'I am viewing a section with multiple subsections') +def view_course_multiple_subsections(step): + # First clear the modulestore so we don't try to recreate + # the same course twice + # This also ensures that the necessary templates are loaded + world.clear_courses() + + # Create the course + # We always use the same org and display name, + # but vary the course identifier (e.g. 600x or 191x) + course = world.CourseFactory.create(org=TEST_COURSE_ORG, + number="model_course", + display_name=TEST_COURSE_NAME) + + # Add a section to the course to contain problems + section1 = world.ItemFactory.create(parent_location=course.location, + display_name=TEST_SECTION_NAME+"1") + + world.ItemFactory.create(parent_location=section1.location, + template='i4x://edx/templates/sequential/Empty', + display_name=TEST_SECTION_NAME+"1") + + section2 = world.ItemFactory.create(parent_location=section1.location, + display_name=TEST_SECTION_NAME+"2") + + global SUBSECTION_2_LOC + SUBSECTION_2_LOC = section2.location + + + add_problem_to_course_section('model_course', 'multiple choice', section=1) + add_problem_to_course_section('model_course', 'drop down', section=1, subsection=2) + + # Create the user + world.create_user('robot') + u = User.objects.get(username='robot') + + # If the user is not already enrolled, enroll the user. + # TODO: change to factory + CourseEnrollment.objects.get_or_create(user=u, course_id=course_id("model_course")) + + world.log_in('robot', 'test') + chapter_name = (TEST_SECTION_NAME+"1").replace(" ", "_") + section_name = chapter_name + url = django_url('/courses/edx/model_course/Test_Course/courseware/%s/%s' % + (chapter_name, section_name)) + + world.browser.visit(url) + + +@step(u'I am viewing a section with multiple sequences') +def view_course_multiple_sequences(step): + # First clear the modulestore so we don't try to recreate + # the same course twice + # This also ensures that the necessary templates are loaded + world.clear_courses() + + # Create the course + # We always use the same org and display name, + # but vary the course identifier (e.g. 600x or 191x) + course = world.CourseFactory.create(org=TEST_COURSE_ORG, + number="model_course", + display_name=TEST_COURSE_NAME) + + # Add a section to the course to contain problems + section1 = world.ItemFactory.create(parent_location=course.location, + display_name=TEST_SECTION_NAME+"1") + + + world.ItemFactory.create(parent_location=section1.location, + template='i4x://edx/templates/sequential/Empty', + display_name=TEST_SECTION_NAME+"1") + + add_problem_to_course_section('model_course', 'multiple choice', section=1) + add_problem_to_course_section('model_course', 'drop down', section=1) + + # Create the user + world.create_user('robot') + u = User.objects.get(username='robot') + + # If the user is not already enrolled, enroll the user. + # TODO: change to factory + CourseEnrollment.objects.get_or_create(user=u, course_id=course_id("model_course")) + + world.log_in('robot', 'test') + chapter_name = (TEST_SECTION_NAME+"1").replace(" ", "_") + section_name = chapter_name + url = django_url('/courses/edx/model_course/Test_Course/courseware/%s/%s' % + (chapter_name, section_name)) + + world.browser.visit(url) + + +@step(u'I click on section "([^"]*)"') +def click_on_section(step, section): + section_css = 'h3[tabindex="-1"]' + elist = world.css_find(section_css) + assert not elist.is_empty() + elist.click() + subid = "ui-accordion-accordion-panel-"+str(int(section)-1) + subsection_css = 'ul[id="%s"]>li[class=" "] a' % subid + elist = world.css_find(subsection_css) + assert not elist.is_empty() + elist.click() + + +@step(u'I click on subsection "([^"]*)"') +def click_on_subsection(step, subsection): + subsection_css = 'ul[id="ui-accordion-accordion-panel-0"]>li[class=" "] a' + elist = world.css_find(subsection_css) + assert not elist.is_empty() + elist.click() + +@step(u'I click on sequence "([^"]*)"') +def click_on_subsection(step, sequence): + sequence_css = 'a[data-element="%s"]' % sequence + elist = world.css_find(sequence_css) + assert not elist.is_empty() + elist.click() + + +@step(u'I see the content of (?:sub)?section "([^"]*)"') +def see_section_content(step, section): + if section == "2": + text = 'The correct answer is Option 2' + elif section == "1": + text = 'The correct answer is Choice 3' + step.given('I should see "' + text + '" somewhere on the page') + + +@step(u'I see the content of sequence "([^"]*)"') +def see_sequence_content(step, sequence): + step.given('I see the content of section "2"') + + +@step(u'I go to the section') +def return_to_course(step): + world.click_link("View Course") + world.click_link("Courseware") + +### +#HELPERS +### + + +def add_problem_to_course_section(course, problem_type, extraMeta=None, section=1, subsection=1): + ''' + Add a problem to the course we have created using factories. + ''' + + assert(problem_type in PROBLEM_DICT) + + # Generate the problem XML using capa.tests.response_xml_factory + factory_dict = PROBLEM_DICT[problem_type] + problem_xml = factory_dict['factory'].build_xml(**factory_dict['kwargs']) + metadata = {'rerandomize': 'always'} if not 'metadata' in factory_dict else factory_dict['metadata'] + if extraMeta: + metadata = dict(metadata, **extraMeta) + + # Create a problem item using our generated XML + # We set rerandomize=always in the metadata so that the "Reset" button + # will appear. + template_name = "i4x://edx/templates/problem/Blank_Common_Problem" + world.ItemFactory.create(parent_location=section_location(course, section) if subsection == 1 else SUBSECTION_2_LOC, + template=template_name, + display_name=str(problem_type), + data=problem_xml, + metadata=metadata) + + +def section_location(course_num, section_num): + return Location(loc_or_tag="i4x", + org=TEST_COURSE_ORG, + course=course_num, + category='sequential', + name=(TEST_SECTION_NAME+str(section_num)).replace(" ", "_")) From c62cc23bc23967307b86f7f4ae5d060db35cbe3d Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Tue, 4 Jun 2013 13:06:18 -0400 Subject: [PATCH 010/375] Refactored Navigation Methods --- .../courseware/features/navigation.feature | 9 +- .../courseware/features/navigation.py | 196 +++++++----------- 2 files changed, 75 insertions(+), 130 deletions(-) diff --git a/lms/djangoapps/courseware/features/navigation.feature b/lms/djangoapps/courseware/features/navigation.feature index f9cee87c89..182a8ad4a9 100644 --- a/lms/djangoapps/courseware/features/navigation.feature +++ b/lms/djangoapps/courseware/features/navigation.feature @@ -6,22 +6,21 @@ Feature: Navigate Course Scenario: I can navigate to a section Given I am viewing a course with multiple sections When I click on section "2" - Then I see the content of section "2" + Then I should see the content of section "2" Scenario: I can navigate to subsections Given I am viewing a section with multiple subsections When I click on subsection "2" - Then I see the content of subsection "2" + Then I should see the content of subsection "2" Scenario: I can navigate to sequences Given I am viewing a section with multiple sequences When I click on sequence "2" - Then I see the content of sequence "2" + Then I should see the content of sequence "2" Scenario: I can go back to where I was after I log out and back in Given I am viewing a course with multiple sections When I click on section "2" - And I visit the homepage - And I go to the section + And I return later Then I should see "You were most recently in Test Section2" somewhere on the page diff --git a/lms/djangoapps/courseware/features/navigation.py b/lms/djangoapps/courseware/features/navigation.py index 2f7f19f39a..06271a3002 100644 --- a/lms/djangoapps/courseware/features/navigation.py +++ b/lms/djangoapps/courseware/features/navigation.py @@ -13,28 +13,18 @@ TEST_COURSE_ORG = 'edx' TEST_COURSE_NAME = 'Test Course' TEST_SECTION_NAME = 'Test Section' SUBSECTION_2_LOC = None +COURSE_LOC = None @step(u'I am viewing a course with multiple sections') def view_course_multiple_sections(step): - # First clear the modulestore so we don't try to recreate - # the same course twice - # This also ensures that the necessary templates are loaded - world.clear_courses() - - # Create the course - # We always use the same org and display name, - # but vary the course identifier (e.g. 600x or 191x) - course = world.CourseFactory.create(org=TEST_COURSE_ORG, - number="model_course", - display_name=TEST_COURSE_NAME) - + create_course() # Add a section to the course to contain problems - section1 = world.ItemFactory.create(parent_location=course.location, + section1 = world.ItemFactory.create(parent_location=COURSE_LOC, display_name=TEST_SECTION_NAME+"1") # Add a section to the course to contain problems - section2 = world.ItemFactory.create(parent_location=course.location, + section2 = world.ItemFactory.create(parent_location=COURSE_LOC, display_name=TEST_SECTION_NAME+"2") world.ItemFactory.create(parent_location=section1.location, @@ -48,39 +38,15 @@ def view_course_multiple_sections(step): add_problem_to_course_section('model_course', 'multiple choice', section=1) add_problem_to_course_section('model_course', 'drop down', section=2) - # Create the user - world.create_user('robot') - u = User.objects.get(username='robot') - - # If the user is not already enrolled, enroll the user. - # TODO: change to factory - CourseEnrollment.objects.get_or_create(user=u, course_id=course_id("model_course")) - - world.log_in('robot', 'test') - chapter_name = (TEST_SECTION_NAME+"1").replace(" ", "_") - section_name = chapter_name - url = django_url('/courses/edx/model_course/Test_Course/courseware/%s/%s' % - (chapter_name, section_name)) - - world.browser.visit(url) + create_user_and_visit_course() @step(u'I am viewing a section with multiple subsections') def view_course_multiple_subsections(step): - # First clear the modulestore so we don't try to recreate - # the same course twice - # This also ensures that the necessary templates are loaded - world.clear_courses() - - # Create the course - # We always use the same org and display name, - # but vary the course identifier (e.g. 600x or 191x) - course = world.CourseFactory.create(org=TEST_COURSE_ORG, - number="model_course", - display_name=TEST_COURSE_NAME) + create_course() # Add a section to the course to contain problems - section1 = world.ItemFactory.create(parent_location=course.location, + section1 = world.ItemFactory.create(parent_location=COURSE_LOC, display_name=TEST_SECTION_NAME+"1") world.ItemFactory.create(parent_location=section1.location, @@ -93,43 +59,17 @@ def view_course_multiple_subsections(step): global SUBSECTION_2_LOC SUBSECTION_2_LOC = section2.location - add_problem_to_course_section('model_course', 'multiple choice', section=1) add_problem_to_course_section('model_course', 'drop down', section=1, subsection=2) - # Create the user - world.create_user('robot') - u = User.objects.get(username='robot') - - # If the user is not already enrolled, enroll the user. - # TODO: change to factory - CourseEnrollment.objects.get_or_create(user=u, course_id=course_id("model_course")) - - world.log_in('robot', 'test') - chapter_name = (TEST_SECTION_NAME+"1").replace(" ", "_") - section_name = chapter_name - url = django_url('/courses/edx/model_course/Test_Course/courseware/%s/%s' % - (chapter_name, section_name)) - - world.browser.visit(url) + create_user_and_visit_course() @step(u'I am viewing a section with multiple sequences') def view_course_multiple_sequences(step): - # First clear the modulestore so we don't try to recreate - # the same course twice - # This also ensures that the necessary templates are loaded - world.clear_courses() - - # Create the course - # We always use the same org and display name, - # but vary the course identifier (e.g. 600x or 191x) - course = world.CourseFactory.create(org=TEST_COURSE_ORG, - number="model_course", - display_name=TEST_COURSE_NAME) - + create_course() # Add a section to the course to contain problems - section1 = world.ItemFactory.create(parent_location=course.location, + section1 = world.ItemFactory.create(parent_location=COURSE_LOC, display_name=TEST_SECTION_NAME+"1") @@ -140,12 +80,70 @@ def view_course_multiple_sequences(step): add_problem_to_course_section('model_course', 'multiple choice', section=1) add_problem_to_course_section('model_course', 'drop down', section=1) - # Create the user + create_user_and_visit_course() + + +@step(u'I click on section "([^"]*)"') +def click_on_section(step, section): + section_css = 'h3[tabindex="-1"]' + world.css_click(section_css) + + subid = "ui-accordion-accordion-panel-"+str(int(section)-1) + subsection_css = 'ul[id="%s"]>li[class=" "] a' % subid + world.css_click(subsection_css) + + +@step(u'I click on subsection "([^"]*)"') +def click_on_subsection(step, subsection): + subsection_css = 'ul[id="ui-accordion-accordion-panel-0"]>li[class=" "]>a' + world.css_click(subsection_css) + + +@step(u'I click on sequence "([^"]*)"') +def click_on_sequence(step, sequence): + sequence_css = 'a[data-element="%s"]' % sequence + world.css_click(sequence_css) + + +@step(u'I should see the content of (?:sub)?section "([^"]*)"') +def see_section_content(step, section): + if section == "2": + text = 'The correct answer is Option 2' + elif section == "1": + text = 'The correct answer is Choice 3' + step.given('I should see "' + text + '" somewhere on the page') + + +@step(u'I should see the content of sequence "([^"]*)"') +def see_sequence_content(step, sequence): + step.given('I should see the content of section "2"') + + +@step(u'I return later') +def return_to_course(step): + step.given('I visit the homepage') + world.click_link("View Course") + world.click_link("Courseware") + +##################### +# HELPERS +##################### + + +def create_course(): + world.clear_courses() + + course = world.CourseFactory.create(org=TEST_COURSE_ORG, + number="model_course", + display_name=TEST_COURSE_NAME) + global COURSE_LOC + COURSE_LOC = course.location + + +def create_user_and_visit_course(): world.create_user('robot') u = User.objects.get(username='robot') - # If the user is not already enrolled, enroll the user. - # TODO: change to factory CourseEnrollment.objects.get_or_create(user=u, course_id=course_id("model_course")) world.log_in('robot', 'test') @@ -157,58 +155,6 @@ def view_course_multiple_sequences(step): world.browser.visit(url) -@step(u'I click on section "([^"]*)"') -def click_on_section(step, section): - section_css = 'h3[tabindex="-1"]' - elist = world.css_find(section_css) - assert not elist.is_empty() - elist.click() - subid = "ui-accordion-accordion-panel-"+str(int(section)-1) - subsection_css = 'ul[id="%s"]>li[class=" "] a' % subid - elist = world.css_find(subsection_css) - assert not elist.is_empty() - elist.click() - - -@step(u'I click on subsection "([^"]*)"') -def click_on_subsection(step, subsection): - subsection_css = 'ul[id="ui-accordion-accordion-panel-0"]>li[class=" "] a' - elist = world.css_find(subsection_css) - assert not elist.is_empty() - elist.click() - -@step(u'I click on sequence "([^"]*)"') -def click_on_subsection(step, sequence): - sequence_css = 'a[data-element="%s"]' % sequence - elist = world.css_find(sequence_css) - assert not elist.is_empty() - elist.click() - - -@step(u'I see the content of (?:sub)?section "([^"]*)"') -def see_section_content(step, section): - if section == "2": - text = 'The correct answer is Option 2' - elif section == "1": - text = 'The correct answer is Choice 3' - step.given('I should see "' + text + '" somewhere on the page') - - -@step(u'I see the content of sequence "([^"]*)"') -def see_sequence_content(step, sequence): - step.given('I see the content of section "2"') - - -@step(u'I go to the section') -def return_to_course(step): - world.click_link("View Course") - world.click_link("Courseware") - -### -#HELPERS -### - - def add_problem_to_course_section(course, problem_type, extraMeta=None, section=1, subsection=1): ''' Add a problem to the course we have created using factories. From a85a7f71df6c0bc889b2d5cbe40926b3663d375e Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Wed, 29 May 2013 13:34:58 -0400 Subject: [PATCH 011/375] Rename variables; get rid of OPS --- common/lib/calc/calc.py | 170 ++++++++++++++++++++-------------------- 1 file changed, 87 insertions(+), 83 deletions(-) diff --git a/common/lib/calc/calc.py b/common/lib/calc/calc.py index 5d0aeb3fd1..f862b41542 100644 --- a/common/lib/calc/calc.py +++ b/common/lib/calc/calc.py @@ -11,16 +11,15 @@ import operator import re import numpy -import numbers import scipy.constants -from pyparsing import Word, nums, Literal -from pyparsing import ZeroOrMore, MatchFirst -from pyparsing import Optional, Forward -from pyparsing import CaselessLiteral -from pyparsing import NoMatch, stringEnd, Suppress, Combine +from pyparsing import (Word, nums, Literal, + ZeroOrMore, MatchFirst, + Optional, Forward, + CaselessLiteral, + NoMatch, stringEnd, Suppress, Combine) -default_functions = {'sin': numpy.sin, +DEFAULT_FUNCTIONS = {'sin': numpy.sin, 'cos': numpy.cos, 'tan': numpy.tan, 'sqrt': numpy.sqrt, @@ -34,7 +33,7 @@ default_functions = {'sin': numpy.sin, 'fact': math.factorial, 'factorial': math.factorial } -default_variables = {'j': numpy.complex(0, 1), +DEFAULT_VARIABLES = {'j': numpy.complex(0, 1), 'e': numpy.e, 'pi': numpy.pi, 'k': scipy.constants.k, @@ -43,22 +42,15 @@ default_variables = {'j': numpy.complex(0, 1), 'q': scipy.constants.e } - -ops = {"^": operator.pow, - "*": operator.mul, - "/": operator.truediv, - "+": operator.add, - "-": operator.sub, -} # We eliminated extreme ones, since they're rarely used, and potentially # confusing. They may also conflict with variables if we ever allow e.g. # 5R instead of 5*R -suffixes = {'%': 0.01, 'k': 1e3, 'M': 1e6, 'G': 1e9, +SUFFIXES = {'%': 0.01, 'k': 1e3, 'M': 1e6, 'G': 1e9, 'T': 1e12, # 'P':1e15,'E':1e18,'Z':1e21,'Y':1e24, 'c': 1e-2, 'm': 1e-3, 'u': 1e-6, 'n': 1e-9, 'p': 1e-12} # ,'f':1e-15,'a':1e-18,'z':1e-21,'y':1e-24} -log = logging.getLogger("mitx.courseware.capa") +LOG = logging.getLogger("mitx.courseware.capa") class UndefinedVariable(Exception): @@ -73,13 +65,12 @@ class UndefinedVariable(Exception): # raise self -general_whitespace = re.compile('[^\\w]+') - - def check_variables(string, variables): """ Confirm the only variables in string are defined. + Otherwise, raise an UndefinedVariable containing all bad variables. + Pyparsing uses a left-to-right parser, which makes the more elegant approach pretty hopeless. @@ -88,19 +79,22 @@ def check_variables(string, variables): undefined_variable.setParseAction(lambda x:UndefinedVariable("".join(x)).raiseself()) varnames = varnames | undefined_variable """ - possible_variables = re.split(general_whitespace, string) # List of all alnums in string + general_whitespace = re.compile('[^\\w]+') + # List of all alnums in string + possible_variables = re.split(general_whitespace, string) bad_variables = list() - for v in possible_variables: - if len(v) == 0: + for var in possible_variables: + if len(var) == 0: continue - if v[0] <= '9' and '0' <= v: # Skip things that begin with numbers + if var[0] <= '9' and '0' <= var: # Skip things that begin with numbers continue - if v not in variables: - bad_variables.append(v) + if var not in variables: + bad_variables.append(var) if len(bad_variables) > 0: raise UndefinedVariable(' '.join(bad_variables)) -def lower_dict(d): + +def lower_dict(input_dict): """ takes each key in the dict and makes it lowercase, still mapping to the same value. @@ -109,7 +103,8 @@ def lower_dict(d): variables that have the same lowercase representation. It would be hard to tell which is used in the final dict and which isn't. """ - return dict([(k.lower(), d[k]) for k in d]) + return dict([(k.lower(), input_dict[k]) for k in input_dict]) + # The following few functions define parse actions, which are run on lists of # results from each parse component. They convert the strings and (previously @@ -119,32 +114,37 @@ def super_float(text): """ Like float, but with si extensions. 1k goes to 1000 """ - if text[-1] in suffixes: - return float(text[:-1]) * suffixes[text[-1]] + if text[-1] in SUFFIXES: + return float(text[:-1]) * SUFFIXES[text[-1]] else: return float(text) -def number_parse_action(x): + +def number_parse_action(parse_result): """ Create a float out of its string parts e.g. [ '7', '.', '13' ] -> [ 7.13 ] Calls super_float above """ - return [super_float("".join(x))] + return super_float("".join(parse_result)) -def exp_parse_action(x): + +def exp_parse_action(parse_result): """ Take a list of numbers and exponentiate them, right to left e.g. [ 3, 2, 3 ] (which is 3^2^3 = 3^(2^3)) -> 6561 """ - x = [e for e in x if isinstance(e, numbers.Number)] # Ignore ^ - x.reverse() - x = reduce(lambda a, b: b ** a, x) - return x + # pyparsing.ParseResults doesn't play well with reverse() + parse_result = parse_result.asList() + parse_result.reverse() + # the result of an exponentiation is called a power + power = reduce(lambda a, b: b ** a, parse_result) + return power -def parallel(x): + +def parallel(parse_result): """ Compute numbers according to the parallel resistors operator @@ -154,15 +154,17 @@ def parallel(x): Return NaN if there is a zero among the inputs """ - x = list(x) - if len(x) == 1: - return x[0] - if 0 in x: + # convert from pyparsing.ParseResults, which doesn't support '0 in parse_result' + parse_result = parse_result.asList() + if len(parse_result) == 1: + return parse_result[0] + if 0 in parse_result: return float('nan') - x = [1. / e for e in x if isinstance(e, numbers.Number)] # Ignore || - return 1. / sum(x) + reciprocals = [1. / e for e in parse_result] + return 1. / sum(reciprocals) -def sum_parse_action(x): # [ 1 + 2 - 3 ] -> 0 + +def sum_parse_action(parse_result): """ Add the inputs @@ -171,29 +173,35 @@ def sum_parse_action(x): # [ 1 + 2 - 3 ] -> 0 Allow a leading + or - """ total = 0.0 - op = ops['+'] - for e in x: - if e in set('+-'): - op = ops[e] + current_op = operator.add + for token in parse_result: + if token is '+': + current_op = operator.add + elif token is '-': + current_op = operator.sub else: - total = op(total, e) + total = current_op(total, token) return total -def prod_parse_action(x): # [ 1 * 2 / 3 ] => 0.66 + +def prod_parse_action(parse_result): """ Multiply the inputs [ 1, '*', 2, '/', 3 ] => 0.66 """ prod = 1.0 - op = ops['*'] - for e in x: - if e in set('*/'): - op = ops[e] + current_op = operator.mul + for token in parse_result: + if token is '*': + current_op = operator.mul + elif token is '/': + current_op = operator.truediv else: - prod = op(prod, e) + prod = current_op(prod, token) return prod + def evaluator(variables, functions, string, cs=False): """ Evaluate an expression. Variables are passed as a dictionary @@ -202,20 +210,12 @@ def evaluator(variables, functions, string, cs=False): cs: Case sensitive """ - # log.debug("variables: {0}".format(variables)) - # log.debug("functions: {0}".format(functions)) - # log.debug("string: {0}".format(string)) - - all_variables = copy.copy(default_variables) - all_functions = copy.copy(default_functions) - - def func_parse_action(x): - return [all_functions[x[0]](x[1])] - - if not cs: - all_variables = lower_dict(all_variables) - all_functions = lower_dict(all_functions) + # LOG.debug("variables: {0}".format(variables)) + # LOG.debug("functions: {0}".format(functions)) + # LOG.debug("string: {0}".format(string)) + all_variables = copy.copy(DEFAULT_VARIABLES) + all_functions = copy.copy(DEFAULT_FUNCTIONS) all_variables.update(variables) all_functions.update(functions) @@ -234,7 +234,7 @@ def evaluator(variables, functions, string, cs=False): return float('nan') # SI suffixes and percent - number_suffix = MatchFirst([Literal(k) for k in suffixes.keys()]) + number_suffix = MatchFirst([Literal(k) for k in SUFFIXES.keys()]) plus_minus = Literal('+') | Literal('-') times_div = Literal('*') | Literal('/') @@ -249,11 +249,10 @@ def evaluator(variables, functions, string, cs=False): number = (inner_number + Optional(CaselessLiteral("E") + Optional(plus_minus) + number_part) + Optional(number_suffix)) - number = number.setParseAction(number_parse_action) # Convert to number + number.setParseAction(number_parse_action) # Convert to number # Predefine recursive variables expr = Forward() - factor = Forward() # Handle variables passed in. E.g. if we have {'R':0.5}, we make the substitution. # Special case for no variables because of how we understand PyParsing is put together @@ -261,9 +260,10 @@ def evaluator(variables, functions, string, cs=False): # We sort the list so that var names (like "e2") match before # mathematical constants (like "e"). This is kind of a hack. all_variables_keys = sorted(all_variables.keys(), key=len, reverse=True) - literal_all_vars = [CasedLiteral(k) for k in all_variables_keys] - varnames = MatchFirst(literal_all_vars) - varnames.setParseAction(lambda x: [all_variables[k] for k in x]) + varnames = MatchFirst([CasedLiteral(k) for k in all_variables_keys]) + varnames.setParseAction( + lambda x: [all_variables[k] for k in x] + ) else: # all_variables includes DEFAULT_VARIABLES, which isn't empty # this is unreachable. Get rid of it? @@ -273,7 +273,9 @@ def evaluator(variables, functions, string, cs=False): if len(all_functions) > 0: funcnames = MatchFirst([CasedLiteral(k) for k in all_functions.keys()]) function = funcnames + Suppress("(") + expr + Suppress(")") - function.setParseAction(func_parse_action) + function.setParseAction( + lambda x: [all_functions[x[0]](x[1])] + ) else: # see note above (this is unreachable) function = NoMatch() @@ -281,11 +283,13 @@ def evaluator(variables, functions, string, cs=False): atom = number | function | varnames | Suppress("(") + expr + Suppress(")") # Do the following in the correct order to preserve order of operation - factor << (atom + ZeroOrMore("^" + atom)).setParseAction(exp_parse_action) # 7^6 - paritem = factor + ZeroOrMore(Literal('||') + factor) # 5k || 4k - paritem = paritem.setParseAction(parallel) - term = paritem + ZeroOrMore(times_div + paritem) # 7 * 5 / 4 - 3 - term = term.setParseAction(prod_parse_action) - expr << Optional(plus_minus) + term + ZeroOrMore(plus_minus + term) # -5 + 4 - 3 - expr = expr.setParseAction(sum_parse_action) + pow_term = atom + ZeroOrMore(Suppress("^") + atom) + pow_term.setParseAction(exp_parse_action) # 7^6 + par_term = pow_term + ZeroOrMore(Suppress('||') + pow_term) # 5k || 4k + par_term.setParseAction(parallel) + prod_term = par_term + ZeroOrMore(times_div + par_term) # 7 * 5 / 4 - 3 + prod_term.setParseAction(prod_parse_action) + sum_term = Optional(plus_minus) + prod_term + ZeroOrMore(plus_minus + prod_term) # -5 + 4 - 3 + sum_term.setParseAction(sum_parse_action) + expr << sum_term # finish the recursion return (expr + stringEnd).parseString(string)[0] From 83f1f9c2fc78442c77376457094ba674bca59c49 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Wed, 5 Jun 2013 15:50:35 -0400 Subject: [PATCH 012/375] Set numpy so it does not print out warnings on student input --- common/lib/calc/calc.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/common/lib/calc/calc.py b/common/lib/calc/calc.py index f862b41542..cc3a883221 100644 --- a/common/lib/calc/calc.py +++ b/common/lib/calc/calc.py @@ -13,6 +13,10 @@ import re import numpy import scipy.constants +# have numpy raise errors on functions outside its domain +# See http://docs.scipy.org/doc/numpy/reference/generated/numpy.seterr.html +numpy.seterr(all='ignore') # Also: 'ignore', 'warn' (default), 'raise' + from pyparsing import (Word, nums, Literal, ZeroOrMore, MatchFirst, Optional, Forward, From af2416756edaf34f257e3ff20c156202506d13df Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Wed, 5 Jun 2013 17:06:36 -0400 Subject: [PATCH 013/375] Moved from djanalytics to edinsights --- lms/envs/analyticsserver.py | 4 ++-- lms/urls.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lms/envs/analyticsserver.py b/lms/envs/analyticsserver.py index 54b4993232..9bc6745129 100644 --- a/lms/envs/analyticsserver.py +++ b/lms/envs/analyticsserver.py @@ -43,8 +43,8 @@ MITX_FEATURES['RUN_AS_ANALYTICS_SERVER_ENABLED'] = True INSTALLED_APPS = INSTALLED_APPS + ( 'djeventstream.httphandler', 'djcelery', 'south', - 'djanalytics.core', - 'djanalytics.modulefs', + 'edinsights.core', + 'edinsights.modulefs', ) INSTALLED_ANALYTICS_MODULES = open("../analytics_modules.txt").readlines() diff --git a/lms/urls.py b/lms/urls.py index 8fffa80586..cec118fe7d 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -355,9 +355,9 @@ if settings.MITX_FEATURES.get('ENABLE_SQL_TRACKING_LOGS'): if settings.MITX_FEATURES.get('RUN_AS_ANALYTICS_SERVER_ENABLED'): urlpatterns += ( - url('^', include('djanalytics.core.urls')), + url('^', include('edinsights.core.urls')), ) - import djanalytics.core.registry + import edinsights.core.registry # FoldIt views urlpatterns += ( From 4415fb4c42da8bbc3b9a677621b5ba97ac23af20 Mon Sep 17 00:00:00 2001 From: Peter Fogg Date: Thu, 6 Jun 2013 10:29:24 -0400 Subject: [PATCH 014/375] Started removing XML from video editor. TODO: This breaks the 1.5x and .75x speeds. I'm still looking into why. TODO: VideoDescriptor inherits from RawDescriptor in order to use the from_xml and export_to_xml methods. This seems really ugly, though; I'd rather find a better way to do this. --- .../features/video-editor.feature | 2 +- .../contentstore/features/video-editor.py | 14 +- .../xmodule/js/src/video/display.coffee | 11 +- common/lib/xmodule/xmodule/video_module.py | 158 ++++++++++-------- lms/templates/video.html | 12 +- 5 files changed, 116 insertions(+), 81 deletions(-) diff --git a/cms/djangoapps/contentstore/features/video-editor.feature b/cms/djangoapps/contentstore/features/video-editor.feature index 4c2a460042..9f2d44442b 100644 --- a/cms/djangoapps/contentstore/features/video-editor.feature +++ b/cms/djangoapps/contentstore/features/video-editor.feature @@ -4,7 +4,7 @@ Feature: Video Component Editor Scenario: User can view metadata Given I have created a Video component And I edit and select Settings - Then I see only the Video display name setting + Then I see the correct settings and default values Scenario: User can modify display name Given I have created a Video component diff --git a/cms/djangoapps/contentstore/features/video-editor.py b/cms/djangoapps/contentstore/features/video-editor.py index 27423575c3..124bb4f68a 100644 --- a/cms/djangoapps/contentstore/features/video-editor.py +++ b/cms/djangoapps/contentstore/features/video-editor.py @@ -4,6 +4,14 @@ from lettuce import world, step -@step('I see only the video display name setting$') -def i_see_only_the_video_display_name(step): - world.verify_all_setting_entries([['Display Name', "default", True]]) +@step('I see the correct settings and default values$') +def i_see_the_correct_settings_and_values(step): + world.verify_all_setting_entries([['.75x', 'JMD_ifUUfsU', False], + ['1.25x', 'AKqURZnYqpk', False], + ['1.5x', 'DYpADpL7jAY', False], + ['Display Name', "default", True], + ['External Source', '', False], + ['External Track', '', False], + ['Normal Speed', 'OEoXaMPEzfM', False], + ['Show Captions', 'True', False], + ]) diff --git a/common/lib/xmodule/xmodule/js/src/video/display.coffee b/common/lib/xmodule/xmodule/js/src/video/display.coffee index aadafbc8d0..79460a4e24 100644 --- a/common/lib/xmodule/xmodule/js/src/video/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/video/display.coffee @@ -8,7 +8,7 @@ class @Video @show_captions = @el.data('show-captions') == "true" window.player = null @el = $("#video_#{@id}") - @parseVideos @el.data('streams') + @parseVideos() @fetchMetadata() @parseSpeed() $("#video_#{@id}").data('video', this).addClass('video-load-complete') @@ -27,10 +27,11 @@ class @Video parseVideos: (videos) -> @videos = {} - $.each videos.split(/,/), (index, video) => - video = video.split(/:/) - speed = parseFloat(video[0]).toFixed(2).replace /\.00$/, '.0' - @videos[speed] = video[1] + @videos['.75'] = @el.data('youtube-id-0-75') + @videos['1.0'] = @el.data('normal-speed-video-id') + @videos['1.25'] = @el.data('youtube-id-1-25') + @videos['1.5'] = @el.data('youtube-id-1-5') + alert @videos['1.5'] parseSpeed: -> @setSpeed($.cookie('video_speed')) diff --git a/common/lib/xmodule/xmodule/video_module.py b/common/lib/xmodule/xmodule/video_module.py index f902a9665b..5b3913160f 100644 --- a/common/lib/xmodule/xmodule/video_module.py +++ b/common/lib/xmodule/xmodule/video_module.py @@ -8,18 +8,25 @@ from django.http import Http404 from xmodule.x_module import XModule from xmodule.raw_module import RawDescriptor -from xmodule.contentstore.content import StaticContent -from xblock.core import Integer, Scope, String - -import datetime -import time +from xmodule.editing_module import MetadataOnlyEditingDescriptor +from xblock.core import Integer, Scope, String, Boolean, Float log = logging.getLogger(__name__) +YOUTUBE_SPEEDS = ['.75', '1.0', '1.25', '1.5'] + class VideoFields(object): - data = String(help="XML data for the problem", scope=Scope.content) position = Integer(help="Current position in the video", scope=Scope.user_state, default=0) + show_captions = Boolean(help="Whether or not captions are shown", display_name="Show Captions", scope=Scope.settings, default=True) + youtube_id_1_0 = String(help="Youtube ID for normal speed video", display_name="Normal Speed", scope=Scope.settings, default="OEoXaMPEzfM") + youtube_id_0_75 = String(help="Youtube ID for .75x speed video", display_name=".75x", scope=Scope.settings, default="JMD_ifUUfsU") + youtube_id_1_25 = String(help="Youtube ID for 1.25x speed video", display_name="1.25x", scope=Scope.settings, default="AKqURZnYqpk") + youtube_id_1_5 = String(help="Youtube ID for 1.5x speed video", display_name="1.5x", scope=Scope.settings, default="DYpADpL7jAY") + start_time = Float(help="Time the video starts", display_name="Start Time", scope=Scope.settings, default=0.0) + end_time = Float(help="Time the video ends", display_name="End Time", scope=Scope.settings, default=0.0) + source = String(help="External source to download video", display_name="External Source", scope=Scope.settings, default="") + track = String(help="External source to download subtitle strack", display_name="External Track", scope=Scope.settings, default="") class VideoModule(VideoFields, XModule): @@ -39,52 +46,6 @@ class VideoModule(VideoFields, XModule): def __init__(self, *args, **kwargs): XModule.__init__(self, *args, **kwargs) - xmltree = etree.fromstring(self.data) - self.youtube = xmltree.get('youtube') - self.show_captions = xmltree.get('show_captions', 'true') - self.source = self._get_source(xmltree) - self.track = self._get_track(xmltree) - self.start_time, self.end_time = self._get_timeframe(xmltree) - - def _get_source(self, xmltree): - # find the first valid source - return self._get_first_external(xmltree, 'source') - - def _get_track(self, xmltree): - # find the first valid track - return self._get_first_external(xmltree, 'track') - - def _get_first_external(self, xmltree, tag): - """ - Will return the first valid element - of the given tag. - 'valid' means has a non-empty 'src' attribute - """ - result = None - for element in xmltree.findall(tag): - src = element.get('src') - if src: - result = src - break - return result - - def _get_timeframe(self, xmltree): - """ Converts 'from' and 'to' parameters in video tag to seconds. - If there are no parameters, returns empty string. """ - - def parse_time(s): - """Converts s in '12:34:45' format to seconds. If s is - None, returns empty string""" - if s is None: - return '' - else: - x = time.strptime(s, '%H:%M:%S') - return datetime.timedelta(hours=x.tm_hour, - minutes=x.tm_min, - seconds=x.tm_sec).total_seconds() - - return parse_time(xmltree.get('from')), parse_time(xmltree.get('to')) - def handle_ajax(self, dispatch, get): ''' Handle ajax calls to this video. @@ -113,37 +74,92 @@ class VideoModule(VideoFields, XModule): #log.debug(u"STATE POSITION {0}".format(self.position)) return json.dumps({'position': self.position}) - def video_list(self): - return self.youtube - def get_html(self): - # We normally let JS parse this, but in the case that we need a hacked - # out player because YouTube has broken their ') + storageContainer.close() + storageOwner = storageContainer.w.frames[0].document + storage = storageOwner.createElement('div') + } catch(e) { + // somehow ActiveXObject instantiation failed (perhaps some special + // security settings or otherwse), fall back to per-path storage + storage = doc.createElement('div') + storageOwner = doc.body + } + function withIEStorage(storeFunction) { + return function() { + var args = Array.prototype.slice.call(arguments, 0) + args.unshift(storage) + // See http://msdn.microsoft.com/en-us/library/ms531081(v=VS.85).aspx + // and http://msdn.microsoft.com/en-us/library/ms531424(v=VS.85).aspx + storageOwner.appendChild(storage) + storage.addBehavior('#default#userData') + storage.load(localStorageName) + var result = storeFunction.apply(store, args) + storageOwner.removeChild(storage) + return result + } + } + + // In IE7, keys may not contain special chars. See all of https://github.com/marcuswestin/store.js/issues/40 + var forbiddenCharsRegex = new RegExp("[!\"#$%&'()*+,/\\\\:;<=>?@[\\]^`{|}~]", "g") + function ieKeyFix(key) { + return key.replace(forbiddenCharsRegex, '___') + } + store.set = withIEStorage(function(storage, key, val) { + key = ieKeyFix(key) + if (val === undefined) { return store.remove(key) } + storage.setAttribute(key, store.serialize(val)) + storage.save(localStorageName) + return val + }) + store.get = withIEStorage(function(storage, key) { + key = ieKeyFix(key) + return store.deserialize(storage.getAttribute(key)) + }) + store.remove = withIEStorage(function(storage, key) { + key = ieKeyFix(key) + storage.removeAttribute(key) + storage.save(localStorageName) + }) + store.clear = withIEStorage(function(storage) { + var attributes = storage.XMLDocument.documentElement.attributes + storage.load(localStorageName) + for (var i=0, attr; attr=attributes[i]; i++) { + storage.removeAttribute(attr.name) + } + storage.save(localStorageName) + }) + store.getAll = withIEStorage(function(storage) { + var attributes = storage.XMLDocument.documentElement.attributes + var ret = {} + for (var i=0, attr; attr=attributes[i]; ++i) { + var key = ieKeyFix(attr.name) + ret[attr.name] = store.deserialize(storage.getAttribute(key)) + } + return ret + }) +} + +try { + store.set(namespace, namespace) + if (store.get(namespace) != namespace) { store.disabled = true } + store.remove(namespace) +} catch(e) { + store.disabled = true +} +store.enabled = !store.disabled + +module.exports = store; +}); +require.register("segmentio-top-domain/index.js", function(exports, require, module){ + +var url = require('url'); + +// Official Grammar: http://tools.ietf.org/html/rfc883#page-56 +// Look for tlds with up to 2-6 characters. + +module.exports = function (urlStr) { + + var host = url.parse(urlStr).hostname + , topLevel = host.match(/[a-z0-9][a-z0-9\-]*[a-z0-9]\.[a-z\.]{2,6}$/i); + + return topLevel ? topLevel[0] : host; +}; +}); +require.register("timoxley-next-tick/index.js", function(exports, require, module){ +"use strict" + +if (typeof setImmediate == 'function') { + module.exports = function(f){ setImmediate(f) } +} +// legacy node.js +else if (typeof process != 'undefined' && typeof process.nextTick == 'function') { + module.exports = process.nextTick +} +// fallback for other environments / postMessage behaves badly on IE8 +else if (typeof window == 'undefined' || window.ActiveXObject || !window.postMessage) { + module.exports = function(f){ setTimeout(f) }; +} else { + var q = []; + + window.addEventListener('message', function(){ + var i = 0; + while (i < q.length) { + try { q[i++](); } + catch (e) { + q = q.slice(i); + window.postMessage('tic!', '*'); + throw e; + } + } + q.length = 0; + }, true); + + module.exports = function(fn){ + if (!q.length) window.postMessage('tic!', '*'); + q.push(fn); + } +} + +}); +require.register("yields-prevent/index.js", function(exports, require, module){ + +/** + * prevent default on the given `e`. + * + * examples: + * + * anchor.onclick = prevent; + * anchor.onclick = function(e){ + * if (something) return prevent(e); + * }; + * + * @param {Event} e + */ + +module.exports = function(e){ + e = e || window.event + return e.preventDefault + ? e.preventDefault() + : e.returnValue = false; +}; + +}); +require.register("analytics/src/index.js", function(exports, require, module){ +// Analytics.js +// +// (c) 2013 Segment.io Inc. +// Analytics.js may be freely distributed under the MIT license. + +var Analytics = require('./analytics') + , providers = require('./providers'); + + +module.exports = new Analytics(providers); +}); +require.register("analytics/src/analytics.js", function(exports, require, module){ +var after = require('after') + , bind = require('event').bind + , clone = require('clone') + , cookie = require('./cookie') + , each = require('each') + , extend = require('extend') + , isEmail = require('is-email') + , isMeta = require('is-meta') + , localStore = require('./localStore') + , newDate = require('new-date') + , size = require('object').length + , preventDefault = require('prevent') + , Provider = require('./provider') + , providers = require('./providers') + , querystring = require('querystring') + , type = require('type') + , url = require('url') + , user = require('./user') + , utils = require('./utils'); + + +module.exports = Analytics; + + +/** + * Analytics. + * + * @param {Object} Providers - Provider classes that the user can initialize. + */ + +function Analytics (Providers) { + var self = this; + + this.VERSION = '0.11.9'; + + each(Providers, function (Provider) { + self.addProvider(Provider); + }); + + // Wrap `onload` with our own that will cache the loaded state of the page. + var oldonload = window.onload; + window.onload = function () { + self.loaded = true; + if ('function' === type(oldonload)) oldonload(); + }; +} + + +/** + * Extend the Analytics prototype. + */ + +extend(Analytics.prototype, { + + // Whether `onload` has fired. + loaded : false, + + // Whether `analytics` has been initialized. + initialized : false, + + // Whether all of our analytics providers are ready to accept calls. Give it a + // real jank name since we already use `analytics.ready` for the method. + readied : false, + + // A queue for ready callbacks to run when our `readied` state becomes `true`. + callbacks : [], + + // Milliseconds to wait for requests to clear before leaving the current page. + timeout : 300, + + // A reference to the current user object. + user : user, + + // The default Provider. + Provider : Provider, + + // Providers that can be initialized. Add using `this.addProvider`. + _providers : {}, + + // The currently initialized providers. + providers : [], + + + /** + * Add a provider to `_providers` to be initialized later. + * + * @param {String} name - The name of the provider. + * @param {Function} Provider - The provider's class. + */ + + addProvider : function (Provider) { + this._providers[Provider.prototype.name] = Provider; + }, + + + /** + * Initialize + * + * Call `initialize` to setup analytics.js before identifying or + * tracking any users or events. For example: + * + * analytics.initialize({ + * 'Google Analytics' : 'UA-XXXXXXX-X', + * 'Segment.io' : 'XXXXXXXXXXX', + * 'KISSmetrics' : 'XXXXXXXXXXX' + * }); + * + * @param {Object} providers - a dictionary of the providers you want to + * enable. The keys are the names of the providers and their values are either + * an api key, or dictionary of extra settings (including the api key). + * + * @param {Object} options (optional) - extra settings to initialize with. + */ + + initialize : function (providers, options) { + options || (options = {}); + + var self = this; + + // Reset our state. + this.providers = []; + this.initialized = false; + this.readied = false; + + // Set the storage options + cookie.options(options.cookie); + localStore.options(options.localStorage); + + // Set the options for loading and saving the user + user.options(options.user); + user.load(); + + // Create a ready method that will call all of our ready callbacks after all + // of our providers have been initialized and loaded. We'll pass the + // function into each provider's initialize method, so they can callback + // after they've loaded successfully. + var ready = after(size(providers), function () { + self.readied = true; + var callback; + while(callback = self.callbacks.shift()) { + callback(); + } + }); + + // Initialize a new instance of each provider with their `options`, and + // copy the provider into `this.providers`. + each(providers, function (key, options) { + var Provider = self._providers[key]; + if (!Provider) return; + self.providers.push(new Provider(options, ready, self)); + }); + + // Identify and track any `ajs_uid` and `ajs_event` parameters in the URL. + var query = url.parse(window.location.href).query; + var queries = querystring.parse(query); + if (queries.ajs_uid) this.identify(queries.ajs_uid); + if (queries.ajs_event) this.track(queries.ajs_event); + + // Update the initialized state that other methods rely on. + this.initialized = true; + }, + + + /** + * Ready + * + * Add a callback that will get called when all of the analytics services you + * initialize are ready to be called. It's like jQuery's `ready` except for + * analytics instead of the DOM. + * + * If we're already ready, it will callback immediately. + * + * @param {Function} callback - The callback to attach. + */ + + ready : function (callback) { + if (type(callback) !== 'function') return; + if (this.readied) return callback(); + this.callbacks.push(callback); + }, + + + /** + * Identify + * + * Identifying a user ties all of their actions to an ID you recognize + * and records properties about a user. For example: + * + * analytics.identify('4d3ed089fb60ab534684b7e0', { + * name : 'Achilles', + * email : 'achilles@segment.io', + * age : 23 + * }); + * + * @param {String} userId (optional) - The ID you recognize the user by. + * Ideally this isn't an email, because that might change in the future. + * + * @param {Object} traits (optional) - A dictionary of traits you know about + * the user. Things like `name`, `age`, etc. + * + * @param {Object} options (optional) - Settings for the identify call. + * + * @param {Function} callback (optional) - A function to call after a small + * timeout, giving the identify call time to make requests. + */ + + identify : function (userId, traits, options, callback) { + if (!this.initialized) return; + + // Allow for optional arguments. + if (type(options) === 'function') { + callback = options; + options = undefined; + } + if (type(traits) === 'function') { + callback = traits; + traits = undefined; + } + if (type(userId) === 'object') { + if (traits && type(traits) === 'function') callback = traits; + traits = userId; + userId = undefined; + } + + // Use our cookied ID if they didn't provide one. + if (userId === undefined || user === null) userId = user.id(); + + // Update the cookie with the new userId and traits. + var alias = user.update(userId, traits); + + // Clone `traits` before we manipulate it, so we don't do anything uncouth + // and take the user.traits() so anonymous users carry over traits. + traits = cleanTraits(userId, clone(user.traits())); + + // Call `identify` on all of our enabled providers that support it. + each(this.providers, function (provider) { + if (provider.identify && isEnabled(provider, options)) { + var args = [userId, clone(traits), clone(options)]; + if (provider.ready) { + provider.identify.apply(provider, args); + } else { + provider.enqueue('identify', args); + } + } + }); + + // If we should alias, go ahead and do it. + // if (alias) this.alias(userId); + + if (callback && type(callback) === 'function') { + setTimeout(callback, this.timeout); + } + }, + + + + /** + * Group + * + * Groups multiple users together under one "account" or "team" or "company". + * Acts on the currently identified user, so you need to call identify before + * calling group. For example: + * + * analytics.identify('4d3ed089fb60ab534684b7e0', { + * name : 'Achilles', + * email : 'achilles@segment.io', + * age : 23 + * }); + * + * analytics.group('5we93je3889fb60a937dk033', { + * name : 'Acme Co.', + * numberOfEmployees : 42, + * location : 'San Francisco' + * }); + * + * @param {String} groupId - The ID you recognize the group by. + * + * @param {Object} properties (optional) - A dictionary of properties you know + * about the group. Things like `numberOfEmployees`, `location`, etc. + * + * @param {Object} options (optional) - Settings for the group call. + * + * @param {Function} callback (optional) - A function to call after a small + * timeout, giving the group call time to make requests. + */ + + group : function (groupId, properties, options, callback) { + if (!this.initialized) return; + + // Allow for optional arguments. + if (type(options) === 'function') { + callback = options; + options = undefined; + } + if (type(properties) === 'function') { + callback = properties; + properties = undefined; + } + + // Clone `properties` before we manipulate it, so we don't do anything bad, + // and back it by an empty object so that providers can assume it exists. + properties = clone(properties) || {}; + + // Convert dates from more types of input into Date objects. + if (properties.created) properties.created = newDate(properties.created); + + // Call `group` on all of our enabled providers that support it. + each(this.providers, function (provider) { + if (provider.group && isEnabled(provider, options)) { + var args = [groupId, clone(properties), clone(options)]; + if (provider.ready) { + provider.group.apply(provider, args); + } else { + provider.enqueue('group', args); + } + } + }); + + // If we have a callback, call it after a small timeout. + if (callback && type(callback) === 'function') { + setTimeout(callback, this.timeout); + } + }, + + + /** + * Track + * + * Record an event (or action) that your user has triggered. For example: + * + * analytics.track('Added a Friend', { + * level : 'hard', + * volume : 11 + * }); + * + * @param {String} event - The name of your event. + * + * @param {Object} properties (optional) - A dictionary of properties of the + * event. `properties` are all camelCase (we'll automatically conver them to + * the proper case each provider needs). + * + * @param {Object} options (optional) - Settings for the track call. + * + * @param {Function} callback - A function to call after a small + * timeout, giving the identify time to make requests. + */ + + track : function (event, properties, options, callback) { + if (!this.initialized) return; + + // Allow for optional arguments. + if (type(options) === 'function') { + callback = options; + options = undefined; + } + if (type(properties) === 'function') { + callback = properties; + properties = undefined; + } + + // Call `track` on all of our enabled providers that support it. + each(this.providers, function (provider) { + if (provider.track && isEnabled(provider, options)) { + var args = [event, clone(properties), clone(options)]; + if (provider.ready) { + provider.track.apply(provider, args); + } else { + provider.enqueue('track', args); + } + } + }); + + if (callback && type(callback) === 'function') { + setTimeout(callback, this.timeout); + } + }, + + + /** + * Track Link + * + * A helper for tracking outbound links that would normally navigate away from + * the page before the track requests were made. It works by wrapping the + * calls in a short timeout, giving the requests time to fire. + * + * @param {Element|Array} links - The link element or array of link elements + * to bind to. (Allowing arrays makes it easy to pass in jQuery objects.) + * + * @param {String|Function} event - Passed directly to `track`. Or in the case + * that it's a function, it will be called with the link element as the first + * argument. + * + * @param {Object|Function} properties (optional) - Passed directly to + * `track`. Or in the case that it's a function, it will be called with the + * link element as the first argument. + */ + + trackLink : function (links, event, properties) { + if (!links) return; + + // Turn a single link into an array so that we're always handling + // arrays, which allows for passing jQuery objects. + if ('element' === type(links)) links = [links]; + + var self = this + , eventFunction = 'function' === type(event) + , propertiesFunction = 'function' === type(properties); + + each(links, function (el) { + bind(el, 'click', function (e) { + + // Allow for `event` or `properties` to be a function. And pass it the + // link element that was clicked. + var newEvent = eventFunction ? event(el) : event; + var newProperties = propertiesFunction ? properties(el) : properties; + + self.track(newEvent, newProperties); + + // To justify us preventing the default behavior we must: + // + // * Have an `href` to use. + // * Not have a `target="_blank"` attribute. + // * Not have any special keys pressed, because they might be trying to + // open in a new tab, or window, or download. + // + // This might not cover all cases, but we'd rather throw out an event + // than miss a case that breaks the user experience. + if (el.href && el.target !== '_blank' && !isMeta(e)) { + + preventDefault(e); + + // Navigate to the url after just enough of a timeout. + setTimeout(function () { + window.location.href = el.href; + }, self.timeout); + } + }); + }); + }, + + + /** + * Track Form + * + * Similar to `trackClick`, this is a helper for tracking form submissions + * that would normally navigate away from the page before a track request can + * be sent. It works by preventing the default submit event, sending our + * track requests, and then submitting the form programmatically. + * + * @param {Element|Array} forms - The form element or array of form elements + * to bind to. (Allowing arrays makes it easy to pass in jQuery objects.) + * + * @param {String|Function} event - Passed directly to `track`. Or in the case + * that it's a function, it will be called with the form element as the first + * argument. + * + * @param {Object|Function} properties (optional) - Passed directly to + * `track`. Or in the case that it's a function, it will be called with the + * form element as the first argument. + */ + + trackForm : function (form, event, properties) { + if (!form) return; + + // Turn a single element into an array so that we're always handling arrays, + // which allows for passing jQuery objects. + if ('element' === type(form)) form = [form]; + + var self = this + , eventFunction = 'function' === type(event) + , propertiesFunction = 'function' === type(properties); + + each(form, function (el) { + var handler = function (e) { + + // Allow for `event` or `properties` to be a function. And pass it the + // form element that was submitted. + var newEvent = eventFunction ? event(el) : event; + var newProperties = propertiesFunction ? properties(el) : properties; + + self.track(newEvent, newProperties); + + preventDefault(e); + + // Submit the form after a timeout, giving the event time to fire. + setTimeout(function () { + el.submit(); + }, self.timeout); + }; + + // Support the form being submitted via jQuery instead of for real. This + // doesn't happen automatically because `el.submit()` doesn't actually + // fire submit handlers, which is what jQuery uses internally. >_< + var dom = window.jQuery || window.Zepto; + if (dom) { + dom(el).submit(handler); + } else { + bind(el, 'submit', handler); + } + }); + }, + + + /** + * Pageview + * + * Simulate a pageview in single-page applications, where real pageviews don't + * occur. This isn't support by all providers. + * + * @param {String} url (optional) - The path of the page (eg. '/login'). Most + * providers will default to the current pages URL, so you don't need this. + * + * @param {Object} options (optional) - Settings for the pageview call. + * + */ + + pageview : function (url,options) { + if (!this.initialized) return; + + // Call `pageview` on all of our enabled providers that support it. + each(this.providers, function (provider) { + if (provider.pageview && isEnabled(provider, options)) { + var args = [url]; + if (provider.ready) { + provider.pageview.apply(provider, args); + } else { + provider.enqueue('pageview', args); + } + } + }); + }, + + + /** + * Alias + * + * Merges two previously unassociate user identities. This comes in handy if + * the same user visits from two different devices and you want to combine + * their analytics history. + * + * Some providers don't support merging users. + * + * @param {String} newId - The new ID you want to recognize the user by. + * + * @param {String} originalId (optional) - The original ID that the user was + * recognized by. This defaults to the current identified user's ID if there + * is one. In most cases you don't need to pass in the `originalId`. + */ + + alias : function (newId, originalId, options) { + if (!this.initialized) return; + + if (type(originalId) === 'object') { + options = originalId; + originalId = undefined; + } + + // Call `alias` on all of our enabled providers that support it. + each(this.providers, function (provider) { + if (provider.alias && isEnabled(provider, options)) { + var args = [newId, originalId]; + if (provider.ready) { + provider.alias.apply(provider, args); + } else { + provider.enqueue('alias', args); + } + } + }); + }, + + + /** + * Log + * + * Log an error to analytics providers that support it, like Sentry. + * + * @param {Error|String} error - The error or string to log. + * @param {Object} properties - Properties about the error. + * @param {Object} options (optional) - Settings for the log call. + */ + + log : function (error, properties, options) { + if (!this.initialized) return; + + each(this.providers, function (provider) { + if (provider.log && isEnabled(provider, options)) { + var args = [error, properties, options]; + if (provider.ready) { + provider.log.apply(provider, args); + } else { + provider.enqueue('log', args); + } + } + }); + } + +}); + + +/** + * Backwards compatibility. + */ + +// Alias `trackClick` and `trackSubmit`. +Analytics.prototype.trackClick = Analytics.prototype.trackLink; +Analytics.prototype.trackSubmit = Analytics.prototype.trackForm; + + +/** + * Determine whether a provider is enabled or not based on the options object. + * + * @param {Object} provider - the current provider. + * @param {Object} options - the current call's options. + * + * @return {Boolean} - wether the provider is enabled. + */ + +var isEnabled = function (provider, options) { + var enabled = true; + if (!options || !options.providers) return enabled; + + // Default to the 'all' or 'All' setting. + var map = options.providers; + if (map.all !== undefined) enabled = map.all; + if (map.All !== undefined) enabled = map.All; + + // Look for this provider's specific setting. + var name = provider.name; + if (map[name] !== undefined) enabled = map[name]; + + return enabled; +}; + + +/** + * Clean up traits, default some useful things both so the user doesn't have to + * and so we don't have to do it on a provider-basis. + * + * @param {Object} traits The traits object. + * @return {Object} The new traits object. + */ + +var cleanTraits = function (userId, traits) { + + // Add the `email` trait if it doesn't exist and the `userId` is an email. + if (!traits.email && isEmail(userId)) traits.email = userId; + + // Create the `name` trait if it doesn't exist and `firstName` and `lastName` + // are both supplied. + if (!traits.name && traits.firstName && traits.lastName) { + traits.name = traits.firstName + ' ' + traits.lastName; + } + + // Convert dates from more types of input into Date objects. + if (traits.created) traits.created = newDate(traits.created); + if (traits.company && traits.company.created) { + traits.company.created = newDate(traits.company.created); + } + + return traits; +}; + +}); +require.register("analytics/src/cookie.js", function(exports, require, module){ + +var bindAll = require('bind-all') + , cookie = require('cookie') + , clone = require('clone') + , defaults = require('defaults') + , json = require('json') + , topDomain = require('top-domain'); + + +function Cookie (options) { + this.options(options); +} + +/** + * Get or set the cookie options + * + * @param {Object} options + * @field {Number} maxage (1 year) + * @field {String} domain + * @field {String} path + * @field {Boolean} secure + */ + +Cookie.prototype.options = function (options) { + if (arguments.length === 0) return this._options; + + options || (options = {}); + + var domain = '.' + topDomain(window.location.href); + + // localhost cookies are special: http://curl.haxx.se/rfc/cookie_spec.html + if (domain === '.localhost') domain = ''; + + defaults(options, { + maxage : 31536000000, // default to a year + path : '/', + domain : domain + }); + + this._options = options; +}; + + +/** + * Set a value in our cookie + * + * @param {String} key + * @param {Object} value + * @return {Boolean} saved + */ + +Cookie.prototype.set = function (key, value) { + try { + value = json.stringify(value); + cookie(key, value, clone(this._options)); + return true; + } catch (e) { + return false; + } +}; + + +/** + * Get a value from our cookie + * @param {String} key + * @return {Object} value + */ + +Cookie.prototype.get = function (key) { + try { + var value = cookie(key); + value = value ? json.parse(value) : null; + return value; + } catch (e) { + return null; + } +}; + + +/** + * Remove a value from the cookie + * + * @param {String} key + * @return {Boolean} removed + */ + +Cookie.prototype.remove = function (key) { + try { + cookie(key, null, clone(this._options)); + return true; + } catch (e) { + return false; + } +}; + + +/** + * Export singleton cookie + */ + +module.exports = bindAll(new Cookie()); + + +module.exports.Cookie = Cookie; + +}); +require.register("analytics/src/localStore.js", function(exports, require, module){ + +var bindAll = require('bind-all') + , defaults = require('defaults') + , store = require('store'); + + +function Store (options) { + this.options(options); +} + + +/** + * Sets the options for the store + * + * @param {Object} options + * @field {Boolean} enabled (true) + */ + +Store.prototype.options = function (options) { + if (arguments.length === 0) return this._options; + + options || (options = {}); + defaults(options, { enabled : true }); + + this.enabled = options.enabled && store.enabled; + this._options = options; +}; + + +/** + * Sets a value in local storage + * + * @param {String} key + * @param {Object} value + */ + +Store.prototype.set = function (key, value) { + if (!this.enabled) return false; + return store.set(key, value); +}; + + +/** + * Gets a value from local storage + * + * @param {String} key + * @return {Object} + */ + +Store.prototype.get = function (key) { + if (!this.enabled) return null; + return store.get(key); +}; + + +/** + * Removes a value from local storage + * + * @param {String} key + */ + +Store.prototype.remove = function (key) { + if (!this.enabled) return false; + return store.remove(key); +}; + + +/** + * Singleton exports + */ + +module.exports = bindAll(new Store()); +}); +require.register("analytics/src/provider.js", function(exports, require, module){ +var each = require('each') + , extend = require('extend') + , type = require('type'); + + +module.exports = Provider; + + +/** + * Provider + * + * @param {Object} options - settings to initialize the Provider with. This will + * be merged with the Provider's own defaults. + * + * @param {Function} ready - a ready callback, to be called when the provider is + * ready to handle analytics calls. + */ + +function Provider (options, ready, analytics) { + var self = this; + + // Store the reference to the global `analytics` object. + this.analytics = analytics; + + // Make a queue of `{ method : 'identify', args : [] }` to unload once ready. + this.queue = []; + this.ready = false; + + // Allow for `options` to only be a string if the provider has specified + // a default `key`, in which case convert `options` into a dictionary. Also + // allow for it to be `true`, like in Optimizely's case where there is no need + // for any default key. + if (type(options) !== 'object') { + if (options === true) { + options = {}; + } else if (this.key) { + var key = options; + options = {}; + options[this.key] = key; + } else { + throw new Error('Couldnt resolve options.'); + } + } + + // Extend the passed-in options with our defaults. + this.options = extend({}, this.defaults, options); + + // Wrap our ready function, so that it ready from our internal queue first + // and then marks us as ready. + var dequeue = function () { + each(self.queue, function (call) { + var method = call.method + , args = call.args; + self[method].apply(self, args); + }); + self.ready = true; + self.queue = []; + ready(); + }; + + // Call our initialize method. + this.initialize.call(this, this.options, dequeue); +} + + +/** + * Inheritance helper. + * + * Modeled after Backbone's `extend` method: + * https://github.com/documentcloud/backbone/blob/master/backbone.js#L1464 + */ + +Provider.extend = function (properties) { + var parent = this; + var child = function () { return parent.apply(this, arguments); }; + var Surrogate = function () { this.constructor = child; }; + Surrogate.prototype = parent.prototype; + child.prototype = new Surrogate(); + extend(child.prototype, properties); + return child; +}; + + +/** + * Augment Provider's prototype. + */ + +extend(Provider.prototype, { + + /** + * Default settings for the provider. + */ + + options : {}, + + + /** + * The single required API key for the provider. This lets us support a terse + * initialization syntax: + * + * analytics.initialize({ + * 'Provider' : 'XXXXXXX' + * }); + * + * Only add this if the provider has a _single_ required key. + */ + + key : undefined, + + + /** + * Initialize our provider. + * + * @param {Object} options - the settings for the provider. + * @param {Function} ready - a ready callback to call when we're ready to + * start accept analytics method calls. + */ + initialize : function (options, ready) { + ready(); + }, + + + /** + * Adds an item to the our internal pre-ready queue. + * + * @param {String} method - the analytics method to call (eg. 'track'). + * @param {Object} args - the arguments to pass to the method. + */ + enqueue : function (method, args) { + this.queue.push({ + method : method, + args : args + }); + } + +}); +}); +require.register("analytics/src/user.js", function(exports, require, module){ +var bindAll = require('bind-all') + , clone = require('clone') + , cookie = require('./cookie') + , defaults = require('defaults') + , extend = require('extend') + , localStore = require('./localStore'); + + +function User (options) { + this._id = null; + this._traits = {}; + this.options(options); +} + + +/** + * Sets the options for the user + * + * @param {Object} options + * @field {Object} cookie + * @field {Object} localStorage + * @field {Boolean} persist (true) + */ + +User.prototype.options = function (options) { + options || (options = {}); + + defaults(options, { + persist : true + }); + + this.cookie(options.cookie); + this.localStorage(options.localStorage); + this.persist = options.persist; +}; + + +/** + * Get or set cookie options + * + * @param {Object} options + */ + +User.prototype.cookie = function (options) { + if (arguments.length === 0) return this.cookieOptions; + + options || (options = {}); + defaults(options, { + key : 'ajs_user_id', + oldKey : 'ajs_user' + }); + this.cookieOptions = options; +}; + + +/** + * Get or set local storage options + * + * @param {Object} options + */ + +User.prototype.localStorage = function (options) { + if (arguments.length === 0) return this.localStorageOptions; + + options || (options = {}); + defaults(options, { + key : 'ajs_user_traits' + }); + this.localStorageOptions = options; +}; + + +/** + * Get or set the user id + * + * @param {String} id + */ + +User.prototype.id = function (id) { + if (arguments.length === 0) return this._id; + this._id = id; +}; + + +/** + * Get or set the user traits + * + * @param {Object} traits + */ + +User.prototype.traits = function (traits) { + if (arguments.length === 0) return clone(this._traits); + traits || (traits = {}); + + this._traits = traits; +}; + + +/** + * Updates the current stored user with id and traits. + * + * @param {String} userId - the new user ID. + * @param {Object} traits - any new traits. + * @return {Boolean} whether alias should be called. + */ + +User.prototype.update = function (userId, traits) { + + // Make an alias call if there was no previous userId, there is one + // now, and we are using a cookie between page loads. + var alias = !this.id() && userId && this.persist; + + traits || (traits = {}); + + // If there is a current user and the new user isn't the same, + // we want to just replace their traits. Otherwise extend. + if (this.id() && userId && this.id() !== userId) this.traits(traits); + else this.traits(extend(this.traits(), traits)); + + if (userId) this.id(userId); + + this.save(); + + return alias; +}; + + +/** + * Save the user to localstorage and cookie + * + * @return {Boolean} saved + */ + +User.prototype.save = function () { + if (!this.persist) return false; + + cookie.set(this.cookie().key, this.id()); + localStore.set(this.localStorage().key, this.traits()); + return true; +}; + + +/** + * Loads a saved user, and set its information + * + * @return {Object} user + */ + +User.prototype.load = function () { + if (this.loadOldCookie()) return this.toJSON(); + + var id = cookie.get(this.cookie().key) + , traits = localStore.get(this.localStorage().key); + + this.id(id); + this.traits(traits); + return this.toJSON(); +}; + + +/** + * Clears the user, and removes the stored version + * + */ + +User.prototype.clear = function () { + cookie.remove(this.cookie().key); + localStore.remove(this.localStorage().key); + this.id(null); + this.traits({}); +}; + + +/** + * Load the old user from the cookie. Should be phased + * out at some point + * + * @return {Boolean} loaded + */ + +User.prototype.loadOldCookie = function () { + var user = cookie.get(this.cookie().oldKey); + if (!user) return false; + + this.id(user.id); + this.traits(user.traits); + cookie.remove(this.cookie().oldKey); + return true; +}; + + +/** + * Get the user info + * + * @return {Object} + */ + +User.prototype.toJSON = function () { + return { + id : this.id(), + traits : this.traits() + }; +}; + + +/** + * Export the new user as a singleton. + */ + +module.exports = bindAll(new User()); + +}); +require.register("analytics/src/utils.js", function(exports, require, module){ +// A helper to track events based on the 'anjs' url parameter +exports.getUrlParameter = function (urlSearchParameter, paramKey) { + var params = urlSearchParameter.replace('?', '').split('&'); + for (var i = 0; i < params.length; i += 1) { + var param = params[i].split('='); + if (param.length === 2 && param[0] === paramKey) { + return decodeURIComponent(param[1]); + } + } +}; +}); +require.register("analytics/src/providers/adroll.js", function(exports, require, module){ +// https://www.adroll.com/dashboard + +var Provider = require('../provider') + , load = require('load-script'); + + +module.exports = Provider.extend({ + + name : 'AdRoll', + + defaults : { + // Adroll requires two options: `advId` and `pixId`. + advId : null, + pixId : null + }, + + initialize : function (options, ready) { + window.adroll_adv_id = options.advId; + window.adroll_pix_id = options.pixId; + window.__adroll_loaded = true; + + load({ + http : 'http://a.adroll.com/j/roundtrip.js', + https : 'https://s.adroll.com/j/roundtrip.js' + }, ready); + } + +}); +}); +require.register("analytics/src/providers/amplitude.js", function(exports, require, module){ +// https://github.com/amplitude/Amplitude-Javascript + +var Provider = require('../provider') + , alias = require('alias') + , load = require('load-script'); + + +module.exports = Provider.extend({ + + name : 'Amplitude', + + key : 'apiKey', + + defaults : { + // Amplitude's required API key. + apiKey : null, + // Whether to track pageviews to Amplitude. + pageview : false + }, + + initialize : function (options, ready) { + // Create the Amplitude global and queuer methods. + (function(e,t){var r=e.amplitude||{}; + r._q=[];function i(e){r[e]=function(){r._q.push([e].concat(Array.prototype.slice.call(arguments,0)))}} + var s=["init","logEvent","setUserId","setGlobalUserProperties","setVersionName"]; + for(var c=0;c<",i,' onl' + 'oad="var d=',g,";d.getElementsByTagName('head')[0].",j,"(d.",h,"('script')).",k,"='",l,"//",a.l,"'",'"',">"].join("")}var i="body",m=d[i];if(!m){return setTimeout(ld,100)}a.P(1);var j="appendChild",h="createElement",k="src",n=d[h]("div"),v=n[j](d[h](z)),b=d[h]("iframe"),g="document",e="domain",o;n.style.display="none";m.insertBefore(n,m.firstChild).id=z;b.frameBorder="0";b.id=z+"-loader";if(/MSIE[ ]+6/.test(navigator.userAgent)){b.src="javascript:false"}b.allowTransparency="true";v[j](b);try{b.contentWindow[g].open()}catch(w){c[e]=d[e];o="javascript:var d="+g+".open();d.domain='"+d.domain+"';";b[k]=o+"void(0);"}try{var t=b.contentWindow[g];t.write(p());t.close()}catch(x){b[k]=o+'d.write("'+p().replace(/"/g,String.fromCharCode(92)+'"')+'");d.close();'}a.P(2)};ld()};nt()})({loader: "static.olark.com/jsclient/loader0.js",name:"olark",methods:["configure","extend","declare","identify"]}); + window.olark.identify(options.siteId); + + // Set up event handlers for chat box open and close so that + // we know whether a conversation is active. If it is active, + // then we'll send track and pageview information. + var self = this; + window.olark('api.box.onExpand', function () { self.chatting = true; }); + window.olark('api.box.onShrink', function () { self.chatting = false; }); + + // Olark creates it's method in the snippet, so it's ready immediately. + ready(); + }, + + // Update traits about the user in Olark to make the operator's life easier. + identify : function (userId, traits) { + if (!this.options.identify) return; + + var email = traits.email + , name = traits.name || traits.firstName + , phone = traits.phone + , nickname = name || email || userId; + + // If we have a name and an email, add the email too to be more helpful. + if (name && email) nickname += ' ('+email+')'; + + // Call all of Olark's settings APIs. + window.olark('api.visitor.updateCustomFields', traits); + if (email) window.olark('api.visitor.updateEmailAddress', { emailAddress : email }); + if (name) window.olark('api.visitor.updateFullName', { fullName : name }); + if (phone) window.olark('api.visitor.updatePhoneNumber', { phoneNumber : phone }); + if (nickname) window.olark('api.chat.updateVisitorNickname', { snippet : nickname }); + }, + + // Log events the user triggers to the chat console, if you so desire it. + track : function (event, properties) { + if (!this.options.track || !this.chatting) return; + + // To stay consistent with olark's default messages, it's all lowercase. + window.olark('api.chat.sendNotificationToOperator', { + body : 'visitor triggered "'+event+'"' + }); + }, + + // Mimic the functionality Olark has for normal pageviews with pseudo- + // pageviews, telling the operator when a visitor changes pages. + pageview : function (url) { + if (!this.options.pageview || !this.chatting) return; + + // To stay consistent with olark's default messages, it's all lowercase. + window.olark('api.chat.sendNotificationToOperator', { + body : 'looking at ' + window.location.href + }); + } + +}); +}); +require.register("analytics/src/providers/optimizely.js", function(exports, require, module){ +// https://www.optimizely.com/docs/api + +var each = require('each') + , nextTick = require('next-tick') + , Provider = require('../provider'); + + +module.exports = Provider.extend({ + + name : 'Optimizely', + + defaults : { + // Whether to replay variations into other enabled integrations as traits. + variations : true + }, + + initialize : function (options, ready, analytics) { + // Create the `optimizely` object in case it doesn't exist already. + // https://www.optimizely.com/docs/api#function-calls + window.optimizely = window.optimizely || []; + + // If the `variations` option is true, replay our variations on the next + // tick to wait for the entire library to be ready for replays. + if (options.variations) { + var self = this; + nextTick(function () { self.replay(); }); + } + + // Optimizely should be on the page already, so it's always ready. + ready(); + }, + + track : function (event, properties) { + // Optimizely takes revenue as cents, not dollars. + if (properties && properties.revenue) properties.revenue = properties.revenue * 100; + + window.optimizely.push(['trackEvent', event, properties]); + }, + + replay : function () { + // Make sure we have access to Optimizely's `data` dictionary. + var data = window.optimizely.data; + if (!data) return; + + // Grab a few pieces of data we'll need for replaying. + var experiments = data.experiments + , variationNamesMap = data.state.variationNamesMap; + + // Create our traits object to add variations to. + var traits = {}; + + // Loop through all the experiement the user has been assigned a variation + // for and add them to our traits. + each(variationNamesMap, function (experimentId, variation) { + traits['Experiment: ' + experiments[experimentId].name] = variation; + }); + + this.analytics.identify(traits); + } + +}); +}); +require.register("analytics/src/providers/perfect-audience.js", function(exports, require, module){ +// https://www.perfectaudience.com/docs#javascript_api_autoopen + +var Provider = require('../provider') + , load = require('load-script'); + + +module.exports = Provider.extend({ + + name : 'Perfect Audience', + + key : 'siteId', + + defaults : { + siteId : null + }, + + initialize : function (options, ready) { + window._pa || (window._pa = {}); + load('//tag.perfectaudience.com/serve/' + options.siteId + '.js', ready); + }, + + track : function (event, properties) { + window._pa.track(event, properties); + } + +}); +}); +require.register("analytics/src/providers/pingdom.js", function(exports, require, module){ +var date = require('load-date') + , Provider = require('../provider') + , load = require('load-script'); + + +module.exports = Provider.extend({ + + name : 'Pingdom', + + key : 'id', + + defaults : { + id : null + }, + + initialize : function (options, ready) { + + window._prum = [ + ['id', options.id], + ['mark', 'firstbyte', date.getTime()] + ]; + + // We've replaced the original snippet loader with our own load method. + load('//rum-static.pingdom.net/prum.min.js', ready); + } + +}); +}); +require.register("analytics/src/providers/preact.js", function(exports, require, module){ +// http://www.preact.io/api/javascript + +var Provider = require('../provider') + , isEmail = require('is-email') + , load = require('load-script'); + +module.exports = Provider.extend({ + + name : 'Preact', + + key : 'projectCode', + + defaults : { + projectCode : null + }, + + initialize : function (options, ready) { + var _lnq = window._lnq = window._lnq || []; + _lnq.push(["_setCode", options.projectCode]); + + load('//d2bbvl6dq48fa6.cloudfront.net/js/ln-2.4.min.js'); + ready(); + }, + + identify : function (userId, traits) { + // Don't do anything if we just have traits. Preact requires a `userId`. + if (!userId) return; + + // Swap the `created` trait to the `created_at` that Preact needs + // and convert it from milliseconds to seconds. + if (traits.created) { + traits.created_at = Math.floor(traits.created/1000); + delete traits.created; + } + + window._lnq.push(['_setPersonData', { + name : traits.name, + email : traits.email, + uid : userId, + properties : traits + }]); + }, + + group : function (groupId, properties) { + if (!groupId) return; + properties.id = groupId; + window._lnq.push(['_setAccount', properties]); + }, + + track : function (event, properties) { + properties || (properties = {}); + + // Preact takes a few special properties, and the rest in `extras`. So first + // convert and remove the special ones from `properties`. + var special = { name : event }; + + // They take `revenue` in cents. + if (properties.revenue) { + special.revenue = properties.revenue * 100; + delete properties.revenue; + } + + if (properties.note) { + special.note = properties.note; + delete properties.note; + } + + window._lnq.push(['_logEvent', special, properties]); + } + +}); +}); +require.register("analytics/src/providers/qualaroo.js", function(exports, require, module){ +// http://help.qualaroo.com/customer/portal/articles/731085-identify-survey-nudge-takers +// http://help.qualaroo.com/customer/portal/articles/731091-set-additional-user-properties + +var Provider = require('../provider') + , isEmail = require('is-email') + , load = require('load-script'); + + +module.exports = Provider.extend({ + + name : 'Qualaroo', + + defaults : { + // Qualaroo has two required options. + customerId : null, + siteToken : null, + // Whether to record traits when a user triggers an event. This can be + // useful for sending targetted questionnaries. + track : false + }, + + // Qualaroo's script has two options in its URL. + initialize : function (options, ready) { + window._kiq = window._kiq || []; + load('//s3.amazonaws.com/ki.js/' + options.customerId + '/' + options.siteToken + '.js'); + + // Qualaroo creates a queue, so it's ready immediately. + ready(); + }, + + // Qualaroo uses two separate methods: `identify` for storing the `userId`, + // and `set` for storing `traits`. + identify : function (userId, traits) { + var identity = traits.email || userId; + if (identity) window._kiq.push(['identify', identity]); + if (traits) window._kiq.push(['set', traits]); + }, + + // Qualaroo doesn't have `track` method yet, but to allow the users to do + // targetted questionnaires we can set name-value pairs on the user properties + // that apply to the current visit. + track : function (event, properties) { + if (!this.options.track) return; + + // Create a name-value pair that will be pretty unique. For an event like + // 'Loaded a Page' this will make it 'Triggered: Loaded a Page'. + var traits = {}; + traits['Triggered: ' + event] = true; + + // Fire a normal identify, with traits only. + this.identify(null, traits); + } + +}); +}); +require.register("analytics/src/providers/quantcast.js", function(exports, require, module){ +// https://www.quantcast.com/learning-center/guides/using-the-quantcast-asynchronous-tag/ + +var Provider = require('../provider') + , load = require('load-script'); + + +module.exports = Provider.extend({ + + name : 'Quantcast', + + key : 'pCode', + + defaults : { + pCode : null + }, + + initialize : function (options, ready) { + window._qevents = window._qevents || []; + window._qevents.push({ qacct: options.pCode }); + load({ + http : 'http://edge.quantserve.com/quant.js', + https : 'https://secure.quantserve.com/quant.js' + }, ready); + } + +}); +}); +require.register("analytics/src/providers/sentry.js", function(exports, require, module){ +// http://raven-js.readthedocs.org/en/latest/config/index.html + +var Provider = require('../provider') + , load = require('load-script'); + + +module.exports = Provider.extend({ + + name : 'Sentry', + + key : 'config', + + defaults : { + config : null + }, + + initialize : function (options, ready) { + load('//d3nslu0hdya83q.cloudfront.net/dist/1.0/raven.min.js', function () { + // For now, Raven basically requires `install` to be called. + // https://github.com/getsentry/raven-js/blob/master/src/raven.js#L87 + window.Raven.config(options.config).install(); + ready(); + }); + }, + + identify : function (userId, traits) { + traits.id = userId; + window.Raven.setUser(traits); + }, + + // Raven will automatically use `captureMessage` if the error is a string. + log : function (error, properties) { + window.Raven.captureException(error, properties); + } + +}); +}); +require.register("analytics/src/providers/snapengage.js", function(exports, require, module){ +// http://help.snapengage.com/installation-guide-getting-started-in-a-snap/ + +var Provider = require('../provider') + , isEmail = require('is-email') + , load = require('load-script'); + + +module.exports = Provider.extend({ + + name : 'SnapEngage', + + key : 'apiKey', + + defaults : { + apiKey : null + }, + + initialize : function (options, ready) { + load('//commondatastorage.googleapis.com/code.snapengage.com/js/' + options.apiKey + '.js', ready); + }, + + // Set the email in the chat window if we have it. + identify : function (userId, traits, options) { + if (!traits.email) return; + window.SnapABug.setUserEmail(traits.email); + } + +}); +}); +require.register("analytics/src/providers/usercycle.js", function(exports, require, module){ +// http://docs.usercycle.com/javascript_api + +var Provider = require('../provider') + , load = require('load-script') + , user = require('../user'); + + +module.exports = Provider.extend({ + + name : 'USERcycle', + + key : 'key', + + defaults : { + key : null + }, + + initialize : function (options, ready) { + window._uc = window._uc || []; + window._uc.push(['_key', options.key]); + load('//api.usercycle.com/javascripts/track.js'); + + // USERcycle makes a queue, so it's ready immediately. + ready(); + }, + + identify : function (userId, traits) { + if (userId) window._uc.push(['uid', userId]); + + // USERcycle has a special "hidden" event that is used just for retention measurement. + // Lukas suggested on 6/4/2013 that we send traits on that event, since they use the + // the latest value of every event property as a "trait" + window._uc.push(['action', 'came_back', traits]); + }, + + track : function (event, properties) { + window._uc.push(['action', event, properties]); + } + +}); +}); +require.register("analytics/src/providers/userfox.js", function(exports, require, module){ +// https://www.userfox.com/docs/ + +var Provider = require('../provider') + , extend = require('extend') + , load = require('load-script') + , isEmail = require('is-email'); + + +module.exports = Provider.extend({ + + name : 'userfox', + + key : 'clientId', + + defaults : { + // userfox's required key. + clientId : null + }, + + initialize : function (options, ready) { + window._ufq = window._ufq || []; + load('//d2y71mjhnajxcg.cloudfront.net/js/userfox-stable.js'); + + // userfox creates its own queue, so we're ready right away. + ready(); + }, + + identify : function (userId, traits) { + if (!traits.email) return; + + // Initialize the library with the email now that we have it. + window._ufq.push(['init', { + clientId : this.options.clientId, + email : traits.email + }]); + + // Record traits to "track" if we have the required signup date `created`. + // userfox takes `signup_date` as a string of seconds since the epoch. + if (traits.created) { + traits.signup_date = (traits.created.getTime() / 1000).toString(); + delete traits.created; + window._ufq.push(['track', traits]); + } + } + +}); + +}); +require.register("analytics/src/providers/uservoice.js", function(exports, require, module){ +// http://feedback.uservoice.com/knowledgebase/articles/225-how-do-i-pass-custom-data-through-the-widget-and-i + +var Provider = require('../provider') + , load = require('load-script') + , alias = require('alias') + , clone = require('clone'); + + +module.exports = Provider.extend({ + + name : 'UserVoice', + + defaults : { + // These first two options are required. + widgetId : null, + forumId : null, + // Should we show the tab automatically? + showTab : true, + // There's tons of options for the tab. + mode : 'full', + primaryColor : '#cc6d00', + linkColor : '#007dbf', + defaultMode : 'support', + tabLabel : 'Feedback & Support', + tabColor : '#cc6d00', + tabPosition : 'middle-right', + tabInverted : false + }, + + initialize : function (options, ready) { + window.UserVoice = window.UserVoice || []; + load('//widget.uservoice.com/' + options.widgetId + '.js', ready); + + var optionsClone = clone(options); + alias(optionsClone, { + 'forumId' : 'forum_id', + 'primaryColor' : 'primary_color', + 'linkColor' : 'link_color', + 'defaultMode' : 'default_mode', + 'tabLabel' : 'tab_label', + 'tabColor' : 'tab_color', + 'tabPosition' : 'tab_position', + 'tabInverted' : 'tab_inverted' + }); + + // If we don't automatically show the tab, let them show it via + // javascript. This is the default name for the function in their snippet. + window.showClassicWidget = function (showWhat) { + window.UserVoice.push([showWhat || 'showLightbox', 'classic_widget', optionsClone]); + }; + + // If we *do* automatically show the tab, get on with it! + if (options.showTab) { + window.showClassicWidget('showTab'); + } + }, + + identify : function (userId, traits) { + // Pull the ID into traits. + traits.id = userId; + window.UserVoice.push(['setCustomFields', traits]); + } + +}); +}); +require.register("analytics/src/providers/vero.js", function(exports, require, module){ +// https://github.com/getvero/vero-api/blob/master/sections/js.md + +var Provider = require('../provider') + , isEmail = require('is-email') + , load = require('load-script'); + + +module.exports = Provider.extend({ + + name : 'Vero', + + key : 'apiKey', + + defaults : { + apiKey : null + }, + + initialize : function (options, ready) { + window._veroq = window._veroq || []; + window._veroq.push(['init', { api_key: options.apiKey }]); + load('//d3qxef4rp70elm.cloudfront.net/m.js'); + + // Vero creates a queue, so it's ready immediately. + ready(); + }, + + identify : function (userId, traits) { + // Don't do anything if we just have traits, because Vero + // requires a `userId`. + if (!userId || !traits.email) return; + + // Vero takes the `userId` as part of the traits object. + traits.id = userId; + + window._veroq.push(['user', traits]); + }, + + track : function (event, properties) { + window._veroq.push(['track', event, properties]); + } + +}); +}); +require.register("analytics/src/providers/visual-website-optimizer.js", function(exports, require, module){ +// http://v2.visualwebsiteoptimizer.com/tools/get_tracking_code.php +// http://visualwebsiteoptimizer.com/knowledge/integration-of-vwo-with-kissmetrics/ + +var each = require('each') + , inherit = require('inherit') + , nextTick = require('next-tick') + , Provider = require('../provider'); + + +/** + * Expose `VWO`. + */ + +module.exports = VWO; + + +/** + * `VWO` inherits from the generic `Provider`. + */ + +function VWO () { + Provider.apply(this, arguments); +} + +inherit(VWO, Provider); + + +/** + * Name. + */ + +VWO.prototype.name = 'Visual Website Optimizer'; + + +/** + * Default options. + */ + +VWO.prototype.defaults = { + // Whether to replay variations into other integrations as traits. + replay : true +}; + + +/** + * Initialize. + */ + +VWO.prototype.initialize = function (options, ready) { + if (options.replay) this.replay(); + ready(); +}; + + +/** + * Replay the experiments the user has seen as traits to all other integrations. + * Wait for the next tick to replay so that the `analytics` object and all of + * the integrations are fully initialized. + */ + +VWO.prototype.replay = function () { + var analytics = this.analytics; + nextTick(function () { + experiments(function (err, traits) { + if (traits) analytics.identify(traits); + }); + }); +}; + + +/** + * Get dictionary of experiment keys and variations. + * http://visualwebsiteoptimizer.com/knowledge/integration-of-vwo-with-kissmetrics/ + * + * @param {Function} callback Called with `err, experiments`. + * @return {Object} Dictionary of experiments and variations. + */ + +function experiments (callback) { + enqueue(function () { + var data = {}; + var ids = window._vwo_exp_ids; + if (!ids) return callback(); + each(ids, function (id) { + var name = variation(id); + if (name) data['Experiment: ' + id] = name; + }); + callback(null, data); + }); +} + + +/** + * Add a function to the VWO queue, creating one if it doesn't exist. + * + * @param {Function} fn Function to enqueue. + */ + +function enqueue (fn) { + window._vis_opt_queue || (window._vis_opt_queue = []); + window._vis_opt_queue.push(fn); +} + + +/** + * Get the chosen variation's name from an experiment `id`. + * http://visualwebsiteoptimizer.com/knowledge/integration-of-vwo-with-kissmetrics/ + * + * @param {String} id ID of the experiment to read. + * @return {String} Variation name. + */ + +function variation (id) { + var experiments = window._vwo_exp; + if (!experiments) return null; + var experiment = experiments[id]; + var variationId = experiment.combination_chosen; + return variationId ? experiment.comb_n[variationId] : null; +} +}); +require.register("analytics/src/providers/woopra.js", function(exports, require, module){ +// http://www.woopra.com/docs/setup/javascript-tracking/ + +var Provider = require('../provider') + , each = require('each') + , extend = require('extend') + , isEmail = require('is-email') + , load = require('load-script') + , type = require('type') + , user = require('../user'); + + +module.exports = Provider.extend({ + + name : 'Woopra', + + key : 'domain', + + defaults : { + domain : null + }, + + initialize : function (options, ready) { + // Woopra gives us a nice ready callback. + var self = this; + + window.woopraReady = function (tracker) { + tracker.setDomain(self.options.domain); + tracker.setIdleTimeout(300000); + + var userId = user.id() + , traits = user.traits(); + + addTraits(userId, traits, tracker); + tracker.track(); + + ready(); + return false; + }; + + load('//static.woopra.com/js/woopra.js'); + }, + + identify : function (userId, traits) { + // We aren't guaranteed a tracker. + if (!window.woopraTracker) return; + addTraits(userId, traits, window.woopraTracker); + }, + + track : function (event, properties) { + // We aren't guaranteed a tracker. + if (!window.woopraTracker) return; + + // Woopra takes its `event` as the `name` key. + properties || (properties = {}); + properties.name = event; + + window.woopraTracker.pushEvent(properties); + } + +}); + + +/** + * Convenience function for updating the userId and traits. + * + * @param {String} userId The user's ID. + * @param {Object} traits The user's traits. + * @param {Tracker} tracker The Woopra tracker object. + */ + +function addTraits (userId, traits, tracker) { + // Move a `userId` into `traits`. + if (userId) traits.id = userId; + each(traits, function (key, value) { + // Woopra seems to only support strings as trait values. + if ('string' === type(value)) tracker.addVisitorProperty(key, value); + }); +} +}); +require.alias("avetisk-defaults/index.js", "analytics/deps/defaults/index.js"); +require.alias("avetisk-defaults/index.js", "defaults/index.js"); + +require.alias("component-clone/index.js", "analytics/deps/clone/index.js"); +require.alias("component-clone/index.js", "clone/index.js"); +require.alias("component-type/index.js", "component-clone/deps/type/index.js"); + +require.alias("component-cookie/index.js", "analytics/deps/cookie/index.js"); +require.alias("component-cookie/index.js", "cookie/index.js"); + +require.alias("component-each/index.js", "analytics/deps/each/index.js"); +require.alias("component-each/index.js", "each/index.js"); +require.alias("component-type/index.js", "component-each/deps/type/index.js"); + +require.alias("component-event/index.js", "analytics/deps/event/index.js"); +require.alias("component-event/index.js", "event/index.js"); + +require.alias("component-inherit/index.js", "analytics/deps/inherit/index.js"); +require.alias("component-inherit/index.js", "inherit/index.js"); + +require.alias("component-object/index.js", "analytics/deps/object/index.js"); +require.alias("component-object/index.js", "object/index.js"); + +require.alias("component-querystring/index.js", "analytics/deps/querystring/index.js"); +require.alias("component-querystring/index.js", "querystring/index.js"); +require.alias("component-trim/index.js", "component-querystring/deps/trim/index.js"); + +require.alias("component-type/index.js", "analytics/deps/type/index.js"); +require.alias("component-type/index.js", "type/index.js"); + +require.alias("component-url/index.js", "analytics/deps/url/index.js"); +require.alias("component-url/index.js", "url/index.js"); + +require.alias("segmentio-after/index.js", "analytics/deps/after/index.js"); +require.alias("segmentio-after/index.js", "after/index.js"); + +require.alias("segmentio-alias/index.js", "analytics/deps/alias/index.js"); +require.alias("segmentio-alias/index.js", "alias/index.js"); + +require.alias("segmentio-bind-all/index.js", "analytics/deps/bind-all/index.js"); +require.alias("segmentio-bind-all/index.js", "analytics/deps/bind-all/index.js"); +require.alias("segmentio-bind-all/index.js", "bind-all/index.js"); +require.alias("component-bind/index.js", "segmentio-bind-all/deps/bind/index.js"); + +require.alias("component-type/index.js", "segmentio-bind-all/deps/type/index.js"); + +require.alias("segmentio-bind-all/index.js", "segmentio-bind-all/index.js"); + +require.alias("segmentio-canonical/index.js", "analytics/deps/canonical/index.js"); +require.alias("segmentio-canonical/index.js", "canonical/index.js"); + +require.alias("segmentio-extend/index.js", "analytics/deps/extend/index.js"); +require.alias("segmentio-extend/index.js", "extend/index.js"); + +require.alias("segmentio-is-email/index.js", "analytics/deps/is-email/index.js"); +require.alias("segmentio-is-email/index.js", "is-email/index.js"); + +require.alias("segmentio-is-meta/index.js", "analytics/deps/is-meta/index.js"); +require.alias("segmentio-is-meta/index.js", "is-meta/index.js"); + +require.alias("segmentio-json/index.js", "analytics/deps/json/index.js"); +require.alias("segmentio-json/index.js", "json/index.js"); +require.alias("component-json-fallback/index.js", "segmentio-json/deps/json-fallback/index.js"); + +require.alias("segmentio-load-date/index.js", "analytics/deps/load-date/index.js"); +require.alias("segmentio-load-date/index.js", "load-date/index.js"); + +require.alias("segmentio-load-script/index.js", "analytics/deps/load-script/index.js"); +require.alias("segmentio-load-script/index.js", "load-script/index.js"); +require.alias("component-type/index.js", "segmentio-load-script/deps/type/index.js"); + +require.alias("segmentio-new-date/index.js", "analytics/deps/new-date/index.js"); +require.alias("segmentio-new-date/index.js", "new-date/index.js"); +require.alias("component-type/index.js", "segmentio-new-date/deps/type/index.js"); + +require.alias("segmentio-on-body/index.js", "analytics/deps/on-body/index.js"); +require.alias("segmentio-on-body/index.js", "on-body/index.js"); +require.alias("component-each/index.js", "segmentio-on-body/deps/each/index.js"); +require.alias("component-type/index.js", "component-each/deps/type/index.js"); + +require.alias("segmentio-store.js/store.js", "analytics/deps/store/store.js"); +require.alias("segmentio-store.js/store.js", "analytics/deps/store/index.js"); +require.alias("segmentio-store.js/store.js", "store/index.js"); +require.alias("segmentio-json/index.js", "segmentio-store.js/deps/json/index.js"); +require.alias("component-json-fallback/index.js", "segmentio-json/deps/json-fallback/index.js"); + +require.alias("segmentio-store.js/store.js", "segmentio-store.js/index.js"); + +require.alias("segmentio-top-domain/index.js", "analytics/deps/top-domain/index.js"); +require.alias("segmentio-top-domain/index.js", "analytics/deps/top-domain/index.js"); +require.alias("segmentio-top-domain/index.js", "top-domain/index.js"); +require.alias("component-url/index.js", "segmentio-top-domain/deps/url/index.js"); + +require.alias("segmentio-top-domain/index.js", "segmentio-top-domain/index.js"); + +require.alias("timoxley-next-tick/index.js", "analytics/deps/next-tick/index.js"); +require.alias("timoxley-next-tick/index.js", "next-tick/index.js"); + +require.alias("yields-prevent/index.js", "analytics/deps/prevent/index.js"); +require.alias("yields-prevent/index.js", "prevent/index.js"); + +require.alias("analytics/src/index.js", "analytics/index.js"); + +if (typeof exports == "object") { + module.exports = require("analytics"); +} else if (typeof define == "function" && define.amd) { + define(function(){ return require("analytics"); }); +} else { + this["analytics"] = require("analytics"); +}})(); \ No newline at end of file diff --git a/common/templates/jasmine/jasmine_test_runner.html.erb b/common/templates/jasmine/jasmine_test_runner.html.erb index 9ea6ef42a3..cf5885ce2c 100644 --- a/common/templates/jasmine/jasmine_test_runner.html.erb +++ b/common/templates/jasmine/jasmine_test_runner.html.erb @@ -27,6 +27,7 @@ + - <%block name="title">About ${course.number}
@@ -92,7 +115,7 @@ - +
diff --git a/lms/templates/extauth_failure.html b/lms/templates/extauth_failure.html index fa53ab1084..330c63e604 100644 --- a/lms/templates/extauth_failure.html +++ b/lms/templates/extauth_failure.html @@ -2,10 +2,10 @@ "http://www.w3.org/TR/html4/strict.dtd"> - OpenID failed + External Authentication failed -

OpenID failed

+

External Authentication failed

${message}

diff --git a/lms/templates/navigation.html b/lms/templates/navigation.html index 190a58f691..a26e1ca367 100644 --- a/lms/templates/navigation.html +++ b/lms/templates/navigation.html @@ -95,16 +95,26 @@ site_status_msg = get_site_status_msg(course_id) % endif % if not settings.MITX_FEATURES['DISABLE_LOGIN_BUTTON']: - + % if course and settings.MITX_FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD') and course.enrollment_domain: + + % else: + + % endif % endif diff --git a/lms/templates/register.html b/lms/templates/register.html index 73a6df9319..2cad6955eb 100644 --- a/lms/templates/register.html +++ b/lms/templates/register.html @@ -136,16 +136,37 @@ % else:
-

Welcome ${extauth_email}

+

Welcome ${extauth_id}

Enter a public username:

    + + % if ask_for_email: + +
  1. + + +
  2. + + % endif +
  3. Will be shown in any discussions or forums you participate in
  4. + + % if ask_for_fullname: + +
  5. + + + Needed for any certificates you may earn (cannot be changed later) +
  6. + + % endif +
% endif @@ -246,6 +267,8 @@

Registration Help

+ % if has_extauth_info is UNDEFINED: +

Already registered?

@@ -254,6 +277,8 @@

+ + % endif ## TODO: Use a %block tag or something to allow themes to ## override in a more generalizable fashion. diff --git a/lms/templates/signup_modal.html b/lms/templates/signup_modal.html index a68e36e902..9c1a868e2d 100644 --- a/lms/templates/signup_modal.html +++ b/lms/templates/signup_modal.html @@ -32,11 +32,23 @@ % else: -

Welcome ${extauth_email}


+

Welcome ${extauth_id}


Enter a public username:

- + - + + + % if ask_for_email: + + + % endif + + + % if ask_for_fullname: + + + % endif + % endif diff --git a/lms/urls.py b/lms/urls.py index 1d34ebf3af..6a4819aedb 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -363,6 +363,21 @@ if settings.MITX_FEATURES.get('AUTH_USE_OPENID'): url(r'^openid/logo.gif$', 'django_openid_auth.views.logo', name='openid-logo'), ) +if settings.MITX_FEATURES.get('AUTH_USE_SHIB'): + urlpatterns += ( + url(r'^shib-login/$', 'external_auth.views.shib_login', name='shib-login'), + ) + +if settings.MITX_FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD'): + urlpatterns += ( + url(r'^course_specific_login/(?P[^/]+/[^/]+/[^/]+)/$', + 'external_auth.views.course_specific_login', name='course-specific-login'), + url(r'^course_specific_register/(?P[^/]+/[^/]+/[^/]+)/$', + 'external_auth.views.course_specific_register', name='course-specific-register'), + + ) + + if settings.MITX_FEATURES.get('AUTH_USE_OPENID_PROVIDER'): urlpatterns += ( url(r'^openid/provider/login/$', 'external_auth.views.provider_login', name='openid-provider-login'), diff --git a/lms/wsgi_apache.py b/lms/wsgi_apache_lms.py similarity index 53% rename from lms/wsgi_apache.py rename to lms/wsgi_apache_lms.py index e2d8a23dc0..0f9950ca41 100644 --- a/lms/wsgi_apache.py +++ b/lms/wsgi_apache_lms.py @@ -1,15 +1,12 @@ import os os.environ.setdefault("DJANGO_SETTINGS_MODULE", "lms.envs.aws") +os.environ.setdefault("SERVICE_VARIANT", "lms") + # This application object is used by the development server # as well as any WSGI server configured to use this file. -from django.core.wsgi import WSGIHandler -_application = WSGIHandler() - -def application(environ, start_response): - #copy SERVICE_VARIANT from apache environ to os environ - os.environ.setdefault("SERVICE_VARIANT", environ.get("SERVICE_VARIANT", "lms")) - return _application(environ, start_response) +from django.core.wsgi import get_wsgi_application +application = get_wsgi_application() from django.conf import settings from xmodule.modulestore.django import modulestore From a39a384ed22e9fe82e10293b146c1b14a3f7e787 Mon Sep 17 00:00:00 2001 From: Jason Bau Date: Fri, 14 Jun 2013 15:07:44 -0700 Subject: [PATCH 211/375] Handle the case where an existing user has email returned by shib By linking the users --- common/djangoapps/external_auth/views.py | 39 +++++++++++++++++++++--- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/common/djangoapps/external_auth/views.py b/common/djangoapps/external_auth/views.py index 8288b27ec9..d4a0b56293 100644 --- a/common/djangoapps/external_auth/views.py +++ b/common/djangoapps/external_auth/views.py @@ -147,11 +147,42 @@ def external_login_or_signup(request, internal_user = eamap.user if internal_user is None: - log.debug('No user for %s yet, doing signup' % eamap.external_email) - return signup(request, eamap) + if settings.MITX_FEATURES.get('AUTH_USE_SHIB'): + # if we are using shib, try to link accounts using email + try: + link_user = User.objects.get(email=eamap.external_email) + if not ExternalAuthMap.objects.filter(user=link_user).exists(): + # if there's no pre-existing linked eamap, we link the user + eamap.user = link_user + eamap.save() + internal_user = link_user + log.debug('Linking existing account for %s' % eamap.external_email) + # now pass through to log in + else: + # otherwise, set external_email to '' to ask for a new one at user signup + eamap.external_email = '' + eamap.save() + log.debug('User with external login found for %s, asking for new email during signup' % email) + return signup(request, eamap) + except User.DoesNotExist: + log.debug('No user for %s yet, doing signup' % eamap.external_email) + return signup(request, eamap) + else: + log.debug('No user for %s yet, doing signup' % eamap.external_email) + return signup(request, eamap) - uname = internal_user.username - user = authenticate(username=uname, password=eamap.internal_password) + # We trust shib's authentication, so no need to authenticate using the password again + if settings.MITX_FEATURES.get('AUTH_USE_SHIB'): + user = internal_user + # Assuming this 'AUTHENTICATION_BACKENDS' is set in settings, which I think is safe + if settings.AUTHENTICATION_BACKENDS: + auth_backend = settings.AUTHENTICATION_BACKENDS[0] + else: + auth_backend = 'django.contrib.auth.backends.ModelBackend' + user.backend = auth_backend + else: + uname = internal_user.username + user = authenticate(username=uname, password=eamap.internal_password) if user is None: log.warning("External Auth Login failed for %s / %s" % (uname, eamap.internal_password)) From ca649d3c33a8c660d6bf06eba75d17649b311589 Mon Sep 17 00:00:00 2001 From: Jason Bau Date: Fri, 14 Jun 2013 22:12:35 -0700 Subject: [PATCH 212/375] Turn off Agreement to Terms of Service for Stanford shib As stipulated by Stanford's office of general counsel --- common/djangoapps/external_auth/views.py | 5 +++++ common/djangoapps/student/views.py | 20 +++++++++++++++----- lms/templates/register.html | 5 +++++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/common/djangoapps/external_auth/views.py b/common/djangoapps/external_auth/views.py index d4a0b56293..097cdefe77 100644 --- a/common/djangoapps/external_auth/views.py +++ b/common/djangoapps/external_auth/views.py @@ -239,8 +239,13 @@ def signup(request, eamap=None): 'extauth_email': eamap.external_email, 'extauth_username': username, 'extauth_name': eamap.external_name, + 'ask_for_tos': True, } + # Can't have terms of service for Stanford users, according to Stanford's Office of General Counsel + if settings.MITX_FEATURES['AUTH_USE_SHIB'] and ('stanford' in eamap.external_domain): + context['ask_for_tos'] = False + # detect if full name is blank and ask for it from user context['ask_for_fullname'] = eamap.external_name.strip() == '' diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 98587cd782..8bc29fa671 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -613,17 +613,27 @@ def create_account(request, post_override=None): js['field'] = 'honor_code' return HttpResponse(json.dumps(js)) - if post_vars.get('terms_of_service', 'false') != u'true': - js['value'] = "You must accept the terms of service.".format(field=a) - js['field'] = 'terms_of_service' - return HttpResponse(json.dumps(js)) + # Can't have terms of service for Stanford users, according to Stanford's Office of General Counsel + if settings.MITX_FEATURES.get("AUTH_USE_SHIB") and DoExternalAuth and ("stanford" in eamap.external_domain): + pass + else: + if post_vars.get('terms_of_service', 'false') != u'true': + js['value'] = "You must accept the terms of service.".format(field=a) + js['field'] = 'terms_of_service' + return HttpResponse(json.dumps(js)) # Confirm appropriate fields are there. # TODO: Check e-mail format is correct. # TODO: Confirm e-mail is not from a generic domain (mailinator, etc.)? Not sure if # this is a good idea # TODO: Check password is sane - for a in ['username', 'email', 'name', 'password', 'terms_of_service', 'honor_code']: + + required_post_vars = ['username', 'email', 'name', 'password', 'terms_of_service', 'honor_code'] + if settings.MITX_FEATURES.get("AUTH_USE_SHIB") and DoExternalAuth and ("stanford" in eamap.external_domain): + # Can't have terms of service for Stanford users, according to Stanford's Office of General Counsel + required_post_vars = ['username', 'email', 'name', 'password', 'honor_code'] + + for a in required_post_vars: if len(post_vars[a]) < 2: error_str = {'username': 'Username must be minimum of two characters long.', 'email': 'A properly formatted e-mail is required.', diff --git a/lms/templates/register.html b/lms/templates/register.html index 2cad6955eb..1a42d402e5 100644 --- a/lms/templates/register.html +++ b/lms/templates/register.html @@ -231,11 +231,16 @@
  1. + + % if has_extauth_info is UNDEFINED or ask_for_tos : +
    + % endif +
    <% From 084160c1c9b76e0c09eb6221591503f9e1b1e3f2 Mon Sep 17 00:00:00 2001 From: Jason Bau Date: Wed, 19 Jun 2013 00:16:41 -0700 Subject: [PATCH 213/375] Finishing up tests/modifications per @ormsbee feedback --- .../external_auth/tests/test_shib.py | 196 +++++++++++------- common/djangoapps/external_auth/views.py | 49 +++-- common/djangoapps/student/views.py | 7 +- 3 files changed, 148 insertions(+), 104 deletions(-) diff --git a/common/djangoapps/external_auth/tests/test_shib.py b/common/djangoapps/external_auth/tests/test_shib.py index f342aa4c74..e5059e5635 100644 --- a/common/djangoapps/external_auth/tests/test_shib.py +++ b/common/djangoapps/external_auth/tests/test_shib.py @@ -1,3 +1,4 @@ +# coding=utf-8 """ Tests for Shibboleth Authentication @jbau @@ -6,11 +7,12 @@ import unittest from django.conf import settings from django.http import HttpResponseRedirect -from django.test.client import RequestFactory +from django.test.client import RequestFactory, Client as DjangoTestClient from django.test.utils import override_settings from django.core.urlresolvers import reverse from django.contrib.auth.models import AnonymousUser, User from django.contrib.sessions.backends.base import SessionBase +from django.utils.importlib import import_module from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase @@ -34,23 +36,27 @@ from student.tests.factories import UserFactory IDP = 'https://idp.stanford.edu/' REMOTE_USER = 'test_user@stanford.edu' MAILS = [None, '', 'test_user@stanford.edu'] -GIVENNAMES = [None, '', 'Jason', 'jason; John; bob'] # At Stanford, the givenNames can be a list delimited by ';' -SNS = [None, '', 'Bau', 'bau; smith'] # At Stanford, the sns can be a list delimited by ';' +GIVENNAMES = [None, '', 'Jason', 'jasön; John; bob'] # At Stanford, the givenNames can be a list delimited by ';' +SNS = [None, '', 'Bau', '包; smith'] # At Stanford, the sns can be a list delimited by ';' def gen_all_identities(): - """A generator for all combinations of identity inputs""" + """ + A generator for all combinations of test inputs. + Each generated item is a dict that represents what a shib IDP + could potentially pass to django via request.META, i.e. + setting (or not) request.META['givenName'], etc. + """ def _build_identity_dict(mail, given_name, surname): """ Helper function to return a dict of test identity """ - meta_dict = {} - meta_dict.update({'Shib-Identity-Provider': IDP, - 'REMOTE_USER': REMOTE_USER}) + meta_dict = {'Shib-Identity-Provider': IDP, + 'REMOTE_USER': REMOTE_USER} if mail is not None: - meta_dict.update({'mail': mail}) + meta_dict['mail'] = mail if given_name is not None: - meta_dict.update({'givenName': given_name}) + meta_dict['givenName'] = given_name if surname is not None: - meta_dict.update({'sn': surname}) + meta_dict['sn'] = surname return meta_dict for mail in MAILS: @@ -59,48 +65,84 @@ def gen_all_identities(): yield _build_identity_dict(mail, given_name, surname) -@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE, SESSION_ENGINE='django.contrib.sessions.backends.cache') class ShibSPTest(ModuleStoreTestCase): """ Tests for the Shibboleth SP, which communicates via request.META (Apache environment variables set by mod_shib) """ - factory = RequestFactory() + request_factory = RequestFactory() def setUp(self): self.store = modulestore() + @unittest.skipUnless(settings.MITX_FEATURES.get('AUTH_USE_SHIB'), True) + def test_exception_shib_login(self): + """ + Tests that we get the error page when there is no REMOTE_USER + or Shib-Identity-Provider in request.META + """ + no_remote_user_request = self.request_factory.get('/shib-login') + no_remote_user_request.META.update({'Shib-Identity-Provider': IDP}) + no_remote_user_response = shib_login(no_remote_user_request) + self.assertEqual(no_remote_user_response.status_code, 403) + self.assertIn("identity server did not return your ID information", no_remote_user_response.content) + + no_idp_request = self.request_factory.get('/shib-login') + no_idp_request.META.update({'REMOTE_USER': REMOTE_USER}) + no_idp_response = shib_login(no_idp_request) + self.assertEqual(no_idp_response.status_code, 403) + self.assertIn("identity server did not return your ID information", no_idp_response.content) + + @unittest.skipUnless(settings.MITX_FEATURES.get('AUTH_USE_SHIB'), True) def test_shib_login(self): """ - Tests that a user with a shib ExternalAuthMap gets logged in while when - shib-login is called, while a user without such gets the registration form. + Tests that: + * shib credentials that match an existing ExternalAuthMap with a linked user logs the user in + * shib credentials that match an existing ExternalAuthMap without a linked user and also match the email + of an existing user without an existing ExternalAuthMap links the two and log the user in + * shib credentials that match an existing ExternalAuthMap without a linked user and also match the email + of an existing user that already has an ExternalAuthMap causes an error (403) + * shib credentials that do not match an existing ExternalAuthMap causes the registration form to appear """ - student = UserFactory.create() - extauth = ExternalAuthMap(external_id='testuser@stanford.edu', + user_w_map = UserFactory.create(email='withmap@stanford.edu') + extauth = ExternalAuthMap(external_id='withmap@stanford.edu', external_email='', external_domain='shib:https://idp.stanford.edu/', external_credentials="", - user=student) - student.save() + user=user_w_map) + user_wo_map = UserFactory.create(email='womap@stanford.edu') + user_w_map.save() + user_wo_map.save() extauth.save() idps = ['https://idp.stanford.edu/', 'https://someother.idp.com/'] - remote_users = ['testuser@stanford.edu', 'testuser2@someother_idp.com'] + remote_users = ['withmap@stanford.edu', 'womap@stanford.edu', 'testuser2@someother_idp.com'] for idp in idps: for remote_user in remote_users: - request = self.factory.get('/shib-login') - request.session = SessionBase() # empty session + request = self.request_factory.get('/shib-login') + request.session = import_module(settings.SESSION_ENGINE).SessionStore() # empty session request.META.update({'Shib-Identity-Provider': idp, - 'REMOTE_USER': remote_user}) + 'REMOTE_USER': remote_user, + 'mail': remote_user}) request.user = AnonymousUser() response = shib_login(request) - if idp == "https://idp.stanford.edu" and remote_user == 'testuser@stanford.edu': + if idp == "https://idp.stanford.edu/" and remote_user == 'withmap@stanford.edu': self.assertIsInstance(response, HttpResponseRedirect) - self.assertEqual(request.user, student) + self.assertEqual(request.user, user_w_map) self.assertEqual(response['Location'], '/') + elif idp == "https://idp.stanford.edu/" and remote_user == 'womap@stanford.edu': + self.assertIsNotNone(ExternalAuthMap.objects.get(user=user_wo_map)) + self.assertIsInstance(response, HttpResponseRedirect) + self.assertEqual(request.user, user_wo_map) + self.assertEqual(response['Location'], '/') + elif idp == "https://someother.idp.com/" and remote_user in \ + ['withmap@stanford.edu', 'womap@stanford.edu']: + self.assertEqual(response.status_code, 403) + self.assertIn("You have already created an account using an external login", response.content) else: self.assertEqual(response.status_code, 200) self.assertContains(response, "Register for") @@ -113,10 +155,9 @@ class ShibSPTest(ModuleStoreTestCase): Uses django test client for its session support """ for identity in gen_all_identities(): - self.client.logout() - request_kwargs = {'path': '/shib-login/', 'data': {}, 'follow': False} - request_kwargs.update(identity) - response = self.client.get(**request_kwargs) # identity k/v pairs will show up in request.META + client = DjangoTestClient() + # identity k/v pairs will show up in request.META + response = client.get(path='/shib-login/', data={}, follow=False, **identity) self.assertEquals(response.status_code, 200) mail_input_HTML = '<input class="" id="email" type="email" name="email"' @@ -124,8 +165,8 @@ class ShibSPTest(ModuleStoreTestCase): self.assertContains(response, mail_input_HTML) else: self.assertNotContains(response, mail_input_HTML) - sn_empty = identity.get('sn', '') == '' - given_name_empty = identity.get('givenName', '') == '' + sn_empty = not identity.get('sn') + given_name_empty = not identity.get('givenName') fullname_input_HTML = '<input id="name" type="text" name="name"' if sn_empty and given_name_empty: self.assertContains(response, fullname_input_HTML) @@ -133,7 +174,7 @@ class ShibSPTest(ModuleStoreTestCase): self.assertNotContains(response, fullname_input_HTML) #clean up b/c we don't want existing ExternalAuthMap for the next run - self.client.session['ExternalAuthMap'].delete() + client.session['ExternalAuthMap'].delete() @unittest.skipUnless(settings.MITX_FEATURES.get('AUTH_USE_SHIB'), True) def test_registration_formSubmit(self): @@ -146,10 +187,8 @@ class ShibSPTest(ModuleStoreTestCase): """ for identity in gen_all_identities(): #First we pop the registration form - self.client.logout() - request1_kwargs = {'path': '/shib-login/', 'data': {}, 'follow': False} - request1_kwargs.update(identity) - response1 = self.client.get(**request1_kwargs) + client = DjangoTestClient() + response1 = client.get(path='/shib-login/', data={}, follow=False, **identity) #Then we have the user answer the registration form postvars = {'email': 'post_email@stanford.edu', 'username': 'post_username', @@ -158,8 +197,8 @@ class ShibSPTest(ModuleStoreTestCase): 'terms_of_service': 'true', 'honor_code': 'true'} #use RequestFactory instead of TestClient here because we want access to request.user - request2 = self.factory.post('/create_account', data=postvars) - request2.session = self.client.session + request2 = self.request_factory.post('/create_account', data=postvars) + request2.session = client.session request2.user = AnonymousUser() response2 = create_account(request2) @@ -177,13 +216,12 @@ class ShibSPTest(ModuleStoreTestCase): #check that the created user profile has the right name, either taken from shib or user input profile = UserProfile.objects.get(user=user) - sn_empty = identity.get('sn', '') == '' - given_name_empty = identity.get('givenName', '') == '' + sn_empty = not identity.get('sn') + given_name_empty = not identity.get('givenName') if sn_empty and given_name_empty: self.assertEqual(profile.name, postvars['name']) else: self.assertEqual(profile.name, request2.session['ExternalAuthMap'].external_name) - #clean up for next loop request2.session['ExternalAuthMap'].delete() UserProfile.objects.filter(user=user).delete() @@ -206,12 +244,12 @@ class ShibSPTest(ModuleStoreTestCase): self.store.update_metadata(course.location.url(), metadata) #setting location to test that GET params get passed through - login_request = self.factory.get('/course_specific_login/MITx/999/Robot_Super_Course' + - '?course_id=MITx/999/Robot_Super_Course' + - '&enrollment_action=enroll') - reg_request = self.factory.get('/course_specific_register/MITx/999/Robot_Super_Course' + - '?course_id=MITx/999/course/Robot_Super_Course' + - '&enrollment_action=enroll') + login_request = self.request_factory.get('/course_specific_login/MITx/999/Robot_Super_Course' + + '?course_id=MITx/999/Robot_Super_Course' + + '&enrollment_action=enroll') + reg_request = self.request_factory.get('/course_specific_register/MITx/999/Robot_Super_Course' + + '?course_id=MITx/999/course/Robot_Super_Course' + + '&enrollment_action=enroll') login_response = course_specific_login(login_request, 'MITx/999/Robot_Super_Course') reg_response = course_specific_register(login_request, 'MITx/999/Robot_Super_Course') @@ -241,12 +279,12 @@ class ShibSPTest(ModuleStoreTestCase): # Now test for non-existent course #setting location to test that GET params get passed through - login_request = self.factory.get('/course_specific_login/DNE/DNE/DNE' + - '?course_id=DNE/DNE/DNE' + - '&enrollment_action=enroll') - reg_request = self.factory.get('/course_specific_register/DNE/DNE/DNE' + - '?course_id=DNE/DNE/DNE/Robot_Super_Course' + - '&enrollment_action=enroll') + login_request = self.request_factory.get('/course_specific_login/DNE/DNE/DNE' + + '?course_id=DNE/DNE/DNE' + + '&enrollment_action=enroll') + reg_request = self.request_factory.get('/course_specific_register/DNE/DNE/DNE' + + '?course_id=DNE/DNE/DNE/Robot_Super_Course' + + '&enrollment_action=enroll') login_response = course_specific_login(login_request, 'DNE/DNE/DNE') reg_response = course_specific_register(login_request, 'DNE/DNE/DNE') @@ -270,54 +308,54 @@ class ShibSPTest(ModuleStoreTestCase): """ #create 2 course, one with limited enrollment one without - course1 = CourseFactory.create(org='Stanford', number='123', display_name='Shib Only') - course1.enrollment_domain = 'shib:https://idp.stanford.edu/' - metadata = own_metadata(course1) - metadata['enrollment_domain'] = course1.enrollment_domain - self.store.update_metadata(course1.location.url(), metadata) + shib_course = CourseFactory.create(org='Stanford', number='123', display_name='Shib Only') + shib_course.enrollment_domain = 'shib:https://idp.stanford.edu/' + metadata = own_metadata(shib_course) + metadata['enrollment_domain'] = shib_course.enrollment_domain + self.store.update_metadata(shib_course.location.url(), metadata) - course2 = CourseFactory.create(org='MITx', number='999', display_name='Robot Super Course') - course2.enrollment_domain = '' - metadata = own_metadata(course2) - metadata['enrollment_domain'] = course2.enrollment_domain - self.store.update_metadata(course2.location.url(), metadata) + open_enroll_course = CourseFactory.create(org='MITx', number='999', display_name='Robot Super Course') + open_enroll_course.enrollment_domain = '' + metadata = own_metadata(open_enroll_course) + metadata['enrollment_domain'] = open_enroll_course.enrollment_domain + self.store.update_metadata(open_enroll_course.location.url(), metadata) - # create 3 kinds of students, external_auth matching course1, external_auth not matching, no external auth - student1 = UserFactory.create() - student1.save() + # create 3 kinds of students, external_auth matching shib_course, external_auth not matching, no external auth + shib_student = UserFactory.create() + shib_student.save() extauth = ExternalAuthMap(external_id='testuser@stanford.edu', external_email='', external_domain='shib:https://idp.stanford.edu/', external_credentials="", - user=student1) + user=shib_student) extauth.save() - student2 = UserFactory.create() - student2.username = "teststudent2" - student2.email = "teststudent2@other.edu" - student2.save() + other_ext_student = UserFactory.create() + other_ext_student.username = "teststudent2" + other_ext_student.email = "teststudent2@other.edu" + other_ext_student.save() extauth = ExternalAuthMap(external_id='testuser1@other.edu', external_email='', external_domain='shib:https://other.edu/', external_credentials="", - user=student2) + user=other_ext_student) extauth.save() - student3 = UserFactory.create() - student3.username = "teststudent3" - student3.email = "teststudent3@gmail.com" - student3.save() + int_student = UserFactory.create() + int_student.username = "teststudent3" + int_student.email = "teststudent3@gmail.com" + int_student.save() #Tests the two case for courses, limited and not - for course in [course1, course2]: - for student in [student1, student2, student3]: - request = self.factory.post('/change_enrollment') + for course in [shib_course, open_enroll_course]: + for student in [shib_student, other_ext_student, int_student]: + request = self.request_factory.post('/change_enrollment') request.POST.update({'enrollment_action': 'enroll', 'course_id': course.id}) request.user = student response = change_enrollment(request) #if course is not limited or student has correct shib extauth then enrollment should be allowed - if course is course2 or student is student1: + if course is open_enroll_course or student is shib_student: self.assertEqual(response.status_code, 200) self.assertEqual(CourseEnrollment.objects.filter(user=student, course_id=course.id).count(), 1) #clean up diff --git a/common/djangoapps/external_auth/views.py b/common/djangoapps/external_auth/views.py index 097cdefe77..1ae8edfc52 100644 --- a/common/djangoapps/external_auth/views.py +++ b/common/djangoapps/external_auth/views.py @@ -145,6 +145,7 @@ def external_login_or_signup(request, eamap.save() + log.info("External_Auth login_or_signup for %s : %s : %s : %s" % (external_domain, external_id, email, fullname)) internal_user = eamap.user if internal_user is None: if settings.MITX_FEATURES.get('AUTH_USE_SHIB'): @@ -156,19 +157,21 @@ def external_login_or_signup(request, eamap.user = link_user eamap.save() internal_user = link_user - log.debug('Linking existing account for %s' % eamap.external_email) + log.info('SHIB: Linking existing account for %s' % eamap.external_email) # now pass through to log in else: - # otherwise, set external_email to '' to ask for a new one at user signup - eamap.external_email = '' - eamap.save() - log.debug('User with external login found for %s, asking for new email during signup' % email) - return signup(request, eamap) + # otherwise, there must have been an error, b/c we've already linked a user with these external + # creds + failure_msg = _(dedent(""" + You have already created an account using an external login like WebAuth or Shibboleth. + Please contact %s for support """ + % getattr(settings, 'TECH_SUPPORT_EMAIL', 'techsupport@class.stanford.edu'))) + return default_render_failure(request, failure_msg) except User.DoesNotExist: - log.debug('No user for %s yet, doing signup' % eamap.external_email) + log.info('SHIB: No user for %s yet, doing signup' % eamap.external_email) return signup(request, eamap) else: - log.debug('No user for %s yet, doing signup' % eamap.external_email) + log.info('No user for %s yet, doing signup' % eamap.external_email) return signup(request, eamap) # We trust shib's authentication, so no need to authenticate using the password again @@ -180,6 +183,7 @@ def external_login_or_signup(request, else: auth_backend = 'django.contrib.auth.backends.ModelBackend' user.backend = auth_backend + log.info('SHIB: Logging in linked user %s' % user.email) else: uname = internal_user.username user = authenticate(username=uname, password=eamap.internal_password) @@ -193,14 +197,13 @@ def external_login_or_signup(request, # TODO: improve error page msg = 'Account not yet activated: please look for link in your email' return default_render_failure(request, msg) - login(request, user) request.session.set_expiry(0) # Now to try enrollment # Need to special case Shibboleth here because it logs in via a GET. # testing request.method for extra paranoia - if 'shib:' in external_domain and request.method == 'GET': + if settings.MITX_FEATURES.get('AUTH_USE_SHIB') and 'shib:' in external_domain and request.method == 'GET': enroll_request = make_shib_enrollment_request(request) student_views.try_change_enrollment(enroll_request) else: @@ -256,7 +259,7 @@ def signup(request, eamap=None): except ValidationError: context['ask_for_email'] = True - log.debug('Doing signup for %s' % eamap.external_email) + log.info('EXTAUTH: Doing signup for %s' % eamap.external_id) return student_views.register_user(request, extra_context=context) @@ -370,7 +373,7 @@ def ssl_login(request): # ----------------------------------------------------------------------------- # Shibboleth (Stanford and others. Uses *Apache* environment variables) # ----------------------------------------------------------------------------- -def shib_login(request, retfun=None): +def shib_login(request): """ Uses Apache's REMOTE_USER environment variable as the external id. This in turn typically uses EduPersonPrincipalName @@ -384,29 +387,31 @@ def shib_login(request, retfun=None): """)) if not request.META.get('REMOTE_USER'): + log.exception("SHIB: no REMOTE_USER found in request.META") + return default_render_failure(request, shib_error_msg) + elif not request.META.get('Shib-Identity-Provider'): + log.exception("SHIB: no Shib-Identity-Provider in request.META") return default_render_failure(request, shib_error_msg) else: #if we get here, the user has authenticated properly - attrs = ['REMOTE_USER', 'givenName', 'sn', 'mail', - 'Shib-Identity-Provider'] - shib = {} - - for attr in attrs: - shib[attr] = request.META.get(attr, '') + shib = {attr: request.META.get(attr, '') + for attr in ['REMOTE_USER', 'givenName', 'sn', 'mail', 'Shib-Identity-Provider']} #Clean up first name, last name, and email address #TODO: Make this less hardcoded re: format, but split will work #even if ";" is not present since we are accessing 1st element - shib['sn'] = shib['sn'].split(";")[0].strip().capitalize() - shib['givenName'] = shib['givenName'].split(";")[0].strip().capitalize() + shib['sn'] = shib['sn'].split(";")[0].strip().capitalize().decode('utf-8') + shib['givenName'] = shib['givenName'].split(";")[0].strip().capitalize().decode('utf-8') + + log.info("SHIB creds returned: %r" % shib) return external_login_or_signup(request, external_id=shib['REMOTE_USER'], external_domain="shib:" + shib['Shib-Identity-Provider'], credentials=shib, email=shib['mail'], - fullname="%s %s" % (shib['givenName'], shib['sn']), - retfun=retfun) + fullname=u'%s %s' % (shib['givenName'], shib['sn']), + ) def make_shib_enrollment_request(request): diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 8bc29fa671..0aac873c03 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -599,7 +599,7 @@ def create_account(request, post_override=None): password = eamap.internal_password post_vars = dict(post_vars.items()) post_vars.update(dict(email=email, name=name, password=password)) - log.debug('extauth test: post_vars = %s' % post_vars) + log.info('In create_account with external_auth: post_vars = %s' % post_vars) # Confirm we have a properly formed request for a in ['username', 'email', 'password', 'name']: @@ -699,10 +699,11 @@ def create_account(request, post_override=None): eamap.user = login_user eamap.dtsignup = datetime.datetime.now(UTC) eamap.save() - log.debug('Updated ExternalAuthMap for %s to be %s' % (post_vars['username'], eamap)) + log.info("User registered with external_auth %s" % post_vars['username']) + log.info('Updated ExternalAuthMap for %s to be %s' % (post_vars['username'], eamap)) if settings.MITX_FEATURES.get('BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH'): - log.debug('bypassing activation email') + log.info('bypassing activation email') login_user.is_active = True login_user.save() From 03605ab686f1e567a0c9ac5868f701039ffc6123 Mon Sep 17 00:00:00 2001 From: Calen Pennington <cale@edx.org> Date: Wed, 19 Jun 2013 09:01:20 -0400 Subject: [PATCH 214/375] Don't print error messages if log/db/data directories already exist --- scripts/create-dev-env.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/create-dev-env.sh b/scripts/create-dev-env.sh index ede86b123a..1f3e078107 100755 --- a/scripts/create-dev-env.sh +++ b/scripts/create-dev-env.sh @@ -495,9 +495,9 @@ pip install argcomplete cd $BASE/edx-platform bundle install -mkdir "$BASE/log" || true -mkdir "$BASE/db" || true -mkdir "$BASE/data" || true +mkdir -p "$BASE/log" +mkdir -p "$BASE/db" +mkdir -p "$BASE/data" rake django-admin[syncdb] rake django-admin[migrate] From 2de645599a1a173a8d247c92d79b7e0c9738f31f Mon Sep 17 00:00:00 2001 From: Calen Pennington <cale@edx.org> Date: Wed, 19 Jun 2013 09:01:30 -0400 Subject: [PATCH 215/375] Remove trailing whitespace --- scripts/create-dev-env.sh | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/scripts/create-dev-env.sh b/scripts/create-dev-env.sh index 1f3e078107..cc0efbef0d 100755 --- a/scripts/create-dev-env.sh +++ b/scripts/create-dev-env.sh @@ -73,7 +73,7 @@ change_git_push_defaults() { #Set git push defaults to upstream rather than master output "Changing git defaults" git config --global push.default upstream - + } clone_repos() { @@ -206,10 +206,10 @@ case `uname -s` in distro=`lsb_release -cs` case $distro in - wheezy|jessie|maya|olivia|nadia|precise|quantal) + wheezy|jessie|maya|olivia|nadia|precise|quantal) warning " Debian support is not fully debugged. Assuming you have standard - development packages already working like scipy rvm, the + development packages already working like scipy rvm, the installation should go fine, but this is still a work in progress. Please report issues you have and let us know if you are able to figure @@ -218,7 +218,7 @@ case `uname -s` in Press return to continue or control-C to abort" read dummy - sudo apt-get install git ;; + sudo apt-get install git ;; squeeze|lisa|katya|oneiric|natty|raring) warning " It seems like you're using $distro which has been deprecated. @@ -231,7 +231,7 @@ case `uname -s` in Press return to continue or control-C to abort" read dummy sudo apt-get install git - ;; + ;; *) error "Unsupported distribution - $distro" @@ -283,7 +283,7 @@ clone_repos if [[ -d $BASE/edx-platform/scripts ]]; then output "Installing system-level dependencies" bash $BASE/edx-platform/scripts/install-system-req.sh -else +else error "It appears that our directory structure has changed and somebody failed to update this script. raise an issue on Github and someone should fix it." exit 1 @@ -314,14 +314,14 @@ case `uname -s` in [Ll]inux) warning "Setting up rvm on linux. This is a known pain point. If the script fails here - refer to the following stack overflow question: + refer to the following stack overflow question: http://stackoverflow.com/questions/9056008/installed-ruby-1-9-3-with-rvm-but-command-line-doesnt-show-ruby-v/9056395#9056395" sudo apt-get --purge remove ruby-rvm sudo rm -rf /usr/share/ruby-rvm /etc/rvmrc /etc/profile.d/rvm.sh curl -sL https://get.rvm.io | bash -s stable --ruby --autolibs=enable --auto-dotfiles ;; esac - + # Ensure we have RVM available as a shell function so that it can mess # with the environment and set everything up properly. The RVM install From fc6043876a7652a7fde90a1985cc2d5a71e5f383 Mon Sep 17 00:00:00 2001 From: Calen Pennington <cale@edx.org> Date: Wed, 19 Jun 2013 09:01:52 -0400 Subject: [PATCH 216/375] Install python and node prerequisites before trying to run django-admin --- scripts/create-dev-env.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/create-dev-env.sh b/scripts/create-dev-env.sh index cc0efbef0d..d3b7715904 100755 --- a/scripts/create-dev-env.sh +++ b/scripts/create-dev-env.sh @@ -494,6 +494,7 @@ cd $BASE pip install argcomplete cd $BASE/edx-platform bundle install +rake install_prereqs mkdir -p "$BASE/log" mkdir -p "$BASE/db" From dece800888f31d7e3594dde9f795a459bda18527 Mon Sep 17 00:00:00 2001 From: lapentab <blapenta@edx.org> Date: Wed, 19 Jun 2013 09:57:16 -0400 Subject: [PATCH 217/375] Renames self.get_test_system to test_system, removes duplicate function. --- .../xmodule/tests/test_combined_open_ended.py | 47 ++++++++++--------- .../xmodule/xmodule/tests/test_conditional.py | 18 +++---- .../xmodule/tests/test_error_module.py | 22 ++++----- .../xmodule/tests/test_peer_grading.py | 8 ++-- .../xmodule/tests/test_util_open_ended.py | 2 +- 5 files changed, 49 insertions(+), 48 deletions(-) diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py index f84259f8bd..e1f8d135de 100644 --- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py +++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py @@ -68,8 +68,8 @@ class OpenEndedChildTest(unittest.TestCase): descriptor = Mock() def setUp(self): - self.get_test_system = get_test_system() - self.openendedchild = OpenEndedChild(self.get_test_system, self.location, + self.test_system = get_test_system() + self.openendedchild = OpenEndedChild(self.test_system, self.location, self.definition, self.descriptor, self.static_data, self.metadata) def test_latest_answer_empty(self): @@ -81,7 +81,7 @@ class OpenEndedChildTest(unittest.TestCase): self.assertEqual(answer, None) def test_latest_post_assessment_empty(self): - answer = self.openendedchild.latest_post_assessment(self.get_test_system) + answer = self.openendedchild.latest_post_assessment(self.test_system) self.assertEqual(answer, "") def test_new_history_entry(self): @@ -116,7 +116,7 @@ class OpenEndedChildTest(unittest.TestCase): post_assessment = "Post assessment" self.openendedchild.record_latest_post_assessment(post_assessment) self.assertEqual(post_assessment, - self.openendedchild.latest_post_assessment(self.get_test_system)) + self.openendedchild.latest_post_assessment(self.test_system)) def test_get_score(self): new_answer = "New Answer" @@ -134,7 +134,7 @@ class OpenEndedChildTest(unittest.TestCase): self.assertEqual(score['total'], self.static_data['max_score']) def test_reset(self): - self.openendedchild.reset(self.get_test_system) + self.openendedchild.reset(self.test_system) state = json.loads(self.openendedchild.get_instance_state()) self.assertEqual(state['child_state'], OpenEndedChild.INITIAL) @@ -192,19 +192,19 @@ class OpenEndedModuleTest(unittest.TestCase): descriptor = Mock() def setUp(self): - self.get_test_system = get_test_system() + self.test_system = get_test_system() - self.get_test_system.location = self.location + self.test_system.location = self.location self.mock_xqueue = MagicMock() self.mock_xqueue.send_to_queue.return_value = (None, "Message") def constructed_callback(dispatch="score_update"): return dispatch - self.get_test_system.xqueue = {'interface': self.mock_xqueue, 'construct_callback': constructed_callback, + self.test_system.xqueue = {'interface': self.mock_xqueue, 'construct_callback': constructed_callback, 'default_queuename': 'testqueue', 'waittime': 1} - self.openendedmodule = OpenEndedModule(self.get_test_system, self.location, + self.openendedmodule = OpenEndedModule(self.test_system, self.location, self.definition, self.descriptor, self.static_data, self.metadata) def test_message_post(self): @@ -213,7 +213,7 @@ class OpenEndedModuleTest(unittest.TestCase): 'grader_id': '1', 'score': 3} qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat) - student_info = {'anonymous_student_id': self.get_test_system.anonymous_student_id, + student_info = {'anonymous_student_id': self.test_system.anonymous_student_id, 'submission_time': qtime} contents = { 'feedback': get['feedback'], @@ -223,7 +223,7 @@ class OpenEndedModuleTest(unittest.TestCase): 'student_info': json.dumps(student_info) } - result = self.openendedmodule.message_post(get, self.get_test_system) + result = self.openendedmodule.message_post(get, self.test_system) self.assertTrue(result['success']) # make sure it's actually sending something we want to the queue self.mock_xqueue.send_to_queue.assert_called_with(body=json.dumps(contents), header=ANY) @@ -234,7 +234,7 @@ class OpenEndedModuleTest(unittest.TestCase): def test_send_to_grader(self): submission = "This is a student submission" qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat) - student_info = {'anonymous_student_id': self.get_test_system.anonymous_student_id, + student_info = {'anonymous_student_id': self.test_system.anonymous_student_id, 'submission_time': qtime} contents = self.openendedmodule.payload.copy() contents.update({ @@ -242,7 +242,7 @@ class OpenEndedModuleTest(unittest.TestCase): 'student_response': submission, 'max_score': self.max_score }) - result = self.openendedmodule.send_to_grader(submission, self.get_test_system) + result = self.openendedmodule.send_to_grader(submission, self.test_system) self.assertTrue(result) self.mock_xqueue.send_to_queue.assert_called_with(body=json.dumps(contents), header=ANY) @@ -256,7 +256,7 @@ class OpenEndedModuleTest(unittest.TestCase): } get = {'queuekey': "abcd", 'xqueue_body': score_msg} - self.openendedmodule.update_score(get, self.get_test_system) + self.openendedmodule.update_score(get, self.test_system) def update_score_single(self): self.openendedmodule.new_history_entry("New Entry") @@ -279,11 +279,11 @@ class OpenEndedModuleTest(unittest.TestCase): } get = {'queuekey': "abcd", 'xqueue_body': json.dumps(score_msg)} - self.openendedmodule.update_score(get, self.get_test_system) + self.openendedmodule.update_score(get, self.test_system) def test_latest_post_assessment(self): self.update_score_single() - assessment = self.openendedmodule.latest_post_assessment(self.get_test_system) + assessment = self.openendedmodule.latest_post_assessment(self.test_system) self.assertFalse(assessment == '') # check for errors self.assertFalse('errors' in assessment) @@ -367,9 +367,9 @@ class CombinedOpenEndedModuleTest(unittest.TestCase): definition = {'prompt': etree.XML(prompt), 'rubric': etree.XML(rubric), 'task_xml': [task_xml1, task_xml2]} full_definition = definition_template.format(prompt=prompt, rubric=rubric, task1=task_xml1, task2=task_xml2) descriptor = Mock(data=full_definition) - get_test_system = get_test_system() + test_system = get_test_system() combinedoe_container = CombinedOpenEndedModule( - get_test_system, + test_system, descriptor, model_data={ 'data': full_definition, @@ -381,7 +381,7 @@ class CombinedOpenEndedModuleTest(unittest.TestCase): def setUp(self): # TODO: this constructor call is definitely wrong, but neither branch # of the merge matches the module constructor. Someone (Vik?) should fix this. - self.combinedoe = CombinedOpenEndedV1Module(self.get_test_system, + self.combinedoe = CombinedOpenEndedV1Module(self.test_system, self.location, self.definition, self.descriptor, @@ -441,7 +441,7 @@ class CombinedOpenEndedModuleTest(unittest.TestCase): for xml in xml_to_test: definition = {'prompt': etree.XML(self.prompt), 'rubric': etree.XML(self.rubric), 'task_xml': xml} descriptor = Mock(data=definition) - combinedoe = CombinedOpenEndedV1Module(self.get_test_system, + combinedoe = CombinedOpenEndedV1Module(self.test_system, self.location, definition, descriptor, @@ -471,7 +471,7 @@ class CombinedOpenEndedModuleTest(unittest.TestCase): definition = {'prompt': etree.XML(self.prompt), 'rubric': etree.XML(rubric), 'task_xml': [self.task_xml1, self.task_xml2]} descriptor = Mock(data=definition) - combinedoe = CombinedOpenEndedV1Module(self.get_test_system, + combinedoe = CombinedOpenEndedV1Module(self.test_system, self.location, definition, descriptor, @@ -493,8 +493,8 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore): hint = "blah" def setUp(self): - self.get_test_system = get_test_system() - self.get_test_system.xqueue['interface'] = Mock( + self.test_system = get_test_system() + self.test_system.xqueue['interface'] = Mock( send_to_queue=Mock(side_effect=[1, "queued"]) ) self.setup_modulestore(COURSE) @@ -569,6 +569,7 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore): #Mock a student submitting an assessment assessment_dict = MockQueryDict() assessment_dict.update({'assessment': sum(assessment), 'score_list[]': assessment}) + #from nose.tools import set_trace; set_trace() module.handle_ajax("save_assessment", assessment_dict) task_one_json = json.loads(module.task_states[0]) self.assertEqual(json.loads(task_one_json['child_history'][0]['post_assessment']), assessment) diff --git a/common/lib/xmodule/xmodule/tests/test_conditional.py b/common/lib/xmodule/xmodule/tests/test_conditional.py index e40bddebcb..00072ce879 100644 --- a/common/lib/xmodule/xmodule/tests/test_conditional.py +++ b/common/lib/xmodule/xmodule/tests/test_conditional.py @@ -103,11 +103,11 @@ class ConditionalModuleBasicTest(unittest.TestCase): """ def setUp(self): - self.get_test_system = get_test_system() + self.test_system = get_test_system() def test_icon_class(self): '''verify that get_icon_class works independent of condition satisfaction''' - modules = ConditionalFactory.create(self.get_test_system) + modules = ConditionalFactory.create(self.test_system) for attempted in ["false", "true"]: for icon_class in [ 'other', 'problem', 'video']: modules['source_module'].is_attempted = attempted @@ -116,7 +116,7 @@ class ConditionalModuleBasicTest(unittest.TestCase): def test_get_html(self): - modules = ConditionalFactory.create(self.get_test_system) + modules = ConditionalFactory.create(self.test_system) # because get_test_system returns the repr of the context dict passed to render_template, # we reverse it here html = modules['cond_module'].get_html() @@ -126,7 +126,7 @@ class ConditionalModuleBasicTest(unittest.TestCase): self.assertEqual(html_dict['depends'], 'i4x-edX-conditional_test-problem-SampleProblem') def test_handle_ajax(self): - modules = ConditionalFactory.create(self.get_test_system) + modules = ConditionalFactory.create(self.test_system) modules['source_module'].is_attempted = "false" ajax = json.loads(modules['cond_module'].handle_ajax('', '')) print "ajax: ", ajax @@ -145,7 +145,7 @@ class ConditionalModuleBasicTest(unittest.TestCase): Check that handle_ajax works properly if the source is really an ErrorModule, and that the condition is not satisfied. ''' - modules = ConditionalFactory.create(self.get_test_system, source_is_error_module=True) + modules = ConditionalFactory.create(self.test_system, source_is_error_module=True) ajax = json.loads(modules['cond_module'].handle_ajax('', '')) html = ajax['html'] self.assertFalse(any(['This is a secret' in item for item in html])) @@ -161,7 +161,7 @@ class ConditionalModuleXmlTest(unittest.TestCase): return DummySystem(load_error_modules) def setUp(self): - self.get_test_system = get_test_system() + self.test_system = get_test_system() def get_course(self, name): """Get a test course by directory name. If there's more than one, error.""" @@ -186,7 +186,7 @@ class ConditionalModuleXmlTest(unittest.TestCase): if isinstance(descriptor, Location): location = descriptor descriptor = self.modulestore.get_instance(course.id, location, depth=None) - return descriptor.xmodule(self.get_test_system) + return descriptor.xmodule(self.test_system) # edx - HarvardX # cond_test - ER22x @@ -194,8 +194,8 @@ class ConditionalModuleXmlTest(unittest.TestCase): def replace_urls(text, staticfiles_prefix=None, replace_prefix='/static/', course_namespace=None): return text - self.get_test_system.replace_urls = replace_urls - self.get_test_system.get_module = inner_get_module + self.test_system.replace_urls = replace_urls + self.test_system.get_module = inner_get_module module = inner_get_module(location) print "module: ", module diff --git a/common/lib/xmodule/xmodule/tests/test_error_module.py b/common/lib/xmodule/xmodule/tests/test_error_module.py index 82b181bb9f..f91bf7cb37 100644 --- a/common/lib/xmodule/xmodule/tests/test_error_module.py +++ b/common/lib/xmodule/xmodule/tests/test_error_module.py @@ -9,10 +9,7 @@ from xmodule.x_module import XModuleDescriptor from mock import MagicMock -class TestErrorModule(unittest.TestCase): - """ - Tests for ErrorModule and ErrorDescriptor - """ +class SetupTestErrorModules(): def setUp(self): self.system = get_test_system() self.org = "org" @@ -21,6 +18,14 @@ class TestErrorModule(unittest.TestCase): self.valid_xml = u"<problem>ABC \N{SNOWMAN}</problem>" self.error_msg = "Error" + +class TestErrorModule(unittest.TestCase, SetupTestErrorModules): + """ + Tests for ErrorModule and ErrorDescriptor + """ + def setUp(self): + SetupTestErrorModules.setUp(self) + def test_error_module_xml_rendering(self): descriptor = error_module.ErrorDescriptor.from_xml( self.valid_xml, self.system, self.org, self.course, self.error_msg) @@ -45,17 +50,12 @@ class TestErrorModule(unittest.TestCase): self.assertIn(repr(descriptor), context_repr) -class TestNonStaffErrorModule(unittest.TestCase): +class TestNonStaffErrorModule(unittest.TestCase, SetupTestErrorModules): """ Tests for NonStaffErrorModule and NonStaffErrorDescriptor """ def setUp(self): - self.system = get_test_system() - self.org = "org" - self.course = "course" - self.location = Location(['i4x', self.org, self.course, None, None]) - self.valid_xml = u"<problem>ABC \N{SNOWMAN}</problem>" - self.error_msg = "Error" + SetupTestErrorModules.setUp(self) def test_non_staff_error_module_create(self): descriptor = error_module.NonStaffErrorDescriptor.from_xml( diff --git a/common/lib/xmodule/xmodule/tests/test_peer_grading.py b/common/lib/xmodule/xmodule/tests/test_peer_grading.py index 32c23aedb5..3e1a578118 100644 --- a/common/lib/xmodule/xmodule/tests/test_peer_grading.py +++ b/common/lib/xmodule/xmodule/tests/test_peer_grading.py @@ -39,8 +39,8 @@ class PeerGradingModuleTest(unittest.TestCase, DummyModulestore): Create a peer grading module from a test system @return: """ - self.get_test_system = get_test_system() - self.get_test_system.open_ended_grading_interface = None + self.test_system = get_test_system() + self.test_system.open_ended_grading_interface = None self.setup_modulestore(COURSE) self.peer_grading = self.get_module_from_location(self.problem_location, COURSE) @@ -151,8 +151,8 @@ class PeerGradingModuleScoredTest(unittest.TestCase, DummyModulestore): Create a peer grading module from a test system @return: """ - self.get_test_system = get_test_system() - self.get_test_system.open_ended_grading_interface = None + self.test_system = get_test_system() + self.test_system.open_ended_grading_interface = None self.setup_modulestore(COURSE) def test_metadata_load(self): diff --git a/common/lib/xmodule/xmodule/tests/test_util_open_ended.py b/common/lib/xmodule/xmodule/tests/test_util_open_ended.py index 4916599de9..9dbb17ae2f 100644 --- a/common/lib/xmodule/xmodule/tests/test_util_open_ended.py +++ b/common/lib/xmodule/xmodule/tests/test_util_open_ended.py @@ -52,4 +52,4 @@ class DummyModulestore(object): if not isinstance(location, Location): location = Location(location) descriptor = self.modulestore.get_instance(course.id, location, depth=None) - return descriptor.xmodule(self.get_test_system) + return descriptor.xmodule(self.test_system) From 4cc30aab00c9a0c266adaee00df726589c2adcbd Mon Sep 17 00:00:00 2001 From: Calen Pennington <cale@edx.org> Date: Wed, 19 Jun 2013 10:04:32 -0400 Subject: [PATCH 218/375] Cleanup pep8 violations in test_correctmap.py --- common/lib/capa/capa/tests/test_correctmap.py | 122 +++++++++++------- 1 file changed, 75 insertions(+), 47 deletions(-) diff --git a/common/lib/capa/capa/tests/test_correctmap.py b/common/lib/capa/capa/tests/test_correctmap.py index 270ba4d849..c5e49edecb 100644 --- a/common/lib/capa/capa/tests/test_correctmap.py +++ b/common/lib/capa/capa/tests/test_correctmap.py @@ -1,31 +1,44 @@ +""" +Tests to verify that CorrectMap behaves correctly +""" + import unittest from capa.correctmap import CorrectMap import datetime + class CorrectMapTest(unittest.TestCase): + """ + Tests to verify that CorrectMap behaves correctly + """ def setUp(self): self.cmap = CorrectMap() def test_set_input_properties(self): - # Set the correctmap properties for two inputs - self.cmap.set(answer_id='1_2_1', - correctness='correct', - npoints=5, - msg='Test message', - hint='Test hint', - hintmode='always', - queuestate={'key':'secretstring', - 'time':'20130228100026'}) + self.cmap.set( + answer_id='1_2_1', + correctness='correct', + npoints=5, + msg='Test message', + hint='Test hint', + hintmode='always', + queuestate={ + 'key': 'secretstring', + 'time': '20130228100026' + } + ) - self.cmap.set(answer_id='2_2_1', - correctness='incorrect', - npoints=None, - msg=None, - hint=None, - hintmode=None, - queuestate=None) + self.cmap.set( + answer_id='2_2_1', + correctness='incorrect', + npoints=None, + msg=None, + hint=None, + hintmode=None, + queuestate=None + ) # Assert that each input has the expected properties self.assertTrue(self.cmap.is_correct('1_2_1')) @@ -62,7 +75,6 @@ class CorrectMapTest(unittest.TestCase): self.assertFalse(self.cmap.is_right_queuekey('2_2_1', '')) self.assertFalse(self.cmap.is_right_queuekey('2_2_1', None)) - def test_get_npoints(self): # Set the correctmap properties for 4 inputs # 1) correct, 5 points @@ -70,25 +82,35 @@ class CorrectMapTest(unittest.TestCase): # 3) incorrect, 5 points # 4) incorrect, None points # 5) correct, 0 points - self.cmap.set(answer_id='1_2_1', - correctness='correct', - npoints=5) + self.cmap.set( + answer_id='1_2_1', + correctness='correct', + npoints=5 + ) - self.cmap.set(answer_id='2_2_1', - correctness='correct', - npoints=None) + self.cmap.set( + answer_id='2_2_1', + correctness='correct', + npoints=None + ) - self.cmap.set(answer_id='3_2_1', - correctness='incorrect', - npoints=5) + self.cmap.set( + answer_id='3_2_1', + correctness='incorrect', + npoints=5 + ) - self.cmap.set(answer_id='4_2_1', - correctness='incorrect', - npoints=None) + self.cmap.set( + answer_id='4_2_1', + correctness='incorrect', + npoints=None + ) - self.cmap.set(answer_id='5_2_1', - correctness='correct', - npoints=0) + self.cmap.set( + answer_id='5_2_1', + correctness='correct', + npoints=0 + ) # Assert that we get the expected points # If points assigned --> npoints @@ -100,7 +122,6 @@ class CorrectMapTest(unittest.TestCase): self.assertEqual(self.cmap.get_npoints('4_2_1'), 0) self.assertEqual(self.cmap.get_npoints('5_2_1'), 0) - def test_set_overall_message(self): # Default is an empty string string @@ -118,14 +139,18 @@ class CorrectMapTest(unittest.TestCase): def test_update_from_correctmap(self): # Initialize a CorrectMap with some properties - self.cmap.set(answer_id='1_2_1', - correctness='correct', - npoints=5, - msg='Test message', - hint='Test hint', - hintmode='always', - queuestate={'key':'secretstring', - 'time':'20130228100026'}) + self.cmap.set( + answer_id='1_2_1', + correctness='correct', + npoints=5, + msg='Test message', + hint='Test hint', + hintmode='always', + queuestate={ + 'key': 'secretstring', + 'time': '20130228100026' + } + ) self.cmap.set_overall_message("Test message") @@ -133,14 +158,17 @@ class CorrectMapTest(unittest.TestCase): # as the first cmap other_cmap = CorrectMap() other_cmap.update(self.cmap) - + # Assert that it has all the same properties - self.assertEqual(other_cmap.get_overall_message(), - self.cmap.get_overall_message()) - - self.assertEqual(other_cmap.get_dict(), - self.cmap.get_dict()) + self.assertEqual( + other_cmap.get_overall_message(), + self.cmap.get_overall_message() + ) + self.assertEqual( + other_cmap.get_dict(), + self.cmap.get_dict() + ) def test_update_from_invalid(self): # Should get an exception if we try to update() a CorrectMap From 6e96b885186b8ea44b76a38aa788538b9a0d68c2 Mon Sep 17 00:00:00 2001 From: cahrens <christina@edx.org> Date: Wed, 19 Jun 2013 11:28:16 -0400 Subject: [PATCH 219/375] On marketing site, disable course settings options that do not work. When on the marketing site (edx.org) disable portions of the course settings page in Studio that do not actually work in that environment. --- .../tests/test_course_settings.py | 45 +++++ cms/djangoapps/contentstore/views/course.py | 3 +- cms/templates/settings.html | 172 ++++++++++-------- 3 files changed, 139 insertions(+), 81 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py index 8c15b1ae95..3d676390ea 100644 --- a/cms/djangoapps/contentstore/tests/test_course_settings.py +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -1,11 +1,13 @@ import datetime import json import copy +import mock from django.contrib.auth.models import User from django.test.client import Client from django.core.urlresolvers import reverse from django.utils.timezone import UTC +from django.test.utils import override_settings from xmodule.modulestore import Location from models.settings.course_details import (CourseDetails, CourseSettingsEncoder) @@ -118,6 +120,49 @@ class CourseDetailsTestCase(CourseTestCase): jsondetails.effort, "After set effort" ) + @override_settings(MKTG_URLS={'ROOT': 'dummy-root'}) + def test_marketing_site_fetch(self): + settings_details_url = reverse('settings_details', + kwargs= {'org': self.course_location.org, + 'name': self.course_location.name, + 'course': self.course_location.course + }) + + with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': True}): + response = self.client.get(settings_details_url) + self.assertContains(response, "Course Summary Page") + self.assertContains(response, "your course summary page will not be viewable") + + self.assertContains(response, "Course Start Date") + self.assertContains(response, "Course End Date") + self.assertNotContains(response, "Enrollment Start Date") + self.assertNotContains(response, "Enrollment End Date") + self.assertContains(response, "not the dates shown on your course summary page") + + self.assertNotContains(response, "Introducing Your Course") + self.assertNotContains(response, "Requirements") + + def test_regular_site_fetch(self): + settings_details_url = reverse('settings_details', + kwargs= {'org': self.course_location.org, + 'name': self.course_location.name, + 'course': self.course_location.course + }) + + with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': False}): + response = self.client.get(settings_details_url) + self.assertContains(response, "Course Summary Page") + self.assertNotContains(response, "your course summary page will not be viewable") + + self.assertContains(response, "Course Start Date") + self.assertContains(response, "Course End Date") + self.assertContains(response, "Enrollment Start Date") + self.assertContains(response, "Enrollment End Date") + self.assertNotContains(response, "not the dates shown on your course summary page") + + self.assertContains(response, "Introducing Your Course") + self.assertContains(response, "Requirements") + class CourseDetailsViewTest(CourseTestCase): def alter_field(self, url, details, field, val): diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index 8762eb3a2a..62df50d5f4 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -230,7 +230,8 @@ def get_course_settings(request, org, course, name): kwargs={"org": org, "course": course, "name": name, - "section": "details"}) + "section": "details"}), + 'about_page_editable': not settings.MITX_FEATURES.get('ENABLE_MKTG_SITE', False) }) diff --git a/cms/templates/settings.html b/cms/templates/settings.html index 2adc0cd980..55dd2b67b2 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -1,3 +1,5 @@ +<%! from django.utils.translation import ugettext as _ %> + <%inherit file="base.html" /> <%block name="title">Schedule & Details Settings</%block> <%block name="bodyclass">is-signedin course schedule settings</%block> @@ -50,8 +52,8 @@ from contentstore import utils <div class="wrapper-mast wrapper"> <header class="mast has-subtitle"> <h1 class="page-header"> - <small class="subtitle">Settings</small> - <span class="sr">> </span>Schedule & Details + <small class="subtitle">${_("Settings")}</small> + <span class="sr">> </span>${_("Schedule & Details")} </h1> </header> </div> @@ -62,36 +64,40 @@ from contentstore import utils <form id="settings_details" class="settings-details" method="post" action=""> <section class="group-settings basic"> <header> - <h2 class="title-2">Basic Information</h2> - <span class="tip">The nuts and bolts of your course</span> + <h2 class="title-2">${_("Basic Information")}</h2> + <span class="tip">${_("The nuts and bolts of your course")}</span> </header> <ol class="list-input"> <li class="field text is-not-editable" id="field-course-organization"> - <label for="course-organization">Organization</label> - <input title="This field is disabled: this information cannot be changed." type="text" class="long" id="course-organization" value="[Course Organization]" readonly /> + <label for="course-organization">${_("Organization")}</label> + <input title="${_('This field is disabled: this information cannot be changed.')}" type="text" class="long" id="course-organization" value="[Course Organization]" readonly /> </li> <li class="field text is-not-editable" id="field-course-number"> - <label for="course-number">Course Number</label> - <input title="This field is disabled: this information cannot be changed." type="text" class="short" id="course-number" value="[Course No.]" readonly> + <label for="course-number">${_("Course Number")}</label> + <input title="${_('This field is disabled: this information cannot be changed.')}" type="text" class="short" id="course-number" value="[Course No.]" readonly> </li> <li class="field text is-not-editable" id="field-course-name"> - <label for="course-name">Course Name</label> - <input title="This field is disabled: this information cannot be changed." type="text" class="long" id="course-name" value="[Course Name]" readonly /> + <label for="course-name">${_("Course Name")}</label> + <input title="${_('This field is disabled: this information cannot be changed.')}" type="text" class="long" id="course-name" value="[Course Name]" readonly /> </li> </ol> <div class="note note-promotion note-promotion-courseURL has-actions"> - <h3 class="title">Course Summary Page <span class="tip">(for student enrollment and access)</span></h3> + <h3 class="title">${_("Course Summary Page")} <span class="tip">${_("(for student enrollment and access)")}</span></h3> <div class="copy"> <p><a class="link-courseURL" rel="external" href="https:${utils.get_lms_link_for_about_page(course_location)}" />https:${utils.get_lms_link_for_about_page(course_location)}</a></p> </div> - + % if not about_page_editable: + <div> + <p>${_("Note: your course summary page will not be viewable until your course has been announced. To provide content for the page and preview it, follow the instructions provided by your PM or Conrad Warre (conrad@edx.org).")}</p> + </div> + % endif <ul class="list-actions"> <li class="action-item"> - <a title="Send a note to students via email" href="mailto:someone@domain.com?Subject=Enroll%20in%20${context_course.display_name_with_default}&body=The%20course%20"${context_course.display_name_with_default}",%20provided%20by%20edX,%20is%20open%20for%20enrollment.%20Please%20navigate%20to%20this%20course%20at%20https:${utils.get_lms_link_for_about_page(course_location)}%20to%20enroll." class="action action-primary"><i class="icon-envelope-alt icon-inline"></i> Invite your students</a> + <a title="${_('Send a note to students via email')}" href="mailto:someone@domain.com?Subject=Enroll%20in%20${context_course.display_name_with_default}&body=The%20course%20"${context_course.display_name_with_default}",%20provided%20by%20edX,%20is%20open%20for%20enrollment.%20Please%20navigate%20to%20this%20course%20at%20https:${utils.get_lms_link_for_about_page(course_location)}%20to%20enroll." class="action action-primary"><i class="icon-envelope-alt icon-inline"></i>${_("Invite your students")}</a> </li> </ul> </div> @@ -101,20 +107,26 @@ from contentstore import utils <section class="group-settings schedule"> <header> - <h2 class="title-2">Course Schedule</h2> - <span class="tip">Important steps and segments of your course</span> + <h2 class="title-2">${_('Course Schedule')}</h2> + <span class="tip">${_('Dates that control when your course can be viewed.')}</span> </header> + % if not about_page_editable: + <div> + <p>${_("Note: these dates impact when your courseware can be viewed, but they are not the dates shown on your course summary page. To provide the course start and registration dates as shown on your course summary page, follow the instructions provided by your PM or Conrad Warre (conrad@edx.org).")}</p> + </div> + % endif + <ol class="list-input"> <li class="field-group field-group-course-start" id="course-start"> <div class="field date" id="field-course-start-date"> - <label for="course-start-date">Course Start Date</label> + <label for="course-start-date">${_("Course Start Date")}</label> <input type="text" class="start-date date start datepicker" id="course-start-date" placeholder="MM/DD/YYYY" autocomplete="off" /> - <span class="tip tip-stacked">First day the course begins</span> + <span class="tip tip-stacked">${_("First day the course begins")}</span> </div> <div class="field time" id="field-course-start-time"> - <label for="course-start-time">Course Start Time</label> + <label for="course-start-time">${_("Course Start Time")}</label> <input type="text" class="time start timepicker" id="course-start-time" value="" placeholder="HH:MM" autocomplete="off" /> <span class="tip tip-stacked" id="timezone"></span> </div> @@ -122,29 +134,29 @@ from contentstore import utils <li class="field-group field-group-course-end" id="course-end"> <div class="field date" id="field-course-end-date"> - <label for="course-end-date">Course End Date</label> + <label for="course-end-date">${_("Course End Date")}</label> <input type="text" class="end-date date end" id="course-end-date" placeholder="MM/DD/YYYY" autocomplete="off" /> - <span class="tip tip-stacked">Last day your course is active</span> + <span class="tip tip-stacked">${_("Last day your course is active")}</span> </div> <div class="field time" id="field-course-end-time"> - <label for="course-end-time">Course End Time</label> + <label for="course-end-time">${_("Course End Time")}</label> <input type="text" class="time end" id="course-end-time" value="" placeholder="HH:MM" autocomplete="off" /> <span class="tip tip-stacked" id="timezone"></span> </div> </li> </ol> - + % if about_page_editable: <ol class="list-input"> <li class="field-group field-group-enrollment-start" id="enrollment-start"> <div class="field date" id="field-enrollment-start-date"> - <label for="course-enrollment-start-date">Enrollment Start Date</label> + <label for="course-enrollment-start-date">${_("Enrollment Start Date")}</label> <input type="text" class="start-date date start" id="course-enrollment-start-date" placeholder="MM/DD/YYYY" autocomplete="off" /> - <span class="tip tip-stacked">First day students can enroll</span> + <span class="tip tip-stacked">${_("First day students can enroll")}</span> </div> <div class="field time" id="field-enrollment-start-time"> - <label for="course-enrollment-start-time">Enrollment Start Time</label> + <label for="course-enrollment-start-time">${_("Enrollment Start Time")}</label> <input type="text" class="time start" id="course-enrollment-start-time" value="" placeholder="HH:MM" autocomplete="off" /> <span class="tip tip-stacked" id="timezone"></span> </div> @@ -152,91 +164,91 @@ from contentstore import utils <li class="field-group field-group-enrollment-end" id="enrollment-end"> <div class="field date" id="field-enrollment-end-date"> - <label for="course-enrollment-end-date">Enrollment End Date</label> + <label for="course-enrollment-end-date">${_("Enrollment End Date")}</label> <input type="text" class="end-date date end" id="course-enrollment-end-date" placeholder="MM/DD/YYYY" autocomplete="off" /> - <span class="tip tip-stacked">Last day students can enroll</span> + <span class="tip tip-stacked">${_("Last day students can enroll")}</span> </div> <div class="field time" id="field-enrollment-end-time"> - <label for="course-enrollment-end-time">Enrollment End Time</label> + <label for="course-enrollment-end-time">${_("Enrollment End Time")}</label> <input type="text" class="time end" id="course-enrollment-end-time" value="" placeholder="HH:MM" autocomplete="off" /> <span class="tip tip-stacked" id="timezone"></span> </div> </li> </ol> + % endif </section> - <hr class="divide" /> + % if about_page_editable: + <section class="group-settings marketing"> + <header> + <h2 class="title-2">${_("Introducing Your Course")}</h2> + <span class="tip">${_("Information for prospective students")}</span> + </header> - <section class="group-settings marketing"> - <header> - <h2 class="title-2">Introducing Your Course</h2> - <span class="tip">Information for prospective students</span> - </header> + <ol class="list-input"> + <li class="field text" id="field-course-overview"> + <label for="course-overview">${_("Course Overview")}</label> + <textarea class="tinymce text-editor" id="course-overview"></textarea> + <span class="tip tip-stacked">${_("Introductions, prerequisites, FAQs that are used on ")}<a class="link-courseURL" rel="external" href="${utils.get_lms_link_for_about_page(course_location)}">${_("your course summary page")}</a>${_(" (formatted in HTML)")}</span> + </li> - <ol class="list-input"> - <li class="field text" id="field-course-overview"> - <label for="course-overview">Course Overview</label> - <textarea class="tinymce text-editor" id="course-overview"></textarea> - <span class="tip tip-stacked">Introductions, prerequisites, FAQs that are used on <a class="link-courseURL" rel="external" href="${utils.get_lms_link_for_about_page(course_location)}">your course summary page</a> (formatted in HTML)</span> - </li> + <li class="field video" id="field-course-introduction-video"> + <label for="course-overview">${_("Course Introduction Video")}</label> + <div class="input input-existing"> + <div class="current current-course-introduction-video"> + <iframe width="618" height="350" src="" frameborder="0" allowfullscreen></iframe> + </div> + <div class="actions"> + <a href="#" class="remove-item remove-course-introduction-video remove-video-data"><span class="delete-icon"></span>${_("Delete Current Video")}</a> + </div> + </div> - <li class="field video" id="field-course-introduction-video"> - <label for="course-overview">Course Introduction Video</label> - <div class="input input-existing"> - <div class="current current-course-introduction-video"> - <iframe width="618" height="350" src="" frameborder="0" allowfullscreen></iframe> - </div> - <div class="actions"> - <a href="#" class="remove-item remove-course-introduction-video remove-video-data"><span class="delete-icon"></span> Delete Current Video</a> - </div> - </div> + <div class="input"> + <input type="text" class="long new-course-introduction-video add-video-data" id="course-introduction-video" value="" placeholder="your YouTube video's ID" autocomplete="off" /> + <span class="tip tip-stacked">${_("Enter your YouTube video's ID (along with any restriction parameters)")}</span> + </div> + </li> + </ol> + </section> - <div class="input"> - <input type="text" class="long new-course-introduction-video add-video-data" id="course-introduction-video" value="" placeholder="your YouTube video's ID" autocomplete="off" /> - <span class="tip tip-stacked">Enter your YouTube video's ID (along with any restriction parameters)</span> - </div> - </li> - </ol> - </section> + <hr class="divide" /> - <hr class="divide" /> + <section class="group-settings requirements"> + <header> + <h2 class="title-2">${_("Requirements")}</h2> + <span class="tip">${_("Expectations of the students taking this course")}</span> + </header> - <section class="group-settings requirements"> - <header> - <h2 class="title-2">Requirements</h2> - <span class="tip">Expectations of the students taking this course</span> - </header> - - <ol class="list-input"> - <li class="field text" id="field-course-effort"> - <label for="course-effort">Hours of Effort per Week</label> - <input type="text" class="short time" id="course-effort" placeholder="HH:MM" /> - <span class="tip tip-inline">Time spent on all course work</span> - </li> - </ol> - </section> + <ol class="list-input"> + <li class="field text" id="field-course-effort"> + <label for="course-effort">${_("Hours of Effort per Week")}</label> + <input type="text" class="short time" id="course-effort" placeholder="HH:MM" /> + <span class="tip tip-inline">${_("Time spent on all course work")}</span> + </li> + </ol> + </section> + % endif </form> </article> - <aside class="content-supplementary" role="complimentary"> <div class="bit"> - <h3 class="title-3">How will these settings be used?</h3> - <p>Your course's schedule settings determine when students can enroll in and begin a course as well as when the course.</p> + <h3 class="title-3">${_("How will these settings be used?")}</h3> + <p>${_("Your course's schedule settings determine when students can enroll in and begin a course.")}</p> - <p>Additionally, details provided on this page are also used in edX's catalog of courses, which new and returning students use to choose new courses to study.</p> + <p>${_("Additionally, details provided on this page are also used in edX's catalog of courses, which new and returning students use to choose new courses to study.")}</p> </div> <div class="bit"> % if context_course: <% ctx_loc = context_course.location %> <%! from django.core.urlresolvers import reverse %> - <h3 class="title-3">Other Course Settings</h3> + <h3 class="title-3">${_("Other Course Settings")}</h3> <nav class="nav-related"> <ul> - <li class="nav-item"><a href="${reverse('contentstore.views.course_config_graders_page', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">Grading</a></li> - <li class="nav-item"><a href="${reverse('manage_users', kwargs=dict(location=ctx_loc))}">Course Team</a></li> - <li class="nav-item"><a href="${reverse('course_advanced_settings', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">Advanced Settings</a></li> + <li class="nav-item"><a href="${reverse('contentstore.views.course_config_graders_page', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Grading")}</a></li> + <li class="nav-item"><a href="${reverse('manage_users', kwargs=dict(location=ctx_loc))}">${_("Course Team")}</a></li> + <li class="nav-item"><a href="${reverse('course_advanced_settings', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Advanced Settings")}</a></li> </ul> </nav> % endif From 687779ba34de067c1252e5af823954a71020433d Mon Sep 17 00:00:00 2001 From: David Baumgold <david@davidbaumgold.com> Date: Wed, 19 Jun 2013 11:29:57 -0400 Subject: [PATCH 220/375] Clean up assets page notifications Remove hack to work around multiple notification click issues -- and actually resolve the issue so that the hack isn't necessary --- cms/static/js/views/assets.js | 33 ++++++++++----------------------- cms/static/js/views/feedback.js | 1 + cms/templates/asset_index.html | 7 +------ 3 files changed, 12 insertions(+), 29 deletions(-) diff --git a/cms/static/js/views/assets.js b/cms/static/js/views/assets.js index 9eb521dcb6..18ef131f52 100644 --- a/cms/static/js/views/assets.js +++ b/cms/static/js/views/assets.js @@ -9,7 +9,7 @@ function removeAsset(e){ e.preventDefault(); var that = this; - var msg = new CMS.Models.ConfirmAssetDeleteMessage({ + var msg = new CMS.Views.Prompt.Confirmation({ title: gettext("Delete File Confirmation"), message: gettext("Are you sure you wish to delete this item. It cannot be reversed!\n\nAlso any content that links/refers to this item will no longer work (e.g. broken images and/or links)"), actions: { @@ -17,15 +17,17 @@ function removeAsset(e){ text: gettext("OK"), click: function(view) { // call the back-end to actually remove the asset - $.post(view.model.get('remove_asset_url'), - { 'location': view.model.get('asset_location') }, + var url = $('.asset-library').data('remove-asset-callback-url'); + var row = $(that).closest('tr'); + $.post(url, + { 'location': row.data('id') }, function() { // show the post-commit confirmation $(".wrapper-alert-confirmation").addClass("is-shown").attr('aria-hidden','false'); - view.model.get('row_to_remove').remove(); + row.remove(); analytics.track('Deleted Asset', { 'course': course_location_analytics, - 'id': view.model.get('asset_location') + 'id': row.data('id') }); } ); @@ -38,24 +40,9 @@ function removeAsset(e){ view.hide(); } }] - }, - remove_asset_url: $('.asset-library').data('remove-asset-callback-url'), - asset_location: $(this).closest('tr').data('id'), - row_to_remove: $(this).closest('tr') + } }); - - // workaround for now. We can't spawn multiple instances of the Prompt View - // so for now, a bit of hackery to just make sure we have a single instance - // note: confirm_delete_prompt is in asset_index.html - if (confirm_delete_prompt === null) - confirm_delete_prompt = new CMS.Views.Prompt({model: msg}); - else - { - confirm_delete_prompt.model = msg; - confirm_delete_prompt.show(); - } - - return; + return msg.show(); } function showUploadModal(e) { @@ -125,4 +112,4 @@ function displayFinishedUpload(xhr) { 'course': course_location_analytics, 'asset_url': resp.url }); -} \ No newline at end of file +} diff --git a/cms/static/js/views/feedback.js b/cms/static/js/views/feedback.js index b04fb6e3d1..0cfd6fa4ef 100644 --- a/cms/static/js/views/feedback.js +++ b/cms/static/js/views/feedback.js @@ -90,6 +90,7 @@ CMS.Views.SystemFeedback = Backbone.View.extend({ var parent = CMS.Views[_.str.capitalize(this.options.type)]; if(parent && parent.active && parent.active !== this) { parent.active.stopListening(); + parent.active.undelegateEvents(); } this.$el.html(this.template(this.options)); parent.active = this; diff --git a/cms/templates/asset_index.html b/cms/templates/asset_index.html index 0006d29d38..abbc5bb1b4 100644 --- a/cms/templates/asset_index.html +++ b/cms/templates/asset_index.html @@ -8,11 +8,6 @@ <%block name="jsextra"> <script src="${static.url('js/vendor/mustache.js')}"></script> - -<script type='text/javascript'> - // we just want a singleton - confirm_delete_prompt = null; -</script> </%block> <%block name="content"> @@ -98,7 +93,7 @@ </td> <td class="delete-col"> <a href="#" data-tooltip="${_('Delete this asset')}" class="remove-asset-button"><span class="delete-icon"></span></a> - </td> + </td> </tr> % endfor </tbody> From 66439943480a1e69544b9ec8c7913c7366233601 Mon Sep 17 00:00:00 2001 From: cahrens <christina@edx.org> Date: Wed, 19 Jun 2013 13:51:40 -0400 Subject: [PATCH 221/375] Add an optional success lambda to css_click. --- .../contentstore/features/advanced-settings.py | 10 ++-------- common/djangoapps/terrain/ui_helpers.py | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/cms/djangoapps/contentstore/features/advanced-settings.py b/cms/djangoapps/contentstore/features/advanced-settings.py index a2eb79bfa2..3113603467 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.py +++ b/cms/djangoapps/contentstore/features/advanced-settings.py @@ -34,14 +34,8 @@ def press_the_notification_button(step, name): save_clicked = lambda : world.is_css_not_present('.is-shown.wrapper-notification-warning') or \ world.is_css_present('.is-shown.wrapper-notification-error') - attempts = 0 - while attempts < 5: - world.css_click(css) - if save_clicked(): - break - attempts+=1 - - assert_true(save_clicked(), 'The save button was not clicked after 5 attempts.') + assert_true(world.css_click(css, success_condition=save_clicked), + 'The save button was not clicked after 5 attempts.') @step(u'I edit the value of a policy key$') diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py index b1c5f30467..8e4330d940 100644 --- a/common/djangoapps/terrain/ui_helpers.py +++ b/common/djangoapps/terrain/ui_helpers.py @@ -58,10 +58,16 @@ def css_find(css, wait_time=5): @world.absorb -def css_click(css_selector, index=0, attempts=5): +def css_click(css_selector, index=0, attempts=5, success_condition=lambda:True): """ - Perform a click on a CSS selector, retrying if it initially fails - This function will return if the click worked (since it is try/excepting all errors) + Perform a click on a CSS selector, retrying if it initially fails. + + This function handles errors that may be thrown if the component cannot be clicked on. + However, there are cases where an error may not be thrown, and yet the operation did not + actually succeed. For those cases, a success_condition lambda can be supplied to verify that the click worked. + + This function will return True if the click worked (taking into account both errors and the optional + success_condition). """ assert is_css_present(css_selector) attempt = 0 @@ -69,8 +75,9 @@ def css_click(css_selector, index=0, attempts=5): while attempt < attempts: try: world.css_find(css_selector)[index].click() - result = True - break + if success_condition(): + result = True + break except WebDriverException: # Occasionally, MathJax or other JavaScript can cover up # an element temporarily. From e1b071be3057190100073271ebbcf785ef0eb7b9 Mon Sep 17 00:00:00 2001 From: David Ormsbee <dave@edx.org> Date: Wed, 19 Jun 2013 14:39:02 -0400 Subject: [PATCH 222/375] Initialize MakoMiddleware manually during certificate grading runs. Without this, problems fail to load because they can't find how to render themselves, and the certificate generation grading run will get an inaccurately low count of the possible points a user could get (anything they didn't see will be omitted), inflating their grade during certificate calculation and making it inconsistent with their Progress page. --- lms/djangoapps/certificates/queue.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/certificates/queue.py b/lms/djangoapps/certificates/queue.py index b4632ce9ab..af1037f903 100644 --- a/lms/djangoapps/certificates/queue.py +++ b/lms/djangoapps/certificates/queue.py @@ -3,6 +3,7 @@ from certificates.models import certificate_status_for_student from certificates.models import CertificateStatuses as status from certificates.models import CertificateWhitelist +from mitxmako.middleware import MakoMiddleware from courseware import grades, courses from django.test.client import RequestFactory from capa.xqueue_interface import XQueueInterface @@ -51,6 +52,14 @@ class XQueueCertInterface(object): """ def __init__(self, request=None): + # MakoMiddleware Note: + # Line below has the side-effect of writing to a module level lookup + # table that will allow problems to render themselves. If this is not + # present, problems that a student hasn't seen will error when loading, + # causing the grading system to under-count the possible score and + # inflate their grade. This dependency is bad and was probably recently + # introduced. This is the bandage until we can trace the root cause. + m = MakoMiddleware() # Get basic auth (username/password) for # xqueue connection if it's in the settings @@ -161,6 +170,10 @@ class XQueueCertInterface(object): cert, created = GeneratedCertificate.objects.get_or_create( user=student, course_id=course_id) + # Needed + self.request.user = student + self.request.session = {} + grade = grades.grade(student, self.request, course) is_whitelisted = self.whitelist.filter( user=student, course_id=course_id, whitelist=True).exists() @@ -211,5 +224,5 @@ class XQueueCertInterface(object): (error, msg) = self.xqueue_interface.send_to_queue( header=xheader, body=json.dumps(contents)) if error: - logger.critical('Unable to add a request to the queue') + logger.critical('Unable to add a request to the queue: {} {}'.format(error, msg)) raise Exception('Unable to send queue message') From 7b074424b540db5ad9b2c0c5840de04162680134 Mon Sep 17 00:00:00 2001 From: David Ormsbee <dave@edx.org> Date: Wed, 19 Jun 2013 14:39:02 -0400 Subject: [PATCH 223/375] Initialize MakoMiddleware manually during certificate grading runs. Without this, problems fail to load because they can't find how to render themselves, and the certificate generation grading run will get an inaccurately low count of the possible points a user could get (anything they didn't see will be omitted), inflating their grade during certificate calculation and making it inconsistent with their Progress page. --- lms/djangoapps/certificates/queue.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/certificates/queue.py b/lms/djangoapps/certificates/queue.py index b4632ce9ab..af1037f903 100644 --- a/lms/djangoapps/certificates/queue.py +++ b/lms/djangoapps/certificates/queue.py @@ -3,6 +3,7 @@ from certificates.models import certificate_status_for_student from certificates.models import CertificateStatuses as status from certificates.models import CertificateWhitelist +from mitxmako.middleware import MakoMiddleware from courseware import grades, courses from django.test.client import RequestFactory from capa.xqueue_interface import XQueueInterface @@ -51,6 +52,14 @@ class XQueueCertInterface(object): """ def __init__(self, request=None): + # MakoMiddleware Note: + # Line below has the side-effect of writing to a module level lookup + # table that will allow problems to render themselves. If this is not + # present, problems that a student hasn't seen will error when loading, + # causing the grading system to under-count the possible score and + # inflate their grade. This dependency is bad and was probably recently + # introduced. This is the bandage until we can trace the root cause. + m = MakoMiddleware() # Get basic auth (username/password) for # xqueue connection if it's in the settings @@ -161,6 +170,10 @@ class XQueueCertInterface(object): cert, created = GeneratedCertificate.objects.get_or_create( user=student, course_id=course_id) + # Needed + self.request.user = student + self.request.session = {} + grade = grades.grade(student, self.request, course) is_whitelisted = self.whitelist.filter( user=student, course_id=course_id, whitelist=True).exists() @@ -211,5 +224,5 @@ class XQueueCertInterface(object): (error, msg) = self.xqueue_interface.send_to_queue( header=xheader, body=json.dumps(contents)) if error: - logger.critical('Unable to add a request to the queue') + logger.critical('Unable to add a request to the queue: {} {}'.format(error, msg)) raise Exception('Unable to send queue message') From 51fc6280b67a59d0bcda2b9d8d5b3f75392c9734 Mon Sep 17 00:00:00 2001 From: cahrens <christina@edx.org> Date: Wed, 19 Jun 2013 16:23:00 -0400 Subject: [PATCH 224/375] Don't concatenate together multiple strings. --- cms/templates/settings.html | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cms/templates/settings.html b/cms/templates/settings.html index 55dd2b67b2..a331c481a6 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -190,7 +190,13 @@ from contentstore import utils <li class="field text" id="field-course-overview"> <label for="course-overview">${_("Course Overview")}</label> <textarea class="tinymce text-editor" id="course-overview"></textarea> - <span class="tip tip-stacked">${_("Introductions, prerequisites, FAQs that are used on ")}<a class="link-courseURL" rel="external" href="${utils.get_lms_link_for_about_page(course_location)}">${_("your course summary page")}</a>${_(" (formatted in HTML)")}</span> + <%def name='overview_text()'><% + a_link_start = '<a class="link-courseURL" rel="external" href="' + a_link_end = '">' + _("your course summary page") + '</a>' + a_link = a_link_start + utils.get_lms_link_for_about_page(course_location) + a_link_end + text = _("Introductions, prerequisites, FAQs that are used on %s (formatted in HTML)") % a_link + %>${text}</%def> + <span class="tip tip-stacked">${overview_text()}</span> </li> <li class="field video" id="field-course-introduction-video"> From 29d93aff8953c2691e79fc5b5a8c64179e250399 Mon Sep 17 00:00:00 2001 From: cahrens <christina@edx.org> Date: Wed, 19 Jun 2013 16:34:23 -0400 Subject: [PATCH 225/375] pep8 fixes. --- .../tests/test_course_settings.py | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py index 3d676390ea..d038b9f1e2 100644 --- a/cms/djangoapps/contentstore/tests/test_course_settings.py +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -1,3 +1,6 @@ +""" +Tests for Studio Course Settings. +""" import datetime import json import copy @@ -23,6 +26,9 @@ from xmodule.fields import Date class CourseTestCase(ModuleStoreTestCase): + """ + Base class for test classes below. + """ def setUp(self): """ These tests need a user in the DB so that the django Test Client @@ -53,6 +59,9 @@ class CourseTestCase(ModuleStoreTestCase): class CourseDetailsTestCase(CourseTestCase): + """ + Tests the first course settings page (course dates, overview, etc.). + """ def test_virgin_fetch(self): details = CourseDetails.fetch(self.course_location) self.assertEqual(details.course_location, self.course_location, "Location not copied into") @@ -83,9 +92,9 @@ class CourseDetailsTestCase(CourseTestCase): Test the encoder out of its original constrained purpose to see if it functions for general use """ details = {'location': Location(['tag', 'org', 'course', 'category', 'name']), - 'number': 1, - 'string': 'string', - 'datetime': datetime.datetime.now(UTC())} + 'number': 1, + 'string': 'string', + 'datetime': datetime.datetime.now(UTC())} jsondetails = json.dumps(details, cls=CourseSettingsEncoder) jsondetails = json.loads(jsondetails) @@ -123,10 +132,8 @@ class CourseDetailsTestCase(CourseTestCase): @override_settings(MKTG_URLS={'ROOT': 'dummy-root'}) def test_marketing_site_fetch(self): settings_details_url = reverse('settings_details', - kwargs= {'org': self.course_location.org, - 'name': self.course_location.name, - 'course': self.course_location.course - }) + kwargs={'org': self.course_location.org, 'name': self.course_location.name, + 'course': self.course_location.course}) with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': True}): response = self.client.get(settings_details_url) @@ -144,10 +151,8 @@ class CourseDetailsTestCase(CourseTestCase): def test_regular_site_fetch(self): settings_details_url = reverse('settings_details', - kwargs= {'org': self.course_location.org, - 'name': self.course_location.name, - 'course': self.course_location.course - }) + kwargs={'org': self.course_location.org, 'name': self.course_location.name, + 'course': self.course_location.course}) with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': False}): response = self.client.get(settings_details_url) @@ -165,6 +170,9 @@ class CourseDetailsTestCase(CourseTestCase): class CourseDetailsViewTest(CourseTestCase): + """ + Tests for modifying content on the first course settings page (course dates, overview, etc.). + """ def alter_field(self, url, details, field, val): setattr(details, field, val) # Need to partially serialize payload b/c the mock doesn't handle it correctly @@ -226,6 +234,9 @@ class CourseDetailsViewTest(CourseTestCase): class CourseGradingTest(CourseTestCase): + """ + Tests for the course settings grading page. + """ def test_initial_grader(self): descriptor = get_modulestore(self.course_location).get_item(self.course_location) test_grader = CourseGradingModel(descriptor) @@ -301,6 +312,9 @@ class CourseGradingTest(CourseTestCase): class CourseMetadataEditingTest(CourseTestCase): + """ + Tests for CourseMetadata. + """ def setUp(self): CourseTestCase.setUp(self) # add in the full class too From 0bf02cabff6194704be366775b16014069414d83 Mon Sep 17 00:00:00 2001 From: Renzo Lucioni <renzolucioni@gmail.com> Date: Wed, 19 Jun 2013 16:48:25 -0400 Subject: [PATCH 226/375] Fix for the failing acceptance tests --- common/static/coffee/spec/logger_spec.coffee | 8 ++++---- lms/djangoapps/courseware/features/navigation.py | 2 +- lms/templates/widgets/segment-io.html | 14 ++++++++++---- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/common/static/coffee/spec/logger_spec.coffee b/common/static/coffee/spec/logger_spec.coffee index 8866daa570..119acea16d 100644 --- a/common/static/coffee/spec/logger_spec.coffee +++ b/common/static/coffee/spec/logger_spec.coffee @@ -3,10 +3,10 @@ describe 'Logger', -> expect(window.log_event).toBe Logger.log describe 'log', -> - it 'sends an event to Segment.io, if the event is whitelisted', -> - spyOn(analytics, 'track') - Logger.log 'seq_goto', 'data' - expect(analytics.track).toHaveBeenCalledWith 'seq_goto', 'data' + # it 'sends an event to Segment.io, if the event is whitelisted', -> + # spyOn(analytics, 'track') + # Logger.log 'seq_goto', 'data' + # expect(analytics.track).toHaveBeenCalledWith 'seq_goto', 'data' it 'send a request to log event', -> spyOn $, 'getWithPrefix' diff --git a/lms/djangoapps/courseware/features/navigation.py b/lms/djangoapps/courseware/features/navigation.py index edd748e46f..e0f82f9251 100644 --- a/lms/djangoapps/courseware/features/navigation.py +++ b/lms/djangoapps/courseware/features/navigation.py @@ -91,7 +91,7 @@ def click_on_section(step, section): @step(u'I click on subsection "([^"]*)"$') def click_on_subsection(step, subsection): subsection_css = 'ul[id="ui-accordion-accordion-panel-0"]> li > a' - world.css_find(subsection_css)[int(subsection) - 1].click() + world.css_click(subsection_css, index=(int(subsection) - 1)) @step(u'I click on sequence "([^"]*)"$') diff --git a/lms/templates/widgets/segment-io.html b/lms/templates/widgets/segment-io.html index dea222653e..dd9787a77c 100644 --- a/lms/templates/widgets/segment-io.html +++ b/lms/templates/widgets/segment-io.html @@ -1,9 +1,7 @@ +% if settings.MITX_FEATURES.get('SEGMENT_IO_LMS'): <!-- begin Segment.io --> <script type="text/javascript"> - // Leaving this line out of the feature flag block is intentional. Pulling the line outside of the if statement allows it to serve as its own dummy object. var analytics=analytics||[];analytics.load=function(e){var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=("https:"===document.location.protocol?"https://":"http://")+"d2dq2ahtl5zl1z.cloudfront.net/analytics.js/v1/"+e+"/analytics.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(t,n);var r=function(e){return function(){analytics.push([e].concat(Array.prototype.slice.call(arguments,0)))}},i=["identify","track","trackLink","trackForm","trackClick","trackSubmit","pageview","ab","alias","ready"];for(var s=0;s<i.length;s++)analytics[i[s]]=r(i[s])}; - -% if settings.MITX_FEATURES.get('SEGMENT_IO_LMS'): analytics.load("${ settings.SEGMENT_IO_LMS_KEY }"); % if user.is_authenticated(): @@ -13,6 +11,14 @@ }); % endif -% endif </script> <!-- end Segment.io --> +% else: +<!-- dummy segment.io --> +<script type="text/javascript"> + var analytics = { + track: function() { return; } + }; +</script> +<!-- end dummy segment.io --> +% endif \ No newline at end of file From 142762c1374dbafd8cb1264cd334a9cf0d1d148d Mon Sep 17 00:00:00 2001 From: Ned Batchelder <ned@nedbatchelder.com> Date: Fri, 14 Jun 2013 15:03:57 -0400 Subject: [PATCH 227/375] Rearrange pylintrc a little bit. --- pylintrc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pylintrc b/pylintrc index d4085379b4..0d4ce31185 100644 --- a/pylintrc +++ b/pylintrc @@ -36,8 +36,9 @@ load-plugins= disable= # Never going to use these # C0301: Line too long -# W0142: Used * or ** magic # W0141: Used builtin function 'map' +# W0142: Used * or ** magic + C0301,W0141,W0142, # Might use these when the code is in better shape # C0302: Too many lines in module @@ -50,7 +51,7 @@ disable= # R0912: Too many branches # R0913: Too many arguments # R0914: Too many local variables - C0301,C0302,W0141,W0142,R0201,R0901,R0902,R0903,R0904,R0911,R0912,R0913,R0914 + C0302,R0201,R0901,R0902,R0903,R0904,R0911,R0912,R0913,R0914 [REPORTS] From c53fff9ff4a12bac48425623a0ff455acaf0dead Mon Sep 17 00:00:00 2001 From: Ned Batchelder <ned@nedbatchelder.com> Date: Fri, 14 Jun 2013 15:24:29 -0400 Subject: [PATCH 228/375] Tell pylint to shut up about us telling it to shut up. --- pylintrc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pylintrc b/pylintrc index 0d4ce31185..af958e4af4 100644 --- a/pylintrc +++ b/pylintrc @@ -35,10 +35,11 @@ load-plugins= # it should appear only once). disable= # Never going to use these +# I0011: Locally disabling W0232 # C0301: Line too long # W0141: Used builtin function 'map' # W0142: Used * or ** magic - C0301,W0141,W0142, + I0011,C0301,W0141,W0142, # Might use these when the code is in better shape # C0302: Too many lines in module From 181b1e979b22a119220c4e211f86f7f9d4cdf38c Mon Sep 17 00:00:00 2001 From: Ned Batchelder <ned@nedbatchelder.com> Date: Fri, 14 Jun 2013 13:30:47 -0400 Subject: [PATCH 229/375] Remove unused imports from common, as reported by pylint. --- common/djangoapps/cache_toolbox/core.py | 1 - common/djangoapps/course_groups/cohorts.py | 3 +-- common/djangoapps/course_groups/views.py | 10 ++-------- common/djangoapps/mitxmako/makoloader.py | 1 - common/djangoapps/status/status.py | 1 - .../student/management/commands/6002exportusers.py | 5 ----- .../student/management/commands/6002importusers.py | 6 ------ .../student/management/commands/assigngroups.py | 5 ----- .../management/commands/create_random_users.py | 4 +--- .../student/management/commands/emaillist.py | 5 ----- .../student/management/commands/massemail.py | 5 ----- .../student/management/commands/massemailtxt.py | 3 --- .../student/management/commands/pearson_dump.py | 2 +- .../management/commands/pearson_import_conf_zip.py | 5 +---- .../management/commands/tests/test_pearson.py | 2 +- .../student/management/commands/userinfo.py | 5 ----- common/djangoapps/student/views.py | 8 ++------ common/djangoapps/terrain/browser.py | 6 +++--- common/djangoapps/terrain/course_helpers.py | 3 +-- common/djangoapps/terrain/steps.py | 2 +- common/djangoapps/track/middleware.py | 2 -- common/djangoapps/util/models.py | 2 -- common/djangoapps/util/tests/test_memcache.py | 1 - common/djangoapps/util/tests/test_submit_feedback.py | 1 - common/djangoapps/util/views.py | 12 ++---------- common/lib/capa/capa/checker.py | 1 - common/lib/capa/capa/customrender.py | 2 -- common/lib/capa/capa/responsetypes.py | 1 - common/lib/capa/capa/tests/test_html_render.py | 1 - common/lib/capa/capa/util.py | 2 +- common/lib/chem/chem/chemcalc.py | 11 +---------- common/lib/symmath/symmath/formula.py | 4 +--- common/lib/symmath/symmath/symmath_check.py | 4 ---- .../xmodule/xmodule/modulestore/tests/factories.py | 1 - .../xmodule/modulestore/tests/test_modulestore.py | 4 ++-- .../xmodule/xmodule/modulestore/tests/test_mongo.py | 1 - .../combined_open_ended_modulev1.py | 3 --- .../grading_service_module.py | 1 - .../open_ended_image_submission.py | 2 -- .../open_ended_grading_classes/open_ended_module.py | 2 -- .../open_ended_grading_classes/openendedchild.py | 6 ------ common/lib/xmodule/xmodule/progress.py | 1 - common/lib/xmodule/xmodule/schematic_module.py | 1 - common/lib/xmodule/xmodule/template_module.py | 1 - common/lib/xmodule/xmodule/tests/test_html_module.py | 1 - .../lib/xmodule/xmodule/tests/test_peer_grading.py | 4 ---- .../xmodule/xmodule/tests/test_randomize_module.py | 10 +--------- common/lib/xmodule/xmodule/tests/test_stringify.py | 2 +- .../xmodule/xmodule/tests/test_util_open_ended.py | 2 +- common/lib/xmodule/xmodule/timelimit_module.py | 1 - 50 files changed, 24 insertions(+), 145 deletions(-) diff --git a/common/djangoapps/cache_toolbox/core.py b/common/djangoapps/cache_toolbox/core.py index a9c7002aa6..9a7be940b8 100644 --- a/common/djangoapps/cache_toolbox/core.py +++ b/common/djangoapps/cache_toolbox/core.py @@ -12,7 +12,6 @@ from django.core.cache import cache from django.db import DEFAULT_DB_ALIAS from . import app_settings -from xmodule.contentstore.content import StaticContent def get_instance(model, instance_or_pk, timeout=None, using=None): diff --git a/common/djangoapps/course_groups/cohorts.py b/common/djangoapps/course_groups/cohorts.py index 7924012bfe..d2c7e3a782 100644 --- a/common/djangoapps/course_groups/cohorts.py +++ b/common/djangoapps/course_groups/cohorts.py @@ -3,7 +3,6 @@ This file contains the logic for cohort groups, as exposed internally to the forums, and to the cohort admin views. """ -from django.contrib.auth.models import User from django.http import Http404 import logging import random @@ -27,7 +26,7 @@ def local_random(): """ # ironic, isn't it? global _local_random - + if _local_random is None: _local_random = random.Random() diff --git a/common/djangoapps/course_groups/views.py b/common/djangoapps/course_groups/views.py index 6d5ac43fb0..764f6c301d 100644 --- a/common/djangoapps/course_groups/views.py +++ b/common/djangoapps/course_groups/views.py @@ -1,24 +1,18 @@ from django_future.csrf import ensure_csrf_cookie -from django.contrib.auth.decorators import login_required from django.views.decorators.http import require_POST from django.contrib.auth.models import User -from django.core.context_processors import csrf from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.core.urlresolvers import reverse -from django.http import HttpResponse, HttpResponseForbidden, Http404 -from django.shortcuts import redirect +from django.http import HttpResponse import json import logging import re from courseware.courses import get_course_with_access -from mitxmako.shortcuts import render_to_response, render_to_string +from mitxmako.shortcuts import render_to_response -from .models import CourseUserGroup from . import cohorts -import track.views - log = logging.getLogger(__name__) diff --git a/common/djangoapps/mitxmako/makoloader.py b/common/djangoapps/mitxmako/makoloader.py index 6b6b31d464..06ae2219e6 100644 --- a/common/djangoapps/mitxmako/makoloader.py +++ b/common/djangoapps/mitxmako/makoloader.py @@ -7,7 +7,6 @@ from django.template.loaders.filesystem import Loader as FilesystemLoader from django.template.loaders.app_directories import Loader as AppDirectoriesLoader from mitxmako.template import Template -import mitxmako.middleware import tempdir diff --git a/common/djangoapps/status/status.py b/common/djangoapps/status/status.py index deacd9c631..b3ffd6a84c 100644 --- a/common/djangoapps/status/status.py +++ b/common/djangoapps/status/status.py @@ -6,7 +6,6 @@ from django.conf import settings import json import logging import os -import sys log = logging.getLogger(__name__) diff --git a/common/djangoapps/student/management/commands/6002exportusers.py b/common/djangoapps/student/management/commands/6002exportusers.py index 31d8092d3f..a92bb0a60c 100644 --- a/common/djangoapps/student/management/commands/6002exportusers.py +++ b/common/djangoapps/student/management/commands/6002exportusers.py @@ -11,12 +11,7 @@ import datetime import json -import os.path - -from lxml import etree - from django.core.management.base import BaseCommand -from django.conf import settings from django.contrib.auth.models import User from student.models import UserProfile diff --git a/common/djangoapps/student/management/commands/6002importusers.py b/common/djangoapps/student/management/commands/6002importusers.py index 64be84d910..1f98bd7522 100644 --- a/common/djangoapps/student/management/commands/6002importusers.py +++ b/common/djangoapps/student/management/commands/6002importusers.py @@ -3,17 +3,11 @@ ## See export for more info -import datetime import json import dateutil.parser -import os.path - -from lxml import etree - from django.core.management.base import BaseCommand -from django.conf import settings from django.contrib.auth.models import User from student.models import UserProfile diff --git a/common/djangoapps/student/management/commands/assigngroups.py b/common/djangoapps/student/management/commands/assigngroups.py index 5269c8690e..3e36bf3129 100644 --- a/common/djangoapps/student/management/commands/assigngroups.py +++ b/common/djangoapps/student/management/commands/assigngroups.py @@ -1,9 +1,4 @@ -import os.path - -from lxml import etree - from django.core.management.base import BaseCommand -from django.conf import settings from django.contrib.auth.models import User import mitxmako.middleware as middleware diff --git a/common/djangoapps/student/management/commands/create_random_users.py b/common/djangoapps/student/management/commands/create_random_users.py index 70374d02f2..3000c86601 100644 --- a/common/djangoapps/student/management/commands/create_random_users.py +++ b/common/djangoapps/student/management/commands/create_random_users.py @@ -2,9 +2,7 @@ ## A script to create some dummy users from django.core.management.base import BaseCommand -from django.conf import settings -from django.contrib.auth.models import User -from student.models import UserProfile, CourseEnrollment +from student.models import CourseEnrollment from student.views import _do_create_account, get_random_post_override diff --git a/common/djangoapps/student/management/commands/emaillist.py b/common/djangoapps/student/management/commands/emaillist.py index 4011c41bd2..d3911927ac 100644 --- a/common/djangoapps/student/management/commands/emaillist.py +++ b/common/djangoapps/student/management/commands/emaillist.py @@ -1,9 +1,4 @@ -import os.path - -from lxml import etree - from django.core.management.base import BaseCommand -from django.conf import settings from django.contrib.auth.models import User import mitxmako.middleware as middleware diff --git a/common/djangoapps/student/management/commands/massemail.py b/common/djangoapps/student/management/commands/massemail.py index c6f6e5f6d4..1bb65fd169 100644 --- a/common/djangoapps/student/management/commands/massemail.py +++ b/common/djangoapps/student/management/commands/massemail.py @@ -1,9 +1,4 @@ -import os.path - -from lxml import etree - from django.core.management.base import BaseCommand -from django.conf import settings from django.contrib.auth.models import User import mitxmako.middleware as middleware diff --git a/common/djangoapps/student/management/commands/massemailtxt.py b/common/djangoapps/student/management/commands/massemailtxt.py index 4ea75f972b..fec354e974 100644 --- a/common/djangoapps/student/management/commands/massemailtxt.py +++ b/common/djangoapps/student/management/commands/massemailtxt.py @@ -1,11 +1,8 @@ import os.path import time -from lxml import etree - from django.core.management.base import BaseCommand from django.conf import settings -from django.contrib.auth.models import User import mitxmako.middleware as middleware diff --git a/common/djangoapps/student/management/commands/pearson_dump.py b/common/djangoapps/student/management/commands/pearson_dump.py index 2aade8cf5f..0c9e215f77 100644 --- a/common/djangoapps/student/management/commands/pearson_dump.py +++ b/common/djangoapps/student/management/commands/pearson_dump.py @@ -2,7 +2,7 @@ from optparse import make_option from json import dump from datetime import datetime -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import BaseCommand from student.models import TestCenterRegistration diff --git a/common/djangoapps/student/management/commands/pearson_import_conf_zip.py b/common/djangoapps/student/management/commands/pearson_import_conf_zip.py index 2339383719..1e06a0931a 100644 --- a/common/djangoapps/student/management/commands/pearson_import_conf_zip.py +++ b/common/djangoapps/student/management/commands/pearson_import_conf_zip.py @@ -3,11 +3,8 @@ import csv from zipfile import ZipFile, is_zipfile from time import strptime, strftime -from collections import OrderedDict from datetime import datetime -from os.path import isdir -from optparse import make_option -from dogapi import dog_http_api, dog_stats_api +from dogapi import dog_http_api from django.core.management.base import BaseCommand, CommandError from django.conf import settings diff --git a/common/djangoapps/student/management/commands/tests/test_pearson.py b/common/djangoapps/student/management/commands/tests/test_pearson.py index 65d628fba0..ca6e20673b 100644 --- a/common/djangoapps/student/management/commands/tests/test_pearson.py +++ b/common/djangoapps/student/management/commands/tests/test_pearson.py @@ -14,7 +14,7 @@ from django.test import TestCase from django.core.management import call_command from nose.plugins.skip import SkipTest -from student.models import User, TestCenterRegistration, TestCenterUser, get_testcenter_registration +from student.models import User, TestCenterUser, get_testcenter_registration log = logging.getLogger(__name__) diff --git a/common/djangoapps/student/management/commands/userinfo.py b/common/djangoapps/student/management/commands/userinfo.py index e458995284..5467db1733 100644 --- a/common/djangoapps/student/management/commands/userinfo.py +++ b/common/djangoapps/student/management/commands/userinfo.py @@ -1,9 +1,4 @@ -import os.path - -from lxml import etree - from django.core.management.base import BaseCommand -from django.conf import settings from django.contrib.auth.models import User import mitxmako.middleware as middleware diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index f129f1b4b1..de3e52b080 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -4,7 +4,6 @@ import json import logging import random import string -import sys import urllib import uuid import time @@ -20,9 +19,9 @@ from django.core.mail import send_mail from django.core.urlresolvers import reverse from django.core.validators import validate_email, validate_slug, ValidationError from django.db import IntegrityError, transaction -from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseNotAllowed, HttpResponseRedirect, Http404 +from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseNotAllowed, Http404 from django.shortcuts import redirect -from django_future.csrf import ensure_csrf_cookie, csrf_exempt +from django_future.csrf import ensure_csrf_cookie from django.utils.http import cookie_date from mitxmako.shortcuts import render_to_response, render_to_string @@ -39,14 +38,11 @@ from certificates.models import CertificateStatuses, certificate_status_for_stud from xmodule.course_module import CourseDescriptor from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.django import modulestore -from xmodule.modulestore import Location from collections import namedtuple from courseware.courses import get_courses, sort_by_announcement from courseware.access import has_access -from courseware.views import get_module_for_descriptor, jump_to -from courseware.model_data import ModelDataCache from statsd import statsd from pytz import UTC diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index d2a9480b35..d237edc4b7 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -4,7 +4,6 @@ Browser set up for acceptance tests. #pylint: disable=E1101 #pylint: disable=W0613 -#pylint: disable=W0611 from lettuce import before, after, world from splinter.browser import Browser @@ -15,8 +14,9 @@ from selenium.common.exceptions import WebDriverException # Let the LMS and CMS do their one-time setup # For example, setting up mongo caches -from lms import one_time_startup -from cms import one_time_startup +# These names aren't used, but do important work on import. +from lms import one_time_startup # pylint: disable=W0611 +from cms import one_time_startup # pylint: disable=W0611 # There is an import issue when using django-staticfiles with lettuce # Lettuce assumes that we are using django.contrib.staticfiles, diff --git a/common/djangoapps/terrain/course_helpers.py b/common/djangoapps/terrain/course_helpers.py index fc666d7904..fbc9409e7b 100644 --- a/common/djangoapps/terrain/course_helpers.py +++ b/common/djangoapps/terrain/course_helpers.py @@ -1,7 +1,7 @@ # pylint: disable=C0111 # pylint: disable=W0621 -from lettuce import world, step +from lettuce import world from .factories import * from django.conf import settings from django.http import HttpRequest @@ -15,7 +15,6 @@ from xmodule.templates import update_templates from bs4 import BeautifulSoup import os.path from urllib import quote_plus -from lettuce.django import django_url @world.absorb diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index 6e512982b7..f31be894f9 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -15,7 +15,7 @@ from lettuce import world, step from .course_helpers import * from .ui_helpers import * from lettuce.django import django_url -from nose.tools import assert_equals, assert_in +from nose.tools import assert_equals from logging import getLogger logger = getLogger(__name__) diff --git a/common/djangoapps/track/middleware.py b/common/djangoapps/track/middleware.py index 52d914aeef..7fc02d9969 100644 --- a/common/djangoapps/track/middleware.py +++ b/common/djangoapps/track/middleware.py @@ -1,7 +1,5 @@ import json -from django.conf import settings - import views diff --git a/common/djangoapps/util/models.py b/common/djangoapps/util/models.py index 71a8362390..6b20219993 100644 --- a/common/djangoapps/util/models.py +++ b/common/djangoapps/util/models.py @@ -1,3 +1 @@ -from django.db import models - # Create your models here. diff --git a/common/djangoapps/util/tests/test_memcache.py b/common/djangoapps/util/tests/test_memcache.py index de8d352c38..60b3a0d0cc 100644 --- a/common/djangoapps/util/tests/test_memcache.py +++ b/common/djangoapps/util/tests/test_memcache.py @@ -4,7 +4,6 @@ Tests for memcache in util app from django.test import TestCase from django.core.cache import get_cache -from django.conf import settings from util.memcache import safe_key diff --git a/common/djangoapps/util/tests/test_submit_feedback.py b/common/djangoapps/util/tests/test_submit_feedback.py index b66d3d642b..6461ffa8b7 100644 --- a/common/djangoapps/util/tests/test_submit_feedback.py +++ b/common/djangoapps/util/tests/test_submit_feedback.py @@ -1,6 +1,5 @@ """Tests for the Zendesk""" -from django.conf import settings from django.contrib.auth.models import AnonymousUser from django.http import Http404 from django.test import TestCase diff --git a/common/djangoapps/util/views.py b/common/djangoapps/util/views.py index aa592d25e8..851202caec 100644 --- a/common/djangoapps/util/views.py +++ b/common/djangoapps/util/views.py @@ -1,20 +1,12 @@ -import datetime import json import logging -import pprint import sys from django.conf import settings -from django.contrib.auth.models import User -from django.core.context_processors import csrf -from django.core.mail import send_mail from django.core.validators import ValidationError, validate_email -from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed, HttpResponseServerError -from django.shortcuts import redirect -from django_future.csrf import ensure_csrf_cookie +from django.http import Http404, HttpResponse, HttpResponseNotAllowed from dogapi import dog_stats_api -from mitxmako.shortcuts import render_to_response, render_to_string -from urllib import urlencode +from mitxmako.shortcuts import render_to_response import zendesk import calc diff --git a/common/lib/capa/capa/checker.py b/common/lib/capa/capa/checker.py index 15358aac9e..87cf68d230 100755 --- a/common/lib/capa/capa/checker.py +++ b/common/lib/capa/capa/checker.py @@ -10,7 +10,6 @@ import sys from path import path from cStringIO import StringIO -from collections import defaultdict from .calc import UndefinedVariable from .capa_problem import LoncapaProblem diff --git a/common/lib/capa/capa/customrender.py b/common/lib/capa/capa/customrender.py index 60d3ce578b..9d7ff719ac 100644 --- a/common/lib/capa/capa/customrender.py +++ b/common/lib/capa/capa/customrender.py @@ -10,8 +10,6 @@ from .registry import TagRegistry import logging import re -import shlex # for splitting quoted strings -import json from lxml import etree import xml.sax.saxutils as saxutils diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 80227490da..be70e3866c 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -11,7 +11,6 @@ Used by capa_problem.py # standard library imports import abc import cgi -import hashlib import inspect import json import logging diff --git a/common/lib/capa/capa/tests/test_html_render.py b/common/lib/capa/capa/tests/test_html_render.py index 62605b48f5..9bc326d7b9 100644 --- a/common/lib/capa/capa/tests/test_html_render.py +++ b/common/lib/capa/capa/tests/test_html_render.py @@ -2,7 +2,6 @@ import unittest from lxml import etree import os import textwrap -import json import mock diff --git a/common/lib/capa/capa/util.py b/common/lib/capa/capa/util.py index ec43da6093..433e99171d 100644 --- a/common/lib/capa/capa/util.py +++ b/common/lib/capa/capa/util.py @@ -1,4 +1,4 @@ -from calc import evaluator, UndefinedVariable +from calc import evaluator from cmath import isinf #----------------------------------------------------------------------------- diff --git a/common/lib/chem/chem/chemcalc.py b/common/lib/chem/chem/chemcalc.py index 5b80005044..612e63c0f0 100644 --- a/common/lib/chem/chem/chemcalc.py +++ b/common/lib/chem/chem/chemcalc.py @@ -1,16 +1,7 @@ from __future__ import division -import copy from fractions import Fraction -import logging -import math -import operator -import re -import numpy -import numbers -import scipy.constants -from pyparsing import (Literal, Keyword, Word, nums, StringEnd, Optional, - Forward, OneOrMore, ParseException) +from pyparsing import (Literal, StringEnd, OneOrMore, ParseException) import nltk from nltk.tree import Tree diff --git a/common/lib/symmath/symmath/formula.py b/common/lib/symmath/symmath/formula.py index 8369baa27c..a926d9ae45 100644 --- a/common/lib/symmath/symmath/formula.py +++ b/common/lib/symmath/symmath/formula.py @@ -10,7 +10,6 @@ # Provides sympy representation. import os -import sys import string import re import logging @@ -25,8 +24,7 @@ from sympy.physics.quantum.state import * # from sympy.core.operations import LatticeOp # import sympy.physics.quantum.qubit -import urllib -from xml.sax.saxutils import escape, unescape +from xml.sax.saxutils import unescape import sympy import unicodedata from lxml import etree diff --git a/common/lib/symmath/symmath/symmath_check.py b/common/lib/symmath/symmath/symmath_check.py index 65a17883f5..3f09ebf659 100644 --- a/common/lib/symmath/symmath/symmath_check.py +++ b/common/lib/symmath/symmath/symmath_check.py @@ -8,10 +8,6 @@ # # Takes in math expressions given as Presentation MathML (from ASCIIMathML), converts to Content MathML using SnuggleTeX -import os -import sys -import string -import re import traceback from .formula import * import logging diff --git a/common/lib/xmodule/xmodule/modulestore/tests/factories.py b/common/lib/xmodule/xmodule/modulestore/tests/factories.py index 99c5ec2c91..0a62849d8d 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/factories.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/factories.py @@ -1,5 +1,4 @@ from factory import Factory, lazy_attribute_sequence, lazy_attribute -from time import gmtime from uuid import uuid4 from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_modulestore.py b/common/lib/xmodule/xmodule/modulestore/tests/test_modulestore.py index 469eedac05..1e2035075a 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_modulestore.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_modulestore.py @@ -1,6 +1,6 @@ -from nose.tools import assert_equals, assert_raises, assert_not_equals, with_setup +from nose.tools import assert_equals, assert_raises -from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem +from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.search import path_to_location diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py index 07e6124537..c5ef0d751a 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py @@ -1,6 +1,5 @@ import pymongo -from mock import Mock from nose.tools import assert_equals, assert_raises, assert_not_equals, assert_false from pprint import pprint diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py index 01be4c61ab..9fc438d4c0 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py @@ -1,13 +1,10 @@ import json import logging from lxml import etree -from lxml.html import rewrite_links from xmodule.timeinfo import TimeInfo from xmodule.capa_module import ComplexEncoder -from xmodule.editing_module import EditingDescriptor from xmodule.progress import Progress from xmodule.stringify import stringify_children -from xmodule.xml_module import XmlDescriptor import self_assessment_module import open_ended_module from .combined_open_ended_rubric import CombinedOpenEndedRubric, GRADER_TYPE_IMAGE_DICT, HUMAN_GRADER_TYPE, LEGEND_LIST diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/grading_service_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/grading_service_module.py index 3e3f943cd7..6857876703 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/grading_service_module.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/grading_service_module.py @@ -3,7 +3,6 @@ import json import logging import requests from requests.exceptions import RequestException, ConnectionError, HTTPError -import sys from .combined_open_ended_rubric import CombinedOpenEndedRubric from lxml import etree diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_image_submission.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_image_submission.py index 2eb9502269..ea5c3b3527 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_image_submission.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_image_submission.py @@ -14,9 +14,7 @@ from urlparse import urlparse import requests from boto.s3.connection import S3Connection from boto.s3.key import Key -import pickle import logging -import re log = logging.getLogger(__name__) diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py index 1e5b1b233b..2ac55a8318 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py @@ -11,10 +11,8 @@ from lxml import etree import capa.xqueue_interface as xqueue_interface from xmodule.capa_module import ComplexEncoder -from xmodule.editing_module import EditingDescriptor from xmodule.progress import Progress from xmodule.stringify import stringify_children -from xmodule.xml_module import XmlDescriptor from capa.util import * import openendedchild diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py index b5d4e1b676..4f524d2cd7 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py @@ -3,14 +3,8 @@ import logging from lxml.html.clean import Cleaner, autolink_html import re -from xmodule.capa_module import ComplexEncoder import open_ended_image_submission -from xmodule.editing_module import EditingDescriptor -from xmodule.html_checker import check_html from xmodule.progress import Progress -from xmodule.stringify import stringify_children -from xmodule.xml_module import XmlDescriptor -from xmodule.modulestore import Location from capa.util import * from .peer_grading_service import PeerGradingService, MockPeerGradingService import controller_query_service diff --git a/common/lib/xmodule/xmodule/progress.py b/common/lib/xmodule/xmodule/progress.py index 7adbb02646..bad5105fd0 100644 --- a/common/lib/xmodule/xmodule/progress.py +++ b/common/lib/xmodule/xmodule/progress.py @@ -13,7 +13,6 @@ For most subclassing needs, you should only need to reimplement frac() and __str__(). ''' -from collections import namedtuple import numbers diff --git a/common/lib/xmodule/xmodule/schematic_module.py b/common/lib/xmodule/xmodule/schematic_module.py index d15d629c24..83bcc5351d 100644 --- a/common/lib/xmodule/xmodule/schematic_module.py +++ b/common/lib/xmodule/xmodule/schematic_module.py @@ -1,4 +1,3 @@ -import json from .x_module import XModule, XModuleDescriptor diff --git a/common/lib/xmodule/xmodule/template_module.py b/common/lib/xmodule/xmodule/template_module.py index 9a9666c0b6..bf8f616913 100644 --- a/common/lib/xmodule/xmodule/template_module.py +++ b/common/lib/xmodule/xmodule/template_module.py @@ -3,7 +3,6 @@ from xmodule.raw_module import RawDescriptor from lxml import etree from mako.template import Template from xmodule.modulestore.django import modulestore -import logging class CustomTagModule(XModule): diff --git a/common/lib/xmodule/xmodule/tests/test_html_module.py b/common/lib/xmodule/xmodule/tests/test_html_module.py index e0a49ed98f..4fe0242378 100644 --- a/common/lib/xmodule/xmodule/tests/test_html_module.py +++ b/common/lib/xmodule/xmodule/tests/test_html_module.py @@ -3,7 +3,6 @@ import unittest from mock import Mock from xmodule.html_module import HtmlModule -from xmodule.modulestore import Location from . import get_test_system diff --git a/common/lib/xmodule/xmodule/tests/test_peer_grading.py b/common/lib/xmodule/xmodule/tests/test_peer_grading.py index 3e1a578118..c386f77e9b 100644 --- a/common/lib/xmodule/xmodule/tests/test_peer_grading.py +++ b/common/lib/xmodule/xmodule/tests/test_peer_grading.py @@ -2,10 +2,6 @@ import unittest from xmodule.modulestore import Location from .import get_test_system from test_util_open_ended import MockQueryDict, DummyModulestore -import json - -from xmodule.peer_grading_module import PeerGradingModule, PeerGradingDescriptor -from xmodule.open_ended_grading_classes.grading_service_module import GradingServiceError import logging diff --git a/common/lib/xmodule/xmodule/tests/test_randomize_module.py b/common/lib/xmodule/xmodule/tests/test_randomize_module.py index 81935c4013..81ba45b56c 100644 --- a/common/lib/xmodule/xmodule/tests/test_randomize_module.py +++ b/common/lib/xmodule/xmodule/tests/test_randomize_module.py @@ -1,11 +1,6 @@ import unittest -from time import strptime -from fs.memoryfs import MemoryFS - -from mock import Mock, patch - -from xmodule.modulestore.xml import ImportSystem, XMLModuleStore +from .test_course_module import DummySystem as DummyImportSystem ORG = 'test_org' COURSE = 'test_course' @@ -13,9 +8,6 @@ COURSE = 'test_course' START = '2013-01-01T01:00:00' -from .test_course_module import DummySystem as DummyImportSystem - - class RandomizeModuleTestCase(unittest.TestCase): """Make sure the randomize module works""" @staticmethod diff --git a/common/lib/xmodule/xmodule/tests/test_stringify.py b/common/lib/xmodule/xmodule/tests/test_stringify.py index e44b93b0b8..6c2e44eed5 100644 --- a/common/lib/xmodule/xmodule/tests/test_stringify.py +++ b/common/lib/xmodule/xmodule/tests/test_stringify.py @@ -1,4 +1,4 @@ -from nose.tools import assert_equals, assert_true, assert_false +from nose.tools import assert_equals from lxml import etree from xmodule.stringify import stringify_children diff --git a/common/lib/xmodule/xmodule/tests/test_util_open_ended.py b/common/lib/xmodule/xmodule/tests/test_util_open_ended.py index 9dbb17ae2f..63fb4631c9 100644 --- a/common/lib/xmodule/xmodule/tests/test_util_open_ended.py +++ b/common/lib/xmodule/xmodule/tests/test_util_open_ended.py @@ -1,6 +1,6 @@ from .import get_test_system from xmodule.modulestore import Location -from xmodule.modulestore.xml import ImportSystem, XMLModuleStore +from xmodule.modulestore.xml import XMLModuleStore from xmodule.tests.test_export import DATA_DIR OPEN_ENDED_GRADING_INTERFACE = { diff --git a/common/lib/xmodule/xmodule/timelimit_module.py b/common/lib/xmodule/xmodule/timelimit_module.py index 6be14e7574..9446176f01 100644 --- a/common/lib/xmodule/xmodule/timelimit_module.py +++ b/common/lib/xmodule/xmodule/timelimit_module.py @@ -1,4 +1,3 @@ -import json import logging from lxml import etree From df6d3f9b2f9f9023b4f2710c8c1ee5c05aeef9b1 Mon Sep 17 00:00:00 2001 From: Ned Batchelder <ned@nedbatchelder.com> Date: Mon, 17 Jun 2013 20:43:18 -0400 Subject: [PATCH 230/375] Fix strings that should be raw. --- cms/djangoapps/models/settings/course_details.py | 4 ++-- common/djangoapps/student/management/commands/set_staff.py | 2 +- common/djangoapps/student/views.py | 6 +++--- common/djangoapps/terrain/steps.py | 2 +- common/lib/capa/capa/capa_problem.py | 4 ++-- common/lib/capa/capa/customrender.py | 4 ++-- common/lib/capa/capa/inputtypes.py | 2 +- common/lib/capa/capa/responsetypes.py | 5 ++--- common/lib/symmath/symmath/formula.py | 4 ++-- common/lib/xmodule/xmodule/modulestore/xml.py | 2 +- common/lib/xmodule/xmodule/tests/test_stringify.py | 2 +- lms/djangoapps/course_wiki/views.py | 2 +- lms/djangoapps/foldit/views.py | 2 +- 13 files changed, 20 insertions(+), 21 deletions(-) diff --git a/cms/djangoapps/models/settings/course_details.py b/cms/djangoapps/models/settings/course_details.py index 3f0c87917a..884a4e4fef 100644 --- a/cms/djangoapps/models/settings/course_details.py +++ b/cms/djangoapps/models/settings/course_details.py @@ -153,9 +153,9 @@ class CourseDetails(object): if not raw_video: return None - keystring_matcher = re.search('(?<=embed/)[a-zA-Z0-9_-]+', raw_video) + keystring_matcher = re.search(r'(?<=embed/)[a-zA-Z0-9_-]+', raw_video) if keystring_matcher is None: - keystring_matcher = re.search('<?=\d+:[a-zA-Z0-9_-]+', raw_video) + keystring_matcher = re.search(r'<?=\d+:[a-zA-Z0-9_-]+', raw_video) if keystring_matcher: return keystring_matcher.group(0) diff --git a/common/djangoapps/student/management/commands/set_staff.py b/common/djangoapps/student/management/commands/set_staff.py index 30d0483f50..869e37f13b 100644 --- a/common/djangoapps/student/management/commands/set_staff.py +++ b/common/djangoapps/student/management/commands/set_staff.py @@ -26,7 +26,7 @@ class Command(BaseCommand): raise CommandError('Usage is set_staff {0}'.format(self.args)) for user in args: - if re.match('[^@]+@[^@]+\.[^@]+', user): + if re.match(r'[^@]+@[^@]+\.[^@]+', user): try: v = User.objects.get(email=user) except: diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index de3e52b080..4da7b9d789 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -3,6 +3,7 @@ import feedparser import json import logging import random +import re import string import urllib import uuid @@ -95,9 +96,8 @@ def course_from_id(course_id): course_loc = CourseDescriptor.id_to_location(course_id) return modulestore().get_instance(course_id, course_loc) -import re -day_pattern = re.compile('\s\d+,\s') -multimonth_pattern = re.compile('\s?\-\s?\S+\s') +day_pattern = re.compile(r'\s\d+,\s') +multimonth_pattern = re.compile(r'\s?\-\s?\S+\s') def get_date_for_press(publish_date): diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index f31be894f9..e69476a5b7 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -21,7 +21,7 @@ from logging import getLogger logger = getLogger(__name__) -@step(u'I wait (?:for )?"(\d+)" seconds?$') +@step(r'I wait (?:for )?"(\d+)" seconds?$') def wait(step, seconds): world.wait(seconds) diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index 2a9f3d82a3..d620bac60a 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -103,8 +103,8 @@ class LoncapaProblem(object): self.input_state = state.get('input_state', {}) # Convert startouttext and endouttext to proper <text></text> - problem_text = re.sub("startouttext\s*/", "text", problem_text) - problem_text = re.sub("endouttext\s*/", "/text", problem_text) + problem_text = re.sub(r"startouttext\s*/", "text", problem_text) + problem_text = re.sub(r"endouttext\s*/", "/text", problem_text) self.problem_text = problem_text # parse problem XML file into an element tree diff --git a/common/lib/capa/capa/customrender.py b/common/lib/capa/capa/customrender.py index 9d7ff719ac..f7d586c9d5 100644 --- a/common/lib/capa/capa/customrender.py +++ b/common/lib/capa/capa/customrender.py @@ -26,7 +26,7 @@ class MathRenderer(object): tags = ['math'] def __init__(self, system, xml): - ''' + r''' Render math using latex-like formatting. Examples: @@ -41,7 +41,7 @@ class MathRenderer(object): self.system = system self.xml = xml - mathstr = re.sub('\$(.*)\$', r'[mathjaxinline]\1[/mathjaxinline]', xml.text) + mathstr = re.sub(r'\$(.*)\$', r'[mathjaxinline]\1[/mathjaxinline]', xml.text) mtag = 'mathjax' if not r'\displaystyle' in mathstr: mtag += 'inline' diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 446b832dd7..f026568da1 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -856,7 +856,7 @@ class ImageInput(InputTypeBase): """ if value is of the form [x,y] then parse it and send along coordinates of previous answer """ - m = re.match('\[([0-9]+),([0-9]+)]', + m = re.match(r'\[([0-9]+),([0-9]+)]', self.value.strip().replace(' ', '')) if m: # Note: we subtract 15 to compensate for the size of the dot on the screen. diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index be70e3866c..97319bdb9e 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1902,8 +1902,7 @@ class ImageResponse(LoncapaResponse): if not given: # No answer to parse. Mark as incorrect and move on continue # parse given answer - m = re.match( - '\[([0-9]+),([0-9]+)]', given.strip().replace(' ', '')) + m = re.match(r'\[([0-9]+),([0-9]+)]', given.strip().replace(' ', '')) if not m: raise Exception('[capamodule.capa.responsetypes.imageinput] ' 'error grading %s (input=%s)' % (aid, given)) @@ -1918,7 +1917,7 @@ class ImageResponse(LoncapaResponse): # parse expected answer # TODO: Compile regexp on file load m = re.match( - '[\(\[]([0-9]+),([0-9]+)[\)\]]-[\(\[]([0-9]+),([0-9]+)[\)\]]', + r'[\(\[]([0-9]+),([0-9]+)[\)\]]-[\(\[]([0-9]+),([0-9]+)[\)\]]', solution_rectangle.strip().replace(' ', '')) if not m: msg = 'Error in problem specification! cannot parse rectangle in %s' % ( diff --git a/common/lib/symmath/symmath/formula.py b/common/lib/symmath/symmath/formula.py index a926d9ae45..ca4e20ace3 100644 --- a/common/lib/symmath/symmath/formula.py +++ b/common/lib/symmath/symmath/formula.py @@ -50,7 +50,7 @@ class dot(sympy.operations.LatticeOp): # my dot product def _print_dot(self, expr): - return '{((%s) \cdot (%s))}' % (expr.args[0], expr.args[1]) + return r'{((%s) \cdot (%s))}' % (expr.args[0], expr.args[1]) LatexPrinter._print_dot = _print_dot @@ -202,7 +202,7 @@ class formula(object): return xml def preprocess_pmathml(self, xml): - ''' + r''' Pre-process presentation MathML from ASCIIMathML to make it more acceptable for SnuggleTeX, and also to accomodate some sympy conventions (eg hat(i) for \hat{i}). diff --git a/common/lib/xmodule/xmodule/modulestore/xml.py b/common/lib/xmodule/xmodule/modulestore/xml.py index a704fc2ae8..ef5fa617de 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml.py +++ b/common/lib/xmodule/xmodule/modulestore/xml.py @@ -38,7 +38,7 @@ log = logging.getLogger(__name__) # into the cms from xml def clean_out_mako_templating(xml_string): xml_string = xml_string.replace('%include', 'include') - xml_string = re.sub("(?m)^\s*%.*$", '', xml_string) + xml_string = re.sub(r"(?m)^\s*%.*$", '', xml_string) return xml_string diff --git a/common/lib/xmodule/xmodule/tests/test_stringify.py b/common/lib/xmodule/xmodule/tests/test_stringify.py index 6c2e44eed5..49852ee233 100644 --- a/common/lib/xmodule/xmodule/tests/test_stringify.py +++ b/common/lib/xmodule/xmodule/tests/test_stringify.py @@ -12,7 +12,7 @@ def test_stringify(): def test_stringify_again(): - html = """<html name="Voltage Source Answer" >A voltage source is non-linear! + html = r"""<html name="Voltage Source Answer" >A voltage source is non-linear! <div align="center"> <img src="/static/images/circuits/voltage-source.png"/> \(V=V_C\) diff --git a/lms/djangoapps/course_wiki/views.py b/lms/djangoapps/course_wiki/views.py index 6ab106ed70..74ef7d4a74 100644 --- a/lms/djangoapps/course_wiki/views.py +++ b/lms/djangoapps/course_wiki/views.py @@ -49,7 +49,7 @@ def course_wiki_redirect(request, course_id): if not course_slug: log.exception("This course is improperly configured. The slug cannot be empty.") valid_slug = False - if re.match('^[-\w\.]+$', course_slug) is None: + if re.match(r'^[-\w\.]+$', course_slug) is None: log.exception("This course is improperly configured. The slug can only contain letters, numbers, periods or hyphens.") valid_slug = False diff --git a/lms/djangoapps/foldit/views.py b/lms/djangoapps/foldit/views.py index da361a2a82..76d9bfff98 100644 --- a/lms/djangoapps/foldit/views.py +++ b/lms/djangoapps/foldit/views.py @@ -46,7 +46,7 @@ def foldit_ops(request): # To allow for fixes without breaking this, the regex should only # match unquoted strings, a = re.compile(r':([a-zA-Z]*),') - puzzle_scores_json = re.sub(a, ':"\g<1>",', puzzle_scores_json) + puzzle_scores_json = re.sub(a, r':"\g<1>",', puzzle_scores_json) puzzle_scores = json.loads(puzzle_scores_json) responses.append(save_scores(request.user, puzzle_scores)) From 61b53713d2e1d40b6f15a2a51c6dd8e303f04e27 Mon Sep 17 00:00:00 2001 From: Ned Batchelder <ned@nedbatchelder.com> Date: Tue, 18 Jun 2013 22:29:53 -0400 Subject: [PATCH 231/375] Remove unused imports from lms, as detected by pylint. --- lms/djangoapps/circuit/models.py | 3 --- lms/djangoapps/circuit/views.py | 5 +---- lms/djangoapps/courseware/access.py | 1 - lms/djangoapps/courseware/courses.py | 6 ------ .../courseware/management/commands/clean_xml.py | 3 --- .../courseware/management/commands/metadata_to_json.py | 1 - lms/djangoapps/courseware/module_render.py | 1 - lms/djangoapps/courseware/tabs.py | 7 ------- lms/djangoapps/courseware/tests/test_access.py | 2 +- lms/djangoapps/courseware/tests/test_masquerade.py | 2 +- lms/djangoapps/courseware/tests/test_module_render.py | 1 - lms/djangoapps/dashboard/models.py | 2 -- lms/djangoapps/dashboard/views.py | 5 +---- lms/djangoapps/debug/models.py | 2 -- lms/djangoapps/debug/views.py | 2 +- lms/djangoapps/django_comment_client/helpers.py | 3 --- .../management/commands/show_permissions.py | 1 - lms/djangoapps/django_comment_client/mustache_helpers.py | 1 - lms/djangoapps/django_comment_client/permissions.py | 5 ----- lms/djangoapps/django_comment_client/tests/test_models.py | 3 +-- lms/djangoapps/foldit/models.py | 3 --- lms/djangoapps/foldit/tests.py | 1 - .../instructor/management/commands/compute_grades.py | 8 -------- lms/djangoapps/instructor/offline_gradecalc.py | 6 +----- lms/djangoapps/instructor/tests/test_enrollment.py | 1 - lms/djangoapps/instructor/tests/test_gradebook.py | 4 +--- lms/djangoapps/instructor/tests/test_xss.py | 1 - lms/djangoapps/instructor_task/tests/test_integration.py | 1 - lms/djangoapps/instructor_task/tests/test_tasks.py | 2 +- .../management/commands/generate_serial_numbers.py | 2 -- .../licenses/management/commands/import_serial_numbers.py | 1 - .../lms_migration/management/commands/create_groups.py | 5 +---- .../lms_migration/management/commands/create_user.py | 1 - .../management/commands/manage_course_groups.py | 8 -------- lms/djangoapps/lms_migration/migrate.py | 1 - lms/djangoapps/notes/tests.py | 2 -- lms/djangoapps/notes/views.py | 1 - lms/djangoapps/open_ended_grading/staff_grading.py | 1 - lms/djangoapps/open_ended_grading/tests.py | 1 - lms/djangoapps/open_ended_grading/views.py | 1 - .../management/commands/init_psychometrics.py | 4 ---- lms/djangoapps/static_template_view/models.py | 2 -- lms/djangoapps/staticbook/models.py | 2 -- lms/djangoapps/staticbook/views.py | 1 - lms/envs/common.py | 2 +- lms/envs/dev_edx4edx.py | 1 - lms/envs/dev_ike.py | 1 - lms/lib/comment_client/comment_client.py | 2 ++ lms/lib/perfstats/models.py | 2 -- lms/one_time_startup.py | 3 +-- lms/urls.py | 3 ++- 51 files changed, 16 insertions(+), 114 deletions(-) diff --git a/lms/djangoapps/circuit/models.py b/lms/djangoapps/circuit/models.py index 21a70bcb25..8da678f08a 100644 --- a/lms/djangoapps/circuit/models.py +++ b/lms/djangoapps/circuit/models.py @@ -1,7 +1,4 @@ -import uuid - from django.db import models -from django.contrib.auth.models import User class ServerCircuit(models.Model): diff --git a/lms/djangoapps/circuit/views.py b/lms/djangoapps/circuit/views.py index 40a31a2e3a..cc85c2a452 100644 --- a/lms/djangoapps/circuit/views.py +++ b/lms/djangoapps/circuit/views.py @@ -1,13 +1,10 @@ import json -import os import xml.etree.ElementTree -from django.conf import settings from django.http import Http404 from django.http import HttpResponse -from django.shortcuts import redirect -from mitxmako.shortcuts import render_to_response, render_to_string +from mitxmako.shortcuts import render_to_response from .models import ServerCircuit diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py index 07987a8edf..e25f44b939 100644 --- a/lms/djangoapps/courseware/access.py +++ b/lms/djangoapps/courseware/access.py @@ -2,7 +2,6 @@ Ideally, it will be the only place that needs to know about any special settings like DISABLE_START_DATES""" import logging -import time from datetime import datetime, timedelta from functools import partial diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index 3e1162bc03..71c9630964 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -1,14 +1,9 @@ from collections import defaultdict from fs.errors import ResourceNotFoundError -from functools import wraps import logging import inspect -from lxml.html import rewrite_links - from path import path -from django.conf import settings -from django.core.urlresolvers import reverse from django.http import Http404 from .module_render import get_module @@ -18,7 +13,6 @@ from xmodule.modulestore.django import modulestore from xmodule.contentstore.content import StaticContent from xmodule.modulestore.xml import XMLModuleStore from xmodule.modulestore.exceptions import ItemNotFoundError -from xmodule.x_module import XModule from courseware.model_data import ModelDataCache from static_replace import replace_static_urls from courseware.access import has_access diff --git a/lms/djangoapps/courseware/management/commands/clean_xml.py b/lms/djangoapps/courseware/management/commands/clean_xml.py index 1989361b85..45674f66e0 100644 --- a/lms/djangoapps/courseware/management/commands/clean_xml.py +++ b/lms/djangoapps/courseware/management/commands/clean_xml.py @@ -2,15 +2,12 @@ import os import sys import traceback -from filecmp import dircmp from fs.osfs import OSFS from path import path -from lxml import etree from django.core.management.base import BaseCommand from xmodule.modulestore.xml import XMLModuleStore -from xmodule.errortracker import make_error_tracker def traverse_tree(course): diff --git a/lms/djangoapps/courseware/management/commands/metadata_to_json.py b/lms/djangoapps/courseware/management/commands/metadata_to_json.py index 58d087c316..a910db7028 100644 --- a/lms/djangoapps/courseware/management/commands/metadata_to_json.py +++ b/lms/djangoapps/courseware/management/commands/metadata_to_json.py @@ -2,7 +2,6 @@ A script to walk a course xml tree, generate a dictionary of all the metadata, and print it out as a json dict. """ -import os import sys import json diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index ab0306ed2e..3ffb1d1b1d 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -1,6 +1,5 @@ import json import logging -import pyparsing import re import sys import static_replace diff --git a/lms/djangoapps/courseware/tabs.py b/lms/djangoapps/courseware/tabs.py index 42b1c05743..149542c344 100644 --- a/lms/djangoapps/courseware/tabs.py +++ b/lms/djangoapps/courseware/tabs.py @@ -11,23 +11,16 @@ actually generates the CourseTab. from collections import namedtuple import logging -import json from django.conf import settings from django.core.urlresolvers import reverse -from fs.errors import ResourceNotFoundError - from courseware.access import has_access -from lxml.html import rewrite_links from .module_render import get_module from courseware.access import has_access from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore -from xmodule.modulestore.xml import XMLModuleStore -from xmodule.x_module import XModule -from student.models import unique_id_for_user from courseware.model_data import ModelDataCache from open_ended_grading import open_ended_notifications diff --git a/lms/djangoapps/courseware/tests/test_access.py b/lms/djangoapps/courseware/tests/test_access.py index 34d064971f..f93fa0d659 100644 --- a/lms/djangoapps/courseware/tests/test_access.py +++ b/lms/djangoapps/courseware/tests/test_access.py @@ -1,4 +1,4 @@ -from mock import Mock, patch +from mock import Mock from django.test import TestCase diff --git a/lms/djangoapps/courseware/tests/test_masquerade.py b/lms/djangoapps/courseware/tests/test_masquerade.py index f9ddf88b5f..47d437a316 100644 --- a/lms/djangoapps/courseware/tests/test_masquerade.py +++ b/lms/djangoapps/courseware/tests/test_masquerade.py @@ -12,7 +12,7 @@ from django.test.utils import override_settings from django.core.urlresolvers import reverse -from django.contrib.auth.models import User, Group +from django.contrib.auth.models import Group from courseware.access import _course_staff_group_name from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE, get_user from xmodule.modulestore.django import modulestore diff --git a/lms/djangoapps/courseware/tests/test_module_render.py b/lms/djangoapps/courseware/tests/test_module_render.py index 94ab4b7e94..775b6ff0fc 100644 --- a/lms/djangoapps/courseware/tests/test_module_render.py +++ b/lms/djangoapps/courseware/tests/test_module_render.py @@ -8,7 +8,6 @@ from django.test import TestCase from django.test.client import RequestFactory from django.test.utils import override_settings -from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.django import modulestore import courseware.module_render as render from courseware.tests.tests import LoginEnrollmentTestCase diff --git a/lms/djangoapps/dashboard/models.py b/lms/djangoapps/dashboard/models.py index 71a8362390..6b20219993 100644 --- a/lms/djangoapps/dashboard/models.py +++ b/lms/djangoapps/dashboard/models.py @@ -1,3 +1 @@ -from django.db import models - # Create your models here. diff --git a/lms/djangoapps/dashboard/views.py b/lms/djangoapps/dashboard/views.py index 266e769db5..e04588fff4 100644 --- a/lms/djangoapps/dashboard/views.py +++ b/lms/djangoapps/dashboard/views.py @@ -1,11 +1,8 @@ -# Create your views here. -import json -from datetime import datetime from django.http import Http404 from mitxmako.shortcuts import render_to_response from django.db import connection -from student.models import CourseEnrollment, CourseEnrollmentAllowed +from student.models import CourseEnrollment from django.contrib.auth.models import User diff --git a/lms/djangoapps/debug/models.py b/lms/djangoapps/debug/models.py index 71a8362390..6b20219993 100644 --- a/lms/djangoapps/debug/models.py +++ b/lms/djangoapps/debug/models.py @@ -1,3 +1 @@ -from django.db import models - # Create your models here. diff --git a/lms/djangoapps/debug/views.py b/lms/djangoapps/debug/views.py index c1d4155fdd..317ebcada9 100644 --- a/lms/djangoapps/debug/views.py +++ b/lms/djangoapps/debug/views.py @@ -5,7 +5,7 @@ import traceback from django.http import Http404 from django.contrib.auth.decorators import login_required -from django_future.csrf import ensure_csrf_cookie, csrf_exempt +from django_future.csrf import ensure_csrf_cookie from mitxmako.shortcuts import render_to_response from codejail.safe_exec import safe_exec diff --git a/lms/djangoapps/django_comment_client/helpers.py b/lms/djangoapps/django_comment_client/helpers.py index fbe7a2401b..a8a51ad95c 100644 --- a/lms/djangoapps/django_comment_client/helpers.py +++ b/lms/djangoapps/django_comment_client/helpers.py @@ -1,8 +1,5 @@ -from django.core.urlresolvers import reverse from django.conf import settings -from mitxmako.shortcuts import render_to_string from .mustache_helpers import mustache_helpers -from django.core.urlresolvers import reverse from functools import partial from .utils import * diff --git a/lms/djangoapps/django_comment_client/management/commands/show_permissions.py b/lms/djangoapps/django_comment_client/management/commands/show_permissions.py index 67fc29ea97..f24f183193 100644 --- a/lms/djangoapps/django_comment_client/management/commands/show_permissions.py +++ b/lms/djangoapps/django_comment_client/management/commands/show_permissions.py @@ -1,5 +1,4 @@ from django.core.management.base import BaseCommand, CommandError -from django_comment_common.models import Permission, Role from django.contrib.auth.models import User diff --git a/lms/djangoapps/django_comment_client/mustache_helpers.py b/lms/djangoapps/django_comment_client/mustache_helpers.py index 5743dba9cb..adaf26c9e0 100644 --- a/lms/djangoapps/django_comment_client/mustache_helpers.py +++ b/lms/djangoapps/django_comment_client/mustache_helpers.py @@ -1,7 +1,6 @@ from .utils import url_for_tags as _url_for_tags import django.core.urlresolvers as urlresolvers -import urllib import sys import inspect diff --git a/lms/djangoapps/django_comment_client/permissions.py b/lms/djangoapps/django_comment_client/permissions.py index 1a523a170a..b868d46e36 100644 --- a/lms/djangoapps/django_comment_client/permissions.py +++ b/lms/djangoapps/django_comment_client/permissions.py @@ -1,8 +1,3 @@ -from django_comment_common.models import Role, Permission -from django.db.models.signals import post_save -from django.dispatch import receiver -from student.models import CourseEnrollment - import logging from util.cache import cache from django.core import cache diff --git a/lms/djangoapps/django_comment_client/tests/test_models.py b/lms/djangoapps/django_comment_client/tests/test_models.py index e45c883931..6d46df113a 100644 --- a/lms/djangoapps/django_comment_client/tests/test_models.py +++ b/lms/djangoapps/django_comment_client/tests/test_models.py @@ -1,5 +1,4 @@ import django_comment_common.models as models -import django_comment_client.permissions as permissions from django.test import TestCase @@ -44,7 +43,7 @@ class RoleClassTestCase(TestCase): class PermissionClassTestCase(TestCase): def setUp(self): - self.permission = permissions.Permission.objects.get_or_create(name="test")[0] + self.permission = models.Permission.objects.get_or_create(name="test")[0] def testUnicode(self): self.assertEqual(str(self.permission), "test") diff --git a/lms/djangoapps/foldit/models.py b/lms/djangoapps/foldit/models.py index 0dce956756..c0ef553d7e 100644 --- a/lms/djangoapps/foldit/models.py +++ b/lms/djangoapps/foldit/models.py @@ -1,11 +1,8 @@ import logging -from django.conf import settings from django.contrib.auth.models import User from django.db import models -from student.models import unique_id_for_user - log = logging.getLogger(__name__) diff --git a/lms/djangoapps/foldit/tests.py b/lms/djangoapps/foldit/tests.py index 9928f596be..0c55049cb6 100644 --- a/lms/djangoapps/foldit/tests.py +++ b/lms/djangoapps/foldit/tests.py @@ -5,7 +5,6 @@ from functools import partial from django.contrib.auth.models import User from django.test import TestCase from django.test.client import RequestFactory -from django.conf import settings from django.core.urlresolvers import reverse from foldit.views import foldit_ops, verify_code diff --git a/lms/djangoapps/instructor/management/commands/compute_grades.py b/lms/djangoapps/instructor/management/commands/compute_grades.py index 92db04f09a..4518450e39 100644 --- a/lms/djangoapps/instructor/management/commands/compute_grades.py +++ b/lms/djangoapps/instructor/management/commands/compute_grades.py @@ -3,18 +3,10 @@ # django management command: dump grades to csv files # for use by batch processes -import os -import sys -import string -import datetime -import json - -#import student.models from instructor.offline_gradecalc import * from courseware.courses import get_course_by_id from xmodule.modulestore.django import modulestore -from django.conf import settings from django.core.management.base import BaseCommand diff --git a/lms/djangoapps/instructor/offline_gradecalc.py b/lms/djangoapps/instructor/offline_gradecalc.py index 8182c4e58a..fe5b95c3b9 100644 --- a/lms/djangoapps/instructor/offline_gradecalc.py +++ b/lms/djangoapps/instructor/offline_gradecalc.py @@ -6,16 +6,12 @@ # The grades are stored in the OfflineComputedGrade table of the courseware model. import json -import logging import time -import courseware.models - -from collections import namedtuple from json import JSONEncoder from courseware import grades, models from courseware.courses import get_course_by_id -from django.contrib.auth.models import User, Group +from django.contrib.auth.models import User class MyEncoder(JSONEncoder): diff --git a/lms/djangoapps/instructor/tests/test_enrollment.py b/lms/djangoapps/instructor/tests/test_enrollment.py index ce5f2d2e50..3ce82b700b 100644 --- a/lms/djangoapps/instructor/tests/test_enrollment.py +++ b/lms/djangoapps/instructor/tests/test_enrollment.py @@ -9,7 +9,6 @@ from django.core.urlresolvers import reverse from courseware.access import _course_staff_group_name from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE, get_user from xmodule.modulestore.django import modulestore -import xmodule.modulestore.django from student.models import CourseEnrollment, CourseEnrollmentAllowed from instructor.views import get_and_clean_student_list diff --git a/lms/djangoapps/instructor/tests/test_gradebook.py b/lms/djangoapps/instructor/tests/test_gradebook.py index 4b1d22b594..3d0a1b09b8 100644 --- a/lms/djangoapps/instructor/tests/test_gradebook.py +++ b/lms/djangoapps/instructor/tests/test_gradebook.py @@ -2,13 +2,11 @@ Tests of the instructor dashboard gradebook """ -from django.test import TestCase from django.test.utils import override_settings from django.core.urlresolvers import reverse from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory -from student.tests.factories import UserFactory, CourseEnrollmentFactory, UserProfileFactory, AdminFactory +from student.tests.factories import UserFactory, CourseEnrollmentFactory, AdminFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -from mock import patch, DEFAULT from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE from capa.tests.response_xml_factory import StringResponseXMLFactory from courseware.tests.factories import StudentModuleFactory diff --git a/lms/djangoapps/instructor/tests/test_xss.py b/lms/djangoapps/instructor/tests/test_xss.py index d6b8adc908..87bd2ee16b 100644 --- a/lms/djangoapps/instructor/tests/test_xss.py +++ b/lms/djangoapps/instructor/tests/test_xss.py @@ -3,7 +3,6 @@ Tests of various instructor dashboard features that include lists of students """ from django.conf import settings -from django.test import TestCase from django.test.client import RequestFactory from django.test.utils import override_settings from markupsafe import escape diff --git a/lms/djangoapps/instructor_task/tests/test_integration.py b/lms/djangoapps/instructor_task/tests/test_integration.py index d7a81a5b39..5a17e32329 100644 --- a/lms/djangoapps/instructor_task/tests/test_integration.py +++ b/lms/djangoapps/instructor_task/tests/test_integration.py @@ -17,7 +17,6 @@ from django.core.urlresolvers import reverse from capa.tests.response_xml_factory import (CodeResponseXMLFactory, CustomResponseXMLFactory) from xmodule.modulestore.tests.factories import ItemFactory -from xmodule.modulestore.exceptions import ItemNotFoundError from courseware.model_data import StudentModule diff --git a/lms/djangoapps/instructor_task/tests/test_tasks.py b/lms/djangoapps/instructor_task/tests/test_tasks.py index 9eb81a98c9..c59a7065ae 100644 --- a/lms/djangoapps/instructor_task/tests/test_tasks.py +++ b/lms/djangoapps/instructor_task/tests/test_tasks.py @@ -19,7 +19,7 @@ from courseware.tests.factories import StudentModuleFactory from student.tests.factories import UserFactory from instructor_task.models import InstructorTask -from instructor_task.tests.test_base import InstructorTaskModuleTestCase, TEST_COURSE_ORG, TEST_COURSE_NUMBER +from instructor_task.tests.test_base import InstructorTaskModuleTestCase from instructor_task.tests.factories import InstructorTaskFactory from instructor_task.tasks import rescore_problem, reset_problem_attempts, delete_problem_state from instructor_task.tasks_helper import UpdateProblemModuleStateError, update_problem_module_state diff --git a/lms/djangoapps/licenses/management/commands/generate_serial_numbers.py b/lms/djangoapps/licenses/management/commands/generate_serial_numbers.py index 7c6b0d310e..4409f1cb45 100644 --- a/lms/djangoapps/licenses/management/commands/generate_serial_numbers.py +++ b/lms/djangoapps/licenses/management/commands/generate_serial_numbers.py @@ -1,6 +1,4 @@ -import os.path from uuid import uuid4 -from optparse import make_option from django.utils.html import escape from django.core.management.base import BaseCommand, CommandError diff --git a/lms/djangoapps/licenses/management/commands/import_serial_numbers.py b/lms/djangoapps/licenses/management/commands/import_serial_numbers.py index a3a8c0bad1..0a08ea83d3 100644 --- a/lms/djangoapps/licenses/management/commands/import_serial_numbers.py +++ b/lms/djangoapps/licenses/management/commands/import_serial_numbers.py @@ -1,5 +1,4 @@ import os.path -from optparse import make_option from django.utils.html import escape from django.core.management.base import BaseCommand, CommandError diff --git a/lms/djangoapps/lms_migration/management/commands/create_groups.py b/lms/djangoapps/lms_migration/management/commands/create_groups.py index 95c9e4238b..6cdc032278 100644 --- a/lms/djangoapps/lms_migration/management/commands/create_groups.py +++ b/lms/djangoapps/lms_migration/management/commands/create_groups.py @@ -5,13 +5,10 @@ # Create all staff_* groups for classes in data directory. import os -import sys -import string -import re from django.core.management.base import BaseCommand from django.conf import settings -from django.contrib.auth.models import User, Group +from django.contrib.auth.models import Group from path import path from lxml import etree diff --git a/lms/djangoapps/lms_migration/management/commands/create_user.py b/lms/djangoapps/lms_migration/management/commands/create_user.py index ca0e1a756f..87abf4f73a 100644 --- a/lms/djangoapps/lms_migration/management/commands/create_user.py +++ b/lms/djangoapps/lms_migration/management/commands/create_user.py @@ -7,7 +7,6 @@ import os import sys import string -import re import datetime from getpass import getpass import json diff --git a/lms/djangoapps/lms_migration/management/commands/manage_course_groups.py b/lms/djangoapps/lms_migration/management/commands/manage_course_groups.py index b63ef7859b..3c87762624 100644 --- a/lms/djangoapps/lms_migration/management/commands/manage_course_groups.py +++ b/lms/djangoapps/lms_migration/management/commands/manage_course_groups.py @@ -4,17 +4,9 @@ # # interactively list and edit membership in course staff and instructor groups -import os -import sys -import string import re -import datetime -from getpass import getpass -import json -import readline from django.core.management.base import BaseCommand -from django.conf import settings from django.contrib.auth.models import User, Group #----------------------------------------------------------------------------- diff --git a/lms/djangoapps/lms_migration/migrate.py b/lms/djangoapps/lms_migration/migrate.py index a677383035..3768b557ed 100644 --- a/lms/djangoapps/lms_migration/migrate.py +++ b/lms/djangoapps/lms_migration/migrate.py @@ -5,7 +5,6 @@ import json import logging import os -from pprint import pprint import xmodule.modulestore.django as xmodule_django from xmodule.modulestore.django import modulestore diff --git a/lms/djangoapps/notes/tests.py b/lms/djangoapps/notes/tests.py index a7609b91ac..21b5cd7b36 100644 --- a/lms/djangoapps/notes/tests.py +++ b/lms/djangoapps/notes/tests.py @@ -9,9 +9,7 @@ from django.contrib.auth.models import User from django.core.exceptions import ValidationError import collections -import unittest import json -import logging from . import utils, api, models diff --git a/lms/djangoapps/notes/views.py b/lms/djangoapps/notes/views.py index 654d7fb31d..01671b7ccd 100644 --- a/lms/djangoapps/notes/views.py +++ b/lms/djangoapps/notes/views.py @@ -4,7 +4,6 @@ from mitxmako.shortcuts import render_to_response from courseware.courses import get_course_with_access from notes.models import Note from notes.utils import notes_enabled_for_course -import json @login_required diff --git a/lms/djangoapps/open_ended_grading/staff_grading.py b/lms/djangoapps/open_ended_grading/staff_grading.py index fad5268294..3ea55f1df0 100644 --- a/lms/djangoapps/open_ended_grading/staff_grading.py +++ b/lms/djangoapps/open_ended_grading/staff_grading.py @@ -5,7 +5,6 @@ LMS part of instructor grading: - calls the instructor grading service """ -import json import logging log = logging.getLogger(__name__) diff --git a/lms/djangoapps/open_ended_grading/tests.py b/lms/djangoapps/open_ended_grading/tests.py index 3b6c992881..99b8b1a929 100644 --- a/lms/djangoapps/open_ended_grading/tests.py +++ b/lms/djangoapps/open_ended_grading/tests.py @@ -9,7 +9,6 @@ from mock import MagicMock, patch, Mock from django.core.urlresolvers import reverse from django.contrib.auth.models import Group -from django.http import HttpResponse from django.conf import settings from mitxmako.shortcuts import render_to_string diff --git a/lms/djangoapps/open_ended_grading/views.py b/lms/djangoapps/open_ended_grading/views.py index a914e434a9..7cf5aaf024 100644 --- a/lms/djangoapps/open_ended_grading/views.py +++ b/lms/djangoapps/open_ended_grading/views.py @@ -1,7 +1,6 @@ # Grading Views import logging -import urllib from django.conf import settings from django.views.decorators.cache import cache_control diff --git a/lms/djangoapps/psychometrics/management/commands/init_psychometrics.py b/lms/djangoapps/psychometrics/management/commands/init_psychometrics.py index 53f6e17e9d..87e62f4a2c 100644 --- a/lms/djangoapps/psychometrics/management/commands/init_psychometrics.py +++ b/lms/djangoapps/psychometrics/management/commands/init_psychometrics.py @@ -2,10 +2,6 @@ # # generate pyschometrics data from tracking logs and student module data -import os -import sys -import string -import datetime import json from courseware.models import * diff --git a/lms/djangoapps/static_template_view/models.py b/lms/djangoapps/static_template_view/models.py index 71a8362390..6b20219993 100644 --- a/lms/djangoapps/static_template_view/models.py +++ b/lms/djangoapps/static_template_view/models.py @@ -1,3 +1 @@ -from django.db import models - # Create your models here. diff --git a/lms/djangoapps/staticbook/models.py b/lms/djangoapps/staticbook/models.py index 71a8362390..6b20219993 100644 --- a/lms/djangoapps/staticbook/models.py +++ b/lms/djangoapps/staticbook/models.py @@ -1,3 +1 @@ -from django.db import models - # Create your models here. diff --git a/lms/djangoapps/staticbook/views.py b/lms/djangoapps/staticbook/views.py index 6d3dcbd5ca..fcfba9e22c 100644 --- a/lms/djangoapps/staticbook/views.py +++ b/lms/djangoapps/staticbook/views.py @@ -1,6 +1,5 @@ from django.contrib.auth.decorators import login_required from django.http import Http404 -from django.core.urlresolvers import reverse from mitxmako.shortcuts import render_to_response from courseware.access import has_access diff --git a/lms/envs/common.py b/lms/envs/common.py index cc45739562..0eb931e308 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -305,7 +305,7 @@ COURSES_WITH_UNSAFE_CODE = [] ############################ SIGNAL HANDLERS ################################ # This is imported to register the exception signal handling that logs exceptions -import monitoring.exceptions # noqa +import monitoring.exceptions # noqa # pylint: disable=W0611 ############################### DJANGO BUILT-INS ############################### # Change DEBUG/TEMPLATE_DEBUG in your environment settings files, not here diff --git a/lms/envs/dev_edx4edx.py b/lms/envs/dev_edx4edx.py index c90f369bc6..13a66ed1e8 100644 --- a/lms/envs/dev_edx4edx.py +++ b/lms/envs/dev_edx4edx.py @@ -18,7 +18,6 @@ if 'eecs1' in socket.gethostname(): MITX_ROOT_URL = '/mitx2' from .common import * -from logsettings import get_logger_config from .dev import * if 'eecs1' in socket.gethostname(): diff --git a/lms/envs/dev_ike.py b/lms/envs/dev_ike.py index 3f54b11d1e..50bbfff096 100644 --- a/lms/envs/dev_ike.py +++ b/lms/envs/dev_ike.py @@ -13,7 +13,6 @@ sessions. Assumes structure: # pylint: disable=W0401, W0614 from .common import * -from logsettings import get_logger_config from .dev import * import socket diff --git a/lms/lib/comment_client/comment_client.py b/lms/lib/comment_client/comment_client.py index 9b1a0baee2..d91c5ea47f 100644 --- a/lms/lib/comment_client/comment_client.py +++ b/lms/lib/comment_client/comment_client.py @@ -1,3 +1,5 @@ +# Import other classes here so they can be imported from here. +# pylint: disable=W0611 from .comment import Comment from .thread import Thread from .user import User diff --git a/lms/lib/perfstats/models.py b/lms/lib/perfstats/models.py index 71a8362390..6b20219993 100644 --- a/lms/lib/perfstats/models.py +++ b/lms/lib/perfstats/models.py @@ -1,3 +1 @@ -from django.db import models - # Create your models here. diff --git a/lms/one_time_startup.py b/lms/one_time_startup.py index e1b1f79444..e10ec06685 100644 --- a/lms/one_time_startup.py +++ b/lms/one_time_startup.py @@ -1,10 +1,9 @@ -import logging from dogapi import dog_http_api, dog_stats_api from django.conf import settings from xmodule.modulestore.django import modulestore from request_cache.middleware import RequestCache -from django.core.cache import get_cache, InvalidCacheBackendError +from django.core.cache import get_cache cache = get_cache('mongo_metadata_inheritance') for store_name in settings.MODULESTORE: diff --git a/lms/urls.py b/lms/urls.py index 1d34ebf3af..80f1224837 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -3,7 +3,8 @@ from django.conf.urls import patterns, include, url from django.contrib import admin from django.conf.urls.static import static -from . import one_time_startup +# Not used, the work is done in the imported module. +from . import one_time_startup # pylint: disable=W0611 import django.contrib.auth.views From 645d847bb116eeb273423852a2814891e3d4b66a Mon Sep 17 00:00:00 2001 From: Ned Batchelder <ned@nedbatchelder.com> Date: Wed, 19 Jun 2013 12:42:13 -0400 Subject: [PATCH 232/375] Remove unused imports from cms, as detected by pylint. --- cms/djangoapps/contentstore/tests/test_item.py | 1 - cms/djangoapps/models/settings/course_metadata.py | 1 - cms/envs/common.py | 4 ++-- cms/envs/dev_ike.py | 2 -- cms/urls.py | 3 +++ 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_item.py b/cms/djangoapps/contentstore/tests/test_item.py index 07264cdc30..1831a5769a 100644 --- a/cms/djangoapps/contentstore/tests/test_item.py +++ b/cms/djangoapps/contentstore/tests/test_item.py @@ -1,4 +1,3 @@ -from contentstore.utils import get_modulestore, get_url_reverse from contentstore.tests.test_course_settings import CourseTestCase from xmodule.modulestore.tests.factories import CourseFactory from django.core.urlresolvers import reverse diff --git a/cms/djangoapps/models/settings/course_metadata.py b/cms/djangoapps/models/settings/course_metadata.py index 708e79f0a3..937ba56f69 100644 --- a/cms/djangoapps/models/settings/course_metadata.py +++ b/cms/djangoapps/models/settings/course_metadata.py @@ -1,6 +1,5 @@ from xmodule.modulestore import Location from contentstore.utils import get_modulestore -from xmodule.x_module import XModuleDescriptor from xmodule.modulestore.inheritance import own_metadata from xblock.core import Scope from xmodule.course_module import CourseDescriptor diff --git a/cms/envs/common.py b/cms/envs/common.py index d7c9e6bb90..da3f39ea49 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -21,7 +21,7 @@ Longer TODO: # We intentionally define lots of variables that aren't used, and # want to import all variables from base settings files -# pylint: disable=W0401, W0614 +# pylint: disable=W0401, W0611, W0614 import sys import lms.envs.common @@ -155,7 +155,7 @@ MIDDLEWARE_CLASSES = ( ############################ SIGNAL HANDLERS ################################ # This is imported to register the exception signal handling that logs exceptions -import monitoring.exceptions # noqa +import monitoring.exceptions # noqa # pylint: disable=W0611 ############################ DJANGO_BUILTINS ################################ # Change DEBUG/TEMPLATE_DEBUG in your environment settings files, not here diff --git a/cms/envs/dev_ike.py b/cms/envs/dev_ike.py index 0c798b68aa..6e67f78f36 100644 --- a/cms/envs/dev_ike.py +++ b/cms/envs/dev_ike.py @@ -7,9 +7,7 @@ # FORCE_SCRIPT_NAME = '/cms' from .common import * -from logsettings import get_logger_config from .dev import * -import socket MITX_FEATURES['AUTH_USE_MIT_CERTIFICATES'] = True diff --git a/cms/urls.py b/cms/urls.py index a9a7f0a68a..d04c311161 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -1,5 +1,8 @@ from django.conf import settings from django.conf.urls import patterns, include, url + +# Import this file so it can do its work, even though we don't use the name. +# pylint: disable=W0611 from . import one_time_startup # Uncomment the next two lines to enable the admin: From acd66200781c1025d9d18bffa50cda5ab1c9e758 Mon Sep 17 00:00:00 2001 From: Renzo Lucioni <renzolucioni@gmail.com> Date: Wed, 19 Jun 2013 17:00:10 -0400 Subject: [PATCH 233/375] Re-enable the Jasmine test --- common/static/coffee/spec/logger_spec.coffee | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/common/static/coffee/spec/logger_spec.coffee b/common/static/coffee/spec/logger_spec.coffee index 119acea16d..8866daa570 100644 --- a/common/static/coffee/spec/logger_spec.coffee +++ b/common/static/coffee/spec/logger_spec.coffee @@ -3,10 +3,10 @@ describe 'Logger', -> expect(window.log_event).toBe Logger.log describe 'log', -> - # it 'sends an event to Segment.io, if the event is whitelisted', -> - # spyOn(analytics, 'track') - # Logger.log 'seq_goto', 'data' - # expect(analytics.track).toHaveBeenCalledWith 'seq_goto', 'data' + it 'sends an event to Segment.io, if the event is whitelisted', -> + spyOn(analytics, 'track') + Logger.log 'seq_goto', 'data' + expect(analytics.track).toHaveBeenCalledWith 'seq_goto', 'data' it 'send a request to log event', -> spyOn $, 'getWithPrefix' From 2c72fa9e8b5a4a94c4c315d136144dd7137b6298 Mon Sep 17 00:00:00 2001 From: Brian Talbot <btalbot@edx.org> Date: Wed, 19 Jun 2013 15:11:43 -0400 Subject: [PATCH 234/375] studio - revises markup/content for course about mgmt notices --- cms/templates/settings.html | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/cms/templates/settings.html b/cms/templates/settings.html index a331c481a6..2ee4ad1c07 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -90,17 +90,22 @@ from contentstore import utils <div class="copy"> <p><a class="link-courseURL" rel="external" href="https:${utils.get_lms_link_for_about_page(course_location)}" />https:${utils.get_lms_link_for_about_page(course_location)}</a></p> </div> - % if not about_page_editable: - <div> - <p>${_("Note: your course summary page will not be viewable until your course has been announced. To provide content for the page and preview it, follow the instructions provided by your PM or Conrad Warre (conrad@edx.org).")}</p> - </div> - % endif + <ul class="list-actions"> <li class="action-item"> <a title="${_('Send a note to students via email')}" href="mailto:someone@domain.com?Subject=Enroll%20in%20${context_course.display_name_with_default}&body=The%20course%20"${context_course.display_name_with_default}",%20provided%20by%20edX,%20is%20open%20for%20enrollment.%20Please%20navigate%20to%20this%20course%20at%20https:${utils.get_lms_link_for_about_page(course_location)}%20to%20enroll." class="action action-primary"><i class="icon-envelope-alt icon-inline"></i>${_("Invite your students")}</a> </li> </ul> </div> + + % if not about_page_editable: + <div class="notice notice-incontext notice-workflow"> + <h3 class="title">${_("Note: About Your Course's Promotion")}</h3> + <div class="copy"> + <p>${_('Your course summary page will not be viewable until your course has been announced. To provide content for the page and preview it, follow the instructions provided by <abbr title="Program Manager">PM</abbr> or Conrad Warre <a rel="email" class="action action-email" href="mailto:conrad@edx.org">(conrad@edx.org)</a>.')}</p> + </div> + </div> + % endif </section> <hr class="divide" /> @@ -111,12 +116,6 @@ from contentstore import utils <span class="tip">${_('Dates that control when your course can be viewed.')}</span> </header> - % if not about_page_editable: - <div> - <p>${_("Note: these dates impact when your courseware can be viewed, but they are not the dates shown on your course summary page. To provide the course start and registration dates as shown on your course summary page, follow the instructions provided by your PM or Conrad Warre (conrad@edx.org).")}</p> - </div> - % endif - <ol class="list-input"> <li class="field-group field-group-course-start" id="course-start"> <div class="field date" id="field-course-start-date"> @@ -146,6 +145,7 @@ from contentstore import utils </div> </li> </ol> + % if about_page_editable: <ol class="list-input"> <li class="field-group field-group-enrollment-start" id="enrollment-start"> @@ -177,6 +177,15 @@ from contentstore import utils </li> </ol> % endif + + % if not about_page_editable: + <div class="notice notice-incontext notice-workflow"> + <h3 class="title">${_("Note: These Dates Are Not Used When Promoting Your Course")}</h3> + <div class="copy"> + <p>${_('These dates impact <strong>when your courseware can be viewed</strong>, but they are <strong>not the dates shown on your course summary page</strong>. To provide the course start and registration dates as shown on your course summary page, follow the instructions provided by your <abbr title="Program Manager">PM</abbr> or Conrad Warre <a rel="email" class="action action-email" href="mailto:conrad@edx.org">(conrad@edx.org)</a>.')}</p> + </div> + </div> + % endif </section> <hr class="divide" /> % if about_page_editable: From d2f0d85085f5b4ca00c3b1076b8adfbd208c0853 Mon Sep 17 00:00:00 2001 From: Brian Talbot <btalbot@edx.org> Date: Wed, 19 Jun 2013 17:42:05 -0400 Subject: [PATCH 235/375] studio - adds in basic rules for notices UI --- cms/static/sass/elements/_system-help.scss | 39 ++++++++++++++++++++++ cms/static/sass/views/_settings.scss | 8 ++++- common/static/sass/_mixins.scss | 7 ++++ 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/cms/static/sass/elements/_system-help.scss b/cms/static/sass/elements/_system-help.scss index 7fcb218282..017faab54d 100644 --- a/cms/static/sass/elements/_system-help.scss +++ b/cms/static/sass/elements/_system-help.scss @@ -1,2 +1,41 @@ // studio - elements - system help // ==================== + +// notices - in-context: to be used as notices to users within the context of a form/action +.notice-incontext { + @extend .ui-well; + + .title { + @extend .t-title7; + margin-bottom: ($baseline/4); + font-weight: 600; + + [class^="icon-"] { + @extend .t-icon5; + display: inline-block; + vertical-align: middle; + margin-right: ($baseline/4); + } + } + + .copy { + @extend .t-copy-sub2; + } + + strong { + font-weight: 600; + } +} + +// particular warnings around a workflow for something +.notice-workflow { + background: $yellow-l5; + + .copy { + color: $gray-d1; + } + + .icon-warning-sign { + color: $yellow-s3; + } +} diff --git a/cms/static/sass/views/_settings.scss b/cms/static/sass/views/_settings.scss index 735774511f..cbb1034626 100644 --- a/cms/static/sass/views/_settings.scss +++ b/cms/static/sass/views/_settings.scss @@ -21,7 +21,7 @@ body.course.settings { font-size: 14px; } - .message-status { + .message-status { display: none; @include border-top-radius(2px); @include box-sizing(border-box); @@ -52,6 +52,12 @@ body.course.settings { } } + // notices - used currently for edx mktg + .notice-workflow { + margin-top: ($baseline); + } + + // in form - elements .group-settings { margin: 0 0 ($baseline*2) 0; diff --git a/common/static/sass/_mixins.scss b/common/static/sass/_mixins.scss index c3a548bbf7..c26738a1b7 100644 --- a/common/static/sass/_mixins.scss +++ b/common/static/sass/_mixins.scss @@ -189,3 +189,10 @@ } } + +// UI archetypes - well +.ui-well { + @include box-shadow(inset 0 1px 2px 1px $shadow-l1); + padding: ($baseline*0.75); +} + From 7904464975f3d20ebb33a65600c0c589352434a7 Mon Sep 17 00:00:00 2001 From: Brian Talbot <btalbot@edx.org> Date: Wed, 19 Jun 2013 17:42:36 -0400 Subject: [PATCH 236/375] studio - revises notice headings to use icons --- cms/templates/settings.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cms/templates/settings.html b/cms/templates/settings.html index 2ee4ad1c07..e0a99acff9 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -100,7 +100,7 @@ from contentstore import utils % if not about_page_editable: <div class="notice notice-incontext notice-workflow"> - <h3 class="title">${_("Note: About Your Course's Promotion")}</h3> + <h3 class="title"><i class="icon-warning-sign"></i>${_("Note: About Your Course's Promotion")}</h3> <div class="copy"> <p>${_('Your course summary page will not be viewable until your course has been announced. To provide content for the page and preview it, follow the instructions provided by <abbr title="Program Manager">PM</abbr> or Conrad Warre <a rel="email" class="action action-email" href="mailto:conrad@edx.org">(conrad@edx.org)</a>.')}</p> </div> @@ -180,7 +180,7 @@ from contentstore import utils % if not about_page_editable: <div class="notice notice-incontext notice-workflow"> - <h3 class="title">${_("Note: These Dates Are Not Used When Promoting Your Course")}</h3> + <h3 class="title"><i class="icon-warning-sign"></i>${_("Note: These Dates Are Not Used When Promoting Your Course")}</h3> <div class="copy"> <p>${_('These dates impact <strong>when your courseware can be viewed</strong>, but they are <strong>not the dates shown on your course summary page</strong>. To provide the course start and registration dates as shown on your course summary page, follow the instructions provided by your <abbr title="Program Manager">PM</abbr> or Conrad Warre <a rel="email" class="action action-email" href="mailto:conrad@edx.org">(conrad@edx.org)</a>.')}</p> </div> From 4ddca36bbecfd8df3f4f4d53ccb48d31ea6b4c9a Mon Sep 17 00:00:00 2001 From: Brian Talbot <btalbot@edx.org> Date: Wed, 19 Jun 2013 18:13:54 -0400 Subject: [PATCH 237/375] Studio - adds minor UI/content changes to settings notices --- cms/static/sass/elements/_system-help.scss | 1 + cms/templates/settings.html | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/cms/static/sass/elements/_system-help.scss b/cms/static/sass/elements/_system-help.scss index 017faab54d..5f4cec26d7 100644 --- a/cms/static/sass/elements/_system-help.scss +++ b/cms/static/sass/elements/_system-help.scss @@ -4,6 +4,7 @@ // notices - in-context: to be used as notices to users within the context of a form/action .notice-incontext { @extend .ui-well; + @include border-radius(($baseline/10)); .title { @extend .t-title7; diff --git a/cms/templates/settings.html b/cms/templates/settings.html index e0a99acff9..6f2ef7653d 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -100,9 +100,9 @@ from contentstore import utils % if not about_page_editable: <div class="notice notice-incontext notice-workflow"> - <h3 class="title"><i class="icon-warning-sign"></i>${_("Note: About Your Course's Promotion")}</h3> + <h3 class="title"><i class="icon-warning-sign"></i>${_("Promoting Your Course with edX")}</h3> <div class="copy"> - <p>${_('Your course summary page will not be viewable until your course has been announced. To provide content for the page and preview it, follow the instructions provided by <abbr title="Program Manager">PM</abbr> or Conrad Warre <a rel="email" class="action action-email" href="mailto:conrad@edx.org">(conrad@edx.org)</a>.')}</p> + <p>${_('Your course summary page will not be viewable until your course has been announced. To provide content for the page and preview it, follow the instructions provided by your <abbr title="Program Manager">PM</abbr> or Conrad Warre <a rel="email" class="action action-email" href="mailto:conrad@edx.org">(conrad@edx.org)</a>.')}</p> </div> </div> % endif @@ -180,7 +180,7 @@ from contentstore import utils % if not about_page_editable: <div class="notice notice-incontext notice-workflow"> - <h3 class="title"><i class="icon-warning-sign"></i>${_("Note: These Dates Are Not Used When Promoting Your Course")}</h3> + <h3 class="title"><i class="icon-warning-sign"></i>${_("These Dates Are Not Used When Promoting Your Course")}</h3> <div class="copy"> <p>${_('These dates impact <strong>when your courseware can be viewed</strong>, but they are <strong>not the dates shown on your course summary page</strong>. To provide the course start and registration dates as shown on your course summary page, follow the instructions provided by your <abbr title="Program Manager">PM</abbr> or Conrad Warre <a rel="email" class="action action-email" href="mailto:conrad@edx.org">(conrad@edx.org)</a>.')}</p> </div> From bc2f7b96eccb638844d9e3488525c9e71e6126f4 Mon Sep 17 00:00:00 2001 From: Ned Batchelder <ned@nedbatchelder.com> Date: Wed, 19 Jun 2013 22:31:05 -0400 Subject: [PATCH 238/375] Remove a redundant pylint suppression. --- 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 da3f39ea49..7f4c106e6d 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -155,7 +155,7 @@ MIDDLEWARE_CLASSES = ( ############################ SIGNAL HANDLERS ################################ # This is imported to register the exception signal handling that logs exceptions -import monitoring.exceptions # noqa # pylint: disable=W0611 +import monitoring.exceptions # noqa ############################ DJANGO_BUILTINS ################################ # Change DEBUG/TEMPLATE_DEBUG in your environment settings files, not here From e775852ee4c6c4ea8c5f5660b44b9b50f5cba8d6 Mon Sep 17 00:00:00 2001 From: Ned Batchelder <ned@nedbatchelder.com> Date: Wed, 19 Jun 2013 22:44:55 -0400 Subject: [PATCH 239/375] Make lms and cms more similar. --- lms/envs/common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lms/envs/common.py b/lms/envs/common.py index 0eb931e308..f9bfa878dd 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -21,7 +21,7 @@ Longer TODO: # We intentionally define lots of variables that aren't used, and # want to import all variables from base settings files -# pylint: disable=W0401, W0614 +# pylint: disable=W0401, W0611, W0614 import sys import os @@ -305,7 +305,7 @@ COURSES_WITH_UNSAFE_CODE = [] ############################ SIGNAL HANDLERS ################################ # This is imported to register the exception signal handling that logs exceptions -import monitoring.exceptions # noqa # pylint: disable=W0611 +import monitoring.exceptions # noqa ############################### DJANGO BUILT-INS ############################### # Change DEBUG/TEMPLATE_DEBUG in your environment settings files, not here From 5d0f9059616247a76d878480ec8a60dbe60489b8 Mon Sep 17 00:00:00 2001 From: Alexander Kryklia <kryklia@gmail.com> Date: Tue, 11 Jun 2013 18:04:35 +0300 Subject: [PATCH 240/375] fixes pep8 and pylint errors and rename mitx->edx --- docs/source/conf.py | 55 ++++++++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 3b1e9dc5b9..0efe03568c 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,23 +1,26 @@ # -*- coding: utf-8 -*- -# -# MITx documentation build configuration file, created by -# sphinx-quickstart on Fri Nov 2 15:43:00 2012. -# -# This file is execfile()d with the current directory set to its containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. +""" EdX documentation build configuration file, created by + sphinx-quickstart on Fri Nov 2 15:43:00 2012. -import sys, os + This file is execfile()d with the current directory set to its containing dir. + + Note that not all possible configuration values are present in this + autogenerated file. + + All configuration values have a default; values that are commented out + serve to show the default.""" + +import sys +import os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath('.')) sys.path.insert(0, os.path.abspath('../..')) # mitx folder +sys.path.insert(0, os.path.join(os.path.abspath('../..'), 'common', 'lib', 'calc')) # calc module +sys.path.insert(0, os.path.join(os.path.abspath('../..'), 'common', 'lib', 'chem')) # calc module +sys.path.insert(0, os.path.join(os.path.abspath('../..'), 'common', 'lib', 'sandbox-packages')) # calc module sys.path.insert(0, os.path.join(os.path.abspath('../..'), 'common', 'lib', 'capa')) # capa module sys.path.insert(0, os.path.join(os.path.abspath('../..'), 'common', 'lib', 'xmodule')) # xmodule sys.path.insert(0, os.path.join(os.path.abspath('../..'), 'lms', 'djangoapps')) # lms djangoapps @@ -36,7 +39,9 @@ os.environ['DJANGO_SETTINGS_MODULE'] = 'lms.envs.dev' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.pngmath', 'sphinx.ext.mathjax', 'sphinx.ext.viewcode'] +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', + 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.pngmath', + 'sphinx.ext.mathjax', 'sphinx.ext.viewcode'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -51,17 +56,17 @@ source_suffix = '.rst' master_doc = 'index' # General information about the project. -project = u'MITx' -copyright = u'2012, MITx team' +project = u'EdX Dev Data' +copyright = u'2012-13, EdX team' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '1.0' +version = '0.2' # The full version, including alpha/beta/rc tags. -release = '1.0' +release = '0.2' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -75,7 +80,7 @@ release = '1.0' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = [] +exclude_patterns = ['build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None @@ -175,7 +180,7 @@ html_static_path = ['_static'] #html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'MITxdoc' +htmlhelp_basename = 'edXDocs' # -- Options for LaTeX output -------------------------------------------------- @@ -194,8 +199,8 @@ latex_elements = { # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'MITx.tex', u'MITx Documentation', - u'MITx team', 'manual'), + ('index', 'edXDocs.tex', u'EdX Dev Data Documentation', + u'EdX Team', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -224,8 +229,8 @@ latex_documents = [ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'mitx', u'MITx Documentation', - [u'MITx team'], 1) + ('index', 'edxdocs', u'EdX Dev Data Documentation', + [u'EdX Team'], 1) ] # If true, show URL addresses after external links. @@ -238,8 +243,8 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'MITx', u'MITx Documentation', - u'MITx team', 'MITx', 'One line description of project.', + ('index', 'EdXDocs', u'EdX Dev Data Documentation', + u'EdX Team', 'EdXDocs', 'One line description of project.', 'Miscellaneous'), ] From e7f1baad620e773f407a13610e44256626682383 Mon Sep 17 00:00:00 2001 From: Alexander Kryklia <kryklia@gmail.com> Date: Wed, 12 Jun 2013 14:09:06 +0300 Subject: [PATCH 241/375] Fixes broken imports in checker.py --- common/lib/capa/capa/checker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/lib/capa/capa/checker.py b/common/lib/capa/capa/checker.py index 15358aac9e..fe906efc79 100755 --- a/common/lib/capa/capa/checker.py +++ b/common/lib/capa/capa/checker.py @@ -12,8 +12,8 @@ from path import path from cStringIO import StringIO from collections import defaultdict -from .calc import UndefinedVariable -from .capa_problem import LoncapaProblem +from calc import UndefinedVariable +from capa.capa_problem import LoncapaProblem from mako.lookup import TemplateLookup logging.basicConfig(format="%(levelname)s %(message)s") From 37cad5dc0ca10e1fc5e5ce78fbabbdcb66432b19 Mon Sep 17 00:00:00 2001 From: Alexander Kryklia <kryklia@gmail.com> Date: Wed, 12 Jun 2013 14:49:01 +0300 Subject: [PATCH 242/375] fixes launcy not working with Dir.chdir --- rakefiles/docs.rake | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/rakefiles/docs.rake b/rakefiles/docs.rake index f10fc80d59..2247b686fa 100644 --- a/rakefiles/docs.rake +++ b/rakefiles/docs.rake @@ -22,9 +22,7 @@ task :showdocs, [:options] do |t, args| path = "docs" end - Dir.chdir("#{path}/build/html") do - Launchy.open('index.html') - end + Launchy.open("#{path}/build/html/index.html") end desc "Build docs and show them in browser" From f152668e124f40981331f451cf0374ae5fdbdc75 Mon Sep 17 00:00:00 2001 From: Alexander Kryklia <kryklia@gmail.com> Date: Thu, 13 Jun 2013 12:57:31 +0300 Subject: [PATCH 243/375] Fixes bugs in documentation and improves it. --- docs/source/calc.rst | 7 +++ docs/source/capa.rst | 8 --- docs/source/chem.rst | 10 ++-- docs/source/cms.rst | 83 -------------------------------- docs/source/common-lib.rst | 7 ++- docs/source/conf.py | 13 +---- docs/source/index.rst | 6 +-- docs/source/overview.rst | 19 ++------ docs/source/sandbox-packages.rst | 11 +++++ docs/source/symmath.rst | 31 ++++++++++++ 10 files changed, 69 insertions(+), 126 deletions(-) create mode 100644 docs/source/calc.rst create mode 100644 docs/source/sandbox-packages.rst create mode 100644 docs/source/symmath.rst diff --git a/docs/source/calc.rst b/docs/source/calc.rst new file mode 100644 index 0000000000..659ebc11d7 --- /dev/null +++ b/docs/source/calc.rst @@ -0,0 +1,7 @@ +******************************************* +Calc +******************************************* + +.. automodule:: calc + :members: + :show-inheritance: diff --git a/docs/source/capa.rst b/docs/source/capa.rst index 345855af5e..be828ba33a 100644 --- a/docs/source/capa.rst +++ b/docs/source/capa.rst @@ -8,14 +8,6 @@ Contents: .. toctree:: :maxdepth: 2 - chem.rst - -Calc -==== - -.. automodule:: capa.calc - :members: - :show-inheritance: Capa_problem ============ diff --git a/docs/source/chem.rst b/docs/source/chem.rst index 26e01a3238..025c436d37 100644 --- a/docs/source/chem.rst +++ b/docs/source/chem.rst @@ -1,5 +1,5 @@ ******************************************* -Chem module +Chemistry modules ******************************************* .. module:: chem @@ -7,7 +7,7 @@ Chem module Miller ====== -.. automodule:: capa.chem.miller +.. automodule:: chem.miller :members: :show-inheritance: @@ -47,14 +47,14 @@ Documentation from **crystallography.js**:: Chemcalc ======== -.. automodule:: capa.chem.chemcalc +.. automodule:: chem.chemcalc :members: :show-inheritance: Chemtools ========= -.. automodule:: capa.chem.chemtools +.. automodule:: chem.chemtools :members: :show-inheritance: @@ -62,7 +62,7 @@ Chemtools Tests ===== -.. automodule:: capa.chem.tests +.. automodule:: chem.tests :members: :show-inheritance: diff --git a/docs/source/cms.rst b/docs/source/cms.rst index 02dcaccb5a..11fa243f90 100644 --- a/docs/source/cms.rst +++ b/docs/source/cms.rst @@ -4,86 +4,3 @@ CMS module .. module:: cms -Auth -==== - -.. automodule:: auth - :members: - :show-inheritance: - -Authz ------ - -.. automodule:: auth.authz - :members: - :show-inheritance: - -Content store -============= - -.. .. automodule:: contentstore -.. :members: -.. :show-inheritance: - -.. Utils -.. ----- - -.. .. automodule:: contentstore.untils -.. :members: -.. :show-inheritance: - -.. Views -.. ----- - -.. .. automodule:: contentstore.views -.. :members: -.. :show-inheritance: - -.. Management -.. ---------- - -.. .. automodule:: contentstore.management -.. :members: -.. :show-inheritance: - -.. Tests -.. ----- - -.. .. automodule:: contentstore.tests -.. :members: -.. :show-inheritance: - -Github sync -=========== - -.. automodule:: github_sync - :members: - :show-inheritance: - -Exceptions ----------- - -.. automodule:: github_sync.exceptions - :members: - :show-inheritance: - -Views ------ - -.. automodule:: github_sync.views - :members: - :show-inheritance: - -Management ----------- - -.. automodule:: github_sync.management - :members: - :show-inheritance: - -Tests ------ - -.. .. automodule:: github_sync.tests -.. :members: -.. :show-inheritance: \ No newline at end of file diff --git a/docs/source/common-lib.rst b/docs/source/common-lib.rst index 4fa5eaeb0a..2079ae7a23 100644 --- a/docs/source/common-lib.rst +++ b/docs/source/common-lib.rst @@ -6,4 +6,9 @@ Contents: :maxdepth: 2 xmodule.rst - capa.rst \ No newline at end of file + capa.rst + chem.rst + sandbox-packages.rst + symmath.rst + calc.rst + diff --git a/docs/source/conf.py b/docs/source/conf.py index 0efe03568c..8c49dec851 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -16,20 +16,11 @@ import os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) sys.path.insert(0, os.path.abspath('../..')) # mitx folder -sys.path.insert(0, os.path.join(os.path.abspath('../..'), 'common', 'lib', 'calc')) # calc module -sys.path.insert(0, os.path.join(os.path.abspath('../..'), 'common', 'lib', 'chem')) # calc module -sys.path.insert(0, os.path.join(os.path.abspath('../..'), 'common', 'lib', 'sandbox-packages')) # calc module -sys.path.insert(0, os.path.join(os.path.abspath('../..'), 'common', 'lib', 'capa')) # capa module -sys.path.insert(0, os.path.join(os.path.abspath('../..'), 'common', 'lib', 'xmodule')) # xmodule -sys.path.insert(0, os.path.join(os.path.abspath('../..'), 'lms', 'djangoapps')) # lms djangoapps -sys.path.insert(0, os.path.join(os.path.abspath('../..'), 'cms', 'djangoapps')) # cms djangoapps -sys.path.insert(0, os.path.join(os.path.abspath('../..'), 'common', 'djangoapps')) # common djangoapps # django configuration - careful here -import os -os.environ['DJANGO_SETTINGS_MODULE'] = 'lms.envs.dev' +os.environ['DJANGO_SETTINGS_MODULE'] = 'lms.envs.test' # -- General configuration ----------------------------------------------------- diff --git a/docs/source/index.rst b/docs/source/index.rst index eceb5e23e8..780bc55049 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,10 +1,10 @@ -.. MITx documentation master file, created by +.. EdX Dev documentation master file, created by sphinx-quickstart on Fri Nov 2 15:43:00 2012. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -Welcome to MITx's documentation! -================================ +Welcome to EdX's Dev documentation! +=================================== Contents: diff --git a/docs/source/overview.rst b/docs/source/overview.rst index 007c7582ad..a6d71cbd88 100644 --- a/docs/source/overview.rst +++ b/docs/source/overview.rst @@ -1,20 +1,9 @@ ******************************************* -What the pieces are? +Overview ******************************************* -What -==== -... +This is EdX Dev documentation, mainly extracted from docstrings. +Autogenerated by Sphinx from python code. +Soon support for JS will be impemented. -How -=== - -... - - -Who -=== - - -... \ No newline at end of file diff --git a/docs/source/sandbox-packages.rst b/docs/source/sandbox-packages.rst new file mode 100644 index 0000000000..f63c99c6aa --- /dev/null +++ b/docs/source/sandbox-packages.rst @@ -0,0 +1,11 @@ +******************************************* +Sandbox-packages +******************************************* +.. module:: sandbox-packages + +Loncapa +======= + +.. automodule:: loncapa.loncapa_check + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/symmath.rst b/docs/source/symmath.rst new file mode 100644 index 0000000000..7664e8ac6e --- /dev/null +++ b/docs/source/symmath.rst @@ -0,0 +1,31 @@ +******************************************* +Symmath +******************************************* + +.. module:: symmath + + +Formula +======= + +.. automodule:: symmath.formula + :members: + :show-inheritance: + +Symmath check +============= + +.. automodule:: symmath.symmath_check + :members: + :show-inheritance: + +Symmath tests +============= + +.. automodule:: symmath.test_formula + :members: + :show-inheritance: + +.. automodule:: symmath.test_symmath_check + :members: + :show-inheritance: \ No newline at end of file From a3a2412c8818009f1c009150d924b95ba71bdb2e Mon Sep 17 00:00:00 2001 From: Alexander Kryklia <kryklia@gmail.com> Date: Thu, 13 Jun 2013 13:13:26 +0300 Subject: [PATCH 244/375] fixes pep8 and pylint errors --- docs/source/conf.py | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 8c49dec851..2c398c1b9a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,4 +1,8 @@ # -*- coding: utf-8 -*- +#pylint: disable=C0103 +#pylint: disable=W0622 +#pylint: disable=W0212 +#pylint: disable=W0613 """ EdX documentation build configuration file, created by sphinx-quickstart on Fri Nov 2 15:43:00 2012. @@ -30,9 +34,9 @@ os.environ['DJANGO_SETTINGS_MODULE'] = 'lms.envs.test' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', - 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.pngmath', - 'sphinx.ext.mathjax', 'sphinx.ext.viewcode'] +extensions = [ + 'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage', + 'sphinx.ext.pngmath', 'sphinx.ext.mathjax', 'sphinx.ext.viewcode'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -177,21 +181,21 @@ htmlhelp_basename = 'edXDocs' # -- Options for LaTeX output -------------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # Additional stuff for the LaTeX preamble. + #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'edXDocs.tex', u'EdX Dev Data Documentation', - u'EdX Team', 'manual'), + ('index', 'edXDocs.tex', u'EdX Dev Data Documentation', + u'EdX Team', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -234,9 +238,9 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'EdXDocs', u'EdX Dev Data Documentation', - u'EdX Team', 'EdXDocs', 'One line description of project.', - 'Miscellaneous'), + ('index', 'EdXDocs', u'EdX Dev Data Documentation', + u'EdX Team', 'EdXDocs', 'One line description of project.', + 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. @@ -261,8 +265,12 @@ from django.utils.encoding import force_unicode def process_docstring(app, what, name, obj, options, lines): + """Autodoc django models""" + # This causes import errors if left outside the function from django.db import models + + # If you want extract docs from django forms: # from django import forms # from django.forms.models import BaseInlineFormSet @@ -322,5 +330,6 @@ def process_docstring(app, what, name, obj, options, lines): def setup(app): - # Register the docstring processor with sphinx + """Setup docsting processors""" + #Register the docstring processor with sphinx app.connect('autodoc-process-docstring', process_docstring) From 1da7bcf8b705bef31bd486b8d6d49fc37f036fe0 Mon Sep 17 00:00:00 2001 From: Alexander Kryklia <kryklia@gmail.com> Date: Thu, 13 Jun 2013 19:12:46 +0300 Subject: [PATCH 245/375] Fixes bugs in documentation (rst format) --- .../conditional_module/conditional_module.rst | 2 +- .../drag_and_drop/drag_and_drop_input.rst | 2 +- .../graphical_slider_tool/graphical_slider_tool.rst | 2 +- doc/public/course_data_formats/symbolic_response.rst | 6 +++--- doc/public/index.rst | 1 + 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/doc/public/course_data_formats/conditional_module/conditional_module.rst b/doc/public/course_data_formats/conditional_module/conditional_module.rst index 82c555d3e7..8f7ba17ffc 100644 --- a/doc/public/course_data_formats/conditional_module/conditional_module.rst +++ b/doc/public/course_data_formats/conditional_module/conditional_module.rst @@ -53,7 +53,7 @@ Examples of conditional depends on poll </conditional> Examples of conditional depends on poll (use <show> tag) -------------------------------------------- +-------------------------------------------------------- .. code-block:: xml diff --git a/doc/public/course_data_formats/drag_and_drop/drag_and_drop_input.rst b/doc/public/course_data_formats/drag_and_drop/drag_and_drop_input.rst index 4927deeec6..a5efd866b6 100644 --- a/doc/public/course_data_formats/drag_and_drop/drag_and_drop_input.rst +++ b/doc/public/course_data_formats/drag_and_drop/drag_and_drop_input.rst @@ -420,6 +420,6 @@ Draggables can be reused .. literalinclude:: drag-n-drop-demo2.xml Examples of targets on draggables ------------------------- +--------------------------------- .. literalinclude:: drag-n-drop-demo3.xml diff --git a/doc/public/course_data_formats/graphical_slider_tool/graphical_slider_tool.rst b/doc/public/course_data_formats/graphical_slider_tool/graphical_slider_tool.rst index 3fac46e873..7119c53c41 100644 --- a/doc/public/course_data_formats/graphical_slider_tool/graphical_slider_tool.rst +++ b/doc/public/course_data_formats/graphical_slider_tool/graphical_slider_tool.rst @@ -362,7 +362,7 @@ that has to be updated on a parameter's change, then one can define a special function to handle this. The "output" of such a function must be set to "none", and the JavaScript code inside this function must update the MathJax element by itself. Before exiting, MathJax typeset function should -be called so that the new text will be re-rendered by MathJax. For example, +be called so that the new text will be re-rendered by MathJax. For example:: <render> ... diff --git a/doc/public/course_data_formats/symbolic_response.rst b/doc/public/course_data_formats/symbolic_response.rst index 8463faab3c..4abb0ec990 100644 --- a/doc/public/course_data_formats/symbolic_response.rst +++ b/doc/public/course_data_formats/symbolic_response.rst @@ -19,11 +19,11 @@ This is a partial list of features, to be revised as we go along: An example of a problem:: - <symbolicresponse expect="a_b^c + b_x__d" size="30"> - <textline math="1" + <symbolicresponse expect="a_b^c + b_x__d" size="30"> + <textline math="1" preprocessorClassName="SymbolicMathjaxPreprocessor" preprocessorSrc="/static/js/capa/symbolic_mathjax_preprocessor.js"/> - </symbolicresponse> + </symbolicresponse> It's a bit of a pain to enter that. diff --git a/doc/public/index.rst b/doc/public/index.rst index 064b3ff443..cda3809237 100644 --- a/doc/public/index.rst +++ b/doc/public/index.rst @@ -28,6 +28,7 @@ Specific Problem Types course_data_formats/conditional_module/conditional_module.rst course_data_formats/word_cloud/word_cloud.rst course_data_formats/custom_response.rst + course_data_formats/symbolic_response.rst Internal Data Formats From 0eaa7cccb510745d1a82c513680b0340ad3866c0 Mon Sep 17 00:00:00 2001 From: Alexander Kryklia <kryklia@gmail.com> Date: Fri, 14 Jun 2013 12:49:44 +0300 Subject: [PATCH 246/375] adds test for doc generation --- rakefiles/tests.rake | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/rakefiles/tests.rake b/rakefiles/tests.rake index b4754c2c3c..ae16801f5d 100644 --- a/rakefiles/tests.rake +++ b/rakefiles/tests.rake @@ -33,6 +33,17 @@ def run_acceptance_tests(system, report_dir, harvest_args) test_sh(django_admin(system, 'acceptance', 'harvest', '--debug-mode', '--tag -skip', harvest_args)) end +# Run documentation tests +desc "Run documentation tests" +task :test_docs do + # Be sure that sphinx can build docs w/o exceptions. + test_message = "If test fails, you shoud run %s and look at whole output and fix exceptions. +(You shouldn't fix rst warnings and errors for this to pass, just get rid of exceptions.)" + puts (test_message % ["rake doc"]).colorize( :light_green ) + test_sh('rake doc') + puts (test_message % ["rake doc[pub]"]).colorize( :light_green ) + test_sh('rake doc[pub]') +end directory REPORT_DIR @@ -103,7 +114,7 @@ TEST_TASK_DIRS.each do |dir| end desc "Run all tests" -task :test +task :test => :test_docs desc "Build the html, xml, and diff coverage reports" task :coverage => :report_dirs do From 26565f565e2804700361a7d231f014dbb1005f8c Mon Sep 17 00:00:00 2001 From: Alexander Kryklia <kryklia@gmail.com> Date: Thu, 20 Jun 2013 13:07:41 +0300 Subject: [PATCH 247/375] adds changes to changelog --- CHANGELOG.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index bbaf3f3a6b..7fc07a3f19 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes, in roughly chronological order, most recent first. Add your entries at or near the top. Include a label indicating the component affected. +Common: Repairs development documentation generation by sphinx. + LMS: Problem rescoring. Added options on the Grades tab of the Instructor Dashboard to allow all students' submissions for a particular problem to be rescored. Also supports resetting all From 0af88b70a3a12aa8b1db0b0767d7a618112489f5 Mon Sep 17 00:00:00 2001 From: Alexander Kryklia <kryklia@gmail.com> Date: Thu, 20 Jun 2013 13:09:29 +0300 Subject: [PATCH 248/375] makes tests for sphynx doc generation not to run browser --- rakefiles/tests.rake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rakefiles/tests.rake b/rakefiles/tests.rake index ae16801f5d..20bd34f4e8 100644 --- a/rakefiles/tests.rake +++ b/rakefiles/tests.rake @@ -40,9 +40,9 @@ task :test_docs do test_message = "If test fails, you shoud run %s and look at whole output and fix exceptions. (You shouldn't fix rst warnings and errors for this to pass, just get rid of exceptions.)" puts (test_message % ["rake doc"]).colorize( :light_green ) - test_sh('rake doc') + test_sh('rake builddocs') puts (test_message % ["rake doc[pub]"]).colorize( :light_green ) - test_sh('rake doc[pub]') + test_sh('rake builddocs[pub]') end directory REPORT_DIR From d57fb777655f7d98101e3ad48938c12659d33936 Mon Sep 17 00:00:00 2001 From: Alexander Kryklia <kryklia@gmail.com> Date: Thu, 20 Jun 2013 13:18:04 +0300 Subject: [PATCH 249/375] removes reference to removed module in docs --- docs/source/xmodule.rst | 7 ------- 1 file changed, 7 deletions(-) diff --git a/docs/source/xmodule.rst b/docs/source/xmodule.rst index d68ab779f6..d7552812a0 100644 --- a/docs/source/xmodule.rst +++ b/docs/source/xmodule.rst @@ -144,13 +144,6 @@ Templates :members: :show-inheritance: -Time parse -========== - -.. automodule:: xmodule.timeparse - :members: - :show-inheritance: - Vertical ======== From e76ef3aa31aaaa1f02a4f920084ce040777ec532 Mon Sep 17 00:00:00 2001 From: Will Daly <will@edx.org> Date: Tue, 18 Jun 2013 08:06:42 -0400 Subject: [PATCH 250/375] Combined video and videoalpha acceptance tests. Resolved conflict between two steps with the same name. --- .../courseware/features/video.feature | 10 ++++-- lms/djangoapps/courseware/features/video.py | 22 ++++++++++++ .../courseware/features/videoalpha.py | 36 ------------------- 3 files changed, 29 insertions(+), 39 deletions(-) delete mode 100644 lms/djangoapps/courseware/features/videoalpha.py diff --git a/lms/djangoapps/courseware/features/video.feature b/lms/djangoapps/courseware/features/video.feature index c4d96f93f7..2b8d0f013a 100644 --- a/lms/djangoapps/courseware/features/video.feature +++ b/lms/djangoapps/courseware/features/video.feature @@ -1,6 +1,10 @@ Feature: Video component As a student, I want to view course videos in LMS. - Scenario: Autoplay is enabled in LMS - Given the course has a Video component - Then when I view the video it has autoplay enabled + Scenario: Autoplay is enabled in LMS for a Video component + Given the course has a Video component + Then when I view the video it has autoplay enabled + + Scenario: Autoplay is enabled in the LMS for a VideoAlpha component + Given the course has a VideoAlpha component + Then when I view the video it has autoplay enabled diff --git a/lms/djangoapps/courseware/features/video.py b/lms/djangoapps/courseware/features/video.py index 8cef5564f3..745f0ae99a 100644 --- a/lms/djangoapps/courseware/features/video.py +++ b/lms/djangoapps/courseware/features/video.py @@ -27,8 +27,30 @@ def view_video(_step): world.browser.visit(url) +@step('the course has a VideoAlpha component') +def view_videoalpha(step): + coursename = TEST_COURSE_NAME.replace(' ', '_') + i_am_registered_for_the_course(step, coursename) + + # Make sure we have a videoalpha + add_videoalpha_to_course(coursename) + chapter_name = TEST_SECTION_NAME.replace(" ", "_") + section_name = chapter_name + url = django_url('/courses/edx/Test_Course/Test_Course/courseware/%s/%s' % + (chapter_name, section_name)) + + world.browser.visit(url) + + def add_video_to_course(course): template_name = 'i4x://edx/templates/video/default' world.ItemFactory.create(parent_location=section_location(course), template=template_name, display_name='Video') + + +def add_videoalpha_to_course(course): + template_name = 'i4x://edx/templates/videoalpha/default' + world.ItemFactory.create(parent_location=section_location(course), + template=template_name, + display_name='Video Alpha 1') diff --git a/lms/djangoapps/courseware/features/videoalpha.py b/lms/djangoapps/courseware/features/videoalpha.py deleted file mode 100644 index cabf8c681f..0000000000 --- a/lms/djangoapps/courseware/features/videoalpha.py +++ /dev/null @@ -1,36 +0,0 @@ -#pylint: disable=C0111 -#pylint: disable=W0613 -#pylint: disable=W0621 - -from lettuce import world, step -from lettuce.django import django_url -from common import TEST_COURSE_NAME, TEST_SECTION_NAME, i_am_registered_for_the_course, section_location - -############### ACTIONS #################### - - -@step('when I view the video it has autoplay enabled') -def does_autoplay(step): - assert(world.css_find('.videoalpha')[0]['data-autoplay'] == 'True') - - -@step('the course has a Video component') -def view_videoalpha(step): - coursename = TEST_COURSE_NAME.replace(' ', '_') - i_am_registered_for_the_course(step, coursename) - - # Make sure we have a videoalpha - add_videoalpha_to_course(coursename) - chapter_name = TEST_SECTION_NAME.replace(" ", "_") - section_name = chapter_name - url = django_url('/courses/edx/Test_Course/Test_Course/courseware/%s/%s' % - (chapter_name, section_name)) - - world.browser.visit(url) - - -def add_videoalpha_to_course(course): - template_name = 'i4x://edx/templates/videoalpha/default' - world.ItemFactory.create(parent_location=section_location(course), - template=template_name, - display_name='Video Alpha 1') From 3b37e0c19f13ab5dfacbdd197e6a50f7e1dc4bb0 Mon Sep 17 00:00:00 2001 From: Will Daly <will@edx.org> Date: Tue, 18 Jun 2013 08:35:56 -0400 Subject: [PATCH 251/375] Fixed incorrect videoalpha template name --- lms/djangoapps/courseware/features/video.py | 2 +- lms/djangoapps/courseware/features/videoalpha.feature | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) delete mode 100644 lms/djangoapps/courseware/features/videoalpha.feature diff --git a/lms/djangoapps/courseware/features/video.py b/lms/djangoapps/courseware/features/video.py index 745f0ae99a..90f68c1daf 100644 --- a/lms/djangoapps/courseware/features/video.py +++ b/lms/djangoapps/courseware/features/video.py @@ -50,7 +50,7 @@ def add_video_to_course(course): def add_videoalpha_to_course(course): - template_name = 'i4x://edx/templates/videoalpha/default' + template_name = 'i4x://edx/templates/videoalpha/Video_Alpha_1' world.ItemFactory.create(parent_location=section_location(course), template=template_name, display_name='Video Alpha 1') diff --git a/lms/djangoapps/courseware/features/videoalpha.feature b/lms/djangoapps/courseware/features/videoalpha.feature deleted file mode 100644 index 2a0acb0f9b..0000000000 --- a/lms/djangoapps/courseware/features/videoalpha.feature +++ /dev/null @@ -1,6 +0,0 @@ -Feature: Video Alpha component - As a student, I want to view course videos in LMS. - - Scenario: Autoplay is enabled in LMS - Given the course has a Video component - Then when I view the video it has autoplay enabled From 6ab5bb2f205953070c9c9099352761acf5239419 Mon Sep 17 00:00:00 2001 From: Will Daly <will@edx.org> Date: Tue, 18 Jun 2013 08:47:00 -0400 Subject: [PATCH 252/375] Changed videoalpha stub to exclude video sources, but include the rest of the player. --- lms/templates/videoalpha.html | 56 +++++++++++++++++------------------ 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/lms/templates/videoalpha.html b/lms/templates/videoalpha.html index 07c7dbee27..4e136bd170 100644 --- a/lms/templates/videoalpha.html +++ b/lms/templates/videoalpha.html @@ -2,34 +2,34 @@ <h2> ${display_name} </h2> % endif -%if settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']: - <div id="stub_out_video_for_testing"></div> -%else: - <div - id="video_${id}" - class="video" - data-streams="${youtube_streams}" - ${'data-sub="{}"'.format(sub) if sub else ''} - ${'data-mp4-source="{}"'.format(sources.get('mp4')) if sources.get('mp4') else ''} - ${'data-webm-source="{}"'.format(sources.get('webm')) if sources.get('webm') else ''} - ${'data-ogg-source="{}"'.format(sources.get('ogv')) if sources.get('ogv') else ''} - data-caption-data-dir="${data_dir}" - data-show-captions="${show_captions}" - data-start="${start}" - data-end="${end}" - data-caption-asset-path="${caption_asset_path}" - data-autoplay="${autoplay}" - > - <div class="tc-wrapper"> - <article class="video-wrapper"> - <section class="video-player"> - <div id="${id}"></div> - </section> - <section class="video-controls"></section> - </article> - </div> - </div> -%endif +<div + id="video_${id}" + class="video" + data-streams="${youtube_streams}" + ${'data-sub="{}"'.format(sub) if sub else ''} + + % if not settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']: + ${'data-mp4-source="{}"'.format(sources.get('mp4')) if sources.get('mp4') else ''} + ${'data-webm-source="{}"'.format(sources.get('webm')) if sources.get('webm') else ''} + ${'data-ogg-source="{}"'.format(sources.get('ogv')) if sources.get('ogv') else ''} + % endif + + data-caption-data-dir="${data_dir}" + data-show-captions="${show_captions}" + data-start="${start}" + data-end="${end}" + data-caption-asset-path="${caption_asset_path}" + data-autoplay="${autoplay}" +> +<div class="tc-wrapper"> + <article class="video-wrapper"> + <section class="video-player"> + <div id="${id}"></div> + </section> + <section class="video-controls"></section> + </article> +</div> +</div> % if sources.get('main'): <div class="video-sources"> From b1c963ab5e35c7999ee43e949876000010421d39 Mon Sep 17 00:00:00 2001 From: Will Daly <will@edx.org> Date: Tue, 18 Jun 2013 09:02:21 -0400 Subject: [PATCH 253/375] Changed stubbing behavior in video templates to exclude only the sources (not the player) --- lms/templates/video.html | 48 ++++++++++++++++------------------- lms/templates/videoalpha.html | 4 +++ 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/lms/templates/video.html b/lms/templates/video.html index 267372176a..91b5f63b81 100644 --- a/lms/templates/video.html +++ b/lms/templates/video.html @@ -2,37 +2,33 @@ <h2> ${display_name} </h2> % endif -%if settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']: - <div id="stub_out_video_for_testing"> - <div class="video" data-autoplay="${settings.MITX_FEATURES['AUTOPLAY_VIDEOS']}"> - <section class="video-controls"> - <div class="slider"></div> - <div> - <ul class="vcr"> - <li><a class="video_control" href="#"></a></li> - <li> - <div class="vidtime">0:00 / 0:00</div> - </li> - </ul> - <div class="secondary-controls"> - <a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a> - </div> - </div> - </section> - </div> - </div> -%elif settings.MITX_FEATURES.get('USE_YOUTUBE_OBJECT_API') and normal_speed_video_id: +%if settings.MITX_FEATURES.get('USE_YOUTUBE_OBJECT_API') and normal_speed_video_id: <object width="640" height="390"> <param name="movie" - value="https://www.youtube.com/v/${normal_speed_video_id}?version=3&autoplay=1&rel=0"></param> + % if not settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']: + value="https://www.youtube.com/v/${normal_speed_video_id}?version=3&autoplay=1&rel=0"> + % endif + </param> <param name="allowScriptAccess" value="always"></param> - <embed src="https://www.youtube.com/v/${normal_speed_video_id}?version=3&autoplay=1&rel=0" - type="application/x-shockwave-flash" - allowscriptaccess="always" - width="640" height="390"></embed> + <embed + % if not settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']: + src="https://www.youtube.com/v/${normal_speed_video_id}?version=3&autoplay=1&rel=0" + % endif + type="application/x-shockwave-flash" + allowscriptaccess="always" + width="640" height="390"></embed> </object> %else: - <div id="video_${id}" class="video" data-streams="${streams}" data-show-captions="${show_captions}" data-start="${start}" data-end="${end}" data-caption-asset-path="${caption_asset_path}" data-autoplay="${settings.MITX_FEATURES['AUTOPLAY_VIDEOS']}"> +<div id="video_${id}" class="video" + + % if not settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']: + data-streams="${streams}" + % endif + + data-show-captions="${show_captions}" + data-start="${start}" data-end="${end}" + data-caption-asset-path="${caption_asset_path}" + data-autoplay="${settings.MITX_FEATURES['AUTOPLAY_VIDEOS']}"> <div class="tc-wrapper"> <article class="video-wrapper"> <section class="video-player"> diff --git a/lms/templates/videoalpha.html b/lms/templates/videoalpha.html index 4e136bd170..2bb5d817a8 100644 --- a/lms/templates/videoalpha.html +++ b/lms/templates/videoalpha.html @@ -5,7 +5,11 @@ <div id="video_${id}" class="video" + + % if not settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']: data-streams="${youtube_streams}" + % endif + ${'data-sub="{}"'.format(sub) if sub else ''} % if not settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']: From dbd2716e29c7f2ad6391bfc0bb73c8000d3a769a Mon Sep 17 00:00:00 2001 From: Will Daly <will@edx.org> Date: Thu, 20 Jun 2013 07:44:33 -0400 Subject: [PATCH 254/375] Updated video alpha template name to reflect recent changes in master. --- lms/djangoapps/courseware/features/video.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/courseware/features/video.py b/lms/djangoapps/courseware/features/video.py index 90f68c1daf..cd1bdcf60f 100644 --- a/lms/djangoapps/courseware/features/video.py +++ b/lms/djangoapps/courseware/features/video.py @@ -50,7 +50,7 @@ def add_video_to_course(course): def add_videoalpha_to_course(course): - template_name = 'i4x://edx/templates/videoalpha/Video_Alpha_1' + template_name = 'i4x://edx/templates/videoalpha/Video_Alpha' world.ItemFactory.create(parent_location=section_location(course), template=template_name, - display_name='Video Alpha 1') + display_name='Video Alpha') From 6f964acec5d1558bd1d572ba158c141e5f05bf6e Mon Sep 17 00:00:00 2001 From: Alexander Kryklia <kryklia@gmail.com> Date: Thu, 16 May 2013 12:35:58 +0300 Subject: [PATCH 255/375] added docs and added is_correct to conditional --- common/lib/xmodule/xmodule/capa_module.py | 10 ++++++++-- common/lib/xmodule/xmodule/conditional_module.py | 10 ++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index a03c0f4160..792860e642 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -517,8 +517,14 @@ class CapaModule(CapaFields, XModule): return False def is_completed(self): - # used by conditional module - # return self.answer_available() + """ Used to decide to show or hide RESET or CHECK buttons. + + Actually means that student submitted problem and nothing more. + Problem can be completely wrong. + Pressing RESET button makes this function to return False. + Suggestion: rename it to is_submitted. + + # older comment: return self.answer_available()""" return self.lcp.done def is_attempted(self): diff --git a/common/lib/xmodule/xmodule/conditional_module.py b/common/lib/xmodule/xmodule/conditional_module.py index 9fda387ecb..080a7c48ea 100644 --- a/common/lib/xmodule/xmodule/conditional_module.py +++ b/common/lib/xmodule/xmodule/conditional_module.py @@ -70,8 +70,18 @@ class ConditionalModule(ConditionalFields, XModule): # value: <name of module attribute> conditions_map = { 'poll_answer': 'poll_answer', # poll_question attr + + # problem was submitted (it can be wrong) + # if student will press reset button after that, + # state will be reverted 'completed': 'is_completed', # capa_problem attr + + # if student attempted problem 'attempted': 'is_attempted', # capa_problem attr + + # if problem is full points + 'correct': 'is_correct', + 'voted': 'voted' # poll_question attr } From a7cf9d186dc600d609f954835d05b4416106410a Mon Sep 17 00:00:00 2001 From: Alexander Kryklia <kryklia@gmail.com> Date: Thu, 16 May 2013 13:19:04 +0300 Subject: [PATCH 256/375] adds doc about correct --- common/lib/xmodule/xmodule/conditional_module.py | 1 + 1 file changed, 1 insertion(+) diff --git a/common/lib/xmodule/xmodule/conditional_module.py b/common/lib/xmodule/xmodule/conditional_module.py index 080a7c48ea..a214dd290a 100644 --- a/common/lib/xmodule/xmodule/conditional_module.py +++ b/common/lib/xmodule/xmodule/conditional_module.py @@ -37,6 +37,7 @@ class ConditionalModule(ConditionalFields, XModule): completed - map to `is_completed` module method attempted - map to `is_attempted` module method + correct - map to `is_correct` module method poll_answer - map to `poll_answer` module attribute voted - map to `voted` module attribute From 23d4a2b3db715395f6d8d28fe091d6225441ada3 Mon Sep 17 00:00:00 2001 From: Alexander Kryklia <kryklia@gmail.com> Date: Thu, 20 Jun 2013 15:09:05 +0300 Subject: [PATCH 257/375] renames is_completed to is_submitted, fixes docstrings and rst docs --- common/lib/xmodule/xmodule/capa_module.py | 19 +++++++++---------- .../lib/xmodule/xmodule/conditional_module.py | 6 ++++-- .../conditional_module/conditional_module.rst | 5 ++++- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 792860e642..d9f7fc61aa 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -279,7 +279,7 @@ class CapaModule(CapaFields, XModule): """ Return True/False to indicate whether to show the "Check" button. """ - submitted_without_reset = (self.is_completed() and self.rerandomize == "always") + submitted_without_reset = (self.is_submitted() and self.rerandomize == "always") # If the problem is closed (past due / too many attempts) # then we do NOT show the "check" button @@ -302,7 +302,7 @@ class CapaModule(CapaFields, XModule): # then do NOT show the reset button. # If the problem hasn't been submitted yet, then do NOT show # the reset button. - if (self.closed() and not is_survey_question) or not self.is_completed(): + if (self.closed() and not is_survey_question) or not self.is_submitted(): return False else: return True @@ -322,7 +322,7 @@ class CapaModule(CapaFields, XModule): return not self.closed() else: is_survey_question = (self.max_attempts == 0) - needs_reset = self.is_completed() and self.rerandomize == "always" + needs_reset = self.is_submitted() and self.rerandomize == "always" # If the student has unlimited attempts, and their answers # are not randomized, then we do not need a save button @@ -516,19 +516,18 @@ class CapaModule(CapaFields, XModule): return False - def is_completed(self): - """ Used to decide to show or hide RESET or CHECK buttons. + def is_submitted(self): + """ + Used to decide to show or hide RESET or CHECK buttons. - Actually means that student submitted problem and nothing more. + Means that student submitted problem and nothing more. Problem can be completely wrong. Pressing RESET button makes this function to return False. - Suggestion: rename it to is_submitted. - - # older comment: return self.answer_available()""" + """ return self.lcp.done def is_attempted(self): - # used by conditional module + """Used by conditional module""" return self.attempts > 0 def is_correct(self): diff --git a/common/lib/xmodule/xmodule/conditional_module.py b/common/lib/xmodule/xmodule/conditional_module.py index a214dd290a..6dc86880ae 100644 --- a/common/lib/xmodule/xmodule/conditional_module.py +++ b/common/lib/xmodule/xmodule/conditional_module.py @@ -35,7 +35,9 @@ class ConditionalModule(ConditionalFields, XModule): <conditional> tag attributes: sources - location id of required modules, separated by ';' - completed - map to `is_completed` module method + submitted - map to `is_submitted` module method. + (pressing RESET button makes this function to return False.) + attempted - map to `is_attempted` module method correct - map to `is_correct` module method poll_answer - map to `poll_answer` module attribute @@ -75,7 +77,7 @@ class ConditionalModule(ConditionalFields, XModule): # problem was submitted (it can be wrong) # if student will press reset button after that, # state will be reverted - 'completed': 'is_completed', # capa_problem attr + 'submitted': 'is_submitted', # capa_problem attr # if student attempted problem 'attempted': 'is_attempted', # capa_problem attr diff --git a/doc/public/course_data_formats/conditional_module/conditional_module.rst b/doc/public/course_data_formats/conditional_module/conditional_module.rst index 8f7ba17ffc..c0c3a3c338 100644 --- a/doc/public/course_data_formats/conditional_module/conditional_module.rst +++ b/doc/public/course_data_formats/conditional_module/conditional_module.rst @@ -23,8 +23,11 @@ be specified for this tag:: sources - location id of required modules, separated by ';' [message | ""] - message for case, where one or more are not passed. Here you can use variable {link}, which generate link to required module. + + [submitted] - map to `is_submitted` module method. + (pressing RESET button makes this function to return False.) - [completed] - map to `is_completed` module method + [correct] - map to `is_correct` module method [attempted] - map to `is_attempted` module method [poll_answer] - map to `poll_answer` module attribute [voted] - map to `voted` module attribute From 448ca26cdf23e848a0adca25a7ed29bd7e1de9e4 Mon Sep 17 00:00:00 2001 From: Calen Pennington <cale@edx.org> Date: Thu, 20 Jun 2013 09:06:29 -0400 Subject: [PATCH 258/375] Remove simplewiki from the codebase --- doc/overview.md | 5 - docs/source/lms.rst | 28 - .../multicourse/multicourse_settings.py | 2 +- lms/djangoapps/simplewiki/__init__.py | 9 - lms/djangoapps/simplewiki/admin.py | 70 -- lms/djangoapps/simplewiki/mdx_circuit.py | 72 -- lms/djangoapps/simplewiki/mdx_image.py | 71 -- lms/djangoapps/simplewiki/mdx_mathjax.py | 30 - lms/djangoapps/simplewiki/mdx_video.py | 289 ------- lms/djangoapps/simplewiki/mdx_wikipath.py | 96 --- .../simplewiki/migrations/0001_initial.py | 216 ----- .../migrations/0002_unique_slugs.py | 136 --- ...article_parent__add_field_article_names.py | 161 ---- .../0004_multicourse_data_migration.py | 134 --- .../0005_auto__add_unique_namespace_name.py | 129 --- .../simplewiki/migrations/0006_auto.py | 129 --- .../simplewiki/migrations/0007_auto.py | 129 --- .../simplewiki/migrations/__init__.py | 0 lms/djangoapps/simplewiki/models.py | 387 --------- .../simplewiki/templatetags/__init__.py | 0 .../templatetags/simplewiki_utils.py | 20 - lms/djangoapps/simplewiki/tests.py | 23 - lms/djangoapps/simplewiki/urls.py | 19 - lms/djangoapps/simplewiki/usage.txt | 800 ------------------ lms/djangoapps/simplewiki/views.py | 552 ------------ lms/djangoapps/simplewiki/wiki_settings.py | 111 --- lms/envs/common.py | 1 - lms/templates/simplewiki/simplewiki_base.html | 164 ---- lms/templates/simplewiki/simplewiki_edit.html | 76 -- .../simplewiki/simplewiki_error.html | 79 -- .../simplewiki/simplewiki_history.html | 92 -- .../simplewiki/simplewiki_instructions.html | 24 - .../simplewiki/simplewiki_revision_feed.html | 63 -- .../simplewiki/simplewiki_searchresults.html | 34 - .../simplewiki_updateprogressbar.html | 37 - lms/templates/simplewiki/simplewiki_view.html | 15 - 36 files changed, 1 insertion(+), 4202 deletions(-) delete mode 100644 lms/djangoapps/simplewiki/__init__.py delete mode 100644 lms/djangoapps/simplewiki/admin.py delete mode 100755 lms/djangoapps/simplewiki/mdx_circuit.py delete mode 100755 lms/djangoapps/simplewiki/mdx_image.py delete mode 100644 lms/djangoapps/simplewiki/mdx_mathjax.py delete mode 100755 lms/djangoapps/simplewiki/mdx_video.py delete mode 100755 lms/djangoapps/simplewiki/mdx_wikipath.py delete mode 100644 lms/djangoapps/simplewiki/migrations/0001_initial.py delete mode 100644 lms/djangoapps/simplewiki/migrations/0002_unique_slugs.py delete mode 100644 lms/djangoapps/simplewiki/migrations/0003_auto__add_namespace__del_field_article_parent__add_field_article_names.py delete mode 100644 lms/djangoapps/simplewiki/migrations/0004_multicourse_data_migration.py delete mode 100644 lms/djangoapps/simplewiki/migrations/0005_auto__add_unique_namespace_name.py delete mode 100644 lms/djangoapps/simplewiki/migrations/0006_auto.py delete mode 100644 lms/djangoapps/simplewiki/migrations/0007_auto.py delete mode 100644 lms/djangoapps/simplewiki/migrations/__init__.py delete mode 100644 lms/djangoapps/simplewiki/models.py delete mode 100644 lms/djangoapps/simplewiki/templatetags/__init__.py delete mode 100644 lms/djangoapps/simplewiki/templatetags/simplewiki_utils.py delete mode 100644 lms/djangoapps/simplewiki/tests.py delete mode 100644 lms/djangoapps/simplewiki/urls.py delete mode 100644 lms/djangoapps/simplewiki/usage.txt delete mode 100644 lms/djangoapps/simplewiki/views.py delete mode 100644 lms/djangoapps/simplewiki/wiki_settings.py delete mode 100644 lms/templates/simplewiki/simplewiki_base.html delete mode 100644 lms/templates/simplewiki/simplewiki_edit.html delete mode 100644 lms/templates/simplewiki/simplewiki_error.html delete mode 100644 lms/templates/simplewiki/simplewiki_history.html delete mode 100644 lms/templates/simplewiki/simplewiki_instructions.html delete mode 100644 lms/templates/simplewiki/simplewiki_revision_feed.html delete mode 100644 lms/templates/simplewiki/simplewiki_searchresults.html delete mode 100644 lms/templates/simplewiki/simplewiki_updateprogressbar.html delete mode 100644 lms/templates/simplewiki/simplewiki_view.html diff --git a/doc/overview.md b/doc/overview.md index 4d074dfaf3..31ddd011ff 100644 --- a/doc/overview.md +++ b/doc/overview.md @@ -122,11 +122,6 @@ In production, the django `collectstatic` command recompiles everything and puts In development, we don't use collectstatic, instead accessing the files in place. The auto-compilation is run via `common/djangoapps/pipeline_mako/templates/static_content.html`. Details: templates include `<%namespace name='static' file='static_content.html'/>`, then something like `<%static:css group='application'/>` to call the functions in `common/djangoapps/pipeline_mako/__init__.py`, which call the `django-pipeline` compilers. -### Other modules - -- Wiki -- in `lms/djangoapps/simplewiki`. Has some markdown extentions for embedding circuits, videos, etc. - - ## Testing See `testing.md`. diff --git a/docs/source/lms.rst b/docs/source/lms.rst index 36622114ab..6548cd71a0 100644 --- a/docs/source/lms.rst +++ b/docs/source/lms.rst @@ -314,34 +314,6 @@ Psychoanalyze :members: :show-inheritance: -Simple wiki -=========== - -.. automodule:: simplewiki - :members: - :show-inheritance: - -Models ------- - -.. automodule:: simplewiki.models - :members: - :show-inheritance: - -Views ------ - -.. automodule:: simplewiki.views - :members: - :show-inheritance: - -Tests ------ - -.. automodule:: simplewiki.tests - :members: - :show-inheritance: - Static template view ==================== diff --git a/lms/djangoapps/multicourse/multicourse_settings.py b/lms/djangoapps/multicourse/multicourse_settings.py index c3df167ad8..de445dc0e1 100644 --- a/lms/djangoapps/multicourse/multicourse_settings.py +++ b/lms/djangoapps/multicourse/multicourse_settings.py @@ -10,7 +10,7 @@ # keys being the COURSE_NAME (spaces ok), and the value being a dict of # parameter,value pairs. The required parameters are: # -# - number : course number (used in the simplewiki pages) +# - number : course number (used in the wiki pages) # - title : humanized descriptive course title # # Optional parameters: diff --git a/lms/djangoapps/simplewiki/__init__.py b/lms/djangoapps/simplewiki/__init__.py deleted file mode 100644 index 9f9c332419..0000000000 --- a/lms/djangoapps/simplewiki/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Source: django-simplewiki. GPL license. - -import os -import sys - -# allow mdx_* parsers to be just dropped in the simplewiki folder -module_path = os.path.abspath(os.path.dirname(__file__)) -if module_path not in sys.path: - sys.path.append(module_path) diff --git a/lms/djangoapps/simplewiki/admin.py b/lms/djangoapps/simplewiki/admin.py deleted file mode 100644 index 2ba6405956..0000000000 --- a/lms/djangoapps/simplewiki/admin.py +++ /dev/null @@ -1,70 +0,0 @@ -# Source: django-simplewiki. GPL license. - -from django import forms -from django.contrib import admin -from django.utils.translation import ugettext as _ - -from .models import Article, Revision, Permission, ArticleAttachment - - -class RevisionInline(admin.TabularInline): - model = Revision - extra = 1 - - -class RevisionAdmin(admin.ModelAdmin): - list_display = ('article', '__unicode__', 'revision_date', 'revision_user', 'revision_text') - search_fields = ('article', 'counter') - - -class AttachmentAdmin(admin.ModelAdmin): - list_display = ('article', '__unicode__', 'uploaded_on', 'uploaded_by') - - -class ArticleAdminForm(forms.ModelForm): - def clean(self): - cleaned_data = self.cleaned_data - if cleaned_data.get("slug").startswith('_'): - raise forms.ValidationError(_('Slug cannot start with _ character.' - 'Reserved for internal use.')) - if not self.instance.pk: - parent = cleaned_data.get("parent") - slug = cleaned_data.get("slug") - if Article.objects.filter(slug__exact=slug, parent__exact=parent): - raise forms.ValidationError(_('Article slug and parent must be ' - 'unique together.')) - return cleaned_data - - class Meta: - model = Article - - -class ArticleAdmin(admin.ModelAdmin): - list_display = ('created_by', 'slug', 'modified_on', 'namespace') - search_fields = ('slug',) - prepopulated_fields = {'slug': ('title',)} - inlines = [RevisionInline] - form = ArticleAdminForm - save_on_top = True - - def formfield_for_foreignkey(self, db_field, request, **kwargs): - if db_field.name == 'current_revision': - # Try to determine the id of the article being edited - id = request.path.split('/') - import re - if len(id) > 0 and re.match(r"\d+", id[-2]): - kwargs["queryset"] = Revision.objects.filter(article=id[-2]) - return db_field.formfield(**kwargs) - else: - db_field.editable = False - return db_field.formfield(**kwargs) - return super(ArticleAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs) - - -class PermissionAdmin(admin.ModelAdmin): - search_fields = ('article', 'counter') - -admin.site.register(Article, ArticleAdmin) -admin.site.register(Revision, RevisionAdmin) -admin.site.register(Permission, PermissionAdmin) -admin.site.register(ArticleAttachment, AttachmentAdmin) diff --git a/lms/djangoapps/simplewiki/mdx_circuit.py b/lms/djangoapps/simplewiki/mdx_circuit.py deleted file mode 100755 index 4ec7501341..0000000000 --- a/lms/djangoapps/simplewiki/mdx_circuit.py +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env python -''' -Image Circuit Extension for Python-Markdown -====================================== - - -Any single line beginning with circuit-schematic: and followed by data (which should be json data, but this -is not enforced at this level) will be displayed as a circuit schematic. This is simply an input element with -the value set to the data. It is left to javascript on the page to render that input as a circuit schematic. - -ex: -circuit-schematic:[["r",[128,48,0],{"r":"1","_json_":0},["2","1"]],["view",0,0,2,null,null,null,null,null,null,null],["dc",{"0":0,"1":1,"I(_3)":-1}]] - -(This is a schematic with a single one-ohm resistor. Note that this data is not meant to be user-editable.) - -''' -import markdown -import re - -from django.utils.html import escape - -try: - # Markdown 2.1.0 changed from 2.0.3. We try importing the new version first, - # but import the 2.0.3 version if it fails - from markdown.util import etree -except: - from markdown import etree - - -class CircuitExtension(markdown.Extension): - def __init__(self, configs): - for key, value in configs: - self.setConfig(key, value) - - def extendMarkdown(self, md, md_globals): - ## Because Markdown treats contigous lines as one block of text, it is hard to match - ## a regex that must occupy the whole line (like the circuit regex). This is why we have - ## a preprocessor that inspects the lines and replaces the matched lines with text that is - ## easier to match - md.preprocessors.add('circuit', CircuitPreprocessor(md), "_begin") - - pattern = CircuitLink(r'processed-schematic:(?P<data>.*?)processed-schematic-end') - pattern.md = md - pattern.ext = self - md.inlinePatterns.add('circuit', pattern, "<reference") - - -class CircuitPreprocessor(markdown.preprocessors.Preprocessor): - preRegex = re.compile(r'^circuit-schematic:(?P<data>.*)$') - - def run(self, lines): - def convertLine(line): - m = self.preRegex.match(line) - if m: - return 'processed-schematic:{0}processed-schematic-end'.format(m.group('data')) - else: - return line - - return [convertLine(line) for line in lines] - - -class CircuitLink(markdown.inlinepatterns.Pattern): - def handleMatch(self, m): - data = m.group('data') - data = escape(data) - return etree.fromstring("<div align='center'><input type='hidden' parts='' value='" + data + "' analyses='' class='schematic ctrls' width='640' height='480'/></div>") - - -def makeExtension(configs=None): - to_return = CircuitExtension(configs=configs) - print "circuit returning ", to_return - return to_return diff --git a/lms/djangoapps/simplewiki/mdx_image.py b/lms/djangoapps/simplewiki/mdx_image.py deleted file mode 100755 index af0413f841..0000000000 --- a/lms/djangoapps/simplewiki/mdx_image.py +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env python -''' -Image Embedding Extension for Python-Markdown -====================================== - -Converts lone links to embedded images, provided the file extension is allowed. - -Ex: - http://www.ericfehse.net/media/img/ef/blog/django-pony.jpg - becomes - <img src="http://www.ericfehse.net/media/img/ef/blog/django-pony.jpg"> - - mypic.jpg becomes <img src="/MEDIA_PATH/mypic.jpg"> - -Requires Python-Markdown 1.6+ -''' - -import simplewiki.settings as settings - -import markdown -try: - # Markdown 2.1.0 changed from 2.0.3. We try importing the new version first, - # but import the 2.0.3 version if it fails - from markdown.util import etree -except: - from markdown import etree - - -class ImageExtension(markdown.Extension): - def __init__(self, configs): - for key, value in configs: - self.setConfig(key, value) - - def add_inline(self, md, name, klass, re): - pattern = klass(re) - pattern.md = md - pattern.ext = self - md.inlinePatterns.add(name, pattern, "<reference") - - def extendMarkdown(self, md, md_globals): - self.add_inline(md, 'image', ImageLink, - r'^(?P<proto>([^:/?#])+://)?(?P<domain>([^/?#]*)/)?(?P<path>[^?#]*\.(?P<ext>[^?#]{3,4}))(?:\?([^#]*))?(?:#(.*))?$') - - -class ImageLink(markdown.inlinepatterns.Pattern): - def handleMatch(self, m): - img = etree.Element('img') - proto = m.group('proto') or "http://" - domain = m.group('domain') - path = m.group('path') - ext = m.group('ext') - - # A fixer upper - if ext.lower() in settings.WIKI_IMAGE_EXTENSIONS: - if domain: - src = proto + domain + path - elif path: - # We need a nice way to source local attachments... - src = "/wiki/media/" + path + ".upload" - else: - src = '' - img.set('src', src) - return img - - -def makeExtension(configs=None): - return ImageExtension(configs=configs) - -if __name__ == "__main__": - import doctest - doctest.testmod() diff --git a/lms/djangoapps/simplewiki/mdx_mathjax.py b/lms/djangoapps/simplewiki/mdx_mathjax.py deleted file mode 100644 index b14803744b..0000000000 --- a/lms/djangoapps/simplewiki/mdx_mathjax.py +++ /dev/null @@ -1,30 +0,0 @@ -# Source: https://github.com/mayoff/python-markdown-mathjax - -import markdown -try: - # Markdown 2.1.0 changed from 2.0.3. We try importing the new version first, - # but import the 2.0.3 version if it fails - from markdown.util import etree, AtomicString -except: - from markdown import etree, AtomicString - - -class MathJaxPattern(markdown.inlinepatterns.Pattern): - - def __init__(self): - markdown.inlinepatterns.Pattern.__init__(self, r'(?<!\\)(\$\$?)(.+?)\2') - - def handleMatch(self, m): - el = etree.Element('span') - el.text = AtomicString(m.group(2) + m.group(3) + m.group(2)) - return el - - -class MathJaxExtension(markdown.Extension): - def extendMarkdown(self, md, md_globals): - # Needs to come before escape matching because \ is pretty important in LaTeX - md.inlinePatterns.add('mathjax', MathJaxPattern(), '<escape') - - -def makeExtension(configs=None): - return MathJaxExtension(configs) diff --git a/lms/djangoapps/simplewiki/mdx_video.py b/lms/djangoapps/simplewiki/mdx_video.py deleted file mode 100755 index f27b1b63ba..0000000000 --- a/lms/djangoapps/simplewiki/mdx_video.py +++ /dev/null @@ -1,289 +0,0 @@ -#!/usr/bin/env python - -""" -Embeds web videos using URLs. For instance, if a URL to an youtube video is -found in the text submitted to markdown and it isn't enclosed in parenthesis -like a normal link in markdown, then the URL will be swapped with a embedded -youtube video. - -All resulting HTML is XHTML Strict compatible. - ->>> import markdown - -Test Metacafe - ->>> s = "http://www.metacafe.com/watch/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room/" ->>> markdown.markdown(s, ['video']) -u'<p><object data="http://www.metacafe.com/fplayer/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room.swf" height="423" type="application/x-shockwave-flash" width="498"><param name="movie" value="http://www.metacafe.com/fplayer/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room.swf" /><param name="allowFullScreen" value="true" /></object></p>' - - -Test Metacafe with arguments - ->>> markdown.markdown(s, ['video(metacafe_width=500,metacafe_height=425)']) -u'<p><object data="http://www.metacafe.com/fplayer/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room.swf" height="425" type="application/x-shockwave-flash" width="500"><param name="movie" value="http://www.metacafe.com/fplayer/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room.swf" /><param name="allowFullScreen" value="true" /></object></p>' - - -Test Link To Metacafe - ->>> s = "[Metacafe link](http://www.metacafe.com/watch/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room/)" ->>> markdown.markdown(s, ['video']) -u'<p><a href="http://www.metacafe.com/watch/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room/">Metacafe link</a></p>' - - -Test Markdown Escaping - ->>> s = "\\http://www.metacafe.com/watch/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room/" ->>> markdown.markdown(s, ['video']) -u'<p>http://www.metacafe.com/watch/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room/</p>' ->>> s = "`http://www.metacafe.com/watch/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room/`" ->>> markdown.markdown(s, ['video']) -u'<p><code>http://www.metacafe.com/watch/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room/</code></p>' - - -Test Youtube - ->>> s = "http://www.youtube.com/watch?v=u1mA-0w8XPo&hd=1&fs=1&feature=PlayList&p=34C6046F7FEACFD3&playnext=1&playnext_from=PL&index=1" ->>> markdown.markdown(s, ['video']) -u'<p><object data="http://www.youtube.com/v/u1mA-0w8XPo&hd=1&fs=1&feature=PlayList&p=34C6046F7FEACFD3&playnext=1&playnext_from=PL&index=1" height="344" type="application/x-shockwave-flash" width="425"><param name="movie" value="http://www.youtube.com/v/u1mA-0w8XPo&hd=1&fs=1&feature=PlayList&p=34C6046F7FEACFD3&playnext=1&playnext_from=PL&index=1" /><param name="allowFullScreen" value="true" /></object></p>' - - -Test Youtube with argument - ->>> markdown.markdown(s, ['video(youtube_width=200,youtube_height=100)']) -u'<p><object data="http://www.youtube.com/v/u1mA-0w8XPo&hd=1&fs=1&feature=PlayList&p=34C6046F7FEACFD3&playnext=1&playnext_from=PL&index=1" height="100" type="application/x-shockwave-flash" width="200"><param name="movie" value="http://www.youtube.com/v/u1mA-0w8XPo&hd=1&fs=1&feature=PlayList&p=34C6046F7FEACFD3&playnext=1&playnext_from=PL&index=1" /><param name="allowFullScreen" value="true" /></object></p>' - - -Test Youtube Link - ->>> s = "[Youtube link](http://www.youtube.com/watch?v=u1mA-0w8XPo&feature=PlayList&p=34C6046F7FEACFD3&playnext=1&playnext_from=PL&index=1)" ->>> markdown.markdown(s, ['video']) -u'<p><a href="http://www.youtube.com/watch?v=u1mA-0w8XPo&feature=PlayList&p=34C6046F7FEACFD3&playnext=1&playnext_from=PL&index=1">Youtube link</a></p>' - - -Test Dailymotion - ->>> s = "http://www.dailymotion.com/relevance/search/ut2004/video/x3kv65_ut2004-ownage_videogames" ->>> markdown.markdown(s, ['video']) -u'<p><object data="http://www.dailymotion.com/swf/x3kv65_ut2004-ownage_videogames" height="405" type="application/x-shockwave-flash" width="480"><param name="movie" value="http://www.dailymotion.com/swf/x3kv65_ut2004-ownage_videogames" /><param name="allowFullScreen" value="true" /></object></p>' - - -Test Dailymotion again (Dailymotion and their crazy URLs) - ->>> s = "http://www.dailymotion.com/us/video/x8qak3_iron-man-vs-bruce-lee_fun" ->>> markdown.markdown(s, ['video']) -u'<p><object data="http://www.dailymotion.com/swf/x8qak3_iron-man-vs-bruce-lee_fun" height="405" type="application/x-shockwave-flash" width="480"><param name="movie" value="http://www.dailymotion.com/swf/x8qak3_iron-man-vs-bruce-lee_fun" /><param name="allowFullScreen" value="true" /></object></p>' - - -Test Yahoo! Video - ->>> s = "http://video.yahoo.com/watch/1981791/4769603" ->>> markdown.markdown(s, ['video']) -u'<p><object data="http://d.yimg.com/static.video.yahoo.com/yep/YV_YEP.swf?ver=2.2.40" height="322" type="application/x-shockwave-flash" width="512"><param name="movie" value="http://d.yimg.com/static.video.yahoo.com/yep/YV_YEP.swf?ver=2.2.40" /><param name="allowFullScreen" value="true" /><param name="flashVars" value="id=4769603&vid=1981791" /></object></p>' - - -Test Veoh Video - ->>> s = "http://www.veoh.com/search/videos/q/mario#watch%3De129555XxCZanYD" ->>> markdown.markdown(s, ['video']) -u'<p><object data="http://www.veoh.com/videodetails2.swf?permalinkId=e129555XxCZanYD" height="341" type="application/x-shockwave-flash" width="410"><param name="movie" value="http://www.veoh.com/videodetails2.swf?permalinkId=e129555XxCZanYD" /><param name="allowFullScreen" value="true" /></object></p>' - - -Test Veoh Video Again (More fun URLs) - ->>> s = "http://www.veoh.com/group/BigCatRescuers#watch%3Dv16771056hFtSBYEr" ->>> markdown.markdown(s, ['video']) -u'<p><object data="http://www.veoh.com/videodetails2.swf?permalinkId=v16771056hFtSBYEr" height="341" type="application/x-shockwave-flash" width="410"><param name="movie" value="http://www.veoh.com/videodetails2.swf?permalinkId=v16771056hFtSBYEr" /><param name="allowFullScreen" value="true" /></object></p>' - - -Test Veoh Video Yet Again (Even more fun URLs) - ->>> s = "http://www.veoh.com/browse/videos/category/anime/watch/v181645607JyXPWcQ" ->>> markdown.markdown(s, ['video']) -u'<p><object data="http://www.veoh.com/videodetails2.swf?permalinkId=v181645607JyXPWcQ" height="341" type="application/x-shockwave-flash" width="410"><param name="movie" value="http://www.veoh.com/videodetails2.swf?permalinkId=v181645607JyXPWcQ" /><param name="allowFullScreen" value="true" /></object></p>' - - -Test Vimeo Video - ->>> s = "http://www.vimeo.com/1496152" ->>> markdown.markdown(s, ['video']) -u'<p><object data="http://vimeo.com/moogaloop.swf?clip_id=1496152&amp;server=vimeo.com" height="321" type="application/x-shockwave-flash" width="400"><param name="movie" value="http://vimeo.com/moogaloop.swf?clip_id=1496152&amp;server=vimeo.com" /><param name="allowFullScreen" value="true" /></object></p>' - -Test Vimeo Video with some GET values - ->>> s = "http://vimeo.com/1496152?test=test" ->>> markdown.markdown(s, ['video']) -u'<p><object data="http://vimeo.com/moogaloop.swf?clip_id=1496152&amp;server=vimeo.com" height="321" type="application/x-shockwave-flash" width="400"><param name="movie" value="http://vimeo.com/moogaloop.swf?clip_id=1496152&amp;server=vimeo.com" /><param name="allowFullScreen" value="true" /></object></p>' - -Test Blip.tv - ->>> s = "http://blip.tv/file/get/Pycon-PlenarySprintIntro563.flv" ->>> markdown.markdown(s, ['video']) -u'<p><object data="http://blip.tv/scripts/flash/showplayer.swf?file=http://blip.tv/file/get/Pycon-PlenarySprintIntro563.flv" height="300" type="application/x-shockwave-flash" width="480"><param name="movie" value="http://blip.tv/scripts/flash/showplayer.swf?file=http://blip.tv/file/get/Pycon-PlenarySprintIntro563.flv" /><param name="allowFullScreen" value="true" /></object></p>' - -Test Gametrailers - ->>> s = "http://www.gametrailers.com/video/console-comparison-borderlands/58079" ->>> markdown.markdown(s, ['video']) -u'<p><object data="http://www.gametrailers.com/remote_wrap.php?mid=58079" height="392" type="application/x-shockwave-flash" width="480"><param name="movie" value="http://www.gametrailers.com/remote_wrap.php?mid=58079" /><param name="allowFullScreen" value="true" /></object></p>' -""" - -import markdown -try: - # Markdown 2.1.0 changed from 2.0.3. We try importing the new version first, - # but import the 2.0.3 version if it fails - from markdown.util import etree -except: - from markdown import etree - - -version = "0.1.6" - - -class VideoExtension(markdown.Extension): - def __init__(self, configs): - self.config = { - 'bliptv_width': ['480', 'Width for Blip.tv videos'], - 'bliptv_height': ['300', 'Height for Blip.tv videos'], - 'dailymotion_width': ['480', 'Width for Dailymotion videos'], - 'dailymotion_height': ['405', 'Height for Dailymotion videos'], - 'gametrailers_width': ['480', 'Width for Gametrailers videos'], - 'gametrailers_height': ['392', 'Height for Gametrailers videos'], - 'metacafe_width': ['498', 'Width for Metacafe videos'], - 'metacafe_height': ['423', 'Height for Metacafe videos'], - 'veoh_width': ['410', 'Width for Veoh videos'], - 'veoh_height': ['341', 'Height for Veoh videos'], - 'vimeo_width': ['400', 'Width for Vimeo videos'], - 'vimeo_height': ['321', 'Height for Vimeo videos'], - 'yahoo_width': ['512', 'Width for Yahoo! videos'], - 'yahoo_height': ['322', 'Height for Yahoo! videos'], - 'youtube_width': ['425', 'Width for Youtube videos'], - 'youtube_height': ['344', 'Height for Youtube videos'], - } - - # Override defaults with user settings - for key, value in configs: - self.setConfig(key, value) - - def add_inline(self, md, name, klass, re): - pattern = klass(re) - pattern.md = md - pattern.ext = self - md.inlinePatterns.add(name, pattern, "<reference") - - def extendMarkdown(self, md, md_globals): - self.add_inline(md, 'bliptv', Bliptv, - r'([^(]|^)http://(\w+\.|)blip.tv/file/get/(?P<bliptvfile>\S+.flv)') - self.add_inline(md, 'dailymotion', Dailymotion, - r'([^(]|^)http://www\.dailymotion\.com/(?P<dailymotionid>\S+)') - self.add_inline(md, 'gametrailers', Gametrailers, - r'([^(]|^)http://www.gametrailers.com/video/[a-z0-9-]+/(?P<gametrailersid>\d+)') - self.add_inline(md, 'metacafe', Metacafe, - r'([^(]|^)http://www\.metacafe\.com/watch/(?P<metacafeid>\S+)/') - self.add_inline(md, 'veoh', Veoh, - r'([^(]|^)http://www\.veoh\.com/\S*(#watch%3D|watch/)(?P<veohid>\w+)') - self.add_inline(md, 'vimeo', Vimeo, - r'([^(]|^)http://(www.|)vimeo\.com/(?P<vimeoid>\d+)\S*') - self.add_inline(md, 'yahoo', Yahoo, - r'([^(]|^)http://video\.yahoo\.com/watch/(?P<yahoovid>\d+)/(?P<yahooid>\d+)') - self.add_inline(md, 'youtube', Youtube, - r'([^(]|^)http://www\.youtube\.com/watch\?\S*v=(?P<youtubeargs>[A-Za-z0-9_&=-]+)\S*') - - -class Bliptv(markdown.inlinepatterns.Pattern): - def handleMatch(self, m): - url = 'http://blip.tv/scripts/flash/showplayer.swf?file=http://blip.tv/file/get/%s' % m.group('bliptvfile') - width = self.ext.config['bliptv_width'][0] - height = self.ext.config['bliptv_height'][0] - return flash_object(url, width, height) - - -class Dailymotion(markdown.inlinepatterns.Pattern): - def handleMatch(self, m): - url = 'http://www.dailymotion.com/swf/%s' % m.group('dailymotionid').split('/')[-1] - width = self.ext.config['dailymotion_width'][0] - height = self.ext.config['dailymotion_height'][0] - return flash_object(url, width, height) - - -class Gametrailers(markdown.inlinepatterns.Pattern): - def handleMatch(self, m): - url = 'http://www.gametrailers.com/remote_wrap.php?mid=%s' % \ - m.group('gametrailersid').split('/')[-1] - width = self.ext.config['gametrailers_width'][0] - height = self.ext.config['gametrailers_height'][0] - return flash_object(url, width, height) - - -class Metacafe(markdown.inlinepatterns.Pattern): - def handleMatch(self, m): - url = 'http://www.metacafe.com/fplayer/%s.swf' % m.group('metacafeid') - width = self.ext.config['metacafe_width'][0] - height = self.ext.config['metacafe_height'][0] - return flash_object(url, width, height) - - -class Veoh(markdown.inlinepatterns.Pattern): - def handleMatch(self, m): - url = 'http://www.veoh.com/videodetails2.swf?permalinkId=%s' % m.group('veohid') - width = self.ext.config['veoh_width'][0] - height = self.ext.config['veoh_height'][0] - return flash_object(url, width, height) - - -class Vimeo(markdown.inlinepatterns.Pattern): - def handleMatch(self, m): - url = 'http://vimeo.com/moogaloop.swf?clip_id=%s&server=vimeo.com' % m.group('vimeoid') - width = self.ext.config['vimeo_width'][0] - height = self.ext.config['vimeo_height'][0] - return flash_object(url, width, height) - - -class Yahoo(markdown.inlinepatterns.Pattern): - def handleMatch(self, m): - url = "http://d.yimg.com/static.video.yahoo.com/yep/YV_YEP.swf?ver=2.2.40" - width = self.ext.config['yahoo_width'][0] - height = self.ext.config['yahoo_height'][0] - obj = flash_object(url, width, height) - param = etree.Element('param') - param.set('name', 'flashVars') - param.set('value', "id=%s&vid=%s" % (m.group('yahooid'), - m.group('yahoovid'))) - obj.append(param) - return obj - - -class Youtube(markdown.inlinepatterns.Pattern): - def handleMatch(self, m): - url = 'http://www.youtube.com/v/%s' % m.group('youtubeargs') - width = self.ext.config['youtube_width'][0] - height = self.ext.config['youtube_height'][0] - return flash_object(url, width, height) - - -def flash_object(url, width, height): - obj = etree.Element('object') - obj.set('type', 'application/x-shockwave-flash') - obj.set('width', width) - obj.set('height', height) - obj.set('data', url) - param = etree.Element('param') - param.set('name', 'movie') - param.set('value', url) - obj.append(param) - param = etree.Element('param') - param.set('name', 'allowFullScreen') - param.set('value', 'true') - obj.append(param) - #param = etree.Element('param') - #param.set('name', 'allowScriptAccess') - #param.set('value', 'sameDomain') - #obj.append(param) - return obj - - -def makeExtension(configs=None): - return VideoExtension(configs=configs) - -if __name__ == "__main__": - import doctest - doctest.testmod() diff --git a/lms/djangoapps/simplewiki/mdx_wikipath.py b/lms/djangoapps/simplewiki/mdx_wikipath.py deleted file mode 100755 index 17c2b65591..0000000000 --- a/lms/djangoapps/simplewiki/mdx_wikipath.py +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env python - -''' -Wikipath Extension for Python-Markdown -====================================== - -Converts [Link Name](wiki:ArticleName) to relative links pointing to article. Requires Python-Markdown 2.0+ - -Basic usage: - - >>> import markdown - >>> text = "Some text with a [Link Name](wiki:ArticleName)." - >>> html = markdown.markdown(text, ['wikipath(base_url="/wiki/view/")']) - >>> html - u'<p>Some text with a <a class="wikipath" href="/wiki/view/ArticleName/">Link Name</a>.</p>' - -Dependencies: -* [Python 2.3+](http://python.org) -* [Markdown 2.0+](http://www.freewisdom.org/projects/python-markdown/) -''' - - -import markdown -try: - # Markdown 2.1.0 changed from 2.0.3. We try importing the new version first, - # but import the 2.0.3 version if it fails - from markdown.util import etree -except: - from markdown import etree - - -class WikiPathExtension(markdown.Extension): - def __init__(self, configs): - # set extension defaults - self.config = { - 'default_namespace': ['edX', 'Default namespace for when one isn\'t specified.'], - 'html_class': ['wikipath', 'CSS hook. Leave blank for none.'] - } - - # Override defaults with user settings - for key, value in configs: - # self.config[key][0] = value - self.setConfig(key, value) - - def extendMarkdown(self, md, md_globals): - self.md = md - - # append to end of inline patterns - WIKI_RE = r'\[(?P<linkTitle>.+?)\]\(wiki:(?P<wikiTitle>[a-zA-Z\d/_-]*)\)' - wikiPathPattern = WikiPath(WIKI_RE, self.config) - wikiPathPattern.md = md - md.inlinePatterns.add('wikipath', wikiPathPattern, "<reference") - - -class WikiPath(markdown.inlinepatterns.Pattern): - def __init__(self, pattern, config): - markdown.inlinepatterns.Pattern.__init__(self, pattern) - self.config = config - - def handleMatch(self, m): - article_title = m.group('wikiTitle') - if article_title.startswith("/"): - article_title = article_title[1:] - - if not "/" in article_title: - article_title = self.config['default_namespace'][0] + "/" + article_title - - url = "../" + article_title - label = m.group('linkTitle') - a = etree.Element('a') - a.set('href', url) - a.text = label - - if self.config['html_class'][0]: - a.set('class', self.config['html_class'][0]) - - return a - - def _getMeta(self): - """ Return meta data or config data. """ - base_url = self.config['base_url'][0] - html_class = self.config['html_class'][0] - if hasattr(self.md, 'Meta'): - if self.md.Meta.has_key('wiki_base_url'): - base_url = self.md.Meta['wiki_base_url'][0] - if self.md.Meta.has_key('wiki_html_class'): - html_class = self.md.Meta['wiki_html_class'][0] - return base_url, html_class - - -def makeExtension(configs=None): - return WikiPathExtension(configs=configs) - -if __name__ == "__main__": - import doctest - doctest.testmod() diff --git a/lms/djangoapps/simplewiki/migrations/0001_initial.py b/lms/djangoapps/simplewiki/migrations/0001_initial.py deleted file mode 100644 index b56a28295a..0000000000 --- a/lms/djangoapps/simplewiki/migrations/0001_initial.py +++ /dev/null @@ -1,216 +0,0 @@ -# -*- coding: utf-8 -*- -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - - -class Migration(SchemaMigration): - - def forwards(self, orm): - # Adding model 'Article' - db.create_table('simplewiki_article', ( - ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('title', self.gf('django.db.models.fields.CharField')(max_length=512)), - ('slug', self.gf('django.db.models.fields.SlugField')(max_length=100, blank=True)), - ('created_by', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, blank=True)), - ('created_on', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=1, blank=True)), - ('modified_on', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=1, blank=True)), - ('parent', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['simplewiki.Article'], null=True, blank=True)), - ('locked', self.gf('django.db.models.fields.BooleanField')(default=False)), - ('permissions', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['simplewiki.Permission'], null=True, blank=True)), - ('current_revision', self.gf('django.db.models.fields.related.OneToOneField')(blank=True, related_name='current_rev', unique=True, null=True, to=orm['simplewiki.Revision'])), - )) - db.send_create_signal('simplewiki', ['Article']) - - # Adding unique constraint on 'Article', fields ['slug', 'parent'] - db.create_unique('simplewiki_article', ['slug', 'parent_id']) - - # Adding M2M table for field related on 'Article' - db.create_table('simplewiki_article_related', ( - ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), - ('from_article', models.ForeignKey(orm['simplewiki.article'], null=False)), - ('to_article', models.ForeignKey(orm['simplewiki.article'], null=False)) - )) - db.create_unique('simplewiki_article_related', ['from_article_id', 'to_article_id']) - - # Adding model 'ArticleAttachment' - db.create_table('simplewiki_articleattachment', ( - ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('article', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['simplewiki.Article'])), - ('file', self.gf('django.db.models.fields.files.FileField')(max_length=255)), - ('uploaded_by', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, blank=True)), - ('uploaded_on', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), - )) - db.send_create_signal('simplewiki', ['ArticleAttachment']) - - # Adding model 'Revision' - db.create_table('simplewiki_revision', ( - ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('article', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['simplewiki.Article'])), - ('revision_text', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)), - ('revision_user', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='wiki_revision_user', null=True, to=orm['auth.User'])), - ('revision_date', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), - ('contents', self.gf('django.db.models.fields.TextField')()), - ('contents_parsed', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), - ('counter', self.gf('django.db.models.fields.IntegerField')(default=1)), - ('previous_revision', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['simplewiki.Revision'], null=True, blank=True)), - ('deleted', self.gf('django.db.models.fields.IntegerField')(default=0)), - )) - db.send_create_signal('simplewiki', ['Revision']) - - # Adding model 'Permission' - db.create_table('simplewiki_permission', ( - ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('permission_name', self.gf('django.db.models.fields.CharField')(max_length=255)), - )) - db.send_create_signal('simplewiki', ['Permission']) - - # Adding M2M table for field can_write on 'Permission' - db.create_table('simplewiki_permission_can_write', ( - ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), - ('permission', models.ForeignKey(orm['simplewiki.permission'], null=False)), - ('user', models.ForeignKey(orm['auth.user'], null=False)) - )) - db.create_unique('simplewiki_permission_can_write', ['permission_id', 'user_id']) - - # Adding M2M table for field can_read on 'Permission' - db.create_table('simplewiki_permission_can_read', ( - ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), - ('permission', models.ForeignKey(orm['simplewiki.permission'], null=False)), - ('user', models.ForeignKey(orm['auth.user'], null=False)) - )) - db.create_unique('simplewiki_permission_can_read', ['permission_id', 'user_id']) - - def backwards(self, orm): - # Removing unique constraint on 'Article', fields ['slug', 'parent'] - db.delete_unique('simplewiki_article', ['slug', 'parent_id']) - - # Deleting model 'Article' - db.delete_table('simplewiki_article') - - # Removing M2M table for field related on 'Article' - db.delete_table('simplewiki_article_related') - - # Deleting model 'ArticleAttachment' - db.delete_table('simplewiki_articleattachment') - - # Deleting model 'Revision' - db.delete_table('simplewiki_revision') - - # Deleting model 'Permission' - db.delete_table('simplewiki_permission') - - # Removing M2M table for field can_write on 'Permission' - db.delete_table('simplewiki_permission_can_write') - - # Removing M2M table for field can_read on 'Permission' - db.delete_table('simplewiki_permission_can_read') - - models = { - 'auth.group': { - 'Meta': {'object_name': 'Group'}, - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), - 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) - }, - 'auth.permission': { - 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, - 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) - }, - 'auth.user': { - 'Meta': {'object_name': 'User'}, - 'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}), - 'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), - 'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), - 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}), - 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), - 'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), - 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), - 'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), - 'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}), - 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), - 'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}), - 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), - 'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), - 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}), - 'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), - 'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), - 'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), - 'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), - 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}), - 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), - 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}), - 'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}) - }, - 'contenttypes.contenttype': { - 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, - 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) - }, - 'simplewiki.article': { - 'Meta': {'unique_together': "(('slug', 'parent'),)", 'object_name': 'Article'}, - 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), - 'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}), - 'current_revision': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'current_rev'", 'unique': 'True', 'null': 'True', 'to': "orm['simplewiki.Revision']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'modified_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}), - 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']", 'null': 'True', 'blank': 'True'}), - 'permissions': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Permission']", 'null': 'True', 'blank': 'True'}), - 'related': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'related_rel_+'", 'null': 'True', 'to': "orm['simplewiki.Article']"}), - 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '100', 'blank': 'True'}), - 'title': ('django.db.models.fields.CharField', [], {'max_length': '512'}) - }, - 'simplewiki.articleattachment': { - 'Meta': {'object_name': 'ArticleAttachment'}, - 'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}), - 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '255'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), - 'uploaded_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) - }, - 'simplewiki.permission': { - 'Meta': {'object_name': 'Permission'}, - 'can_read': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'read'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}), - 'can_write': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'write'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'permission_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) - }, - 'simplewiki.revision': { - 'Meta': {'object_name': 'Revision'}, - 'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}), - 'contents': ('django.db.models.fields.TextField', [], {}), - 'contents_parsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), - 'counter': ('django.db.models.fields.IntegerField', [], {'default': '1'}), - 'deleted': ('django.db.models.fields.IntegerField', [], {'default': '0'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'previous_revision': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Revision']", 'null': 'True', 'blank': 'True'}), - 'revision_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), - 'revision_text': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), - 'revision_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'wiki_revision_user'", 'null': 'True', 'to': "orm['auth.User']"}) - } - } - - complete_apps = ['simplewiki'] diff --git a/lms/djangoapps/simplewiki/migrations/0002_unique_slugs.py b/lms/djangoapps/simplewiki/migrations/0002_unique_slugs.py deleted file mode 100644 index 79f1c195e1..0000000000 --- a/lms/djangoapps/simplewiki/migrations/0002_unique_slugs.py +++ /dev/null @@ -1,136 +0,0 @@ -# -*- coding: utf-8 -*- -import datetime -from south.db import db -from south.v2 import DataMigration -from django.db import models - - -class Migration(DataMigration): - - def forwards(self, orm): - # We collect every article slug in a set. Any time we see a duplicate, we change the duplicate's name - unique_slugs = set() - for article in orm.Article.objects.all(): - if article.slug in unique_slugs: - i = 2 - new_name = article.slug + str(i) - while new_name in unique_slugs: - i += 1 - new_name = article.slug + str(i) - print "Changing", article.slug, "to", new_name - article.slug = new_name - article.save() - - unique_slugs.add(article.slug) - - def backwards(self, orm): - "Write your backwards methods here." - - models = { - 'auth.group': { - 'Meta': {'object_name': 'Group'}, - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), - 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) - }, - 'auth.permission': { - 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, - 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) - }, - 'auth.user': { - 'Meta': {'object_name': 'User'}, - 'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}), - 'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), - 'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), - 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}), - 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), - 'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), - 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), - 'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), - 'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}), - 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), - 'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}), - 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), - 'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), - 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}), - 'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), - 'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), - 'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), - 'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), - 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}), - 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), - 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}), - 'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}) - }, - 'contenttypes.contenttype': { - 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, - 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) - }, - 'simplewiki.article': { - 'Meta': {'unique_together': "(('slug', 'parent'),)", 'object_name': 'Article'}, - 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), - 'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}), - 'current_revision': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'current_rev'", 'unique': 'True', 'null': 'True', 'to': "orm['simplewiki.Revision']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'modified_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}), - 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']", 'null': 'True', 'blank': 'True'}), - 'permissions': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Permission']", 'null': 'True', 'blank': 'True'}), - 'related': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'related_rel_+'", 'null': 'True', 'to': "orm['simplewiki.Article']"}), - 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '100', 'blank': 'True'}), - 'title': ('django.db.models.fields.CharField', [], {'max_length': '512'}) - }, - 'simplewiki.articleattachment': { - 'Meta': {'object_name': 'ArticleAttachment'}, - 'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}), - 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '255'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), - 'uploaded_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) - }, - 'simplewiki.permission': { - 'Meta': {'object_name': 'Permission'}, - 'can_read': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'read'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}), - 'can_write': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'write'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'permission_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) - }, - 'simplewiki.revision': { - 'Meta': {'object_name': 'Revision'}, - 'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}), - 'contents': ('django.db.models.fields.TextField', [], {}), - 'contents_parsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), - 'counter': ('django.db.models.fields.IntegerField', [], {'default': '1'}), - 'deleted': ('django.db.models.fields.IntegerField', [], {'default': '0'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'previous_revision': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Revision']", 'null': 'True', 'blank': 'True'}), - 'revision_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), - 'revision_text': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), - 'revision_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'wiki_revision_user'", 'null': 'True', 'to': "orm['auth.User']"}) - } - } - - complete_apps = ['simplewiki'] - symmetrical = True diff --git a/lms/djangoapps/simplewiki/migrations/0003_auto__add_namespace__del_field_article_parent__add_field_article_names.py b/lms/djangoapps/simplewiki/migrations/0003_auto__add_namespace__del_field_article_parent__add_field_article_names.py deleted file mode 100644 index 85bfdadb00..0000000000 --- a/lms/djangoapps/simplewiki/migrations/0003_auto__add_namespace__del_field_article_parent__add_field_article_names.py +++ /dev/null @@ -1,161 +0,0 @@ -# -*- coding: utf-8 -*- -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - - -class Migration(SchemaMigration): - - def forwards(self, orm): - # Removing unique constraint on 'Article', fields ['slug', 'parent'] - db.delete_unique('simplewiki_article', ['slug', 'parent_id']) - - # Adding model 'Namespace' - db.create_table('simplewiki_namespace', ( - ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('name', self.gf('django.db.models.fields.CharField')(max_length=30)), - )) - db.send_create_signal('simplewiki', ['Namespace']) - - # Deleting field 'Article.parent' - db.delete_column('simplewiki_article', 'parent_id') - - # Adding field 'Article.namespace' - db.add_column('simplewiki_article', 'namespace', - self.gf('django.db.models.fields.related.ForeignKey')(default=1, to=orm['simplewiki.Namespace']), - keep_default=False) - - # Adding unique constraint on 'Article', fields ['namespace', 'slug'] - db.create_unique('simplewiki_article', ['namespace_id', 'slug']) - - def backwards(self, orm): - # Removing unique constraint on 'Article', fields ['namespace', 'slug'] - db.delete_unique('simplewiki_article', ['namespace_id', 'slug']) - - # Deleting model 'Namespace' - db.delete_table('simplewiki_namespace') - - # Adding field 'Article.parent' - db.add_column('simplewiki_article', 'parent', - self.gf('django.db.models.fields.related.ForeignKey')(to=orm['simplewiki.Article'], null=True, blank=True), - keep_default=False) - - # Deleting field 'Article.namespace' - db.delete_column('simplewiki_article', 'namespace_id') - - # Adding unique constraint on 'Article', fields ['slug', 'parent'] - db.create_unique('simplewiki_article', ['slug', 'parent_id']) - - models = { - 'auth.group': { - 'Meta': {'object_name': 'Group'}, - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), - 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) - }, - 'auth.permission': { - 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, - 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) - }, - 'auth.user': { - 'Meta': {'object_name': 'User'}, - 'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}), - 'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), - 'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), - 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}), - 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), - 'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), - 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), - 'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), - 'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}), - 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), - 'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}), - 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), - 'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), - 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}), - 'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), - 'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), - 'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), - 'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), - 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}), - 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), - 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}), - 'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}) - }, - 'contenttypes.contenttype': { - 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, - 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) - }, - 'simplewiki.article': { - 'Meta': {'unique_together': "(('slug', 'namespace'),)", 'object_name': 'Article'}, - 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), - 'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}), - 'current_revision': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'current_rev'", 'unique': 'True', 'null': 'True', 'to': "orm['simplewiki.Revision']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'modified_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}), - 'namespace': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Namespace']"}), - 'permissions': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Permission']", 'null': 'True', 'blank': 'True'}), - 'related': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'related_rel_+'", 'null': 'True', 'to': "orm['simplewiki.Article']"}), - 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '100', 'blank': 'True'}), - 'title': ('django.db.models.fields.CharField', [], {'max_length': '512'}) - }, - 'simplewiki.articleattachment': { - 'Meta': {'object_name': 'ArticleAttachment'}, - 'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}), - 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '255'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), - 'uploaded_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) - }, - 'simplewiki.namespace': { - 'Meta': {'object_name': 'Namespace'}, - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '30'}) - }, - 'simplewiki.permission': { - 'Meta': {'object_name': 'Permission'}, - 'can_read': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'read'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}), - 'can_write': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'write'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'permission_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) - }, - 'simplewiki.revision': { - 'Meta': {'object_name': 'Revision'}, - 'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}), - 'contents': ('django.db.models.fields.TextField', [], {}), - 'contents_parsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), - 'counter': ('django.db.models.fields.IntegerField', [], {'default': '1'}), - 'deleted': ('django.db.models.fields.IntegerField', [], {'default': '0'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'previous_revision': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Revision']", 'null': 'True', 'blank': 'True'}), - 'revision_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), - 'revision_text': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), - 'revision_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'wiki_revision_user'", 'null': 'True', 'to': "orm['auth.User']"}) - } - } - - complete_apps = ['simplewiki'] diff --git a/lms/djangoapps/simplewiki/migrations/0004_multicourse_data_migration.py b/lms/djangoapps/simplewiki/migrations/0004_multicourse_data_migration.py deleted file mode 100644 index 69adeb4641..0000000000 --- a/lms/djangoapps/simplewiki/migrations/0004_multicourse_data_migration.py +++ /dev/null @@ -1,134 +0,0 @@ -# -*- coding: utf-8 -*- -import datetime -from south.db import db -from south.v2 import DataMigration -from django.db import models - - -class Migration(DataMigration): - - def forwards(self, orm): - namespace6002x, created = orm.Namespace.objects.get_or_create(name="6.002xS12") - if created: - namespace6002x.save() - - for article in orm.Article.objects.all(): - article.namespace = namespace6002x - article.save() - - def backwards(self, orm): - raise RuntimeError("Cannot reverse this migration.") - - models = { - 'auth.group': { - 'Meta': {'object_name': 'Group'}, - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), - 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) - }, - 'auth.permission': { - 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, - 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) - }, - 'auth.user': { - 'Meta': {'object_name': 'User'}, - 'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}), - 'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), - 'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), - 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}), - 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), - 'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), - 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), - 'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), - 'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}), - 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), - 'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}), - 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), - 'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), - 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}), - 'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), - 'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), - 'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), - 'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), - 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}), - 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), - 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}), - 'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}) - }, - 'contenttypes.contenttype': { - 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, - 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) - }, - 'simplewiki.article': { - 'Meta': {'unique_together': "(('slug', 'namespace'),)", 'object_name': 'Article'}, - 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), - 'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}), - 'current_revision': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'current_rev'", 'unique': 'True', 'null': 'True', 'to': "orm['simplewiki.Revision']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'modified_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}), - 'namespace': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Namespace']"}), - 'permissions': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Permission']", 'null': 'True', 'blank': 'True'}), - 'related': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'related_rel_+'", 'null': 'True', 'to': "orm['simplewiki.Article']"}), - 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '100', 'blank': 'True'}), - 'title': ('django.db.models.fields.CharField', [], {'max_length': '512'}) - }, - 'simplewiki.articleattachment': { - 'Meta': {'object_name': 'ArticleAttachment'}, - 'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}), - 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '255'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), - 'uploaded_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) - }, - 'simplewiki.namespace': { - 'Meta': {'object_name': 'Namespace'}, - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '30'}) - }, - 'simplewiki.permission': { - 'Meta': {'object_name': 'Permission'}, - 'can_read': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'read'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}), - 'can_write': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'write'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'permission_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) - }, - 'simplewiki.revision': { - 'Meta': {'object_name': 'Revision'}, - 'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}), - 'contents': ('django.db.models.fields.TextField', [], {}), - 'contents_parsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), - 'counter': ('django.db.models.fields.IntegerField', [], {'default': '1'}), - 'deleted': ('django.db.models.fields.IntegerField', [], {'default': '0'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'previous_revision': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Revision']", 'null': 'True', 'blank': 'True'}), - 'revision_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), - 'revision_text': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), - 'revision_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'wiki_revision_user'", 'null': 'True', 'to': "orm['auth.User']"}) - } - } - - complete_apps = ['simplewiki'] - symmetrical = True diff --git a/lms/djangoapps/simplewiki/migrations/0005_auto__add_unique_namespace_name.py b/lms/djangoapps/simplewiki/migrations/0005_auto__add_unique_namespace_name.py deleted file mode 100644 index c37fe13544..0000000000 --- a/lms/djangoapps/simplewiki/migrations/0005_auto__add_unique_namespace_name.py +++ /dev/null @@ -1,129 +0,0 @@ -# -*- coding: utf-8 -*- -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - - -class Migration(SchemaMigration): - - def forwards(self, orm): - # Adding unique constraint on 'Namespace', fields ['name'] - db.create_unique('simplewiki_namespace', ['name']) - - def backwards(self, orm): - # Removing unique constraint on 'Namespace', fields ['name'] - db.delete_unique('simplewiki_namespace', ['name']) - - models = { - 'auth.group': { - 'Meta': {'object_name': 'Group'}, - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), - 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) - }, - 'auth.permission': { - 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, - 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) - }, - 'auth.user': { - 'Meta': {'object_name': 'User'}, - 'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}), - 'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), - 'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), - 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}), - 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), - 'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), - 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), - 'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), - 'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}), - 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), - 'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}), - 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), - 'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), - 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}), - 'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), - 'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), - 'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), - 'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), - 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}), - 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), - 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}), - 'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}) - }, - 'contenttypes.contenttype': { - 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, - 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) - }, - 'simplewiki.article': { - 'Meta': {'unique_together': "(('slug', 'namespace'),)", 'object_name': 'Article'}, - 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), - 'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}), - 'current_revision': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'current_rev'", 'unique': 'True', 'null': 'True', 'to': "orm['simplewiki.Revision']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'modified_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}), - 'namespace': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Namespace']"}), - 'permissions': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Permission']", 'null': 'True', 'blank': 'True'}), - 'related': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'related_rel_+'", 'null': 'True', 'to': "orm['simplewiki.Article']"}), - 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '100', 'blank': 'True'}), - 'title': ('django.db.models.fields.CharField', [], {'max_length': '512'}) - }, - 'simplewiki.articleattachment': { - 'Meta': {'object_name': 'ArticleAttachment'}, - 'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}), - 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '255'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), - 'uploaded_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) - }, - 'simplewiki.namespace': { - 'Meta': {'object_name': 'Namespace'}, - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) - }, - 'simplewiki.permission': { - 'Meta': {'object_name': 'Permission'}, - 'can_read': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'read'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}), - 'can_write': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'write'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'permission_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) - }, - 'simplewiki.revision': { - 'Meta': {'object_name': 'Revision'}, - 'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}), - 'contents': ('django.db.models.fields.TextField', [], {}), - 'contents_parsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), - 'counter': ('django.db.models.fields.IntegerField', [], {'default': '1'}), - 'deleted': ('django.db.models.fields.IntegerField', [], {'default': '0'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'previous_revision': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Revision']", 'null': 'True', 'blank': 'True'}), - 'revision_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), - 'revision_text': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), - 'revision_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'wiki_revision_user'", 'null': 'True', 'to': "orm['auth.User']"}) - } - } - - complete_apps = ['simplewiki'] diff --git a/lms/djangoapps/simplewiki/migrations/0006_auto.py b/lms/djangoapps/simplewiki/migrations/0006_auto.py deleted file mode 100644 index b5b18c39c0..0000000000 --- a/lms/djangoapps/simplewiki/migrations/0006_auto.py +++ /dev/null @@ -1,129 +0,0 @@ -# -*- coding: utf-8 -*- -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - - -class Migration(SchemaMigration): - - def forwards(self, orm): - # Adding index on 'Namespace', fields ['name'] - db.create_index('simplewiki_namespace', ['name']) - - def backwards(self, orm): - # Removing index on 'Namespace', fields ['name'] - db.delete_index('simplewiki_namespace', ['name']) - - models = { - 'auth.group': { - 'Meta': {'object_name': 'Group'}, - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), - 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) - }, - 'auth.permission': { - 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, - 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) - }, - 'auth.user': { - 'Meta': {'object_name': 'User'}, - 'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}), - 'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), - 'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), - 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}), - 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), - 'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), - 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), - 'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), - 'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}), - 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), - 'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}), - 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), - 'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), - 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}), - 'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), - 'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), - 'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), - 'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), - 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}), - 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), - 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}), - 'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}) - }, - 'contenttypes.contenttype': { - 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, - 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) - }, - 'simplewiki.article': { - 'Meta': {'unique_together': "(('slug', 'namespace'),)", 'object_name': 'Article'}, - 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), - 'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}), - 'current_revision': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'current_rev'", 'unique': 'True', 'null': 'True', 'to': "orm['simplewiki.Revision']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'modified_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}), - 'namespace': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Namespace']"}), - 'permissions': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Permission']", 'null': 'True', 'blank': 'True'}), - 'related': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'related_rel_+'", 'null': 'True', 'to': "orm['simplewiki.Article']"}), - 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '100', 'blank': 'True'}), - 'title': ('django.db.models.fields.CharField', [], {'max_length': '512'}) - }, - 'simplewiki.articleattachment': { - 'Meta': {'object_name': 'ArticleAttachment'}, - 'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}), - 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '255'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), - 'uploaded_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) - }, - 'simplewiki.namespace': { - 'Meta': {'object_name': 'Namespace'}, - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30', 'db_index': 'True'}) - }, - 'simplewiki.permission': { - 'Meta': {'object_name': 'Permission'}, - 'can_read': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'read'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}), - 'can_write': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'write'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'permission_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) - }, - 'simplewiki.revision': { - 'Meta': {'object_name': 'Revision'}, - 'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}), - 'contents': ('django.db.models.fields.TextField', [], {}), - 'contents_parsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), - 'counter': ('django.db.models.fields.IntegerField', [], {'default': '1'}), - 'deleted': ('django.db.models.fields.IntegerField', [], {'default': '0'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'previous_revision': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Revision']", 'null': 'True', 'blank': 'True'}), - 'revision_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), - 'revision_text': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), - 'revision_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'wiki_revision_user'", 'null': 'True', 'to': "orm['auth.User']"}) - } - } - - complete_apps = ['simplewiki'] diff --git a/lms/djangoapps/simplewiki/migrations/0007_auto.py b/lms/djangoapps/simplewiki/migrations/0007_auto.py deleted file mode 100644 index 6e3071e4d3..0000000000 --- a/lms/djangoapps/simplewiki/migrations/0007_auto.py +++ /dev/null @@ -1,129 +0,0 @@ -# -*- coding: utf-8 -*- -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - - -class Migration(SchemaMigration): - - def forwards(self, orm): - # Removing index on 'Namespace', fields ['name'] - db.delete_index('simplewiki_namespace', ['name']) - - def backwards(self, orm): - # Adding index on 'Namespace', fields ['name'] - db.create_index('simplewiki_namespace', ['name']) - - models = { - 'auth.group': { - 'Meta': {'object_name': 'Group'}, - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), - 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) - }, - 'auth.permission': { - 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, - 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) - }, - 'auth.user': { - 'Meta': {'object_name': 'User'}, - 'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}), - 'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), - 'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), - 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}), - 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), - 'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), - 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), - 'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), - 'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}), - 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), - 'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}), - 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), - 'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), - 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}), - 'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), - 'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), - 'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), - 'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), - 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}), - 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), - 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}), - 'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}) - }, - 'contenttypes.contenttype': { - 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, - 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) - }, - 'simplewiki.article': { - 'Meta': {'unique_together': "(('slug', 'namespace'),)", 'object_name': 'Article'}, - 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), - 'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}), - 'current_revision': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'current_rev'", 'unique': 'True', 'null': 'True', 'to': "orm['simplewiki.Revision']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'modified_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': '1', 'blank': 'True'}), - 'namespace': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Namespace']"}), - 'permissions': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Permission']", 'null': 'True', 'blank': 'True'}), - 'related': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'related_rel_+'", 'null': 'True', 'to': "orm['simplewiki.Article']"}), - 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '100', 'blank': 'True'}), - 'title': ('django.db.models.fields.CharField', [], {'max_length': '512'}) - }, - 'simplewiki.articleattachment': { - 'Meta': {'object_name': 'ArticleAttachment'}, - 'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}), - 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '255'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), - 'uploaded_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) - }, - 'simplewiki.namespace': { - 'Meta': {'object_name': 'Namespace'}, - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) - }, - 'simplewiki.permission': { - 'Meta': {'object_name': 'Permission'}, - 'can_read': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'read'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}), - 'can_write': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'write'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'permission_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) - }, - 'simplewiki.revision': { - 'Meta': {'object_name': 'Revision'}, - 'article': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Article']"}), - 'contents': ('django.db.models.fields.TextField', [], {}), - 'contents_parsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), - 'counter': ('django.db.models.fields.IntegerField', [], {'default': '1'}), - 'deleted': ('django.db.models.fields.IntegerField', [], {'default': '0'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'previous_revision': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['simplewiki.Revision']", 'null': 'True', 'blank': 'True'}), - 'revision_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), - 'revision_text': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), - 'revision_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'wiki_revision_user'", 'null': 'True', 'to': "orm['auth.User']"}) - } - } - - complete_apps = ['simplewiki'] diff --git a/lms/djangoapps/simplewiki/migrations/__init__.py b/lms/djangoapps/simplewiki/migrations/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/lms/djangoapps/simplewiki/models.py b/lms/djangoapps/simplewiki/models.py deleted file mode 100644 index 4026f40b87..0000000000 --- a/lms/djangoapps/simplewiki/models.py +++ /dev/null @@ -1,387 +0,0 @@ -import difflib -import os - -from django import forms -from django.contrib.auth.models import User -from django.core.urlresolvers import reverse -from django.db import models -from django.db.models import signals -from django.utils.translation import ugettext_lazy as _ -from markdown import markdown - -from .wiki_settings import * -from util.cache import cache -from pytz import UTC - - -class ShouldHaveExactlyOneRootSlug(Exception): - pass - - -class Namespace(models.Model): - name = models.CharField(max_length=30, unique=True, verbose_name=_('namespace')) - # TODO: We may want to add permissions, etc later - - @classmethod - def ensure_namespace(cls, name): - try: - namespace = Namespace.objects.get(name__exact=name) - except Namespace.DoesNotExist: - new_namespace = Namespace(name=name) - new_namespace.save() - - -class Article(models.Model): - """Wiki article referring to Revision model for actual content. - 'slug' and 'title' field should be maintained centrally, since users - aren't allowed to change them, anyways. - """ - - title = models.CharField(max_length=512, verbose_name=_('Article title'), - blank=False) - slug = models.SlugField(max_length=100, verbose_name=_('slug'), - help_text=_('Letters, numbers, underscore and hyphen.'), - blank=True) - namespace = models.ForeignKey(Namespace, verbose_name=_('Namespace')) - created_by = models.ForeignKey(User, verbose_name=_('Created by'), blank=True, null=True) - created_on = models.DateTimeField(auto_now_add=1) - modified_on = models.DateTimeField(auto_now_add=1) - locked = models.BooleanField(default=False, verbose_name=_('Locked for editing')) - permissions = models.ForeignKey('Permission', verbose_name=_('Permissions'), - blank=True, null=True, - help_text=_('Permission group')) - current_revision = models.OneToOneField('Revision', related_name='current_rev', - blank=True, null=True, editable=True) - related = models.ManyToManyField('self', verbose_name=_('Related articles'), symmetrical=True, - help_text=_('Sets a symmetrical relation other articles'), - blank=True, null=True) - - def attachments(self): - return ArticleAttachment.objects.filter(article__exact=self) - - def get_path(self): - return self.namespace.name + "/" + self.slug - - @classmethod - def get_article(cls, article_path): - """ - Given an article_path like namespace/slug, this returns the article. It may raise - a Article.DoesNotExist if no matching article is found or ValueError if the - article_path is not constructed properly. - """ - #TODO: Verify the path, throw a meaningful error? - namespace, slug = article_path.split("/") - return Article.objects.get(slug__exact=slug, namespace__name__exact=namespace) - - @classmethod - def get_root(cls, namespace): - """Return the root article, which should ALWAYS exist.. - except the very first time the wiki is loaded, in which - case the user is prompted to create this article.""" - try: - return Article.objects.filter(slug__exact="", namespace__name__exact=namespace)[0] - except: - raise ShouldHaveExactlyOneRootSlug() - - # @classmethod - # def get_url_reverse(cls, path, article, return_list=[]): - # """Lookup a URL and return the corresponding set of articles - # in the path.""" - # if path == []: - # return return_list + [article] - # # Lookup next child in path - # try: - # a = Article.objects.get(parent__exact = article, slug__exact=str(path[0])) - # return cls.get_url_reverse(path[1:], a, return_list+[article]) - # except Exception, e: - # return None - - def can_read(self, user): - """ Check read permissions and return True/False.""" - if user.is_superuser: - return True - if self.permissions: - perms = self.permissions.can_read.all() - return perms.count() == 0 or (user in perms) - else: - # TODO: We can inherit namespace permissions here - return True - - def can_write(self, user): - """ Check write permissions and return True/False.""" - if user.is_superuser: - return True - if self.permissions: - perms = self.permissions.can_write.all() - return perms.count() == 0 or (user in perms) - else: - # TODO: We can inherit namespace permissions here - return True - - def can_write_l(self, user): - """Check write permissions and locked status""" - if user.is_superuser: - return True - return not self.locked and self.can_write(user) - - def can_attach(self, user): - return self.can_write_l(user) and (WIKI_ALLOW_ANON_ATTACHMENTS or not user.is_anonymous()) - - def __unicode__(self): - if self.slug == '': - return unicode(_('Root article')) - else: - return self.slug - - class Meta: - unique_together = (('slug', 'namespace'),) - verbose_name = _('Article') - verbose_name_plural = _('Articles') - - -def get_attachment_filepath(instance, filename): - """Store file, appending new extension for added security""" - dir_ = WIKI_ATTACHMENTS + instance.article.get_url() - dir_ = '/'.join(filter(lambda x: x != '', dir_.split('/'))) - if not os.path.exists(WIKI_ATTACHMENTS_ROOT + dir_): - os.makedirs(WIKI_ATTACHMENTS_ROOT + dir_) - return dir_ + '/' + filename + '.upload' - - -class ArticleAttachment(models.Model): - article = models.ForeignKey(Article, verbose_name=_('Article')) - file = models.FileField(max_length=255, upload_to=get_attachment_filepath, verbose_name=_('Attachment')) - uploaded_by = models.ForeignKey(User, blank=True, verbose_name=_('Uploaded by'), null=True) - uploaded_on = models.DateTimeField(auto_now_add=True, verbose_name=_('Upload date')) - - def download_url(self): - return reverse('wiki_view_attachment', args=(self.article.get_url(), self.filename())) - - def filename(self): - return '.'.join(self.file.name.split('/')[-1].split('.')[:-1]) - - def get_size(self): - try: - size = self.file.size - except OSError: - size = 0 - return size - - def filename(self): - return '.'.join(self.file.name.split('/')[-1].split('.')[:-1]) - - def is_image(self): - fname = self.filename().split('.') - if len(fname) > 1 and fname[-1].lower() in WIKI_IMAGE_EXTENSIONS: - return True - return False - - def get_thumb(self): - return self.get_thumb_impl(*WIKI_IMAGE_THUMB_SIZE) - - def get_thumb_small(self): - return self.get_thumb_impl(*WIKI_IMAGE_THUMB_SIZE_SMALL) - - def mk_thumbs(self): - self.mk_thumb(*WIKI_IMAGE_THUMB_SIZE, **{'force': True}) - self.mk_thumb(*WIKI_IMAGE_THUMB_SIZE_SMALL, **{'force': True}) - - def mk_thumb(self, width, height, force=False): - """Requires Python Imaging Library (PIL)""" - if not self.get_size(): - return False - - if not self.is_image(): - return False - - base_path = os.path.dirname(self.file.path) - orig_name = self.filename().split('.') - thumb_filename = "%s__thumb__%d_%d.%s" % ('.'.join(orig_name[:-1]), width, height, orig_name[-1]) - thumb_filepath = "%s%s%s" % (base_path, os.sep, thumb_filename) - - if force or not os.path.exists(thumb_filepath): - try: - import Image - img = Image.open(self.file.path) - img.thumbnail((width, height), Image.ANTIALIAS) - img.save(thumb_filepath) - except IOError: - return False - - return True - - def get_thumb_impl(self, width, height): - """Requires Python Imaging Library (PIL)""" - - if not self.get_size(): - return False - - if not self.is_image(): - return False - - self.mk_thumb(width, height) - - orig_name = self.filename().split('.') - thumb_filename = "%s__thumb__%d_%d.%s" % ('.'.join(orig_name[:-1]), width, height, orig_name[-1]) - thumb_url = settings.MEDIA_URL + WIKI_ATTACHMENTS + self.article.get_url() + '/' + thumb_filename - - return thumb_url - - def __unicode__(self): - return self.filename() - - -class Revision(models.Model): - - article = models.ForeignKey(Article, verbose_name=_('Article')) - revision_text = models.CharField(max_length=255, blank=True, null=True, - verbose_name=_('Description of change')) - revision_user = models.ForeignKey(User, verbose_name=_('Modified by'), - blank=True, null=True, related_name='wiki_revision_user') - revision_date = models.DateTimeField(auto_now_add=True, verbose_name=_('Revision date')) - contents = models.TextField(verbose_name=_('Contents (Use MarkDown format)')) - contents_parsed = models.TextField(editable=False, blank=True, null=True) - counter = models.IntegerField(verbose_name=_('Revision#'), default=1, editable=False) - previous_revision = models.ForeignKey('self', blank=True, null=True, editable=False) - - # Deleted has three values. 0 is normal, non-deleted. 1 is if it was deleted by a normal user. It should - # be a NEW revision, so that it appears in the history. 2 is a special flag that can be applied or removed - # from a normal revision. It means it has been admin-deleted, and can only been seen by an admin. It doesn't - # show up in the history. - deleted = models.IntegerField(verbose_name=_('Deleted group'), default=0) - - def get_user(self): - return self.revision_user if self.revision_user else _('Anonymous') - - # Called after the deleted fied has been changed (between 0 and 2). This bypasses the normal checks put in - # save that update the revision or reject the save if contents haven't changed - def adminSetDeleted(self, deleted): - self.deleted = deleted - super(Revision, self).save() - - def save(self, **kwargs): - # Check if contents have changed... if not, silently ignore save - if self.article and self.article.current_revision: - if self.deleted == 0 and self.article.current_revision.contents == self.contents: - return - else: - import datetime - self.article.modified_on = datetime.datetime.now(UTC) - self.article.save() - - # Increment counter according to previous revision - previous_revision = Revision.objects.filter(article=self.article).order_by('-counter') - if previous_revision.count() > 0: - if previous_revision.count() > previous_revision[0].counter: - self.counter = previous_revision.count() + 1 - else: - self.counter = previous_revision[0].counter + 1 - else: - self.counter = 1 - if (self.article.current_revision and self.article.current_revision.deleted == 0): - self.previous_revision = self.article.current_revision - - # Create pre-parsed contents - no need to parse on-the-fly - ext = WIKI_MARKDOWN_EXTENSIONS - ext += ["wikipath(default_namespace=%s)" % self.article.namespace.name] - self.contents_parsed = markdown(self.contents, - extensions=ext, - safe_mode='escape',) - super(Revision, self).save(**kwargs) - - def delete(self, **kwargs): - """If a current revision is deleted, then regress to the previous - revision or insert a stub, if no other revisions are available""" - article = self.article - if article.current_revision == self: - prev_revision = Revision.objects.filter(article__exact=article, - pk__not=self.pk).order_by('-counter') - if prev_revision: - article.current_revision = prev_revision[0] - article.save() - else: - r = Revision(article=article, - revision_user=article.created_by) - r.contents = unicode(_('Auto-generated stub')) - r.revision_text = unicode(_('Auto-generated stub')) - r.save() - article.current_revision = r - article.save() - super(Revision, self).delete(**kwargs) - - def get_diff(self): - if (self.deleted == 1): - yield "Article Deletion" - return - - if self.previous_revision: - previous = self.previous_revision.contents.splitlines(1) - else: - previous = [] - - # Todo: difflib.HtmlDiff would look pretty for our history pages! - diff = difflib.unified_diff(previous, self.contents.splitlines(1)) - # let's skip the preamble - diff.next(); diff.next(); diff.next() - - for d in diff: - yield d - - def __unicode__(self): - return "r%d" % self.counter - - class Meta: - verbose_name = _('article revision') - verbose_name_plural = _('article revisions') - - -class Permission(models.Model): - permission_name = models.CharField(max_length=255, verbose_name=_('Permission name')) - can_write = models.ManyToManyField(User, blank=True, null=True, related_name='write', - help_text=_('Select none to grant anonymous access.')) - can_read = models.ManyToManyField(User, blank=True, null=True, related_name='read', - help_text=_('Select none to grant anonymous access.')) - - def __unicode__(self): - return self.permission_name - - class Meta: - verbose_name = _('Article permission') - verbose_name_plural = _('Article permissions') - - -class RevisionForm(forms.ModelForm): - contents = forms.CharField(label=_('Contents'), widget=forms.Textarea(attrs={'rows': 8, 'cols': 50})) - - class Meta: - model = Revision - fields = ['contents', 'revision_text'] - - -class RevisionFormWithTitle(forms.ModelForm): - title = forms.CharField(label=_('Title')) - - class Meta: - model = Revision - fields = ['title', 'contents', 'revision_text'] - - -class CreateArticleForm(RevisionForm): - title = forms.CharField(label=_('Title')) - - class Meta: - model = Revision - fields = ['title', 'contents', ] - - -def set_revision(sender, *args, **kwargs): - """Signal handler to ensure that a new revision is always chosen as the - current revision - automatically. It simplifies stuff greatly. Also - stores previous revision for diff-purposes""" - instance = kwargs['instance'] - created = kwargs['created'] - if created and instance.article: - instance.article.current_revision = instance - instance.article.save() - -signals.post_save.connect(set_revision, Revision) diff --git a/lms/djangoapps/simplewiki/templatetags/__init__.py b/lms/djangoapps/simplewiki/templatetags/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/lms/djangoapps/simplewiki/templatetags/simplewiki_utils.py b/lms/djangoapps/simplewiki/templatetags/simplewiki_utils.py deleted file mode 100644 index 6325aeb2bd..0000000000 --- a/lms/djangoapps/simplewiki/templatetags/simplewiki_utils.py +++ /dev/null @@ -1,20 +0,0 @@ -from django import template -from django.conf import settings -from django.template.defaultfilters import stringfilter -from django.utils.http import urlquote as django_urlquote - -from simplewiki.wiki_settings import * - -register = template.Library() - - -@register.filter() -def prepend_media_url(value): - """Prepend user defined media root to url""" - return settings.MEDIA_URL + value - - -@register.filter() -def urlquote(value): - """Prepend user defined media root to url""" - return django_urlquote(value) diff --git a/lms/djangoapps/simplewiki/tests.py b/lms/djangoapps/simplewiki/tests.py deleted file mode 100644 index 6b60485805..0000000000 --- a/lms/djangoapps/simplewiki/tests.py +++ /dev/null @@ -1,23 +0,0 @@ -""" -This file demonstrates two different styles of tests (one doctest and one -unittest). These will both pass when you run "manage.py test". - -Replace these 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.failUnlessEqual(1 + 1, 2) - -__test__ = {"doctest": """ -Another way to test that 1 + 1 is equal to 2. - ->>> 1 + 1 == 2 -True -"""} diff --git a/lms/djangoapps/simplewiki/urls.py b/lms/djangoapps/simplewiki/urls.py deleted file mode 100644 index 629b753654..0000000000 --- a/lms/djangoapps/simplewiki/urls.py +++ /dev/null @@ -1,19 +0,0 @@ -from django.conf.urls import patterns, url - -namespace_regex = r"[a-zA-Z\d._-]+" -article_slug = r'/(?P<article_path>' + namespace_regex + r'/[a-zA-Z\d_-]*)' -namespace = r'/(?P<namespace>' + namespace_regex + r')' - -urlpatterns = patterns('', # nopep8 - url(r'^$', 'simplewiki.views.root_redirect', name='wiki_root'), - url(r'^view' + article_slug, 'simplewiki.views.view', name='wiki_view'), - url(r'^view_revision/(?P<revision_number>[0-9]+)' + article_slug, 'simplewiki.views.view_revision', name='wiki_view_revision'), - url(r'^edit' + article_slug, 'simplewiki.views.edit', name='wiki_edit'), - url(r'^create' + article_slug, 'simplewiki.views.create', name='wiki_create'), - url(r'^history' + article_slug + r'(?:/(?P<page>[0-9]+))?$', 'simplewiki.views.history', name='wiki_history'), - url(r'^search_related' + article_slug, 'simplewiki.views.search_add_related', name='search_related'), - url(r'^random/?$', 'simplewiki.views.random_article', name='wiki_random'), - url(r'^revision_feed' + namespace + r'/(?P<page>[0-9]+)?$', 'simplewiki.views.revision_feed', name='wiki_revision_feed'), - url(r'^search' + namespace + r'?$', 'simplewiki.views.search_articles', name='wiki_search_articles'), - url(r'^list' + namespace + r'?$', 'simplewiki.views.search_articles', name='wiki_list_articles'), # Just an alias for the search, but you usually don't submit a search term -) diff --git a/lms/djangoapps/simplewiki/usage.txt b/lms/djangoapps/simplewiki/usage.txt deleted file mode 100644 index 4a74ffaf8e..0000000000 --- a/lms/djangoapps/simplewiki/usage.txt +++ /dev/null @@ -1,800 +0,0 @@ -# Markdown: Syntax - -[TOC] - -## Overview - -### Philosophy - -Markdown is intended to be as easy-to-read and easy-to-write as is feasible. - -Readability, however, is emphasized above all else. A Markdown-formatted -document should be publishable as-is, as plain text, without looking -like it's been marked up with tags or formatting instructions. While -Markdown's syntax has been influenced by several existing text-to-HTML -filters -- including [Setext] [1], [atx] [2], [Textile] [3], [reStructuredText] [4], -[Grutatext] [5], and [EtText] [6] -- the single biggest source of -inspiration for Markdown's syntax is the format of plain text email. - - [1]: http://docutils.sourceforge.net/mirror/setext.html - [2]: http://www.aaronsw.com/2002/atx/ - [3]: http://textism.com/tools/textile/ - [4]: http://docutils.sourceforge.net/rst.html - [5]: http://www.triptico.com/software/grutatxt.html - [6]: http://ettext.taint.org/doc/ - -To this end, Markdown's syntax is comprised entirely of punctuation -characters, which punctuation characters have been carefully chosen so -as to look like what they mean. E.g., asterisks around a word actually -look like \*emphasis\*. Markdown lists look like, well, lists. Even -blockquotes look like quoted passages of text, assuming you've ever -used email. - -### Automatic Escaping for Special Characters - -In HTML, there are two characters that demand special treatment: `<` -and `&`. Left angle brackets are used to start tags; ampersands are -used to denote HTML entities. If you want to use them as literal -characters, you must escape them as entities, e.g. `<`, and -`&`. - -Ampersands in particular are bedeviling for web writers. If you want to -write about 'AT&T', you need to write '`AT&T`'. You even need to -escape ampersands within URLs. Thus, if you want to link to: - - http://images.google.com/images?num=30&q=larry+bird - -you need to encode the URL as: - - http://images.google.com/images?num=30&q=larry+bird - -in your anchor tag `href` attribute. Needless to say, this is easy to -forget, and is probably the single most common source of HTML validation -errors in otherwise well-marked-up web sites. - -Markdown allows you to use these characters naturally, taking care of -all the necessary escaping for you. If you use an ampersand as part of -an HTML entity, it remains unchanged; otherwise it will be translated -into `&`. - -So, if you want to include a copyright symbol in your article, you can write: - - © - -and Markdown will leave it alone. But if you write: - - AT&T - -Markdown will translate it to: - - AT&T - -Similarly, because Markdown supports [inline HTML](#html), if you use -angle brackets as delimiters for HTML tags, Markdown will treat them as -such. But if you write: - - 4 < 5 - -Markdown will translate it to: - - 4 < 5 - -However, inside Markdown code spans and blocks, angle brackets and -ampersands are *always* encoded automatically. This makes it easy to use -Markdown to write about HTML code. (As opposed to raw HTML, which is a -terrible format for writing about HTML syntax, because every single `<` -and `&` in your example code needs to be escaped.) - - -* * * - - -## Block Elements - -### Paragraphs and Line Breaks - -A paragraph is simply one or more consecutive lines of text, separated -by one or more blank lines. (A blank line is any line that looks like a -blank line -- a line containing nothing but spaces or tabs is considered -blank.) Normal paragraphs should not be indented with spaces or tabs. - -The implication of the "one or more consecutive lines of text" rule is -that Markdown supports "hard-wrapped" text paragraphs. This differs -significantly from most other text-to-HTML formatters (including Movable -Type's "Convert Line Breaks" option) which translate every line break -character in a paragraph into a `<br />` tag. - -When you *do* want to insert a `<br />` break tag using Markdown, you -end a line with two or more spaces, then type return. - -Yes, this takes a tad more effort to create a `<br />`, but a simplistic -"every line break is a `<br />`" rule wouldn't work for Markdown. -Markdown's email-style [blockquoting][bq] and multi-paragraph [list items][l] -work best -- and look better -- when you format them with hard breaks. - - [bq]: #blockquote - [l]: #list - -### Headers - -Markdown supports two styles of headers, [Setext] [1] and [atx] [2]. - -Setext-style headers are "underlined" using equal signs (for first-level -headers) and dashes (for second-level headers). For example: - - This is an H1 - ============= - - This is an H2 - ------------- - - This is an H3 - _____________ - -Any number of underlining `=`'s or `-`'s will work. - -Atx-style headers use 1-6 hash characters at the start of the line, -corresponding to header levels 1-6. For example: - - # This is an H1 - - ## This is an H2 - - ###### This is an H6 - -Optionally, you may "close" atx-style headers. This is purely -cosmetic -- you can use this if you think it looks better. The -closing hashes don't even need to match the number of hashes -used to open the header. (The number of opening hashes -determines the header level.) : - - # This is an H1 # - - ## This is an H2 ## - - ### This is an H3 ###### - - -### Blockquotes - -Markdown uses email-style `>` characters for blockquoting. If you're -familiar with quoting passages of text in an email message, then you -know how to create a blockquote in Markdown. It looks best if you hard -wrap the text and put a `>` before every line: - - > This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet, - > consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus. - > Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus. - > - > Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse - > id sem consectetuer libero luctus adipiscing. - -Markdown allows you to be lazy and only put the `>` before the first -line of a hard-wrapped paragraph: - - > This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet, - consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus. - Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus. - - > Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse - id sem consectetuer libero luctus adipiscing. - -Blockquotes can be nested (i.e. a blockquote-in-a-blockquote) by -adding additional levels of `>`: - - > This is the first level of quoting. - > - > > This is nested blockquote. - > - > Back to the first level. - -Blockquotes can contain other Markdown elements, including headers, lists, -and code blocks: - - > ## This is a header. - > - > 1. This is the first list item. - > 2. This is the second list item. - > - > Here's some example code: - > - > return shell_exec("echo $input | $markdown_script"); - -Any decent text editor should make email-style quoting easy. For -example, with BBEdit, you can make a selection and choose Increase -Quote Level from the Text menu. - - -### Lists - -Markdown supports ordered (numbered) and unordered (bulleted) lists. - -Unordered lists use asterisks, pluses, and hyphens -- interchangably --- as list markers: - - * Red - * Green - * Blue - -is equivalent to: - - + Red - + Green - + Blue - -and: - - - Red - - Green - - Blue - -Ordered lists use numbers followed by periods: - - 1. Bird - 2. McHale - 3. Parish - -It's important to note that the actual numbers you use to mark the -list have no effect on the HTML output Markdown produces. The HTML -Markdown produces from the above list is: - - <ol> - <li>Bird</li> - <li>McHale</li> - <li>Parish</li> - </ol> - -If you instead wrote the list in Markdown like this: - - 1. Bird - 1. McHale - 1. Parish - -or even: - - 3. Bird - 1. McHale - 8. Parish - -you'd get the exact same HTML output. The point is, if you want to, -you can use ordinal numbers in your ordered Markdown lists, so that -the numbers in your source match the numbers in your published HTML. -But if you want to be lazy, you don't have to. - -If you do use lazy list numbering, however, you should still start the -list with the number 1. At some point in the future, Markdown may support -starting ordered lists at an arbitrary number. - -List markers typically start at the left margin, but may be indented by -up to three spaces. List markers must be followed by one or more spaces -or a tab. - -To make lists look nice, you can wrap items with hanging indents: - - * Lorem ipsum dolor sit amet, consectetuer adipiscing elit. - Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi, - viverra nec, fringilla in, laoreet vitae, risus. - * Donec sit amet nisl. Aliquam semper ipsum sit amet velit. - Suspendisse id sem consectetuer libero luctus adipiscing. - -But if you want to be lazy, you don't have to: - - * Lorem ipsum dolor sit amet, consectetuer adipiscing elit. - Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi, - viverra nec, fringilla in, laoreet vitae, risus. - * Donec sit amet nisl. Aliquam semper ipsum sit amet velit. - Suspendisse id sem consectetuer libero luctus adipiscing. - -If list items are separated by blank lines, Markdown will wrap the -items in `<p>` tags in the HTML output. For example, this input: - - * Bird - * Magic - -will turn into: - - <ul> - <li>Bird</li> - <li>Magic</li> - </ul> - -But this: - - * Bird - - * Magic - -will turn into: - - <ul> - <li><p>Bird</p></li> - <li><p>Magic</p></li> - </ul> - -List items may consist of multiple paragraphs. Each subsequent -paragraph in a list item must be indented by either 4 spaces -or one tab: - - 1. This is a list item with two paragraphs. Lorem ipsum dolor - sit amet, consectetuer adipiscing elit. Aliquam hendrerit - mi posuere lectus. - - Vestibulum enim wisi, viverra nec, fringilla in, laoreet - vitae, risus. Donec sit amet nisl. Aliquam semper ipsum - sit amet velit. - - 2. Suspendisse id sem consectetuer libero luctus adipiscing. - -It looks nice if you indent every line of the subsequent -paragraphs, but here again, Markdown will allow you to be -lazy: - - * This is a list item with two paragraphs. - - This is the second paragraph in the list item. You're - only required to indent the first line. Lorem ipsum dolor - sit amet, consectetuer adipiscing elit. - - * Another item in the same list. - -To put a blockquote within a list item, the blockquote's `>` -delimiters need to be indented: - - * A list item with a blockquote: - - > This is a blockquote - > inside a list item. - -To put a code block within a list item, the code block needs -to be indented *twice* -- 8 spaces or two tabs: - - * A list item with a code block: - - <code goes here> - - -It's worth noting that it's possible to trigger an ordered list by -accident, by writing something like this: - - 1986. What a great season. - -In other words, a *number-period-space* sequence at the beginning of a -line. To avoid this, you can backslash-escape the period: - - 1986\. What a great season. - - - -### Code Blocks - -Pre-formatted code blocks are used for writing about programming or -markup source code. Rather than forming normal paragraphs, the lines -of a code block are interpreted literally. Markdown wraps a code block -in both `<pre>` and `<code>` tags. - -To produce a code block in Markdown, simply indent every line of the -block by at least 4 spaces or 1 tab. For example, given this input: - - This is a normal paragraph: - - This is a code block. - -Markdown will generate: - - <p>This is a normal paragraph:</p> - - <pre><code>This is a code block. - </code></pre> - -One level of indentation -- 4 spaces or 1 tab -- is removed from each -line of the code block. For example, this: - - Here is an example of AppleScript: - - tell application "Foo" - beep - end tell - -will turn into: - - <p>Here is an example of AppleScript:</p> - - <pre><code>tell application "Foo" - beep - end tell - </code></pre> - -A code block continues until it reaches a line that is not indented -(or the end of the article). - -Within a code block, ampersands (`&`) and angle brackets (`<` and `>`) -are automatically converted into HTML entities. This makes it very -easy to include example HTML source code using Markdown -- just paste -it and indent it, and Markdown will handle the hassle of encoding the -ampersands and angle brackets. For example, this: - - <div class="footer"> - © 2004 Foo Corporation - </div> - -will turn into: - - <pre><code><div class="footer"> - &copy; 2004 Foo Corporation - </div> - </code></pre> - -Regular Markdown syntax is not processed within code blocks. E.g., -asterisks are just literal asterisks within a code block. This means -it's also easy to use Markdown to write about Markdown's own syntax. - - - -### Horizontal Rules - -You can produce a horizontal rule tag (`<hr />`) by placing three or -more hyphens, asterisks, or underscores on a line by themselves. If you -wish, you may use spaces between the hyphens or asterisks. Each of the -following lines will produce a horizontal rule: - - * * * - - *** - - ***** - - - - - - - --------------------------------------- - - -## Span Elements - -### Links - -Markdown supports two style of links: *inline* and *reference*. - -In both styles, the link text is delimited by [square brackets]. - -To create an inline link, use a set of regular parentheses immediately -after the link text's closing square bracket. Inside the parentheses, -put the URL where you want the link to point, along with an *optional* -title for the link, surrounded in quotes. For example: - - This is [an example](http://example.com/ "Title") inline link. - - [This link](http://example.net/) has no title attribute. - -Will produce: - - <p>This is <a href="http://example.com/" title="Title"> - an example</a> inline link.</p> - - <p><a href="http://example.net/">This link</a> has no - title attribute.</p> - -If you're referring to a local resource on the same server, you can -use relative paths: - - See my [About](/about/) page for details. - -Reference-style links use a second set of square brackets, inside -which you place a label of your choosing to identify the link: - - This is [an example][id] reference-style link. - -You can optionally use a space to separate the sets of brackets: - - This is [an example] [id] reference-style link. - -Then, anywhere in the document, you define your link label like this, -on a line by itself: - - [id]: http://example.com/ "Optional Title Here" - -That is: - -* Square brackets containing the link identifier (optionally - indented from the left margin using up to three spaces); -* followed by a colon; -* followed by one or more spaces (or tabs); -* followed by the URL for the link; -* optionally followed by a title attribute for the link, enclosed - in double or single quotes, or enclosed in parentheses. - -The following three link definitions are equivalent: - - [foo]: http://example.com/ "Optional Title Here" - [foo]: http://example.com/ 'Optional Title Here' - [foo]: http://example.com/ (Optional Title Here) - -**Note:** There is a known bug in Markdown.pl 1.0.1 which prevents -single quotes from being used to delimit link titles. - -The link URL may, optionally, be surrounded by angle brackets: - - [id]: <http://example.com/> "Optional Title Here" - -You can put the title attribute on the next line and use extra spaces -or tabs for padding, which tends to look better with longer URLs: - - [id]: http://example.com/longish/path/to/resource/here - "Optional Title Here" - -Link definitions are only used for creating links during Markdown -processing, and are stripped from your document in the HTML output. - -Link definition names may consist of letters, numbers, spaces, and -punctuation -- but they are *not* case sensitive. E.g. these two -links: - - [link text][a] - [link text][A] - -are equivalent. - -The *implicit link name* shortcut allows you to omit the name of the -link, in which case the link text itself is used as the name. -Just use an empty set of square brackets -- e.g., to link the word -"Google" to the google.com web site, you could simply write: - - [Google][] - -And then define the link: - - [Google]: http://google.com/ - -Because link names may contain spaces, this shortcut even works for -multiple words in the link text: - - Visit [Daring Fireball][] for more information. - -And then define the link: - - [Daring Fireball]: http://daringfireball.net/ - -Link definitions can be placed anywhere in your Markdown document. I -tend to put them immediately after each paragraph in which they're -used, but if you want, you can put them all at the end of your -document, sort of like footnotes. - -Here's an example of reference links in action: - - I get 10 times more traffic from [Google] [1] than from - [Yahoo] [2] or [MSN] [3]. - - [1]: http://google.com/ "Google" - [2]: http://search.yahoo.com/ "Yahoo Search" - [3]: http://search.msn.com/ "MSN Search" - -Using the implicit link name shortcut, you could instead write: - - I get 10 times more traffic from [Google][] than from - [Yahoo][] or [MSN][]. - - [google]: http://google.com/ "Google" - [yahoo]: http://search.yahoo.com/ "Yahoo Search" - [msn]: http://search.msn.com/ "MSN Search" - -Both of the above examples will produce the following HTML output: - - <p>I get 10 times more traffic from <a href="http://google.com/" - title="Google">Google</a> than from - <a href="http://search.yahoo.com/" title="Yahoo Search">Yahoo</a> - or <a href="http://search.msn.com/" title="MSN Search">MSN</a>.</p> - -For comparison, here is the same paragraph written using -Markdown's inline link style: - - I get 10 times more traffic from [Google](http://google.com/ "Google") - than from [Yahoo](http://search.yahoo.com/ "Yahoo Search") or - [MSN](http://search.msn.com/ "MSN Search"). - -The point of reference-style links is not that they're easier to -write. The point is that with reference-style links, your document -source is vastly more readable. Compare the above examples: using -reference-style links, the paragraph itself is only 81 characters -long; with inline-style links, it's 176 characters; and as raw HTML, -it's 234 characters. In the raw HTML, there's more markup than there -is text. - -With Markdown's reference-style links, a source document much more -closely resembles the final output, as rendered in a browser. By -allowing you to move the markup-related metadata out of the paragraph, -you can add links without interrupting the narrative flow of your -prose. - -### Emphasis - -Markdown treats asterisks (`*`) and underscores (`_`) as indicators of -emphasis. Text wrapped with one `*` or `_` will be wrapped with an -HTML `<em>` tag; double `*`'s or `_`'s will be wrapped with an HTML -`<strong>` tag. E.g., this input: - - *single asterisks* - - _single underscores_ - - **double asterisks** - - __double underscores__ - -will produce: - - <em>single asterisks</em> - - <em>single underscores</em> - - <strong>double asterisks</strong> - - <strong>double underscores</strong> - -You can use whichever style you prefer; the lone restriction is that -the same character must be used to open and close an emphasis span. - -Emphasis can be used in the middle of a word: - - un*frigging*believable - -But if you surround an `*` or `_` with spaces, it'll be treated as a -literal asterisk or underscore. - -To produce a literal asterisk or underscore at a position where it -would otherwise be used as an emphasis delimiter, you can backslash -escape it: - - \*this text is surrounded by literal asterisks\* - - -### Code - -To indicate a span of code, wrap it with backtick quotes (`` ` ``). -Unlike a pre-formatted code block, a code span indicates code within a -normal paragraph. For example: - - Use the `printf()` function. - -will produce: - - <p>Use the <code>printf()</code> function.</p> - -To include a literal backtick character within a code span, you can use -multiple backticks as the opening and closing delimiters: - - ``There is a literal backtick (`) here.`` - -which will produce this: - - <p><code>There is a literal backtick (`) here.</code></p> - -The backtick delimiters surrounding a code span may include spaces -- -one after the opening, one before the closing. This allows you to place -literal backtick characters at the beginning or end of a code span: - - A single backtick in a code span: `` ` `` - - A backtick-delimited string in a code span: `` `foo` `` - -will produce: - - <p>A single backtick in a code span: <code>`</code></p> - - <p>A backtick-delimited string in a code span: <code>`foo`</code></p> - -With a code span, ampersands and angle brackets are encoded as HTML -entities automatically, which makes it easy to include example HTML -tags. Markdown will turn this: - - Please don't use any `<blink>` tags. - -into: - - <p>Please don't use any <code><blink></code> tags.</p> - -You can write this: - - `—` is the decimal-encoded equivalent of `—`. - -to produce: - - <p><code>&#8212;</code> is the decimal-encoded - equivalent of <code>&mdash;</code>.</p> - - -### Images - -Admittedly, it's fairly difficult to devise a "natural" syntax for -placing images into a plain text document format. - -Markdown uses an image syntax that is intended to resemble the syntax -for links, allowing for two styles: *inline* and *reference*. - -Inline image syntax looks like this: - - ![Alt text](/path/to/img.jpg) - - ![Alt text](/path/to/img.jpg "Optional title") - -That is: - -* An exclamation mark: `!`; -* followed by a set of square brackets, containing the `alt` - attribute text for the image; -* followed by a set of parentheses, containing the URL or path to - the image, and an optional `title` attribute enclosed in double - or single quotes. - -Reference-style image syntax looks like this: - - ![Alt text][id] - -Where "id" is the name of a defined image reference. Image references -are defined using syntax identical to link references: - - [id]: url/to/image "Optional title attribute" - -As of this writing, Markdown has no syntax for specifying the -dimensions of an image; if this is important to you, you can simply -use regular HTML `<img>` tags. - - -## Miscellaneous - -### Automatic Links - -Markdown supports a shortcut style for creating "automatic" links for URLs and email addresses: simply surround the URL or email address with angle brackets. What this means is that if you want to show the actual text of a URL or email address, and also have it be a clickable link, you can do this: - - <http://example.com/> - -Markdown will turn this into: - - <a href="http://example.com/">http://example.com/</a> - -Automatic links for email addresses work similarly, except that -Markdown will also perform a bit of randomized decimal and hex -entity-encoding to help obscure your address from address-harvesting -spambots. For example, Markdown will turn this: - - <address@example.com> - -into something like this: - - <a href="mailto:addre - ss@example.co - m">address@exa - mple.com</a> - -which will render in a browser as a clickable link to "address@example.com". - -(This sort of entity-encoding trick will indeed fool many, if not -most, address-harvesting bots, but it definitely won't fool all of -them. It's better than nothing, but an address published in this way -will probably eventually start receiving spam.) - - - -### Backslash Escapes - -Markdown allows you to use backslash escapes to generate literal -characters which would otherwise have special meaning in Markdown's -formatting syntax. For example, if you wanted to surround a word -with literal asterisks (instead of an HTML `<em>` tag), you can use -backslashes before the asterisks, like this: - - \*literal asterisks\* - -Markdown provides backslash escapes for the following characters: - - \ backslash - ` backtick - * asterisk - _ underscore - {} curly braces - [] square brackets - () parentheses - # hash mark - + plus sign - - minus sign (hyphen) - . dot - ! exclamation mark - diff --git a/lms/djangoapps/simplewiki/views.py b/lms/djangoapps/simplewiki/views.py deleted file mode 100644 index a84fac6e7d..0000000000 --- a/lms/djangoapps/simplewiki/views.py +++ /dev/null @@ -1,552 +0,0 @@ -# -*- coding: utf-8 -*- -from django.conf import settings as settings -from django.contrib.auth.decorators import login_required -from django.core.context_processors import csrf -from django.core.urlresolvers import reverse -from django.db.models import Q -from django.http import HttpResponse, HttpResponseRedirect, Http404 -from django.utils import simplejson -from django.utils.translation import ugettext_lazy as _ -from mitxmako.shortcuts import render_to_response - -from courseware.courses import get_opt_course_with_access -from courseware.access import has_access -from xmodule.course_module import CourseDescriptor -from xmodule.modulestore.django import modulestore - -from .models import Revision, Article, Namespace, CreateArticleForm, RevisionFormWithTitle, RevisionForm -import wiki_settings - - -def wiki_reverse(wiki_page, article=None, course=None, namespace=None, args=[], kwargs={}): - kwargs = dict(kwargs) # TODO: Figure out why if I don't do this kwargs sometimes contains {'article_path'} - if not 'course_id' in kwargs and course: - kwargs['course_id'] = course.id - if not 'article_path' in kwargs and article: - kwargs['article_path'] = article.get_path() - if not 'namespace' in kwargs and namespace: - kwargs['namespace'] = namespace - return reverse(wiki_page, kwargs=kwargs, args=args) - - -def update_template_dictionary(dictionary, request=None, course=None, article=None, revision=None): - if article: - dictionary['wiki_article'] = article - dictionary['wiki_title'] = article.title # TODO: What is the title when viewing the article in a course? - if not course and 'namespace' not in dictionary: - dictionary['namespace'] = article.namespace.name - - if course: - dictionary['course'] = course - if 'namespace' not in dictionary: - dictionary['namespace'] = "edX" - else: - dictionary['course'] = None - - if revision: - dictionary['wiki_article_revision'] = revision - dictionary['wiki_current_revision_deleted'] = not (revision.deleted == 0) - - if request: - dictionary.update(csrf(request)) - - if request and course: - dictionary['staff_access'] = has_access(request.user, course, 'staff') - else: - dictionary['staff_access'] = False - - -def view(request, article_path, course_id=None): - course = get_opt_course_with_access(request.user, course_id, 'load') - - (article, err) = get_article(request, article_path, course) - if err: - return err - - perm_err = check_permissions(request, article, course, check_read=True, check_deleted=True) - if perm_err: - return perm_err - - d = {} - update_template_dictionary(d, request, course, article, article.current_revision) - return render_to_response('simplewiki/simplewiki_view.html', d) - - -def view_revision(request, revision_number, article_path, course_id=None): - course = get_opt_course_with_access(request.user, course_id, 'load') - - (article, err) = get_article(request, article_path, course) - if err: - return err - - try: - revision = Revision.objects.get(counter=int(revision_number), article=article) - except: - d = {'wiki_err_norevision': revision_number} - update_template_dictionary(d, request, course, article) - return render_to_response('simplewiki/simplewiki_error.html', d) - - perm_err = check_permissions(request, article, course, check_read=True, check_deleted=True, revision=revision) - if perm_err: - return perm_err - - d = {} - update_template_dictionary(d, request, course, article, revision) - - return render_to_response('simplewiki/simplewiki_view.html', d) - - -def root_redirect(request, course_id=None): - course = get_opt_course_with_access(request.user, course_id, 'load') - - #TODO: Add a default namespace to settings. - namespace = "edX" - - try: - root = Article.get_root(namespace) - return HttpResponseRedirect(reverse('wiki_view', kwargs={'course_id': course_id, 'article_path': root.get_path()})) - except: - # If the root is not found, we probably are loading this class for the first time - # We should make sure the namespace exists so the root article can be created. - Namespace.ensure_namespace(namespace) - - err = not_found(request, namespace + '/', course) - return err - - -def create(request, article_path, course_id=None): - course = get_opt_course_with_access(request.user, course_id, 'load') - - article_path_components = article_path.split('/') - - # Ensure the namespace exists - if not len(article_path_components) >= 1 or len(article_path_components[0]) == 0: - d = {'wiki_err_no_namespace': True} - update_template_dictionary(d, request, course) - return render_to_response('simplewiki/simplewiki_error.html', d) - - namespace = None - try: - namespace = Namespace.objects.get(name__exact=article_path_components[0]) - except Namespace.DoesNotExist, ValueError: - d = {'wiki_err_bad_namespace': True} - update_template_dictionary(d, request, course) - return render_to_response('simplewiki/simplewiki_error.html', d) - - # See if the article already exists - article_slug = article_path_components[1] if len(article_path_components) >= 2 else '' - #TODO: Make sure the slug only contains legal characters (which is already done a bit by the url regex) - - try: - existing_article = Article.objects.get(namespace=namespace, slug__exact=article_slug) - #It already exists, so we just redirect to view the article - return HttpResponseRedirect(wiki_reverse("wiki_view", existing_article, course)) - except Article.DoesNotExist: - #This is good. The article doesn't exist - pass - - #TODO: Once we have permissions for namespaces, we should check for create permissions - #check_permissions(request, #namespace#, check_locked=False, check_write=True, check_deleted=True) - - if request.method == 'POST': - f = CreateArticleForm(request.POST) - if f.is_valid(): - article = Article() - article.slug = article_slug - if not request.user.is_anonymous(): - article.created_by = request.user - article.title = f.cleaned_data.get('title') - article.namespace = namespace - a = article.save() - new_revision = f.save(commit=False) - if not request.user.is_anonymous(): - new_revision.revision_user = request.user - new_revision.article = article - new_revision.save() - - return HttpResponseRedirect(wiki_reverse("wiki_view", article, course)) - else: - f = CreateArticleForm(initial={'title': request.GET.get('wiki_article_name', article_slug), - 'contents': _('Headline\n===\n\n')}) - - d = {'wiki_form': f, 'create_article': True, 'namespace': namespace.name} - update_template_dictionary(d, request, course) - - return render_to_response('simplewiki/simplewiki_edit.html', d) - - -def edit(request, article_path, course_id=None): - course = get_opt_course_with_access(request.user, course_id, 'load') - - (article, err) = get_article(request, article_path, course) - if err: - return err - - # Check write permissions - perm_err = check_permissions(request, article, course, check_write=True, check_locked=True, check_deleted=False) - if perm_err: - return perm_err - - if wiki_settings.WIKI_ALLOW_TITLE_EDIT: - EditForm = RevisionFormWithTitle - else: - EditForm = RevisionForm - - if request.method == 'POST': - f = EditForm(request.POST) - if f.is_valid(): - new_revision = f.save(commit=False) - new_revision.article = article - - if request.POST.__contains__('delete'): - if (article.current_revision.deleted == 1): # This article has already been deleted. Redirect - return HttpResponseRedirect(wiki_reverse('wiki_view', article, course)) - new_revision.contents = "" - new_revision.deleted = 1 - elif not new_revision.get_diff(): - return HttpResponseRedirect(wiki_reverse('wiki_view', article, course)) - - if not request.user.is_anonymous(): - new_revision.revision_user = request.user - new_revision.save() - if wiki_settings.WIKI_ALLOW_TITLE_EDIT: - new_revision.article.title = f.cleaned_data['title'] - new_revision.article.save() - return HttpResponseRedirect(wiki_reverse('wiki_view', article, course)) - else: - startContents = article.current_revision.contents if (article.current_revision.deleted == 0) else 'Headline\n===\n\n' - - f = EditForm({'contents': startContents, 'title': article.title}) - - d = {'wiki_form': f} - update_template_dictionary(d, request, course, article) - return render_to_response('simplewiki/simplewiki_edit.html', d) - - -def history(request, article_path, page=1, course_id=None): - course = get_opt_course_with_access(request.user, course_id, 'load') - - (article, err) = get_article(request, article_path, course) - if err: - return err - - perm_err = check_permissions(request, article, course, check_read=True, check_deleted=False) - if perm_err: - return perm_err - - page_size = 10 - - if page is None: - page = 1 - try: - p = int(page) - except ValueError: - p = 1 - - history = Revision.objects.filter(article__exact=article).order_by('-counter').select_related('previous_revision__counter', 'revision_user', 'wiki_article') - - if request.method == 'POST': - if request.POST.__contains__('revision'): # They selected a version, but they can be either deleting or changing the version - perm_err = check_permissions(request, article, course, check_write=True, check_locked=True) - if perm_err: - return perm_err - - redirectURL = wiki_reverse('wiki_view', article, course) - try: - r = int(request.POST['revision']) - revision = Revision.objects.get(id=r) - if request.POST.__contains__('change'): - article.current_revision = revision - article.save() - elif request.POST.__contains__('view'): - redirectURL = wiki_reverse('wiki_view_revision', course=course, - kwargs={'revision_number': revision.counter, 'article_path': article.get_path()}) - #The rese of these are admin functions - elif request.POST.__contains__('delete') and request.user.is_superuser: - if (revision.deleted == 0): - revision.adminSetDeleted(2) - elif request.POST.__contains__('restore') and request.user.is_superuser: - if (revision.deleted == 2): - revision.adminSetDeleted(0) - elif request.POST.__contains__('delete_all') and request.user.is_superuser: - Revision.objects.filter(article__exact=article, deleted=0).update(deleted=2) - elif request.POST.__contains__('lock_article'): - article.locked = not article.locked - article.save() - except Exception as e: - print str(e) - pass - finally: - return HttpResponseRedirect(redirectURL) - # - # - # <input type="submit" name="delete" value="Delete revision"/> - # <input type="submit" name="restore" value="Restore revision"/> - # <input type="submit" name="delete_all" value="Delete all revisions"> - # %else: - # <input type="submit" name="delete_article" value="Delete all revisions"> - # - - page_count = (history.count() + (page_size - 1)) / page_size - if p > page_count: - p = 1 - beginItem = (p - 1) * page_size - - next_page = p + 1 if page_count > p else None - prev_page = p - 1 if p > 1 else None - - d = {'wiki_page': p, - 'wiki_next_page': next_page, - 'wiki_prev_page': prev_page, - 'wiki_history': history[beginItem:beginItem + page_size], - 'show_delete_revision': request.user.is_superuser} - update_template_dictionary(d, request, course, article) - - return render_to_response('simplewiki/simplewiki_history.html', d) - - -def revision_feed(request, page=1, namespace=None, course_id=None): - course = get_opt_course_with_access(request.user, course_id, 'load') - - page_size = 10 - - if page is None: - page = 1 - try: - p = int(page) - except ValueError: - p = 1 - - history = Revision.objects.order_by('-revision_date').select_related('revision_user', 'article', 'previous_revision') - - page_count = (history.count() + (page_size - 1)) / page_size - if p > page_count: - p = 1 - beginItem = (p - 1) * page_size - - next_page = p + 1 if page_count > p else None - prev_page = p - 1 if p > 1 else None - - d = {'wiki_page': p, - 'wiki_next_page': next_page, - 'wiki_prev_page': prev_page, - 'wiki_history': history[beginItem:beginItem + page_size], - 'show_delete_revision': request.user.is_superuser, - 'namespace': namespace} - update_template_dictionary(d, request, course) - - return render_to_response('simplewiki/simplewiki_revision_feed.html', d) - - -def search_articles(request, namespace=None, course_id=None): - course = get_opt_course_with_access(request.user, course_id, 'load') - - # blampe: We should check for the presence of other popular django search - # apps and use those if possible. Only fall back on this as a last resort. - # Adding some context to results (eg where matches were) would also be nice. - - # todo: maybe do some perm checking here - - if request.method == 'GET': - querystring = request.GET.get('value', '').strip() - else: - querystring = "" - - results = Article.objects.all() - if namespace: - results = results.filter(namespace__name__exact=namespace) - - if request.user.is_superuser: - results = results.order_by('current_revision__deleted') - else: - results = results.filter(current_revision__deleted=0) - - if querystring: - for queryword in querystring.split(): - # Basic negation is as fancy as we get right now - if queryword[0] == '-' and len(queryword) > 1: - results._search = lambda x: results.exclude(x) - queryword = queryword[1:] - else: - results._search = lambda x: results.filter(x) - - results = results._search(Q(current_revision__contents__icontains=queryword) | \ - Q(title__icontains=queryword)) - - results = results.select_related('current_revision__deleted', 'namespace') - - results = sorted(results, key=lambda article: (article.current_revision.deleted, article.get_path().lower())) - - if len(results) == 1 and querystring: - return HttpResponseRedirect(wiki_reverse('wiki_view', article=results[0], course=course)) - else: - d = {'wiki_search_results': results, - 'wiki_search_query': querystring, - 'namespace': namespace} - update_template_dictionary(d, request, course) - return render_to_response('simplewiki/simplewiki_searchresults.html', d) - - -def search_add_related(request, course_id, slug, namespace): - course = get_opt_course_with_access(request.user, course_id, 'load') - - (article, err) = get_article(request, slug, namespace if namespace else course_id) - if err: - return err - - perm_err = check_permissions(request, article, course, check_read=True) - if perm_err: - return perm_err - - search_string = request.GET.get('query', None) - self_pk = request.GET.get('self', None) - if search_string: - results = [] - related = Article.objects.filter(title__istartswith=search_string) - others = article.related.all() - if self_pk: - related = related.exclude(pk=self_pk) - if others: - related = related.exclude(related__in=others) - related = related.order_by('title')[:10] - for item in related: - results.append({'id': str(item.id), - 'value': item.title, - 'info': item.get_url()}) - else: - results = [] - - json = simplejson.dumps({'results': results}) - return HttpResponse(json, mimetype='application/json') - - -def add_related(request, course_id, slug, namespace): - course = get_opt_course_with_access(request.user, course_id, 'load') - - (article, err) = get_article(request, slug, namespace if namespace else course_id) - if err: - return err - - perm_err = check_permissions(request, article, course, check_write=True, check_locked=True) - if perm_err: - return perm_err - - try: - related_id = request.POST['id'] - rel = Article.objects.get(id=related_id) - has_already = article.related.filter(id=related_id).count() - if has_already == 0 and not rel == article: - article.related.add(rel) - article.save() - except: - pass - finally: - return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),))) - - -def remove_related(request, course_id, namespace, slug, related_id): - course = get_opt_course_with_access(request.user, course_id, 'load') - - (article, err) = get_article(request, slug, namespace if namespace else course_id) - - if err: - return err - - perm_err = check_permissions(request, article, course, check_write=True, check_locked=True) - if perm_err: - return perm_err - - try: - rel_id = int(related_id) - rel = Article.objects.get(id=rel_id) - article.related.remove(rel) - article.save() - except: - pass - finally: - return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),))) - - -def random_article(request, course_id=None): - course = get_opt_course_with_access(request.user, course_id, 'load') - - from random import randint - num_arts = Article.objects.count() - article = Article.objects.all()[randint(0, num_arts - 1)] - return HttpResponseRedirect(wiki_reverse('wiki_view', article, course)) - - -def not_found(request, article_path, course): - """Generate a NOT FOUND message for some URL""" - d = {'wiki_err_notfound': True, - 'article_path': article_path, - 'namespace': "edX"} - update_template_dictionary(d, request, course) - return render_to_response('simplewiki/simplewiki_error.html', d) - - -def get_article(request, article_path, course): - err = None - article = None - - try: - article = Article.get_article(article_path) - except Article.DoesNotExist, ValueError: - err = not_found(request, article_path, course) - - return (article, err) - - -def check_permissions(request, article, course, check_read=False, check_write=False, check_locked=False, check_deleted=False, revision=None): - read_err = check_read and not article.can_read(request.user) - - write_err = check_write and not article.can_write(request.user) - - locked_err = check_locked and article.locked - - if revision is None: - revision = article.current_revision - deleted_err = check_deleted and not (revision.deleted == 0) - if (request.user.is_superuser): - deleted_err = False - locked_err = False - - if read_err or write_err or locked_err or deleted_err: - d = {'wiki_article': article, - 'wiki_err_noread': read_err, - 'wiki_err_nowrite': write_err, - 'wiki_err_locked': locked_err, - 'wiki_err_deleted': deleted_err, } - update_template_dictionary(d, request, course) - # TODO: Make this a little less jarring by just displaying an error - # on the current page? (no such redirect happens for an anon upload yet) - # benjaoming: I think this is the nicest way of displaying an error, but - # these errors shouldn't occur, but rather be prevented on the other pages. - return render_to_response('simplewiki/simplewiki_error.html', d) - else: - return None - -#################### -# LOGIN PROTECTION # -#################### - - -if wiki_settings.WIKI_REQUIRE_LOGIN_VIEW: - view = login_required(view) - history = login_required(history) - search_articles = login_required(search_articles) - root_redirect = login_required(root_redirect) - revision_feed = login_required(revision_feed) - random_article = login_required(random_article) - search_add_related = login_required(search_add_related) - not_found = login_required(not_found) - view_revision = login_required(view_revision) - -if wiki_settings.WIKI_REQUIRE_LOGIN_EDIT: - create = login_required(create) - edit = login_required(edit) - add_related = login_required(add_related) - remove_related = login_required(remove_related) - -if wiki_settings.WIKI_CONTEXT_PREPROCESSORS: - settings.TEMPLATE_CONTEXT_PROCESSORS += wiki_settings.WIKI_CONTEXT_PREPROCESSORS diff --git a/lms/djangoapps/simplewiki/wiki_settings.py b/lms/djangoapps/simplewiki/wiki_settings.py deleted file mode 100644 index 6054ab1909..0000000000 --- a/lms/djangoapps/simplewiki/wiki_settings.py +++ /dev/null @@ -1,111 +0,0 @@ -from django.utils.translation import ugettext_lazy as _ -from django.conf import settings - -# Default settings.. overwrite in your own settings.py - -# Planned feature. -WIKI_USE_MARKUP_WIDGET = True - -#################### -# LOGIN PROTECTION # -#################### -# Before setting the below parameters, please note that permissions can -# be set in the django permission system on individual articles and their -# child articles. In this way you can add a user group and give them -# special permissions, be it on the root article or some other. Permissions -# are inherited on lower levels. - -# Adds standard django login protection for viewing -WIKI_REQUIRE_LOGIN_VIEW = getattr(settings, 'SIMPLE_WIKI_REQUIRE_LOGIN_VIEW', - True) - -# Adds standard django login protection for editing -WIKI_REQUIRE_LOGIN_EDIT = getattr(settings, 'SIMPLE_WIKI_REQUIRE_LOGIN_EDIT', - True) - -#################### -# ATTACHMENTS # -#################### - -# This should be a directory that's writable for the web server. -# It's relative to the MEDIA_ROOT. -WIKI_ATTACHMENTS = getattr(settings, 'SIMPLE_WIKI_ATTACHMENTS', - 'simplewiki/attachments/') - -# If false, attachments will completely disappear -WIKI_ALLOW_ATTACHMENTS = getattr(settings, 'SIMPLE_WIKI_ALLOW_ATTACHMENTS', - False) - -# If WIKI_REQUIRE_LOGIN_EDIT is False, then attachments can still be disallowed -WIKI_ALLOW_ANON_ATTACHMENTS = getattr(settings, 'SIMPLE_WIKI_ALLOW_ANON_ATTACHMENTS', False) - -# Attachments are automatically stored with a dummy extension and delivered -# back to the user with their original extension. -# This setting does not add server security, but might add user security -# if set -- or force users to use standard formats, which might also -# be a good idea. -# Example: ('pdf', 'doc', 'gif', 'jpeg', 'jpg', 'png') -WIKI_ATTACHMENTS_ALLOWED_EXTENSIONS = getattr(settings, 'SIMPLE_WIKI_ATTACHMENTS_ALLOWED_EXTENSIONS', - None) - -# At the moment this variable should not be modified, because -# it breaks compatibility with the normal Django FileField and uploading -# from the admin interface. -WIKI_ATTACHMENTS_ROOT = settings.MEDIA_ROOT - -# Bytes! Default: 1 MB. -WIKI_ATTACHMENTS_MAX = getattr(settings, 'SIMPLE_WIKI_ATTACHMENTS_MAX', - 1 * 1024 * 1024) - -# Allow users to edit titles of pages -# (warning! titles are not maintained in the revision system.) -WIKI_ALLOW_TITLE_EDIT = getattr(settings, 'SIMPLE_WIKI_ALLOW_TITLE_EDIT', False) - -# Global context processors -# These are appended to TEMPLATE_CONTEXT_PROCESSORS in your Django settings -# whenever the wiki is in use. It can be used as a simple, but effective -# way of extending simplewiki without touching original code (and thus keeping -# everything easily maintainable) -WIKI_CONTEXT_PREPROCESSORS = getattr(settings, 'SIMPLE_WIKI_CONTEXT_PREPROCESSORS', - ()) - -#################### -# AESTHETICS # -#################### - -# List of extensions to be used by Markdown. Custom extensions (i.e., with file -# names of mdx_*.py) can be dropped into the simplewiki (or project) directory -# and then added to this list to be utilized. Wiki is enabled automatically. -# -# For more information, see -# http://www.freewisdom.org/projects/python-markdown/Available_Extensions -WIKI_MARKDOWN_EXTENSIONS = getattr(settings, 'SIMPLE_WIKI_MARKDOWN_EXTENSIONS', - ['footnotes', - 'tables', - 'headerid', - 'fenced_code', - 'def_list', - #'codehilite', #This was throwing errors - 'abbr', - 'toc', - 'mathjax', - 'video', # In-line embedding for YouTube, etc. - 'circuit', - ]) - - -WIKI_IMAGE_EXTENSIONS = getattr(settings, - 'SIMPLE_WIKI_IMAGE_EXTENSIONS', - ('jpg', 'jpeg', 'gif', 'png', 'tiff', 'bmp')) -# Planned features -WIKI_PAGE_WIDTH = getattr(settings, - 'SIMPLE_WIKI_PAGE_WIDTH', "100%") - -WIKI_PAGE_ALIGN = getattr(settings, - 'SIMPLE_WIKI_PAGE_ALIGN', "center") - -WIKI_IMAGE_THUMB_SIZE = getattr(settings, - 'SIMPLE_WIKI_IMAGE_THUMB_SIZE', (200, 150)) - -WIKI_IMAGE_THUMB_SIZE_SMALL = getattr(settings, - 'SIMPLE_WIKI_IMAGE_THUMB_SIZE_SMALL', (100, 100)) diff --git a/lms/envs/common.py b/lms/envs/common.py index 076528e91e..6c027f39f6 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -689,7 +689,6 @@ INSTALLED_APPS = ( 'student', 'static_template_view', 'staticbook', - 'simplewiki', 'track', 'util', 'certificates', diff --git a/lms/templates/simplewiki/simplewiki_base.html b/lms/templates/simplewiki/simplewiki_base.html deleted file mode 100644 index e19d8d61ca..0000000000 --- a/lms/templates/simplewiki/simplewiki_base.html +++ /dev/null @@ -1,164 +0,0 @@ -##This file is based on the template from the SimpleWiki source which carries the GPL license - -<%inherit file="../main.html"/> -<%namespace name='static' file='../static_content.html'/> - -<%block name="headextra"> - <%static:css group='course'/> -</%block> - -<%! - from django.core.urlresolvers import reverse - from simplewiki.views import wiki_reverse -%> - -<%block name="js_extra"> -<script type="text/javascript" src="${static.url('js/simplewiki-AutoSuggest_c_2.0.js')}"></script> - -## TODO (cpennington): Remove this when we have a good way for modules to specify js to load on the page -## and in the wiki -<script type="text/javascript" src="${static.url('js/schematic.js')}"></script> - - <script type="text/javascript"> - function set_related_article_id(s) { - document.getElementById('wiki_related_input_id').value = s.id; - document.getElementById('wiki_related_input_submit').disabled=false; - } - %if wiki_article is not UNDEFINED: - var x = window.onload; - window.onload = function(){ - var options = { - script: "${ wiki_reverse('search_related', wiki_article, course)}/?self=${wiki_article.pk}&", - json: true, - varname: "query", - maxresults: 35, - callback: set_related_article_id, - noresults: "Nothing found!" - }; - var as = new AutoSuggest('wiki_related_input', options); - if (typeof x == 'function') - x(); - } - %endif - </script> - <script type="text/x-mathjax-config"> - MathJax.Hub.Config({ - tex2jax: {inlineMath: [ ['$','$'], ["\\(","\\)"]], - displayMath: [ ['$$','$$'], ["\\[","\\]"]]} - }); - </script> - <script> - $(function(){ - $.ajaxSetup ({ - // Disable caching of AJAX responses - cache: false - }); - - $(".div_wiki_circuit").each(function(d,e) { - id = $(this).attr("id"); - name = id.substring(17); - //alert(name); - $("#"+id).load("/edit_circuit/"+name, function(){update_schematics();}); - f=this; - }); - - $("#wiki_create_form").hide(); - - $("#create-article").click(function() { - $("#wiki_create_form").slideToggle(); - $(this).parent().toggleClass("active"); - }); - - }); - </script> - - <%block name="wiki_head"/> - -</%block> - -<%block name="bodyextra"> - -%if course: -<%include file="/courseware/course_navigation.html" args="active_page='wiki'" /> -%endif - -<section class="container"> - <div class="wiki-wrapper"> - <%block name="wiki_panel"> - <div aria-label="Wiki Navigation" id="wiki_panel"> - <h2>Course Wiki</h2> - <ul class="action"> - <li> - <h3> - <a href="${wiki_reverse("wiki_list_articles", course=course, namespace=namespace)}">All Articles</a> - </h3> - </li> - - <li class="create-article"> - <h3> - <a href="#" id="create-article"/>Create Article</a> - </h3> - - <div id="wiki_create_form"> - <% - baseURL = wiki_reverse("wiki_create", course=course, kwargs={"article_path" : namespace + "/" }) - %> - <form method="GET" onsubmit="this.action='${baseURL}' + this.wiki_article_name.value.replace(/([^a-zA-Z0-9\-])/g, '');"> - <div> - <label for="id_wiki_article_name">Title of article</label> - <input type="text" name="wiki_article_name" id="id_wiki_article_name" /> - </div> - <ul> - <li> - <input type="submit" class="button" value="Create" /> - </li> - </ul> - </form> - </div> - </li> - - <li class="search"> - <form method="GET" action='${wiki_reverse("wiki_search_articles", course=course, namespace=namespace)}'> - <label class="wiki_box_title">Search</label> - <input type="text" placeholder="Search" name="value" id="wiki_search_input" value="${wiki_search_query if wiki_search_query is not UNDEFINED else '' |h}"/> - <input type="submit" id="wiki_search_input_submit" value="Go!" /> - </form> - </li> - </ul> - - </div> - </%block> - - <section class="wiki-body"> - %if wiki_article is not UNDEFINED: - <header> - %if wiki_article.locked: - <p><strong>This article has been locked</strong></p> - %endif - <p>Last modified: ${wiki_article.modified_on.strftime("%b %d, %Y, %I:%M %p")}</p> - %endif - - %if wiki_article is not UNDEFINED: - <ul> - - <li> - <a href="${ wiki_reverse('wiki_view', wiki_article, course)}" class="view">View</a> - </li> - - <li> - <a href="${ wiki_reverse('wiki_edit', wiki_article, course)}" class="edit">Edit</a> - </li> - - <li> - <a href="${ wiki_reverse('wiki_history', wiki_article, course)}" class="history">History</a> - </li> - </ul> - </header> - %endif - - <%block name="wiki_page_title"/> - <%block name="wiki_body"/> - </section> - </div> -</section> -</%block> diff --git a/lms/templates/simplewiki/simplewiki_edit.html b/lms/templates/simplewiki/simplewiki_edit.html deleted file mode 100644 index 0381a21857..0000000000 --- a/lms/templates/simplewiki/simplewiki_edit.html +++ /dev/null @@ -1,76 +0,0 @@ -##This file is based on the template from the SimpleWiki source which carries the GPL license - -<%inherit file="simplewiki_base.html"/> - -<%block name="title"> -<title> -%if create_article: -Wiki – Create Article – MITx 6.002x -%else: -${"Edit " + wiki_title + " - " if wiki_title is not UNDEFINED else ""}MITx 6.002x Wiki -%endif - - -<%block name="wiki_page_title"> -%if create_article: -

    Create article

    -%else: -

    ${ wiki_article.title }

    -%endif - - -<%block name="wiki_head"> - - - - - - - - - - -<%block name="wiki_body"> -
    -
    - -
    - ${wiki_form} - %if create_article: - - %else: - - - %endif - -<%include file="simplewiki_instructions.html"/> - - diff --git a/lms/templates/simplewiki/simplewiki_error.html b/lms/templates/simplewiki/simplewiki_error.html deleted file mode 100644 index 0ce0763def..0000000000 --- a/lms/templates/simplewiki/simplewiki_error.html +++ /dev/null @@ -1,79 +0,0 @@ -##This file is based on the template from the SimpleWiki source which carries the GPL license - -<%inherit file="simplewiki_base.html"/> - -<%! - from simplewiki.views import wiki_reverse -%> - -<%block name="title">Wiki Error – MITx 6.002x - - -<%block name="wiki_page_title"> -

    Oops...

    - - - -<%block name="wiki_body"> -
    -%if wiki_error is not UNDEFINED: -${wiki_error} -%endif - -%if wiki_err_notfound is not UNDEFINED: -

    - The page you requested could not be found. - Click here to create it. -

    -%elif wiki_err_no_namespace is not UNDEFINED and wiki_err_no_namespace: -

    - You must specify a namespace to create an article in. -

    -%elif wiki_err_bad_namespace is not UNDEFINED and wiki_err_bad_namespace: -

    - The namespace for this article does not exist. This article cannot be created. -

    -%elif wiki_err_locked is not UNDEFINED and wiki_err_locked: -

    - The article you are trying to modify is locked. -

    -%elif wiki_err_noread is not UNDEFINED and wiki_err_noread: -

    - You do not have access to read this article. -

    -%elif wiki_err_nowrite is not UNDEFINED and wiki_err_nowrite: -

    - You do not have access to edit this article. -

    -%elif wiki_err_noanon is not UNDEFINED and wiki_err_noanon: -

    - Anonymous attachments are not allowed. Try logging in. -

    -%elif wiki_err_create is not UNDEFINED and wiki_err_create: -

    - You do not have access to create this article. -

    -%elif wiki_err_encode is not UNDEFINED and wiki_err_encode: -

    - The url you requested could not be handled by the wiki. - Probably you used a bad character in the URL. - Only use digits, English letters, underscore and dash. For instance - /wiki/An_Article-1 -

    -%elif wiki_err_deleted is not UNDEFINED and wiki_err_deleted: -

    - The article you tried to access has been deleted. You may be able to restore it to an earlier version in its history, or create a new version. -

    -%elif wiki_err_norevision is not UNDEFINED: -

    - This article does not contain revision ${wiki_err_norevision | h}. -

    -%else: -

    - An error has occured. -

    -%endif - -
    - - diff --git a/lms/templates/simplewiki/simplewiki_history.html b/lms/templates/simplewiki/simplewiki_history.html deleted file mode 100644 index 0fc77eeb0c..0000000000 --- a/lms/templates/simplewiki/simplewiki_history.html +++ /dev/null @@ -1,92 +0,0 @@ -##This file is based on the template from the SimpleWiki source which carries the GPL license - -<%inherit file="simplewiki_base.html"/> - -<%block name="title">${"Revision history of " + wiki_title + " - " if wiki_title is not UNDEFINED else ""}Wiki – MITx 6.002x - -<%! - from django.core.urlresolvers import reverse - from simplewiki.views import wiki_reverse -%> - -<%block name="wiki_page_title"> -

    -${ wiki_article.title } -

    - - -<%block name="wiki_body"> - -
    - -
    - - - - - - - - - - - <% loopCount = 0 %> - %for revision in wiki_history: - %if revision.deleted < 2 or show_delete_revision: - <% loopCount += 1 %> - - - - - - - %endif - %endfor - - %if wiki_prev_page or wiki_next_page: - - - - - - %endif -
    RevisionCommentDiffModified
    - - - - ${ revision.revision_text if revision.revision_text else "None" } - %for x in revision.get_diff(): - ${x|h}
    - %endfor
    ${revision.get_user()} -
    - ${revision.revision_date.strftime("%b %d, %Y, %I:%M %p")} -
    - %if wiki_prev_page: - Previous page - %endif - %if wiki_next_page: - Next page - %endif -
    -
    - - %if show_delete_revision: - - - - - %endif -
    -
    - diff --git a/lms/templates/simplewiki/simplewiki_instructions.html b/lms/templates/simplewiki/simplewiki_instructions.html deleted file mode 100644 index 449b92b004..0000000000 --- a/lms/templates/simplewiki/simplewiki_instructions.html +++ /dev/null @@ -1,24 +0,0 @@ -
    - This wiki uses Markdown for styling. -

    MITx Additions:

    -

    circuit-schematic:

    -

    $LaTeX Math Expression$

    - To create a new wiki article, create a link to it. Clicking the link gives you the creation page. -

    [Article Name](wiki:ArticleName)

    - -

    Useful examples:

    -

    [Link](http://google.com)

    -

    Huge Header -
    ====

    -

    Smaller Header -
    -------

    -

    *emphasis* or _emphasis_

    -

    **strong** or __strong__

    -

    - Unordered List -
      - Sub Item 1 -
      - Sub Item 2

    -

    1. Ordered -
    2. List

    - -

    Need more help? There are several useful guides online.

    -
    diff --git a/lms/templates/simplewiki/simplewiki_revision_feed.html b/lms/templates/simplewiki/simplewiki_revision_feed.html deleted file mode 100644 index 69b69afdff..0000000000 --- a/lms/templates/simplewiki/simplewiki_revision_feed.html +++ /dev/null @@ -1,63 +0,0 @@ -##This file is based on the template from the SimpleWiki source which carries the GPL license - -<%inherit file="simplewiki_base.html"/> - -<%block name="title">Wiki - Revision feed - MITx 6.002x - -<%! - from simplewiki.views import wiki_reverse -%> - -<%block name="wiki_page_title"> -

    Revision Feed - Page ${wiki_page}

    - - -<%block name="wiki_body"> - - - - - - - - - - - <% loopCount = 0 %> - %for revision in wiki_history: - %if revision.deleted < 2 or show_delete_revision: - <% loopCount += 1 %> - - - - - - - %endif - %endfor - - %if wiki_prev_page or wiki_next_page: - - - - - - %endif -
    RevisionCommentDiffModified
    - ${revision.article.title} - ${revision} - - ${ revision.revision_text if revision.revision_text else "None" } - %for x in revision.get_diff(): - ${x|h}
    - %endfor
    ${revision.get_user()} -
    - ${revision.revision_date.strftime("%b %d, %Y, %I:%M %p")} -
    - %if wiki_prev_page: - Previous page - %endif - %if wiki_next_page: - Next page - %endif -
    - diff --git a/lms/templates/simplewiki/simplewiki_searchresults.html b/lms/templates/simplewiki/simplewiki_searchresults.html deleted file mode 100644 index e64a01ae62..0000000000 --- a/lms/templates/simplewiki/simplewiki_searchresults.html +++ /dev/null @@ -1,34 +0,0 @@ -##This file is based on the template from the SimpleWiki source which carries the GPL license - -<%inherit file="simplewiki_base.html"/> - -<%block name="title">Wiki - Search Results - MITx 6.002x - -<%! - from simplewiki.views import wiki_reverse -%> - -<%block name="wiki_page_title"> -

    -%if wiki_search_query: -Search results for ${wiki_search_query | h} -%else: -Displaying all articles -%endif -

    - - -<%block name="wiki_body"> -
    -
      -%for article in wiki_search_results: -<% article_deleted = not article.current_revision.deleted == 0 %> -
    • ${article.title} ${'(Deleted)' if article_deleted else ''}

    • -%endfor - -%if not wiki_search_results: -No articles matching ${wiki_search_query if wiki_search_query is not UNDEFINED else ""} ! -%endif -
    -
    - diff --git a/lms/templates/simplewiki/simplewiki_updateprogressbar.html b/lms/templates/simplewiki/simplewiki_updateprogressbar.html deleted file mode 100644 index a7739d6bf1..0000000000 --- a/lms/templates/simplewiki/simplewiki_updateprogressbar.html +++ /dev/null @@ -1,37 +0,0 @@ -##This file is based on the template from the SimpleWiki source which carries the GPL license -##This file has been converted to Mako, but not tested. It is because uploads are disabled for the wiki. If they are reenabled, this may contain bugs. -<%! - from django.template.defaultfilters import filesizeformat -%> - - -%if started: - -%else: -%if finished: - -%else: -%if overwrite_warning: - -%else: -%if too_big: - -%else: - -%endif -%endif -%endif -%endif diff --git a/lms/templates/simplewiki/simplewiki_view.html b/lms/templates/simplewiki/simplewiki_view.html deleted file mode 100644 index 53f0030eaf..0000000000 --- a/lms/templates/simplewiki/simplewiki_view.html +++ /dev/null @@ -1,15 +0,0 @@ -##This file is based on the template from the SimpleWiki source which carries the GPL license - -<%inherit file="simplewiki_base.html"/> - -<%block name="title">${wiki_title + " - " if wiki_title is not UNDEFINED else ""}Wiki – MITx 6.002x - -<%block name="wiki_page_title"> -

    ${ wiki_article.title } ${'- Deleted Revision!' if wiki_current_revision_deleted else ''}

    - - -<%block name="wiki_body"> -
    - ${ wiki_article_revision.contents_parsed| n} -
    - From 308fe26b65cede710374bd2aa9566fd8b63afd0a Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 20 Jun 2013 09:08:25 -0400 Subject: [PATCH 259/375] Clean up pep8 E128 issues --- lms/djangoapps/foldit/tests.py | 62 ++++++++++++------- lms/djangoapps/foldit/views.py | 20 ++++-- lms/djangoapps/instructor_task/models.py | 16 ++--- lms/djangoapps/lms_migration/migrate.py | 10 +-- .../staff_grading_service.py | 12 ++-- lms/djangoapps/static_template_view/tests.py | 37 +++++------ lms/djangoapps/static_template_view/views.py | 9 +-- lms/lib/comment_client/legacy.py | 6 +- 8 files changed, 102 insertions(+), 70 deletions(-) diff --git a/lms/djangoapps/foldit/tests.py b/lms/djangoapps/foldit/tests.py index 9928f596be..d391dd3650 100644 --- a/lms/djangoapps/foldit/tests.py +++ b/lms/djangoapps/foldit/tests.py @@ -95,13 +95,19 @@ class FolditTestCase(TestCase): response = self.make_puzzle_score_request([1, 2], [0.078034, 0.080000]) self.assertEqual(response.content, json.dumps( - [{"OperationID": "SetPlayerPuzzleScores", - "Value": [{ - "PuzzleID": 1, - "Status": "Success"}, - - {"PuzzleID": 2, - "Status": "Success"}]}])) + [{ + "OperationID": "SetPlayerPuzzleScores", + "Value": [ + { + "PuzzleID": 1, + "Status": "Success" + }, { + "PuzzleID": 2, + "Status": "Success" + } + ] + }] + )) def test_SetPlayerPuzzleScores_multiple(self): @@ -126,9 +132,11 @@ class FolditTestCase(TestCase): self.assertEqual(len(top_10), 1) # Floats always get in the way, so do almostequal - self.assertAlmostEqual(top_10[0]['score'], - Score.display_score(better_score), - delta=0.5) + self.assertAlmostEqual( + top_10[0]['score'], + Score.display_score(better_score), + delta=0.5 + ) # reporting a worse score shouldn't worse_score = 0.065 @@ -137,9 +145,11 @@ class FolditTestCase(TestCase): top_10 = Score.get_tops_n(10, puzzle_id) self.assertEqual(len(top_10), 1) # should still be the better score - self.assertAlmostEqual(top_10[0]['score'], - Score.display_score(better_score), - delta=0.5) + self.assertAlmostEqual( + top_10[0]['score'], + Score.display_score(better_score), + delta=0.5 + ) def test_SetPlayerPuzzleScores_manyplayers(self): """ @@ -150,28 +160,34 @@ class FolditTestCase(TestCase): puzzle_id = ['1'] player1_score = 0.08 player2_score = 0.02 - response1 = self.make_puzzle_score_request(puzzle_id, player1_score, - self.user) + response1 = self.make_puzzle_score_request( + puzzle_id, player1_score, self.user + ) # There should now be a score in the db. top_10 = Score.get_tops_n(10, puzzle_id) self.assertEqual(len(top_10), 1) self.assertEqual(top_10[0]['score'], Score.display_score(player1_score)) - response2 = self.make_puzzle_score_request(puzzle_id, player2_score, - self.user2) + response2 = self.make_puzzle_score_request( + puzzle_id, player2_score, self.user2 + ) # There should now be two scores in the db top_10 = Score.get_tops_n(10, puzzle_id) self.assertEqual(len(top_10), 2) # Top score should be player2_score. Second should be player1_score - self.assertAlmostEqual(top_10[0]['score'], - Score.display_score(player2_score), - delta=0.5) - self.assertAlmostEqual(top_10[1]['score'], - Score.display_score(player1_score), - delta=0.5) + self.assertAlmostEqual( + top_10[0]['score'], + Score.display_score(player2_score), + delta=0.5 + ) + self.assertAlmostEqual( + top_10[1]['score'], + Score.display_score(player1_score), + delta=0.5 + ) # Top score user should be self.user2.username self.assertEqual(top_10[0]['username'], self.user2.username) diff --git a/lms/djangoapps/foldit/views.py b/lms/djangoapps/foldit/views.py index da361a2a82..8d52e09aa1 100644 --- a/lms/djangoapps/foldit/views.py +++ b/lms/djangoapps/foldit/views.py @@ -36,9 +36,13 @@ def foldit_ops(request): "Success": "false", "ErrorString": "Verification failed", "ErrorCode": "VerifyFailed"}) - log.warning("Verification of SetPlayerPuzzleScores failed:" + - "user %s, scores json %r, verify %r", - request.user, puzzle_scores_json, pz_verify_json) + log.warning( + "Verification of SetPlayerPuzzleScores failed:" + "user %s, scores json %r, verify %r", + request.user, + puzzle_scores_json, + pz_verify_json + ) else: # This is needed because we are not getting valid json - the # value of ScoreType is an unquoted string. Right now regexes are @@ -65,9 +69,13 @@ def foldit_ops(request): "Success": "false", "ErrorString": "Verification failed", "ErrorCode": "VerifyFailed"}) - log.warning("Verification of SetPuzzlesComplete failed:" + - " user %s, puzzles json %r, verify %r", - request.user, puzzles_complete_json, pc_verify_json) + log.warning( + "Verification of SetPuzzlesComplete failed:" + " user %s, puzzles json %r, verify %r", + request.user, + puzzles_complete_json, + pc_verify_json + ) else: puzzles_complete = json.loads(puzzles_complete_json) responses.append(save_complete(request.user, puzzles_complete)) diff --git a/lms/djangoapps/instructor_task/models.py b/lms/djangoapps/instructor_task/models.py index f1ebf814fa..f01cc4e3ad 100644 --- a/lms/djangoapps/instructor_task/models.py +++ b/lms/djangoapps/instructor_task/models.py @@ -84,13 +84,15 @@ class InstructorTask(models.Model): raise ValueError(msg) # create the task, then save it: - instructor_task = cls(course_id=course_id, - task_type=task_type, - task_id=task_id, - task_key=task_key, - task_input=json_task_input, - task_state=QUEUING, - requester=requester) + instructor_task = cls( + course_id=course_id, + task_type=task_type, + task_id=task_id, + task_key=task_key, + task_input=json_task_input, + task_state=QUEUING, + requester=requester + ) instructor_task.save_now() return instructor_task diff --git a/lms/djangoapps/lms_migration/migrate.py b/lms/djangoapps/lms_migration/migrate.py index a677383035..83af73a842 100644 --- a/lms/djangoapps/lms_migration/migrate.py +++ b/lms/djangoapps/lms_migration/migrate.py @@ -118,10 +118,12 @@ def manage_modulestores(request, reload_dir=None, commit_id=None): html += '

    Courses loaded in the modulestore

    ' html += '
      ' for cdir, course in def_ms.courses.items(): - html += '
    1. %s (%s)
    2. ' % (settings.MITX_ROOT_URL, - escape(cdir), - escape(cdir), - course.location.url()) + html += '
    3. %s (%s)
    4. ' % ( + settings.MITX_ROOT_URL, + escape(cdir), + escape(cdir), + course.location.url() + ) html += '
    ' #---------------------------------------- diff --git a/lms/djangoapps/open_ended_grading/staff_grading_service.py b/lms/djangoapps/open_ended_grading/staff_grading_service.py index 2c611b4481..6b2b4707bb 100644 --- a/lms/djangoapps/open_ended_grading/staff_grading_service.py +++ b/lms/djangoapps/open_ended_grading/staff_grading_service.py @@ -270,8 +270,10 @@ def get_problem_list(request, course_id): mimetype="application/json") except GradingServiceError: #This is a dev_facing_error - log.exception("Error from staff grading service in open ended grading. server url: {0}" - .format(staff_grading_service().url)) + log.exception( + "Error from staff grading service in open " + "ended grading. server url: {0}".format(staff_grading_service().url) + ) #This is a staff_facing_error return HttpResponse(json.dumps({'success': False, 'error': STAFF_ERROR_MESSAGE})) @@ -285,8 +287,10 @@ def _get_next(course_id, grader_id, location): return staff_grading_service().get_next(course_id, location, grader_id) except GradingServiceError: #This is a dev facing error - log.exception("Error from staff grading service in open ended grading. server url: {0}" - .format(staff_grading_service().url)) + log.exception( + "Error from staff grading service in open " + "ended grading. server url: {0}".format(staff_grading_service().url) + ) #This is a staff_facing_error return json.dumps({'success': False, 'error': STAFF_ERROR_MESSAGE}) diff --git a/lms/djangoapps/static_template_view/tests.py b/lms/djangoapps/static_template_view/tests.py index 9cd5502d5d..813a94e294 100644 --- a/lms/djangoapps/static_template_view/tests.py +++ b/lms/djangoapps/static_template_view/tests.py @@ -21,23 +21,24 @@ class SimpleTest(TestCase): """ # since I had to remap files, pedantically test all press releases # published to date. Decent positive test while we're at it. - all_releases = ["/press/mit-and-harvard-announce-edx", - "/press/uc-berkeley-joins-edx", - "/press/edX-announces-proctored-exam-testing", - "/press/elsevier-collaborates-with-edx", - "/press/ut-joins-edx", - "/press/cengage-to-provide-book-content", - "/press/gates-foundation-announcement", - "/press/wellesley-college-joins-edx", - "/press/georgetown-joins-edx", - "/press/spring-courses", - "/press/lewin-course-announcement", - "/press/bostonx-announcement", - "/press/eric-lander-secret-of-life", - "/press/edx-expands-internationally", - "/press/xblock_announcement", - "/press/stanford-to-work-with-edx", - ] + all_releases = [ + "/press/mit-and-harvard-announce-edx", + "/press/uc-berkeley-joins-edx", + "/press/edX-announces-proctored-exam-testing", + "/press/elsevier-collaborates-with-edx", + "/press/ut-joins-edx", + "/press/cengage-to-provide-book-content", + "/press/gates-foundation-announcement", + "/press/wellesley-college-joins-edx", + "/press/georgetown-joins-edx", + "/press/spring-courses", + "/press/lewin-course-announcement", + "/press/bostonx-announcement", + "/press/eric-lander-secret-of-life", + "/press/edx-expands-internationally", + "/press/xblock_announcement", + "/press/stanford-to-work-with-edx", + ] for rel in all_releases: response = self.client.get(rel) @@ -55,7 +56,7 @@ class SimpleTest(TestCase): response = self.client.get("/press/../homework.html") self.assertEqual(response.status_code, 404) - # "." in is ascii 2E + # "." in is ascii 2E response = self.client.get("/press/%2E%2E/homework.html") self.assertEqual(response.status_code, 404) diff --git a/lms/djangoapps/static_template_view/views.py b/lms/djangoapps/static_template_view/views.py index 56a7f32780..e5a8c43ca8 100644 --- a/lms/djangoapps/static_template_view/views.py +++ b/lms/djangoapps/static_template_view/views.py @@ -15,10 +15,11 @@ from util.cache import cache_if_anonymous valid_templates = [] if settings.STATIC_GRAB: - valid_templates = valid_templates + ['server-down.html', - 'server-error.html' - 'server-overloaded.html', - ] + valid_templates = valid_templates + [ + 'server-down.html', + 'server-error.html' + 'server-overloaded.html', + ] def index(request, template): diff --git a/lms/lib/comment_client/legacy.py b/lms/lib/comment_client/legacy.py index fbf66a09fd..de7ce201ce 100644 --- a/lms/lib/comment_client/legacy.py +++ b/lms/lib/comment_client/legacy.py @@ -5,16 +5,14 @@ def delete_threads(commentable_id, *args, **kwargs): def get_threads(commentable_id, recursive=False, query_params={}, *args, **kwargs): default_params = {'page': 1, 'per_page': 20, 'recursive': recursive} attributes = dict(default_params.items() + query_params.items()) - response = _perform_request('get', _url_for_threads(commentable_id), \ - attributes, *args, **kwargs) + response = _perform_request('get', _url_for_threads(commentable_id), attributes, *args, **kwargs) return response.get('collection', []), response.get('page', 1), response.get('num_pages', 1) def search_threads(course_id, recursive=False, query_params={}, *args, **kwargs): default_params = {'page': 1, 'per_page': 20, 'course_id': course_id, 'recursive': recursive} attributes = dict(default_params.items() + query_params.items()) - response = _perform_request('get', _url_for_search_threads(), \ - attributes, *args, **kwargs) + response = _perform_request('get', _url_for_search_threads(), attributes, *args, **kwargs) return response.get('collection', []), response.get('page', 1), response.get('num_pages', 1) From 83af3e594f561c4f1c3c35b59f34089b2843a887 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 17 Jun 2013 13:49:40 -0400 Subject: [PATCH 260/375] Use built-in rakelibdir feature Rake by default imports all .rake files in the rakelib dir, so we can use that rather than doing our own import loop. --- rakefile | 6 +----- {rakefiles => rakelib}/assets.rake | 0 {rakefiles => rakelib}/deploy.rake | 0 {rakefiles => rakelib}/deprecated.rake | 0 {rakefiles => rakelib}/django.rake | 0 {rakefiles => rakelib}/docs.rake | 0 {rakefiles => rakelib}/helpers.rb | 0 {rakefiles => rakelib}/i18n.rake | 0 {rakefiles => rakelib}/jasmine.rake | 0 {rakefiles => rakelib}/prereqs.rake | 2 -- {rakefiles => rakelib}/quality.rake | 0 {rakefiles => rakelib}/tests.rake | 0 {rakefiles => rakelib}/workspace.rake | 0 13 files changed, 1 insertion(+), 7 deletions(-) rename {rakefiles => rakelib}/assets.rake (100%) rename {rakefiles => rakelib}/deploy.rake (100%) rename {rakefiles => rakelib}/deprecated.rake (100%) rename {rakefiles => rakelib}/django.rake (100%) rename {rakefiles => rakelib}/docs.rake (100%) rename {rakefiles => rakelib}/helpers.rb (100%) rename {rakefiles => rakelib}/i18n.rake (100%) rename {rakefiles => rakelib}/jasmine.rake (100%) rename {rakefiles => rakelib}/prereqs.rake (98%) rename {rakefiles => rakelib}/quality.rake (100%) rename {rakefiles => rakelib}/tests.rake (100%) rename {rakefiles => rakelib}/workspace.rake (100%) diff --git a/rakefile b/rakefile index 20101a14db..3fcd16f995 100644 --- a/rakefile +++ b/rakefile @@ -1,10 +1,6 @@ require 'json' require 'rake/clean' -require './rakefiles/helpers.rb' - -Dir['rakefiles/*.rake'].each do |rakefile| - import rakefile -end +require './rakelib/helpers.rb' # Build Constants REPO_ROOT = File.dirname(__FILE__) diff --git a/rakefiles/assets.rake b/rakelib/assets.rake similarity index 100% rename from rakefiles/assets.rake rename to rakelib/assets.rake diff --git a/rakefiles/deploy.rake b/rakelib/deploy.rake similarity index 100% rename from rakefiles/deploy.rake rename to rakelib/deploy.rake diff --git a/rakefiles/deprecated.rake b/rakelib/deprecated.rake similarity index 100% rename from rakefiles/deprecated.rake rename to rakelib/deprecated.rake diff --git a/rakefiles/django.rake b/rakelib/django.rake similarity index 100% rename from rakefiles/django.rake rename to rakelib/django.rake diff --git a/rakefiles/docs.rake b/rakelib/docs.rake similarity index 100% rename from rakefiles/docs.rake rename to rakelib/docs.rake diff --git a/rakefiles/helpers.rb b/rakelib/helpers.rb similarity index 100% rename from rakefiles/helpers.rb rename to rakelib/helpers.rb diff --git a/rakefiles/i18n.rake b/rakelib/i18n.rake similarity index 100% rename from rakefiles/i18n.rake rename to rakelib/i18n.rake diff --git a/rakefiles/jasmine.rake b/rakelib/jasmine.rake similarity index 100% rename from rakefiles/jasmine.rake rename to rakelib/jasmine.rake diff --git a/rakefiles/prereqs.rake b/rakelib/prereqs.rake similarity index 98% rename from rakefiles/prereqs.rake rename to rakelib/prereqs.rake index ff8b4b8784..e06d411435 100644 --- a/rakefiles/prereqs.rake +++ b/rakelib/prereqs.rake @@ -1,5 +1,3 @@ -require './rakefiles/helpers.rb' - PREREQS_MD5_DIR = ENV["PREREQ_CACHE_DIR"] || File.join(REPO_ROOT, '.prereqs_cache') CLOBBER.include(PREREQS_MD5_DIR) diff --git a/rakefiles/quality.rake b/rakelib/quality.rake similarity index 100% rename from rakefiles/quality.rake rename to rakelib/quality.rake diff --git a/rakefiles/tests.rake b/rakelib/tests.rake similarity index 100% rename from rakefiles/tests.rake rename to rakelib/tests.rake diff --git a/rakefiles/workspace.rake b/rakelib/workspace.rake similarity index 100% rename from rakefiles/workspace.rake rename to rakelib/workspace.rake From d9268acd6bb9b19175315998de623818e0a799f1 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 17 Jun 2013 13:52:00 -0400 Subject: [PATCH 261/375] Provide instructions of ruby imports fail in rake `rake install_prereqs` requires a minimal level of ruby and rake already installed. If it doesn't exist, then print out a helpful message indicating next steps. --- CHANGELOG.rst | 2 ++ rakefile | 12 +++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7fc07a3f19..9c7af1520b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes, in roughly chronological order, most recent first. Add your entries at or near the top. Include a label indicating the component affected. +Common: Make rake provide better error messages if packages are missing. + Common: Repairs development documentation generation by sphinx. LMS: Problem rescoring. Added options on the Grades tab of the diff --git a/rakefile b/rakefile index 3fcd16f995..96bd4c2e96 100644 --- a/rakefile +++ b/rakefile @@ -1,6 +1,12 @@ -require 'json' -require 'rake/clean' -require './rakelib/helpers.rb' +begin + require 'json' + require 'rake/clean' + require './rakelib/helpers.rb' +rescue LoadError => error + puts "Import faild (#{error})" + puts "Please run `bundle install` to bootstrap ruby dependencies" + exit 1 +end # Build Constants REPO_ROOT = File.dirname(__FILE__) From d99ad53ae90bebc0fb2c099b42a2ffa1c64ab4b8 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Wed, 19 Jun 2013 15:30:47 -0400 Subject: [PATCH 262/375] Add system and env arguments to asset tasks The preprocess task requires system and env arguments in order to correctly load up the django environment to preprocess sass files to inject themes. In order for that task to recieve the arguments, all tasks that depend on it also have to accept that same set of arguments. This will all go away once the next evolution of themes arrives, which will remove the preprocessing needed to inject theme names. --- rakelib/assets.rake | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/rakelib/assets.rake b/rakelib/assets.rake index 764d049a68..0c58047bc2 100644 --- a/rakelib/assets.rake +++ b/rakelib/assets.rake @@ -55,8 +55,9 @@ def sass_cmd(watch=false, debug=false) "#{watch ? '--watch' : '--update'} -E utf-8 #{sass_watch_paths.join(' ')}" end +# This task takes arguments purely to pass them via dependencies to the preprocess task desc "Compile all assets" -multitask :assets => 'assets:all' +task :assets, [:system, :env] => 'assets:all' namespace :assets do @@ -80,8 +81,9 @@ namespace :assets do {:xmodule => [:install_python_prereqs], :coffee => [:install_node_prereqs, :'assets:coffee:clobber'], :sass => [:install_ruby_prereqs, :preprocess]}.each_pair do |asset_type, prereq_tasks| + # This task takes arguments purely to pass them via dependencies to the preprocess task desc "Compile all #{asset_type} assets" - task asset_type => prereq_tasks do + task asset_type, [:system, :env] => prereq_tasks do |t, args| cmd = send(asset_type.to_s + "_cmd", watch=false, debug=false) if cmd.kind_of?(Array) cmd.each {|c| sh(c)} @@ -90,7 +92,8 @@ namespace :assets do end end - multitask :all => asset_type + # This task takes arguments purely to pass them via dependencies to the preprocess task + multitask :all, [:system, :env] => asset_type multitask :debug => "assets:#{asset_type}:debug" multitask :_watch => "assets:#{asset_type}:_watch" From 2e480f6404b2ae640623266a4cbd9f91dfa067c6 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 17 Jun 2013 15:00:55 -0400 Subject: [PATCH 263/375] Switch to standard coffee watcher Using `ulimit -n` to set the limit much higher than the default of 256 in Darwin seems to avoid the `EMFILE` error that was plaguing our Mac developers. --- CHANGELOG.rst | 2 ++ doc/development.md | 19 +++++++++++++++++++ rakelib/assets.rake | 26 +++++++++----------------- 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9c7af1520b..99accb83cb 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes, in roughly chronological order, most recent first. Add your entries at or near the top. Include a label indicating the component affected. +Common: Use coffee directly when watching for coffeescript file changes. + Common: Make rake provide better error messages if packages are missing. Common: Repairs development documentation generation by sphinx. diff --git a/doc/development.md b/doc/development.md index c99e99f906..e6ab650002 100644 --- a/doc/development.md +++ b/doc/development.md @@ -63,6 +63,25 @@ To get a full list of available rake tasks, use: rake -T +### Troubleshooting + +#### Reference Error: XModule is not defined (javascript) +This means that the javascript defining an xmodule hasn't loaded correctly. There are a number +of different things that could be causing this: + +1. See `Error: watch EMFILE` + +#### Error: watch EMFILE (coffee) +When running a development server, we also start a watcher process alongside to recompile coffeescript +and sass as changes are made. On Mac OSX systems, the coffee watcher process takes more file handles +than are allowed by default. This will result in `EMFILE` errors when coffeescript is running, and +will prevent javascript from compiling, leading to the error 'XModule is not defined' + +To work around this issue, we use `Process::setrlimit` to set the number of allowed open files. +Coffee watches both directories and files, so you will need to set this fairly high (anecdotally, +8000 seems to do the trick on OSX 10.7.5, 10.8.3, and 10.8.4) + + ## Running Tests See `testing.md` for instructions on running the test suite. diff --git a/rakelib/assets.rake b/rakelib/assets.rake index 0c58047bc2..10dfb14f18 100644 --- a/rakelib/assets.rake +++ b/rakelib/assets.rake @@ -6,6 +6,8 @@ if USE_CUSTOM_THEME THEME_SASS = File.join(THEME_ROOT, "static", "sass") end +MINIMAL_DARWIN_NOFILE_LIMIT = 8000 + def xmodule_cmd(watch=false, debug=false) xmodule_cmd = 'xmodule_assets common/static/xmodule' if watch @@ -21,24 +23,14 @@ def xmodule_cmd(watch=false, debug=false) end def coffee_cmd(watch=false, debug=false) - if watch - # On OSx, coffee fails with EMFILE when - # trying to watch all of our coffee files at the same - # time. - # - # Ref: https://github.com/joyent/node/issues/2479 - # - # So, instead, we use watchmedo, which works around the problem - "watchmedo shell-command " + - "--command 'node_modules/.bin/coffee -c ${watch_src_path}' " + - "--recursive " + - "--patterns '*.coffee' " + - "--ignore-directories " + - "--wait " + - "." - else - 'node_modules/.bin/coffee --compile .' + if watch && Launchy::Application.new.host_os_family.darwin? + available_files = Process::getrlimit(:NOFILE)[0] + if available_files < MINIMAL_DARWIN_NOFILE_LIMIT + Process.setrlimit(:NOFILE, MINIMAL_DARWIN_NOFILE_LIMIT) + + end end + "node_modules/.bin/coffee --compile #{watch ? '--watch' : ''} ." end def sass_cmd(watch=false, debug=false) From f4200127bcc91fc30eca2f727a21c6a71139fbfd Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 14 Jun 2013 21:46:27 -0400 Subject: [PATCH 264/375] Make coffee watch message more informative When running under watchmedo, coffee doesn't display any useful information when it recompiles a changed file, so we make watchmedo echo that information instead. --- rakelib/assets.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rakelib/assets.rake b/rakelib/assets.rake index 0c58047bc2..c80b275c27 100644 --- a/rakelib/assets.rake +++ b/rakelib/assets.rake @@ -30,7 +30,7 @@ def coffee_cmd(watch=false, debug=false) # # So, instead, we use watchmedo, which works around the problem "watchmedo shell-command " + - "--command 'node_modules/.bin/coffee -c ${watch_src_path}' " + + "--command 'echo \">>> Change detected to ${watch_src_path}\" && node_modules/.bin/coffee -c ${watch_src_path}' " + "--recursive " + "--patterns '*.coffee' " + "--ignore-directories " + From eb3e94660bf166b75a34601549b7093507c46bf7 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 14 Jun 2013 21:47:22 -0400 Subject: [PATCH 265/375] Don't delete generated files from xmodule-assets xmodule-assets creates coffeescript files in the output directories. On its next run, it used to delete the javascript files compiled from those coffee files. Now it doesn't which should make coffee have to do less work. Fixes LMS-451 --- CHANGELOG.rst | 4 ++++ common/lib/xmodule/xmodule/static_content.py | 16 ++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9c7af1520b..fbc007949c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,10 @@ These are notable changes in edx-platform. This is a rolling list of changes, in roughly chronological order, most recent first. Add your entries at or near the top. Include a label indicating the component affected. +XModule: Don't delete generated xmodule asset files when compiling (for +instance, when XModule provides a coffeescript file, don't delete +the associated javascript) + Common: Make rake provide better error messages if packages are missing. Common: Repairs development documentation generation by sphinx. diff --git a/common/lib/xmodule/xmodule/static_content.py b/common/lib/xmodule/xmodule/static_content.py index 4c4827e0aa..42fef65b11 100755 --- a/common/lib/xmodule/xmodule/static_content.py +++ b/common/lib/xmodule/xmodule/static_content.py @@ -121,15 +121,23 @@ def _write_js(output_root, classes): type=filetype) contents[filename] = fragment - _write_files(output_root, contents) + _write_files(output_root, contents, {'.coffee': '.js'}) return [output_root / filename for filename in contents.keys()] -def _write_files(output_root, contents): +def _write_files(output_root, contents, generated_suffix_map=None): _ensure_dir(output_root) - for extra_file in set(output_root.files()) - set(contents.keys()): - extra_file.remove_p() + to_delete = set(file.basename() for file in output_root.files()) - set(contents.keys()) + + if generated_suffix_map: + for output_file in contents.keys(): + for suffix, generated_suffix in generated_suffix_map.items(): + if output_file.endswith(suffix): + to_delete.discard(output_file.replace(suffix, generated_suffix)) + + for extra_file in to_delete: + (output_root / extra_file).remove_p() for filename, file_content in contents.iteritems(): (output_root / filename).write_bytes(file_content) From cd57e281f5eab2d7616e47d4d9b95adf46efda1e Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 14 Jun 2013 22:27:24 -0400 Subject: [PATCH 266/375] Only write to xmodule asset files that have changed Use md5 checksumming to verify that we only write out xmodule asset files whose contents differ from what we are about to write. This minimizes thrashing of the other watchers. Fixes LMS-452 --- CHANGELOG.rst | 2 ++ common/lib/xmodule/xmodule/static_content.py | 12 +++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fbc007949c..d74816990b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes, in roughly chronological order, most recent first. Add your entries at or near the top. Include a label indicating the component affected. +XModule: Only write out assets files if the contents have changed. + XModule: Don't delete generated xmodule asset files when compiling (for instance, when XModule provides a coffeescript file, don't delete the associated javascript) diff --git a/common/lib/xmodule/xmodule/static_content.py b/common/lib/xmodule/xmodule/static_content.py index 42fef65b11..7662499c16 100755 --- a/common/lib/xmodule/xmodule/static_content.py +++ b/common/lib/xmodule/xmodule/static_content.py @@ -4,6 +4,7 @@ This module has utility functions for gathering up the static content that is defined by XModules and XModuleDescriptors (javascript and css) """ +import logging import hashlib import os import errno @@ -15,6 +16,9 @@ from path import path from xmodule.x_module import XModuleDescriptor +LOG = logging.getLogger(__name__) + + def write_module_styles(output_root): return _write_styles('.xmodule_display', output_root, _list_modules()) @@ -140,7 +144,13 @@ def _write_files(output_root, contents, generated_suffix_map=None): (output_root / extra_file).remove_p() for filename, file_content in contents.iteritems(): - (output_root / filename).write_bytes(file_content) + output_file = output_root / filename + + if not output_file.isfile() or output_file.read_md5() != hashlib.md5(file_content).digest(): + LOG.debug("Writing %s", output_file) + output_file.write_bytes(file_content) + else: + LOG.debug("%s unchanged, skipping", output_file) def main(): From 64912a774199e40a68cab3020d3e98ad9bb8c6e5 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 14 Jun 2013 21:17:08 -0400 Subject: [PATCH 267/375] Make assets watchers run as singletons Previously, multiple copies of the watchers started from the different shells would run simultaneously, which left the possiblity of zombie watchers, increased resource consumption, and incorrect results. This fixes that problem by only starting a watcher if that same command isn't already in the process list. Fixes LMS-499 --- CHANGELOG.rst | 3 +++ Gemfile | 1 + rakelib/assets.rake | 4 ++-- rakelib/helpers.rb | 12 ++++++++++++ 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9c7af1520b..18855e82ae 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,9 @@ These are notable changes in edx-platform. This is a rolling list of changes, in roughly chronological order, most recent first. Add your entries at or near the top. Include a label indicating the component affected. +Common: Make asset watchers run as singletons (so they won't start if the +watcher is already running in another shell). + Common: Make rake provide better error messages if packages are missing. Common: Repairs development documentation generation by sphinx. diff --git a/Gemfile b/Gemfile index 7f7b146978..1ad685c34d 100644 --- a/Gemfile +++ b/Gemfile @@ -4,3 +4,4 @@ gem 'sass', '3.1.15' gem 'bourbon', '~> 1.3.6' gem 'colorize', '~> 0.5.8' gem 'launchy', '~> 2.1.2' +gem 'sys-proctable', '~> 0.9.3' diff --git a/rakelib/assets.rake b/rakelib/assets.rake index 0c58047bc2..c935b0d53b 100644 --- a/rakelib/assets.rake +++ b/rakelib/assets.rake @@ -114,9 +114,9 @@ namespace :assets do task :_watch => (prereq_tasks + ["assets:#{asset_type}:debug"]) do cmd = send(asset_type.to_s + "_cmd", watch=true, debug=true) if cmd.kind_of?(Array) - cmd.each {|c| background_process(c)} + cmd.each {|c| singleton_process(c)} else - background_process(cmd) + singleton_process(cmd) end end end diff --git a/rakelib/helpers.rb b/rakelib/helpers.rb index 4b10bef709..3373214a19 100644 --- a/rakelib/helpers.rb +++ b/rakelib/helpers.rb @@ -1,4 +1,6 @@ require 'digest/md5' +require 'sys/proctable' +require 'colorize' def find_executable(exec) path = %x(which #{exec}).strip @@ -84,6 +86,16 @@ def background_process(*command) end end +# Runs a command as a background process, as long as no other processes +# tagged with the same tag are running +def singleton_process(*command) + if Sys::ProcTable.ps.select {|proc| proc.cmdline.include?(command.join(' '))}.empty? + background_process(*command) + else + puts "Process '#{command.join(' ')} already running, skipping".blue + end +end + def environments(system) Dir["#{system}/envs/**/*.py"].select{|file| ! (/__init__.py$/ =~ file)}.map do |env_file| env_file.gsub("#{system}/envs/", '').gsub(/\.py/, '').gsub('/', '.') From 419c5e7a5ce513d64291759658d6d8930e5fe2c5 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Thu, 20 Jun 2013 09:23:16 -0400 Subject: [PATCH 268/375] studio - removes course settings promo link and notice icons --- cms/templates/settings.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cms/templates/settings.html b/cms/templates/settings.html index 6f2ef7653d..72df12cdd5 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -85,6 +85,7 @@ from contentstore import utils
+ % if about_page_editable:

${_("Course Summary Page")} ${_("(for student enrollment and access)")}

@@ -97,10 +98,11 @@ from contentstore import utils
+ % endif % if not about_page_editable:
-

${_("Promoting Your Course with edX")}

+

${_("Promoting Your Course with edX")}

${_('Your course summary page will not be viewable until your course has been announced. To provide content for the page and preview it, follow the instructions provided by your PM or Conrad Warre (conrad@edx.org).')}

@@ -180,7 +182,7 @@ from contentstore import utils % if not about_page_editable:
-

${_("These Dates Are Not Used When Promoting Your Course")}

+

${_("These Dates Are Not Used When Promoting Your Course")}

${_('These dates impact when your courseware can be viewed, but they are not the dates shown on your course summary page. To provide the course start and registration dates as shown on your course summary page, follow the instructions provided by your PM or Conrad Warre (conrad@edx.org).')}

From 604e820211755d80a9aac6759c75930ec58c9c4a Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Thu, 20 Jun 2013 09:23:56 -0400 Subject: [PATCH 269/375] studio - revises styling of in-context notices UI --- cms/static/sass/elements/_system-help.scss | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/cms/static/sass/elements/_system-help.scss b/cms/static/sass/elements/_system-help.scss index 5f4cec26d7..a9a3e16128 100644 --- a/cms/static/sass/elements/_system-help.scss +++ b/cms/static/sass/elements/_system-help.scss @@ -10,22 +10,24 @@ @extend .t-title7; margin-bottom: ($baseline/4); font-weight: 600; - - [class^="icon-"] { - @extend .t-icon5; - display: inline-block; - vertical-align: middle; - margin-right: ($baseline/4); - } } .copy { - @extend .t-copy-sub2; + @extend .t-copy-sub1; + @include transition(opacity 0.25s ease-in-out 0); + opacity: 0.75; } strong { font-weight: 600; } + + &:hover { + + .copy { + opacity: 1.0; + } + } } // particular warnings around a workflow for something @@ -35,8 +37,4 @@ .copy { color: $gray-d1; } - - .icon-warning-sign { - color: $yellow-s3; - } } From c00721bbe6dd341590bea992e93803b98291c3e1 Mon Sep 17 00:00:00 2001 From: Felix Sun Date: Tue, 18 Jun 2013 13:10:47 -0400 Subject: [PATCH 270/375] Fixed the preferences scope of xblock. Added self to authors. Conflicts: AUTHORS CHANGELOG.rst --- AUTHORS | 1 + CHANGELOG.rst | 2 ++ lms/djangoapps/courseware/model_data.py | 2 +- lms/djangoapps/courseware/tests/factories.py | 2 +- lms/djangoapps/courseware/tests/test_model_data.py | 7 ++++++- 5 files changed, 11 insertions(+), 3 deletions(-) diff --git a/AUTHORS b/AUTHORS index 1af7349491..9bb4ede121 100644 --- a/AUTHORS +++ b/AUTHORS @@ -77,3 +77,4 @@ Slater Victoroff Peter Fogg Bethany LaPenta Renzo Lucioni +Felix Sun diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9c405ed365..206be44c87 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -27,6 +27,8 @@ students' number of attempts to zero. Provides a list of background tasks that are currently running for the course, and an option to see a history of background tasks for a given problem. +LMS: Fixed the preferences scope for storing data in xmodules. + LMS: Forums. Added handling for case where discussion module can get `None` as value of lms.start in `lms/djangoapps/django_comment_client/utils.py` diff --git a/lms/djangoapps/courseware/model_data.py b/lms/djangoapps/courseware/model_data.py index f363546af0..790f1fd721 100644 --- a/lms/djangoapps/courseware/model_data.py +++ b/lms/djangoapps/courseware/model_data.py @@ -163,7 +163,7 @@ class ModelDataCache(object): return self._chunked_query( XModuleStudentPrefsField, 'module_type__in', - set(descriptor.location.category for descriptor in self.descriptors), + set(descriptor.module_class.__name__ for descriptor in self.descriptors), student=self.user.pk, field_name__in=set(field.name for field in fields), ) diff --git a/lms/djangoapps/courseware/tests/factories.py b/lms/djangoapps/courseware/tests/factories.py index 26df68ca7e..69f8f54eec 100644 --- a/lms/djangoapps/courseware/tests/factories.py +++ b/lms/djangoapps/courseware/tests/factories.py @@ -75,7 +75,7 @@ class StudentPrefsFactory(DjangoModelFactory): field_name = 'existing_field' value = json.dumps('old_value') student = SubFactory(UserFactory) - module_type = 'problem' + module_type = 'MockProblemModule' class StudentInfoFactory(DjangoModelFactory): diff --git a/lms/djangoapps/courseware/tests/test_model_data.py b/lms/djangoapps/courseware/tests/test_model_data.py index 9f225f73bd..e961f80939 100644 --- a/lms/djangoapps/courseware/tests/test_model_data.py +++ b/lms/djangoapps/courseware/tests/test_model_data.py @@ -29,6 +29,7 @@ def mock_descriptor(fields=[], lms_fields=[]): descriptor.location = location('def_id') descriptor.module_class.fields = fields descriptor.module_class.lms.fields = lms_fields + descriptor.module_class.__name__ = 'MockProblemModule' return descriptor location = partial(Location, 'i4x', 'edX', 'test_course', 'problem') @@ -37,7 +38,7 @@ course_id = 'edX/test_course/test' content_key = partial(LmsKeyValueStore.Key, Scope.content, None, location('def_id')) settings_key = partial(LmsKeyValueStore.Key, Scope.settings, None, location('def_id')) user_state_key = partial(LmsKeyValueStore.Key, Scope.user_state, 'user', location('def_id')) -prefs_key = partial(LmsKeyValueStore.Key, Scope.preferences, 'user', 'problem') +prefs_key = partial(LmsKeyValueStore.Key, Scope.preferences, 'user', 'MockProblemModule') user_info_key = partial(LmsKeyValueStore.Key, Scope.user_info, 'user', None) @@ -190,6 +191,10 @@ class StorageTestBase(object): self.mdc = ModelDataCache([mock_descriptor([mock_field(self.scope, 'existing_field')])], course_id, self.user) self.kvs = LmsKeyValueStore(self.desc_md, self.mdc) + def test_set_and_get_existing_field(self): + self.kvs.set(self.key_factory('existing_field'), 'test_value') + self.assertEquals('test_value', self.kvs.get(self.key_factory('existing_field'))) + def test_get_existing_field(self): "Test that getting an existing field in an existing Storage Field works" self.assertEquals('old_value', self.kvs.get(self.key_factory('existing_field'))) From 9e69586bb360db757c1d3c43bbe862cb65a6b670 Mon Sep 17 00:00:00 2001 From: cahrens Date: Thu, 20 Jun 2013 10:31:44 -0400 Subject: [PATCH 271/375] pep8 fixes. --- .../features/advanced-settings.py | 7 +++-- common/djangoapps/terrain/ui_helpers.py | 26 +++++++++---------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/cms/djangoapps/contentstore/features/advanced-settings.py b/cms/djangoapps/contentstore/features/advanced-settings.py index 3113603467..473fc20a68 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.py +++ b/cms/djangoapps/contentstore/features/advanced-settings.py @@ -31,11 +31,10 @@ def press_the_notification_button(step, name): # Save was clicked if either the save notification bar is gone, or we have a error notification # overlaying it (expected in the case of typing Object into display_name). - save_clicked = lambda : world.is_css_not_present('.is-shown.wrapper-notification-warning') or \ - world.is_css_present('.is-shown.wrapper-notification-error') + save_clicked = lambda: world.is_css_not_present('.is-shown.wrapper-notification-warning') or\ + world.is_css_present('.is-shown.wrapper-notification-error') - assert_true(world.css_click(css, success_condition=save_clicked), - 'The save button was not clicked after 5 attempts.') + assert_true(world.css_click(css, success_condition=save_clicked), 'Save button not clicked after 5 attempts.') @step(u'I edit the value of a policy key$') diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py index 8e4330d940..77667f2d63 100644 --- a/common/djangoapps/terrain/ui_helpers.py +++ b/common/djangoapps/terrain/ui_helpers.py @@ -49,7 +49,7 @@ def css_has_text(css_selector, text): @world.absorb def css_find(css, wait_time=5): - def is_visible(driver): + def is_visible(_driver): return EC.visibility_of_element_located((By.CSS_SELECTOR, css,)) world.browser.is_element_present_by_css(css, wait_time=wait_time) @@ -58,7 +58,7 @@ def css_find(css, wait_time=5): @world.absorb -def css_click(css_selector, index=0, attempts=5, success_condition=lambda:True): +def css_click(css_selector, index=0, attempts=5, success_condition=lambda: True): """ Perform a click on a CSS selector, retrying if it initially fails. @@ -90,15 +90,15 @@ def css_click(css_selector, index=0, attempts=5, success_condition=lambda:True): @world.absorb -def css_click_at(css, x=10, y=10): +def css_click_at(css, x_cord=10, y_cord=10): ''' A method to click at x,y coordinates of the element rather than in the center of the element ''' - e = css_find(css).first - e.action_chains.move_to_element_with_offset(e._element, x, y) - e.action_chains.click() - e.action_chains.perform() + element = css_find(css).first + element.action_chains.move_to_element_with_offset(element._element, x_cord, y_cord) + element.action_chains.click() + element.action_chains.perform() @world.absorb @@ -143,7 +143,7 @@ def css_visible(css_selector): @world.absorb def dialogs_closed(): - def are_dialogs_closed(driver): + def are_dialogs_closed(_driver): ''' Return True when no modal dialogs are visible ''' @@ -154,12 +154,12 @@ def dialogs_closed(): @world.absorb def save_the_html(path='/tmp'): - u = world.browser.url + url = world.browser.url html = world.browser.html.encode('ascii', 'ignore') - filename = '%s.html' % quote_plus(u) - f = open('%s/%s' % (path, filename), 'w') - f.write(html) - f.close() + filename = '%s.html' % quote_plus(url) + file = open('%s/%s' % (path, filename), 'w') + file.write(html) + file.close() @world.absorb From 485b5b2d6dada79a31a0f7b5276d5140f3faab69 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Thu, 20 Jun 2013 10:50:15 -0400 Subject: [PATCH 272/375] studio - unconditionalizes course summary info in settings --- cms/templates/settings.html | 2 -- 1 file changed, 2 deletions(-) diff --git a/cms/templates/settings.html b/cms/templates/settings.html index 72df12cdd5..14c79e586a 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -85,7 +85,6 @@ from contentstore import utils - % if about_page_editable:

${_("Course Summary Page")} ${_("(for student enrollment and access)")}

@@ -98,7 +97,6 @@ from contentstore import utils
- % endif % if not about_page_editable:
From 033b974047822ecd9b4921e04a6eac79e7485db3 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 20 Jun 2013 10:54:43 -0400 Subject: [PATCH 273/375] Fixed flakey show answer test --- lms/djangoapps/courseware/features/problems.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index 0278ee9b42..094d495b53 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -135,12 +135,10 @@ def action_button_present(_step, buttonname, doesnt_appear): @step(u'the button with the label "([^"]*)" does( not)? appear') def button_with_label_present(step, buttonname, doesnt_appear): - button_css = 'button span.show-label' - elem = world.css_find(button_css).first if doesnt_appear: - assert_not_equal(elem.text, buttonname) + world.browser.is_text_not_present(buttonname, wait_time=5) else: - assert_equal(elem.text, buttonname) + world.browser.is_text_present(buttonname, wait_time=5) @step(u'My "([^"]*)" answer is marked "([^"]*)"') From 1e51bd7314d3580f0d3d0b122c6b6f71c3cc20e9 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 20 Jun 2013 10:58:11 -0400 Subject: [PATCH 274/375] Fixed flakey navigation tests by changing css Now only check for the css that appears when the accordion is done --- lms/djangoapps/courseware/features/navigation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/courseware/features/navigation.py b/lms/djangoapps/courseware/features/navigation.py index e0f82f9251..88c540b232 100644 --- a/lms/djangoapps/courseware/features/navigation.py +++ b/lms/djangoapps/courseware/features/navigation.py @@ -83,9 +83,9 @@ def click_on_section(step, section): world.css_click(section_css) subid = "ui-accordion-accordion-panel-" + str(int(section) - 1) - subsection_css = 'ul[id="%s"]> li > a' % subid + subsection_css = 'ul.ui-accordion-content-active[id=\'%s\'][aria-expanded=\'true\']> li > a' % subid #for some reason needed to do it in two steps - world.css_find(subsection_css).click() + world.css_click(subsection_css) @step(u'I click on subsection "([^"]*)"$') From a8789dced903ff6236974fc268305379cd2ce057 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 20 Jun 2013 11:14:17 -0400 Subject: [PATCH 275/375] Fixed flakey check box by making a css_check function that behaves like css_click As a result, changed inputfield to return the css, and all of the element.fill() to css_fill --- common/djangoapps/terrain/ui_helpers.py | 32 +++++++++++++++++++ .../courseware/features/problems_setup.py | 32 +++++++++---------- 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py index 8e4330d940..9c837cbd0d 100644 --- a/common/djangoapps/terrain/ui_helpers.py +++ b/common/djangoapps/terrain/ui_helpers.py @@ -89,6 +89,38 @@ def css_click(css_selector, index=0, attempts=5, success_condition=lambda:True): return result +@world.absorb +def css_check(css_selector, index=0, attempts=5, success_condition=lambda: True): + """ + Checks a check box based on a CSS selector, retrying if it initially fails. + + This function handles errors that may be thrown if the component cannot be clicked on. + However, there are cases where an error may not be thrown, and yet the operation did not + actually succeed. For those cases, a success_condition lambda can be supplied to verify that the check worked. + + This function will return True if the check worked (taking into account both errors and the optional + success_condition). + """ + assert is_css_present(css_selector) + attempt = 0 + result = False + while attempt < attempts: + try: + world.css_find(css_selector)[index].check() + if success_condition(): + result = True + break + except WebDriverException: + # Occasionally, MathJax or other JavaScript can cover up + # an element temporarily. + # If this happens, wait a second, then try again + world.wait(1) + attempt += 1 + except: + attempt += 1 + return result + + @world.absorb def css_click_at(css, x=10, y=10): ''' diff --git a/lms/djangoapps/courseware/features/problems_setup.py b/lms/djangoapps/courseware/features/problems_setup.py index ce343bb853..b8f817f933 100644 --- a/lms/djangoapps/courseware/features/problems_setup.py +++ b/lms/djangoapps/courseware/features/problems_setup.py @@ -142,34 +142,34 @@ def answer_problem(problem_type, correctness): elif problem_type == "multiple choice": if correctness == 'correct': - inputfield('multiple choice', choice='choice_2').check() + world.css_check(inputfield('multiple choice', choice='choice_2')) else: - inputfield('multiple choice', choice='choice_1').check() + world.css_check(inputfield('multiple choice', choice='choice_1')) elif problem_type == "checkbox": if correctness == 'correct': - inputfield('checkbox', choice='choice_0').check() - inputfield('checkbox', choice='choice_2').check() + world.css_check(inputfield('checkbox', choice='choice_0')) + world.css_check(inputfield('checkbox', choice='choice_2')) else: - inputfield('checkbox', choice='choice_3').check() + world.css_check(inputfield('checkbox', choice='choice_3')) elif problem_type == 'radio': if correctness == 'correct': - inputfield('radio', choice='choice_2').check() + world.css_check(inputfield('radio', choice='choice_2')) else: - inputfield('radio', choice='choice_1').check() + world.css_check(inputfield('radio', choice='choice_1')) elif problem_type == 'string': textvalue = 'correct string' if correctness == 'correct' else 'incorrect' - inputfield('string').fill(textvalue) + world.css_fill(inputfield('string'), textvalue) elif problem_type == 'numerical': textvalue = "pi + 1" if correctness == 'correct' else str(random.randint(-2, 2)) - inputfield('numerical').fill(textvalue) + world.css_fill(inputfield('numerical'), textvalue) elif problem_type == 'formula': textvalue = "x^2+2*x+y" if correctness == 'correct' else 'x^2' - inputfield('formula').fill(textvalue) + world.css_fill(inputfield('formula'), textvalue) elif problem_type == 'script': # Correct answer is any two integers that sum to 10 @@ -181,8 +181,8 @@ def answer_problem(problem_type, correctness): if correctness == 'incorrect': second_addend += random.randint(1, 10) - inputfield('script', input_num=1).fill(str(first_addend)) - inputfield('script', input_num=2).fill(str(second_addend)) + world.css_fill(inputfield('script', input_num=1), str(first_addend)) + world.css_fill(inputfield('script', input_num=2), str(second_addend)) elif problem_type == 'code': # The fake xqueue server is configured to respond @@ -281,7 +281,7 @@ def add_problem_to_course(course, problem_type, extraMeta=None): def inputfield(problem_type, choice=None, input_num=1): - """ Return the element for *problem_type*. + """ Return the css element for *problem_type*. For example, if problem_type is 'string', return the text field for the string problem in the test course. @@ -299,7 +299,7 @@ def inputfield(problem_type, choice=None, input_num=1): assert world.is_css_present(sel) # Retrieve the input element - return world.browser.find_by_css(sel) + return sel def assert_checked(problem_type, choices): @@ -312,7 +312,7 @@ def assert_checked(problem_type, choices): all_choices = ['choice_0', 'choice_1', 'choice_2', 'choice_3'] for this_choice in all_choices: - element = inputfield(problem_type, choice=this_choice) + element = world.css_find(inputfield(problem_type, choice=this_choice)) if this_choice in choices: assert element.checked @@ -321,5 +321,5 @@ def assert_checked(problem_type, choices): def assert_textfield(problem_type, expected_text, input_num=1): - element = inputfield(problem_type, input_num=input_num) + element = world.css_find(inputfield(problem_type, input_num=input_num)) assert element.value == expected_text From c77eb4fd4cb41a74d1c6de5d11579c15c4627b9c Mon Sep 17 00:00:00 2001 From: Peter Fogg Date: Thu, 20 Jun 2013 11:14:33 -0400 Subject: [PATCH 276/375] Fix VideoAlpha acceptance test step definitions to not clash with Video module. --- lms/djangoapps/courseware/features/videoalpha.feature | 4 ++-- lms/djangoapps/courseware/features/videoalpha.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lms/djangoapps/courseware/features/videoalpha.feature b/lms/djangoapps/courseware/features/videoalpha.feature index 2a0acb0f9b..8cff43f45f 100644 --- a/lms/djangoapps/courseware/features/videoalpha.feature +++ b/lms/djangoapps/courseware/features/videoalpha.feature @@ -2,5 +2,5 @@ Feature: Video Alpha component As a student, I want to view course videos in LMS. Scenario: Autoplay is enabled in LMS - Given the course has a Video component - Then when I view the video it has autoplay enabled + Given the course has a Video Alpha component + Then when I view the Video Alpha it has autoplay enabled diff --git a/lms/djangoapps/courseware/features/videoalpha.py b/lms/djangoapps/courseware/features/videoalpha.py index cabf8c681f..4cc1581839 100644 --- a/lms/djangoapps/courseware/features/videoalpha.py +++ b/lms/djangoapps/courseware/features/videoalpha.py @@ -9,12 +9,12 @@ from common import TEST_COURSE_NAME, TEST_SECTION_NAME, i_am_registered_for_the_ ############### ACTIONS #################### -@step('when I view the video it has autoplay enabled') +@step('when I view the Video Alpha it has autoplay enabled') def does_autoplay(step): assert(world.css_find('.videoalpha')[0]['data-autoplay'] == 'True') -@step('the course has a Video component') +@step('the course has a Video Alpha component') def view_videoalpha(step): coursename = TEST_COURSE_NAME.replace(' ', '_') i_am_registered_for_the_course(step, coursename) From a498fa52bae85ed9ae6d02714efbaaeadea9593a Mon Sep 17 00:00:00 2001 From: cahrens Date: Thu, 20 Jun 2013 11:29:31 -0400 Subject: [PATCH 277/375] Modify after UX text changes. --- cms/djangoapps/contentstore/tests/test_course_settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py index d038b9f1e2..6b8622f992 100644 --- a/cms/djangoapps/contentstore/tests/test_course_settings.py +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -138,7 +138,7 @@ class CourseDetailsTestCase(CourseTestCase): with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': True}): response = self.client.get(settings_details_url) self.assertContains(response, "Course Summary Page") - self.assertContains(response, "your course summary page will not be viewable") + self.assertContains(response, "course summary page will not be viewable") self.assertContains(response, "Course Start Date") self.assertContains(response, "Course End Date") @@ -157,7 +157,7 @@ class CourseDetailsTestCase(CourseTestCase): with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': False}): response = self.client.get(settings_details_url) self.assertContains(response, "Course Summary Page") - self.assertNotContains(response, "your course summary page will not be viewable") + self.assertNotContains(response, "course summary page will not be viewable") self.assertContains(response, "Course Start Date") self.assertContains(response, "Course End Date") From 4f78c1977f2256fc832514256f4f967040f7eaff Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Wed, 19 Jun 2013 10:59:24 -0400 Subject: [PATCH 278/375] Allow error messages with non-ascii characters to be handled correctly Also, add a test for this behavior. --- CHANGELOG.rst | 2 ++ common/lib/xmodule/xmodule/capa_module.py | 2 +- .../xmodule/xmodule/tests/test_capa_module.py | 28 +++++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 206be44c87..6a79757c0f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -42,6 +42,8 @@ setting now run entirely outside the Python sandbox. Blades: Added tests for Video Alpha player. +Common: Have the capa module handle unicode better (especially errors) + Blades: Video Alpha bug fix for speed changing to 1.0 in Firefox. Blades: Additional event tracking added to Video Alpha: fullscreen switch, show/hide diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index d9f7fc61aa..85c935c9e7 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -781,7 +781,7 @@ class CapaModule(CapaFields, XModule): # Otherwise, display just an error message, # without a stack trace else: - msg = "Error: %s" % str(inst.message) + msg = u"Error: {msg}".format(msg=inst.message) return {'success': msg} diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py index 696ef58268..85e69cabc1 100644 --- a/common/lib/xmodule/xmodule/tests/test_capa_module.py +++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """Tests of the Capa XModule""" #pylint: disable=C0111 #pylint: disable=R0904 @@ -520,6 +521,33 @@ class CapaModuleTest(unittest.TestCase): # Expect that the number of attempts is NOT incremented self.assertEqual(module.attempts, 1) + def test_check_problem_error_nonascii(self): + + # Try each exception that capa_module should handle + for exception_class in [StudentInputError, + LoncapaProblemError, + ResponseError]: + + # Create the module + module = CapaFactory.create(attempts=1) + + # Ensure that the user is NOT staff + module.system.user_is_staff = False + + # Simulate answering a problem that raises the exception + with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade: + mock_grade.side_effect = exception_class(u"ȧƈƈḗƞŧḗḓ ŧḗẋŧ ƒǿř ŧḗşŧīƞɠ") + + get_request_dict = {CapaFactory.input_key(): '3.14'} + result = module.check_problem(get_request_dict) + + # Expect an AJAX alert message in 'success' + expected_msg = u'Error: ȧƈƈḗƞŧḗḓ ŧḗẋŧ ƒǿř ŧḗşŧīƞɠ' + self.assertEqual(expected_msg, result['success']) + + # Expect that the number of attempts is NOT incremented + self.assertEqual(module.attempts, 1) + def test_check_problem_error_with_staff_user(self): # Try each exception that capa module should handle From 401dd550e477ca0616313f85aa2f64d64dc88a2b Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Tue, 18 Jun 2013 13:14:52 -0400 Subject: [PATCH 279/375] Convert many byte strings to unicode; change string formatting --- common/lib/calc/calc.py | 2 +- common/lib/xmodule/xmodule/capa_module.py | 49 +++++++++++++---------- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/common/lib/calc/calc.py b/common/lib/calc/calc.py index f0934a9ed5..bbfd9545f6 100644 --- a/common/lib/calc/calc.py +++ b/common/lib/calc/calc.py @@ -93,7 +93,7 @@ def check_variables(string, variables): Pyparsing uses a left-to-right parser, which makes a more elegant approach pretty hopeless. """ - general_whitespace = re.compile('[^\\w]+') + general_whitespace = re.compile('[^\\w]+') # TODO consider non-ascii # List of all alnums in string possible_variables = re.split(general_whitespace, string) bad_variables = [] diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 85c935c9e7..3bd8331678 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -60,7 +60,7 @@ class Randomization(String): class ComplexEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, complex): - return "{real:.7g}{imag:+.7g}*j".format(real=obj.real, imag=obj.imag) + return u"{real:.7g}{imag:+.7g}*j".format(real=obj.real, imag=obj.imag) return json.JSONEncoder.default(self, obj) @@ -167,7 +167,7 @@ class CapaModule(CapaFields, XModule): self.seed = self.lcp.seed except Exception as err: - msg = 'cannot create LoncapaProblem {loc}: {err}'.format( + msg = u'cannot create LoncapaProblem {loc}: {err}'.format( loc=self.location.url(), err=err) # TODO (vshnayder): do modules need error handlers too? # We shouldn't be switching on DEBUG. @@ -176,12 +176,15 @@ class CapaModule(CapaFields, XModule): # TODO (vshnayder): This logic should be general, not here--and may # want to preserve the data instead of replacing it. # e.g. in the CMS - msg = '

%s

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

%s

' % traceback.format_exc().replace('<', '<') + msg = u'

{msg}

'.format(msg=cgi.escape(msg)) + msg += u'

{tb}

'.format( + tb=cgi.escape(traceback.format_exc())) # create a dummy problem with error message instead of failing - problem_text = ('' - 'Problem %s has an error:%s' % - (self.location.url(), msg)) + problem_text = (u'' + u'Problem {url} has an error:{msg}'.format( + url=self.location.url(), + msg=msg) + ) self.lcp = self.new_lcp(self.get_state_for_lcp(), text=problem_text) else: # add extra info and raise @@ -362,15 +365,14 @@ class CapaModule(CapaFields, XModule): # TODO (vshnayder): another switch on DEBUG. if self.system.DEBUG: msg = ( - '[courseware.capa.capa_module] ' - 'Failed to generate HTML for problem %s' % - (self.location.url())) - msg += '

Error:

%s

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

%s

' % traceback.format_exc().replace('<', '<') + u'[courseware.capa.capa_module] ' + u'Failed to generate HTML for problem {url}'.format( + url=cgi.escape(self.location.url())) + ) + msg += u'

Error:

{msg}

'.format(msg=cgi.escape(err.message)) + msg += u'

{tb}

'.format(tb=cgi.escape(traceback.format_exc())) html = msg - # We're in non-debug mode, and possibly even in production. We want - # to avoid bricking of problem as much as possible else: # We're in non-debug mode, and possibly even in production. We want # to avoid bricking of problem as much as possible @@ -454,8 +456,9 @@ class CapaModule(CapaFields, XModule): html = self.system.render_template('problem.html', context) if encapsulate: - html = '
'.format( - id=self.location.html_id(), ajax_url=self.system.ajax_url) + html + "
" + html = u'
'.format( + id=self.location.html_id(), ajax_url=self.system.ajax_url + ) + html + "
" # now do the substitutions which are filesystem based, e.g. '/static/' prefixes return self.system.replace_urls(html) @@ -641,7 +644,8 @@ class CapaModule(CapaFields, XModule): try: new_answer = {answer_id: self.system.replace_urls(answers[answer_id])} except TypeError: - log.debug('Unable to perform URL substitution on answers[%s]: %s' % (answer_id, answers[answer_id])) + log.debug(u'Unable to perform URL substitution on answers[%s]: %s', + answer_id, answers[answer_id]) new_answer = {answer_id: answers[answer_id]} new_answers.update(new_answer) @@ -693,7 +697,7 @@ class CapaModule(CapaFields, XModule): # will return (key, '', '') # We detect this and raise an error if not name: - raise ValueError("%s must contain at least one underscore" % str(key)) + raise ValueError(u"{key} must contain at least one underscore".format(key=key)) else: # This allows for answers which require more than one value for @@ -711,7 +715,7 @@ class CapaModule(CapaFields, XModule): # If the name already exists, then we don't want # to override it. Raise an error instead if name in answers: - raise ValueError("Key %s already exists in answers dict" % str(name)) + raise ValueError(u"Key {name} already exists in answers dict".format(name=name)) else: answers[name] = val @@ -759,7 +763,8 @@ class CapaModule(CapaFields, XModule): prev_submit_time = self.lcp.get_recentmost_queuetime() waittime_between_requests = self.system.xqueue['waittime'] if (current_time - prev_submit_time).total_seconds() < waittime_between_requests: - msg = 'You must wait at least %d seconds between submissions' % waittime_between_requests + msg = u'You must wait at least {wait} seconds between submissions'.format( + wait=waittime_between_requests) return {'success': msg, 'html': ''} # Prompts a modal dialog in ajax callback try: @@ -776,7 +781,7 @@ class CapaModule(CapaFields, XModule): # the full exception, including traceback, # in the response if self.system.user_is_staff: - msg = "Staff debug info: %s" % traceback.format_exc() + msg = u"Staff debug info: {tb}".format(tb=cgi.escape(traceback.format_exc())) # Otherwise, display just an error message, # without a stack trace @@ -787,7 +792,7 @@ class CapaModule(CapaFields, XModule): except Exception as err: if self.system.DEBUG: - msg = "Error checking problem: " + str(err) + msg = "Error checking problem: " + err.message msg += '\nTraceback:\n' + traceback.format_exc() return {'success': msg} raise From b68e1e207e3fb99980de4eb8bf8b904a7ceabb13 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Tue, 18 Jun 2013 13:24:22 -0400 Subject: [PATCH 280/375] Fix some line lengths to make pylint happy --- common/lib/xmodule/xmodule/capa_module.py | 45 ++++++++++++++++------- 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 3bd8331678..40f685baee 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -22,7 +22,7 @@ from xblock.core import Scope, String, Boolean, Dict, Integer, Float from .fields import Timedelta, Date from django.utils.timezone import UTC -log = logging.getLogger("mitx.courseware") +log = logging.getLogger("mitx.courseware") # pylint: disable=C0103 # Generate this many different variants of problems with rerandomize=per_student @@ -65,17 +65,23 @@ class ComplexEncoder(json.JSONEncoder): class CapaFields(object): - attempts = Integer(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.user_state) + attempts = Integer(help="Number of attempts taken by the student on this problem", + default=0, scope=Scope.user_state) max_attempts = Integer( display_name="Maximum Attempts", - help="Defines the number of times a student can try to answer this problem. If the value is not set, infinite attempts are allowed.", + help=("Defines the number of times a student can try to answer this problem. " + "If the value is not set, infinite attempts are allowed."), values={"min": 0}, scope=Scope.settings ) due = Date(help="Date that this problem is due by", scope=Scope.settings) - graceperiod = Timedelta(help="Amount of time after the due date that submissions will be accepted", scope=Scope.settings) + graceperiod = Timedelta( + help="Amount of time after the due date that submissions will be accepted", + scope=Scope.settings + ) showanswer = String( display_name="Show Answer", - help="Defines when to show the answer to the problem. A default value can be set in Advanced Settings.", + help=("Defines when to show the answer to the problem. " + "A default value can be set in Advanced Settings."), scope=Scope.settings, default="closed", values=[ {"display_name": "Always", "value": "always"}, @@ -86,23 +92,33 @@ class CapaFields(object): {"display_name": "Past Due", "value": "past_due"}, {"display_name": "Never", "value": "never"}] ) - force_save_button = Boolean(help="Whether to force the save button to appear on the page", scope=Scope.settings, default=False) + force_save_button = Boolean( + help="Whether to force the save button to appear on the page", + scope=Scope.settings, default=False + ) rerandomize = Randomization( - display_name="Randomization", help="Defines how often inputs are randomized when a student loads the problem. This setting only applies to problems that can have randomly generated numeric values. A default value can be set in Advanced Settings.", - default="always", scope=Scope.settings, values=[{"display_name": "Always", "value": "always"}, - {"display_name": "On Reset", "value": "onreset"}, - {"display_name": "Never", "value": "never"}, - {"display_name": "Per Student", "value": "per_student"}] + display_name="Randomization", + help="Defines how often inputs are randomized when a student loads the problem. " + "This setting only applies to problems that can have randomly generated numeric values. " + "A default value can be set in Advanced Settings.", + default="always", scope=Scope.settings, values=[ + {"display_name": "Always", "value": "always"}, + {"display_name": "On Reset", "value": "onreset"}, + {"display_name": "Never", "value": "never"}, + {"display_name": "Per Student", "value": "per_student"} + ] ) data = String(help="XML data for the problem", scope=Scope.content) - correct_map = Dict(help="Dictionary with the correctness of current student answers", scope=Scope.user_state, default={}) + correct_map = Dict(help="Dictionary with the correctness of current student answers", + scope=Scope.user_state, default={}) input_state = Dict(help="Dictionary for maintaining the state of inputtypes", scope=Scope.user_state) student_answers = Dict(help="Dictionary with the current student responses", scope=Scope.user_state) done = Boolean(help="Whether the student has answered the problem", scope=Scope.user_state) seed = Integer(help="Random seed for this student", scope=Scope.user_state) weight = Float( display_name="Problem Weight", - help="Defines the number of points each problem is worth. If the value is not set, each response field in the problem is worth one point.", + help=("Defines the number of points each problem is worth. " + "If the value is not set, each response field in the problem is worth one point."), values={"min": 0, "step": .1}, scope=Scope.settings ) @@ -998,7 +1014,8 @@ class CapaDescriptor(CapaFields, RawDescriptor): mako_template = "widgets/problem-edit.html" js = {'coffee': [resource_string(__name__, 'js/src/problem/edit.coffee')]} js_module_name = "MarkdownEditingDescriptor" - css = {'scss': [resource_string(__name__, 'css/editor/edit.scss'), resource_string(__name__, 'css/problem/edit.scss')]} + css = {'scss': [resource_string(__name__, 'css/editor/edit.scss'), + resource_string(__name__, 'css/problem/edit.scss')]} # Capa modules have some additional metadata: # TODO (vshnayder): do problems have any other metadata? Do they From f623e42983545f99a0cf7bd69e7bccccb55e285e Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Tue, 18 Jun 2013 15:48:55 -0400 Subject: [PATCH 281/375] Fix formatting of docstrings; add more docstrings --- common/lib/xmodule/xmodule/capa_module.py | 170 ++++++++++++------ .../xmodule/xmodule/tests/test_capa_module.py | 16 +- 2 files changed, 126 insertions(+), 60 deletions(-) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 40f685baee..b927106b4a 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -47,7 +47,13 @@ def randomization_bin(seed, problem_id): class Randomization(String): + """ + Define a field to store how to randomize a problem. + """ def from_json(self, value): + """ + For backward compatability? + """ if value in ("", "true"): return "always" elif value == "false": @@ -58,13 +64,22 @@ class Randomization(String): class ComplexEncoder(json.JSONEncoder): + """ + Extend the JSON encoder to correctly handle complex numbers + """ def default(self, obj): + """ + Print a nicely formatted complex number, or default to the JSON encoder + """ if isinstance(obj, complex): return u"{real:.7g}{imag:+.7g}*j".format(real=obj.real, imag=obj.imag) return json.JSONEncoder.default(self, obj) class CapaFields(object): + """ + Define the possible fields for a Capa problem + """ attempts = Integer(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.user_state) max_attempts = Integer( @@ -130,12 +145,12 @@ class CapaFields(object): class CapaModule(CapaFields, XModule): - ''' + """ An XModule implementing LonCapa format problems, implemented by way of capa.capa_problem.LoncapaProblem CapaModule.__init__ takes the same arguments as xmodule.x_module:XModule.__init__ - ''' + """ icon_class = 'problem' js = {'coffee': [resource_string(__name__, 'js/src/capa/display.coffee'), @@ -150,7 +165,9 @@ class CapaModule(CapaFields, XModule): css = {'scss': [resource_string(__name__, 'css/capa/display.scss')]} def __init__(self, *args, **kwargs): - """ Accepts the same arguments as xmodule.x_module:XModule.__init__ """ + """ + Accepts the same arguments as xmodule.x_module:XModule.__init__ + """ XModule.__init__(self, *args, **kwargs) due_date = self.due @@ -211,7 +228,9 @@ class CapaModule(CapaFields, XModule): assert self.seed is not None def choose_new_seed(self): - """Choose a new seed.""" + """ + Choose a new seed. + """ if self.rerandomize == 'never': self.seed = 1 elif self.rerandomize == "per_student" and hasattr(self.system, 'seed'): @@ -225,6 +244,9 @@ class CapaModule(CapaFields, XModule): self.seed %= MAX_RANDOMIZATION_BINS def new_lcp(self, state, text=None): + """ + Generate a new Loncapa Problem + """ if text is None: text = self.data @@ -237,6 +259,9 @@ class CapaModule(CapaFields, XModule): ) def get_state_for_lcp(self): + """ + Give a dictionary holding the state of the module + """ return { 'done': self.done, 'correct_map': self.correct_map, @@ -246,6 +271,9 @@ class CapaModule(CapaFields, XModule): } def set_state_from_lcp(self): + """ + Set the module's state from the settings in `self.lcp` + """ lcp_state = self.lcp.get_state() self.done = lcp_state['done'] self.correct_map = lcp_state['correct_map'] @@ -254,26 +282,36 @@ class CapaModule(CapaFields, XModule): self.seed = lcp_state['seed'] def get_score(self): + """ + Access the problem's score + """ return self.lcp.get_score() def max_score(self): + """ + Access the problem's max score + """ return self.lcp.get_max_score() def get_progress(self): - ''' For now, just return score / max_score - ''' + """ + For now, just return score / max_score + """ d = self.get_score() score = d['score'] total = d['total'] if total > 0: try: return Progress(score, total) - except Exception: + except (TypeError, ValueError): log.exception("Got bad progress") return None return None def get_html(self): + """ + Return some html with data about the module + """ return self.system.render_template('problem_ajax.html', { 'element_id': self.location.html_id(), 'id': self.id, @@ -284,6 +322,7 @@ class CapaModule(CapaFields, XModule): def check_button_name(self): """ Determine the name for the "check" button. + Usually it is just "Check", but if this is the student's final attempt, change the name to "Final Check" """ @@ -369,12 +408,12 @@ class CapaModule(CapaFields, XModule): def handle_problem_html_error(self, err): """ - Change our problem to a dummy problem containing - a warning message to display to users. + Create a dummy problem to represent any errors. - Returns the HTML to show to users + Change our problem to a dummy problem containing a warning message to + display to users. Returns the HTML to show to users - *err* is the Exception encountered while rendering the problem HTML. + `err` is the Exception encountered while rendering the problem HTML. """ log.exception(err) @@ -434,8 +473,12 @@ class CapaModule(CapaFields, XModule): return html 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.''' + """ + Return html for the problem. + + Adds check, reset, save buttons as necessary based on the problem config + and state. + """ try: html = self.lcp.get_html() @@ -480,15 +523,16 @@ class CapaModule(CapaFields, XModule): return self.system.replace_urls(html) def handle_ajax(self, dispatch, get): - ''' + """ This is called by courseware.module_render, to handle an AJAX call. - "get" is request.POST. + + `get` is request.POST. Returns a json dictionary: { 'progress_changed' : True/False, 'progress' : 'none'/'in_progress'/'done', } - ''' + """ handlers = { 'problem_get': self.get_problem, 'problem_check': self.check_problem, @@ -527,7 +571,9 @@ class CapaModule(CapaFields, XModule): datetime.datetime.now(UTC()) > self.close_date) def closed(self): - ''' Is the student still allowed to submit answers? ''' + """ + Is the student still allowed to submit answers? + """ if self.max_attempts is not None and self.attempts >= self.max_attempts: return True if self.is_past_due(): @@ -546,18 +592,24 @@ class CapaModule(CapaFields, XModule): return self.lcp.done def is_attempted(self): - """Used by conditional module""" + """ + Has the problem been attempted? + + used by conditional module + """ return self.attempts > 0 def is_correct(self): - """True if full points""" + """ + True iff full points + """ d = self.get_score() return d['score'] == d['total'] def answer_available(self): - ''' + """ Is the user allowed to see an answer? - ''' + """ if self.showanswer == '': return False elif self.showanswer == "never": @@ -589,7 +641,7 @@ class CapaModule(CapaFields, XModule): Delivers grading response (e.g. from asynchronous code checking) to the capa problem, so its score can be updated - 'get' must have a field 'response' which is a string that contains the + `get` must have a field `response` which is a string that contains the grader's response No ajax return is needed. Return empty dict. @@ -603,7 +655,7 @@ class CapaModule(CapaFields, XModule): return dict() # No AJAX return is needed def handle_ungraded_response(self, get): - ''' + """ Delivers a response from the XQueue to the capa problem The score of the problem will not be updated @@ -616,7 +668,7 @@ class CapaModule(CapaFields, XModule): empty dictionary No ajax return is needed, so an empty dict is returned - ''' + """ queuekey = get['queuekey'] score_msg = get['xqueue_body'] # pass along the xqueue message to the problem @@ -625,25 +677,25 @@ class CapaModule(CapaFields, XModule): return dict() def handle_input_ajax(self, get): - ''' + """ Handle ajax calls meant for a particular input in the problem Args: - get (dict) - data that should be passed to the input Returns: - dict containing the response from the input - ''' + """ response = self.lcp.handle_input_ajax(get) # save any state changes that may occur self.set_state_from_lcp() return response def get_answer(self, get): - ''' + """ For the "show answer" button. Returns the answers: {'answers' : answers} - ''' + """ event_info = dict() event_info['problem_id'] = self.location.url() self.system.track_function('showanswer', event_info) @@ -669,40 +721,44 @@ class CapaModule(CapaFields, XModule): # Figure out if we should move these to capa_problem? def get_problem(self, get): - ''' Return results of get_problem_html, as a simple dict for json-ing. + """ + 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. - ''' + 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 (Django QueryDict). + """ + Make dictionary of student responses (aka "answers") - The *get* dict has keys of the form 'x_y', which are mapped + `get` is POST dictionary (Django QueryDict). + + The `get` dict has keys of the form 'x_y', which are mapped to key 'y' in the returned dict. For example, 'input_1_2_3' would be mapped to '1_2_3' in the returned dict. Some inputs always expect a list in the returned dict (e.g. checkbox inputs). The convention is that - keys in the *get* dict that end with '[]' will always + keys in the `get` dict that end with '[]' will always have list values in the returned dict. - For example, if the *get* dict contains {'input_1[]': 'test' } + For example, if the `get` dict contains {'input_1[]': 'test' } then the output dict would contain {'1': ['test'] } (the value is a list). Raises an exception if: - A key in the *get* dictionary does not contain >= 1 underscores - (e.g. "input" is invalid; "input_1" is valid) + -A key in the `get` dictionary does not contain at least one underscore + (e.g. "input" is invalid, but "input_1" is valid) - Two keys end up with the same name in the returned dict. - (e.g. 'input_1' and 'input_1[]', which both get mapped - to 'input_1' in the returned dict) - ''' + -Two keys end up with the same name in the returned dict. + (e.g. 'input_1' and 'input_1[]', which both get mapped to 'input_1' + in the returned dict) + """ answers = dict() for key in get: @@ -749,12 +805,13 @@ class CapaModule(CapaFields, XModule): }) def check_problem(self, get): - ''' Checks whether answers to a problem are correct, and - returns a map of correct/incorrect answers: + """ + Checks whether answers to a problem are correct - {'success' : 'correct' | 'incorrect' | AJAX alert msg string, - 'contents' : html} - ''' + Returns a map of correct/incorrect answers: + {'success' : 'correct' | 'incorrect' | AJAX alert msg string, + 'contents' : html} + """ event_info = dict() event_info['state'] = self.lcp.get_state() event_info['problem_id'] = self.location.url() @@ -958,16 +1015,17 @@ class CapaModule(CapaFields, XModule): 'msg': msg} def reset_problem(self, get): - ''' Changes problem state to unfinished -- removes student answers, - and causes problem to rerender itself. + """ + Changes problem state to unfinished -- removes student answers, + and causes problem to rerender itself. - Returns a dictionary of the form: - {'success': True/False, - 'html': Problem HTML string } + Returns a dictionary of the form: + {'success': True/False, + 'html': Problem HTML string } - If an error occurs, the dictionary will also have an - 'error' key containing an error message. - ''' + If an error occurs, the dictionary will also have an + `error` key containing an error message. + """ event_info = dict() event_info['old_state'] = self.lcp.get_state() event_info['problem_id'] = self.location.url() diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py index 85e69cabc1..81df686015 100644 --- a/common/lib/xmodule/xmodule/tests/test_capa_module.py +++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- -"""Tests of the Capa XModule""" +""" +Tests of the Capa XModule +""" #pylint: disable=C0111 #pylint: disable=R0904 #pylint: disable=C0103 @@ -48,12 +50,16 @@ class CapaFactory(object): @staticmethod def input_key(): - """ Return the input key to use when passing GET parameters """ + """ + Return the input key to use when passing GET parameters + """ return ("input_" + CapaFactory.answer_key()) @staticmethod def answer_key(): - """ Return the key stored in the capa problem answer dict """ + """ + Return the key stored in the capa problem answer dict + """ return ("-".join(['i4x', 'edX', 'capa_test', 'problem', 'SampleProblem%d' % CapaFactory.num]) + "_2_1") @@ -362,7 +368,9 @@ class CapaModuleTest(unittest.TestCase): result = CapaModule.make_dict_of_responses(invalid_get_dict) def _querydict_from_dict(self, param_dict): - """ Create a Django QueryDict from a Python dictionary """ + """ + Create a Django QueryDict from a Python dictionary + """ # QueryDict objects are immutable by default, so we make # a copy that we can update. From ea56a0cd4a75c48e23f7d6dce41d6a635ff280e7 Mon Sep 17 00:00:00 2001 From: Peter Fogg Date: Thu, 20 Jun 2013 11:58:01 -0400 Subject: [PATCH 282/375] Make sure we can handle empty youtube attributes. --- common/lib/xmodule/xmodule/tests/test_video_xml.py | 11 +++++++++++ common/lib/xmodule/xmodule/video_module.py | 2 ++ 2 files changed, 13 insertions(+) diff --git a/common/lib/xmodule/xmodule/tests/test_video_xml.py b/common/lib/xmodule/xmodule/tests/test_video_xml.py index f2ed730666..081870792c 100644 --- a/common/lib/xmodule/xmodule/tests/test_video_xml.py +++ b/common/lib/xmodule/xmodule/tests/test_video_xml.py @@ -110,3 +110,14 @@ class VideoModuleLogicTest(LogicTest): youtube_str = '1.00:p2Q6BrNhdh8' youtube_str_hack = '1.0:p2Q6BrNhdh8' self.assertEqual(_parse_youtube(youtube_str), _parse_youtube(youtube_str_hack)) + + def test_parse_youtube_empty(self): + """ + Some courses have empty youtube attributes, so we should handle + that well. + """ + self.assertEqual(_parse_youtube(''), + {'0.75': '', + '1.00': '', + '1.25': '', + '1.50': ''}) diff --git a/common/lib/xmodule/xmodule/video_module.py b/common/lib/xmodule/xmodule/video_module.py index dbb35816db..6344da7994 100644 --- a/common/lib/xmodule/xmodule/video_module.py +++ b/common/lib/xmodule/xmodule/video_module.py @@ -168,6 +168,8 @@ def _parse_youtube(data): XML-based courses. """ ret = {'0.75': '', '1.00': '', '1.25': '', '1.50': ''} + if data == '': + return ret videos = data.split(',') for video in videos: pieces = video.split(':') From 9a753f1cc529e3a1fe0b9a14c48342958daca8ca Mon Sep 17 00:00:00 2001 From: cahrens Date: Thu, 20 Jun 2013 12:03:59 -0400 Subject: [PATCH 283/375] Studio feature to disable course settings on Drupal site. --- CHANGELOG.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9c405ed365..3bc3b6b9bf 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,6 +11,9 @@ XModule: Don't delete generated xmodule asset files when compiling (for instance, when XModule provides a coffeescript file, don't delete the associated javascript) +Studio: For courses running on edx.org (marketing site), disable fields in +Course Settings that do not apply. + Common: Make asset watchers run as singletons (so they won't start if the watcher is already running in another shell). From b7b2f91e7914af462519afcc909404eaee94cc08 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Thu, 20 Jun 2013 12:30:25 -0400 Subject: [PATCH 284/375] studio - revising the visual design of UI well pattern --- common/static/sass/_mixins.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/static/sass/_mixins.scss b/common/static/sass/_mixins.scss index c26738a1b7..e5548aeaaa 100644 --- a/common/static/sass/_mixins.scss +++ b/common/static/sass/_mixins.scss @@ -192,7 +192,7 @@ // UI archetypes - well .ui-well { - @include box-shadow(inset 0 1px 2px 1px $shadow-l1); + @include box-shadow(inset 0 1px 2px 1px $shadow); padding: ($baseline*0.75); } From 83d84e2b65582c55a636de27077d7f6689666bff Mon Sep 17 00:00:00 2001 From: Peter Fogg Date: Thu, 20 Jun 2013 12:26:01 -0400 Subject: [PATCH 285/375] Actually fix merge conflicts. --- .../courseware/features/videoalpha.feature | 6 -- .../courseware/features/videoalpha.py | 36 ---------- lms/templates/video.html | 65 ++++--------------- 3 files changed, 11 insertions(+), 96 deletions(-) delete mode 100644 lms/djangoapps/courseware/features/videoalpha.feature delete mode 100644 lms/djangoapps/courseware/features/videoalpha.py diff --git a/lms/djangoapps/courseware/features/videoalpha.feature b/lms/djangoapps/courseware/features/videoalpha.feature deleted file mode 100644 index 8cff43f45f..0000000000 --- a/lms/djangoapps/courseware/features/videoalpha.feature +++ /dev/null @@ -1,6 +0,0 @@ -Feature: Video Alpha component - As a student, I want to view course videos in LMS. - - Scenario: Autoplay is enabled in LMS - Given the course has a Video Alpha component - Then when I view the Video Alpha it has autoplay enabled diff --git a/lms/djangoapps/courseware/features/videoalpha.py b/lms/djangoapps/courseware/features/videoalpha.py deleted file mode 100644 index 4cc1581839..0000000000 --- a/lms/djangoapps/courseware/features/videoalpha.py +++ /dev/null @@ -1,36 +0,0 @@ -#pylint: disable=C0111 -#pylint: disable=W0613 -#pylint: disable=W0621 - -from lettuce import world, step -from lettuce.django import django_url -from common import TEST_COURSE_NAME, TEST_SECTION_NAME, i_am_registered_for_the_course, section_location - -############### ACTIONS #################### - - -@step('when I view the Video Alpha it has autoplay enabled') -def does_autoplay(step): - assert(world.css_find('.videoalpha')[0]['data-autoplay'] == 'True') - - -@step('the course has a Video Alpha component') -def view_videoalpha(step): - coursename = TEST_COURSE_NAME.replace(' ', '_') - i_am_registered_for_the_course(step, coursename) - - # Make sure we have a videoalpha - add_videoalpha_to_course(coursename) - chapter_name = TEST_SECTION_NAME.replace(" ", "_") - section_name = chapter_name - url = django_url('/courses/edx/Test_Course/Test_Course/courseware/%s/%s' % - (chapter_name, section_name)) - - world.browser.visit(url) - - -def add_videoalpha_to_course(course): - template_name = 'i4x://edx/templates/videoalpha/default' - world.ItemFactory.create(parent_location=section_location(course), - template=template_name, - display_name='Video Alpha 1') diff --git a/lms/templates/video.html b/lms/templates/video.html index b8854965ce..77c8a5ee16 100644 --- a/lms/templates/video.html +++ b/lms/templates/video.html @@ -2,49 +2,6 @@

${display_name}

% endif -<<<<<<< HEAD -%if settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']: -
-
-
-
-
-
    -
  • -
  • -
    0:00 / 0:00
    -
  • -
- -
-
-
-
-%elif settings.MITX_FEATURES.get('USE_YOUTUBE_OBJECT_API') and youtube_id_1_0: - - - - - -%else: -
-======= %if settings.MITX_FEATURES.get('USE_YOUTUBE_OBJECT_API') and normal_speed_video_id: %else: -
->>>>>>> master +
From 5f0a89bc271aa0f324d8eb35dcb4bc0f7a838ea6 Mon Sep 17 00:00:00 2001 From: cahrens Date: Thu, 20 Jun 2013 13:16:13 -0400 Subject: [PATCH 286/375] Don't mention edge in the e-mail; same text used for edge and edx. --- cms/templates/emails/activation_email.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/templates/emails/activation_email.txt b/cms/templates/emails/activation_email.txt index 5a1d63b670..4badb4ca88 100644 --- a/cms/templates/emails/activation_email.txt +++ b/cms/templates/emails/activation_email.txt @@ -1,4 +1,4 @@ -Thank you for signing up for edX edge! To activate your account, +Thank you for signing up for edX Studio! To activate your account, please copy and paste this address into your web browser's address bar: From 96c4d2877f9e671d9dea3c5455ac65c441550edc Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 20 Jun 2013 14:05:00 -0400 Subject: [PATCH 287/375] Navigation now has a click success condition --- lms/djangoapps/courseware/features/navigation.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/courseware/features/navigation.py b/lms/djangoapps/courseware/features/navigation.py index 88c540b232..d5e56e7460 100644 --- a/lms/djangoapps/courseware/features/navigation.py +++ b/lms/djangoapps/courseware/features/navigation.py @@ -84,8 +84,11 @@ def click_on_section(step, section): subid = "ui-accordion-accordion-panel-" + str(int(section) - 1) subsection_css = 'ul.ui-accordion-content-active[id=\'%s\'][aria-expanded=\'true\']> li > a' % subid + prev_url = world.browser.url + changed_section = lambda: prev_url != world.browser.url + #for some reason needed to do it in two steps - world.css_click(subsection_css) + world.css_click(subsection_css, success_condition=changed_section) @step(u'I click on subsection "([^"]*)"$') From eec095a195b12e44905fbf245d1aded0ce74f7e2 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 20 Jun 2013 15:38:56 -0400 Subject: [PATCH 288/375] Changed naming of attempts to max_attempts and changed css_selector --- common/djangoapps/terrain/ui_helpers.py | 8 ++++---- lms/djangoapps/courseware/features/navigation.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py index 6e711a5137..6adaf5db89 100644 --- a/common/djangoapps/terrain/ui_helpers.py +++ b/common/djangoapps/terrain/ui_helpers.py @@ -58,7 +58,7 @@ def css_find(css, wait_time=5): @world.absorb -def css_click(css_selector, index=0, attempts=5, success_condition=lambda: True): +def css_click(css_selector, index=0, max_attempts=5, success_condition=lambda: True): """ Perform a click on a CSS selector, retrying if it initially fails. @@ -72,7 +72,7 @@ def css_click(css_selector, index=0, attempts=5, success_condition=lambda: True) assert is_css_present(css_selector) attempt = 0 result = False - while attempt < attempts: + while attempt < max_attempts: try: world.css_find(css_selector)[index].click() if success_condition(): @@ -90,7 +90,7 @@ def css_click(css_selector, index=0, attempts=5, success_condition=lambda: True) @world.absorb -def css_check(css_selector, index=0, attempts=5, success_condition=lambda: True): +def css_check(css_selector, index=0, max_attempts=5, success_condition=lambda: True): """ Checks a check box based on a CSS selector, retrying if it initially fails. @@ -104,7 +104,7 @@ def css_check(css_selector, index=0, attempts=5, success_condition=lambda: True) assert is_css_present(css_selector) attempt = 0 result = False - while attempt < attempts: + while attempt < max_attempts: try: world.css_find(css_selector)[index].check() if success_condition(): diff --git a/lms/djangoapps/courseware/features/navigation.py b/lms/djangoapps/courseware/features/navigation.py index d5e56e7460..96d5a3de93 100644 --- a/lms/djangoapps/courseware/features/navigation.py +++ b/lms/djangoapps/courseware/features/navigation.py @@ -83,7 +83,7 @@ def click_on_section(step, section): world.css_click(section_css) subid = "ui-accordion-accordion-panel-" + str(int(section) - 1) - subsection_css = 'ul.ui-accordion-content-active[id=\'%s\'][aria-expanded=\'true\']> li > a' % subid + subsection_css = 'ul.ui-accordion-content-active[id=\'%s\'] > li > a' % subid prev_url = world.browser.url changed_section = lambda: prev_url != world.browser.url From 281f9003894860443c54a7e9b990b77bad71e675 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 20 Jun 2013 15:40:58 -0400 Subject: [PATCH 289/375] Changed format of docstring --- lms/djangoapps/courseware/features/problems_setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/courseware/features/problems_setup.py b/lms/djangoapps/courseware/features/problems_setup.py index b8f817f933..0438b82fa2 100644 --- a/lms/djangoapps/courseware/features/problems_setup.py +++ b/lms/djangoapps/courseware/features/problems_setup.py @@ -281,11 +281,11 @@ def add_problem_to_course(course, problem_type, extraMeta=None): def inputfield(problem_type, choice=None, input_num=1): - """ Return the css element for *problem_type*. + """ Return the css selector for `problem_type`. For example, if problem_type is 'string', return the text field for the string problem in the test course. - *choice* is the name of the checkbox input in a group + `choice` is the name of the checkbox input in a group of checkboxes. """ sel = ("input#input_i4x-edx-model_course-problem-%s_2_%s" % From b9d79aea605722d177dd3d9b56d2bf23b56ec7f1 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 20 Jun 2013 15:55:53 -0400 Subject: [PATCH 290/375] A bunch of pylint fixes and explicit use case for uploads/test --- .../contentstore/features/common.py | 25 ++++++++++--------- .../contentstore/features/course-team.py | 16 ++++++------ .../contentstore/features/course-updates.py | 18 ++++++------- .../contentstore/features/static-pages.py | 10 ++++---- .../contentstore/features/upload.py | 16 ++++++------ common/test/data/uploads/test | 2 +- 6 files changed, 44 insertions(+), 43 deletions(-) diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index 0b7cb11d2a..e126b746c5 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -3,7 +3,6 @@ from lettuce import world, step from nose.tools import assert_true -from nose.tools import assert_equal from auth.authz import get_user_by_email @@ -13,12 +12,13 @@ import time from logging import getLogger logger = getLogger(__name__) -COURSE_NAME = 'Robot Super Course' -COURSE_NUM = '999' -COURSE_ORG = 'MITx' +_COURSE_NAME = 'Robot Super Course' +_COURSE_NUM = '999' +_COURSE_ORG = 'MITx' ########### STEP HELPERS ############## + @step('I (?:visit|access|open) the Studio homepage$') def i_visit_the_studio_homepage(_step): # To make this go to port 8001, put @@ -78,10 +78,11 @@ def create_studio_user( registration.register(studio_user) registration.activate() + def fill_in_course_info( - name=COURSE_NAME, - org=COURSE_ORG, - num=COURSE_NUM): + name=_COURSE_NAME, + org=_COURSE_ORG, + num=_COURSE_NUM): world.css_fill('.new-course-name', name) world.css_fill('.new-course-org', org) world.css_fill('.new-course-number', num) @@ -108,14 +109,14 @@ def log_into_studio( def create_a_course(): - c = world.CourseFactory.create(org=COURSE_ORG, course=COURSE_NUM, display_name=COURSE_NAME) + world.CourseFactory.create(org=_COURSE_ORG, course=_COURSE_NUM, display_name=_COURSE_NAME) # Add the user to the instructor group of the course # so they will have the permissions to see it in studio - g = world.GroupFactory.create(name='instructor_MITx/{course_num}/{course_name}'.format(course_num=COURSE_NUM, course_name=COURSE_NAME.replace(" ", "_"))) - u = get_user_by_email('robot+studio@edx.org') - u.groups.add(g) - u.save() + course = world.GroupFactory.create(name='instructor_MITx/{course_num}/{course_name}'.format(course_num=_COURSE_NUM, course_name=_COURSE_NAME.replace(" ", "_"))) + user = get_user_by_email('robot+studio@edx.org') + user.groups.add(course) + user.save() world.browser.reload() course_link_css = 'span.class-name' diff --git a/cms/djangoapps/contentstore/features/course-team.py b/cms/djangoapps/contentstore/features/course-team.py index 439eccb265..15c9e5169d 100644 --- a/cms/djangoapps/contentstore/features/course-team.py +++ b/cms/djangoapps/contentstore/features/course-team.py @@ -9,19 +9,19 @@ EMAIL_EXTENSION = '@edx.org' @step(u'I am viewing the course team settings') -def view_grading_settings(step): +def view_grading_settings(_step): world.click_course_settings() link_css = 'li.nav-course-settings-team a' world.css_click(link_css) @step(u'The user "([^"]*)" exists$') -def create_other_user(step, name): +def create_other_user(_step, name): create_studio_user(uname=name, password=PASSWORD, email=(name + EMAIL_EXTENSION)) @step(u'I add "([^"]*)" to the course team') -def add_other_user(step, name): +def add_other_user(_step, name): new_user_css = 'a.new-user-button' world.css_click(new_user_css) @@ -34,18 +34,18 @@ def add_other_user(step, name): @step(u'I delete "([^"]*)" from the course team') -def delete_other_user(step, name): +def delete_other_user(_step, name): to_delete_css = '.remove-user[data-id="{name}{extension}"]'.format(name=name, extension=EMAIL_EXTENSION) world.css_click(to_delete_css) @step(u'"([^"]*)" logs in$') -def other_user_login(step, name): +def other_user_login(_step, name): log_into_studio(uname=name, password=PASSWORD, email=name + EMAIL_EXTENSION) @step(u'He does( not)? see the course on his page') -def see_course(step, doesnt_see_course): +def see_course(_step, doesnt_see_course): class_css = '.class-name' all_courses = world.css_find(class_css) all_names = [item.html for item in all_courses] @@ -56,12 +56,12 @@ def see_course(step, doesnt_see_course): @step(u'He cannot delete users') -def cannot_delete(step): +def cannot_delete(_step): to_delete_css = '.remove-user' assert world.is_css_not_present(to_delete_css) @step(u'He cannot add users') -def cannot_add(step): +def cannot_add(_step): add_css = '.new-user' assert world.is_css_not_present(add_css) diff --git a/cms/djangoapps/contentstore/features/course-updates.py b/cms/djangoapps/contentstore/features/course-updates.py index 3bbdd75d26..e742b6a40c 100644 --- a/cms/djangoapps/contentstore/features/course-updates.py +++ b/cms/djangoapps/contentstore/features/course-updates.py @@ -7,7 +7,7 @@ from common import type_in_codemirror @step(u'I go to the course updates page') -def go_to_uploads(step): +def go_to_uploads(_step): menu_css = 'li.nav-course-courseware' uploads_css = '.nav-course-courseware-updates' world.css_click(menu_css) @@ -15,14 +15,14 @@ def go_to_uploads(step): @step(u'I add a new update with the text "([^"]*)"$') -def add_update(step, text): +def add_update(_step, text): update_css = '.new-update-button' world.css_click(update_css) change_text(text) @step(u'I should( not)? see the update "([^"]*)"$') -def check_update(step, doesnt_see_update, text): +def check_update(_step, doesnt_see_update, text): update_css = '.update-contents' update = world.css_find(update_css) if doesnt_see_update: @@ -32,20 +32,20 @@ def check_update(step, doesnt_see_update, text): @step(u'I modify the text to "([^"]*)"$') -def modify_update(step, text): +def modify_update(_step, text): button_css = '.post-preview .edit-button' world.css_click(button_css) change_text(text) @step(u'I delete the update$') -def click_button(step): +def click_button(_step): button_css = '.post-preview .delete-button' world.css_click(button_css) @step(u'I edit the date to "([^"]*)"$') -def change_date(step, new_date): +def change_date(_step, new_date): button_css = '.post-preview .edit-button' world.css_click(button_css) date_css = 'input.date' @@ -58,21 +58,21 @@ def change_date(step, new_date): @step(u'I should see the date "([^"]*)"$') -def check_date(step, date): +def check_date(_step, date): date_css = '.date-display' date_html = world.css_find(date_css) assert date == date_html.html @step(u'I modify the handout to "([^"]*)"$') -def edit_handouts(step, text): +def edit_handouts(_step, text): edit_css = '.course-handouts > .edit-button' world.css_click(edit_css) change_text(text) @step(u'I see the handout "([^"]*)"$') -def check_handout(step, handout): +def check_handout(_step, handout): handout_css = '.handouts-content' handouts = world.css_find(handout_css) assert handout in handouts.html diff --git a/cms/djangoapps/contentstore/features/static-pages.py b/cms/djangoapps/contentstore/features/static-pages.py index 56d12f18aa..23656690fc 100644 --- a/cms/djangoapps/contentstore/features/static-pages.py +++ b/cms/djangoapps/contentstore/features/static-pages.py @@ -6,7 +6,7 @@ from selenium.webdriver.common.keys import Keys @step(u'I go to the static pages page') -def go_to_uploads(step): +def go_to_uploads(_step): menu_css = 'li.nav-course-courseware' uploads_css = '.nav-course-courseware-pages' world.css_find(menu_css).click() @@ -14,13 +14,13 @@ def go_to_uploads(step): @step(u'I add a new page') -def add_page(step): +def add_page(_step): button_css = '.new-button' world.css_find(button_css).click() @step(u'I should( not)? see a "([^"]*)" static page$') -def see_page(step, doesnt, page): +def see_page(_step, doesnt, page): index = get_index(page) if doesnt: assert index == -1 @@ -29,7 +29,7 @@ def see_page(step, doesnt, page): @step(u'I "([^"]*)" the "([^"]*)" page$') -def click_edit_delete(step, edit_delete, page): +def click_edit_delete(_step, edit_delete, page): button_css = '.%s-button' % edit_delete index = get_index(page) assert index != -1 @@ -37,7 +37,7 @@ def click_edit_delete(step, edit_delete, page): @step(u'I change the name to "([^"]*)"$') -def change_name(step, new_name): +def change_name(_step, new_name): settings_css = '#settings-mode' world.css_find(settings_css).click() input_css = '.setting-input' diff --git a/cms/djangoapps/contentstore/features/upload.py b/cms/djangoapps/contentstore/features/upload.py index 9b049ccc78..7ef782ea13 100644 --- a/cms/djangoapps/contentstore/features/upload.py +++ b/cms/djangoapps/contentstore/features/upload.py @@ -13,7 +13,7 @@ HTTP_PREFIX = "http://localhost:8001" @step(u'I go to the files and uploads page') -def go_to_uploads(step): +def go_to_uploads(_step): menu_css = 'li.nav-course-courseware' uploads_css = '.nav-course-courseware-uploads' world.css_find(menu_css).click() @@ -21,7 +21,7 @@ def go_to_uploads(step): @step(u'I upload the file "([^"]*)"$') -def upload_file(step, file_name): +def upload_file(_step, file_name): upload_css = '.upload-button' world.css_find(upload_css).click() @@ -36,7 +36,7 @@ def upload_file(step, file_name): @step(u'I should( not)? see the file "([^"]*)" was uploaded$') -def check_upload(step, do_not_see_file, file_name): +def check_upload(_step, do_not_see_file, file_name): index = get_index(file_name) if do_not_see_file: assert index == -1 @@ -45,13 +45,13 @@ def check_upload(step, do_not_see_file, file_name): @step(u'The url for the file "([^"]*)" is valid$') -def check_url(step, file_name): +def check_url(_step, file_name): r = get_file(file_name) assert r.status_code == 200 @step(u'I delete the file "([^"]*)"$') -def delete_file(step, file_name): +def delete_file(_step, file_name): index = get_index(file_name) assert index != -1 delete_css = ".remove-asset-button" @@ -62,7 +62,7 @@ def delete_file(step, file_name): @step(u'I should see only one "([^"]*)"$') -def no_duplicate(step, file_name): +def no_duplicate(_step, file_name): names_css = '.name-col > a.filename' all_names = world.css_find(names_css) only_one = False @@ -73,7 +73,7 @@ def no_duplicate(step, file_name): @step(u'I can download the correct "([^"]*)" file$') -def check_download(step, file_name): +def check_download(_step, file_name): path = os.path.join(TEST_ROOT, 'uploads/', file_name) with open(os.path.abspath(path), 'r') as cur_file: cur_text = cur_file.read() @@ -83,7 +83,7 @@ def check_download(step, file_name): @step(u'I modify "([^"]*)"$') -def modify_upload(step, file_name): +def modify_upload(_step, file_name): new_text = ''.join(random.choice(string.ascii_uppercase + string.digits) for x in range(10)) path = os.path.join(TEST_ROOT, 'uploads/', file_name) with open(os.path.abspath(path), 'w') as cur_file: diff --git a/common/test/data/uploads/test b/common/test/data/uploads/test index 27bb8ecaac..0424951e34 100644 --- a/common/test/data/uploads/test +++ b/common/test/data/uploads/test @@ -1 +1 @@ -R22VMJ2M \ No newline at end of file +This is an arbitrary file for testing uploads From 34f8c044b8a8f97d9a97e72cd0f7981a3ea0e5f3 Mon Sep 17 00:00:00 2001 From: cahrens Date: Thu, 20 Jun 2013 16:06:00 -0400 Subject: [PATCH 291/375] Pep8 cleaning. --- .../features/advanced-settings.py | 6 +++-- .../contentstore/tests/test_checklists.py | 2 -- .../tests/test_course_settings.py | 22 ++++++++++++++----- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/cms/djangoapps/contentstore/features/advanced-settings.py b/cms/djangoapps/contentstore/features/advanced-settings.py index 473fc20a68..2360baea5a 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.py +++ b/cms/djangoapps/contentstore/features/advanced-settings.py @@ -31,8 +31,10 @@ def press_the_notification_button(step, name): # Save was clicked if either the save notification bar is gone, or we have a error notification # overlaying it (expected in the case of typing Object into display_name). - save_clicked = lambda: world.is_css_not_present('.is-shown.wrapper-notification-warning') or\ - world.is_css_present('.is-shown.wrapper-notification-error') + def save_clicked(): + confirmation_dismissed = world.is_css_not_present('.is-shown.wrapper-notification-warning') + error_showing = world.is_css_present('.is-shown.wrapper-notification-error') + return confirmation_dismissed or error_showing assert_true(world.css_click(css, success_condition=save_clicked), 'Save button not clicked after 5 attempts.') diff --git a/cms/djangoapps/contentstore/tests/test_checklists.py b/cms/djangoapps/contentstore/tests/test_checklists.py index 0e5cd9b884..52e9ba14fe 100644 --- a/cms/djangoapps/contentstore/tests/test_checklists.py +++ b/cms/djangoapps/contentstore/tests/test_checklists.py @@ -19,7 +19,6 @@ class ChecklistTestCase(CourseTestCase): modulestore = get_modulestore(self.course.location) return modulestore.get_item(self.course.location).checklists - def compare_checklists(self, persisted, request): """ Handles url expansion as possible difference and descends into guts @@ -99,7 +98,6 @@ class ChecklistTestCase(CourseTestCase): 'name': self.course.location.name, 'checklist_index': 2}) - def get_first_item(checklist): return checklist['items'][0] diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py index 6b8622f992..5c2a15ac87 100644 --- a/cms/djangoapps/contentstore/tests/test_course_settings.py +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -131,9 +131,14 @@ class CourseDetailsTestCase(CourseTestCase): @override_settings(MKTG_URLS={'ROOT': 'dummy-root'}) def test_marketing_site_fetch(self): - settings_details_url = reverse('settings_details', - kwargs={'org': self.course_location.org, 'name': self.course_location.name, - 'course': self.course_location.course}) + settings_details_url = reverse( + 'settings_details', + kwargs={ + 'org': self.course_location.org, + 'name': self.course_location.name, + 'course': self.course_location.course + } + ) with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': True}): response = self.client.get(settings_details_url) @@ -150,9 +155,14 @@ class CourseDetailsTestCase(CourseTestCase): self.assertNotContains(response, "Requirements") def test_regular_site_fetch(self): - settings_details_url = reverse('settings_details', - kwargs={'org': self.course_location.org, 'name': self.course_location.name, - 'course': self.course_location.course}) + settings_details_url = reverse( + 'settings_details', + kwargs={ + 'org': self.course_location.org, + 'name': self.course_location.name, + 'course': self.course_location.course + } + ) with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': False}): response = self.client.get(settings_details_url) From 106e0aae01931920bdd0465fbe11ace727410e71 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Mon, 10 Jun 2013 09:02:22 -0400 Subject: [PATCH 292/375] Clean tests step now removes stale coverage report files. Added test cleaning to fasttest_* instead of test_*, so that it always gets executed for lms and cms tests. Added comment to `directory` command for tests.rake Separated cleaning of test fixtures from cleaning of reports directory to resolve conflicts with collectstatic. --- rakelib/tests.rake | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/rakelib/tests.rake b/rakelib/tests.rake index 20bd34f4e8..f169d28256 100644 --- a/rakelib/tests.rake +++ b/rakelib/tests.rake @@ -1,6 +1,9 @@ # Set up the clean and clobber tasks CLOBBER.include(REPORT_DIR, 'test_root/*_repo', 'test_root/staticfiles') +# Create the directory to hold coverage reports, if it doesn't already exist. +directory REPORT_DIR + def run_under_coverage(cmd, root) cmd0, cmd_rest = cmd.split(" ", 2) # We use "python -m coverage" so that the proper python will run the importable coverage @@ -45,12 +48,19 @@ task :test_docs do test_sh('rake builddocs[pub]') end -directory REPORT_DIR - task :clean_test_files do + desc "Clean fixture files used by tests" sh("git clean -fqdx test_root") end +task :clean_reports_dir do + desc "Clean coverage files, to ensure that we don't use stale data to generate reports." + # We delete the files but preserve the directory structure + # so that coverage.py has a place to put the reports. + sh("find #{REPORT_DIR} -type f -delete") +end + + TEST_TASK_DIRS = [] [:lms, :cms].each do |system| @@ -58,21 +68,21 @@ TEST_TASK_DIRS = [] # Per System tasks desc "Run all django tests on our djangoapps for the #{system}" - task "test_#{system}", [:test_id] => ["clean_test_files", :predjango, "#{system}:gather_assets:test", "fasttest_#{system}"] + task "test_#{system}", [:test_id] => [:clean_test_files, :predjango, "#{system}:gather_assets:test", "fasttest_#{system}"] # Have a way to run the tests without running collectstatic -- useful when debugging without # messing with static files. - task "fasttest_#{system}", [:test_id] => [report_dir, :install_prereqs, :predjango] do |t, args| + task "fasttest_#{system}", [:test_id] => [report_dir, :clean_reports_dir, :install_prereqs, :predjango] do |t, args| args.with_defaults(:test_id => nil) run_tests(system, report_dir, args.test_id) end # Run acceptance tests desc "Run acceptance tests" - task "test_acceptance_#{system}", [:harvest_args] => ["#{system}:gather_assets:acceptance", "fasttest_acceptance_#{system}"] + task "test_acceptance_#{system}", [:harvest_args] => [:clean_test_files, "#{system}:gather_assets:acceptance", "fasttest_acceptance_#{system}"] desc "Run acceptance tests without collectstatic" - task "fasttest_acceptance_#{system}", [:harvest_args] => ["clean_test_files", :predjango, report_dir] do |t, args| + task "fasttest_acceptance_#{system}", [:harvest_args] => [:clean_reports_dir, :predjango, report_dir] do |t, args| args.with_defaults(:harvest_args => '') run_acceptance_tests(system, report_dir, args.harvest_args) end @@ -88,7 +98,7 @@ Dir["common/lib/*"].select{|lib| File.directory?(lib)}.each do |lib| report_dir = report_dir_path(lib) desc "Run tests for common lib #{lib}" - task "test_#{lib}" => ["clean_test_files", report_dir] do + task "test_#{lib}" => [:clean_reports_dir, report_dir] do ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml") cmd = "nosetests #{lib}" test_sh(run_under_coverage(cmd, lib)) From 70d48e2e9fd20d7eac0289fccb24210d68c8af06 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 20 Jun 2013 16:40:13 -0400 Subject: [PATCH 293/375] Now referencing css element --- .../contentstore/features/course-team.py | 14 +++++----- .../contentstore/features/course-updates.py | 26 +++++++++---------- .../contentstore/features/static-pages.py | 14 +++++----- .../contentstore/features/upload.py | 15 ++++++----- common/test/data/uploads/test | 2 +- 5 files changed, 36 insertions(+), 35 deletions(-) diff --git a/cms/djangoapps/contentstore/features/course-team.py b/cms/djangoapps/contentstore/features/course-team.py index 15c9e5169d..4303d5066c 100644 --- a/cms/djangoapps/contentstore/features/course-team.py +++ b/cms/djangoapps/contentstore/features/course-team.py @@ -2,7 +2,7 @@ #pylint: disable=W0621 from lettuce import world, step -from common import create_studio_user, log_into_studio, COURSE_NAME +from common import create_studio_user, log_into_studio, _COURSE_NAME PASSWORD = 'test' EMAIL_EXTENSION = '@edx.org' @@ -35,7 +35,7 @@ def add_other_user(_step, name): @step(u'I delete "([^"]*)" from the course team') def delete_other_user(_step, name): - to_delete_css = '.remove-user[data-id="{name}{extension}"]'.format(name=name, extension=EMAIL_EXTENSION) + to_delete_css = 'a.remove-user[data-id="{name}{extension}"]'.format(name=name, extension=EMAIL_EXTENSION) world.css_click(to_delete_css) @@ -46,22 +46,22 @@ def other_user_login(_step, name): @step(u'He does( not)? see the course on his page') def see_course(_step, doesnt_see_course): - class_css = '.class-name' + class_css = 'span.class-name' all_courses = world.css_find(class_css) all_names = [item.html for item in all_courses] if doesnt_see_course: - assert not COURSE_NAME in all_names + assert not _COURSE_NAME in all_names else: - assert COURSE_NAME in all_names + assert _COURSE_NAME in all_names @step(u'He cannot delete users') def cannot_delete(_step): - to_delete_css = '.remove-user' + to_delete_css = 'a.remove-user' assert world.is_css_not_present(to_delete_css) @step(u'He cannot add users') def cannot_add(_step): - add_css = '.new-user' + add_css = 'a.new-user' assert world.is_css_not_present(add_css) diff --git a/cms/djangoapps/contentstore/features/course-updates.py b/cms/djangoapps/contentstore/features/course-updates.py index e742b6a40c..d838061698 100644 --- a/cms/djangoapps/contentstore/features/course-updates.py +++ b/cms/djangoapps/contentstore/features/course-updates.py @@ -7,23 +7,23 @@ from common import type_in_codemirror @step(u'I go to the course updates page') -def go_to_uploads(_step): +def go_to_updates(_step): menu_css = 'li.nav-course-courseware' - uploads_css = '.nav-course-courseware-updates' + updates_css = 'li.nav-course-courseware-updates' world.css_click(menu_css) - world.css_click(uploads_css) + world.css_click(updates_css) @step(u'I add a new update with the text "([^"]*)"$') def add_update(_step, text): - update_css = '.new-update-button' + update_css = 'a.new-update-button' world.css_click(update_css) change_text(text) @step(u'I should( not)? see the update "([^"]*)"$') def check_update(_step, doesnt_see_update, text): - update_css = '.update-contents' + update_css = 'div.update-contents' update = world.css_find(update_css) if doesnt_see_update: assert len(update) == 0 or not text in update.html @@ -33,52 +33,52 @@ def check_update(_step, doesnt_see_update, text): @step(u'I modify the text to "([^"]*)"$') def modify_update(_step, text): - button_css = '.post-preview .edit-button' + button_css = 'div.post-preview a.edit-button' world.css_click(button_css) change_text(text) @step(u'I delete the update$') def click_button(_step): - button_css = '.post-preview .delete-button' + button_css = 'div.post-preview a.delete-button' world.css_click(button_css) @step(u'I edit the date to "([^"]*)"$') def change_date(_step, new_date): - button_css = '.post-preview .edit-button' + button_css = 'div.post-preview a.edit-button' world.css_click(button_css) date_css = 'input.date' date = world.css_find(date_css) for i in range(len(date.value)): date._element.send_keys(Keys.END, Keys.BACK_SPACE) date._element.send_keys(new_date) - save_css = '.save-button' + save_css = 'a.save-button' world.css_click(save_css) @step(u'I should see the date "([^"]*)"$') def check_date(_step, date): - date_css = '.date-display' + date_css = 'span.date-display' date_html = world.css_find(date_css) assert date == date_html.html @step(u'I modify the handout to "([^"]*)"$') def edit_handouts(_step, text): - edit_css = '.course-handouts > .edit-button' + edit_css = 'div.course-handouts > a.edit-button' world.css_click(edit_css) change_text(text) @step(u'I see the handout "([^"]*)"$') def check_handout(_step, handout): - handout_css = '.handouts-content' + handout_css = 'div.handouts-content' handouts = world.css_find(handout_css) assert handout in handouts.html def change_text(text): type_in_codemirror(0, text) - save_css = '.save-button' + save_css = 'a.save-button' world.css_click(save_css) diff --git a/cms/djangoapps/contentstore/features/static-pages.py b/cms/djangoapps/contentstore/features/static-pages.py index 23656690fc..a16a3246da 100644 --- a/cms/djangoapps/contentstore/features/static-pages.py +++ b/cms/djangoapps/contentstore/features/static-pages.py @@ -6,16 +6,16 @@ from selenium.webdriver.common.keys import Keys @step(u'I go to the static pages page') -def go_to_uploads(_step): +def go_to_static(_step): menu_css = 'li.nav-course-courseware' - uploads_css = '.nav-course-courseware-pages' + static_css = 'li.nav-course-courseware-pages' world.css_find(menu_css).click() - world.css_find(uploads_css).click() + world.css_find(static_css).click() @step(u'I add a new page') def add_page(_step): - button_css = '.new-button' + button_css = 'a.new-button' world.css_find(button_css).click() @@ -30,7 +30,7 @@ def see_page(_step, doesnt, page): @step(u'I "([^"]*)" the "([^"]*)" page$') def click_edit_delete(_step, edit_delete, page): - button_css = '.%s-button' % edit_delete + button_css = 'a.%s-button' % edit_delete index = get_index(page) assert index != -1 world.css_find(button_css)[index].click() @@ -40,13 +40,13 @@ def click_edit_delete(_step, edit_delete, page): def change_name(_step, new_name): settings_css = '#settings-mode' world.css_find(settings_css).click() - input_css = '.setting-input' + input_css = 'input.setting-input' name_input = world.css_find(input_css) old_name = name_input.value for count in range(len(old_name)): name_input._element.send_keys(Keys.END, Keys.BACK_SPACE) name_input._element.send_keys(new_name) - save_button = '.save-button' + save_button = 'a.save-button' world.css_find(save_button).click() diff --git a/cms/djangoapps/contentstore/features/upload.py b/cms/djangoapps/contentstore/features/upload.py index 7ef782ea13..5bf082c774 100644 --- a/cms/djangoapps/contentstore/features/upload.py +++ b/cms/djangoapps/contentstore/features/upload.py @@ -15,23 +15,23 @@ HTTP_PREFIX = "http://localhost:8001" @step(u'I go to the files and uploads page') def go_to_uploads(_step): menu_css = 'li.nav-course-courseware' - uploads_css = '.nav-course-courseware-uploads' + uploads_css = 'li.nav-course-courseware-uploads' world.css_find(menu_css).click() world.css_find(uploads_css).click() @step(u'I upload the file "([^"]*)"$') def upload_file(_step, file_name): - upload_css = '.upload-button' + upload_css = 'a.upload-button' world.css_find(upload_css).click() - file_css = '.file-input' + file_css = 'input.file-input' upload = world.css_find(file_css) #uploading the file itself path = os.path.join(TEST_ROOT, 'uploads/', file_name) upload._element.send_keys(os.path.abspath(path)) - close_css = '.close-button' + close_css = 'a.close-button' world.css_find(close_css).click() @@ -54,7 +54,8 @@ def check_url(_step, file_name): def delete_file(_step, file_name): index = get_index(file_name) assert index != -1 - delete_css = ".remove-asset-button" + from pdb import set_trace; set_trace() + delete_css = "a.remove-asset-button" world.css_click(delete_css, index=index) prompt_confirm_css = 'li.nav-item > a.action-primary' @@ -63,7 +64,7 @@ def delete_file(_step, file_name): @step(u'I should see only one "([^"]*)"$') def no_duplicate(_step, file_name): - names_css = '.name-col > a.filename' + names_css = 'td.name-col > a.filename' all_names = world.css_find(names_css) only_one = False for i in range(len(all_names)): @@ -91,7 +92,7 @@ def modify_upload(_step, file_name): def get_index(file_name): - names_css = '.name-col > a.filename' + names_css = 'td.name-col > a.filename' all_names = world.css_find(names_css) for i in range(len(all_names)): if file_name == all_names[i].html: diff --git a/common/test/data/uploads/test b/common/test/data/uploads/test index 0424951e34..f019db7176 100644 --- a/common/test/data/uploads/test +++ b/common/test/data/uploads/test @@ -1 +1 @@ -This is an arbitrary file for testing uploads +R2FUIGM88K \ No newline at end of file From 4cb6eb6e19c6b338d095864389e9ac73dffda753 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 20 Jun 2013 10:42:16 -0400 Subject: [PATCH 294/375] Fix pylint violations in xmodule static_content.py --- common/lib/xmodule/xmodule/static_content.py | 31 ++++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/static_content.py b/common/lib/xmodule/xmodule/static_content.py index 7662499c16..2cadd34df1 100755 --- a/common/lib/xmodule/xmodule/static_content.py +++ b/common/lib/xmodule/xmodule/static_content.py @@ -20,22 +20,27 @@ LOG = logging.getLogger(__name__) def write_module_styles(output_root): + """Write all registered XModule css, sass, and scss files to output root.""" return _write_styles('.xmodule_display', output_root, _list_modules()) def write_module_js(output_root): + """Write all registered XModule js and coffee files to output root.""" return _write_js(output_root, _list_modules()) def write_descriptor_styles(output_root): + """Write all registered XModuleDescriptor css, sass, and scss files to output root.""" return _write_styles('.xmodule_edit', output_root, _list_descriptors()) def write_descriptor_js(output_root): + """Write all registered XModuleDescriptor js and coffee files to output root.""" return _write_js(output_root, _list_descriptors()) def _list_descriptors(): + """Return a list of all registered XModuleDescriptor classes.""" return [ desc for desc in [ desc for (_, desc) in XModuleDescriptor.load_classes() @@ -44,6 +49,7 @@ def _list_descriptors(): def _list_modules(): + """Return a list of all registered XModule classes.""" return [ desc.module_class for desc @@ -51,9 +57,10 @@ def _list_modules(): ] -def _ensure_dir(dir_): +def _ensure_dir(directory): + """Ensure that `directory` exists.""" try: - os.makedirs(dir_) + os.makedirs(directory) except OSError as exc: if exc.errno == errno.EEXIST: pass @@ -131,6 +138,19 @@ def _write_js(output_root, classes): def _write_files(output_root, contents, generated_suffix_map=None): + """ + Write file contents to output root. + + Any files not listed in contents that exists in output_root will be deleted, + unless it matches one of the patterns in `generated_suffix_map`. + + output_root (path): The root directory to write the file contents in + contents (dict): A map from filenames to file contents to be written to the output_root + generated_suffix_map (dict): Optional. Maps file suffix to generated file suffix. + For any file in contents, if the suffix matches a key in `generated_suffix_map`, + then the same filename with the suffix replaced by the value from `generated_suffix_map` + will be ignored + """ _ensure_dir(output_root) to_delete = set(file.basename() for file in output_root.files()) - set(contents.keys()) @@ -146,7 +166,12 @@ def _write_files(output_root, contents, generated_suffix_map=None): for filename, file_content in contents.iteritems(): output_file = output_root / filename - if not output_file.isfile() or output_file.read_md5() != hashlib.md5(file_content).digest(): + not_file = not output_file.isfile() + + # not_file is included to short-circuit this check, because + # read_md5 depends on the file already existing + write_file = not_file or output_file.read_md5() != hashlib.md5(file_content).digest() # pylint: disable=E1121 + if write_file: LOG.debug("Writing %s", output_file) output_file.write_bytes(file_content) else: From a52d0beb41a75b493ed5d6d3110790a9edd0c505 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 20 Jun 2013 17:00:46 -0400 Subject: [PATCH 295/375] Got rid of leftover set_trace --- cms/djangoapps/contentstore/features/upload.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cms/djangoapps/contentstore/features/upload.py b/cms/djangoapps/contentstore/features/upload.py index 5bf082c774..258fc5ebcf 100644 --- a/cms/djangoapps/contentstore/features/upload.py +++ b/cms/djangoapps/contentstore/features/upload.py @@ -54,7 +54,6 @@ def check_url(_step, file_name): def delete_file(_step, file_name): index = get_index(file_name) assert index != -1 - from pdb import set_trace; set_trace() delete_css = "a.remove-asset-button" world.css_click(delete_css, index=index) From 22b400b3494955e49140a4ea2b87704058b1e4af Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Thu, 20 Jun 2013 17:16:18 -0400 Subject: [PATCH 296/375] Fix typo in output message of rakefile --- rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rakefile b/rakefile index 96bd4c2e96..2cf442bca9 100644 --- a/rakefile +++ b/rakefile @@ -3,7 +3,7 @@ begin require 'rake/clean' require './rakelib/helpers.rb' rescue LoadError => error - puts "Import faild (#{error})" + puts "Import failed (#{error})" puts "Please run `bundle install` to bootstrap ruby dependencies" exit 1 end From aa4e27f77534ad35bece457e097f5226bb95d29a Mon Sep 17 00:00:00 2001 From: Jason Bau Date: Thu, 20 Jun 2013 18:12:06 -0700 Subject: [PATCH 297/375] Shib PR responses to @cpennington and @ormsbee comments * Changed unicode test cases to ascii encoding * Removed 'stanford' hardcoding in TOS logic in lieu of 'SHIB_DISABLE_TOS' MIT_FEATURES flag * made 'external_auth' always an installed_app in lms * log.exception changd to log.error where appropriate But: did not change skipping tests to changing settings, for reasons stated here: https://github.com/edx/edx-platform/pull/67#issuecomment-19790330 --- .../external_auth/tests/test_shib.py | 5 ++-- common/djangoapps/external_auth/views.py | 10 +++++--- common/djangoapps/student/views.py | 25 ++++++++++--------- lms/envs/common.py | 8 ++++++ lms/envs/test.py | 4 +-- 5 files changed, 30 insertions(+), 22 deletions(-) diff --git a/common/djangoapps/external_auth/tests/test_shib.py b/common/djangoapps/external_auth/tests/test_shib.py index e5059e5635..eb05b59afb 100644 --- a/common/djangoapps/external_auth/tests/test_shib.py +++ b/common/djangoapps/external_auth/tests/test_shib.py @@ -1,4 +1,3 @@ -# coding=utf-8 """ Tests for Shibboleth Authentication @jbau @@ -36,8 +35,8 @@ from student.tests.factories import UserFactory IDP = 'https://idp.stanford.edu/' REMOTE_USER = 'test_user@stanford.edu' MAILS = [None, '', 'test_user@stanford.edu'] -GIVENNAMES = [None, '', 'Jason', 'jasön; John; bob'] # At Stanford, the givenNames can be a list delimited by ';' -SNS = [None, '', 'Bau', '包; smith'] # At Stanford, the sns can be a list delimited by ';' +GIVENNAMES = [None, '', 'Jason', 'jas\xc3\xb6n; John; bob'] # At Stanford, the givenNames can be a list delimited by ';' +SNS = [None, '', 'Bau', '\xe5\x8c\x85; smith'] # At Stanford, the sns can be a list delimited by ';' def gen_all_identities(): diff --git a/common/djangoapps/external_auth/views.py b/common/djangoapps/external_auth/views.py index 1ae8edfc52..93ab70debb 100644 --- a/common/djangoapps/external_auth/views.py +++ b/common/djangoapps/external_auth/views.py @@ -245,8 +245,10 @@ def signup(request, eamap=None): 'ask_for_tos': True, } - # Can't have terms of service for Stanford users, according to Stanford's Office of General Counsel - if settings.MITX_FEATURES['AUTH_USE_SHIB'] and ('stanford' in eamap.external_domain): + # Some openEdX instances can't have terms of service for shib users, like + # according to Stanford's Office of General Counsel + if settings.MITX_FEATURES.get('AUTH_USE_SHIB') and settings.MITX_FEATURES.get('SHIB_DISABLE_TOS') and \ + ('shib' in eamap.external_domain): context['ask_for_tos'] = False # detect if full name is blank and ask for it from user @@ -387,10 +389,10 @@ def shib_login(request): """)) if not request.META.get('REMOTE_USER'): - log.exception("SHIB: no REMOTE_USER found in request.META") + log.error("SHIB: no REMOTE_USER found in request.META") return default_render_failure(request, shib_error_msg) elif not request.META.get('Shib-Identity-Provider'): - log.exception("SHIB: no Shib-Identity-Provider in request.META") + log.error("SHIB: no Shib-Identity-Provider in request.META") return default_render_failure(request, shib_error_msg) else: #if we get here, the user has authenticated properly diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 0aac873c03..1a49789a32 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -48,6 +48,8 @@ from courseware.access import has_access from courseware.views import get_module_for_descriptor, jump_to from courseware.model_data import ModelDataCache +from external_auth.models import ExternalAuthMap + from statsd import statsd from pytz import UTC @@ -287,12 +289,10 @@ def dashboard(request): # get info w.r.t ExternalAuthMap external_auth_map = None - if 'external_auth' in settings.INSTALLED_APPS: - from external_auth.models import ExternalAuthMap - try: - external_auth_map = ExternalAuthMap.objects.get(user=user) - except ExternalAuthMap.DoesNotExist: - pass + try: + external_auth_map = ExternalAuthMap.objects.get(user=user) + except ExternalAuthMap.DoesNotExist: + pass context = {'courses': courses, 'message': message, @@ -613,10 +613,12 @@ def create_account(request, post_override=None): js['field'] = 'honor_code' return HttpResponse(json.dumps(js)) - # Can't have terms of service for Stanford users, according to Stanford's Office of General Counsel - if settings.MITX_FEATURES.get("AUTH_USE_SHIB") and DoExternalAuth and ("stanford" in eamap.external_domain): - pass - else: + # Can't have terms of service for certain SHIB users, like at Stanford + tos_not_required = settings.MITX_FEATURES.get("AUTH_USE_SHIB") \ + and settings.MITX_FEATURES.get('SHIB_DISABLE_TOS') \ + and DoExternalAuth and ("shib" in eamap.external_domain) + + if not tos_not_required: if post_vars.get('terms_of_service', 'false') != u'true': js['value'] = "You must accept the terms of service.".format(field=a) js['field'] = 'terms_of_service' @@ -629,8 +631,7 @@ def create_account(request, post_override=None): # TODO: Check password is sane required_post_vars = ['username', 'email', 'name', 'password', 'terms_of_service', 'honor_code'] - if settings.MITX_FEATURES.get("AUTH_USE_SHIB") and DoExternalAuth and ("stanford" in eamap.external_domain): - # Can't have terms of service for Stanford users, according to Stanford's Office of General Counsel + if tos_not_required: required_post_vars = ['username', 'email', 'name', 'password', 'honor_code'] for a in required_post_vars: diff --git a/lms/envs/common.py b/lms/envs/common.py index be570a9796..8a554f5bb9 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -92,6 +92,10 @@ MITX_FEATURES = { 'AUTH_USE_MIT_CERTIFICATES': False, 'AUTH_USE_OPENID_PROVIDER': False, 'AUTH_USE_SHIB': False, + + # This flag disables the requirement of having to agree to the TOS for users registering + # with Shib. Feature was requested by Stanford's office of general counsel + 'SHIB_DISABLE_TOS': False, # Enables ability to restrict enrollment in specific courses by the user account login method 'RESTRICT_ENROLL_BY_REG_METHOD': False, @@ -704,6 +708,10 @@ INSTALLED_APPS = ( 'licenses', 'course_groups', + # External auth (OpenID, shib) + 'external_auth', + 'django_openid_auth', + #For the wiki 'wiki', # The new django-wiki from benjaoming 'django_notify', diff --git a/lms/envs/test.py b/lms/envs/test.py index 3a6c641841..e9b683487e 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -139,6 +139,7 @@ MITX_FEATURES['AUTH_USE_OPENID_PROVIDER'] = True ################################## SHIB ####################################### MITX_FEATURES['AUTH_USE_SHIB'] = True +MITX_FEATURES['SHIB_DISABLE_TOS'] = True MITX_FEATURES['RESTRICT_ENROLL_BY_REG_METHOD'] = True OPENID_CREATE_USERS = False @@ -146,9 +147,6 @@ OPENID_UPDATE_DETAILS_FROM_SREG = True OPENID_USE_AS_ADMIN_LOGIN = False OPENID_PROVIDER_TRUSTED_ROOTS = ['*'] -INSTALLED_APPS += ('external_auth',) -INSTALLED_APPS += ('django_openid_auth',) - ################################# CELERY ###################################### CELERY_ALWAYS_EAGER = True From f109e5b01c0fda46d996cbf0063e3c4af2467a95 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 21 Jun 2013 08:02:23 -0400 Subject: [PATCH 298/375] Changed order of arguments to test tasks so that the report directory is created before it is cleaned. --- rakelib/tests.rake | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rakelib/tests.rake b/rakelib/tests.rake index f169d28256..3cb5e8f4e5 100644 --- a/rakelib/tests.rake +++ b/rakelib/tests.rake @@ -55,6 +55,7 @@ end task :clean_reports_dir do desc "Clean coverage files, to ensure that we don't use stale data to generate reports." + # We delete the files but preserve the directory structure # so that coverage.py has a place to put the reports. sh("find #{REPORT_DIR} -type f -delete") @@ -82,7 +83,7 @@ TEST_TASK_DIRS = [] task "test_acceptance_#{system}", [:harvest_args] => [:clean_test_files, "#{system}:gather_assets:acceptance", "fasttest_acceptance_#{system}"] desc "Run acceptance tests without collectstatic" - task "fasttest_acceptance_#{system}", [:harvest_args] => [:clean_reports_dir, :predjango, report_dir] do |t, args| + task "fasttest_acceptance_#{system}", [:harvest_args] => [report_dir, :clean_reports_dir, :predjango] do |t, args| args.with_defaults(:harvest_args => '') run_acceptance_tests(system, report_dir, args.harvest_args) end @@ -98,7 +99,7 @@ Dir["common/lib/*"].select{|lib| File.directory?(lib)}.each do |lib| report_dir = report_dir_path(lib) desc "Run tests for common lib #{lib}" - task "test_#{lib}" => [:clean_reports_dir, report_dir] do + task "test_#{lib}" => [report_dir, :clean_reports_dir] do ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml") cmd = "nosetests #{lib}" test_sh(run_under_coverage(cmd, lib)) From e4af7287b6f204dc759f1e9a349bf29a6864a025 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Tue, 18 Jun 2013 14:04:43 -0400 Subject: [PATCH 299/375] Initial testing for parallelization --- cms/djangoapps/contentstore/tests/test_contentstore.py | 6 ++++++ cms/envs/test.py | 2 +- .../lib/xmodule/xmodule/modulestore/tests/django_utils.py | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index d24deacecf..86699ef479 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -43,10 +43,13 @@ from django_comment_common.utils import are_permissions_roles_seeded from xmodule.exceptions import InvalidVersionError import datetime from pytz import UTC +#from uuid import uuid4 TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE) TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data') TEST_DATA_MODULESTORE['direct']['OPTIONS']['fs_root'] = path('common/test/data') +TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE) +TEST_DATA_CONTENTSTORE['OPTIONS']['db'] = 'test_xcontent_%s' % 4 #uuid4().hex class MongoCollectionFindWrapper(object): @@ -60,6 +63,7 @@ class MongoCollectionFindWrapper(object): @override_settings(MODULESTORE=TEST_DATA_MODULESTORE) +@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE) class ContentStoreToyCourseTest(ModuleStoreTestCase): """ Tests that rely on the toy courses. @@ -83,6 +87,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): self.client = Client() self.client.login(username=uname, password=password) + def check_components_on_page(self, component_types, expected_types): """ Ensure that the right types end up on the page. @@ -809,6 +814,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): export_to_xml(module_store, content_store, location, root_dir, 'test_export') +@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE) class ContentStoreTest(ModuleStoreTestCase): """ Tests for the CMS ContentStore application. diff --git a/cms/envs/test.py b/cms/envs/test.py index 954a553e10..89813dd937 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -70,7 +70,7 @@ CONTENTSTORE = { 'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore', 'OPTIONS': { 'host': 'localhost', - 'db': 'test_xmodule', + 'db': 'test_xcontent', }, # allow for additional options that can be keyed on a name, e.g. 'trashcan' 'ADDITIONAL_OPTIONS': { diff --git a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py index 04e79ce521..e0e5c1a46f 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py @@ -27,6 +27,7 @@ class ModuleStoreTestCase(TestCase): # Remove everything except templates modulestore.collection.remove(query) + modulestore.collection.drop() @staticmethod def load_templates_if_necessary(): From f90ed69cd792091c9f2d57bce1cbafe0efd51094 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Tue, 18 Jun 2013 15:09:53 -0400 Subject: [PATCH 300/375] move override of MODULESTORE settings into ModuleStore test case class --- cms/djangoapps/contentstore/tests/test_contentstore.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 86699ef479..9c3ec2e3ba 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -45,9 +45,7 @@ import datetime from pytz import UTC #from uuid import uuid4 -TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE) -TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data') -TEST_DATA_MODULESTORE['direct']['OPTIONS']['fs_root'] = path('common/test/data') + TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE) TEST_DATA_CONTENTSTORE['OPTIONS']['db'] = 'test_xcontent_%s' % 4 #uuid4().hex @@ -62,7 +60,7 @@ class MongoCollectionFindWrapper(object): return self.original(query, *args, **kwargs) -@override_settings(MODULESTORE=TEST_DATA_MODULESTORE) +#@override_settings(MODULESTORE=TEST_DATA_MODULESTORE) @override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE) class ContentStoreToyCourseTest(ModuleStoreTestCase): """ @@ -70,6 +68,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): TODO: refactor using CourseFactory so they do not. """ def setUp(self): + + settings.MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data') + settings.MODULESTORE['direct']['OPTIONS']['fs_root'] = path('common/test/data') uname = 'testuser' email = 'test+courses@edx.org' password = 'foo' @@ -88,6 +89,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): self.client.login(username=uname, password=password) + def check_components_on_page(self, component_types, expected_types): """ Ensure that the right types end up on the page. From 51f8c0cfebedb7807b04ef849cfb806a0dcdba0e Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Wed, 19 Jun 2013 11:27:22 -0400 Subject: [PATCH 301/375] Added the beginnings of self cleanup --- .../contentstore/tests/test_contentstore.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 9c3ec2e3ba..46d6a069ce 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -43,11 +43,12 @@ from django_comment_common.utils import are_permissions_roles_seeded from xmodule.exceptions import InvalidVersionError import datetime from pytz import UTC -#from uuid import uuid4 +from uuid import uuid4 +import pymongo TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE) -TEST_DATA_CONTENTSTORE['OPTIONS']['db'] = 'test_xcontent_%s' % 4 #uuid4().hex +TEST_DATA_CONTENTSTORE['OPTIONS']['db'] = 'test_xcontent_%s' % uuid4().hex class MongoCollectionFindWrapper(object): @@ -88,7 +89,10 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): self.client = Client() self.client.login(username=uname, password=password) - + def tearDown(self): + m = pymongo.MongoClient() + m.drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db']) + #contentstore().fs_files.drop() def check_components_on_page(self, component_types, expected_types): """ @@ -449,7 +453,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): content_store = contentstore() trash_store = contentstore('trashcan') module_store = modulestore('direct') - import_from_xml(module_store, 'common/test/data/', ['full'], static_content_store=content_store) # look up original (and thumbnail) in content store, should be there after import @@ -853,6 +856,11 @@ class ContentStoreTest(ModuleStoreTestCase): 'display_name': 'Robot Super Course', } + def tearDown(self): + m = pymongo.MongoClient() + m.drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db']) + #contentstore().fs_files.drop() + def test_create_course(self): """Test new course creation - happy path""" resp = self.client.post(reverse('create_new_course'), self.course_data) From fa18b48f6eaec45bc65f16f9585fa2555462ad55 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 20 Jun 2013 09:05:35 -0400 Subject: [PATCH 302/375] Contentstore singleton is now cleared during teardown --- cms/djangoapps/contentstore/tests/test_contentstore.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 46d6a069ce..b0cbcee032 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -41,6 +41,7 @@ from xmodule.exceptions import NotFoundError from django_comment_common.utils import are_permissions_roles_seeded from xmodule.exceptions import InvalidVersionError +import xmodule.contentstore.django import datetime from pytz import UTC from uuid import uuid4 @@ -92,7 +93,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): def tearDown(self): m = pymongo.MongoClient() m.drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db']) - #contentstore().fs_files.drop() + xmodule.contentstore.django._CONTENTSTORE.clear() def check_components_on_page(self, component_types, expected_types): """ @@ -859,7 +860,7 @@ class ContentStoreTest(ModuleStoreTestCase): def tearDown(self): m = pymongo.MongoClient() m.drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db']) - #contentstore().fs_files.drop() + xmodule.contentstore.django._CONTENTSTORE.clear() def test_create_course(self): """Test new course creation - happy path""" From cb04f9f0b82dfe46777ceb0584fadb656f7b6780 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 20 Jun 2013 17:10:36 -0400 Subject: [PATCH 303/375] Moved port range to rake file --- jenkins/test.sh | 3 --- rakelib/tests.rake | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/jenkins/test.sh b/jenkins/test.sh index e5ac4f6f71..2ff32a9911 100755 --- a/jenkins/test.sh +++ b/jenkins/test.sh @@ -60,9 +60,6 @@ fi export PIP_DOWNLOAD_CACHE=/mnt/pip-cache -# Allow django liveserver tests to use a range of ports -export DJANGO_LIVE_TEST_SERVER_ADDRESS=${DJANGO_LIVE_TEST_SERVER_ADDRESS-localhost:8000-9000} - source /mnt/virtualenvs/"$JOB_NAME"/bin/activate bundle install diff --git a/rakelib/tests.rake b/rakelib/tests.rake index f169d28256..c0592cca7a 100644 --- a/rakelib/tests.rake +++ b/rakelib/tests.rake @@ -16,7 +16,7 @@ def run_tests(system, report_dir, test_id=nil, stop_on_failure=true) ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml") dirs = Dir["common/djangoapps/*"] + Dir["#{system}/djangoapps/*"] test_id = dirs.join(' ') if test_id.nil? or test_id == '' - cmd = django_admin(system, :test, 'test', '--logging-clear-handlers', test_id) + cmd = django_admin(system, :test, 'test', '--logging-clear-handlers', '--liveserver=localhost:8000-9000', test_id) test_sh(run_under_coverage(cmd, system)) end From b8479305797487a54468858073f07a96d0d41aea Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Fri, 21 Jun 2013 09:38:43 -0400 Subject: [PATCH 304/375] Changed a click to css_click and fixed earlier typo --- lms/djangoapps/courseware/features/problems.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index 094d495b53..08c5207303 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -9,7 +9,8 @@ from lettuce import world, step from lettuce.django import django_url from common import i_am_registered_for_the_course, TEST_SECTION_NAME from problems_setup import PROBLEM_DICT, answer_problem, problem_has_answer, add_problem_to_course -from nose.tools import assert_equal, assert_not_equal +from nose.tools import assert_equal + @step(u'I am viewing a "([^"]*)" problem with "([^"]*)" attempt') def view_problem_with_attempts(step, problem_type, attempts): @@ -121,7 +122,7 @@ def press_the_button_with_label(_step, buttonname): button_css = 'button span.show-label' elem = world.css_find(button_css).first assert_equal(elem.text, buttonname) - elem.click() + world.css_click(button_css) @step(u'The "([^"]*)" button does( not)? appear') @@ -136,9 +137,9 @@ def action_button_present(_step, buttonname, doesnt_appear): @step(u'the button with the label "([^"]*)" does( not)? appear') def button_with_label_present(step, buttonname, doesnt_appear): if doesnt_appear: - world.browser.is_text_not_present(buttonname, wait_time=5) + assert world.browser.is_text_not_present(buttonname, wait_time=5) else: - world.browser.is_text_present(buttonname, wait_time=5) + assert world.browser.is_text_present(buttonname, wait_time=5) @step(u'My "([^"]*)" answer is marked "([^"]*)"') From 3d202ffc71631fca47616adff1e66bc0d220a81c Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 21 Jun 2013 10:57:19 -0400 Subject: [PATCH 305/375] Moved generation of pylint/pep8 reports to after running the test suite, so the clean reports command doesn't blow away the reports. --- jenkins/test.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/jenkins/test.sh b/jenkins/test.sh index e5ac4f6f71..c7728ab367 100755 --- a/jenkins/test.sh +++ b/jenkins/test.sh @@ -69,12 +69,14 @@ bundle install rake install_prereqs rake clobber -rake pep8 > pep8.log || cat pep8.log -rake pylint > pylint.log || cat pylint.log # Run the unit tests (use phantomjs for javascript unit tests) rake test +# Generate pylint and pep8 reports +rake pep8 > pep8.log || cat pep8.log +rake pylint > pylint.log || cat pylint.log + # Generate coverage reports rake coverage From 7db93976c5860cf818bc915bb890b1a9c18b6838 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Fri, 21 Jun 2013 11:02:25 -0400 Subject: [PATCH 306/375] PR fixes --- common/lib/xmodule/xmodule/capa_module.py | 9 +++------ .../lib/xmodule/xmodule/tests/test_capa_module.py | 14 ++++++++------ 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index b927106b4a..d740a73946 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -22,7 +22,7 @@ from xblock.core import Scope, String, Boolean, Dict, Integer, Float from .fields import Timedelta, Date from django.utils.timezone import UTC -log = logging.getLogger("mitx.courseware") # pylint: disable=C0103 +log = logging.getLogger("mitx.courseware") # Generate this many different variants of problems with rerandomize=per_student @@ -51,9 +51,6 @@ class Randomization(String): Define a field to store how to randomize a problem. """ def from_json(self, value): - """ - For backward compatability? - """ if value in ("", "true"): return "always" elif value == "false": @@ -865,8 +862,8 @@ class CapaModule(CapaFields, XModule): except Exception as err: if self.system.DEBUG: - msg = "Error checking problem: " + err.message - msg += '\nTraceback:\n' + traceback.format_exc() + msg = u"Error checking problem: {}".format(err.message) + msg += u'\nTraceback:\n{}'.format(traceback.format_exc()) return {'success': msg} raise diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py index 81df686015..c6ffd32e89 100644 --- a/common/lib/xmodule/xmodule/tests/test_capa_module.py +++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py @@ -505,9 +505,10 @@ class CapaModuleTest(unittest.TestCase): def test_check_problem_error(self): # Try each exception that capa_module should handle - for exception_class in [StudentInputError, - LoncapaProblemError, - ResponseError]: + exception_classes = [StudentInputError, + LoncapaProblemError, + ResponseError] + for exception_class in exception_classes: # Create the module module = CapaFactory.create(attempts=1) @@ -532,9 +533,10 @@ class CapaModuleTest(unittest.TestCase): def test_check_problem_error_nonascii(self): # Try each exception that capa_module should handle - for exception_class in [StudentInputError, - LoncapaProblemError, - ResponseError]: + exception_classes = [StudentInputError, + LoncapaProblemError, + ResponseError] + for exception_class in exception_classes: # Create the module module = CapaFactory.create(attempts=1) From 8201ca5e777f6933a8329aab777b86e33b09f843 Mon Sep 17 00:00:00 2001 From: Sarina Canelake Date: Thu, 20 Jun 2013 18:27:45 -0400 Subject: [PATCH 307/375] Fix 500 error on reactivation email --- common/djangoapps/student/tests/test_email.py | 25 ++++++++++++++----- common/djangoapps/student/views.py | 18 +++++++------ 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/common/djangoapps/student/tests/test_email.py b/common/djangoapps/student/tests/test_email.py index 3b31bb5c28..7e2d9ede00 100644 --- a/common/djangoapps/student/tests/test_email.py +++ b/common/djangoapps/student/tests/test_email.py @@ -55,11 +55,15 @@ class ReactivationEmailTests(EmailTestMixin, TestCase): def setUp(self): self.user = UserFactory.create() + self.unregisteredUser = UserFactory.create() self.registration = RegistrationFactory.create(user=self.user) - def reactivation_email(self): - """Send the reactivation email, and return the response as json data""" - return json.loads(reactivation_email_for_user(self.user).content) + def reactivation_email(self, user): + """ + Send the reactivation email to the specified user, + and return the response as json data. + """ + return json.loads(reactivation_email_for_user(user).content) def assertReactivateEmailSent(self, email_user): """Assert that the correct reactivation email has been sent""" @@ -78,13 +82,22 @@ class ReactivationEmailTests(EmailTestMixin, TestCase): def test_reactivation_email_failure(self, email_user): self.user.email_user.side_effect = Exception - response_data = self.reactivation_email() + response_data = self.reactivation_email(self.user) self.assertReactivateEmailSent(email_user) self.assertFalse(response_data['success']) + def test_reactivation_for_unregistered_user(self, email_user): + """ + Test that trying to send a reactivation email to an unregistered + user fails without throwing a 500 error. + """ + response_data = self.reactivation_email(self.unregisteredUser) + + self.assertFalse(response_data['success']) + def test_reactivation_email_success(self, email_user): - response_data = self.reactivation_email() + response_data = self.reactivation_email(self.user) self.assertReactivateEmailSent(email_user) self.assertTrue(response_data['success']) @@ -150,7 +163,7 @@ class EmailChangeRequestTests(TestCase): self.check_duplicate_email(self.new_email) def test_capitalized_duplicate_email(self): - raise SkipTest("We currently don't check for emails in a case insensitive way, but we should") + """Test that we check for email addresses in a case insensitive way""" UserFactory.create(email=self.new_email) self.check_duplicate_email(self.new_email.capitalize()) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 4da7b9d789..135ae59752 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -174,7 +174,7 @@ def _cert_info(user, course, cert_status): CertificateStatuses.downloadable: 'ready', CertificateStatuses.notpassing: 'notpassing', CertificateStatuses.restricted: 'restricted', - } + } status = template_state.get(cert_status['status'], default_status) @@ -183,10 +183,10 @@ def _cert_info(user, course, cert_status): 'show_disabled_download_button': status == 'generating', } if (status in ('generating', 'ready', 'notpassing', 'restricted') and - course.end_of_course_survey_url is not None): + course.end_of_course_survey_url is not None): d.update({ - 'show_survey_button': True, - 'survey_url': process_survey_link(course.end_of_course_survey_url, user)}) + 'show_survey_button': True, + 'survey_url': process_survey_link(course.end_of_course_survey_url, user)}) else: d['show_survey_button'] = False @@ -881,8 +881,8 @@ def get_random_post_override(): 'password': id_generator(), 'name': (id_generator(size=5, chars=string.ascii_lowercase) + " " + id_generator(size=7, chars=string.ascii_lowercase)), - 'honor_code': u'true', - 'terms_of_service': u'true', } + 'honor_code': u'true', + 'terms_of_service': u'true', } def create_random_account(create_account_function): @@ -967,7 +967,11 @@ def reactivation_email(request): def reactivation_email_for_user(user): - reg = Registration.objects.get(user=user) + try: + reg = Registration.objects.get(user=user) + except Registration.DoesNotExist: + return HttpResponse(json.dumps({'success': False, + 'error': 'No inactive user with this e-mail exists'})) d = {'name': user.profile.name, 'key': reg.activation_key} From 75b355c337240104c13094cf0e11e8d21a7552a1 Mon Sep 17 00:00:00 2001 From: Sarina Canelake Date: Fri, 21 Jun 2013 13:12:18 -0400 Subject: [PATCH 308/375] Remove unused reactivation email function --- common/djangoapps/student/views.py | 13 ------------- lms/urls.py | 2 -- 2 files changed, 15 deletions(-) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index faf9ae4cff..6f97f0be63 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -985,19 +985,6 @@ def password_reset(request): 'error': 'Invalid e-mail'})) -@ensure_csrf_cookie -def reactivation_email(request): - ''' Send an e-mail to reactivate a deactivated account, or to - resend an activation e-mail. Untested. ''' - email = request.POST['email'] - try: - user = User.objects.get(email='email') - except User.DoesNotExist: - return HttpResponse(json.dumps({'success': False, - 'error': 'No inactive user with this e-mail exists'})) - return reactivation_email_for_user(user) - - def reactivation_email_for_user(user): reg = Registration.objects.get(user=user) diff --git a/lms/urls.py b/lms/urls.py index f6978f5f7b..a744db39f2 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -116,8 +116,6 @@ if not settings.MITX_FEATURES["USE_CUSTOM_THEME"]: url(r'^submit_feedback$', 'util.views.submit_feedback'), - # TODO: These urls no longer work. They need to be updated before they are re-enabled - # url(r'^reactivate/(?P[^/]*)$', 'student.views.reactivation_email'), ) # Only enable URLs for those marketing links actually enabled in the From e85fa6518227fdfa81cc048109661c9f6354d000 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Fri, 21 Jun 2013 14:24:41 -0400 Subject: [PATCH 309/375] Update discussion documentation --- doc/discussion.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/doc/discussion.md b/doc/discussion.md index 2446485497..752dc6a5e7 100644 --- a/doc/discussion.md +++ b/doc/discussion.md @@ -58,21 +58,24 @@ In the discussion service, notifications are handled asynchronously using a thir bundle exec rake jobs:work -## Initialize roles and permissions +## From the edx-platform django app, initialize roles and permissions To fully test the discussion forum, you might want to act as a moderator or an administrator. Currently, moderators can manage everything in the forum, and administrator can manage everything plus assigning and revoking moderator status of other users. First make sure that the database is up-to-date: - rake django-admin[syncdb] - rake django-admin[migrate] + rake resetdb + +If you have created users in the edx-platform django apps when the comment service was not running, you will need to one-way sync the users into the comment service back end database: + + rake django-admin[sync_user_info] For convenience, add the following environment variables to the terminal (assuming that you're using configuration set lms.envs.dev): export DJANGO_SETTINGS_MODULE=lms.envs.dev export PYTHONPATH=. -Now initialzie roles and permissions, providing a course id eg.: +Now initialize roles and permissions, providing a course id. See the example below. Note that you do not need to do this for Studio-created courses, as the Studio application does this for you. django-admin.py seed_permissions_roles "MITx/6.002x/2012_Fall" From d632ffe9cd8ca16bb6ddf5e34cef3f7ed97a477f Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Fri, 21 Jun 2013 14:52:27 -0400 Subject: [PATCH 310/375] Make Course Team lettuce tests gender-neutral Because it bothers me, although I don't expect anyone else to care. --- .../contentstore/features/course-team.feature | 30 +++++++++---------- .../contentstore/features/course-team.py | 10 +++---- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/cms/djangoapps/contentstore/features/course-team.feature b/cms/djangoapps/contentstore/features/course-team.feature index 502321c49b..fc1212f398 100644 --- a/cms/djangoapps/contentstore/features/course-team.feature +++ b/cms/djangoapps/contentstore/features/course-team.feature @@ -3,32 +3,32 @@ Feature: Course Team Scenario: Users can add other users Given I have opened a new course in Studio - And The user "abcd" exists + And the user "alice" exists And I am viewing the course team settings - When I add "abcd" to the course team - And "abcd" logs in - Then He does see the course on his page + When I add "alice" to the course team + And "alice" logs in + Then she does see the course on her page Scenario: Added users cannot delete or add other users Given I have opened a new course in Studio - And The user "abcd" exists + And the user "bob" exists And I am viewing the course team settings - When I add "abcd" to the course team - And "abcd" logs in - Then He cannot delete users - And He cannot add users + When I add "bob" to the course team + And "bob" logs in + Then he cannot delete users + And he cannot add users Scenario: Users can delete other users Given I have opened a new course in Studio - And The user "abcd" exists + And the user "carol" exists And I am viewing the course team settings - When I add "abcd" to the course team - And I delete "abcd" from the course team - And "abcd" logs in - Then He does not see the course on his page + When I add "carol" to the course team + And I delete "carol" from the course team + And "carol" logs in + Then she does not see the course on her page Scenario: Users cannot add users that do not exist Given I have opened a new course in Studio And I am viewing the course team settings - When I add "abcd" to the course team + When I add "dennis" to the course team Then I should see "Could not find user by email address" somewhere on the page diff --git a/cms/djangoapps/contentstore/features/course-team.py b/cms/djangoapps/contentstore/features/course-team.py index 4303d5066c..c126773db6 100644 --- a/cms/djangoapps/contentstore/features/course-team.py +++ b/cms/djangoapps/contentstore/features/course-team.py @@ -15,7 +15,7 @@ def view_grading_settings(_step): world.css_click(link_css) -@step(u'The user "([^"]*)" exists$') +@step(u'the user "([^"]*)" exists$') def create_other_user(_step, name): create_studio_user(uname=name, password=PASSWORD, email=(name + EMAIL_EXTENSION)) @@ -44,8 +44,8 @@ def other_user_login(_step, name): log_into_studio(uname=name, password=PASSWORD, email=name + EMAIL_EXTENSION) -@step(u'He does( not)? see the course on his page') -def see_course(_step, doesnt_see_course): +@step(u's?he does( not)? see the course on (his|her) page') +def see_course(_step, doesnt_see_course, gender): class_css = 'span.class-name' all_courses = world.css_find(class_css) all_names = [item.html for item in all_courses] @@ -55,13 +55,13 @@ def see_course(_step, doesnt_see_course): assert _COURSE_NAME in all_names -@step(u'He cannot delete users') +@step(u's?he cannot delete users') def cannot_delete(_step): to_delete_css = 'a.remove-user' assert world.is_css_not_present(to_delete_css) -@step(u'He cannot add users') +@step(u's?he cannot add users') def cannot_add(_step): add_css = 'a.new-user' assert world.is_css_not_present(add_css) From 58fe6d4e8367c570648068d3307cc3105a88edf5 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Fri, 21 Jun 2013 16:17:33 -0400 Subject: [PATCH 311/375] Cleaned up import and comment --- cms/djangoapps/contentstore/tests/test_contentstore.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index b0cbcee032..6d2055d459 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -45,7 +45,7 @@ import xmodule.contentstore.django import datetime from pytz import UTC from uuid import uuid4 -import pymongo +from pymongo import MongoClient TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE) @@ -62,7 +62,6 @@ class MongoCollectionFindWrapper(object): return self.original(query, *args, **kwargs) -#@override_settings(MODULESTORE=TEST_DATA_MODULESTORE) @override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE) class ContentStoreToyCourseTest(ModuleStoreTestCase): """ @@ -91,7 +90,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): self.client.login(username=uname, password=password) def tearDown(self): - m = pymongo.MongoClient() + m = MongoClient() m.drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db']) xmodule.contentstore.django._CONTENTSTORE.clear() @@ -858,7 +857,7 @@ class ContentStoreTest(ModuleStoreTestCase): } def tearDown(self): - m = pymongo.MongoClient() + m = MongoClient() m.drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db']) xmodule.contentstore.django._CONTENTSTORE.clear() From 5e6de488abaa45f765b5aef48a1b36851a673be1 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Fri, 21 Jun 2013 16:28:32 -0400 Subject: [PATCH 312/375] Fixed pylint/pep8 violations --- .../contentstore/tests/test_contentstore.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 6d2055d459..514b631521 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -90,8 +90,8 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): self.client.login(username=uname, password=password) def tearDown(self): - m = MongoClient() - m.drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db']) + mongo = MongoClient() + mongo.drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db']) xmodule.contentstore.django._CONTENTSTORE.clear() def check_components_on_page(self, component_types, expected_types): @@ -414,7 +414,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): self.assertGreater(len(all_assets), 0) # make sure we have some thumbnails in our contentstore - all_thumbnails = content_store.get_all_content_thumbnails_for_course(course_location) + content_store.get_all_content_thumbnails_for_course(course_location) # # cdodge: temporarily comment out assertion on thumbnails because many environments @@ -543,7 +543,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): all_assets = trash_store.get_all_content_for_course(course_location) self.assertEqual(len(all_assets), 0) - all_thumbnails = trash_store.get_all_content_thumbnails_for_course(course_location) self.assertEqual(len(all_thumbnails), 0) @@ -608,7 +607,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): self.assertRaises(InvalidVersionError, draft_store.unpublish, location) - def test_bad_contentstore_request(self): resp = self.client.get('http://localhost:8001/c4x/CDX/123123/asset/&images_circuits_Lab7Solution2.png') self.assertEqual(resp.status_code, 400) @@ -857,8 +855,8 @@ class ContentStoreTest(ModuleStoreTestCase): } def tearDown(self): - m = MongoClient() - m.drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db']) + mongo = MongoClient() + mongo.drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db']) xmodule.contentstore.django._CONTENTSTORE.clear() def test_create_course(self): From bea50efc2651767fd5805f8687a7abafe41824c9 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 21 Jun 2013 14:29:28 -0400 Subject: [PATCH 313/375] Remove the unused smart-accordion lettuce feature, and the code it used. --- common/djangoapps/terrain/course_helpers.py | 46 ----- .../features/smart-accordion.feature | 63 ------- .../courseware/features/smart-accordion.py | 158 ------------------ 3 files changed, 267 deletions(-) delete mode 100644 lms/djangoapps/courseware/features/smart-accordion.feature delete mode 100644 lms/djangoapps/courseware/features/smart-accordion.py diff --git a/common/djangoapps/terrain/course_helpers.py b/common/djangoapps/terrain/course_helpers.py index dfe3803dfd..7da49e6315 100644 --- a/common/djangoapps/terrain/course_helpers.py +++ b/common/djangoapps/terrain/course_helpers.py @@ -13,8 +13,6 @@ from student.models import CourseEnrollment from xmodule.modulestore.django import modulestore from xmodule.contentstore.django import contentstore from xmodule.templates import update_templates -from bs4 import BeautifulSoup -import os.path from urllib import quote_plus @@ -76,50 +74,6 @@ def register_by_course_id(course_id, is_staff=False): CourseEnrollment.objects.get_or_create(user=u, course_id=course_id) - -@world.absorb -def save_the_course_content(path='/tmp'): - html = world.browser.html.encode('ascii', 'ignore') - soup = BeautifulSoup(html) - - # get rid of the header, we only want to compare the body - soup.head.decompose() - - # for now, remove the data-id attributes, because they are - # causing mismatches between cms-master and master - for item in soup.find_all(attrs={'data-id': re.compile('.*')}): - del item['data-id'] - - # we also need to remove them from unrendered problems, - # where they are contained in the text of divs instead of - # in attributes of tags - # Be careful of whether or not it was the last attribute - # and needs a trailing space - for item in soup.find_all(text=re.compile(' data-id=".*?" ')): - s = unicode(item.string) - item.string.replace_with(re.sub(' data-id=".*?" ', ' ', s)) - - for item in soup.find_all(text=re.compile(' data-id=".*?"')): - s = unicode(item.string) - item.string.replace_with(re.sub(' data-id=".*?"', ' ', s)) - - # prettify the html so it will compare better, with - # each HTML tag on its own line - output = soup.prettify() - - # use string slicing to grab everything after 'courseware/' in the URL - u = world.browser.url - section_url = u[u.find('courseware/') + 11:] - - if not os.path.exists(path): - os.makedirs(path) - - filename = '%s.html' % (quote_plus(section_url)) - f = open('%s/%s' % (path, filename), 'w') - f.write(output) - f.close - - @world.absorb def clear_courses(): # Flush and initialize the module store diff --git a/lms/djangoapps/courseware/features/smart-accordion.feature b/lms/djangoapps/courseware/features/smart-accordion.feature deleted file mode 100644 index fc51eca25d..0000000000 --- a/lms/djangoapps/courseware/features/smart-accordion.feature +++ /dev/null @@ -1,63 +0,0 @@ -# Here are all the courses for Fall 2012 -# MITx/3.091x/2012_Fall -# MITx/6.002x/2012_Fall -# MITx/6.00x/2012_Fall -# HarvardX/CS50x/2012 (we will not be testing this, as it is anomolistic) -# HarvardX/PH207x/2012_Fall -# BerkeleyX/CS169.1x/2012_Fall -# BerkeleyX/CS169.2x/2012_Fall -# BerkeleyX/CS184.1x/2012_Fall - -#You can load the courses into your data directory with these cmds: -# git clone https://github.com/MITx/3.091x.git -# git clone https://github.com/MITx/6.00x.git -# git clone https://github.com/MITx/content-mit-6002x.git -# git clone https://github.com/MITx/content-mit-6002x.git -# git clone https://github.com/MITx/content-harvard-id270x.git -# git clone https://github.com/MITx/content-berkeley-cs169x.git -# git clone https://github.com/MITx/content-berkeley-cs169.2x.git -# git clone https://github.com/MITx/content-berkeley-cs184x.git - -Feature: There are courses on the homepage - In order to compared rendered content to the database - As an acceptance test - I want to count all the chapters, sections, and tabs for each course - - # Commenting these all out for now because they don't always run, - # they have too many prerequesites, e.g. the course exists, and - # is within the start and end dates, etc. - - # Scenario: Navigate through course MITx/3.091x/2012_Fall - # Given I am registered for course "MITx/3.091x/2012_Fall" - # And I log in - # Then I verify all the content of each course - - # Scenario: Navigate through course MITx/6.002x/2012_Fall - # Given I am registered for course "MITx/6.002x/2012_Fall" - # And I log in - # Then I verify all the content of each course - - # Scenario: Navigate through course MITx/6.00x/2012_Fall - # Given I am registered for course "MITx/6.00x/2012_Fall" - # And I log in - # Then I verify all the content of each course - - # Scenario: Navigate through course HarvardX/PH207x/2012_Fall - # Given I am registered for course "HarvardX/PH207x/2012_Fall" - # And I log in - # Then I verify all the content of each course - - # Scenario: Navigate through course BerkeleyX/CS169.1x/2012_Fall - # Given I am registered for course "BerkeleyX/CS169.1x/2012_Fall" - # And I log in - # Then I verify all the content of each course - - # Scenario: Navigate through course BerkeleyX/CS169.2x/2012_Fall - # Given I am registered for course "BerkeleyX/CS169.2x/2012_Fall" - # And I log in - # Then I verify all the content of each course - - # Scenario: Navigate through course BerkeleyX/CS184.1x/2012_Fall - # Given I am registered for course "BerkeleyX/CS184.1x/2012_Fall" - # And I log in - # Then I verify all the content of each course diff --git a/lms/djangoapps/courseware/features/smart-accordion.py b/lms/djangoapps/courseware/features/smart-accordion.py deleted file mode 100644 index 63408d7683..0000000000 --- a/lms/djangoapps/courseware/features/smart-accordion.py +++ /dev/null @@ -1,158 +0,0 @@ -#pylint: disable=C0111 -#pylint: disable=W0621 - -from lettuce import world, step -from re import sub -from nose.tools import assert_equals -from xmodule.modulestore.django import modulestore -from common import * - -from logging import getLogger -logger = getLogger(__name__) - - -def check_for_errors(): - e = world.browser.find_by_css('.outside-app') - if len(e) > 0: - assert False, 'there was a server error at %s' % (world.browser.url) - else: - assert True - - -@step(u'I verify all the content of each course') -def i_verify_all_the_content_of_each_course(step): - all_possible_courses = get_courses() - logger.debug('Courses found:') - for c in all_possible_courses: - logger.debug(c.id) - ids = [c.id for c in all_possible_courses] - - # Get a list of all the registered courses - registered_courses = world.browser.find_by_css('article.my-course') - if len(all_possible_courses) < len(registered_courses): - assert False, "user is registered for more courses than are uniquely posssible" - else: - pass - - for test_course in registered_courses: - test_course.css_click('a') - check_for_errors() - - # Get the course. E.g. 'MITx/6.002x/2012_Fall' - current_course = sub('/info', '', sub('.*/courses/', '', world.browser.url)) - validate_course(current_course, ids) - - world.click_link('Courseware') - assert world.is_css_present('accordion') - check_for_errors() - browse_course(current_course) - - # clicking the user link gets you back to the user's home page - world.css_click('.user-link') - check_for_errors() - - -def browse_course(course_id): - - ## count chapters from xml and page and compare - chapters = get_courseware_with_tabs(course_id) - num_chapters = len(chapters) - - rendered_chapters = world.browser.find_by_css('#accordion > nav > div') - num_rendered_chapters = len(rendered_chapters) - - msg = '%d chapters expected, %d chapters found on page for %s' % (num_chapters, num_rendered_chapters, course_id) - #logger.debug(msg) - assert num_chapters == num_rendered_chapters, msg - - chapter_it = 0 - - ## Iterate the chapters - while chapter_it < num_chapters: - - ## click into a chapter - world.browser.find_by_css('#accordion > nav > div')[chapter_it].find_by_tag('h3').click() - - ## look for the "there was a server error" div - check_for_errors() - - ## count sections from xml and page and compare - sections = chapters[chapter_it]['sections'] - num_sections = len(sections) - - rendered_sections = world.browser.find_by_css('#accordion > nav > div')[chapter_it].find_by_tag('li') - num_rendered_sections = len(rendered_sections) - - msg = ('%d sections expected, %d sections found on page, %s - %d - %s' % - (num_sections, num_rendered_sections, course_id, chapter_it, chapters[chapter_it]['chapter_name'])) - #logger.debug(msg) - assert num_sections == num_rendered_sections, msg - - section_it = 0 - - ## Iterate the sections - while section_it < num_sections: - - ## click on a section - world.browser.find_by_css('#accordion > nav > div')[chapter_it].find_by_tag('li')[section_it].find_by_tag('a').click() - - ## sometimes the course-content takes a long time to load - assert world.is_css_present('.course-content') - - ## look for server error div - check_for_errors() - - ## count tabs from xml and page and compare - - ## count the number of tabs. If number of tabs is 0, there won't be anything rendered - ## so we explicitly set rendered_tabs because otherwise find_elements returns a None object with no length - num_tabs = sections[section_it]['clickable_tab_count'] - if num_tabs != 0: - rendered_tabs = world.browser.find_by_css('ol#sequence-list > li') - num_rendered_tabs = len(rendered_tabs) - else: - rendered_tabs = 0 - num_rendered_tabs = 0 - - msg = ('%d tabs expected, %d tabs found, %s - %d - %s' % - (num_tabs, num_rendered_tabs, course_id, section_it, sections[section_it]['section_name'])) - #logger.debug(msg) - - # Save the HTML to a file for later comparison - world.save_the_course_content('/tmp/%s' % course_id) - - assert num_tabs == num_rendered_tabs, msg - - tabs = sections[section_it]['tabs'] - tab_it = 0 - - ## Iterate the tabs - while tab_it < num_tabs: - - rendered_tabs[tab_it].find_by_tag('a').click() - - ## do something with the tab sections[section_it] - # e = world.browser.find_by_css('section.course-content section') - # process_section(e) - tab_children = tabs[tab_it]['children_count'] - tab_class = tabs[tab_it]['class'] - if tab_children != 0: - rendered_items = world.browser.find_by_css('div#seq_content > section > ol > li > section') - num_rendered_items = len(rendered_items) - msg = ('%d items expected, %d items found, %s - %d - %s - tab %d' % - (tab_children, num_rendered_items, course_id, section_it, sections[section_it]['section_name'], tab_it)) - #logger.debug(msg) - assert tab_children == num_rendered_items, msg - - tab_it += 1 - - section_it += 1 - - chapter_it += 1 - - -def validate_course(current_course, ids): - try: - ids.index(current_course) - except: - assert False, "invalid course id %s" % current_course From 3f9a72e6ce805a63d091cc387b44021d079d46c4 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Fri, 21 Jun 2013 16:32:13 -0400 Subject: [PATCH 314/375] Consolidated imports --- cms/djangoapps/contentstore/tests/test_contentstore.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 514b631521..66fead562e 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -23,7 +23,7 @@ from xmodule.modulestore import Location from xmodule.modulestore.store_utilities import clone_course from xmodule.modulestore.store_utilities import delete_course from xmodule.modulestore.django import modulestore -from xmodule.contentstore.django import contentstore +from xmodule.contentstore.django import contentstore, _CONTENTSTORE from xmodule.templates import update_templates from xmodule.modulestore.xml_exporter import export_to_xml from xmodule.modulestore.xml_importer import import_from_xml, perform_xlint @@ -41,7 +41,6 @@ from xmodule.exceptions import NotFoundError from django_comment_common.utils import are_permissions_roles_seeded from xmodule.exceptions import InvalidVersionError -import xmodule.contentstore.django import datetime from pytz import UTC from uuid import uuid4 @@ -92,7 +91,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): def tearDown(self): mongo = MongoClient() mongo.drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db']) - xmodule.contentstore.django._CONTENTSTORE.clear() + _CONTENTSTORE.clear() def check_components_on_page(self, component_types, expected_types): """ @@ -857,7 +856,7 @@ class ContentStoreTest(ModuleStoreTestCase): def tearDown(self): mongo = MongoClient() mongo.drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db']) - xmodule.contentstore.django._CONTENTSTORE.clear() + _CONTENTSTORE.clear() def test_create_course(self): """Test new course creation - happy path""" From e045860cb652686f8ab5bcaff659a636db6f4d32 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 20 Jun 2013 20:36:51 -0400 Subject: [PATCH 315/375] Pylint complains if you use string, even if you use it for what its still meant to be used for. --- common/djangoapps/external_auth/views.py | 2 +- common/djangoapps/student/views.py | 2 +- common/lib/symmath/symmath/formula.py | 2 +- lms/djangoapps/django_comment_client/tests.py | 2 +- lms/djangoapps/lms_migration/management/commands/create_user.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/common/djangoapps/external_auth/views.py b/common/djangoapps/external_auth/views.py index 93ab70debb..06709eff9e 100644 --- a/common/djangoapps/external_auth/views.py +++ b/common/djangoapps/external_auth/views.py @@ -3,7 +3,7 @@ import json import logging import random import re -import string +import string # pylint: disable=W0402 import fnmatch from textwrap import dedent diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index e065333409..6b9c9104c5 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -4,7 +4,7 @@ import json import logging import random import re -import string +import string # pylint: disable=W0402 import urllib import uuid import time diff --git a/common/lib/symmath/symmath/formula.py b/common/lib/symmath/symmath/formula.py index ca4e20ace3..d5b97a2550 100644 --- a/common/lib/symmath/symmath/formula.py +++ b/common/lib/symmath/symmath/formula.py @@ -10,7 +10,7 @@ # Provides sympy representation. import os -import string +import string # pylint: disable=W0402 import re import logging import operator diff --git a/lms/djangoapps/django_comment_client/tests.py b/lms/djangoapps/django_comment_client/tests.py index 8fd8ed7e2b..8c6a48d8c1 100644 --- a/lms/djangoapps/django_comment_client/tests.py +++ b/lms/djangoapps/django_comment_client/tests.py @@ -1,4 +1,4 @@ -import string +import string # pylint: disable=W0402 import random from django.contrib.auth.models import User diff --git a/lms/djangoapps/lms_migration/management/commands/create_user.py b/lms/djangoapps/lms_migration/management/commands/create_user.py index 87abf4f73a..5d96d96a8a 100644 --- a/lms/djangoapps/lms_migration/management/commands/create_user.py +++ b/lms/djangoapps/lms_migration/management/commands/create_user.py @@ -6,7 +6,7 @@ import os import sys -import string +import string # pylint: disable=W0402 import datetime from getpass import getpass import json From 250de3fcb6856a05e67d7308235083a300c75549 Mon Sep 17 00:00:00 2001 From: cahrens Date: Fri, 21 Jun 2013 17:13:06 -0400 Subject: [PATCH 316/375] Remove keybinding for work in progress (WIP). STUD-329 --- cms/static/js/base.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 92a16b8417..54a90cc476 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -413,12 +413,6 @@ function hideModal(e) { } } -function onKeyUp(e) { - if (e.which == 87) { - $body.toggleClass('show-wip hide-wip'); - } -} - function toggleSock(e) { e.preventDefault(); From df4b512b6f2651fea6894d4f1ab9e923eaec2bd4 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 21 Jun 2013 10:42:17 -0400 Subject: [PATCH 317/375] Change wildcard imports into specific imports --- common/djangoapps/heartbeat/urls.py | 2 +- common/djangoapps/student/admin.py | 4 ++-- common/djangoapps/track/admin.py | 2 +- lms/djangoapps/courseware/admin.py | 2 +- lms/djangoapps/django_comment_client/helpers.py | 2 +- .../instructor/management/commands/compute_grades.py | 2 +- lms/djangoapps/psychometrics/admin.py | 2 +- .../psychometrics/management/commands/init_psychometrics.py | 6 +++--- lms/djangoapps/psychometrics/psychoanalyze.py | 5 +++-- lms/lib/comment_client/comment.py | 4 ++-- lms/lib/comment_client/comment_client.py | 2 +- lms/lib/comment_client/commentable.py | 2 -- lms/lib/comment_client/thread.py | 3 ++- lms/lib/comment_client/user.py | 2 +- 14 files changed, 20 insertions(+), 20 deletions(-) diff --git a/common/djangoapps/heartbeat/urls.py b/common/djangoapps/heartbeat/urls.py index 3f45a95dd2..6a0be757c9 100644 --- a/common/djangoapps/heartbeat/urls.py +++ b/common/djangoapps/heartbeat/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import * +from django.conf.urls import url, patterns urlpatterns = patterns('', # nopep8 url(r'^$', 'heartbeat.views.heartbeat', name='heartbeat'), diff --git a/common/djangoapps/student/admin.py b/common/djangoapps/student/admin.py index 64fe844801..4d6976d7d4 100644 --- a/common/djangoapps/student/admin.py +++ b/common/djangoapps/student/admin.py @@ -2,9 +2,9 @@ django admin pages for courseware model ''' -from student.models import * +from student.models import UserProfile, UserTestGroup, CourseEnrollmentAllowed +from student.models import CourseEnrollment, Registration, PendingNameChange from django.contrib import admin -from django.contrib.auth.models import User admin.site.register(UserProfile) diff --git a/common/djangoapps/track/admin.py b/common/djangoapps/track/admin.py index 1f19c59a93..d75f206846 100644 --- a/common/djangoapps/track/admin.py +++ b/common/djangoapps/track/admin.py @@ -2,7 +2,7 @@ django admin pages for courseware model ''' -from track.models import * +from track.models import TrackingLog from django.contrib import admin admin.site.register(TrackingLog) diff --git a/lms/djangoapps/courseware/admin.py b/lms/djangoapps/courseware/admin.py index 9ef4c1de20..743d1fed52 100644 --- a/lms/djangoapps/courseware/admin.py +++ b/lms/djangoapps/courseware/admin.py @@ -2,7 +2,7 @@ django admin pages for courseware model ''' -from courseware.models import * +from courseware.models import StudentModule, OfflineComputedGrade, OfflineComputedGradeLog from django.contrib import admin from django.contrib.auth.models import User diff --git a/lms/djangoapps/django_comment_client/helpers.py b/lms/djangoapps/django_comment_client/helpers.py index a8a51ad95c..1310c4e0c1 100644 --- a/lms/djangoapps/django_comment_client/helpers.py +++ b/lms/djangoapps/django_comment_client/helpers.py @@ -2,7 +2,7 @@ from django.conf import settings from .mustache_helpers import mustache_helpers from functools import partial -from .utils import * +from .utils import extend_content, merge_dict, render_mustache import django_comment_client.settings as cc_settings import pystache_custom as pystache diff --git a/lms/djangoapps/instructor/management/commands/compute_grades.py b/lms/djangoapps/instructor/management/commands/compute_grades.py index 4518450e39..d1c66d51d2 100644 --- a/lms/djangoapps/instructor/management/commands/compute_grades.py +++ b/lms/djangoapps/instructor/management/commands/compute_grades.py @@ -3,7 +3,7 @@ # django management command: dump grades to csv files # for use by batch processes -from instructor.offline_gradecalc import * +from instructor.offline_gradecalc import offline_grade_calculation from courseware.courses import get_course_by_id from xmodule.modulestore.django import modulestore diff --git a/lms/djangoapps/psychometrics/admin.py b/lms/djangoapps/psychometrics/admin.py index ff1a14d722..b7c04b5069 100644 --- a/lms/djangoapps/psychometrics/admin.py +++ b/lms/djangoapps/psychometrics/admin.py @@ -2,7 +2,7 @@ django admin pages for courseware model ''' -from psychometrics.models import * +from psychometrics.models import PsychometricData from django.contrib import admin admin.site.register(PsychometricData) diff --git a/lms/djangoapps/psychometrics/management/commands/init_psychometrics.py b/lms/djangoapps/psychometrics/management/commands/init_psychometrics.py index 87e62f4a2c..f9cfbd28f5 100644 --- a/lms/djangoapps/psychometrics/management/commands/init_psychometrics.py +++ b/lms/djangoapps/psychometrics/management/commands/init_psychometrics.py @@ -4,9 +4,9 @@ import json -from courseware.models import * -from track.models import * -from psychometrics.models import * +from courseware.models import StudentModule +from track.models import TrackingLog +from psychometrics.models import PsychometricData from xmodule.modulestore import Location from django.conf import settings diff --git a/lms/djangoapps/psychometrics/psychoanalyze.py b/lms/djangoapps/psychometrics/psychoanalyze.py index ab9a5e6242..c6e66445a4 100644 --- a/lms/djangoapps/psychometrics/psychoanalyze.py +++ b/lms/djangoapps/psychometrics/psychoanalyze.py @@ -14,7 +14,8 @@ from scipy.optimize import curve_fit from django.conf import settings from django.db.models import Sum, Max -from psychometrics.models import * +from psychometrics.models import PsychometricData +from courseware.models import StudentModule from pytz import UTC log = logging.getLogger("mitx.psychometrics") @@ -303,7 +304,7 @@ def generate_plots_for_problem(problem): def make_psychometrics_data_update_handler(course_id, user, module_state_key): """ Construct and return a procedure which may be called to update - the PsychometricsData instance for the given StudentModule instance. + the PsychometricData instance for the given StudentModule instance. """ sm, status = StudentModule.objects.get_or_create( course_id=course_id, diff --git a/lms/lib/comment_client/comment.py b/lms/lib/comment_client/comment.py index fb5a4ad0c3..fd68d5cdeb 100644 --- a/lms/lib/comment_client/comment.py +++ b/lms/lib/comment_client/comment.py @@ -1,6 +1,6 @@ -from .utils import * +from .utils import CommentClientError, perform_request -from .thread import Thread +from .thread import Thread, _url_for_flag_abuse_thread, _url_for_unflag_abuse_thread import models import settings diff --git a/lms/lib/comment_client/comment_client.py b/lms/lib/comment_client/comment_client.py index d91c5ea47f..4f660533f1 100644 --- a/lms/lib/comment_client/comment_client.py +++ b/lms/lib/comment_client/comment_client.py @@ -5,7 +5,7 @@ from .thread import Thread from .user import User from .commentable import Commentable -from .utils import * +from .utils import perform_request import settings diff --git a/lms/lib/comment_client/commentable.py b/lms/lib/comment_client/commentable.py index 111809f8f0..05efd70e50 100644 --- a/lms/lib/comment_client/commentable.py +++ b/lms/lib/comment_client/commentable.py @@ -1,5 +1,3 @@ -from .utils import * - import models import settings diff --git a/lms/lib/comment_client/thread.py b/lms/lib/comment_client/thread.py index 0b0be576b8..00d5f01814 100644 --- a/lms/lib/comment_client/thread.py +++ b/lms/lib/comment_client/thread.py @@ -1,4 +1,5 @@ -from .utils import * +from .utils import merge_dict, strip_blank, strip_none, extract, perform_request +from .utils import CommentClientError import models import settings diff --git a/lms/lib/comment_client/user.py b/lms/lib/comment_client/user.py index a9e47fe6aa..2370052d90 100644 --- a/lms/lib/comment_client/user.py +++ b/lms/lib/comment_client/user.py @@ -1,4 +1,4 @@ -from .utils import * +from .utils import merge_dict, perform_request, CommentClientError import models import settings From 75b390124f402b3a1519ee6a9b40e3827c155f2d Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 21 Jun 2013 10:42:49 -0400 Subject: [PATCH 318/375] Tweaks to our pylintrc rules. --- pylintrc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pylintrc b/pylintrc index af958e4af4..dea0f240c6 100644 --- a/pylintrc +++ b/pylintrc @@ -41,6 +41,10 @@ disable= # W0142: Used * or ** magic I0011,C0301,W0141,W0142, +# Django makes classes that trigger these +# W0232: Class has no __init__ method + W0232, + # Might use these when the code is in better shape # C0302: Too many lines in module # R0201: Method could be a function From 57909ce1aaba363645eef900fd1760d7aa276327 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 21 Jun 2013 10:55:36 -0400 Subject: [PATCH 319/375] Fix all W0602, global used but no assignment done. --- .../djangoapps/student/management/commands/massemailtxt.py | 1 - common/lib/xmodule/xmodule/contentstore/django.py | 2 -- common/lib/xmodule/xmodule/modulestore/django.py | 2 -- lms/djangoapps/django_comment_client/utils.py | 6 ------ 4 files changed, 11 deletions(-) diff --git a/common/djangoapps/student/management/commands/massemailtxt.py b/common/djangoapps/student/management/commands/massemailtxt.py index fec354e974..ae25430a85 100644 --- a/common/djangoapps/student/management/commands/massemailtxt.py +++ b/common/djangoapps/student/management/commands/massemailtxt.py @@ -37,7 +37,6 @@ rate -- messages per second self.log_file.write(datetime.datetime.utcnow().isoformat() + ' -- ' + text + '\n') def handle(self, *args, **options): - global log_file (user_file, message_base, logfilename, ratestr) = args users = [u.strip() for u in open(user_file).readlines()] diff --git a/common/lib/xmodule/xmodule/contentstore/django.py b/common/lib/xmodule/xmodule/contentstore/django.py index f163348cc8..25a5d7912f 100644 --- a/common/lib/xmodule/xmodule/contentstore/django.py +++ b/common/lib/xmodule/xmodule/contentstore/django.py @@ -18,8 +18,6 @@ def load_function(path): def contentstore(name='default'): - global _CONTENTSTORE - if name not in _CONTENTSTORE: class_ = load_function(settings.CONTENTSTORE['ENGINE']) options = {} diff --git a/common/lib/xmodule/xmodule/modulestore/django.py b/common/lib/xmodule/xmodule/modulestore/django.py index a2e2a4a5a5..c98e6cadef 100644 --- a/common/lib/xmodule/xmodule/modulestore/django.py +++ b/common/lib/xmodule/xmodule/modulestore/django.py @@ -26,8 +26,6 @@ def load_function(path): def modulestore(name='default'): - global _MODULESTORES - if name not in _MODULESTORES: class_ = load_function(settings.MODULESTORE[name]['ENGINE']) diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py index 496c834950..6668826b67 100644 --- a/lms/djangoapps/django_comment_client/utils.py +++ b/lms/djangoapps/django_comment_client/utils.py @@ -73,21 +73,17 @@ def get_discussion_id_map(course): """ return a dict of the form {category: modules} """ - global _DISCUSSIONINFO initialize_discussion_info(course) return _DISCUSSIONINFO[course.id]['id_map'] def get_discussion_title(course, discussion_id): - global _DISCUSSIONINFO initialize_discussion_info(course) title = _DISCUSSIONINFO[course.id]['id_map'].get(discussion_id, {}).get('title', '(no title)') return title def get_discussion_category_map(course): - - global _DISCUSSIONINFO initialize_discussion_info(course) return filter_unstarted_categories(_DISCUSSIONINFO[course.id]['category_map']) @@ -141,8 +137,6 @@ def sort_map_entries(category_map): def initialize_discussion_info(course): - global _DISCUSSIONINFO - course_id = course.id discussion_id_map = {} From 45815e2d03f3a53defbf629bda73c653935f28db Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 21 Jun 2013 14:03:56 -0400 Subject: [PATCH 320/375] Remove obsolete file comment_client/legacy.py --- lms/lib/comment_client/legacy.py | 226 ------------------------------- 1 file changed, 226 deletions(-) delete mode 100644 lms/lib/comment_client/legacy.py diff --git a/lms/lib/comment_client/legacy.py b/lms/lib/comment_client/legacy.py deleted file mode 100644 index de7ce201ce..0000000000 --- a/lms/lib/comment_client/legacy.py +++ /dev/null @@ -1,226 +0,0 @@ -def delete_threads(commentable_id, *args, **kwargs): - return _perform_request('delete', _url_for_commentable_threads(commentable_id), *args, **kwargs) - - -def get_threads(commentable_id, recursive=False, query_params={}, *args, **kwargs): - default_params = {'page': 1, 'per_page': 20, 'recursive': recursive} - attributes = dict(default_params.items() + query_params.items()) - response = _perform_request('get', _url_for_threads(commentable_id), attributes, *args, **kwargs) - return response.get('collection', []), response.get('page', 1), response.get('num_pages', 1) - - -def search_threads(course_id, recursive=False, query_params={}, *args, **kwargs): - default_params = {'page': 1, 'per_page': 20, 'course_id': course_id, 'recursive': recursive} - attributes = dict(default_params.items() + query_params.items()) - response = _perform_request('get', _url_for_search_threads(), attributes, *args, **kwargs) - return response.get('collection', []), response.get('page', 1), response.get('num_pages', 1) - - -def search_similar_threads(course_id, recursive=False, query_params={}, *args, **kwargs): - default_params = {'course_id': course_id, 'recursive': recursive} - attributes = dict(default_params.items() + query_params.items()) - return _perform_request('get', _url_for_search_similar_threads(), attributes, *args, **kwargs) - - -def search_recent_active_threads(course_id, recursive=False, query_params={}, *args, **kwargs): - default_params = {'course_id': course_id, 'recursive': recursive} - attributes = dict(default_params.items() + query_params.items()) - return _perform_request('get', _url_for_search_recent_active_threads(), attributes, *args, **kwargs) - - -def search_trending_tags(course_id, query_params={}, *args, **kwargs): - default_params = {'course_id': course_id} - attributes = dict(default_params.items() + query_params.items()) - return _perform_request('get', _url_for_search_trending_tags(), attributes, *args, **kwargs) - - -def create_user(attributes, *args, **kwargs): - return _perform_request('post', _url_for_users(), attributes, *args, **kwargs) - - -def update_user(user_id, attributes, *args, **kwargs): - return _perform_request('put', _url_for_user(user_id), attributes, *args, **kwargs) - - -def get_threads_tags(*args, **kwargs): - return _perform_request('get', _url_for_threads_tags(), {}, *args, **kwargs) - - -def tags_autocomplete(value, *args, **kwargs): - return _perform_request('get', _url_for_threads_tags_autocomplete(), {'value': value}, *args, **kwargs) - - -def create_thread(commentable_id, attributes, *args, **kwargs): - return _perform_request('post', _url_for_threads(commentable_id), attributes, *args, **kwargs) - - -def get_thread(thread_id, recursive=False, *args, **kwargs): - return _perform_request('get', _url_for_thread(thread_id), {'recursive': recursive}, *args, **kwargs) - - -def update_thread(thread_id, attributes, *args, **kwargs): - return _perform_request('put', _url_for_thread(thread_id), attributes, *args, **kwargs) - - -def create_comment(thread_id, attributes, *args, **kwargs): - return _perform_request('post', _url_for_thread_comments(thread_id), attributes, *args, **kwargs) - - -def delete_thread(thread_id, *args, **kwargs): - return _perform_request('delete', _url_for_thread(thread_id), *args, **kwargs) - - -def get_comment(comment_id, recursive=False, *args, **kwargs): - return _perform_request('get', _url_for_comment(comment_id), {'recursive': recursive}, *args, **kwargs) - - -def update_comment(comment_id, attributes, *args, **kwargs): - return _perform_request('put', _url_for_comment(comment_id), attributes, *args, **kwargs) - - -def create_sub_comment(comment_id, attributes, *args, **kwargs): - return _perform_request('post', _url_for_comment(comment_id), attributes, *args, **kwargs) - - -def delete_comment(comment_id, *args, **kwargs): - return _perform_request('delete', _url_for_comment(comment_id), *args, **kwargs) - - -def vote_for_comment(comment_id, user_id, value, *args, **kwargs): - return _perform_request('put', _url_for_vote_comment(comment_id), {'user_id': user_id, 'value': value}, *args, **kwargs) - - -def undo_vote_for_comment(comment_id, user_id, *args, **kwargs): - return _perform_request('delete', _url_for_vote_comment(comment_id), {'user_id': user_id}, *args, **kwargs) - - -def vote_for_thread(thread_id, user_id, value, *args, **kwargs): - return _perform_request('put', _url_for_vote_thread(thread_id), {'user_id': user_id, 'value': value}, *args, **kwargs) - - -def undo_vote_for_thread(thread_id, user_id, *args, **kwargs): - return _perform_request('delete', _url_for_vote_thread(thread_id), {'user_id': user_id}, *args, **kwargs) - - -def get_notifications(user_id, *args, **kwargs): - return _perform_request('get', _url_for_notifications(user_id), *args, **kwargs) - - -def get_user_info(user_id, complete=True, *args, **kwargs): - return _perform_request('get', _url_for_user(user_id), {'complete': complete}, *args, **kwargs) - - -def subscribe(user_id, subscription_detail, *args, **kwargs): - return _perform_request('post', _url_for_subscription(user_id), subscription_detail, *args, **kwargs) - - -def subscribe_user(user_id, followed_user_id, *args, **kwargs): - return subscribe(user_id, {'source_type': 'user', 'source_id': followed_user_id}) - -follow = subscribe_user - - -def subscribe_thread(user_id, thread_id, *args, **kwargs): - return subscribe(user_id, {'source_type': 'thread', 'source_id': thread_id}) - - -def subscribe_commentable(user_id, commentable_id, *args, **kwargs): - return subscribe(user_id, {'source_type': 'other', 'source_id': commentable_id}) - - -def unsubscribe(user_id, subscription_detail, *args, **kwargs): - return _perform_request('delete', _url_for_subscription(user_id), subscription_detail, *args, **kwargs) - - -def unsubscribe_user(user_id, followed_user_id, *args, **kwargs): - return unsubscribe(user_id, {'source_type': 'user', 'source_id': followed_user_id}) - -unfollow = unsubscribe_user - - -def unsubscribe_thread(user_id, thread_id, *args, **kwargs): - return unsubscribe(user_id, {'source_type': 'thread', 'source_id': thread_id}) - - -def unsubscribe_commentable(user_id, commentable_id, *args, **kwargs): - return unsubscribe(user_id, {'source_type': 'other', 'source_id': commentable_id}) - - -def _perform_request(method, url, data_or_params=None, *args, **kwargs): - if method in ['post', 'put', 'patch']: - response = requests.request(method, url, data=data_or_params) - else: - response = requests.request(method, url, params=data_or_params) - if 200 < response.status_code < 500: - raise CommentClientError(response.text) - elif response.status_code == 500: - raise CommentClientUnknownError(response.text) - else: - if kwargs.get("raw", False): - return response.text - else: - return json.loads(response.text) - - -def _url_for_threads(commentable_id): - return "{prefix}/{commentable_id}/threads".format(prefix=PREFIX, commentable_id=commentable_id) - - -def _url_for_thread(thread_id): - return "{prefix}/threads/{thread_id}".format(prefix=PREFIX, thread_id=thread_id) - - -def _url_for_thread_comments(thread_id): - return "{prefix}/threads/{thread_id}/comments".format(prefix=PREFIX, thread_id=thread_id) - - -def _url_for_comment(comment_id): - return "{prefix}/comments/{comment_id}".format(prefix=PREFIX, comment_id=comment_id) - - -def _url_for_vote_comment(comment_id): - return "{prefix}/comments/{comment_id}/votes".format(prefix=PREFIX, comment_id=comment_id) - - -def _url_for_vote_thread(thread_id): - return "{prefix}/threads/{thread_id}/votes".format(prefix=PREFIX, thread_id=thread_id) - - -def _url_for_notifications(user_id): - return "{prefix}/users/{user_id}/notifications".format(prefix=PREFIX, user_id=user_id) - - -def _url_for_subscription(user_id): - return "{prefix}/users/{user_id}/subscriptions".format(prefix=PREFIX, user_id=user_id) - - -def _url_for_user(user_id): - return "{prefix}/users/{user_id}".format(prefix=PREFIX, user_id=user_id) - - -def _url_for_search_threads(): - return "{prefix}/search/threads".format(prefix=PREFIX) - - -def _url_for_search_similar_threads(): - return "{prefix}/search/threads/more_like_this".format(prefix=PREFIX) - - -def _url_for_search_recent_active_threads(): - return "{prefix}/search/threads/recent_active".format(prefix=PREFIX) - - -def _url_for_search_trending_tags(): - return "{prefix}/search/tags/trending".format(prefix=PREFIX) - - -def _url_for_threads_tags(): - return "{prefix}/threads/tags".format(prefix=PREFIX) - - -def _url_for_threads_tags_autocomplete(): - return "{prefix}/threads/tags/autocomplete".format(prefix=PREFIX) - - -def _url_for_users(): - return "{prefix}/users".format(prefix=PREFIX) From 5a5d425eb348e2c646037879d54c997c00b4bf6f Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 21 Jun 2013 14:41:16 -0400 Subject: [PATCH 321/375] Files that may not exist need F0401 suppressed during import. --- cms/envs/dev.py | 2 +- lms/envs/dev.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cms/envs/dev.py b/cms/envs/dev.py index 07630bdf31..2dcb3640ca 100644 --- a/cms/envs/dev.py +++ b/cms/envs/dev.py @@ -181,6 +181,6 @@ if SEGMENT_IO_KEY: ##################################################################### # Lastly, see if the developer has any local overrides. try: - from .private import * + from .private import * # pylint: disable=F0401 except ImportError: pass diff --git a/lms/envs/dev.py b/lms/envs/dev.py index b1519b77bc..813f9cf32c 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -258,6 +258,6 @@ if SEGMENT_IO_LMS_KEY: ##################################################################### # Lastly, see if the developer has any local overrides. try: - from .private import * + from .private import * # pylint: disable=F0401 except ImportError: pass From fa9a8f4af09a27bd88aeea33a81ec0f5086d9363 Mon Sep 17 00:00:00 2001 From: Julian Arni Date: Fri, 21 Jun 2013 18:00:30 -0400 Subject: [PATCH 322/375] Greater dir naming flexibility. Accepts any dirname for the edx-platform repo. Allows the script to be called from any directory, not just $BASE/edx-platform. --- scripts/create-dev-env.sh | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/scripts/create-dev-env.sh b/scripts/create-dev-env.sh index edb0bcdcae..0816b72d21 100755 --- a/scripts/create-dev-env.sh +++ b/scripts/create-dev-env.sh @@ -98,19 +98,23 @@ clone_repos() { set_base_default() { # if PROJECT_HOME not set # 2 possibilities: this is from cloned repo, or not - # this script is in "./scripts" if a git clone - this_repo=$(cd "${BASH_SOURCE%/*}/.." && pwd) - if [[ "${this_repo##*/}" = "edx-platform" && -d "$this_repo/.git" ]]; then - # set BASE one-up from this_repo; - echo "${this_repo%/*}" + + # See if remote's url is named edx-platform (this works for forks too, but + # not if the name was changed). + cd "$( dirname "${BASH_SOURCE[0]}" )" + this_repo=$(basename $(git ls-remote --get-url 2>/dev/null) 2>/dev/null) || + echo -n "" + + if [[ "x$this_repo" = "xedx-platform.git" ]]; then + # We are in the edx repo and already have git installed. Let git do the + # work of finding base dir: + echo "$(dirname $(git rev-parse --show-toplevel))" else echo "$HOME/edx_all" fi } - - ### START PROG=${0##*/} From 2eefa494b145610eb8b9a2037ee3be7aa0daba51 Mon Sep 17 00:00:00 2001 From: Jason Bau Date: Fri, 21 Jun 2013 23:46:56 -0700 Subject: [PATCH 323/375] Width of labels for multiple-choice capa problems = width of text Stanford got some feedback from our students/faculty that students were making accidental clicks on radio-button capa questions. They would click way to the right of the label text, but it would still select the corresponding input, which caused some students to make unintentional changes to their answers. This was because labels for these inputs were display:block and width:100% Changing these labels to float:left clear:both should fix it. --- lms/static/sass/course/base/_base.scss | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lms/static/sass/course/base/_base.scss b/lms/static/sass/course/base/_base.scss index 6d87b7f554..a1c948d4f5 100644 --- a/lms/static/sass/course/base/_base.scss +++ b/lms/static/sass/course/base/_base.scss @@ -46,6 +46,13 @@ form { } } +form.choicegroup { + label { + clear: both; + float: left; + } +} + textarea, input[type="text"], input[type="email"], From 85b4a4ccab37e14e6f0543f8b7165e667c0768ef Mon Sep 17 00:00:00 2001 From: Alexander Kryklia Date: Sat, 22 Jun 2013 16:13:40 +0300 Subject: [PATCH 324/375] removes choiceresponse wiping after clicking Show Answer --- common/lib/xmodule/xmodule/js/src/capa/display.coffee | 3 --- 1 file changed, 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index 987d20b65a..f773fc81c4 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -364,8 +364,6 @@ class @Problem choicegroup: (element, display, answers) => element = $(element) - element.find('input').attr('disabled', 'disabled') - input_id = element.attr('id').replace(/inputtype_/,'') answer = answers[input_id] for choice in answer @@ -379,7 +377,6 @@ class @Problem inputtypeHideAnswerMethods: choicegroup: (element, display) => element = $(element) - element.find('input').attr('disabled', null) element.find('label').removeClass('choicegroup_correct') javascriptinput: (element, display) => From 4a98e2eda75b1a8b036e4f3f5e035c5049aab776 Mon Sep 17 00:00:00 2001 From: Jason Bau Date: Wed, 5 Jun 2013 23:14:18 -0700 Subject: [PATCH 325/375] Moves user activation away from just clicking on reset password To following the link in the password reset email --- common/djangoapps/student/forms.py | 72 +++++++++++++++++++ common/djangoapps/student/views.py | 31 ++++---- .../registration/password_reset_email.html | 2 +- lms/urls.py | 2 +- 4 files changed, 93 insertions(+), 14 deletions(-) create mode 100644 common/djangoapps/student/forms.py diff --git a/common/djangoapps/student/forms.py b/common/djangoapps/student/forms.py new file mode 100644 index 0000000000..75c89e0a26 --- /dev/null +++ b/common/djangoapps/student/forms.py @@ -0,0 +1,72 @@ +from django import forms +from django.utils.translation import ugettext, ugettext_lazy as _ +from django.template import loader +from django.contrib.auth.models import User +from django.contrib.auth.hashers import UNUSABLE_PASSWORD, is_password_usable, get_hasher +from django.contrib.auth.tokens import default_token_generator +from django.contrib.sites.models import get_current_site +from django.utils.http import int_to_base36 + + + +# This is a literal copy from Django 1.4.5's django.contrib.auth.forms.PasswordResetForm +# I think copy-and-paste here is somewhat better than subclassing and +# just changing the definition of clean_email, because it's less +# likely to be broken by incompatibility with a new django version. +# (If this form is good enough now, a snapshot of it ought to last a while) + +class PasswordResetFormNoActive(forms.Form): + error_messages = { + 'unknown': _("That e-mail address doesn't have an associated " + "user account. Are you sure you've registered?"), + 'unusable': _("The user account associated with this e-mail " + "address cannot reset the password."), + } + email = forms.EmailField(label=_("E-mail"), max_length=75) + + def clean_email(self): + """ + Validates that an active user exists with the given email address. + """ + email = self.cleaned_data["email"] + #The line below contains the only change, removing is_active=True + self.users_cache = User.objects.filter(email__iexact=email) + if not len(self.users_cache): + raise forms.ValidationError(self.error_messages['unknown']) + if any((user.password == UNUSABLE_PASSWORD) + for user in self.users_cache): + raise forms.ValidationError(self.error_messages['unusable']) + return email + + def save(self, domain_override=None, + subject_template_name='registration/password_reset_subject.txt', + email_template_name='registration/password_reset_email.html', + use_https=False, token_generator=default_token_generator, + from_email=None, request=None): + """ + Generates a one-use only link for resetting password and sends to the + user. + """ + from django.core.mail import send_mail + for user in self.users_cache: + if not domain_override: + current_site = get_current_site(request) + site_name = current_site.name + domain = current_site.domain + else: + site_name = domain = domain_override + c = { + 'email': user.email, + 'domain': domain, + 'site_name': site_name, + 'uid': int_to_base36(user.id), + 'user': user, + 'token': token_generator.make_token(user), + 'protocol': use_https and 'https' or 'http', + } + subject = loader.render_to_string(subject_template_name, c) + # Email subject *must not* contain newlines + subject = ''.join(subject.splitlines()) + email = loader.render_to_string(email_template_name, c) + send_mail(subject, email, from_email, [user.email]) + diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index e065333409..50f6d90368 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -11,9 +11,9 @@ import time from django.conf import settings from django.contrib.auth import logout, authenticate, login -from django.contrib.auth.forms import PasswordResetForm from django.contrib.auth.models import User from django.contrib.auth.decorators import login_required +from django.contrib.auth.views import password_reset_confirm from django.core.cache import cache from django.core.context_processors import csrf from django.core.mail import send_mail @@ -24,6 +24,7 @@ from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbid from django.shortcuts import redirect from django_future.csrf import ensure_csrf_cookie from django.utils.http import cookie_date +from django.utils.http import base36_to_int from mitxmako.shortcuts import render_to_response, render_to_string from bs4 import BeautifulSoup @@ -34,6 +35,8 @@ from student.models import (Registration, UserProfile, TestCenterUser, TestCente CourseEnrollment, unique_id_for_user, get_testcenter_registration, CourseEnrollmentAllowed) +from student.forms import PasswordResetFormNoActive + from certificates.models import CertificateStatuses, certificate_status_for_student from xmodule.course_module import CourseDescriptor @@ -962,17 +965,7 @@ def password_reset(request): if request.method != "POST": raise Http404 - # By default, Django doesn't allow Users with is_active = False to reset their passwords, - # but this bites people who signed up a long time ago, never activated, and forgot their - # password. So for their sake, we'll auto-activate a user for whom password_reset is called. - try: - user = User.objects.get(email=request.POST['email']) - user.is_active = True - user.save() - except: - log.exception("Tried to auto-activate user to enable password reset, but failed.") - - form = PasswordResetForm(request.POST) + form = PasswordResetFormNoActive(request.POST) if form.is_valid(): form.save(use_https=request.is_secure(), from_email=settings.DEFAULT_FROM_EMAIL, @@ -984,6 +977,20 @@ def password_reset(request): return HttpResponse(json.dumps({'success': False, 'error': 'Invalid e-mail'})) +def password_reset_confirm_wrapper(request, uidb36=None, token=None): + ''' A wrapper around django.contrib.auth.views.password_reset_confirm. + Needed because we want to set the user as active at this step. + ''' + #cribbed from django.contrib.auth.views.password_reset_confirm + try: + uid_int = base36_to_int(uidb36) + user = User.objects.get(id=uid_int) + user.is_active = True + user.save() + except (ValueError, User.DoesNotExist): + pass + return password_reset_confirm(request, uidb36=uidb36, token=token) + def reactivation_email_for_user(user): try: diff --git a/lms/templates/registration/password_reset_email.html b/lms/templates/registration/password_reset_email.html index bf6c3e0891..68073d9ddd 100644 --- a/lms/templates/registration/password_reset_email.html +++ b/lms/templates/registration/password_reset_email.html @@ -3,7 +3,7 @@ {% trans "Please go to the following page and choose a new password:" %} {% block reset_link %} -https://{{domain}}{% url 'django.contrib.auth.views.password_reset_confirm' uidb36=uid token=token %} +https://{{domain}}{% url 'student.views.password_reset_confirm_wrapper' uidb36=uid token=token %} {% endblock %} If you didn't request this change, you can disregard this email - we have not yet reset your password. diff --git a/lms/urls.py b/lms/urls.py index 52ce539f73..50ce35cde0 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -51,7 +51,7 @@ urlpatterns = ('', # nopep8 url(r'^password_change_done/$', django.contrib.auth.views.password_change_done, name='auth_password_change_done'), url(r'^password_reset_confirm/(?P[0-9A-Za-z]+)-(?P.+)/$', - django.contrib.auth.views.password_reset_confirm, + 'student.views.password_reset_confirm_wrapper', name='auth_password_reset_confirm'), url(r'^password_reset_complete/$', django.contrib.auth.views.password_reset_complete, name='auth_password_reset_complete'), From ad6e7457625965386931bb8d43836d3362034468 Mon Sep 17 00:00:00 2001 From: cahrens Date: Mon, 24 Jun 2013 09:09:05 -0400 Subject: [PATCH 326/375] Forgot to remove registration of onKeyUp function. --- cms/static/js/base.js | 1 - 1 file changed, 1 deletion(-) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 54a90cc476..d1cffdc427 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -25,7 +25,6 @@ $(document).ready(function() { $newComponentTemplatePickers = $('.new-component-templates'); $newComponentButton = $('.new-component-button'); $spinner = $(''); - $body.bind('keyup', onKeyUp); $('.expand-collapse-icon').bind('click', toggleSubmodules); $('.visibility-options').bind('change', setVisibility); From 72e08456a8dcebd61e8a3476504c25209546f780 Mon Sep 17 00:00:00 2001 From: Peter Fogg Date: Fri, 21 Jun 2013 16:50:32 -0400 Subject: [PATCH 327/375] Refactor Advanced Settings page to use Backbone notifications. --- cms/static/js/views/settings/advanced_view.js | 91 ++++++++++--------- cms/templates/base.html | 6 -- cms/templates/settings_advanced.html | 57 ------------ 3 files changed, 46 insertions(+), 108 deletions(-) diff --git a/cms/static/js/views/settings/advanced_view.js b/cms/static/js/views/settings/advanced_view.js index 863393d341..69a2c9f622 100644 --- a/cms/static/js/views/settings/advanced_view.js +++ b/cms/static/js/views/settings/advanced_view.js @@ -20,9 +20,6 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ self.render(); } ); - // because these are outside of this.$el, they can't be in the event hash - $('.save-button').on('click', this, this.saveView); - $('.cancel-button').on('click', this, this.revertView); this.listenTo(this.model, 'invalid', this.handleValidationError); }, render: function() { @@ -45,7 +42,6 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ var policyValues = listEle$.find('.json'); _.each(policyValues, this.attachJSONEditor, this); - this.showMessage(); return this; }, attachJSONEditor : function (textarea) { @@ -61,7 +57,9 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ mode: "application/json", lineNumbers: false, lineWrapping: false, onChange: function(instance, changeobj) { // this event's being called even when there's no change :-( - if (instance.getValue() !== oldValue) self.showSaveCancelButtons(); + if (instance.getValue() !== oldValue && !self.notificationBarShowing) { + self.showNotificationBar(); + } }, onFocus : function(mirror) { $(textarea).parent().children('label').addClass("is-focused"); @@ -99,59 +97,62 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ } }); }, - showMessage: function (type) { - $(".wrapper-alert").removeClass("is-shown"); - if (type) { - if (type === this.error_saving) { - $(".wrapper-alert-error").addClass("is-shown").attr('aria-hidden','false'); - } - else if (type === this.successful_changes) { - $(".wrapper-alert-confirmation").addClass("is-shown").attr('aria-hidden','false'); - this.hideSaveCancelButtons(); - } - } - else { - // This is the case of the page first rendering, or when Cancel is pressed. - this.hideSaveCancelButtons(); - } + showNotificationBar: function() { + var self = this; + var message = gettext("Your changes will not take effect until you save your progress. Take care with key and value formatting, as validation is not implemented.") + var confirm = new CMS.Views.Notification.Warning({ + title: gettext("You've Made Some Changes"), + message: message, + actions: { + primary: { + "text": gettext("Save Changes"), + "class": "action-save", + "click": function() { + self.saveView(); + confirm.hide(); + self.notificationBarShowing = false; + } + }, + secondary: [{ + "text": gettext("Cancel"), + "class": "action-cancel", + "click": function() { + self.revertView(); + confirm.hide(); + self.notificationBarShowing = false; + } + }], + }}); + this.notificationBarShowing = true; + confirm.show(); }, - showSaveCancelButtons: function(event) { - if (!this.notificationBarShowing) { - this.$el.find(".message-status").removeClass("is-shown"); - $('.wrapper-notification').removeClass('is-hiding').addClass('is-shown').attr('aria-hidden','false'); - this.notificationBarShowing = true; - } - }, - hideSaveCancelButtons: function() { - if (this.notificationBarShowing) { - $('.wrapper-notification').removeClass('is-shown').addClass('is-hiding').attr('aria-hidden','true'); - this.notificationBarShowing = false; - } - }, - saveView : function(event) { - window.CmsUtils.smoothScrollTop(event); + saveView : function() { // TODO one last verification scan: // call validateKey on each to ensure proper format // check for dupes - var self = event.data; - self.model.save({}, + var self = this; + this.model.save({}, { success : function() { self.render(); - self.showMessage(self.successful_changes); + var message = gettext("Please note that validation of your policy key and value pairs is not currently in place yet. If you are having difficulties, please review your policy pairs."); + var saving = new CMS.Views.Alert.Confirmation({ + title: gettext("Your policy changes have been saved."), + message: message, + closeIcon: false + }); + saving.show(); analytics.track('Saved Advanced Settings', { 'course': course_location_analytics }); - } }); }, - revertView : function(event) { - event.preventDefault(); - var self = event.data; - self.model.deleteKeys = []; - self.model.clear({silent : true}); - self.model.fetch({ + revertView : function() { + var self = this; + this.model.deleteKeys = []; + this.model.clear({silent : true}); + this.model.fetch({ success : function() { self.render(); }, reset: true }); diff --git a/cms/templates/base.html b/cms/templates/base.html index 11e8d41496..695a97f1da 100644 --- a/cms/templates/base.html +++ b/cms/templates/base.html @@ -61,8 +61,6 @@
<%include file="widgets/header.html" /> - ## remove this block after advanced settings notification is rewritten - <%block name="view_alerts">
<%block name="content"> @@ -74,13 +72,9 @@ <%include file="widgets/footer.html" /> <%include file="widgets/tender.html" /> - ## remove this block after advanced settings notification is rewritten - <%block name="view_notifications">
- ## remove this block after advanced settings notification is rewritten - <%block name="view_prompts">
<%block name="jsextra"> diff --git a/cms/templates/settings_advanced.html b/cms/templates/settings_advanced.html index 242148418e..6cc3468590 100644 --- a/cms/templates/settings_advanced.html +++ b/cms/templates/settings_advanced.html @@ -104,60 +104,3 @@ editor.render();
- -<%block name="view_notifications"> - - - - -<%block name="view_alerts"> - -
-
- - -
-

Your policy changes have been saved.

-

Please note that validation of your policy key and value pairs is not currently in place yet. If you are having difficulties, please review your policy pairs.

-
- - - - close alert - -
-
- - -
-
- - -
-

There was an error saving your information

-

Please see the error below and correct it to ensure there are no problems in rendering your course.

-
-
-
- From 3e376bd78031db5a87c890359f1fbc776030404e Mon Sep 17 00:00:00 2001 From: Peter Fogg Date: Mon, 24 Jun 2013 11:06:53 -0400 Subject: [PATCH 328/375] Prevent "saved" and "error" views from showing at the same time. Previously the "saved" view was never hidden, even after more data was edited. So if one field was saved successfully and then another was not, we would find ourselves in the unfortunate situation of seeing both views at once, leading to much confusion. --- cms/static/js/views/settings/advanced_view.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/cms/static/js/views/settings/advanced_view.js b/cms/static/js/views/settings/advanced_view.js index 69a2c9f622..102bb71a52 100644 --- a/cms/static/js/views/settings/advanced_view.js +++ b/cms/static/js/views/settings/advanced_view.js @@ -21,6 +21,7 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ } ); this.listenTo(this.model, 'invalid', this.handleValidationError); + this.savedBar = undefined; }, render: function() { // catch potential outside call before template loaded @@ -136,15 +137,22 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ success : function() { self.render(); var message = gettext("Please note that validation of your policy key and value pairs is not currently in place yet. If you are having difficulties, please review your policy pairs."); - var saving = new CMS.Views.Alert.Confirmation({ + self.saved = new CMS.Views.Alert.Confirmation({ title: gettext("Your policy changes have been saved."), message: message, closeIcon: false }); - saving.show(); + self.saved.show(); analytics.track('Saved Advanced Settings', { 'course': course_location_analytics }); + }, + error: function() { + // If we've already saved some data this will be + // shown; hide it away again. + if(self.saved) { + self.saved.hide(); + } } }); }, From 72ffe2d8f243d00679e97b1385475e33a00e181b Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 24 Jun 2013 13:44:48 -0400 Subject: [PATCH 329/375] Backbone notifications secondary actions Handle secondary actions on notifications either specified as a single object, or as a list of objects. Under the hood, the initialize method converts a single object to a list containing a single object. --- .../coffee/spec/views/feedback_spec.coffee | 43 ++++++++++++++++++- cms/static/js/views/feedback.js | 5 +++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/cms/static/coffee/spec/views/feedback_spec.coffee b/cms/static/coffee/spec/views/feedback_spec.coffee index a3950c0b3c..e5916c5ed3 100644 --- a/cms/static/coffee/spec/views/feedback_spec.coffee +++ b/cms/static/coffee/spec/views/feedback_spec.coffee @@ -100,11 +100,10 @@ describe "CMS.Views.SystemFeedback click events", -> text: "Save", class: "save-button", click: @primaryClickSpy - secondary: [{ + secondary: text: "Revert", class: "cancel-button", click: @secondaryClickSpy - }] ) @view.show() @@ -124,6 +123,46 @@ describe "CMS.Views.SystemFeedback click events", -> it "should apply class to secondary action", -> expect(@view.$(".action-secondary")).toHaveClass("cancel-button") + +describe "CMS.Views.SystemFeedback multiple secondary actions", -> + beforeEach -> + @secondarySpyOne = jasmine.createSpy('secondarySpyOne') + @secondarySpyTwo = jasmine.createSpy('secondarySpyTwo') + @view = new CMS.Views.Notification.Warning( + title: "No Primary", + message: "Pick a secondary action", + actions: + secondary: [ + { + text: "Option One" + class: "option-one" + click: @secondarySpyOne + }, { + text: "Option Two" + class: "option-two" + click: @secondarySpyTwo + } + ] + ) + @view.show() + + it "should render both", -> + expect(@view.el).toContain(".action-secondary.option-one") + expect(@view.el).toContain(".action-secondary.option-two") + expect(@view.el).not.toContain(".action-secondary.option-one.option-two") + expect(@view.$(".action-secondary.option-one")).toContainText("Option One") + expect(@view.$(".action-secondary.option-two")).toContainText("Option Two") + + it "should differentiate clicks (1)", -> + @view.$(".option-one").click() + expect(@secondarySpyOne).toHaveBeenCalled() + expect(@secondarySpyTwo).not.toHaveBeenCalled() + + it "should differentiate clicks (2)", -> + @view.$(".option-two").click() + expect(@secondarySpyOne).not.toHaveBeenCalled() + expect(@secondarySpyTwo).toHaveBeenCalled() + describe "CMS.Views.Notification minShown and maxShown", -> beforeEach -> @showSpy = spyOn(CMS.Views.Notification.Saving.prototype, 'show') diff --git a/cms/static/js/views/feedback.js b/cms/static/js/views/feedback.js index 0cfd6fa4ef..3f161d5b1f 100644 --- a/cms/static/js/views/feedback.js +++ b/cms/static/js/views/feedback.js @@ -49,6 +49,11 @@ CMS.Views.SystemFeedback = Backbone.View.extend({ } this.template = _.template(tpl); this.setElement($("#page-"+this.options.type)); + // handle single "secondary" action + if (this.options.actions && this.options.actions.secondary && + !_.isArray(this.options.actions.secondary)) { + this.options.actions.secondary = [this.options.actions.secondary]; + } return this; }, // public API: show() and hide() From ab7b991e78f7d21389520c33b945e463b45e0e01 Mon Sep 17 00:00:00 2001 From: Jason Bau Date: Mon, 24 Jun 2013 11:53:59 -0600 Subject: [PATCH 330/375] Update CHANGELOG.rst --- CHANGELOG.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0e161e4f72..d06cd89621 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,9 @@ These are notable changes in edx-platform. This is a rolling list of changes, in roughly chronological order, most recent first. Add your entries at or near the top. Include a label indicating the component affected. +LMS: Small UX fix on capa multiple-choice problems. Make labels only +as wide as the text to reduce accidental choice selections. + Studio: Remove XML from the video component editor. All settings are moved to be edited as metadata. From fb573a1db64eefc328169d2881c9b1dd25187d14 Mon Sep 17 00:00:00 2001 From: Peter Fogg Date: Mon, 24 Jun 2013 12:40:45 -0400 Subject: [PATCH 331/375] Hide "success" alert as soon as we start editing another field. --- cms/static/js/views/settings/advanced_view.js | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/cms/static/js/views/settings/advanced_view.js b/cms/static/js/views/settings/advanced_view.js index 102bb71a52..302a918de1 100644 --- a/cms/static/js/views/settings/advanced_view.js +++ b/cms/static/js/views/settings/advanced_view.js @@ -21,7 +21,6 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ } ); this.listenTo(this.model, 'invalid', this.handleValidationError); - this.savedBar = undefined; }, render: function() { // catch potential outside call before template loaded @@ -122,10 +121,13 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ confirm.hide(); self.notificationBarShowing = false; } - }], + }] }}); this.notificationBarShowing = true; confirm.show(); + if(this.saved) { + this.saved.hide(); + } }, saveView : function() { // TODO one last verification scan: @@ -146,13 +148,6 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ analytics.track('Saved Advanced Settings', { 'course': course_location_analytics }); - }, - error: function() { - // If we've already saved some data this will be - // shown; hide it away again. - if(self.saved) { - self.saved.hide(); - } } }); }, From f1825eff819ec28dd42fcd04e84bd73d4d8bd86c Mon Sep 17 00:00:00 2001 From: Jason Bau Date: Mon, 24 Jun 2013 12:19:34 -0600 Subject: [PATCH 332/375] Update CHANGELOG.rst --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d06cd89621..3dda49928b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,7 +5,7 @@ These are notable changes in edx-platform. This is a rolling list of changes, in roughly chronological order, most recent first. Add your entries at or near the top. Include a label indicating the component affected. -LMS: Small UX fix on capa multiple-choice problems. Make labels only +Blades: Small UX fix on capa multiple-choice problems. Make labels only as wide as the text to reduce accidental choice selections. Studio: Remove XML from the video component editor. All settings are From 83062c0b7dd6b85e6f50ad717ba796c92c5ecb8d Mon Sep 17 00:00:00 2001 From: Jason Bau Date: Mon, 24 Jun 2013 11:54:31 -0700 Subject: [PATCH 333/375] Tests + Now subclass PasswordResetForm instead of copy Changed to subclassing django's PasswordResetForm and overriding clean_password() instead of copy/paste. Less lines to worry about for diff-cover this way =) --- common/djangoapps/student/forms.py | 65 ++------------ common/djangoapps/student/tests/tests.py | 106 ++++++++++++++++++++++- common/djangoapps/student/views.py | 2 +- 3 files changed, 111 insertions(+), 62 deletions(-) diff --git a/common/djangoapps/student/forms.py b/common/djangoapps/student/forms.py index 75c89e0a26..1096092117 100644 --- a/common/djangoapps/student/forms.py +++ b/common/djangoapps/student/forms.py @@ -1,33 +1,15 @@ from django import forms -from django.utils.translation import ugettext, ugettext_lazy as _ -from django.template import loader from django.contrib.auth.models import User -from django.contrib.auth.hashers import UNUSABLE_PASSWORD, is_password_usable, get_hasher -from django.contrib.auth.tokens import default_token_generator -from django.contrib.sites.models import get_current_site -from django.utils.http import int_to_base36 +from django.contrib.auth.forms import PasswordResetForm +from django.contrib.auth.hashers import UNUSABLE_PASSWORD - - -# This is a literal copy from Django 1.4.5's django.contrib.auth.forms.PasswordResetForm -# I think copy-and-paste here is somewhat better than subclassing and -# just changing the definition of clean_email, because it's less -# likely to be broken by incompatibility with a new django version. -# (If this form is good enough now, a snapshot of it ought to last a while) - -class PasswordResetFormNoActive(forms.Form): - error_messages = { - 'unknown': _("That e-mail address doesn't have an associated " - "user account. Are you sure you've registered?"), - 'unusable': _("The user account associated with this e-mail " - "address cannot reset the password."), - } - email = forms.EmailField(label=_("E-mail"), max_length=75) - +class PasswordResetFormNoActive(PasswordResetForm): def clean_email(self): """ - Validates that an active user exists with the given email address. - """ + This is a literal copy from Django 1.4.5's django.contrib.auth.forms.PasswordResetForm + Except removing the requirement of active users + Validates that a user exists with the given email address. + """ email = self.cleaned_data["email"] #The line below contains the only change, removing is_active=True self.users_cache = User.objects.filter(email__iexact=email) @@ -37,36 +19,3 @@ class PasswordResetFormNoActive(forms.Form): for user in self.users_cache): raise forms.ValidationError(self.error_messages['unusable']) return email - - def save(self, domain_override=None, - subject_template_name='registration/password_reset_subject.txt', - email_template_name='registration/password_reset_email.html', - use_https=False, token_generator=default_token_generator, - from_email=None, request=None): - """ - Generates a one-use only link for resetting password and sends to the - user. - """ - from django.core.mail import send_mail - for user in self.users_cache: - if not domain_override: - current_site = get_current_site(request) - site_name = current_site.name - domain = current_site.domain - else: - site_name = domain = domain_override - c = { - 'email': user.email, - 'domain': domain, - 'site_name': site_name, - 'uid': int_to_base36(user.id), - 'user': user, - 'token': token_generator.make_token(user), - 'protocol': use_https and 'https' or 'http', - } - subject = loader.render_to_string(subject_template_name, c) - # Email subject *must not* contain newlines - subject = ''.join(subject.splitlines()) - email = loader.render_to_string(email_template_name, c) - send_mail(subject, email, from_email, [user.email]) - diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py index 4638da44b2..10836122b8 100644 --- a/common/djangoapps/student/tests/tests.py +++ b/common/djangoapps/student/tests/tests.py @@ -5,18 +5,118 @@ when you run "manage.py test". Replace this with more appropriate tests for your application. """ import logging +import json +import re +import unittest +from django import forms +from django.conf import settings from django.test import TestCase -from mock import Mock +from django.test.client import RequestFactory +from django.contrib.auth.models import User +from django.contrib.auth.hashers import UNUSABLE_PASSWORD +from django.template.loader import render_to_string, get_template, TemplateDoesNotExist +from django.core.urlresolvers import is_valid_path + +from mock import Mock, patch +from textwrap import dedent from student.models import unique_id_for_user -from student.views import process_survey_link, _cert_info - +from student.views import process_survey_link, _cert_info, password_reset, password_reset_confirm_wrapper +from student.tests.factories import UserFactory +from student.tests.test_email import mock_render_to_string COURSE_1 = 'edX/toy/2012_Fall' COURSE_2 = 'edx/full/6.002_Spring_2012' log = logging.getLogger(__name__) +try: + get_template('registration/password_reset_email.html') + project_uses_password_reset = True +except TemplateDoesNotExist: + project_uses_password_reset = False + + +class ResetPasswordTests(TestCase): + """ Tests that clicking reset password sends email, and doesn't activate the user + """ + request_factory = RequestFactory() + + def setUp(self): + self.user = UserFactory.create() + self.user.is_active = False + self.user.save() + + self.user_bad_passwd = UserFactory.create() + self.user_bad_passwd.is_active = False + self.user_bad_passwd.password = UNUSABLE_PASSWORD + self.user_bad_passwd.save() + + + @unittest.skipUnless(project_uses_password_reset, dedent("""Skipping Test because CMS has not provided + necessary templates for password reset. If this message is in LMS tests, that is a bug and needs to be fixed.""")) + @patch('student.views.password_reset_confirm') + @patch('django.core.mail.send_mail') + @patch('student.views.render_to_string', Mock(side_effect=mock_render_to_string, autospec=True)) + def test_reset_password_email(self, send_email, reset_confirm): + """Tests sending of reset password email""" + + #First test the bad password user, mainly for diff-cover sake + bad_pwd_req = self.request_factory.post('/password_reset/', {'email': self.user_bad_passwd.email}) + bad_pwd_resp = password_reset(bad_pwd_req) + self.assertEquals(bad_pwd_resp.status_code, 200) + self.assertEquals(bad_pwd_resp.content, json.dumps({'success': False, + 'error': 'Invalid e-mail or user'})) + + #Now test the exception cases with invalid email. + bad_email_req = self.request_factory.post('/password_reset/', {'email': self.user.email+"makeItFail"}) + bad_email_resp = password_reset(bad_email_req) + self.assertEquals(bad_email_resp.status_code, 200) + self.assertEquals(bad_email_resp.content, json.dumps({'success': False, + 'error': 'Invalid e-mail or user'})) + + #Now test the legit case where email should have been sent + good_req = self.request_factory.post('/password_reset/', {'email': self.user.email}) + good_resp = password_reset(good_req) + self.assertEquals(good_resp.status_code, 200) + self.assertEquals(good_resp.content, + json.dumps({'success': True, + 'value': "('registration/password_reset_done.html', [])"})) + + ((subject, msg, from_addr, to_addrs), sm_kwargs) = send_email.call_args + self.assertIn("Password reset", subject) + self.assertIn("You're receiving this e-mail because you requested a password reset", msg) + self.assertEquals(from_addr, settings.DEFAULT_FROM_EMAIL) + self.assertEquals(len(to_addrs), 1) + self.assertIn(self.user.email, to_addrs) + + #test that the user is not active + #it's a bit unsettling that we have to reload the user from the db for this test to work + #but I guess the user is cached here in the instance of ResetPasswordTests + #so the update in the view does not know to update this class. + self.user = User.objects.get(pk=self.user.pk) + self.assertFalse(self.user.is_active) + + #now try to activate the user in the password reset phase + bad_reset_req = self.request_factory.get('/password_reset_confirm/NO-OP/') + bad_reset_resp = password_reset_confirm_wrapper(bad_reset_req, 'NO', 'OP') + (confirm_args, confirm_kwargs) = reset_confirm.call_args + self.assertEquals(confirm_kwargs['uidb36'], 'NO') + self.assertEquals(confirm_kwargs['token'], 'OP') + self.user = User.objects.get(pk=self.user.pk) + self.assertFalse(self.user.is_active) + + reset_match = re.search(r'password_reset_confirm/(?P[0-9A-Za-z]+)-(?P.+)/', msg).groupdict() + good_reset_req = self.request_factory.get('/password_reset_confirm/{0}-{1}/'.format(reset_match['uidb36'], + reset_match['token'])) + good_reset_resp = password_reset_confirm_wrapper(good_reset_req, reset_match['uidb36'], reset_match['token']) + (confirm_args, confirm_kwargs) = reset_confirm.call_args + self.assertEquals(confirm_kwargs['uidb36'], reset_match['uidb36']) + self.assertEquals(confirm_kwargs['token'], reset_match['token']) + self.user = User.objects.get(pk=self.user.pk) + self.assertTrue(self.user.is_active) + + class CourseEndingTest(TestCase): """Test things related to course endings: certificates, surveys, etc""" diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 50f6d90368..7ae460b438 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -975,7 +975,7 @@ def password_reset(request): 'value': render_to_string('registration/password_reset_done.html', {})})) else: return HttpResponse(json.dumps({'success': False, - 'error': 'Invalid e-mail'})) + 'error': 'Invalid e-mail or user'})) def password_reset_confirm_wrapper(request, uidb36=None, token=None): ''' A wrapper around django.contrib.auth.views.password_reset_confirm. From 3a8f591fe5280146b66918e55daabd674999b507 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Mon, 24 Jun 2013 16:36:49 -0400 Subject: [PATCH 334/375] Add tests for the diff coverage; fix one hidden unicode bug --- common/lib/xmodule/xmodule/capa_module.py | 2 +- .../xmodule/xmodule/tests/test_capa_module.py | 85 ++++++++++++++++++- 2 files changed, 83 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index d740a73946..bb06912f7a 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -412,7 +412,7 @@ class CapaModule(CapaFields, XModule): `err` is the Exception encountered while rendering the problem HTML. """ - log.exception(err) + log.exception(err.message) # TODO (vshnayder): another switch on DEBUG. if self.system.DEBUG: diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py index c6ffd32e89..1e84174291 100644 --- a/common/lib/xmodule/xmodule/tests/test_capa_module.py +++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py @@ -11,11 +11,12 @@ import datetime from mock import Mock, patch import unittest import random +import json import xmodule -from capa.responsetypes import StudentInputError, \ - LoncapaProblemError, ResponseError -from xmodule.capa_module import CapaModule +from capa.responsetypes import (StudentInputError, LoncapaProblemError, + ResponseError) +from xmodule.capa_module import CapaModule, ComplexEncoder from xmodule.modulestore import Location from django.http import QueryDict @@ -530,6 +531,32 @@ class CapaModuleTest(unittest.TestCase): # Expect that the number of attempts is NOT incremented self.assertEqual(module.attempts, 1) + def test_check_problem_other_errors(self): + """ + Test that errors other than the expected kinds give an appropriate message. + + See also `test_check_problem_error` for the "expected kinds" or errors. + """ + # Create the module + module = CapaFactory.create(attempts=1) + + # Ensure that the user is NOT staff + module.system.user_is_staff = False + + # Ensure that DEBUG is on + module.system.DEBUG = True + + # Simulate answering a problem that raises the exception + with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade: + error_msg = u"Superterrible error happened: ☠" + mock_grade.side_effect = Exception(error_msg) + + get_request_dict = {CapaFactory.input_key(): '3.14'} + result = module.check_problem(get_request_dict) + + # Expect an AJAX alert message in 'success' + self.assertTrue(error_msg in result['success']) + def test_check_problem_error_nonascii(self): # Try each exception that capa_module should handle @@ -1059,6 +1086,33 @@ class CapaModuleTest(unittest.TestCase): # Expect that the module has created a new dummy problem with the error self.assertNotEqual(original_problem, module.lcp) + def test_get_problem_html_error_w_debug(self): + """ + Test the html response when an error occurs with DEBUG on + """ + module = CapaFactory.create() + + # Simulate throwing an exception when the capa problem + # is asked to render itself as HTML + error_msg = u"Superterrible error happened: ☠" + module.lcp.get_html = Mock(side_effect=Exception(error_msg)) + + # Stub out the get_test_system rendering function + module.system.render_template = Mock(return_value="
Test Template HTML
") + + # Make sure DEBUG is on + module.system.DEBUG = True + + # Try to render the module with DEBUG turned on + html = module.get_problem_html() + + self.assertTrue(html is not None) + + # Check the rendering context + render_args, _ = module.system.render_template.call_args + context = render_args[1] + self.assertTrue(error_msg in context['problem']['html']) + def test_random_seed_no_change(self): # Run the test for each possible rerandomize value @@ -1164,3 +1218,28 @@ class CapaModuleTest(unittest.TestCase): for i in range(200): module = CapaFactory.create(rerandomize=rerandomize) assert 0 <= module.seed < 1000 + + @patch('xmodule.capa_module.log') + @patch('xmodule.capa_module.Progress') + def test_get_progress_error(self, mock_progress, mock_log): + """ + Check that an exception given in `Progress` produces a `log.exception` call. + """ + error_types = [TypeError, ValueError] + for error_type in error_types: + mock_progress.side_effect = error_type + module = CapaFactory.create() + self.assertIsNone(module.get_progress()) + mock_log.exception.assert_called_once_with('Got bad progress') + mock_log.reset_mock() + + +class ComplexEncoderTest(unittest.TestCase): + def test_default(self): + """ + Check that complex numbers can be encoded into JSON. + """ + complex_num = 1 - 1j + expected_str = '1-1*j' + json_str = json.dumps(complex_num, cls=ComplexEncoder) + self.assertEqual(expected_str, json_str[1:-1]) # ignore quotes From d5d495c24d622ca9e1b8b6d0b30164df024230a3 Mon Sep 17 00:00:00 2001 From: Peter Fogg Date: Mon, 24 Jun 2013 16:57:14 -0400 Subject: [PATCH 335/375] Fix acceptance tests expecting outdated CSS. --- cms/djangoapps/contentstore/features/advanced-settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/features/advanced-settings.py b/cms/djangoapps/contentstore/features/advanced-settings.py index 2360baea5a..1661e1c391 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.py +++ b/cms/djangoapps/contentstore/features/advanced-settings.py @@ -27,7 +27,7 @@ def i_am_on_advanced_course_settings(step): @step(u'I press the "([^"]*)" notification button$') def press_the_notification_button(step, name): - css = 'a.%s-button' % name.lower() + css = 'a.action-%s' % name.lower() # Save was clicked if either the save notification bar is gone, or we have a error notification # overlaying it (expected in the case of typing Object into display_name). From 332a440539928e7bd8d9d9fab3dd8d5f475f4b97 Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Wed, 19 Jun 2013 18:09:18 -0400 Subject: [PATCH 336/375] Enable per-student background tasks. --- lms/djangoapps/instructor_task/api_helper.py | 7 +- lms/djangoapps/instructor_task/models.py | 21 +++++- .../instructor_task/tests/test_api.py | 2 +- .../instructor_task/tests/test_integration.py | 2 +- .../instructor_task/tests/test_tasks.py | 71 +++++++++++++++---- .../instructor_task/tests/test_views.py | 5 +- .../courseware/instructor_dashboard.html | 6 +- 7 files changed, 87 insertions(+), 27 deletions(-) diff --git a/lms/djangoapps/instructor_task/api_helper.py b/lms/djangoapps/instructor_task/api_helper.py index f9febd17d7..2795fd08c1 100644 --- a/lms/djangoapps/instructor_task/api_helper.py +++ b/lms/djangoapps/instructor_task/api_helper.py @@ -2,8 +2,6 @@ import hashlib import json import logging -from django.db import transaction - from celery.result import AsyncResult from celery.states import READY_STATES, SUCCESS, FAILURE, REVOKED @@ -30,7 +28,6 @@ def _task_is_running(course_id, task_type, task_key): return len(runningTasks) > 0 -@transaction.autocommit def _reserve_task(course_id, task_type, task_key, task_input, requester): """ Creates a database entry to indicate that a task is in progress. @@ -39,9 +36,9 @@ def _reserve_task(course_id, task_type, task_key, task_input, requester): Includes the creation of an arbitrary value for task_id, to be submitted with the task call to celery. - Autocommit annotation makes sure the database entry is committed. + The InstructorTask.create method makes sure the InstructorTask entry is committed. When called from any view that is wrapped by TransactionMiddleware, - and thus in a "commit-on-success" transaction, this autocommit here + and thus in a "commit-on-success" transaction, an autocommit buried within here will cause any pending transaction to be committed by a successful save here. Any future database operations will take place in a separate transaction. diff --git a/lms/djangoapps/instructor_task/models.py b/lms/djangoapps/instructor_task/models.py index f01cc4e3ad..b28a9a3d83 100644 --- a/lms/djangoapps/instructor_task/models.py +++ b/lms/djangoapps/instructor_task/models.py @@ -72,6 +72,16 @@ class InstructorTask(models.Model): @classmethod def create(cls, course_id, task_type, task_key, task_input, requester): + """ + Create an instance of InstructorTask. + + The InstructorTask.save_now method makes sure the InstructorTask entry is committed. + When called from any view that is wrapped by TransactionMiddleware, + and thus in a "commit-on-success" transaction, an autocommit buried within here + will cause any pending transaction to be committed by a successful + save here. Any future database operations will take place in a + separate transaction. + """ # create the task_id here, and pass it into celery: task_id = str(uuid4()) @@ -99,7 +109,16 @@ class InstructorTask(models.Model): @transaction.autocommit def save_now(self): - """Writes InstructorTask immediately, ensuring the transaction is committed.""" + """ + Writes InstructorTask immediately, ensuring the transaction is committed. + + Autocommit annotation makes sure the database entry is committed. + When called from any view that is wrapped by TransactionMiddleware, + and thus in a "commit-on-success" transaction, this autocommit here + will cause any pending transaction to be committed by a successful + save here. Any future database operations will take place in a + separate transaction. + """ self.save() @staticmethod diff --git a/lms/djangoapps/instructor_task/tests/test_api.py b/lms/djangoapps/instructor_task/tests/test_api.py index 841fdca8a0..1e40c51c4b 100644 --- a/lms/djangoapps/instructor_task/tests/test_api.py +++ b/lms/djangoapps/instructor_task/tests/test_api.py @@ -22,7 +22,7 @@ from instructor_task.tests.test_base import (InstructorTaskTestCase, class InstructorTaskReportTest(InstructorTaskTestCase): """ - Tests API and view methods that involve the reporting of status for background tasks. + Tests API methods that involve the reporting of status for background tasks. """ def test_get_running_instructor_tasks(self): diff --git a/lms/djangoapps/instructor_task/tests/test_integration.py b/lms/djangoapps/instructor_task/tests/test_integration.py index 5a17e32329..9b56663753 100644 --- a/lms/djangoapps/instructor_task/tests/test_integration.py +++ b/lms/djangoapps/instructor_task/tests/test_integration.py @@ -1,5 +1,5 @@ """ -Integration Tests for LMS instructor-initiated background tasks +Integration Tests for LMS instructor-initiated background tasks. Runs tasks on answers to course problems to validate that code paths actually work. diff --git a/lms/djangoapps/instructor_task/tests/test_tasks.py b/lms/djangoapps/instructor_task/tests/test_tasks.py index c59a7065ae..090c114720 100644 --- a/lms/djangoapps/instructor_task/tests/test_tasks.py +++ b/lms/djangoapps/instructor_task/tests/test_tasks.py @@ -1,5 +1,5 @@ """ -Unit tests for LMS instructor-initiated background tasks, +Unit tests for LMS instructor-initiated background tasks. Runs tasks on answers to course problems to validate that code paths actually work. @@ -7,6 +7,7 @@ paths actually work. """ import json from uuid import uuid4 +from unittest import skip from mock import Mock, patch @@ -62,6 +63,7 @@ class TestInstructorTasks(InstructorTaskModuleTestCase): } def _run_task_with_mock_celery(self, task_function, entry_id, task_id, expected_failure_message=None): + """Submit a task and mock how celery provides a current_task.""" self.current_task = Mock() self.current_task.request = Mock() self.current_task.request.id = task_id @@ -73,7 +75,7 @@ class TestInstructorTasks(InstructorTaskModuleTestCase): return task_function(entry_id, self._get_xmodule_instance_args()) def _test_missing_current_task(self, task_function): - # run without (mock) Celery running + """Check that a task_function fails when celery doesn't provide a current_task.""" task_entry = self._create_input_entry() with self.assertRaises(UpdateProblemModuleStateError): task_function(task_entry.id, self._get_xmodule_instance_args()) @@ -88,7 +90,7 @@ class TestInstructorTasks(InstructorTaskModuleTestCase): self._test_missing_current_task(delete_problem_state) def _test_undefined_problem(self, task_function): - # run with celery, but no problem defined + """Run with celery, but no problem defined.""" task_entry = self._create_input_entry() with self.assertRaises(ItemNotFoundError): self._run_task_with_mock_celery(task_function, task_entry.id, task_entry.task_id) @@ -103,7 +105,7 @@ class TestInstructorTasks(InstructorTaskModuleTestCase): self._test_undefined_problem(delete_problem_state) def _test_run_with_task(self, task_function, action_name, expected_num_updated): - # run with some StudentModules for the problem + """Run a task and check the number of StudentModules processed.""" task_entry = self._create_input_entry() status = self._run_task_with_mock_celery(task_function, task_entry.id, task_entry.task_id) # check return value @@ -118,7 +120,7 @@ class TestInstructorTasks(InstructorTaskModuleTestCase): self.assertEquals(entry.task_state, SUCCESS) def _test_run_with_no_state(self, task_function, action_name): - # run with no StudentModules for the problem + """Run with no StudentModules defined for the current problem.""" self.define_option_problem(PROBLEM_URL_NAME) self._test_run_with_task(task_function, action_name, 0) @@ -185,7 +187,7 @@ class TestInstructorTasks(InstructorTaskModuleTestCase): module_state_key=self.problem_url) def _test_reset_with_student(self, use_email): - # run with some StudentModules for the problem + """Run a reset task for one student, with several StudentModules for the problem defined.""" num_students = 10 initial_attempts = 3 input_state = json.dumps({'attempts': initial_attempts}) @@ -233,8 +235,7 @@ class TestInstructorTasks(InstructorTaskModuleTestCase): self._test_reset_with_student(True) def _test_run_with_failure(self, task_function, expected_message): - # run with no StudentModules for the problem, - # because we will fail before entering the loop. + """Run a task and trigger an artificial failure with give message.""" task_entry = self._create_input_entry() self.define_option_problem(PROBLEM_URL_NAME) with self.assertRaises(TestTaskFailure): @@ -256,8 +257,10 @@ class TestInstructorTasks(InstructorTaskModuleTestCase): self._test_run_with_failure(delete_problem_state, 'We expected this to fail') def _test_run_with_long_error_msg(self, task_function): - # run with an error message that is so long it will require - # truncation (as well as the jettisoning of the traceback). + """ + Run with an error message that is so long it will require + truncation (as well as the jettisoning of the traceback). + """ task_entry = self._create_input_entry() self.define_option_problem(PROBLEM_URL_NAME) expected_message = "x" * 1500 @@ -282,9 +285,11 @@ class TestInstructorTasks(InstructorTaskModuleTestCase): self._test_run_with_long_error_msg(delete_problem_state) def _test_run_with_short_error_msg(self, task_function): - # run with an error message that is short enough to fit - # in the output, but long enough that the traceback won't. - # Confirm that the traceback is truncated. + """ + Run with an error message that is short enough to fit + in the output, but long enough that the traceback won't. + Confirm that the traceback is truncated. + """ task_entry = self._create_input_entry() self.define_option_problem(PROBLEM_URL_NAME) expected_message = "x" * 900 @@ -330,3 +335,43 @@ class TestInstructorTasks(InstructorTaskModuleTestCase): self.assertEquals(output['exception'], 'ValueError') self.assertTrue("Length of task output is too long" in output['message']) self.assertTrue('traceback' not in output) + + @skip + def test_rescoring_unrescorable(self): + # TODO: this test needs to have Mako templates initialized + # to make sure that the creation of an XModule works. + input_state = json.dumps({'done': True}) + num_students = 1 + self._create_students_with_state(num_students, input_state) + task_entry = self._create_input_entry() + with self.assertRaises(UpdateProblemModuleStateError): + self._run_task_with_mock_celery(rescore_problem, task_entry.id, task_entry.task_id) + # check values stored in table: + entry = InstructorTask.objects.get(id=task_entry.id) + output = json.loads(entry.task_output) + self.assertEquals(output['exception'], "UpdateProblemModuleStateError") + self.assertEquals(output['message'], "Specified problem does not support rescoring.") + self.assertGreater(len(output['traceback']), 0) + + @skip + def test_rescoring_success(self): + # TODO: this test needs to have Mako templates initialized + # to make sure that the creation of an XModule works. + input_state = json.dumps({'done': True}) + num_students = 10 + self._create_students_with_state(num_students, input_state) + task_entry = self._create_input_entry() + mock_instance = Mock() + mock_instance.rescore_problem = Mock({'success': 'correct'}) + # TODO: figure out why this mock is not working.... + with patch('courseware.module_render.get_module_for_descriptor_internal') as mock_get_module: + mock_get_module.return_value = mock_instance + self._run_task_with_mock_celery(rescore_problem, task_entry.id, task_entry.task_id) + # check return value + entry = InstructorTask.objects.get(id=task_entry.id) + output = json.loads(entry.task_output) + self.assertEquals(output.get('attempted'), num_students) + self.assertEquals(output.get('updated'), num_students) + self.assertEquals(output.get('total'), num_students) + self.assertEquals(output.get('action_name'), 'rescored') + self.assertGreater('duration_ms', 0) diff --git a/lms/djangoapps/instructor_task/tests/test_views.py b/lms/djangoapps/instructor_task/tests/test_views.py index 9020bf6e60..41de314abd 100644 --- a/lms/djangoapps/instructor_task/tests/test_views.py +++ b/lms/djangoapps/instructor_task/tests/test_views.py @@ -1,6 +1,6 @@ """ -Test for LMS instructor background task queue management +Test for LMS instructor background task views. """ import json from celery.states import SUCCESS, FAILURE, REVOKED, PENDING @@ -18,7 +18,7 @@ from instructor_task.views import instructor_task_status, get_task_completion_in class InstructorTaskReportTest(InstructorTaskTestCase): """ - Tests API and view methods that involve the reporting of status for background tasks. + Tests view methods that involve the reporting of status for background tasks. """ def _get_instructor_task_status(self, task_id): @@ -263,4 +263,3 @@ class InstructorTaskReportTest(InstructorTaskTestCase): succeeded, message = get_task_completion_info(instructor_task) self.assertFalse(succeeded) self.assertEquals(message, "Problem rescored for 2 of 3 students (out of 5)") - diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index ef1eb174fc..d541962906 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -249,7 +249,7 @@ function goto( mode)

Then select an action: - %if settings.MITX_FEATURES.get('ENABLE_COURSE_BACKGROUND_TASKS'): + %if settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS'): %endif

@@ -260,9 +260,9 @@ function goto( mode)

%endif - %if settings.MITX_FEATURES.get('ENABLE_COURSE_BACKGROUND_TASKS'): + %if settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS'):

Rescoring runs in the background, and status for active tasks will appear in a table below. - To see status for all tasks submitted for this course and student, click on this button: + To see status for all tasks submitted for this problem and student, click on this button:

From ddc986f775e5eb2cf5bb30644ae7d934140805cb Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Tue, 25 Jun 2013 11:25:29 -0400 Subject: [PATCH 337/375] Call event.preventDefault() on notification action buttons But allow you to specify that the event should not be prevented --- .../coffee/spec/views/feedback_spec.coffee | 39 +++++++++++++++++++ cms/static/js/views/feedback.js | 14 ++++++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/cms/static/coffee/spec/views/feedback_spec.coffee b/cms/static/coffee/spec/views/feedback_spec.coffee index e5916c5ed3..adec11e2a7 100644 --- a/cms/static/coffee/spec/views/feedback_spec.coffee +++ b/cms/static/coffee/spec/views/feedback_spec.coffee @@ -17,6 +17,16 @@ beforeEach -> return text.test(trimmedText) else return trimmedText.indexOf(text) != -1; + toHaveBeenPrevented: -> + # remove this when we upgrade jasmine-jquery + eventName = @actual.eventName + selector = @actual.selector + @message = -> + [ + "Expected event #{eventName} to have been prevented on #{selector}", + "Expected event #{eventName} not to have been prevented on #{selector}" + ] + return jasmine.JQuery.events.wasPrevented(selector, eventName) describe "CMS.Views.SystemFeedback", -> beforeEach -> @@ -123,6 +133,35 @@ describe "CMS.Views.SystemFeedback click events", -> it "should apply class to secondary action", -> expect(@view.$(".action-secondary")).toHaveClass("cancel-button") + it "should preventDefault on primary action", -> + spyOnEvent(".action-primary", "click") + @view.$(".action-primary").click() + expect("click").toHaveBeenPreventedOn(".action-primary") + + it "should preventDefault on secondary action", -> + spyOnEvent(".action-secondary", "click") + @view.$(".action-secondary").click() + expect("click").toHaveBeenPreventedOn(".action-secondary") + +describe "CMS.Views.SystemFeedback not preventing events", -> + beforeEach -> + @clickSpy = jasmine.createSpy('clickSpy') + @view = new CMS.Views.Alert.Confirmation( + title: "It's all good" + message: "No reason for this alert" + actions: + primary: + text: "Whatever" + click: @clickSpy + preventDefault: false + ) + @view.show() + + it "should not preventDefault", -> + spyOnEvent(".action-primary", "click") + @view.$(".action-primary").click() + expect("click").not.toHaveBeenPreventedOn(".action-primary") + expect(@clickSpy).toHaveBeenCalled() describe "CMS.Views.SystemFeedback multiple secondary actions", -> beforeEach -> diff --git a/cms/static/js/views/feedback.js b/cms/static/js/views/feedback.js index 3f161d5b1f..3bfeeb5af2 100644 --- a/cms/static/js/views/feedback.js +++ b/cms/static/js/views/feedback.js @@ -10,8 +10,12 @@ CMS.Views.SystemFeedback = Backbone.View.extend({ minShown: 0, // length of time after this view has been shown before it can be hidden (milliseconds) maxShown: Infinity // length of time after this view has been shown before it will be automatically hidden (milliseconds) - /* could also have an "actions" hash: here is an example demonstrating - the expected structure + /* Could also have an "actions" hash: here is an example demonstrating + the expected structure. For each action, by default the framework + will call preventDefault on the click event before the function is + run; to make it not do that, just pass `preventDefault: false` in + the action object. + actions: { primary: { "text": "Save", @@ -106,6 +110,9 @@ CMS.Views.SystemFeedback = Backbone.View.extend({ if(!actions) { return; } var primary = actions.primary; if(!primary) { return; } + if(primary.preventDefault !== false) { + event.preventDefault(); + } if(primary.click) { primary.click.call(event.target, this, event); } @@ -121,6 +128,9 @@ CMS.Views.SystemFeedback = Backbone.View.extend({ i = _.indexOf(this.$(".action-secondary"), event.target); } var secondary = secondaryList[i]; + if(secondary.preventDefault !== false) { + event.preventDefault(); + } if(secondary.click) { secondary.click.call(event.target, this, event); } From a9a7f97d9b694078ccc23706d29a1fcb11dcc74a Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Tue, 25 Jun 2013 11:32:45 -0400 Subject: [PATCH 338/375] Update CHANGELOG for per-student problem rescoring. --- CHANGELOG.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index cb8eec738f..21b8c9f90b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,11 @@ These are notable changes in edx-platform. This is a rolling list of changes, in roughly chronological order, most recent first. Add your entries at or near the top. Include a label indicating the component affected. +LMS: Problem rescoring. Added options on the Grades tab of the +Instructor Dashboard to allow a particular student's submission for a +particular problem to be rescored. Provides an option to see a +history of background tasks for a given problem and student. + Blades: Small UX fix on capa multiple-choice problems. Make labels only as wide as the text to reduce accidental choice selections. From 8a9125f121a1b983b33d4c7f8cd16deeee8335cf Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Tue, 25 Jun 2013 11:33:46 -0400 Subject: [PATCH 339/375] Test Mongo database is now unique and destroyed in teardown --- common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py index c5ef0d751a..44e69fb0ed 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py @@ -13,11 +13,12 @@ from xmodule.templates import update_templates from .test_modulestore import check_path_to_location from . import DATA_DIR +from uuid import uuid4 HOST = 'localhost' PORT = 27017 -DB = 'test' +DB = 'test_mongo_%s' % uuid4().hex COLLECTION = 'modulestore' FS_ROOT = DATA_DIR # TODO (vshnayder): will need a real fs_root for testing load_item DEFAULT_CLASS = 'xmodule.raw_module.RawDescriptor' @@ -39,7 +40,8 @@ class TestMongoModuleStore(object): @classmethod def teardownClass(cls): - pass + cls.connection = pymongo.connection.Connection(HOST, PORT) + cls.connection.drop_database(DB) @staticmethod def initdb(): From c0805c334d1d5bbfe6582e8042fae1ad5ae75f77 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Tue, 25 Jun 2013 13:23:13 -0400 Subject: [PATCH 340/375] Updated diff-cover to version 0.1.3 to fix a bug --- requirements/edx/github.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt index 5ce748e7b5..f64568dc10 100644 --- a/requirements/edx/github.txt +++ b/requirements/edx/github.txt @@ -10,4 +10,4 @@ # Our libraries: -e git+https://github.com/edx/XBlock.git@4d8735e883#egg=XBlock -e git+https://github.com/edx/codejail.git@0a1b468#egg=codejail --e git+https://github.com/edx/diff-cover.git@v0.1.2#egg=diff_cover +-e git+https://github.com/edx/diff-cover.git@v0.1.3#egg=diff_cover From 2f02496c8f14e51eaaa8180ee0acfec9f375cb3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Thu, 13 Jun 2013 13:54:51 -0400 Subject: [PATCH 341/375] Reorder imports on module_render --- lms/djangoapps/courseware/module_render.py | 30 ++++++++++++---------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 3ffb1d1b1d..15a6ad2dab 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -2,8 +2,6 @@ import json import logging import re import sys -import static_replace - from functools import partial from django.conf import settings @@ -15,27 +13,31 @@ from django.http import Http404 from django.http import HttpResponse, HttpResponseBadRequest from django.views.decorators.csrf import csrf_exempt +import pyparsing from requests.auth import HTTPBasicAuth +from statsd import statsd from capa.xqueue_interface import XQueueInterface -from courseware.masquerade import setup_masquerade -from courseware.access import has_access from mitxmako.shortcuts import render_to_string -from .models import StudentModule -from psychometrics.psychoanalyze import make_psychometrics_data_update_handler -from student.models import unique_id_for_user +from xblock.runtime import DbModel +from xmodule.error_module import ErrorDescriptor, NonStaffErrorDescriptor from xmodule.errortracker import exc_info_to_str from xmodule.exceptions import NotFoundError, ProcessingError from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore -from xmodule.x_module import ModuleSystem -from xmodule.error_module import ErrorDescriptor, NonStaffErrorDescriptor -from xblock.runtime import DbModel -from xmodule_modifiers import replace_course_urls, replace_static_urls, add_histogram, wrap_xmodule -from .model_data import LmsKeyValueStore, LmsUsage, ModelDataCache - from xmodule.modulestore.exceptions import ItemNotFoundError -from statsd import statsd +from xmodule.x_module import ModuleSystem +from xmodule_modifiers import replace_course_urls, replace_static_urls, add_histogram, wrap_xmodule + +import static_replace +from psychometrics.psychoanalyze import make_psychometrics_data_update_handler +from student.models import unique_id_for_user + +from courseware.access import has_access +from courseware.masquerade import setup_masquerade +from courseware.model_data import LmsKeyValueStore, LmsUsage, ModelDataCache +from courseware.models import StudentModule + log = logging.getLogger(__name__) From e4ee1c6c9b1527ade4cb7c584d7bb5c7fa1c6753 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Mon, 24 Jun 2013 15:29:54 -0400 Subject: [PATCH 342/375] Rename arguments of modx_dispatch and handle_ajax related functions Refactor a bit modx_dispatch --- common/lib/capa/capa/capa_problem.py | 8 +- common/lib/capa/capa/inputtypes.py | 22 ++-- common/lib/capa/capa/tests/test_inputtypes.py | 8 +- common/lib/xmodule/xmodule/capa_module.py | 73 +++++++------ .../xmodule/combined_open_ended_module.py | 5 +- .../lib/xmodule/xmodule/conditional_module.py | 2 +- .../combined_open_ended_modulev1.py | 40 +++---- .../open_ended_module.py | 42 ++++---- .../openendedchild.py | 40 +++---- .../self_assessment_module.py | 34 +++--- .../xmodule/xmodule/peer_grading_module.py | 73 ++++++------- common/lib/xmodule/xmodule/poll_module.py | 4 +- common/lib/xmodule/xmodule/seq_module.py | 4 +- .../lib/xmodule/xmodule/tests/test_logic.py | 4 +- .../lib/xmodule/xmodule/timelimit_module.py | 3 +- common/lib/xmodule/xmodule/video_module.py | 4 +- .../lib/xmodule/xmodule/videoalpha_module.py | 4 +- .../lib/xmodule/xmodule/word_cloud_module.py | 6 +- common/lib/xmodule/xmodule/x_module.py | 4 +- lms/djangoapps/courseware/module_render.py | 100 +++++++++++------- lms/urls.py | 4 +- 21 files changed, 254 insertions(+), 230 deletions(-) diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index d620bac60a..2c813f49d5 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -373,7 +373,7 @@ class LoncapaProblem(object): html = contextualize_text(etree.tostring(self._extract_html(self.tree)), self.context) return html - def handle_input_ajax(self, get): + def handle_input_ajax(self, data): ''' InputTypes can support specialized AJAX calls. Find the correct input and pass along the correct data @@ -381,10 +381,10 @@ class LoncapaProblem(object): ''' # pull out the id - input_id = get['input_id'] + input_id = data['input_id'] if self.inputs[input_id]: - dispatch = get['dispatch'] - return self.inputs[input_id].handle_ajax(dispatch, get) + dispatch = data['dispatch'] + return self.inputs[input_id].handle_ajax(dispatch, data) else: log.warning("Could not find matching input for id: %s" % input_id) return {} diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index f026568da1..4c40a2cd3e 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -223,13 +223,13 @@ class InputTypeBase(object): """ pass - def handle_ajax(self, dispatch, get): + def handle_ajax(self, dispatch, data): """ InputTypes that need to handle specialized AJAX should override this. Input: dispatch: a string that can be used to determine how to handle the data passed in - get: a dictionary containing the data that was sent with the ajax call + data: a dictionary containing the data that was sent with the ajax call Output: a dictionary object that can be serialized into JSON. This will be sent back to the Javascript. @@ -677,20 +677,20 @@ class MatlabInput(CodeInput): self.queue_len = 1 self.msg = self.plot_submitted_msg - def handle_ajax(self, dispatch, get): + def handle_ajax(self, dispatch, data): ''' Handle AJAX calls directed to this input Args: - dispatch (str) - indicates how we want this ajax call to be handled - - get (dict) - dictionary of key-value pairs that contain useful data + - data (dict) - dictionary of key-value pairs that contain useful data Returns: dict - 'success' - whether or not we successfully queued this submission - 'message' - message to be rendered in case of error ''' if dispatch == 'plot': - return self._plot_data(get) + return self._plot_data(data) return {} def ungraded_response(self, queue_msg, queuekey): @@ -751,7 +751,7 @@ class MatlabInput(CodeInput): msg = result['msg'] return msg - def _plot_data(self, get): + def _plot_data(self, data): ''' AJAX handler for the plot button Args: @@ -765,7 +765,7 @@ class MatlabInput(CodeInput): return {'success': False, 'message': 'Cannot connect to the queue'} # pull relevant info out of get - response = get['submission'] + response = data['submission'] # construct xqueue headers qinterface = self.system.xqueue['interface'] @@ -951,16 +951,16 @@ class ChemicalEquationInput(InputTypeBase): """ return {'previewer': '/static/js/capa/chemical_equation_preview.js', } - def handle_ajax(self, dispatch, get): + def handle_ajax(self, dispatch, data): ''' Since we only have chemcalc preview this input, check to see if it matches the corresponding dispatch and send it through if it does ''' if dispatch == 'preview_chemcalc': - return self.preview_chemcalc(get) + return self.preview_chemcalc(data) return {} - def preview_chemcalc(self, get): + def preview_chemcalc(self, data): """ Render an html preview of a chemical formula or equation. get should contain a key 'formula' and value 'some formula string'. @@ -974,7 +974,7 @@ class ChemicalEquationInput(InputTypeBase): result = {'preview': '', 'error': ''} - formula = get['formula'] + formula = data['formula'] if formula is None: result['error'] = "No formula specified." return result diff --git a/common/lib/capa/capa/tests/test_inputtypes.py b/common/lib/capa/capa/tests/test_inputtypes.py index 313eb28249..1b52d41890 100644 --- a/common/lib/capa/capa/tests/test_inputtypes.py +++ b/common/lib/capa/capa/tests/test_inputtypes.py @@ -467,8 +467,8 @@ class MatlabTest(unittest.TestCase): self.assertEqual(context, expected) def test_plot_data(self): - get = {'submission': 'x = 1234;'} - response = self.the_input.handle_ajax("plot", get) + data = {'submission': 'x = 1234;'} + response = self.the_input.handle_ajax("plot", data) test_system().xqueue['interface'].send_to_queue.assert_called_with(header=ANY, body=ANY) @@ -477,10 +477,10 @@ class MatlabTest(unittest.TestCase): self.assertEqual(self.the_input.input_state['queuestate'], 'queued') def test_plot_data_failure(self): - get = {'submission': 'x = 1234;'} + data = {'submission': 'x = 1234;'} error_message = 'Error message!' test_system().xqueue['interface'].send_to_queue.return_value = (1, error_message) - response = self.the_input.handle_ajax("plot", get) + response = self.the_input.handle_ajax("plot", data) self.assertFalse(response['success']) self.assertEqual(response['message'], error_message) self.assertTrue('queuekey' not in self.the_input.input_state) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index bb06912f7a..eeb8f19439 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -519,11 +519,11 @@ class CapaModule(CapaFields, XModule): # now do the substitutions which are filesystem based, e.g. '/static/' prefixes return self.system.replace_urls(html) - def handle_ajax(self, dispatch, get): + def handle_ajax(self, dispatch, data): """ This is called by courseware.module_render, to handle an AJAX call. - `get` is request.POST. + `data` is request.POST. Returns a json dictionary: { 'progress_changed' : True/False, @@ -547,18 +547,19 @@ class CapaModule(CapaFields, XModule): before = self.get_progress() try: - d = handlers[dispatch](get) - + result = handlers[dispatch](data) except Exception as err: _, _, traceback_obj = sys.exc_info() - raise ProcessingError, err.message, traceback_obj + raise ProcessingError(err.message, traceback_obj) after = self.get_progress() - d.update({ + + result.update({ 'progress_changed': after != before, 'progress_status': Progress.to_js_status_str(after), }) - return json.dumps(d, cls=ComplexEncoder) + + return json.dumps(result, cls=ComplexEncoder) def is_past_due(self): """ @@ -633,32 +634,32 @@ class CapaModule(CapaFields, XModule): return False - def update_score(self, get): + def update_score(self, data): """ Delivers grading response (e.g. from asynchronous code checking) to the capa problem, so its score can be updated - `get` must have a field `response` which is a string that contains the + 'data' must have a key 'response' which is a string that contains the grader's response No ajax return is needed. Return empty dict. """ - queuekey = get['queuekey'] - score_msg = get['xqueue_body'] + queuekey = data['queuekey'] + score_msg = data['xqueue_body'] self.lcp.update_score(score_msg, queuekey) self.set_state_from_lcp() self.publish_grade() return dict() # No AJAX return is needed - def handle_ungraded_response(self, get): + def handle_ungraded_response(self, data): """ Delivers a response from the XQueue to the capa problem The score of the problem will not be updated Args: - - get (dict) must contain keys: + - data (dict) must contain keys: queuekey - a key specific to this response xqueue_body - the body of the response Returns: @@ -666,28 +667,30 @@ class CapaModule(CapaFields, XModule): No ajax return is needed, so an empty dict is returned """ - queuekey = get['queuekey'] - score_msg = get['xqueue_body'] + queuekey = data['queuekey'] + score_msg = data['xqueue_body'] + # pass along the xqueue message to the problem self.lcp.ungraded_response(score_msg, queuekey) self.set_state_from_lcp() return dict() - def handle_input_ajax(self, get): + def handle_input_ajax(self, data): """ Handle ajax calls meant for a particular input in the problem Args: - - get (dict) - data that should be passed to the input + - data (dict) - data that should be passed to the input Returns: - dict containing the response from the input """ - response = self.lcp.handle_input_ajax(get) + response = self.lcp.handle_input_ajax(data) + # save any state changes that may occur self.set_state_from_lcp() return response - def get_answer(self, get): + def get_answer(self, data): """ For the "show answer" button. @@ -717,10 +720,9 @@ class CapaModule(CapaFields, XModule): return {'answers': new_answers} # Figure out if we should move these to capa_problem? - def get_problem(self, get): + def get_problem(self, _data): """ 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 @@ -729,27 +731,27 @@ class CapaModule(CapaFields, XModule): return {'html': self.get_problem_html(encapsulate=False)} @staticmethod - def make_dict_of_responses(get): + def make_dict_of_responses(data): """ Make dictionary of student responses (aka "answers") - `get` is POST dictionary (Django QueryDict). + `data` is POST dictionary (Django QueryDict). - The `get` dict has keys of the form 'x_y', which are mapped + The `data` dict has keys of the form 'x_y', which are mapped to key 'y' in the returned dict. For example, 'input_1_2_3' would be mapped to '1_2_3' in the returned dict. Some inputs always expect a list in the returned dict (e.g. checkbox inputs). The convention is that - keys in the `get` dict that end with '[]' will always + keys in the `data` dict that end with '[]' will always have list values in the returned dict. - For example, if the `get` dict contains {'input_1[]': 'test' } + For example, if the `data` dict contains {'input_1[]': 'test' } then the output dict would contain {'1': ['test'] } (the value is a list). Raises an exception if: - -A key in the `get` dictionary does not contain at least one underscore + -A key in the `data` dictionary does not contain at least one underscore (e.g. "input" is invalid, but "input_1" is valid) -Two keys end up with the same name in the returned dict. @@ -758,7 +760,7 @@ class CapaModule(CapaFields, XModule): """ answers = dict() - for key in get: + for key in data: # e.g. input_resistor_1 ==> resistor_1 _, _, name = key.partition('_') @@ -777,9 +779,9 @@ class CapaModule(CapaFields, XModule): name = name[:-2] if is_list_key else name if is_list_key: - val = get.getlist(key) + val = data.getlist(key) else: - val = get[key] + val = data[key] # If the name already exists, then we don't want # to override it. Raise an error instead @@ -801,7 +803,7 @@ class CapaModule(CapaFields, XModule): 'max_value': score['total'], }) - def check_problem(self, get): + def check_problem(self, data): """ Checks whether answers to a problem are correct @@ -813,8 +815,9 @@ class CapaModule(CapaFields, XModule): event_info['state'] = self.lcp.get_state() event_info['problem_id'] = self.location.url() - answers = self.make_dict_of_responses(get) + answers = self.make_dict_of_responses(data) event_info['answers'] = convert_files_to_filenames(answers) + # Too late. Cannot submit if self.closed(): event_info['failure'] = 'closed' @@ -972,7 +975,7 @@ class CapaModule(CapaFields, XModule): return {'success': success} - def save_problem(self, get): + def save_problem(self, data): """ Save the passed in answers. Returns a dict { 'success' : bool, 'msg' : message } @@ -982,7 +985,7 @@ class CapaModule(CapaFields, XModule): event_info['state'] = self.lcp.get_state() event_info['problem_id'] = self.location.url() - answers = self.make_dict_of_responses(get) + answers = self.make_dict_of_responses(data) event_info['answers'] = answers # Too late. Cannot submit @@ -1011,7 +1014,7 @@ class CapaModule(CapaFields, XModule): return {'success': True, 'msg': msg} - def reset_problem(self, get): + def reset_problem(self, _data): """ Changes problem state to unfinished -- removes student answers, and causes problem to rerender itself. diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 68285cae0d..52d98f032e 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -204,9 +204,9 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule): return_value = self.child_module.get_html() return return_value - def handle_ajax(self, dispatch, get): + def handle_ajax(self, dispatch, data): self.save_instance_data() - return_value = self.child_module.handle_ajax(dispatch, get) + return_value = self.child_module.handle_ajax(dispatch, data) self.save_instance_data() return return_value @@ -266,4 +266,3 @@ class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor): non_editable_fields.extend([CombinedOpenEndedDescriptor.due, CombinedOpenEndedDescriptor.graceperiod, CombinedOpenEndedDescriptor.markdown, CombinedOpenEndedDescriptor.version]) return non_editable_fields - diff --git a/common/lib/xmodule/xmodule/conditional_module.py b/common/lib/xmodule/xmodule/conditional_module.py index 6dc86880ae..5bdc8e7797 100644 --- a/common/lib/xmodule/xmodule/conditional_module.py +++ b/common/lib/xmodule/xmodule/conditional_module.py @@ -135,7 +135,7 @@ class ConditionalModule(ConditionalFields, XModule): 'depends': ';'.join(self.required_html_ids) }) - def handle_ajax(self, dispatch, post): + def handle_ajax(self, _dispatch, _data): """This is called by courseware.moduleodule_render, to handle an AJAX call. """ diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py index 9fc438d4c0..538901890c 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py @@ -500,10 +500,10 @@ class CombinedOpenEndedV1Module(): pass return return_html - def get_rubric(self, get): + def get_rubric(self, _data): """ Gets the results of a given grader via ajax. - Input: AJAX get dictionary + Input: AJAX data dictionary Output: Dictionary to be rendered via ajax that contains the result html. """ all_responses = [] @@ -532,10 +532,10 @@ class CombinedOpenEndedV1Module(): html = self.system.render_template('{0}/combined_open_ended_results.html'.format(self.TEMPLATE_DIR), context) return {'html': html, 'success': True} - def get_legend(self, get): + def get_legend(self, _data): """ Gets the results of a given grader via ajax. - Input: AJAX get dictionary + Input: AJAX data dictionary Output: Dictionary to be rendered via ajax that contains the result html. """ context = { @@ -544,10 +544,10 @@ class CombinedOpenEndedV1Module(): html = self.system.render_template('{0}/combined_open_ended_legend.html'.format(self.TEMPLATE_DIR), context) return {'html': html, 'success': True} - def get_results(self, get): + def get_results(self, _data): """ Gets the results of a given grader via ajax. - Input: AJAX get dictionary + Input: AJAX data dictionary Output: Dictionary to be rendered via ajax that contains the result html. """ self.update_task_states() @@ -588,19 +588,19 @@ class CombinedOpenEndedV1Module(): html = self.system.render_template('{0}/combined_open_ended_results.html'.format(self.TEMPLATE_DIR), context) return {'html': html, 'success': True} - def get_status_ajax(self, get): + def get_status_ajax(self, _data): """ Gets the results of a given grader via ajax. - Input: AJAX get dictionary + Input: AJAX data dictionary Output: Dictionary to be rendered via ajax that contains the result html. """ html = self.get_status(True) return {'html': html, 'success': True} - def handle_ajax(self, dispatch, get): + def handle_ajax(self, dispatch, data): """ This is called by courseware.module_render, to handle an AJAX call. - "get" is request.POST. + "data" is request.POST. Returns a json dictionary: { 'progress_changed' : True/False, @@ -618,30 +618,30 @@ class CombinedOpenEndedV1Module(): } if dispatch not in handlers: - return_html = self.current_task.handle_ajax(dispatch, get, self.system) + return_html = self.current_task.handle_ajax(dispatch, data, self.system) return self.update_task_states_ajax(return_html) - d = handlers[dispatch](get) + d = handlers[dispatch](data) return json.dumps(d, cls=ComplexEncoder) - def next_problem(self, get): + def next_problem(self, _data): """ Called via ajax to advance to the next problem. - Input: AJAX get request. + Input: AJAX data request. Output: Dictionary to be rendered """ self.update_task_states() return {'success': True, 'html': self.get_html_nonsystem(), 'allow_reset': self.ready_to_reset} - def reset(self, get): + def reset(self, data): """ If resetting is allowed, reset the state of the combined open ended module. - Input: AJAX get dictionary + Input: AJAX data dictionary Output: AJAX dictionary to tbe rendered """ if self.state != self.DONE: if not self.ready_to_reset: - return self.out_of_sync_error(get) + return self.out_of_sync_error(data) if self.student_attempts > self.attempts: return { @@ -789,13 +789,13 @@ class CombinedOpenEndedV1Module(): return progress_object - def out_of_sync_error(self, get, msg=''): + def out_of_sync_error(self, data, msg=''): """ return dict out-of-sync error message, and also log. """ #This is a dev_facing_error - log.warning("Combined module state out sync. state: %r, get: %r. %s", - self.state, get, msg) + log.warning("Combined module state out sync. state: %r, data: %r. %s", + self.state, data, msg) #This is a student_facing_error return {'success': False, 'error': 'The problem state got out-of-sync. Please try reloading the page.'} diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py index 2ac55a8318..0f0851fbf7 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py @@ -122,17 +122,17 @@ class OpenEndedModule(openendedchild.OpenEndedChild): self.payload = {'grader_payload': updated_grader_payload} - def skip_post_assessment(self, get, system): + def skip_post_assessment(self, _data, system): """ Ajax function that allows one to skip the post assessment phase - @param get: AJAX dictionary + @param data: AJAX dictionary @param system: ModuleSystem @return: Success indicator """ self.child_state = self.DONE return {'success': True} - def message_post(self, get, system): + def message_post(self, data, system): """ Handles a student message post (a reaction to the grade they received from an open ended grader type) Returns a boolean success/fail and an error message @@ -141,7 +141,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): event_info = dict() event_info['problem_id'] = self.location_string event_info['student_id'] = system.anonymous_student_id - event_info['survey_responses'] = get + event_info['survey_responses'] = data survey_responses = event_info['survey_responses'] for tag in ['feedback', 'submission_id', 'grader_id', 'score']: @@ -587,10 +587,10 @@ class OpenEndedModule(openendedchild.OpenEndedChild): html = system.render_template('{0}/open_ended_evaluation.html'.format(self.TEMPLATE_DIR), context) return html - def handle_ajax(self, dispatch, get, system): + def handle_ajax(self, dispatch, data, system): ''' This is called by courseware.module_render, to handle an AJAX call. - "get" is request.POST. + "data" is request.POST. Returns a json dictionary: { 'progress_changed' : True/False, @@ -612,7 +612,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): return json.dumps({'error': 'Error handling action. Please try again.', 'success': False}) before = self.get_progress() - d = handlers[dispatch](get, system) + d = handlers[dispatch](data, system) after = self.get_progress() d.update({ 'progress_changed': after != before, @@ -620,20 +620,20 @@ class OpenEndedModule(openendedchild.OpenEndedChild): }) return json.dumps(d, cls=ComplexEncoder) - def check_for_score(self, get, system): + def check_for_score(self, _data, system): """ Checks to see if a score has been received yet. - @param get: AJAX get dictionary + @param data: AJAX dictionary @param system: Modulesystem (needed to align with other ajax functions) @return: Returns the current state """ state = self.child_state return {'state': state} - def save_answer(self, get, system): + def save_answer(self, data, system): """ Saves a student answer - @param get: AJAX get dictionary + @param data: AJAX dictionary @param system: modulesystem @return: Success indicator """ @@ -644,17 +644,17 @@ class OpenEndedModule(openendedchild.OpenEndedChild): return msg if self.child_state != self.INITIAL: - return self.out_of_sync_error(get) + return self.out_of_sync_error(data) # add new history element with answer and empty score and hint. - success, get = self.append_image_to_student_answer(get) + success, data = self.append_image_to_student_answer(data) error_message = "" if success: success, allowed_to_submit, error_message = self.check_if_student_can_submit() if allowed_to_submit: - get['student_answer'] = OpenEndedModule.sanitize_html(get['student_answer']) - self.new_history_entry(get['student_answer']) - self.send_to_grader(get['student_answer'], system) + data['student_answer'] = OpenEndedModule.sanitize_html(data['student_answer']) + self.new_history_entry(data['student_answer']) + self.send_to_grader(data['student_answer'], system) self.change_state(self.ASSESSING) else: # Error message already defined @@ -666,17 +666,17 @@ class OpenEndedModule(openendedchild.OpenEndedChild): return { 'success': success, 'error': error_message, - 'student_response': get['student_answer'] + 'student_response': data['student_answer'] } - def update_score(self, get, system): + def update_score(self, data, system): """ Updates the current score via ajax. Called by xqueue. - Input: AJAX get dictionary, modulesystem + Input: AJAX data dictionary, modulesystem Output: None """ - queuekey = get['queuekey'] - score_msg = get['xqueue_body'] + queuekey = data['queuekey'] + score_msg = data['xqueue_body'] # TODO: Remove need for cmap self._update_score(score_msg, queuekey, system) diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py index 4f524d2cd7..047ab0244c 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py @@ -272,13 +272,13 @@ class OpenEndedChild(object): return None return None - def out_of_sync_error(self, get, msg=''): + def out_of_sync_error(self, data, msg=''): """ return dict out-of-sync error message, and also log. """ # This is a dev_facing_error - log.warning("Open ended child state out sync. state: %r, get: %r. %s", - self.child_state, get, msg) + log.warning("Open ended child state out sync. state: %r, data: %r. %s", + self.child_state, data, msg) # This is a student_facing_error return {'success': False, 'error': 'The problem state got out-of-sync. Please try reloading the page.'} @@ -345,24 +345,24 @@ class OpenEndedChild(object): return success, image_ok, s3_public_url - def check_for_image_and_upload(self, get_data): + def check_for_image_and_upload(self, data): """ Checks to see if an image was passed back in the AJAX query. If so, it will upload it to S3 - @param get_data: AJAX get data - @return: Success, whether or not a file was in the get dictionary, + @param data: AJAX data + @return: Success, whether or not a file was in the data dictionary, and the html corresponding to the uploaded image """ has_file_to_upload = False uploaded_to_s3 = False image_tag = "" image_ok = False - if 'can_upload_files' in get_data: - if get_data['can_upload_files'] in ['true', '1']: + if 'can_upload_files' in data: + if data['can_upload_files'] in ['true', '1']: has_file_to_upload = True - file = get_data['student_file'][0] - uploaded_to_s3, image_ok, s3_public_url = self.upload_image_to_s3(file) + student_file = data['student_file'][0] + uploaded_to_s3, image_ok, s3_public_url = self.upload_image_to_s3(student_file) if uploaded_to_s3: - image_tag = self.generate_image_tag_from_url(s3_public_url, file.name) + image_tag = self.generate_image_tag_from_url(s3_public_url, student_file.name) return has_file_to_upload, uploaded_to_s3, image_ok, image_tag @@ -371,27 +371,27 @@ class OpenEndedChild(object): Makes an image tag from a given URL @param s3_public_url: URL of the image @param image_name: Name of the image - @return: Boolean success, updated AJAX get data + @return: Boolean success, updated AJAX data """ image_template = """ {1} """.format(s3_public_url, image_name) return image_template - def append_image_to_student_answer(self, get_data): + def append_image_to_student_answer(self, data): """ Adds an image to a student answer after uploading it to S3 - @param get_data: AJAx get data - @return: Boolean success, updated AJAX get data + @param data: AJAx data + @return: Boolean success, updated AJAX data """ overall_success = False if not self.accept_file_upload: # If the question does not accept file uploads, do not do anything - return True, get_data + return True, data - has_file_to_upload, uploaded_to_s3, image_ok, image_tag = self.check_for_image_and_upload(get_data) + has_file_to_upload, uploaded_to_s3, image_ok, image_tag = self.check_for_image_and_upload(data) if uploaded_to_s3 and has_file_to_upload and image_ok: - get_data['student_answer'] += image_tag + data['student_answer'] += image_tag overall_success = True elif has_file_to_upload and not uploaded_to_s3 and image_ok: # In this case, an image was submitted by the student, but the image could not be uploaded to S3. Likely @@ -403,12 +403,12 @@ class OpenEndedChild(object): overall_success = True elif not has_file_to_upload: # If there is no file to upload, probably the student has embedded the link in the answer text - success, get_data['student_answer'] = self.check_for_url_in_text(get_data['student_answer']) + success, data['student_answer'] = self.check_for_url_in_text(data['student_answer']) overall_success = success # log.debug("Has file: {0} Uploaded: {1} Image Ok: {2}".format(has_file_to_upload, uploaded_to_s3, image_ok)) - return overall_success, get_data + return overall_success, data def check_for_url_in_text(self, string): """ diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py index 7beca7a72f..a5498289e2 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py @@ -75,10 +75,10 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): html = system.render_template('{0}/self_assessment_prompt.html'.format(self.TEMPLATE_DIR), context) return html - def handle_ajax(self, dispatch, get, system): + def handle_ajax(self, dispatch, data, system): """ This is called by courseware.module_render, to handle an AJAX call. - "get" is request.POST. + "data" is request.POST. Returns a json dictionary: { 'progress_changed' : True/False, @@ -99,7 +99,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): return json.dumps({'error': 'Error handling action. Please try again.', 'success': False}) before = self.get_progress() - d = handlers[dispatch](get, system) + d = handlers[dispatch](data, system) after = self.get_progress() d.update({ 'progress_changed': after != before, @@ -160,12 +160,12 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): return system.render_template('{0}/self_assessment_hint.html'.format(self.TEMPLATE_DIR), context) - def save_answer(self, get, system): + def save_answer(self, data, system): """ After the answer is submitted, show the rubric. Args: - get: the GET dictionary passed to the ajax request. Should contain + data: the request dictionary passed to the ajax request. Should contain a key 'student_answer' Returns: @@ -178,16 +178,16 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): return msg if self.child_state != self.INITIAL: - return self.out_of_sync_error(get) + return self.out_of_sync_error(data) error_message = "" # add new history element with answer and empty score and hint. - success, get = self.append_image_to_student_answer(get) + success, data = self.append_image_to_student_answer(data) if success: success, allowed_to_submit, error_message = self.check_if_student_can_submit() if allowed_to_submit: - get['student_answer'] = SelfAssessmentModule.sanitize_html(get['student_answer']) - self.new_history_entry(get['student_answer']) + data['student_answer'] = SelfAssessmentModule.sanitize_html(data['student_answer']) + self.new_history_entry(data['student_answer']) self.change_state(self.ASSESSING) else: # Error message already defined @@ -200,10 +200,10 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): 'success': success, 'rubric_html': self.get_rubric_html(system), 'error': error_message, - 'student_response': get['student_answer'], + 'student_response': data['student_answer'], } - def save_assessment(self, get, system): + def save_assessment(self, data, _system): """ Save the assessment. If the student said they're right, don't ask for a hint, and go straight to the done state. Otherwise, do ask for a hint. @@ -219,11 +219,11 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): """ if self.child_state != self.ASSESSING: - return self.out_of_sync_error(get) + return self.out_of_sync_error(data) try: - score = int(get['assessment']) - score_list = get.getlist('score_list[]') + score = int(data['assessment']) + score_list = data.getlist('score_list[]') for i in xrange(0, len(score_list)): score_list[i] = int(score_list[i]) except ValueError: @@ -244,7 +244,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): d['state'] = self.child_state return d - def save_hint(self, get, system): + def save_hint(self, data, _system): ''' Not used currently, as hints have been removed from the system. Save the hint. @@ -258,9 +258,9 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): if self.child_state != self.POST_ASSESSMENT: # Note: because we only ask for hints on wrong answers, may not have # the same number of hints and answers. - return self.out_of_sync_error(get) + return self.out_of_sync_error(data) - self.record_latest_post_assessment(get['hint']) + self.record_latest_post_assessment(data['hint']) self.change_state(self.DONE) return {'success': True, diff --git a/common/lib/xmodule/xmodule/peer_grading_module.py b/common/lib/xmodule/xmodule/peer_grading_module.py index a13fef8e40..7df444a892 100644 --- a/common/lib/xmodule/xmodule/peer_grading_module.py +++ b/common/lib/xmodule/xmodule/peer_grading_module.py @@ -133,8 +133,8 @@ class PeerGradingModule(PeerGradingFields, XModule): """ return {'success': False, 'error': msg} - def _check_required(self, get, required): - actual = set(get.keys()) + def _check_required(self, data, required): + actual = set(data.keys()) missing = required - actual if len(missing) > 0: return False, "Missing required keys: {0}".format(', '.join(missing)) @@ -153,7 +153,7 @@ class PeerGradingModule(PeerGradingFields, XModule): else: return self.peer_grading_problem({'location': self.link_to_location})['html'] - def handle_ajax(self, dispatch, get): + def handle_ajax(self, dispatch, data): """ Needs to be implemented by child modules. Handles AJAX events. @return: @@ -173,7 +173,7 @@ class PeerGradingModule(PeerGradingFields, XModule): # This is a dev_facing_error return json.dumps({'error': 'Error handling action. Please try again.', 'success': False}) - d = handlers[dispatch](get) + d = handlers[dispatch](data) return json.dumps(d, cls=ComplexEncoder) @@ -244,7 +244,7 @@ class PeerGradingModule(PeerGradingFields, XModule): max_grade = self.max_grade return max_grade - def get_next_submission(self, get): + def get_next_submission(self, data): """ Makes a call to the grading controller for the next essay that should be graded Returns a json dict with the following keys: @@ -263,11 +263,11 @@ class PeerGradingModule(PeerGradingFields, XModule): 'error': if success is False, will have an error message with more info. """ required = set(['location']) - success, message = self._check_required(get, required) + success, message = self._check_required(data, required) if not success: return self._err_response(message) grader_id = self.system.anonymous_student_id - location = get['location'] + location = data['location'] try: response = self.peer_gs.get_next_submission(location, grader_id) @@ -280,7 +280,7 @@ class PeerGradingModule(PeerGradingFields, XModule): return {'success': False, 'error': EXTERNAL_GRADER_NO_CONTACT_ERROR} - def save_grade(self, get): + def save_grade(self, data): """ Saves the grade of a given submission. Input: @@ -298,18 +298,18 @@ class PeerGradingModule(PeerGradingFields, XModule): required = set(['location', 'submission_id', 'submission_key', 'score', 'feedback', 'rubric_scores[]', 'submission_flagged']) - success, message = self._check_required(get, required) + success, message = self._check_required(data, required) if not success: return self._err_response(message) grader_id = self.system.anonymous_student_id - location = get.get('location') - submission_id = get.get('submission_id') - score = get.get('score') - feedback = get.get('feedback') - submission_key = get.get('submission_key') - rubric_scores = get.getlist('rubric_scores[]') - submission_flagged = get.get('submission_flagged') + location = data.get('location') + submission_id = data.get('submission_id') + score = data.get('score') + feedback = data.get('feedback') + submission_key = data.get('submission_key') + rubric_scores = data.getlist('rubric_scores[]') + submission_flagged = data.get('submission_flagged') try: response = self.peer_gs.save_grade(location, grader_id, submission_id, @@ -328,7 +328,7 @@ class PeerGradingModule(PeerGradingFields, XModule): 'error': EXTERNAL_GRADER_NO_CONTACT_ERROR } - def is_student_calibrated(self, get): + def is_student_calibrated(self, data): """ Calls the grading controller to see if the given student is calibrated on the given problem @@ -347,12 +347,12 @@ class PeerGradingModule(PeerGradingFields, XModule): """ required = set(['location']) - success, message = self._check_required(get, required) + success, message = self._check_required(data, required) if not success: return self._err_response(message) grader_id = self.system.anonymous_student_id - location = get['location'] + location = data['location'] try: response = self.peer_gs.is_student_calibrated(location, grader_id) @@ -367,7 +367,7 @@ class PeerGradingModule(PeerGradingFields, XModule): 'error': EXTERNAL_GRADER_NO_CONTACT_ERROR } - def show_calibration_essay(self, get): + def show_calibration_essay(self, data): """ Fetch the next calibration essay from the grading controller and return it Inputs: @@ -392,13 +392,13 @@ class PeerGradingModule(PeerGradingFields, XModule): """ required = set(['location']) - success, message = self._check_required(get, required) + success, message = self._check_required(data, required) if not success: return self._err_response(message) grader_id = self.system.anonymous_student_id - location = get['location'] + location = data['location'] try: response = self.peer_gs.show_calibration_essay(location, grader_id) return response @@ -417,8 +417,7 @@ class PeerGradingModule(PeerGradingFields, XModule): return {'success': False, 'error': 'Error displaying submission. Please notify course staff.'} - - def save_calibration_essay(self, get): + def save_calibration_essay(self, data): """ Saves the grader's grade of a given calibration. Input: @@ -437,17 +436,17 @@ class PeerGradingModule(PeerGradingFields, XModule): """ required = set(['location', 'submission_id', 'submission_key', 'score', 'feedback', 'rubric_scores[]']) - success, message = self._check_required(get, required) + success, message = self._check_required(data, required) if not success: return self._err_response(message) grader_id = self.system.anonymous_student_id - location = get.get('location') - calibration_essay_id = get.get('submission_id') - submission_key = get.get('submission_key') - score = get.get('score') - feedback = get.get('feedback') - rubric_scores = get.getlist('rubric_scores[]') + location = data.get('location') + calibration_essay_id = data.get('submission_id') + submission_key = data.get('submission_key') + score = data.get('score') + feedback = data.get('feedback') + rubric_scores = data.getlist('rubric_scores[]') try: response = self.peer_gs.save_calibration_essay(location, grader_id, calibration_essay_id, @@ -473,8 +472,7 @@ class PeerGradingModule(PeerGradingFields, XModule): }) return html - - def peer_grading(self, get=None): + def peer_grading(self, _data=None): ''' Show a peer grading interface ''' @@ -553,11 +551,11 @@ class PeerGradingModule(PeerGradingFields, XModule): return html - def peer_grading_problem(self, get=None): + def peer_grading_problem(self, data=None): ''' Show individual problem interface ''' - if get is None or get.get('location') is None: + if data is None or data.get('location') is None: if not self.use_for_single_location: # This is an error case, because it must be set to use a single location to be called without get parameters # This is a dev_facing_error @@ -566,8 +564,8 @@ class PeerGradingModule(PeerGradingFields, XModule): return {'html': "", 'success': False} problem_location = self.link_to_location - elif get.get('location') is not None: - problem_location = get.get('location') + elif data.get('location') is not None: + problem_location = data.get('location') ajax_url = self.ajax_url html = self.system.render_template('peer_grading/peer_grading_problem.html', { @@ -617,4 +615,3 @@ class PeerGradingDescriptor(PeerGradingFields, RawDescriptor): non_editable_fields.extend([PeerGradingFields.due_date, PeerGradingFields.grace_period_string, PeerGradingFields.max_grade]) return non_editable_fields - diff --git a/common/lib/xmodule/xmodule/poll_module.py b/common/lib/xmodule/xmodule/poll_module.py index 9f2359865a..ca12f239ab 100644 --- a/common/lib/xmodule/xmodule/poll_module.py +++ b/common/lib/xmodule/xmodule/poll_module.py @@ -47,12 +47,12 @@ class PollModule(PollFields, XModule): css = {'scss': [resource_string(__name__, 'css/poll/display.scss')]} js_module_name = "Poll" - def handle_ajax(self, dispatch, get): + def handle_ajax(self, dispatch, data): """Ajax handler. Args: dispatch: string request slug - get: dict request get parameters + data: dict request data parameters Returns: json string diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py index 580f51f6dd..088967ebc0 100644 --- a/common/lib/xmodule/xmodule/seq_module.py +++ b/common/lib/xmodule/xmodule/seq_module.py @@ -62,10 +62,10 @@ class SequenceModule(SequenceFields, XModule): progress = reduce(Progress.add_counts, progresses) return progress - def handle_ajax(self, dispatch, get): # TODO: bounds checking + def handle_ajax(self, dispatch, data): # TODO: bounds checking ''' get = request.POST instance ''' if dispatch == 'goto_position': - self.position = int(get['position']) + self.position = int(data['position']) return json.dumps({'success': True}) raise NotFoundError('Unexpected dispatch type') diff --git a/common/lib/xmodule/xmodule/tests/test_logic.py b/common/lib/xmodule/xmodule/tests/test_logic.py index e62b9a1cee..9be533885c 100644 --- a/common/lib/xmodule/xmodule/tests/test_logic.py +++ b/common/lib/xmodule/xmodule/tests/test_logic.py @@ -40,9 +40,9 @@ class LogicTest(unittest.TestCase): self.raw_model_data ) - def ajax_request(self, dispatch, get): + def ajax_request(self, dispatch, data): """Call Xmodule.handle_ajax.""" - return json.loads(self.xmodule.handle_ajax(dispatch, get)) + return json.loads(self.xmodule.handle_ajax(dispatch, data)) class PollModuleTest(LogicTest): diff --git a/common/lib/xmodule/xmodule/timelimit_module.py b/common/lib/xmodule/xmodule/timelimit_module.py index 9446176f01..3f52ae0baa 100644 --- a/common/lib/xmodule/xmodule/timelimit_module.py +++ b/common/lib/xmodule/xmodule/timelimit_module.py @@ -98,7 +98,7 @@ class TimeLimitModule(TimeLimitFields, XModule): progress = reduce(Progress.add_counts, progresses) return progress - def handle_ajax(self, dispatch, get): + def handle_ajax(self, _dispatch, _data): raise NotFoundError('Unexpected dispatch type') def render(self): @@ -141,4 +141,3 @@ class TimeLimitDescriptor(TimeLimitFields, XMLEditingDescriptor, XmlDescriptor): xml_object.append( etree.fromstring(child.export_to_xml(resource_fs))) return xml_object - diff --git a/common/lib/xmodule/xmodule/video_module.py b/common/lib/xmodule/xmodule/video_module.py index 6344da7994..04daaea3f2 100644 --- a/common/lib/xmodule/xmodule/video_module.py +++ b/common/lib/xmodule/xmodule/video_module.py @@ -54,9 +54,9 @@ class VideoModule(VideoFields, XModule): def __init__(self, *args, **kwargs): XModule.__init__(self, *args, **kwargs) - def handle_ajax(self, dispatch, get): + def handle_ajax(self, dispatch, data): """This is not being called right now and we raise 404 error.""" - log.debug(u"GET {0}".format(get)) + log.debug(u"GET {0}".format(data)) log.debug(u"DISPATCH {0}".format(dispatch)) raise Http404() diff --git a/common/lib/xmodule/xmodule/videoalpha_module.py b/common/lib/xmodule/xmodule/videoalpha_module.py index a64e094a58..6b27bcda2b 100644 --- a/common/lib/xmodule/xmodule/videoalpha_module.py +++ b/common/lib/xmodule/xmodule/videoalpha_module.py @@ -125,9 +125,9 @@ class VideoAlphaModule(VideoAlphaFields, XModule): return parse_time(xmltree.get('start_time')), parse_time(xmltree.get('end_time')) - def handle_ajax(self, dispatch, get): + def handle_ajax(self, dispatch, data): """This is not being called right now and we raise 404 error.""" - log.debug(u"GET {0}".format(get)) + log.debug(u"GET {0}".format(data)) log.debug(u"DISPATCH {0}".format(dispatch)) raise Http404() diff --git a/common/lib/xmodule/xmodule/word_cloud_module.py b/common/lib/xmodule/xmodule/word_cloud_module.py index ac5b3051de..a7f3f92795 100644 --- a/common/lib/xmodule/xmodule/word_cloud_module.py +++ b/common/lib/xmodule/xmodule/word_cloud_module.py @@ -168,12 +168,12 @@ class WordCloudModule(WordCloudFields, XModule): )[:amount] ) - def handle_ajax(self, dispatch, post): + def handle_ajax(self, dispatch, data): """Ajax handler. Args: dispatch: string request slug - post: dict request get parameters + data: dict request get parameters Returns: json string @@ -187,7 +187,7 @@ class WordCloudModule(WordCloudFields, XModule): # Student words from client. # FIXME: we must use raw JSON, not a post data (multipart/form-data) - raw_student_words = post.getlist('student_words[]') + raw_student_words = data.getlist('student_words[]') student_words = filter(None, map(self.good_word, raw_student_words)) self.student_words = student_words diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index f5705bf662..0f5bbf4f2e 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -272,9 +272,9 @@ class XModule(XModuleFields, HTMLSnippet, XBlock): ''' return None - def handle_ajax(self, _dispatch, _get): + def handle_ajax(self, _dispatch, _data): ''' dispatch is last part of the URL. - get is a dictionary-like object ''' + data is a dictionary-like object with the content of the request''' return "" diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 15a6ad2dab..d17efa3697 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -223,7 +223,7 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours relative_xqueue_callback_url = reverse('xqueue_callback', kwargs=dict(course_id=course_id, userid=str(user.id), - id=descriptor.location.url(), + mod_id=descriptor.location.url(), dispatch=dispatch), ) return xqueue_callback_url_prefix + relative_xqueue_callback_url @@ -399,40 +399,47 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours @csrf_exempt -def xqueue_callback(request, course_id, userid, id, dispatch): +def xqueue_callback(request, course_id, userid, mod_id, dispatch): ''' Entry point for graded results from the queueing system. ''' + data = request.POST.copy() + # Test xqueue package, which we expect to be: # xpackage = {'xqueue_header': json.dumps({'lms_key':'secretkey',...}), # 'xqueue_body' : 'Message from grader'} - get = request.POST.copy() for key in ['xqueue_header', 'xqueue_body']: - if not get.has_key(key): + if key not in data: raise Http404 - header = json.loads(get['xqueue_header']) - if not isinstance(header, dict) or not header.has_key('lms_key'): + + header = json.loads(data['xqueue_header']) + if not isinstance(header, dict) or 'lms_key' not in header: raise Http404 # Retrieve target StudentModule user = User.objects.get(id=userid) - - model_data_cache = ModelDataCache.cache_for_descriptor_descendents(course_id, - user, modulestore().get_instance(course_id, id), depth=0, select_for_update=True) - instance = get_module(user, request, id, model_data_cache, course_id, grade_bucket_type='xqueue') + model_data_cache = ModelDataCache.cache_for_descriptor_descendents( + course_id, + user, + modulestore().get_instance(course_id, mod_id), + depth=0, + select_for_update=True + ) + instance = get_module(user, request, mod_id, model_data_cache, course_id, grade_bucket_type='xqueue') if instance is None: - log.debug("No module {0} for user {1}--access denied?".format(id, user)) + msg = "No module {0} for user {1}--access denied?".format(mod_id, user) + log.debug(msg) raise Http404 - # Transfer 'queuekey' from xqueue response header to 'get'. This is required to - # use the interface defined by 'handle_ajax' - get.update({'queuekey': header['lms_key']}) + # Transfer 'queuekey' from xqueue response header to the data. + # This is required to use the interface defined by 'handle_ajax' + data.update({'queuekey': header['lms_key']}) # We go through the "AJAX" path - # So far, the only dispatch from xqueue will be 'score_update' + # So far, the only dispatch from xqueue will be 'score_update' try: # Can ignore the return value--not used for xqueue_callback - instance.handle_ajax(dispatch, get) + instance.handle_ajax(dispatch, data) except: log.exception("error processing ajax call") raise @@ -466,23 +473,15 @@ def modx_dispatch(request, dispatch, location, course_id): if not request.user.is_authenticated(): raise PermissionDenied - # Check for submitted files and basic file size checks - p = request.POST.copy() - if request.FILES: - for fileinput_id in request.FILES.keys(): - inputfiles = request.FILES.getlist(fileinput_id) + # Get the submitted data + data = request.POST.copy() - if len(inputfiles) > settings.MAX_FILEUPLOADS_PER_INPUT: - too_many_files_msg = 'Submission aborted! Maximum %d files may be submitted at once' % \ - settings.MAX_FILEUPLOADS_PER_INPUT - return HttpResponse(json.dumps({'success': too_many_files_msg})) - - for inputfile in inputfiles: - if inputfile.size > settings.STUDENT_FILEUPLOAD_MAX_SIZE: # Bytes - file_too_big_msg = 'Submission aborted! Your file "%s" is too large (max size: %d MB)' % \ - (inputfile.name, settings.STUDENT_FILEUPLOAD_MAX_SIZE / (1000 ** 2)) - return HttpResponse(json.dumps({'success': file_too_big_msg})) - p[fileinput_id] = inputfiles + # Get and check submitted files + files = request.FILES or {} + error_msg = _check_files_limits(files) + if error_msg: + return HttpResponse(json.dumps({'success': error_msg})) + data.update(files) # Merge files into data dictionary try: descriptor = modulestore().get_instance(course_id, location) @@ -495,8 +494,11 @@ def modx_dispatch(request, dispatch, location, course_id): ) raise Http404 - model_data_cache = ModelDataCache.cache_for_descriptor_descendents(course_id, - request.user, descriptor) + model_data_cache = ModelDataCache.cache_for_descriptor_descendents( + course_id, + request.user, + descriptor + ) instance = get_module(request.user, request, location, model_data_cache, course_id, grade_bucket_type='ajax') if instance is None: @@ -507,7 +509,7 @@ def modx_dispatch(request, dispatch, location, course_id): # Let the module handle the AJAX try: - ajax_return = instance.handle_ajax(dispatch, p) + ajax_return = instance.handle_ajax(dispatch, data) # If we can't find the module, respond with a 404 except NotFoundError: @@ -529,7 +531,6 @@ def modx_dispatch(request, dispatch, location, course_id): return HttpResponse(ajax_return) - def get_score_bucket(grade, max_grade): """ Function to split arbitrary score ranges into 3 buckets. @@ -542,3 +543,30 @@ def get_score_bucket(grade, max_grade): score_bucket = "correct" return score_bucket + + +def _check_files_limits(files): + """ + Check if the files in a request are under the limits defined by + `settings.MAX_FILEUPLOADS_PER_INPUT` and + `settings.STUDENT_FILEUPLOAD_MAX_SIZE`. + + Returns None if files are correct or an error messages otherwise. + """ + for fileinput_id in files.keys(): + inputfiles = files.getlist(fileinput_id) + + # Check number of files submitted + if len(inputfiles) > settings.MAX_FILEUPLOADS_PER_INPUT: + msg = 'Submission aborted! Maximum %d files may be submitted at once' %\ + settings.MAX_FILEUPLOADS_PER_INPUT + return msg + + # Check file sizes + for inputfile in inputfiles: + if inputfile.size > settings.STUDENT_FILEUPLOAD_MAX_SIZE: # Bytes + msg = 'Submission aborted! Your file "%s" is too large (max size: %d MB)' %\ + (inputfile.name, settings.STUDENT_FILEUPLOAD_MAX_SIZE / (1000 ** 2)) + return msg + + return None diff --git a/lms/urls.py b/lms/urls.py index 52ce539f73..88916bd334 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -188,7 +188,7 @@ if settings.COURSEWARE_ENABLED: # into the database. url(r'^software-licenses$', 'licenses.views.user_software_license', name="user_software_license"), - url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/xqueue/(?P[^/]*)/(?P.*?)/(?P[^/]*)$', + url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/xqueue/(?P[^/]*)/(?P.*?)/(?P[^/]*)$', 'courseware.module_render.xqueue_callback', name='xqueue_callback'), url(r'^change_setting$', 'student.views.change_setting', @@ -438,5 +438,3 @@ if settings.DEBUG: #Custom error pages handler404 = 'static_template_view.views.render_404' handler500 = 'static_template_view.views.render_500' - - From ed62c5a6f944f7d917fba7819f243da0c9fac23f Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 25 Jun 2013 14:23:16 -0400 Subject: [PATCH 343/375] Fix LMS-500: Random class in random module was None Deleting the module object isn't needed to replace it, and deleting a module object causes all of its attributes to be set to None. --- common/lib/capa/capa/safe_exec/safe_exec.py | 1 - .../lib/capa/capa/tests/test_responsetypes.py | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/common/lib/capa/capa/safe_exec/safe_exec.py b/common/lib/capa/capa/safe_exec/safe_exec.py index 3ab8f0bf9e..be33bcaa5b 100644 --- a/common/lib/capa/capa/safe_exec/safe_exec.py +++ b/common/lib/capa/capa/safe_exec/safe_exec.py @@ -18,7 +18,6 @@ import random as random_module import sys random = random_module.Random(%r) random.Random = random_module.Random -del random_module sys.modules['random'] = random """ diff --git a/common/lib/capa/capa/tests/test_responsetypes.py b/common/lib/capa/capa/tests/test_responsetypes.py index 68be54b6af..594e2ca629 100644 --- a/common/lib/capa/capa/tests/test_responsetypes.py +++ b/common/lib/capa/capa/tests/test_responsetypes.py @@ -1266,6 +1266,24 @@ class CustomResponseTest(ResponseTest): msg = correct_map.get_msg('1_2_1') self.assertEqual(msg, self._get_random_number_result(problem.seed)) + def test_random_isnt_none(self): + # Bug LMS-500 says random.seed(10) fails with: + # File "", line 61, in + # File "/usr/lib/python2.7/random.py", line 116, in seed + # super(Random, self).seed(a) + # TypeError: must be type, not None + + r = random.Random() + r.seed(10) + num = r.randint(0, 1e9) + + script = textwrap.dedent(""" + random.seed(10) + num = random.randint(0, 1e9) + """) + problem = self.build_problem(script=script) + self.assertEqual(problem.context['num'], num) + def test_module_imports_inline(self): ''' Check that the correct modules are available to custom From bcbce3eff0bec489c718ea8cf414f3c38f54527a Mon Sep 17 00:00:00 2001 From: Renzo Lucioni Date: Mon, 24 Jun 2013 16:53:01 -0400 Subject: [PATCH 344/375] Add handful of events to the Segment.io whitelist --- common/lib/xmodule/xmodule/js/src/capa/display.coffee | 8 ++++---- common/static/coffee/src/logger.coffee | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index f773fc81c4..6b355459e9 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -138,7 +138,7 @@ class @Problem # maybe preferable to consolidate all dispatches to use FormData ### check_fd: => - Logger.log 'problem_check', @answers + Logger.log 'problem_check', answers: @answers # If there are no file inputs in the problem, we can fall back on @check if $('input:file').length == 0 @@ -212,7 +212,7 @@ class @Problem $.ajaxWithPrefix("#{@url}/problem_check", settings) check: => - Logger.log 'problem_check', @answers + Logger.log 'problem_check', answers: @answers $.postWithPrefix "#{@url}/problem_check", @answers, (response) => switch response.success when 'incorrect', 'correct' @@ -224,7 +224,7 @@ class @Problem @gentle_alert response.success reset: => - Logger.log 'problem_reset', @answers + Logger.log 'problem_reset', answers: @answers $.postWithPrefix "#{@url}/problem_reset", id: @id, (response) => @render(response.html) @updateProgress response @@ -284,7 +284,7 @@ class @Problem @el.find('.capa_alert').css(opacity: 0).animate(opacity: 1, 700) save: => - Logger.log 'problem_save', @answers + Logger.log 'problem_save', answers: @answers $.postWithPrefix "#{@url}/problem_save", @answers, (response) => saveMessage = response.msg @gentle_alert saveMessage diff --git a/common/static/coffee/src/logger.coffee b/common/static/coffee/src/logger.coffee index 6da4929fb0..dbc2b8e004 100644 --- a/common/static/coffee/src/logger.coffee +++ b/common/static/coffee/src/logger.coffee @@ -1,6 +1,6 @@ class @Logger # events we want sent to Segment.io for tracking - SEGMENT_IO_WHITELIST = ["seq_goto", "seq_next", "seq_prev"] + SEGMENT_IO_WHITELIST = ["seq_goto", "seq_next", "seq_prev", "problem_check", "problem_reset", "problem_show", "problem_save"] @log: (event_type, data) -> if event_type in SEGMENT_IO_WHITELIST From 84f4361d522c9898b69fa3e16c0540126673b5cc Mon Sep 17 00:00:00 2001 From: Renzo Lucioni Date: Tue, 25 Jun 2013 15:15:28 -0400 Subject: [PATCH 345/375] Avoid changing format of data sent to our logs, and prevent problem_check event from firing twice --- common/lib/xmodule/xmodule/js/src/capa/display.coffee | 9 +++++---- common/static/coffee/src/logger.coffee | 8 ++++++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index 6b355459e9..bf6aba0a21 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -138,7 +138,7 @@ class @Problem # maybe preferable to consolidate all dispatches to use FormData ### check_fd: => - Logger.log 'problem_check', answers: @answers + Logger.log 'problem_check', @answers # If there are no file inputs in the problem, we can fall back on @check if $('input:file').length == 0 @@ -212,7 +212,8 @@ class @Problem $.ajaxWithPrefix("#{@url}/problem_check", settings) check: => - Logger.log 'problem_check', answers: @answers + # Calling check from within check_fd will result in firing the 'problem_check' event twice + # Logger.log 'problem_check', @answers $.postWithPrefix "#{@url}/problem_check", @answers, (response) => switch response.success when 'incorrect', 'correct' @@ -224,7 +225,7 @@ class @Problem @gentle_alert response.success reset: => - Logger.log 'problem_reset', answers: @answers + Logger.log 'problem_reset', @answers $.postWithPrefix "#{@url}/problem_reset", id: @id, (response) => @render(response.html) @updateProgress response @@ -284,7 +285,7 @@ class @Problem @el.find('.capa_alert').css(opacity: 0).animate(opacity: 1, 700) save: => - Logger.log 'problem_save', answers: @answers + Logger.log 'problem_save', @answers $.postWithPrefix "#{@url}/problem_save", @answers, (response) => saveMessage = response.msg @gentle_alert saveMessage diff --git a/common/static/coffee/src/logger.coffee b/common/static/coffee/src/logger.coffee index dbc2b8e004..f2dfef5132 100644 --- a/common/static/coffee/src/logger.coffee +++ b/common/static/coffee/src/logger.coffee @@ -3,9 +3,13 @@ class @Logger SEGMENT_IO_WHITELIST = ["seq_goto", "seq_next", "seq_prev", "problem_check", "problem_reset", "problem_show", "problem_save"] @log: (event_type, data) -> + # Segment.io event tracking if event_type in SEGMENT_IO_WHITELIST - # Segment.io event tracking - analytics.track event_type, data + # to avoid changing the format of data sent to our servers, we only massage it here + if typeof data isnt 'object' or data is null + analytics.track event_type, value: data + else + analytics.track event_type, data $.getWithPrefix '/event', event_type: event_type From 401dc82c46085c6663c67e60d6da32291afb8028 Mon Sep 17 00:00:00 2001 From: cahrens Date: Tue, 25 Jun 2013 15:35:47 -0400 Subject: [PATCH 346/375] Don't mention edge in the subject line; use same message for edx and edge. --- cms/templates/emails/activation_email_subject.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/templates/emails/activation_email_subject.txt b/cms/templates/emails/activation_email_subject.txt index 0b0fb2ffe9..f4ffdccb14 100644 --- a/cms/templates/emails/activation_email_subject.txt +++ b/cms/templates/emails/activation_email_subject.txt @@ -1 +1 @@ -Your account for edX edge +Your account for edX Studio From 881d63dae7177adc4ff9e0cad595eac37d18a604 Mon Sep 17 00:00:00 2001 From: Renzo Lucioni Date: Tue, 25 Jun 2013 16:04:00 -0400 Subject: [PATCH 347/375] Fixed Jasmine tests in light of Logger changes, and wrote test to cover case where data passed is not a dictionary --- common/static/coffee/spec/logger_spec.coffee | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/common/static/coffee/spec/logger_spec.coffee b/common/static/coffee/spec/logger_spec.coffee index 8866daa570..7fe734d8b5 100644 --- a/common/static/coffee/spec/logger_spec.coffee +++ b/common/static/coffee/spec/logger_spec.coffee @@ -3,10 +3,15 @@ describe 'Logger', -> expect(window.log_event).toBe Logger.log describe 'log', -> - it 'sends an event to Segment.io, if the event is whitelisted', -> + it 'sends an event to Segment.io, if the event is whitelisted and the data is not a dictionary', -> spyOn(analytics, 'track') Logger.log 'seq_goto', 'data' - expect(analytics.track).toHaveBeenCalledWith 'seq_goto', 'data' + expect(analytics.track).toHaveBeenCalledWith 'seq_goto', value: 'data' + + it 'sends an event to Segment.io, if the event is whitelisted and the data is a dictionary', -> + spyOn(analytics, 'track') + Logger.log 'seq_goto', value: 'data' + expect(analytics.track).toHaveBeenCalledWith 'seq_goto', value: 'data' it 'send a request to log event', -> spyOn $, 'getWithPrefix' From 6c66736e3c51a3340e2322957e5885207bcf48dc Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Tue, 25 Jun 2013 16:56:36 -0400 Subject: [PATCH 348/375] Specify a different xcontent mongo db for the acceptance tests --- cms/envs/acceptance.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index 6293219f43..871b744282 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -40,6 +40,21 @@ MODULESTORE = { 'OPTIONS': MODULESTORE_OPTIONS } } + +CONTENTSTORE = { + 'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore', + 'OPTIONS': { + 'host': 'localhost', + 'db': 'acceptance_xcontent', + }, + # allow for additional options that can be keyed on a name, e.g. 'trashcan' + 'ADDITIONAL_OPTIONS': { + 'trashcan': { + 'bucket': 'trash_fs' + } + } +} + # Set this up so that rake lms[acceptance] and running the # harvest command both use the same (test) database # which they can flush without messing up your dev db From 3f49da385f1275f5cf24959008103e89a29e050d Mon Sep 17 00:00:00 2001 From: Renzo Lucioni Date: Tue, 25 Jun 2013 17:22:05 -0400 Subject: [PATCH 349/375] Swap Logger call from check_fd to check --- common/lib/xmodule/xmodule/js/src/capa/display.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index bf6aba0a21..1f3be9e5e9 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -138,7 +138,8 @@ class @Problem # maybe preferable to consolidate all dispatches to use FormData ### check_fd: => - Logger.log 'problem_check', @answers + # Calling check from check_fd will result in firing the 'problem_check' event twice, since it is also called in the check function. + #Logger.log 'problem_check', @answers # If there are no file inputs in the problem, we can fall back on @check if $('input:file').length == 0 @@ -212,8 +213,7 @@ class @Problem $.ajaxWithPrefix("#{@url}/problem_check", settings) check: => - # Calling check from within check_fd will result in firing the 'problem_check' event twice - # Logger.log 'problem_check', @answers + Logger.log 'problem_check', @answers $.postWithPrefix "#{@url}/problem_check", @answers, (response) => switch response.success when 'incorrect', 'correct' From 8b23eeca7e8992ac56db892d19cf909c10f9b717 Mon Sep 17 00:00:00 2001 From: Don Mitchell Date: Mon, 24 Jun 2013 15:53:24 -0400 Subject: [PATCH 350/375] Minor pylint/whitespace changes --- cms/djangoapps/contentstore/features/video-editor.py | 2 +- cms/djangoapps/contentstore/utils.py | 2 +- cms/templates/overview.html | 3 ++- common/lib/xmodule/xmodule/modulestore/draft.py | 4 ++-- common/lib/xmodule/xmodule/modulestore/mongo.py | 2 +- .../combined_open_ended_modulev1.py | 2 +- common/lib/xmodule/xmodule/tests/test_import.py | 10 ++++++---- common/lib/xmodule/xmodule/tests/test_xml_module.py | 4 ++-- common/lib/xmodule/xmodule/xml_module.py | 9 ++++----- lms/djangoapps/courseware/access.py | 4 +--- lms/djangoapps/courseware/features/problems.py | 6 +++--- lms/djangoapps/courseware/module_render.py | 2 +- 12 files changed, 25 insertions(+), 25 deletions(-) diff --git a/cms/djangoapps/contentstore/features/video-editor.py b/cms/djangoapps/contentstore/features/video-editor.py index 987b4959b8..a6865fdd6d 100644 --- a/cms/djangoapps/contentstore/features/video-editor.py +++ b/cms/djangoapps/contentstore/features/video-editor.py @@ -1,5 +1,5 @@ # disable missing docstring -#pylint: disable=C0111 +# pylint: disable=C0111 from lettuce import world, step diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 0bfa70e4f5..c9c40ab95d 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -10,7 +10,7 @@ from xmodule.modulestore.draft import DIRECT_ONLY_CATEGORIES log = logging.getLogger(__name__) -#In order to instantiate an open ended tab automatically, need to have this data +# In order to instantiate an open ended tab automatically, need to have this data OPEN_ENDED_PANEL = {"name": "Open Ended Panel", "type": "open_ended"} NOTES_PANEL = {"name": "My Notes", "type": "notes"} EXTRA_TAB_PANELS = dict([(p['type'], p) for p in [OPEN_ENDED_PANEL, NOTES_PANEL]]) diff --git a/cms/templates/overview.html b/cms/templates/overview.html index 43d0afc263..a504d50019 100644 --- a/cms/templates/overview.html +++ b/cms/templates/overview.html @@ -167,7 +167,8 @@ %else: Will Release: ${date_utils.get_default_time_display(section.lms.start)} - Edit + Edit %endif

diff --git a/common/lib/xmodule/xmodule/modulestore/draft.py b/common/lib/xmodule/xmodule/modulestore/draft.py index 94823b0be4..41c8e2ec1e 100644 --- a/common/lib/xmodule/xmodule/modulestore/draft.py +++ b/common/lib/xmodule/xmodule/modulestore/draft.py @@ -101,12 +101,12 @@ class DraftModuleStore(ModuleStoreBase): draft_items = super(DraftModuleStore, self).get_items(draft_loc, course_id=course_id, depth=depth) items = super(DraftModuleStore, self).get_items(location, course_id=course_id, depth=depth) - draft_locs_found = set(item.location._replace(revision=None) for item in draft_items) + draft_locs_found = set(item.location.replace(revision=None) for item in draft_items) non_draft_items = [ item for item in items if (item.location.revision != DRAFT - and item.location._replace(revision=None) not in draft_locs_found) + and item.location.replace(revision=None) not in draft_locs_found) ] return [wrap_draft(item) for item in draft_items + non_draft_items] diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py index 40288a933b..32323d5892 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo.py @@ -195,7 +195,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem): if self.cached_metadata is not None: # parent container pointers don't differentiate between draft and non-draft # so when we do the lookup, we should do so with a non-draft location - non_draft_loc = location._replace(revision=None) + non_draft_loc = location.replace(revision=None) metadata_to_inherit = self.cached_metadata.get(non_draft_loc.url(), {}) inherit_metadata(module, metadata_to_inherit) return module diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py index 538901890c..1fe62035e6 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py @@ -646,7 +646,7 @@ class CombinedOpenEndedV1Module(): if self.student_attempts > self.attempts: return { 'success': False, - #This is a student_facing_error + # This is a student_facing_error 'error': ( 'You have attempted this question {0} times. ' 'You are only allowed to attempt it {1} times.' diff --git a/common/lib/xmodule/xmodule/tests/test_import.py b/common/lib/xmodule/xmodule/tests/test_import.py index 4e9a9f9600..79b49c65ae 100644 --- a/common/lib/xmodule/xmodule/tests/test_import.py +++ b/common/lib/xmodule/xmodule/tests/test_import.py @@ -157,9 +157,10 @@ class ImportTestCase(BaseCourseTestCase): self.assertEqual(child.lms.due, ImportTestCase.date.from_json(v)) self.assertEqual(child._inheritable_metadata, child._inherited_metadata) self.assertEqual(2, len(child._inherited_metadata)) - self.assertLessEqual(ImportTestCase.date.from_json( - child._inherited_metadata['start']), - datetime.datetime.now(UTC())) + self.assertLessEqual( + ImportTestCase.date.from_json(child._inherited_metadata['start']), + datetime.datetime.now(UTC()) + ) self.assertEqual(v, child._inherited_metadata['due']) # Now export and check things @@ -221,7 +222,8 @@ class ImportTestCase(BaseCourseTestCase): # why do these tests look in the internal structure v just calling child.start? self.assertLessEqual( ImportTestCase.date.from_json(child._inherited_metadata['start']), - datetime.datetime.now(UTC())) + datetime.datetime.now(UTC()) + ) def test_metadata_override_default(self): """ diff --git a/common/lib/xmodule/xmodule/tests/test_xml_module.py b/common/lib/xmodule/xmodule/tests/test_xml_module.py index 6ec82275af..6581ce58f6 100644 --- a/common/lib/xmodule/xmodule/tests/test_xml_module.py +++ b/common/lib/xmodule/xmodule/tests/test_xml_module.py @@ -248,7 +248,7 @@ class TestDeserializeFloat(TestDeserialize): test_field = Float def test_deserialize(self): - self.assertDeserializeEqual( -2, '-2') + self.assertDeserializeEqual(-2, '-2') self.assertDeserializeEqual("450", '"450"') self.assertDeserializeEqual(-2.78, '-2.78') self.assertDeserializeEqual("0.45", '"0.45"') @@ -256,7 +256,7 @@ class TestDeserializeFloat(TestDeserialize): # False can be parsed as a float (converts to 0) self.assertDeserializeEqual(False, 'false') # True can be parsed as a float (converts to 1) - self.assertDeserializeEqual( True, 'true') + self.assertDeserializeEqual(True, 'true') def test_deserialize_unsupported_types(self): self.assertDeserializeEqual('[3]', '[3]') diff --git a/common/lib/xmodule/xmodule/xml_module.py b/common/lib/xmodule/xmodule/xml_module.py index 33120ec180..2a7a15d434 100644 --- a/common/lib/xmodule/xmodule/xml_module.py +++ b/common/lib/xmodule/xmodule/xml_module.py @@ -141,9 +141,9 @@ class XmlDescriptor(XModuleDescriptor): # Related: What's the right behavior for clean_metadata? metadata_attributes = ('format', 'graceperiod', 'showanswer', 'rerandomize', 'start', 'due', 'graded', 'display_name', 'url_name', 'hide_from_toc', - 'ispublic', # if True, then course is listed for all users; see - 'xqa_key', # for xqaa server access - 'giturl', # url of git server for origin of file + 'ispublic', # if True, then course is listed for all users; see + 'xqa_key', # for xqaa server access + 'giturl', # url of git server for origin of file # information about testcenter exams is a dict (of dicts), not a string, # so it cannot be easily exportable as a course element's attribute. 'testcenter_info', @@ -347,7 +347,7 @@ class XmlDescriptor(XModuleDescriptor): model_data['children'] = children model_data['xml_attributes'] = {} - model_data['xml_attributes']['filename'] = definition.get('filename', ['', None]) # for git link + model_data['xml_attributes']['filename'] = definition.get('filename', ['', None]) # for git link for key, value in metadata.items(): if key not in set(f.name for f in cls.fields + cls.lms.fields): model_data['xml_attributes'][key] = value @@ -409,7 +409,6 @@ class XmlDescriptor(XModuleDescriptor): # don't want e.g. data_dir if attr not in self.metadata_to_strip and attr not in self.metadata_to_export_to_policy: val = val_for_xml(attr) - #logging.debug('location.category = {0}, attr = {1}'.format(self.location.category, attr)) try: xml_object.set(attr, val) except Exception, e: diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py index ec90260928..50b536d444 100644 --- a/lms/djangoapps/courseware/access.py +++ b/lms/djangoapps/courseware/access.py @@ -523,10 +523,8 @@ def _adjust_start_date_for_beta_testers(user, descriptor): beta_group = course_beta_test_group_name(descriptor.location) if beta_group in user_groups: debug("Adjust start time: user in group %s", beta_group) - start_as_datetime = descriptor.lms.start delta = timedelta(descriptor.lms.days_early_for_beta) - effective = start_as_datetime - delta - # ...and back to time_struct + effective = descriptor.lms.start - delta return effective return descriptor.lms.start diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index 08c5207303..39b99214c8 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -2,8 +2,8 @@ Steps for problem.feature lettuce tests ''' -#pylint: disable=C0111 -#pylint: disable=W0621 +# pylint: disable=C0111 +# pylint: disable=W0621 from lettuce import world, step from lettuce.django import django_url @@ -135,7 +135,7 @@ def action_button_present(_step, buttonname, doesnt_appear): @step(u'the button with the label "([^"]*)" does( not)? appear') -def button_with_label_present(step, buttonname, doesnt_appear): +def button_with_label_present(_step, buttonname, doesnt_appear): if doesnt_appear: assert world.browser.is_text_not_present(buttonname, wait_time=5) else: diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index d17efa3697..5c12725d0a 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -354,7 +354,7 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours system.set('position', position) system.set('DEBUG', settings.DEBUG) if settings.MITX_FEATURES.get('ENABLE_PSYCHOMETRICS'): - system.set('psychometrics_handler', # set callback for updating PsychometricsData + system.set('psychometrics_handler', # set callback for updating PsychometricsData make_psychometrics_data_update_handler(course_id, user, descriptor.location.url())) try: From b42fe277d4f84dd0ec7192f09f172c641ef16a32 Mon Sep 17 00:00:00 2001 From: Don Mitchell Date: Mon, 24 Jun 2013 12:56:03 -0400 Subject: [PATCH 351/375] Add serial commas to modulestore definitions --- cms/envs/acceptance.py | 2 +- cms/envs/dev.py | 2 +- cms/envs/test.py | 2 +- lms/djangoapps/courseware/tests/tests.py | 2 +- lms/envs/acceptance.py | 2 +- lms/envs/cms/dev.py | 2 +- lms/envs/dev_mongo.py | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index 871b744282..c70ca98902 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -23,7 +23,7 @@ MODULESTORE_OPTIONS = { 'db': 'test_xmodule', 'collection': 'acceptance_modulestore', 'fs_root': TEST_ROOT / "data", - 'render_template': 'mitxmako.shortcuts.render_to_string' + 'render_template': 'mitxmako.shortcuts.render_to_string', } MODULESTORE = { diff --git a/cms/envs/dev.py b/cms/envs/dev.py index 2dcb3640ca..26d633484e 100644 --- a/cms/envs/dev.py +++ b/cms/envs/dev.py @@ -22,7 +22,7 @@ modulestore_options = { 'db': 'xmodule', 'collection': 'modulestore', 'fs_root': GITHUB_REPO_ROOT, - 'render_template': 'mitxmako.shortcuts.render_to_string' + 'render_template': 'mitxmako.shortcuts.render_to_string', } MODULESTORE = { diff --git a/cms/envs/test.py b/cms/envs/test.py index 89813dd937..bd833426d6 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -48,7 +48,7 @@ MODULESTORE_OPTIONS = { 'db': 'test_xmodule', 'collection': 'test_modulestore', 'fs_root': TEST_ROOT / "data", - 'render_template': 'mitxmako.shortcuts.render_to_string' + 'render_template': 'mitxmako.shortcuts.render_to_string', } MODULESTORE = { diff --git a/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py index 056a73e7c8..e862ed62c3 100644 --- a/lms/djangoapps/courseware/tests/tests.py +++ b/lms/djangoapps/courseware/tests/tests.py @@ -65,7 +65,7 @@ def mongo_store_config(data_dir): 'db': 'test_xmodule', 'collection': 'modulestore_%s' % uuid4().hex, 'fs_root': data_dir, - 'render_template': 'mitxmako.shortcuts.render_to_string' + 'render_template': 'mitxmako.shortcuts.render_to_string', } } } diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index 700fc89670..3b87bb4326 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -24,7 +24,7 @@ modulestore_options = { 'db': 'test_xmodule', 'collection': 'acceptance_modulestore', 'fs_root': TEST_ROOT / "data", - 'render_template': 'mitxmako.shortcuts.render_to_string' + 'render_template': 'mitxmako.shortcuts.render_to_string', } MODULESTORE = { diff --git a/lms/envs/cms/dev.py b/lms/envs/cms/dev.py index f8c43148b0..e55c6d61b5 100644 --- a/lms/envs/cms/dev.py +++ b/lms/envs/cms/dev.py @@ -21,7 +21,7 @@ modulestore_options = { 'db': 'xmodule', 'collection': 'modulestore', 'fs_root': DATA_DIR, - 'render_template': 'mitxmako.shortcuts.render_to_string' + 'render_template': 'mitxmako.shortcuts.render_to_string', } MODULESTORE = { diff --git a/lms/envs/dev_mongo.py b/lms/envs/dev_mongo.py index 1f6b5899f1..dfbf473b45 100644 --- a/lms/envs/dev_mongo.py +++ b/lms/envs/dev_mongo.py @@ -19,7 +19,7 @@ MODULESTORE = { 'db': 'xmodule', 'collection': 'modulestore', 'fs_root': GITHUB_REPO_ROOT, - 'render_template': 'mitxmako.shortcuts.render_to_string' + 'render_template': 'mitxmako.shortcuts.render_to_string', } } } From d9575a0874eb4fefa20face16f70565b52a1da3d Mon Sep 17 00:00:00 2001 From: Don Mitchell Date: Mon, 24 Jun 2013 12:57:23 -0400 Subject: [PATCH 352/375] Remove traling commas to make json valid --- common/test/data/graphic_slider_tool/policies/2012_Fall.json | 4 ++-- common/test/data/self_assessment/policies/2012_Fall.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/common/test/data/graphic_slider_tool/policies/2012_Fall.json b/common/test/data/graphic_slider_tool/policies/2012_Fall.json index 6958f8432c..9058481dc8 100644 --- a/common/test/data/graphic_slider_tool/policies/2012_Fall.json +++ b/common/test/data/graphic_slider_tool/policies/2012_Fall.json @@ -9,6 +9,6 @@ "display_name": "Overview" }, "graphical_slider_tool/sample_gst": { - "display_name": "Sample GST", - }, + "display_name": "Sample GST" + } } diff --git a/common/test/data/self_assessment/policies/2012_Fall.json b/common/test/data/self_assessment/policies/2012_Fall.json index aae4670296..46529abcee 100644 --- a/common/test/data/self_assessment/policies/2012_Fall.json +++ b/common/test/data/self_assessment/policies/2012_Fall.json @@ -9,6 +9,6 @@ "display_name": "Overview" }, "selfassessment/SampleQuestion": { - "display_name": "Sample Question", - }, + "display_name": "Sample Question" + } } From 734440f4b9e7f6596d870a246dac1c1ca63c2544 Mon Sep 17 00:00:00 2001 From: Jason Bau Date: Tue, 25 Jun 2013 20:21:20 -0700 Subject: [PATCH 353/375] Refactored tests --- common/djangoapps/student/tests/tests.py | 55 ++++++++++++++---------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py index 10836122b8..844ddb536e 100644 --- a/common/djangoapps/student/tests/tests.py +++ b/common/djangoapps/student/tests/tests.py @@ -15,8 +15,11 @@ from django.test import TestCase from django.test.client import RequestFactory from django.contrib.auth.models import User from django.contrib.auth.hashers import UNUSABLE_PASSWORD +from django.contrib.auth.tokens import default_token_generator from django.template.loader import render_to_string, get_template, TemplateDoesNotExist from django.core.urlresolvers import is_valid_path +from django.utils.http import int_to_base36 + from mock import Mock, patch from textwrap import dedent @@ -46,36 +49,40 @@ class ResetPasswordTests(TestCase): self.user = UserFactory.create() self.user.is_active = False self.user.save() + self.token = default_token_generator.make_token(self.user) + self.uidb36 = int_to_base36(self.user.id) self.user_bad_passwd = UserFactory.create() self.user_bad_passwd.is_active = False self.user_bad_passwd.password = UNUSABLE_PASSWORD self.user_bad_passwd.save() + def test_user_bad_password_reset(self): + """Tests password reset behavior for user with password marked UNUSABLE_PASSWORD""" - @unittest.skipUnless(project_uses_password_reset, dedent("""Skipping Test because CMS has not provided - necessary templates for password reset. If this message is in LMS tests, that is a bug and needs to be fixed.""")) - @patch('student.views.password_reset_confirm') - @patch('django.core.mail.send_mail') - @patch('student.views.render_to_string', Mock(side_effect=mock_render_to_string, autospec=True)) - def test_reset_password_email(self, send_email, reset_confirm): - """Tests sending of reset password email""" - - #First test the bad password user, mainly for diff-cover sake bad_pwd_req = self.request_factory.post('/password_reset/', {'email': self.user_bad_passwd.email}) bad_pwd_resp = password_reset(bad_pwd_req) self.assertEquals(bad_pwd_resp.status_code, 200) self.assertEquals(bad_pwd_resp.content, json.dumps({'success': False, 'error': 'Invalid e-mail or user'})) - #Now test the exception cases with invalid email. + def test_nonexist_email_password_reset(self): + """Now test the exception cases with of reset_password called with invalid email.""" + bad_email_req = self.request_factory.post('/password_reset/', {'email': self.user.email+"makeItFail"}) bad_email_resp = password_reset(bad_email_req) self.assertEquals(bad_email_resp.status_code, 200) self.assertEquals(bad_email_resp.content, json.dumps({'success': False, 'error': 'Invalid e-mail or user'})) - #Now test the legit case where email should have been sent + @unittest.skipUnless(project_uses_password_reset, + dedent("""Skipping Test because CMS has not provided necessary templates for password reset. + If LMS tests print this message, that needs to be fixed.""")) + @patch('django.core.mail.send_mail') + @patch('student.views.render_to_string', Mock(side_effect=mock_render_to_string, autospec=True)) + def test_reset_password_email(self, send_email): + """Tests contents of reset password email, and that user is not active""" + good_req = self.request_factory.post('/password_reset/', {'email': self.user.email}) good_resp = password_reset(good_req) self.assertEquals(good_resp.status_code, 200) @@ -91,33 +98,35 @@ class ResetPasswordTests(TestCase): self.assertIn(self.user.email, to_addrs) #test that the user is not active - #it's a bit unsettling that we have to reload the user from the db for this test to work - #but I guess the user is cached here in the instance of ResetPasswordTests - #so the update in the view does not know to update this class. self.user = User.objects.get(pk=self.user.pk) self.assertFalse(self.user.is_active) + reset_match = re.search(r'password_reset_confirm/(?P[0-9A-Za-z]+)-(?P.+)/', msg).groupdict() + + @patch('student.views.password_reset_confirm') + def test_reset_password_bad_token(self, reset_confirm): + """Tests bad token and uidb36 in password reset""" - #now try to activate the user in the password reset phase bad_reset_req = self.request_factory.get('/password_reset_confirm/NO-OP/') - bad_reset_resp = password_reset_confirm_wrapper(bad_reset_req, 'NO', 'OP') + password_reset_confirm_wrapper(bad_reset_req, 'NO', 'OP') (confirm_args, confirm_kwargs) = reset_confirm.call_args self.assertEquals(confirm_kwargs['uidb36'], 'NO') self.assertEquals(confirm_kwargs['token'], 'OP') self.user = User.objects.get(pk=self.user.pk) self.assertFalse(self.user.is_active) - reset_match = re.search(r'password_reset_confirm/(?P[0-9A-Za-z]+)-(?P.+)/', msg).groupdict() - good_reset_req = self.request_factory.get('/password_reset_confirm/{0}-{1}/'.format(reset_match['uidb36'], - reset_match['token'])) - good_reset_resp = password_reset_confirm_wrapper(good_reset_req, reset_match['uidb36'], reset_match['token']) + @patch('student.views.password_reset_confirm') + def test_reset_password_good_token(self, reset_confirm): + """Tests good token and uidb36 in password reset""" + + good_reset_req = self.request_factory.get('/password_reset_confirm/{0}-{1}/'.format(self.uidb36, self.token)) + password_reset_confirm_wrapper(good_reset_req, self.uidb36, self.token) (confirm_args, confirm_kwargs) = reset_confirm.call_args - self.assertEquals(confirm_kwargs['uidb36'], reset_match['uidb36']) - self.assertEquals(confirm_kwargs['token'], reset_match['token']) + self.assertEquals(confirm_kwargs['uidb36'], self.uidb36) + self.assertEquals(confirm_kwargs['token'], self.token) self.user = User.objects.get(pk=self.user.pk) self.assertTrue(self.user.is_active) - class CourseEndingTest(TestCase): """Test things related to course endings: certificates, surveys, etc""" From c41c102b7a467e108f44de4dede411eef057dd54 Mon Sep 17 00:00:00 2001 From: Jason Bau Date: Tue, 25 Jun 2013 21:37:20 -0600 Subject: [PATCH 354/375] Update CHANGELOG.rst --- CHANGELOG.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 21b8c9f90b..4fea30a5c5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,10 @@ These are notable changes in edx-platform. This is a rolling list of changes, in roughly chronological order, most recent first. Add your entries at or near the top. Include a label indicating the component affected. +LMS: Users are no longer auto-activated if they click "reset password" +This is now done when they click on the link in the reset password +email they receive (along with usual path through activation email). + LMS: Problem rescoring. Added options on the Grades tab of the Instructor Dashboard to allow a particular student's submission for a particular problem to be rescored. Provides an option to see a From 72aa56a45e0c0dc9632ec529501002b9dde23820 Mon Sep 17 00:00:00 2001 From: Alexander Kryklia Date: Wed, 26 Jun 2013 13:00:15 +0300 Subject: [PATCH 355/375] updated CHANGELOG.rst --- CHANGELOG.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index cb8eec738f..6e3e02cd3c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,10 @@ These are notable changes in edx-platform. This is a rolling list of changes, in roughly chronological order, most recent first. Add your entries at or near the top. Include a label indicating the component affected. +Common: Add tests for documentation generation to test suite + +Blades: User answer now preserved (and changeable) after clicking "show answer" in choice problems + Blades: Small UX fix on capa multiple-choice problems. Make labels only as wide as the text to reduce accidental choice selections. From 318372f2c0a3c838fb4b785637375e1e632bf143 Mon Sep 17 00:00:00 2001 From: cahrens Date: Tue, 25 Jun 2013 15:11:55 -0400 Subject: [PATCH 356/375] Introduce course creator group. --- cms/djangoapps/auth/authz.py | 87 ++++++++++++++++--- cms/djangoapps/auth/tests/test_authz.py | 78 +++++++++++++++++ .../contentstore/tests/test_contentstore.py | 73 ++++++++++++---- cms/djangoapps/contentstore/views/course.py | 4 +- 4 files changed, 209 insertions(+), 33 deletions(-) create mode 100644 cms/djangoapps/auth/tests/test_authz.py diff --git a/cms/djangoapps/auth/authz.py b/cms/djangoapps/auth/authz.py index 58b63abd23..f27d2fe559 100644 --- a/cms/djangoapps/auth/authz.py +++ b/cms/djangoapps/auth/authz.py @@ -1,5 +1,6 @@ from django.contrib.auth.models import User, Group from django.core.exceptions import PermissionDenied +from django.conf import settings from xmodule.modulestore import Location @@ -12,6 +13,9 @@ but this implementation should be data compatible with the LMS implementation INSTRUCTOR_ROLE_NAME = 'instructor' STAFF_ROLE_NAME = 'staff' +# This is the group of people who have permission to create new courses on edge or edx. +COURSE_CREATOR_GROUP_NAME = "course_creator_group" + # we're just making a Django group for each location/role combo # to do this we're just creating a Group name which is a formatted string # of those two variables @@ -36,10 +40,10 @@ def get_users_in_course_group_by_role(location, role): return group.user_set.all() -''' -Create all permission groups for a new course and subscribe the caller into those roles -''' def create_all_course_groups(creator, location): + """ + Create all permission groups for a new course and subscribe the caller into those roles + """ create_new_course_group(creator, location, INSTRUCTOR_ROLE_NAME) create_new_course_group(creator, location, STAFF_ROLE_NAME) @@ -56,10 +60,10 @@ def create_new_course_group(creator, location, role): return def _delete_course_group(location): - ''' + """ This is to be called only by either a command line code path or through a app which has already asserted permissions - ''' + """ # remove all memberships instructors = Group.objects.get(name=get_course_groupname_for_role(location, INSTRUCTOR_ROLE_NAME)) for user in instructors.user_set.all(): @@ -72,10 +76,10 @@ def _delete_course_group(location): user.save() def _copy_course_group(source, dest): - ''' + """ This is to be called only by either a command line code path or through an app which has already asserted permissions to do this action - ''' + """ instructors = Group.objects.get(name=get_course_groupname_for_role(source, INSTRUCTOR_ROLE_NAME)) new_instructors_group = Group.objects.get(name=get_course_groupname_for_role(dest, INSTRUCTOR_ROLE_NAME)) for user in instructors.user_set.all(): @@ -94,10 +98,29 @@ def add_user_to_course_group(caller, user, location, role): if not is_user_in_course_group_role(caller, location, INSTRUCTOR_ROLE_NAME): raise PermissionDenied - if user.is_active and user.is_authenticated: - groupname = get_course_groupname_for_role(location, role) + group = Group.objects.get(name=get_course_groupname_for_role(location, role)) + return _add_user_to_group(user, group) - group = Group.objects.get(name=groupname) + +def add_user_to_creator_group(user): + """ + Adds the user to the group of course creators. + + Note that on the edX site, we currently limit course creators to edX staff, and this + method is a no-op in that environment. + """ + (group, created) = Group.objects.get_or_create(name=COURSE_CREATOR_GROUP_NAME) + if created: + group.save() + return _add_user_to_group(user, group) + + +def _add_user_to_group(user, group): + """ + This is to be called only by either a command line code path or through an app which has already + asserted permissions to do this action + """ + if user.is_active and user.is_authenticated: user.groups.add(group) user.save() return True @@ -123,11 +146,24 @@ def remove_user_from_course_group(caller, user, location, role): # see if the user is actually in that role, if not then we don't have to do anything if is_user_in_course_group_role(user, location, role): - groupname = get_course_groupname_for_role(location, role) + _remove_user_from_group(user, get_course_groupname_for_role(location, role)) - group = Group.objects.get(name=groupname) - user.groups.remove(group) - user.save() + +def remove_user_from_creator_group(user): + """ + Removes user from the course creator group. + """ + _remove_user_from_group(user, COURSE_CREATOR_GROUP_NAME) + + +def _remove_user_from_group(user, group_name): + """ + This is to be called only by either a command line code path or through an app which has already + asserted permissions to do this action + """ + group = Group.objects.get(name=group_name) + user.groups.remove(group) + user.save() def is_user_in_course_group_role(user, location, role): @@ -136,3 +172,26 @@ def is_user_in_course_group_role(user, location, role): return user.is_staff or user.groups.filter(name=get_course_groupname_for_role(location, role)).count() > 0 return False + + +def is_user_in_creator_group(user): + """ + Returns true if the user has permissions to create a course. + + Will always return True if user.is_staff is True. + + Note that on the edX site, we currently limit course creators to edX staff. On + other sites, this method checks that the user is in the course creator group. + """ + if user.is_staff: + return True + + # On edx, we only allow edX staff to create courses. This may be relaxed in the future. + if settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False): + return False + + # Feature flag for using the creator group setting. Will be removed once the feature is complete. + if settings.MITX_FEATURES.get('ENABLE_CREATOR_GROUP', False): + return user.groups.filter(name=COURSE_CREATOR_GROUP_NAME).count() > 0 + + return True diff --git a/cms/djangoapps/auth/tests/test_authz.py b/cms/djangoapps/auth/tests/test_authz.py new file mode 100644 index 0000000000..4e44471ebf --- /dev/null +++ b/cms/djangoapps/auth/tests/test_authz.py @@ -0,0 +1,78 @@ +""" +Tests authz.py +""" +import mock + +from django.test import TestCase +from django.contrib.auth.models import User + +from auth.authz import add_user_to_creator_group, remove_user_from_creator_group, is_user_in_creator_group + +class CreatorGroupTest(TestCase): + """ + Tests for the course creator group. + """ + def setUp(self): + """ Test case setup """ + self.user = User.objects.create_user('testuser', 'test+courses@edx.org', 'foo') + + def test_creator_group_not_enabled(self): + """ + Tests that is_user_in_creator_group always returns True if ENABLE_CREATOR_GROUP + and DISABLE_COURSE_CREATION are both not turned on. + """ + self.assertTrue(is_user_in_creator_group(self.user)) + + def test_creator_group_enabled_but_empty(self): + """ Tests creator group feature on, but group empty. """ + with mock.patch.dict('django.conf.settings.MITX_FEATURES', {"ENABLE_CREATOR_GROUP" : True}): + self.assertFalse(is_user_in_creator_group(self.user)) + + # Make user staff. This will cause is_user_in_creator_group to return True. + self.user.is_staff = True + self.assertTrue(is_user_in_creator_group(self.user)) + + def test_creator_group_enabled_nonempty(self): + """ Tests creator group feature on, user added. """ + with mock.patch.dict('django.conf.settings.MITX_FEATURES', {"ENABLE_CREATOR_GROUP" : True}): + self.assertTrue(add_user_to_creator_group(self.user)) + self.assertTrue(is_user_in_creator_group(self.user)) + + # check that a user who has not been added to the group still returns false + user_not_added = User.objects.create_user('testuser2', 'test+courses2@edx.org', 'foo2') + self.assertFalse(is_user_in_creator_group(user_not_added)) + + # remove first user from the group and verify that is_user_in_creator_group now returns false + remove_user_from_creator_group(self.user) + self.assertFalse(is_user_in_creator_group(self.user)) + + def test_add_user_not_authenticated(self): + """ + Tests that adding to creator group fails if user is not authenticated + """ + self.user.is_authenticated = False + self.assertFalse(add_user_to_creator_group(self.user)) + + def test_add_user_not_active(self): + """ + Tests that adding to creator group fails if user is not active + """ + self.user.is_active = False + self.assertFalse(add_user_to_creator_group(self.user)) + + def test_course_creation_disabled(self): + """ Tests that the COURSE_CREATION_DISABLED flag overrides course creator group settings. """ + with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'DISABLE_COURSE_CREATION': True, "ENABLE_CREATOR_GROUP" : True}): + # Add user to creator group. + self.assertTrue(add_user_to_creator_group(self.user)) + + # DISABLE_COURSE_CREATION overrides (user is not marked as staff). + self.assertFalse(is_user_in_creator_group(self.user)) + + # Mark as staff. Now is_user_in_creator_group returns true. + self.user.is_staff = True + self.assertTrue(is_user_in_creator_group(self.user)) + + # Remove user from creator group. is_user_in_creator_group still returns true because is_staff=True + remove_user_from_creator_group(self.user) + self.assertTrue(is_user_in_creator_group(self.user)) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 66fead562e..093532c71d 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -1,5 +1,6 @@ import json import shutil +import mock from django.test.client import Client from django.test.utils import override_settings from django.conf import settings @@ -16,6 +17,8 @@ from django.dispatch import Signal from contentstore.utils import get_modulestore from contentstore.tests.utils import parse_json +from auth.authz import add_user_to_creator_group + from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory @@ -860,6 +863,12 @@ class ContentStoreTest(ModuleStoreTestCase): def test_create_course(self): """Test new course creation - happy path""" + self.assert_created_course() + + def assert_created_course(self): + """ + Checks that the course was created properly. + """ resp = self.client.post(reverse('create_new_course'), self.course_data) self.assertEqual(resp.status_code, 200) data = parse_json(resp) @@ -867,41 +876,71 @@ class ContentStoreTest(ModuleStoreTestCase): def test_create_course_check_forum_seeding(self): """Test new course creation and verify forum seeding """ - resp = self.client.post(reverse('create_new_course'), self.course_data) - self.assertEqual(resp.status_code, 200) - data = parse_json(resp) - self.assertEqual(data['id'], 'i4x://MITx/999/course/Robot_Super_Course') + self.assert_created_course() self.assertTrue(are_permissions_roles_seeded('MITx/999/Robot_Super_Course')) def test_create_course_duplicate_course(self): """Test new course creation - error path""" self.client.post(reverse('create_new_course'), self.course_data) + self.assert_course_creation_failed('There is already a course defined with this name.') + + def assert_course_creation_failed(self, error_message): + """ + Checks that the course did not get created + """ resp = self.client.post(reverse('create_new_course'), self.course_data) - data = parse_json(resp) self.assertEqual(resp.status_code, 200) - self.assertEqual(data['ErrMsg'], 'There is already a course defined with this name.') + data = parse_json(resp) + self.assertEqual(data['ErrMsg'], error_message) def test_create_course_duplicate_number(self): """Test new course creation - error path""" self.client.post(reverse('create_new_course'), self.course_data) self.course_data['display_name'] = 'Robot Super Course Two' - resp = self.client.post(reverse('create_new_course'), self.course_data) - data = parse_json(resp) - - self.assertEqual(resp.status_code, 200) - self.assertEqual(data['ErrMsg'], - 'There is already a course defined with the same organization and course number.') + self.assert_course_creation_failed('There is already a course defined with the same organization and course number.') def test_create_course_with_bad_organization(self): """Test new course creation - error path for bad organization name""" self.course_data['org'] = 'University of California, Berkeley' - resp = self.client.post(reverse('create_new_course'), self.course_data) - data = parse_json(resp) + self.assert_course_creation_failed("Unable to create course 'Robot Super Course'.\n\nInvalid characters in 'University of California, Berkeley'.") - self.assertEqual(resp.status_code, 200) - self.assertEqual(data['ErrMsg'], - "Unable to create course 'Robot Super Course'.\n\nInvalid characters in 'University of California, Berkeley'.") + def test_create_course_with_course_creation_disabled_staff(self): + """Test new course creation -- course creation disabled, but staff access.""" + with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'DISABLE_COURSE_CREATION': True}): + self.assert_created_course() + + def test_create_course_with_course_creation_disabled_not_staff(self): + """Test new course creation -- error path for course creation disabled, not staff access.""" + with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'DISABLE_COURSE_CREATION': True}): + self.user.is_staff = False + self.user.save() + self.assert_course_permission_denied() + + def test_create_course_no_course_creators_staff(self): + """Test new course creation -- course creation group enabled, staff, group is empty.""" + with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_CREATOR_GROUP': True}): + self.assert_created_course() + + def test_create_course_no_course_creators_not_staff(self): + """Test new course creation -- error path for course creator group enabled, not staff, group is empty.""" + with mock.patch.dict('django.conf.settings.MITX_FEATURES', {"ENABLE_CREATOR_GROUP" : True}): + self.user.is_staff = False + self.user.save() + self.assert_course_permission_denied() + + def test_create_course_with_course_creator(self): + """Test new course creation -- use course creator group""" + with mock.patch.dict('django.conf.settings.MITX_FEATURES', {"ENABLE_CREATOR_GROUP" : True}): + add_user_to_creator_group(self.user) + self.assert_created_course() + + def assert_course_permission_denied(self): + """ + Checks that the course did not get created due to a PermissionError. + """ + resp = self.client.post(reverse('create_new_course'), self.course_data) + self.assertEqual(resp.status_code, 403) def test_course_index_view_with_no_courses(self): """Test viewing the index page with no courses""" diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index dd7573bad5..8862115c45 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -21,7 +21,7 @@ from contentstore.utils import get_lms_link_for_item, add_extra_panel_tab, remov from models.settings.course_details import CourseDetails, CourseSettingsEncoder from models.settings.course_grading import CourseGradingModel from models.settings.course_metadata import CourseMetadata -from auth.authz import create_all_course_groups +from auth.authz import create_all_course_groups, is_user_in_creator_group from util.json_request import expect_json from .access import has_access, get_location_and_verify_access @@ -81,7 +81,7 @@ def course_index(request, org, course, name): @expect_json def create_new_course(request): - if settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff: + if not is_user_in_creator_group(request.user): raise PermissionDenied() # This logic is repeated in xmodule/modulestore/tests/factories.py From 2c60a7dbc142b1fbd973eca5931b8f0b00f6b397 Mon Sep 17 00:00:00 2001 From: cahrens Date: Tue, 25 Jun 2013 15:46:08 -0400 Subject: [PATCH 357/375] pep8 cleanup --- cms/djangoapps/auth/tests/test_authz.py | 9 +++++--- .../contentstore/tests/test_contentstore.py | 21 +++++++++---------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/cms/djangoapps/auth/tests/test_authz.py b/cms/djangoapps/auth/tests/test_authz.py index 4e44471ebf..8ecf3689b3 100644 --- a/cms/djangoapps/auth/tests/test_authz.py +++ b/cms/djangoapps/auth/tests/test_authz.py @@ -8,10 +8,12 @@ from django.contrib.auth.models import User from auth.authz import add_user_to_creator_group, remove_user_from_creator_group, is_user_in_creator_group + class CreatorGroupTest(TestCase): """ Tests for the course creator group. """ + def setUp(self): """ Test case setup """ self.user = User.objects.create_user('testuser', 'test+courses@edx.org', 'foo') @@ -25,7 +27,7 @@ class CreatorGroupTest(TestCase): def test_creator_group_enabled_but_empty(self): """ Tests creator group feature on, but group empty. """ - with mock.patch.dict('django.conf.settings.MITX_FEATURES', {"ENABLE_CREATOR_GROUP" : True}): + with mock.patch.dict('django.conf.settings.MITX_FEATURES', {"ENABLE_CREATOR_GROUP": True}): self.assertFalse(is_user_in_creator_group(self.user)) # Make user staff. This will cause is_user_in_creator_group to return True. @@ -34,7 +36,7 @@ class CreatorGroupTest(TestCase): def test_creator_group_enabled_nonempty(self): """ Tests creator group feature on, user added. """ - with mock.patch.dict('django.conf.settings.MITX_FEATURES', {"ENABLE_CREATOR_GROUP" : True}): + with mock.patch.dict('django.conf.settings.MITX_FEATURES', {"ENABLE_CREATOR_GROUP": True}): self.assertTrue(add_user_to_creator_group(self.user)) self.assertTrue(is_user_in_creator_group(self.user)) @@ -62,7 +64,8 @@ class CreatorGroupTest(TestCase): def test_course_creation_disabled(self): """ Tests that the COURSE_CREATION_DISABLED flag overrides course creator group settings. """ - with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'DISABLE_COURSE_CREATION': True, "ENABLE_CREATOR_GROUP" : True}): + with mock.patch.dict('django.conf.settings.MITX_FEATURES', + {'DISABLE_COURSE_CREATION': True, "ENABLE_CREATOR_GROUP": True}): # Add user to creator group. self.assertTrue(add_user_to_creator_group(self.user)) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 093532c71d..ea4b0bbb5b 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -531,7 +531,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): self.assertGreater(len(all_assets), 0) # make sure we have some thumbnails in our trashcan - all_thumbnails = trash_store.get_all_content_thumbnails_for_course(course_location) + _all_thumbnails = trash_store.get_all_content_thumbnails_for_course(course_location) # # cdodge: temporarily comment out assertion on thumbnails because many environments # will not have the jpeg converter installed and this test will fail @@ -592,20 +592,18 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): location = Location('i4x://MITx/999/chapter/neuvo') self.assertRaises(InvalidVersionError, draft_store.clone_item, 'i4x://edx/templates/chapter/Empty', - location) + location) direct_store.clone_item('i4x://edx/templates/chapter/Empty', location) - self.assertRaises(InvalidVersionError, draft_store.clone_item, location, - location) + self.assertRaises(InvalidVersionError, draft_store.clone_item, location, location) - self.assertRaises(InvalidVersionError, draft_store.update_item, location, - 'chapter data') + self.assertRaises(InvalidVersionError, draft_store.update_item, location, 'chapter data') # taking advantage of update_children and other functions never checking that the ids are valid self.assertRaises(InvalidVersionError, draft_store.update_children, location, - ['i4x://MITx/999/problem/doesntexist']) + ['i4x://MITx/999/problem/doesntexist']) self.assertRaises(InvalidVersionError, draft_store.update_metadata, location, - {'due': datetime.datetime.now(UTC)}) + {'due': datetime.datetime.now(UTC)}) self.assertRaises(InvalidVersionError, draft_store.unpublish, location) @@ -903,7 +901,8 @@ class ContentStoreTest(ModuleStoreTestCase): def test_create_course_with_bad_organization(self): """Test new course creation - error path for bad organization name""" self.course_data['org'] = 'University of California, Berkeley' - self.assert_course_creation_failed("Unable to create course 'Robot Super Course'.\n\nInvalid characters in 'University of California, Berkeley'.") + self.assert_course_creation_failed( + "Unable to create course 'Robot Super Course'.\n\nInvalid characters in 'University of California, Berkeley'.") def test_create_course_with_course_creation_disabled_staff(self): """Test new course creation -- course creation disabled, but staff access.""" @@ -924,14 +923,14 @@ class ContentStoreTest(ModuleStoreTestCase): def test_create_course_no_course_creators_not_staff(self): """Test new course creation -- error path for course creator group enabled, not staff, group is empty.""" - with mock.patch.dict('django.conf.settings.MITX_FEATURES', {"ENABLE_CREATOR_GROUP" : True}): + with mock.patch.dict('django.conf.settings.MITX_FEATURES', {"ENABLE_CREATOR_GROUP": True}): self.user.is_staff = False self.user.save() self.assert_course_permission_denied() def test_create_course_with_course_creator(self): """Test new course creation -- use course creator group""" - with mock.patch.dict('django.conf.settings.MITX_FEATURES', {"ENABLE_CREATOR_GROUP" : True}): + with mock.patch.dict('django.conf.settings.MITX_FEATURES', {"ENABLE_CREATOR_GROUP": True}): add_user_to_creator_group(self.user) self.assert_created_course() From 190c07d9540c44ee671a7c50322412079c3c0eff Mon Sep 17 00:00:00 2001 From: cahrens Date: Tue, 25 Jun 2013 16:15:47 -0400 Subject: [PATCH 358/375] Add smoke coverage for add and remove of course group permissions. --- cms/djangoapps/auth/tests/test_authz.py | 62 ++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/cms/djangoapps/auth/tests/test_authz.py b/cms/djangoapps/auth/tests/test_authz.py index 8ecf3689b3..61ac682908 100644 --- a/cms/djangoapps/auth/tests/test_authz.py +++ b/cms/djangoapps/auth/tests/test_authz.py @@ -5,8 +5,11 @@ import mock from django.test import TestCase from django.contrib.auth.models import User +from django.core.exceptions import PermissionDenied -from auth.authz import add_user_to_creator_group, remove_user_from_creator_group, is_user_in_creator_group +from auth.authz import add_user_to_creator_group, remove_user_from_creator_group, is_user_in_creator_group,\ + create_all_course_groups, add_user_to_course_group, STAFF_ROLE_NAME, INSTRUCTOR_ROLE_NAME,\ + is_user_in_course_group_role, remove_user_from_course_group class CreatorGroupTest(TestCase): @@ -79,3 +82,60 @@ class CreatorGroupTest(TestCase): # Remove user from creator group. is_user_in_creator_group still returns true because is_staff=True remove_user_from_creator_group(self.user) self.assertTrue(is_user_in_creator_group(self.user)) + + +class CourseGroupTest(TestCase): + """ + Tests for instructor and staff groups for a particular course. + """ + + def setUp(self): + """ Test case setup """ + self.creator = User.objects.create_user('testcreator', 'testcreator+courses@edx.org', 'foo') + self.staff = User.objects.create_user('teststaff', 'teststaff+courses@edx.org', 'foo') + self.location = 'i4x', 'mitX', '101', 'course', 'test' + + def test_add_user_to_course_group(self): + """ + Tests adding user to course group (happy path). + """ + # Create groups for a new course (and assign instructor role to the creator). + self.assertFalse(is_user_in_course_group_role(self.creator, self.location, INSTRUCTOR_ROLE_NAME)) + create_all_course_groups(self.creator, self.location) + self.assertTrue(is_user_in_course_group_role(self.creator, self.location, INSTRUCTOR_ROLE_NAME)) + + # Add another user to the staff role. + self.assertFalse(is_user_in_course_group_role(self.staff, self.location, STAFF_ROLE_NAME)) + self.assertTrue(add_user_to_course_group(self.creator, self.staff, self.location, STAFF_ROLE_NAME)) + self.assertTrue(is_user_in_course_group_role(self.staff, self.location, STAFF_ROLE_NAME)) + + def test_add_user_to_course_group_permission_denied(self): + """ + Verifies PermissionDenied if caller of add_user_to_course_group is not instructor role. + """ + create_all_course_groups(self.creator, self.location) + with self.assertRaises(PermissionDenied): + add_user_to_course_group(self.staff, self.staff, self.location, STAFF_ROLE_NAME) + + def remove_user_from_course_group(self): + """ + Tests removing user from course group (happy path). + """ + create_all_course_groups(self.creator, self.location) + + self.assertTrue(add_user_to_course_group(self.creator, self.staff, self.location, STAFF_ROLE_NAME)) + self.assertTrue(is_user_in_course_group_role(self.staff, self.location, STAFF_ROLE_NAME)) + + remove_user_from_course_group(self.creator, self.location, self.staff, STAFF_ROLE_NAME) + self.assertFalse(is_user_in_course_group_role(self.staff, self.location, STAFF_ROLE_NAME)) + + remove_user_from_course_group(self.creator, self.location, self.creator, INSTRUCTOR_ROLE_NAME) + self.assertFalse(is_user_in_course_group_role(self.creator, self.location, INSTRUCTOR_ROLE_NAME)) + + def test_remove_user_from_course_group_permission_denied(self): + """ + Verifies PermissionDenied if caller of remove_user_from_course_group is not instructor role. + """ + create_all_course_groups(self.creator, self.location) + with self.assertRaises(PermissionDenied): + remove_user_from_course_group(self.staff, self.staff, self.location, STAFF_ROLE_NAME) From 4a697a8da171dad2a1056791607e91ad3e1c6dec Mon Sep 17 00:00:00 2001 From: cahrens Date: Tue, 25 Jun 2013 17:07:51 -0400 Subject: [PATCH 359/375] Verify that caller of add or remove from creator group is staff. --- cms/djangoapps/auth/authz.py | 14 ++++++- cms/djangoapps/auth/tests/test_authz.py | 53 ++++++++++++++++++++----- 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/cms/djangoapps/auth/authz.py b/cms/djangoapps/auth/authz.py index f27d2fe559..a544906875 100644 --- a/cms/djangoapps/auth/authz.py +++ b/cms/djangoapps/auth/authz.py @@ -102,13 +102,18 @@ def add_user_to_course_group(caller, user, location, role): return _add_user_to_group(user, group) -def add_user_to_creator_group(user): +def add_user_to_creator_group(caller, user): """ Adds the user to the group of course creators. + The caller must have staff access to perform this operation. + Note that on the edX site, we currently limit course creators to edX staff, and this method is a no-op in that environment. """ + if not caller.is_active or not caller.is_authenticated or not caller.is_staff: + raise PermissionDenied + (group, created) = Group.objects.get_or_create(name=COURSE_CREATOR_GROUP_NAME) if created: group.save() @@ -149,10 +154,15 @@ def remove_user_from_course_group(caller, user, location, role): _remove_user_from_group(user, get_course_groupname_for_role(location, role)) -def remove_user_from_creator_group(user): +def remove_user_from_creator_group(caller, user): """ Removes user from the course creator group. + + The caller must have staff access to perform this operation. """ + if not caller.is_active or not caller.is_authenticated or not caller.is_staff: + raise PermissionDenied + _remove_user_from_group(user, COURSE_CREATOR_GROUP_NAME) diff --git a/cms/djangoapps/auth/tests/test_authz.py b/cms/djangoapps/auth/tests/test_authz.py index 61ac682908..173155df4c 100644 --- a/cms/djangoapps/auth/tests/test_authz.py +++ b/cms/djangoapps/auth/tests/test_authz.py @@ -20,6 +20,8 @@ class CreatorGroupTest(TestCase): def setUp(self): """ Test case setup """ self.user = User.objects.create_user('testuser', 'test+courses@edx.org', 'foo') + self.admin = User.objects.create_user('Mark', 'admin+courses@edx.org', 'foo') + self.admin.is_staff = True def test_creator_group_not_enabled(self): """ @@ -40,7 +42,7 @@ class CreatorGroupTest(TestCase): def test_creator_group_enabled_nonempty(self): """ Tests creator group feature on, user added. """ with mock.patch.dict('django.conf.settings.MITX_FEATURES', {"ENABLE_CREATOR_GROUP": True}): - self.assertTrue(add_user_to_creator_group(self.user)) + self.assertTrue(add_user_to_creator_group(self.admin, self.user)) self.assertTrue(is_user_in_creator_group(self.user)) # check that a user who has not been added to the group still returns false @@ -48,7 +50,7 @@ class CreatorGroupTest(TestCase): self.assertFalse(is_user_in_creator_group(user_not_added)) # remove first user from the group and verify that is_user_in_creator_group now returns false - remove_user_from_creator_group(self.user) + remove_user_from_creator_group(self.admin, self.user) self.assertFalse(is_user_in_creator_group(self.user)) def test_add_user_not_authenticated(self): @@ -56,21 +58,21 @@ class CreatorGroupTest(TestCase): Tests that adding to creator group fails if user is not authenticated """ self.user.is_authenticated = False - self.assertFalse(add_user_to_creator_group(self.user)) + self.assertFalse(add_user_to_creator_group(self.admin, self.user)) def test_add_user_not_active(self): """ Tests that adding to creator group fails if user is not active """ self.user.is_active = False - self.assertFalse(add_user_to_creator_group(self.user)) + self.assertFalse(add_user_to_creator_group(self.admin, self.user)) def test_course_creation_disabled(self): """ Tests that the COURSE_CREATION_DISABLED flag overrides course creator group settings. """ with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'DISABLE_COURSE_CREATION': True, "ENABLE_CREATOR_GROUP": True}): # Add user to creator group. - self.assertTrue(add_user_to_creator_group(self.user)) + self.assertTrue(add_user_to_creator_group(self.admin, self.user)) # DISABLE_COURSE_CREATION overrides (user is not marked as staff). self.assertFalse(is_user_in_creator_group(self.user)) @@ -80,9 +82,42 @@ class CreatorGroupTest(TestCase): self.assertTrue(is_user_in_creator_group(self.user)) # Remove user from creator group. is_user_in_creator_group still returns true because is_staff=True - remove_user_from_creator_group(self.user) + remove_user_from_creator_group(self.admin, self.user) self.assertTrue(is_user_in_creator_group(self.user)) + def test_add_user_to_group_requires_staff_access(self): + with self.assertRaises(PermissionDenied): + self.admin.is_staff = False + add_user_to_creator_group(self.admin, self.user) + + with self.assertRaises(PermissionDenied): + add_user_to_creator_group(self.user, self.user) + + def test_add_user_to_group_requires_active(self): + with self.assertRaises(PermissionDenied): + self.admin.is_active = False + add_user_to_creator_group(self.admin, self.user) + + def test_add_user_to_group_requires_authenticated(self): + with self.assertRaises(PermissionDenied): + self.admin.is_authenticated = False + add_user_to_creator_group(self.admin, self.user) + + def test_remove_user_from_group_requires_staff_access(self): + with self.assertRaises(PermissionDenied): + self.admin.is_staff = False + remove_user_from_creator_group(self.admin, self.user) + + def test_remove_user_from_group_requires_active(self): + with self.assertRaises(PermissionDenied): + self.admin.is_active = False + remove_user_from_creator_group(self.admin, self.user) + + def test_remove_user_from_group_requires_authenticated(self): + with self.assertRaises(PermissionDenied): + self.admin.is_authenticated = False + remove_user_from_creator_group(self.admin, self.user) + class CourseGroupTest(TestCase): """ @@ -117,7 +152,7 @@ class CourseGroupTest(TestCase): with self.assertRaises(PermissionDenied): add_user_to_course_group(self.staff, self.staff, self.location, STAFF_ROLE_NAME) - def remove_user_from_course_group(self): + def test_remove_user_from_course_group(self): """ Tests removing user from course group (happy path). """ @@ -126,10 +161,10 @@ class CourseGroupTest(TestCase): self.assertTrue(add_user_to_course_group(self.creator, self.staff, self.location, STAFF_ROLE_NAME)) self.assertTrue(is_user_in_course_group_role(self.staff, self.location, STAFF_ROLE_NAME)) - remove_user_from_course_group(self.creator, self.location, self.staff, STAFF_ROLE_NAME) + remove_user_from_course_group(self.creator, self.staff, self.location, STAFF_ROLE_NAME) self.assertFalse(is_user_in_course_group_role(self.staff, self.location, STAFF_ROLE_NAME)) - remove_user_from_course_group(self.creator, self.location, self.creator, INSTRUCTOR_ROLE_NAME) + remove_user_from_course_group(self.creator, self.creator, self.location, INSTRUCTOR_ROLE_NAME) self.assertFalse(is_user_in_course_group_role(self.creator, self.location, INSTRUCTOR_ROLE_NAME)) def test_remove_user_from_course_group_permission_denied(self): From e487521289f4d89dc8164b377b40969cd8912236 Mon Sep 17 00:00:00 2001 From: cahrens Date: Wed, 26 Jun 2013 09:06:12 -0400 Subject: [PATCH 360/375] Update for change in add_user_to_creator_group API. --- cms/djangoapps/contentstore/tests/test_contentstore.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index ea4b0bbb5b..b946aac6bb 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -931,7 +931,7 @@ class ContentStoreTest(ModuleStoreTestCase): def test_create_course_with_course_creator(self): """Test new course creation -- use course creator group""" with mock.patch.dict('django.conf.settings.MITX_FEATURES', {"ENABLE_CREATOR_GROUP": True}): - add_user_to_creator_group(self.user) + add_user_to_creator_group(self.user, self.user) self.assert_created_course() def assert_course_permission_denied(self): From bb8c62d84ce8949a19c08bd4ab8472628fc692ab Mon Sep 17 00:00:00 2001 From: cahrens Date: Wed, 26 Jun 2013 12:33:55 -0400 Subject: [PATCH 361/375] Make the problem handle empty fields and non-integers correctly. --- .../templates/problem/customgrader.yaml | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/common/lib/xmodule/xmodule/templates/problem/customgrader.yaml b/common/lib/xmodule/xmodule/templates/problem/customgrader.yaml index b5b0d71f4d..48feef481b 100644 --- a/common/lib/xmodule/xmodule/templates/problem/customgrader.yaml +++ b/common/lib/xmodule/xmodule/templates/problem/customgrader.yaml @@ -13,15 +13,16 @@ data: | @@ -40,7 +41,7 @@ data: |

Explanation

-

Any set of values on the line \(y = 10 - x\) and \(y = 20 - x\) satisfy these constraints.

+

Any set of integers on the line \(y = 10 - x\) and \(y = 20 - x\) satisfy these constraints.

From 50f837d9d80498b1704c7cdf910c452f1008660a Mon Sep 17 00:00:00 2001 From: Giulio Gratta Date: Wed, 26 Jun 2013 10:22:59 -0700 Subject: [PATCH 362/375] move marketing iframe breakout script to inside existing conditional --- lms/templates/main.html | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lms/templates/main.html b/lms/templates/main.html index b00446d190..5c0c383b84 100644 --- a/lms/templates/main.html +++ b/lms/templates/main.html @@ -21,17 +21,17 @@ Home | class.stanford.edu % else: edX + + % endif - From 391ed8c96475b45ff7d8f86313241692ed9c543d Mon Sep 17 00:00:00 2001 From: Don Mitchell Date: Tue, 25 Jun 2013 11:56:55 -0400 Subject: [PATCH 363/375] Add docstring to CourseDescriptor.__init__ --- common/lib/xmodule/xmodule/course_module.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 62ebe12a03..02b44bd018 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -212,6 +212,9 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): template_dir_name = 'course' def __init__(self, *args, **kwargs): + """ + Expects the same arguments as XModuleDescriptor.__init__ + """ super(CourseDescriptor, self).__init__(*args, **kwargs) if self.wiki_slug is None: From ff6ba014ce8bc51ef110cdf135608b74e4a9d70b Mon Sep 17 00:00:00 2001 From: Don Mitchell Date: Mon, 24 Jun 2013 14:48:45 -0400 Subject: [PATCH 364/375] Remove noop if statement --- cms/djangoapps/contentstore/module_info_model.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/cms/djangoapps/contentstore/module_info_model.py b/cms/djangoapps/contentstore/module_info_model.py index e361c97875..726d4bb0ce 100644 --- a/cms/djangoapps/contentstore/module_info_model.py +++ b/cms/djangoapps/contentstore/module_info_model.py @@ -5,10 +5,7 @@ from xmodule.modulestore import Location def get_module_info(store, location, parent_location=None, rewrite_static_links=False): try: - if location.revision is None: - module = store.get_item(location) - else: - module = store.get_item(location) + module = store.get_item(location) except ItemNotFoundError: # create a new one template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty']) From 65b3bcdba689f7f0212d0f38d06948f409344471 Mon Sep 17 00:00:00 2001 From: Don Mitchell Date: Tue, 25 Jun 2013 11:21:17 -0400 Subject: [PATCH 365/375] Clean up variable naming --- cms/djangoapps/contentstore/views/assets.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cms/djangoapps/contentstore/views/assets.py b/cms/djangoapps/contentstore/views/assets.py index 400013b59b..41077abd8f 100644 --- a/cms/djangoapps/contentstore/views/assets.py +++ b/cms/djangoapps/contentstore/views/assets.py @@ -240,13 +240,13 @@ def import_course(request, org, course, name): # find the 'course.xml' file for dirpath, _dirnames, filenames in os.walk(course_dir): - for files in filenames: - if files == 'course.xml': + for filename in filenames: + if filename == 'course.xml': break - if files == 'course.xml': + if filename == 'course.xml': break - if files != 'course.xml': + if filename != 'course.xml': return HttpResponse(json.dumps({'ErrMsg': 'Could not find the course.xml file in the package.'})) logging.debug('found course.xml at {0}'.format(dirpath)) From 1344b1e521afd83494f99930260c00f679e883d1 Mon Sep 17 00:00:00 2001 From: Don Mitchell Date: Tue, 25 Jun 2013 11:23:07 -0400 Subject: [PATCH 366/375] Make SessionKeyValueStore variable names clearer --- .../contentstore/views/session_kv_store.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cms/djangoapps/contentstore/views/session_kv_store.py b/cms/djangoapps/contentstore/views/session_kv_store.py index 309518c27d..54ab25ff54 100644 --- a/cms/djangoapps/contentstore/views/session_kv_store.py +++ b/cms/djangoapps/contentstore/views/session_kv_store.py @@ -2,27 +2,27 @@ from xblock.runtime import KeyValueStore, InvalidScopeError class SessionKeyValueStore(KeyValueStore): - def __init__(self, request, model_data): - self._model_data = model_data + def __init__(self, request, descriptor_model_data): + self._descriptor_model_data = descriptor_model_data self._session = request.session def get(self, key): try: - return self._model_data[key.field_name] + return self._descriptor_model_data[key.field_name] except (KeyError, InvalidScopeError): return self._session[tuple(key)] def set(self, key, value): try: - self._model_data[key.field_name] = value + self._descriptor_model_data[key.field_name] = value except (KeyError, InvalidScopeError): self._session[tuple(key)] = value def delete(self, key): try: - del self._model_data[key.field_name] + del self._descriptor_model_data[key.field_name] except (KeyError, InvalidScopeError): del self._session[tuple(key)] def has(self, key): - return key in self._model_data or key in self._session + return key in self._descriptor_model_data or key in self._session From b985a7f128851171dde1a3f0927701efa2bb404d Mon Sep 17 00:00:00 2001 From: Don Mitchell Date: Tue, 25 Jun 2013 11:26:02 -0400 Subject: [PATCH 367/375] Remove unused template --- cms/templates/new_item.html | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 cms/templates/new_item.html diff --git a/cms/templates/new_item.html b/cms/templates/new_item.html deleted file mode 100644 index 45cb157845..0000000000 --- a/cms/templates/new_item.html +++ /dev/null @@ -1,19 +0,0 @@ -
-
${parent_name}
-
${parent_location}
- -
- % for module_type, module_templates in templates: -
-
${module_type}
-
- % for template in module_templates: - ${template.display_name_with_default} - % endfor -
-
- % endfor -
- Cancel -
- From f7d6d149461bdad797082cb58f667d8260da8f29 Mon Sep 17 00:00:00 2001 From: Sarina Canelake Date: Tue, 25 Jun 2013 09:42:28 -0400 Subject: [PATCH 368/375] Catch InvalidLocationError --- lms/djangoapps/courseware/courses.py | 6 +++--- lms/djangoapps/courseware/tests/test_courses.py | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 lms/djangoapps/courseware/tests/test_courses.py diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index 71c9630964..ef1b786645 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -12,12 +12,11 @@ from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore from xmodule.contentstore.content import StaticContent from xmodule.modulestore.xml import XMLModuleStore -from xmodule.modulestore.exceptions import ItemNotFoundError +from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError from courseware.model_data import ModelDataCache from static_replace import replace_static_urls from courseware.access import has_access import branding -from xmodule.modulestore.exceptions import ItemNotFoundError log = logging.getLogger(__name__) @@ -49,7 +48,8 @@ def get_course_by_id(course_id, depth=0): return modulestore().get_instance(course_id, course_loc, depth=depth) except (KeyError, ItemNotFoundError): raise Http404("Course not found.") - + except InvalidLocationError: + raise Http404("Invalid location") def get_course_with_access(user, course_id, action, depth=0): """ diff --git a/lms/djangoapps/courseware/tests/test_courses.py b/lms/djangoapps/courseware/tests/test_courses.py new file mode 100644 index 0000000000..60594602a4 --- /dev/null +++ b/lms/djangoapps/courseware/tests/test_courses.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +from django.test import TestCase +from django.http import Http404 +from courseware.courses import get_course_by_id + +class CoursesTest(TestCase): + def test_get_course_by_id_invalid_chars(self): + """ + Test that `get_course_by_id` throws a 404, rather than + an exception, when faced with unexpected characters + (such as unicode characters, and symbols such as = and ' ') + """ + with self.assertRaises(Http404): + get_course_by_id('MITx/foobar/statistics=introduction') + get_course_by_id('MITx/foobar/business and management') + get_course_by_id('MITx/foobar/NiñøJoséMaríáßç') From dec20d76bc33cb185640c424d39f876f724e5e2f Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Wed, 26 Jun 2013 16:25:44 -0400 Subject: [PATCH 369/375] Use separate venv for parallel builds --- jenkins/test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jenkins/test.sh b/jenkins/test.sh index 05f978e32f..70a9e168bc 100755 --- a/jenkins/test.sh +++ b/jenkins/test.sh @@ -60,7 +60,7 @@ fi export PIP_DOWNLOAD_CACHE=/mnt/pip-cache -source /mnt/virtualenvs/"$JOB_NAME"/bin/activate +source $VIRTUALENV_DIR/bin/activate bundle install From ee5389800ad8300a21c90c78976aa61eebadbba8 Mon Sep 17 00:00:00 2001 From: Peter Fogg Date: Wed, 26 Jun 2013 16:24:04 -0400 Subject: [PATCH 370/375] Fix Lyla showing up everywhere. Previously XML data wasn't parsed in VideoDescriptor.__init__, leading to the defaults being used for video settings. --- .../xmodule/tests/test_video_module.py | 28 ++++++ common/lib/xmodule/xmodule/video_module.py | 90 +++++++++++-------- 2 files changed, 80 insertions(+), 38 deletions(-) diff --git a/common/lib/xmodule/xmodule/tests/test_video_module.py b/common/lib/xmodule/xmodule/tests/test_video_module.py index f516e1a179..e11686176a 100644 --- a/common/lib/xmodule/xmodule/tests/test_video_module.py +++ b/common/lib/xmodule/xmodule/tests/test_video_module.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import unittest +from xmodule.modulestore import Location from xmodule.video_module import VideoDescriptor from .test_import import DummySystem @@ -10,6 +11,33 @@ class VideoDescriptorImportTestCase(unittest.TestCase): Make sure that VideoDescriptor can import an old XML-based video correctly. """ + def test_constructor(self): + sample_xml = ''' + + ''' + location = Location(["i4x", "edX", "video", "default", + "SampleProblem1"]) + model_data = {'data': sample_xml, + 'location': location} + system = DummySystem(load_error_modules=True) + descriptor = VideoDescriptor(system, model_data) + self.assertEquals(descriptor.youtube_id_0_75, 'izygArpw-Qo') + self.assertEquals(descriptor.youtube_id_1_0, 'p2Q6BrNhdh8') + self.assertEquals(descriptor.youtube_id_1_25, '1EeWXzPdhSA') + self.assertEquals(descriptor.youtube_id_1_5, 'rABDYkeK0x8') + self.assertEquals(descriptor.show_captions, False) + self.assertEquals(descriptor.start_time, 1.0) + self.assertEquals(descriptor.end_time, 60) + self.assertEquals(descriptor.track, 'http://www.example.com/track') + self.assertEquals(descriptor.source, 'http://www.example.com/source.mp4') + def test_from_xml(self): module_system = DummySystem(load_error_modules=True) xml_data = ''' diff --git a/common/lib/xmodule/xmodule/video_module.py b/common/lib/xmodule/xmodule/video_module.py index 04daaea3f2..3c6203107d 100644 --- a/common/lib/xmodule/xmodule/video_module.py +++ b/common/lib/xmodule/xmodule/video_module.py @@ -88,6 +88,13 @@ class VideoDescriptor(VideoFields, module_class = VideoModule template_dir_name = "video" + def __init__(self, *args, **kwargs): + super(VideoDescriptor, self).__init__(*args, **kwargs) + # If we don't have a `youtube_id_1_0`, this is an XML course + # and we parse out the fields. + if self.data and 'youtube_id_1_0' not in self._model_data: + _parse_video_xml(self, self.data) + @property def non_editable_metadata_fields(self): non_editable_fields = super(MetadataOnlyEditingDescriptor, self).non_editable_metadata_fields @@ -108,47 +115,54 @@ class VideoDescriptor(VideoFields, url identifiers """ video = super(VideoDescriptor, cls).from_xml(xml_data, system, org, course) - xml = etree.fromstring(xml_data) - - display_name = xml.get('display_name') - if display_name: - video.display_name = display_name - - youtube = xml.get('youtube') - if youtube: - speeds = _parse_youtube(youtube) - if speeds['0.75']: - video.youtube_id_0_75 = speeds['0.75'] - if speeds['1.00']: - video.youtube_id_1_0 = speeds['1.00'] - if speeds['1.25']: - video.youtube_id_1_25 = speeds['1.25'] - if speeds['1.50']: - video.youtube_id_1_5 = speeds['1.50'] - - show_captions = xml.get('show_captions') - if show_captions: - video.show_captions = json.loads(show_captions) - - source = _get_first_external(xml, 'source') - if source: - video.source = source - - track = _get_first_external(xml, 'track') - if track: - video.track = track - - start_time = _parse_time(xml.get('from')) - if start_time: - video.start_time = start_time - - end_time = _parse_time(xml.get('to')) - if end_time: - video.end_time = end_time - + _parse_video_xml(video, xml_data) return video +def _parse_video_xml(video, xml_data): + """ + Parse video fields out of xml_data. The fields are set if they are + present in the XML. + """ + xml = etree.fromstring(xml_data) + + display_name = xml.get('display_name') + if display_name: + video.display_name = display_name + + youtube = xml.get('youtube') + if youtube: + speeds = _parse_youtube(youtube) + if speeds['0.75']: + video.youtube_id_0_75 = speeds['0.75'] + if speeds['1.00']: + video.youtube_id_1_0 = speeds['1.00'] + if speeds['1.25']: + video.youtube_id_1_25 = speeds['1.25'] + if speeds['1.50']: + video.youtube_id_1_5 = speeds['1.50'] + + show_captions = xml.get('show_captions') + if show_captions: + video.show_captions = json.loads(show_captions) + + source = _get_first_external(xml, 'source') + if source: + video.source = source + + track = _get_first_external(xml, 'track') + if track: + video.track = track + + start_time = _parse_time(xml.get('from')) + if start_time: + video.start_time = start_time + + end_time = _parse_time(xml.get('to')) + if end_time: + video.end_time = end_time + + def _get_first_external(xmltree, tag): """ Returns the src attribute of the nested `tag` in `xmltree`, if it From 2a49d087de272c6724a2f0d1b9aa775d0653784a Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Thu, 20 Jun 2013 11:21:43 -0400 Subject: [PATCH 371/375] studio - revises transparent Sass color vars to use rgba() method --- cms/static/sass/_variables.scss | 34 ++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/cms/static/sass/_variables.scss b/cms/static/sass/_variables.scss index 14c215c7fd..bad87952d6 100644 --- a/cms/static/sass/_variables.scss +++ b/cms/static/sass/_variables.scss @@ -24,16 +24,16 @@ $f-monospace: 'Bitstream Vera Sans Mono', Consolas, Courier, monospace; // colors - new for re-org $black: rgb(0,0,0); -$black-t0: rgba(0,0,0,0.125); -$black-t1: rgba(0,0,0,0.25); -$black-t2: rgba(0,0,0,0.50); -$black-t3: rgba(0,0,0,0.75); +$black-t0: rgba($black, 0.125); +$black-t1: rgba($black, 0.25); +$black-t2: rgba($black, 0.5); +$black-t3: rgba($black, 0.75); $white: rgb(255,255,255); -$white-t0: rgba(255,255,255,0.125); -$white-t1: rgba(255,255,255,0.25); -$white-t2: rgba(255,255,255,0.50); -$white-t3: rgba(255,255,255,0.75); +$white-t0: rgba($white, 0.125); +$white-t1: rgba($white, 0.25); +$white-t2: rgba($white, 0.5); +$white-t3: rgba($white, 0.75); $gray: rgb(127,127,127); $gray-l1: tint($gray,20%); @@ -63,10 +63,10 @@ $blue-s3: saturate($blue,45%); $blue-u1: desaturate($blue,15%); $blue-u2: desaturate($blue,30%); $blue-u3: desaturate($blue,45%); -$blue-t0: rgba(85, 151, 221,0.125); -$blue-t1: rgba(85, 151, 221,0.25); -$blue-t2: rgba(85, 151, 221,0.50); -$blue-t3: rgba(85, 151, 221,0.75); +$blue-t0: rgba($blue, 0.125); +$blue-t1: rgba($blue, 0.25); +$blue-t2: rgba($blue, 0.50); +$blue-t3: rgba($blue, 0.75); $pink: rgb(183, 37, 103); $pink-l1: tint($pink,20%); @@ -153,10 +153,11 @@ $orange-u1: desaturate($orange,15%); $orange-u2: desaturate($orange,30%); $orange-u3: desaturate($orange,45%); -$shadow: rgba(0,0,0,0.2); -$shadow-l1: rgba(0,0,0,0.1); -$shadow-l2: rgba(0,0,0,0.05); -$shadow-d1: rgba(0,0,0,0.4); +$shadow: rgba($black, 0.2); +$shadow-l1: rgba($black, 0.1); +$shadow-l2: rgba($black, 0.05); +$shadow-d1: rgba($black, 0.4); +$shadow-d2: rgba($black, 0.6); // ==================== @@ -186,4 +187,3 @@ $error-red: rgb(253, 87, 87); // type $sans-serif: $f-sans-serif; $body-line-height: golden-ratio(.875em, 1); - From 9336ffde94e5e626081a2e72356b2a4b8d210cdd Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 26 Jun 2013 17:14:12 -0400 Subject: [PATCH 372/375] Added clean reports dependency to rake JavaScript test tasks. This ensures that `rake test` will clean the report directories *before* running the JS tests. --- rakelib/jasmine.rake | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/rakelib/jasmine.rake b/rakelib/jasmine.rake index 0f532fdf6f..ff72161937 100644 --- a/rakelib/jasmine.rake +++ b/rakelib/jasmine.rake @@ -80,7 +80,7 @@ end namespace :jasmine do namespace system do desc "Open jasmine tests for #{system} in your default browser" - task :browser do + task :browser => [:clean_reports_dir] do Rake::Task[:assets].invoke(system, 'jasmine') django_for_jasmine(system, true) do |jasmine_url| jasmine_browser(jasmine_url) @@ -88,7 +88,7 @@ end end desc "Open jasmine tests for #{system} in your default browser, and dynamically recompile coffeescript" - task :'browser:watch' => :'assets:coffee:_watch' do + task :'browser:watch' => [:clean_reports_dir, :'assets:coffee:_watch'] do django_for_jasmine(system, true) do |jasmine_url| jasmine_browser(jasmine_url, jitter=0, wait=0) end @@ -97,7 +97,7 @@ end end desc "Use phantomjs to run jasmine tests for #{system} from the console" - task :phantomjs do + task :phantomjs => [:clean_reports_dir] do Rake::Task[:assets].invoke(system, 'jasmine') phantomjs = ENV['PHANTOMJS_PATH'] || 'phantomjs' django_for_jasmine(system, false) do |jasmine_url| @@ -122,7 +122,7 @@ static_js_dirs.each do |dir| namespace :jasmine do namespace dir do desc "Open jasmine tests for #{dir} in your default browser" - task :browser do + task :browser => [:clean_reports_dir] do # We need to use either CMS or LMS to preprocess files. Use LMS by default Rake::Task['assets:coffee'].invoke('lms', 'jasmine') template_jasmine_runner(dir) do |f| @@ -131,7 +131,7 @@ static_js_dirs.each do |dir| end desc "Use phantomjs to run jasmine tests for #{dir} from the console" - task :phantomjs do + task :phantomjs => [:clean_reports_dir] do # We need to use either CMS or LMS to preprocess files. Use LMS by default Rake::Task[:assets].invoke('lms', 'jasmine') template_jasmine_runner(dir) do |f| From 306ac482102970a3946e1e3736a6f5f01bb4143f Mon Sep 17 00:00:00 2001 From: dcadams Date: Wed, 26 Jun 2013 14:01:07 -0700 Subject: [PATCH 373/375] Email on enroll/un-enroll actions Optionally email students on enroll/un-enroll actions by instructor from enrollment tab in LMS. --- CHANGELOG.rst | 2 + common/djangoapps/terrain/factories.py | 2 +- .../instructor/tests/test_enrollment.py | 249 ++++++++++++------ lms/djangoapps/instructor/views.py | 137 +++++++++- .../courseware/instructor_dashboard.html | 2 + .../emails/enroll_email_allowedmessage.txt | 13 + .../emails/enroll_email_allowedsubject.txt | 1 + .../emails/enroll_email_enrolledmessage.txt | 8 + .../emails/enroll_email_enrolledsubject.txt | 1 + .../emails/unenroll_email_allowedmessage.txt | 6 + .../emails/unenroll_email_enrolledmessage.txt | 8 + .../emails/unenroll_email_subject.txt | 1 + 12 files changed, 330 insertions(+), 100 deletions(-) create mode 100644 lms/templates/emails/enroll_email_allowedmessage.txt create mode 100644 lms/templates/emails/enroll_email_allowedsubject.txt create mode 100644 lms/templates/emails/enroll_email_enrolledmessage.txt create mode 100644 lms/templates/emails/enroll_email_enrolledsubject.txt create mode 100644 lms/templates/emails/unenroll_email_allowedmessage.txt create mode 100644 lms/templates/emails/unenroll_email_enrolledmessage.txt create mode 100644 lms/templates/emails/unenroll_email_subject.txt diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4fea30a5c5..8481f3a707 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -152,3 +152,5 @@ Common: Updated CodeJail. Common: Allow setting of authentication session cookie name. +LMS: Option to email students when enroll/un-enroll them. + diff --git a/common/djangoapps/terrain/factories.py b/common/djangoapps/terrain/factories.py index decce42368..2ed78aaa9f 100644 --- a/common/djangoapps/terrain/factories.py +++ b/common/djangoapps/terrain/factories.py @@ -44,7 +44,7 @@ class GroupFactory(sf.GroupFactory): @world.absorb -class CourseEnrollmentAllowedFactory(sf.CourseEnrollmentAllowed): +class CourseEnrollmentAllowedFactory(sf.CourseEnrollmentAllowedFactory): """ Users allowed to enroll in the course outside of the usual window """ diff --git a/lms/djangoapps/instructor/tests/test_enrollment.py b/lms/djangoapps/instructor/tests/test_enrollment.py index 3ce82b700b..3b5bdc2ce9 100644 --- a/lms/djangoapps/instructor/tests/test_enrollment.py +++ b/lms/djangoapps/instructor/tests/test_enrollment.py @@ -1,177 +1,256 @@ -''' +""" Unit tests for enrollment methods in views.py -''' +""" from django.test.utils import override_settings -from django.contrib.auth.models import Group, User +from django.contrib.auth.models import User from django.core.urlresolvers import reverse from courseware.access import _course_staff_group_name from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE, get_user from xmodule.modulestore.django import modulestore +from xmodule.modulestore.tests.factories import CourseFactory +from student.tests.factories import UserFactory, CourseEnrollmentFactory, AdminFactory +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE, LoginEnrollmentTestCase from student.models import CourseEnrollment, CourseEnrollmentAllowed -from instructor.views import get_and_clean_student_list +from instructor.views import get_and_clean_student_list, send_mail_to_student +from django.core import mail + +USER_COUNT = 4 -@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) -class TestInstructorEnrollsStudent(LoginEnrollmentTestCase): - ''' - Check Enrollment/Unenrollment with/without auto-enrollment on activation - ''' +@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase): + """ + Check Enrollment/Unenrollment with/without auto-enrollment on activation and with/without email notification + """ def setUp(self): - self.full = modulestore().get_course("edX/full/6.002_Spring_2012") - self.toy = modulestore().get_course("edX/toy/2012_Fall") + instructor = AdminFactory.create() + self.client.login(username=instructor.username, password='test') - #Create instructor and student accounts - self.instructor = 'instructor1@test.com' - self.student1 = 'student1@test.com' - self.student2 = 'student2@test.com' - self.password = 'foo' - self.create_account('it1', self.instructor, self.password) - self.create_account('st1', self.student1, self.password) - self.create_account('st2', self.student2, self.password) - self.activate_user(self.instructor) - self.activate_user(self.student1) - self.activate_user(self.student2) + self.course = CourseFactory.create() - def make_instructor(course): - group_name = _course_staff_group_name(course.location) - g = Group.objects.create(name=group_name) - g.user_set.add(get_user(self.instructor)) + self.users = [ + UserFactory.create(username="student%d" % i, email="student%d@test.com" % i) + for i in xrange(USER_COUNT) + ] - make_instructor(self.toy) + for user in self.users: + CourseEnrollmentFactory.create(user=user, course_id=self.course.id) - #Enroll Students - self.logout() - self.login(self.student1, self.password) - self.enroll(self.toy) + # Empty the test outbox + mail.outbox = [] - self.logout() - self.login(self.student2, self.password) - self.enroll(self.toy) + def test_unenrollment_email_off(self): + """ + Do un-enrollment email off test + """ - #Enroll Instructor - self.logout() - self.login(self.instructor, self.password) - self.enroll(self.toy) + course = self.course - def test_unenrollment(self): - ''' - Do un-enrollment test - ''' - - course = self.toy + #Run the Un-enroll students command url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) - response = self.client.post(url, {'action': 'Unenroll multiple students', 'multiple_students': 'student1@test.com, student2@test.com'}) + response = self.client.post(url, {'action': 'Unenroll multiple students', 'multiple_students': 'student0@test.com student1@test.com'}) - #Check the page output + #Check the page output + self.assertContains(response, 'student0@test.com') self.assertContains(response, 'student1@test.com') - self.assertContains(response, 'student2@test.com') self.assertContains(response, 'un-enrolled') #Check the enrollment table + user = User.objects.get(email='student0@test.com') + ce = CourseEnrollment.objects.filter(course_id=course.id, user=user) + self.assertEqual(0, len(ce)) + user = User.objects.get(email='student1@test.com') ce = CourseEnrollment.objects.filter(course_id=course.id, user=user) self.assertEqual(0, len(ce)) - user = User.objects.get(email='student2@test.com') - ce = CourseEnrollment.objects.filter(course_id=course.id, user=user) - self.assertEqual(0, len(ce)) + #Check the outbox + self.assertEqual(len(mail.outbox), 0) - def test_enrollment_new_student_autoenroll_on(self): - ''' - Do auto-enroll on test - ''' + def test_enrollment_new_student_autoenroll_on_email_off(self): + """ + Do auto-enroll on, email off test + """ + + course = self.course #Run the Enroll students command - course = self.toy url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) - response = self.client.post(url, {'action': 'Enroll multiple students', 'multiple_students': 'test1_1@student.com, test1_2@student.com', 'auto_enroll': 'on'}) + response = self.client.post(url, {'action': 'Enroll multiple students', 'multiple_students': 'student1_1@test.com, student1_2@test.com', 'auto_enroll': 'on'}) #Check the page output - self.assertContains(response, 'test1_1@student.com') - self.assertContains(response, 'test1_2@student.com') + self.assertContains(response, 'student1_1@test.com') + self.assertContains(response, 'student1_2@test.com') self.assertContains(response, 'user does not exist, enrollment allowed, pending with auto enrollment on') + #Check the outbox + self.assertEqual(len(mail.outbox), 0) + #Check the enrollmentallowed db entries - cea = CourseEnrollmentAllowed.objects.filter(email='test1_1@student.com', course_id=course.id) + cea = CourseEnrollmentAllowed.objects.filter(email='student1_1@test.com', course_id=course.id) self.assertEqual(1, cea[0].auto_enroll) - cea = CourseEnrollmentAllowed.objects.filter(email='test1_2@student.com', course_id=course.id) + cea = CourseEnrollmentAllowed.objects.filter(email='student1_2@test.com', course_id=course.id) self.assertEqual(1, cea[0].auto_enroll) - #Check there is no enrollment db entry other than for the setup instructor and students + #Check there is no enrollment db entry other than for the other students ce = CourseEnrollment.objects.filter(course_id=course.id) - self.assertEqual(3, len(ce)) + self.assertEqual(4, len(ce)) #Create and activate student accounts with same email - self.student1 = 'test1_1@student.com' + self.student1 = 'student1_1@test.com' self.password = 'bar' self.create_account('s1_1', self.student1, self.password) self.activate_user(self.student1) - self.student2 = 'test1_2@student.com' + self.student2 = 'student1_2@test.com' self.create_account('s1_2', self.student2, self.password) self.activate_user(self.student2) #Check students are enrolled - user = User.objects.get(email='test1_1@student.com') + user = User.objects.get(email='student1_1@test.com') ce = CourseEnrollment.objects.filter(course_id=course.id, user=user) self.assertEqual(1, len(ce)) - user = User.objects.get(email='test1_2@student.com') + user = User.objects.get(email='student1_2@test.com') ce = CourseEnrollment.objects.filter(course_id=course.id, user=user) self.assertEqual(1, len(ce)) - def test_enrollmemt_new_student_autoenroll_off(self): - ''' - Do auto-enroll off test - ''' + def test_repeat_enroll(self): + """ + Try to enroll an already enrolled student + """ + + course = self.course + + url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) + response = self.client.post(url, {'action': 'Enroll multiple students', 'multiple_students': 'student0@test.com', 'auto_enroll': 'on'}) + self.assertContains(response, 'student0@test.com') + self.assertContains(response, 'already enrolled') + + def test_enrollmemt_new_student_autoenroll_off_email_off(self): + """ + Do auto-enroll off, email off test + """ + + course = self.course #Run the Enroll students command - course = self.toy url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) - response = self.client.post(url, {'action': 'Enroll multiple students', 'multiple_students': 'test2_1@student.com, test2_2@student.com'}) + response = self.client.post(url, {'action': 'Enroll multiple students', 'multiple_students': 'student2_1@test.com, student2_2@test.com'}) #Check the page output - self.assertContains(response, 'test2_1@student.com') - self.assertContains(response, 'test2_2@student.com') + self.assertContains(response, 'student2_1@test.com') + self.assertContains(response, 'student2_2@test.com') self.assertContains(response, 'user does not exist, enrollment allowed, pending with auto enrollment off') + #Check the outbox + self.assertEqual(len(mail.outbox), 0) + #Check the enrollmentallowed db entries - cea = CourseEnrollmentAllowed.objects.filter(email='test2_1@student.com', course_id=course.id) + cea = CourseEnrollmentAllowed.objects.filter(email='student2_1@test.com', course_id=course.id) self.assertEqual(0, cea[0].auto_enroll) - cea = CourseEnrollmentAllowed.objects.filter(email='test2_2@student.com', course_id=course.id) + cea = CourseEnrollmentAllowed.objects.filter(email='student2_2@test.com', course_id=course.id) self.assertEqual(0, cea[0].auto_enroll) #Check there is no enrollment db entry other than for the setup instructor and students ce = CourseEnrollment.objects.filter(course_id=course.id) - self.assertEqual(3, len(ce)) + self.assertEqual(4, len(ce)) #Create and activate student accounts with same email - self.student = 'test2_1@student.com' + self.student = 'student2_1@test.com' self.password = 'bar' self.create_account('s2_1', self.student, self.password) self.activate_user(self.student) - self.student = 'test2_2@student.com' + self.student = 'student2_2@test.com' self.create_account('s2_2', self.student, self.password) self.activate_user(self.student) #Check students are not enrolled - user = User.objects.get(email='test2_1@student.com') + user = User.objects.get(email='student2_1@test.com') ce = CourseEnrollment.objects.filter(course_id=course.id, user=user) self.assertEqual(0, len(ce)) - user = User.objects.get(email='test2_2@student.com') + user = User.objects.get(email='student2_2@test.com') ce = CourseEnrollment.objects.filter(course_id=course.id, user=user) self.assertEqual(0, len(ce)) def test_get_and_clean_student_list(self): - ''' + """ Clean user input test - ''' + """ - string = "abc@test.com, def@test.com ghi@test.com \n \n jkl@test.com " + string = "abc@test.com, def@test.com ghi@test.com \n \n jkl@test.com \n mno@test.com " cleaned_string, cleaned_string_lc = get_and_clean_student_list(string) - self.assertEqual(cleaned_string, ['abc@test.com', 'def@test.com', 'ghi@test.com', 'jkl@test.com']) + self.assertEqual(cleaned_string, ['abc@test.com', 'def@test.com', 'ghi@test.com', 'jkl@test.com', 'mno@test.com']) + + def test_enrollment_email_on(self): + """ + Do email on enroll test + """ + + course = self.course + + #Create activated, but not enrolled, user + UserFactory.create(username="student3_0", email="student3_0@test.com") + + url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) + response = self.client.post(url, {'action': 'Enroll multiple students', 'multiple_students': 'student3_0@test.com, student3_1@test.com, student3_2@test.com', 'auto_enroll': 'on', 'email_students': 'on'}) + + #Check the page output + self.assertContains(response, 'student3_0@test.com') + self.assertContains(response, 'student3_1@test.com') + self.assertContains(response, 'student3_2@test.com') + self.assertContains(response, 'added, email sent') + self.assertContains(response, 'user does not exist, enrollment allowed, pending with auto enrollment on, email sent') + + #Check the outbox + self.assertEqual(len(mail.outbox), 3) + self.assertEqual(mail.outbox[0].subject, 'You have been enrolled in MITx/999/Robot_Super_Course') + + self.assertEqual(mail.outbox[1].subject, 'You have been invited to register for MITx/999/Robot_Super_Course') + self.assertEqual(mail.outbox[1].body, "Dear student,\n\nYou have been invited to join MITx/999/Robot_Super_Course at edx.org by a member of the course staff.\n\n" + + "To finish your registration, please visit https://edx.org/register and fill out the registration form.\n" + + "Once you have registered and activated your account, you will see MITx/999/Robot_Super_Course listed on your dashboard.\n\n" + + "----\nThis email was automatically sent from edx.org to student3_1@test.com") + + def test_unenrollment_email_on(self): + """ + Do email on unenroll test + """ + + course = self.course + + #Create invited, but not registered, user + cea = CourseEnrollmentAllowed(email='student4_0@test.com', course_id=course.id) + cea.save() + + url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) + response = self.client.post(url, {'action': 'Unenroll multiple students', 'multiple_students': 'student4_0@test.com, student2@test.com, student3@test.com', 'email_students': 'on'}) + + #Check the page output + self.assertContains(response, 'student2@test.com') + self.assertContains(response, 'student3@test.com') + self.assertContains(response, 'un-enrolled, email sent') + + #Check the outbox + self.assertEqual(len(mail.outbox), 3) + self.assertEqual(mail.outbox[0].subject, 'You have been un-enrolled from MITx/999/Robot_Super_Course') + self.assertEqual(mail.outbox[0].body, "Dear Student,\n\nYou have been un-enrolled from course MITx/999/Robot_Super_Course by a member of the course staff. " + + "Please disregard the invitation previously sent.\n\n" + + "----\nThis email was automatically sent from edx.org to student4_0@test.com") + self.assertEqual(mail.outbox[1].subject, 'You have been un-enrolled from MITx/999/Robot_Super_Course') + + def test_send_mail_to_student(self): + """ + Do invalid mail template test + """ + + d = {'message': 'message_type_that_doesn\'t_exist'} + + send_mail_ret = send_mail_to_student('student0@test.com', d) + self.assertFalse(send_mail_ret) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index e9fff63698..ea96901bca 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -20,6 +20,8 @@ from django.http import HttpResponse from django_future.csrf import ensure_csrf_cookie from django.views.decorators.cache import cache_control from django.core.urlresolvers import reverse +from django.core.mail import send_mail + import xmodule.graders as xmgraders from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import ItemNotFoundError @@ -45,6 +47,7 @@ from mitxmako.shortcuts import render_to_response from psychometrics import psychoanalyze from student.models import CourseEnrollment, CourseEnrollmentAllowed import track.views +from mitxmako.shortcuts import render_to_string log = logging.getLogger(__name__) @@ -634,13 +637,15 @@ def instructor_dashboard(request, course_id): students = request.POST.get('multiple_students', '') auto_enroll = bool(request.POST.get('auto_enroll')) - ret = _do_enroll_students(course, course_id, students, auto_enroll=auto_enroll) + email_students = bool(request.POST.get('email_students')) + ret = _do_enroll_students(course, course_id, students, auto_enroll=auto_enroll, email_students=email_students) datatable = ret['datatable'] elif action == 'Unenroll multiple students': students = request.POST.get('multiple_students', '') - ret = _do_unenroll_students(course_id, students) + email_students = bool(request.POST.get('email_students')) + ret = _do_unenroll_students(course_id, students, email_students=email_students) datatable = ret['datatable'] elif action == 'List sections available in remote gradebook': @@ -1068,9 +1073,17 @@ def grade_summary(request, course_id): #----------------------------------------------------------------------------- # enrollment -def _do_enroll_students(course, course_id, students, overload=False, auto_enroll=False): - """Do the actual work of enrolling multiple students, presented as a string - of emails separated by commas or returns""" +def _do_enroll_students(course, course_id, students, overload=False, auto_enroll=False, email_students=False): + """ + Do the actual work of enrolling multiple students, presented as a string + of emails separated by commas or returns + `course` is course object + `course_id` id of course (a `str`) + `students` string of student emails separated by commas or returns (a `str`) + `overload` un-enrolls all existing students (a `boolean`) + `auto_enroll` is user input preference (a `boolean`) + `email_students` is user input preference (a `boolean`) + """ new_students, new_students_lc = get_and_clean_student_list(students) status = dict([x, 'unprocessed'] for x in new_students) @@ -1088,12 +1101,22 @@ def _do_enroll_students(course, course_id, students, overload=False, auto_enroll status[cea.email] = 'removed from pending enrollment list' ceaset.delete() + if email_students: + registration_url = 'https://' + settings.SITE_NAME + reverse('student.views.register_user') + #Composition of email + d = {'site_name': settings.SITE_NAME, + 'registration_url': registration_url, + 'course_id': course_id, + 'auto_enroll': auto_enroll, + 'course_url': registration_url + '/courses/' + course_id, + } + for student in new_students: try: user = User.objects.get(email=student) except User.DoesNotExist: - #User not signed up yet, put in pending enrollment allowed table + #Student not signed up yet, put in pending enrollment allowed table cea = CourseEnrollmentAllowed.objects.filter(email=student, course_id=course_id) #If enrollmentallowed already exists, update auto_enroll flag to however it was set in UI @@ -1104,18 +1127,42 @@ def _do_enroll_students(course, course_id, students, overload=False, auto_enroll status[student] = 'user does not exist, enrollment already allowed, pending with auto enrollment ' \ + ('on' if auto_enroll else 'off') continue + + #EnrollmentAllowed doesn't exist so create it cea = CourseEnrollmentAllowed(email=student, course_id=course_id, auto_enroll=auto_enroll) cea.save() - status[student] = 'user does not exist, enrollment allowed, pending with auto enrollment ' + ('on' if auto_enroll else 'off') + + status[student] = 'user does not exist, enrollment allowed, pending with auto enrollment ' \ + + ('on' if auto_enroll else 'off') + + if email_students: + #User is allowed to enroll but has not signed up yet + d['email_address'] = student + d['message'] = 'allowed_enroll' + send_mail_ret = send_mail_to_student(student, d) + status[student] += (', email sent' if send_mail_ret else '') continue + #Student has already registered if CourseEnrollment.objects.filter(user=user, course_id=course_id): status[student] = 'already enrolled' continue + try: + #Not enrolled yet ce = CourseEnrollment(user=user, course_id=course_id) ce.save() status[student] = 'added' + + if email_students: + #User enrolled for first time, populate dict with user specific info + d['email_address'] = student + d['first_name'] = user.first_name + d['last_name'] = user.last_name + d['message'] = 'enrolled_enroll' + send_mail_ret = send_mail_to_student(student, d) + status[student] += (', email sent' if send_mail_ret else '') + except: status[student] = 'rejected' @@ -1133,13 +1180,23 @@ def _do_enroll_students(course, course_id, students, overload=False, auto_enroll #Unenrollment -def _do_unenroll_students(course_id, students): - """Do the actual work of un-enrolling multiple students, presented as a string - of emails separated by commas or returns""" +def _do_unenroll_students(course_id, students, email_students=False): + """ + Do the actual work of un-enrolling multiple students, presented as a string + of emails separated by commas or returns + `course_id` is id of course (a `str`) + `students` is string of student emails separated by commas or returns (a `str`) + `email_students` is user input preference (a `boolean`) + """ old_students, _ = get_and_clean_student_list(students) status = dict([x, 'unprocessed'] for x in old_students) + if email_students: + #Composition of email + d = {'site_name': settings.SITE_NAME, + 'course_id': course_id} + for student in old_students: isok = False @@ -1153,6 +1210,14 @@ def _do_unenroll_students(course_id, students): try: user = User.objects.get(email=student) except User.DoesNotExist: + + if isok and email_students: + #User was allowed to join but had not signed up yet + d['email_address'] = student + d['message'] = 'allowed_unenroll' + send_mail_ret = send_mail_to_student(student, d) + status[student] += (', email sent' if send_mail_ret else '') + continue ce = CourseEnrollment.objects.filter(user=user, course_id=course_id) @@ -1161,6 +1226,15 @@ def _do_unenroll_students(course_id, students): try: ce[0].delete() status[student] = "un-enrolled" + if email_students: + #User was enrolled + d['email_address'] = student + d['first_name'] = user.first_name + d['last_name'] = user.last_name + d['message'] = 'enrolled_unenroll' + send_mail_ret = send_mail_to_student(student, d) + status[student] += (', email sent' if send_mail_ret else '') + except Exception: if not isok: status[student] = "Error! Failed to un-enroll" @@ -1173,13 +1247,48 @@ def _do_unenroll_students(course_id, students): return data +def send_mail_to_student(student, param_dict): + """ + Construct the email using templates and then send it. + `student` is the student's email address (a `str`), + + `param_dict` is a `dict` with keys [ + `site_name`: name given to edX instance (a `str`) + `registration_url`: url for registration (a `str`) + `course_id`: id of course (a `str`) + `auto_enroll`: user input option (a `str`) + `course_url`: url of course (a `str`) + `email_address`: email of student (a `str`) + `first_name`: student first name (a `str`) + `last_name`: student last name (a `str`) + `message`: type of email to send and template to use (a `str`) + ] + Returns a boolean indicating whether the email was sent successfully. + """ + + EMAIL_TEMPLATE_DICT = {'allowed_enroll': ('emails/enroll_email_allowedsubject.txt', 'emails/enroll_email_allowedmessage.txt'), + 'enrolled_enroll': ('emails/enroll_email_enrolledsubject.txt', 'emails/enroll_email_enrolledmessage.txt'), + 'allowed_unenroll': ('emails/unenroll_email_subject.txt', 'emails/unenroll_email_allowedmessage.txt'), + 'enrolled_unenroll': ('emails/unenroll_email_subject.txt', 'emails/unenroll_email_enrolledmessage.txt')} + + subject_template, message_template = EMAIL_TEMPLATE_DICT.get(param_dict['message'], (None, None)) + if subject_template is not None and message_template is not None: + subject = render_to_string(subject_template, param_dict) + message = render_to_string(message_template, param_dict) + + # Email subject *must not* contain newlines + subject = ''.join(subject.splitlines()) + send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [student], fail_silently=False) + return True + else: + return False + + def get_and_clean_student_list(students): """ Separate out individual student email from the comma, or space separated string. - - In: - students: string coming from the input text area - Return: + `students` is string of student emails separated by commas or returns (a `str`) + Returns: students: list of cleaned student emails students_lc: list of lower case cleaned student emails """ diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index d541962906..bc49cda427 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -382,6 +382,8 @@ function goto( mode)

Enroll or un-enroll one or many students: enter emails, separated by new lines or commas;

+ Notify students by email +

Auto-enroll students when they activate

diff --git a/lms/templates/emails/enroll_email_allowedmessage.txt b/lms/templates/emails/enroll_email_allowedmessage.txt new file mode 100644 index 0000000000..eab347166e --- /dev/null +++ b/lms/templates/emails/enroll_email_allowedmessage.txt @@ -0,0 +1,13 @@ +Dear student, + +You have been invited to join ${course_id} at ${site_name} by a member of the course staff. + +To finish your registration, please visit ${registration_url} and fill out the registration form. +% if auto_enroll: +Once you have registered and activated your account, you will see ${course_id} listed on your dashboard. +% else: +Once you have registered and activated your account, visit ${course_url} to join the course. +% endif + +---- +This email was automatically sent from ${site_name} to ${email_address} \ No newline at end of file diff --git a/lms/templates/emails/enroll_email_allowedsubject.txt b/lms/templates/emails/enroll_email_allowedsubject.txt new file mode 100644 index 0000000000..41da60d1db --- /dev/null +++ b/lms/templates/emails/enroll_email_allowedsubject.txt @@ -0,0 +1 @@ +You have been invited to register for ${course_id} \ No newline at end of file diff --git a/lms/templates/emails/enroll_email_enrolledmessage.txt b/lms/templates/emails/enroll_email_enrolledmessage.txt new file mode 100644 index 0000000000..8e8f24efed --- /dev/null +++ b/lms/templates/emails/enroll_email_enrolledmessage.txt @@ -0,0 +1,8 @@ +Dear ${first_name} ${last_name} + +You have been enrolled in ${course_id} at ${site_name} by a member of the course staff. The course should now appear on your ${site_name} dashboard. + +To start accessing course materials, please visit ${course_url} + +---- +This email was automatically sent from ${site_name} to ${first_name} ${last_name} \ No newline at end of file diff --git a/lms/templates/emails/enroll_email_enrolledsubject.txt b/lms/templates/emails/enroll_email_enrolledsubject.txt new file mode 100644 index 0000000000..db897a3299 --- /dev/null +++ b/lms/templates/emails/enroll_email_enrolledsubject.txt @@ -0,0 +1 @@ +You have been enrolled in ${course_id} \ No newline at end of file diff --git a/lms/templates/emails/unenroll_email_allowedmessage.txt b/lms/templates/emails/unenroll_email_allowedmessage.txt new file mode 100644 index 0000000000..9bd0bd3cfd --- /dev/null +++ b/lms/templates/emails/unenroll_email_allowedmessage.txt @@ -0,0 +1,6 @@ +Dear Student, + +You have been un-enrolled from course ${course_id} by a member of the course staff. Please disregard the invitation previously sent. + +---- +This email was automatically sent from ${site_name} to ${email_address} \ No newline at end of file diff --git a/lms/templates/emails/unenroll_email_enrolledmessage.txt b/lms/templates/emails/unenroll_email_enrolledmessage.txt new file mode 100644 index 0000000000..8a7f9f996e --- /dev/null +++ b/lms/templates/emails/unenroll_email_enrolledmessage.txt @@ -0,0 +1,8 @@ +Dear ${first_name} ${last_name} + +You have been un-enrolled in ${course_id} at ${site_name} by a member of the course staff. The course will no longer appear on your ${site_name} dashboard. + +Your other courses have not been affected. + +---- +This email was automatically sent from ${site_name} to ${first_name} ${last_name} \ No newline at end of file diff --git a/lms/templates/emails/unenroll_email_subject.txt b/lms/templates/emails/unenroll_email_subject.txt new file mode 100644 index 0000000000..f79218ff22 --- /dev/null +++ b/lms/templates/emails/unenroll_email_subject.txt @@ -0,0 +1 @@ +You have been un-enrolled from ${course_id} \ No newline at end of file From c98a77565fef43ebc92c5d58d614cacb6d17c8f2 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Thu, 27 Jun 2013 10:52:26 -0400 Subject: [PATCH 374/375] Make the UrlResetMixin load the urlconf after resetting it, and fix the comment client test that was leaving ENABLE_DISCUSSION_SERVICE at True --- common/djangoapps/util/testing.py | 5 ++++- lms/djangoapps/django_comment_client/base/tests.py | 12 ++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/common/djangoapps/util/testing.py b/common/djangoapps/util/testing.py index d33f1c8f8b..062b04c8a0 100644 --- a/common/djangoapps/util/testing.py +++ b/common/djangoapps/util/testing.py @@ -1,7 +1,7 @@ import sys from django.conf import settings -from django.core.urlresolvers import clear_url_caches +from django.core.urlresolvers import clear_url_caches, resolve class UrlResetMixin(object): @@ -27,6 +27,9 @@ class UrlResetMixin(object): reload(sys.modules[urlconf]) clear_url_caches() + # Resolve a URL so that the new urlconf gets loaded + resolve('/') + def setUp(self): """Reset django default urlconf before tests and after tests""" super(UrlResetMixin, self).setUp() diff --git a/lms/djangoapps/django_comment_client/base/tests.py b/lms/djangoapps/django_comment_client/base/tests.py index aa5b657bd6..434d4d616b 100644 --- a/lms/djangoapps/django_comment_client/base/tests.py +++ b/lms/djangoapps/django_comment_client/base/tests.py @@ -1,6 +1,5 @@ import logging -from django.conf import settings from django.test.utils import override_settings from django.test.client import Client from django.contrib.auth.models import User @@ -21,16 +20,13 @@ log = logging.getLogger(__name__) @override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) @patch('comment_client.utils.requests.request') class ViewsTestCase(UrlResetMixin, ModuleStoreTestCase): + + @patch.dict("django.conf.settings.MITX_FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) def setUp(self): - # This feature affects the contents of urls.py, so we change - # it before the call to super.setUp() which reloads urls.py (because + # Patching the ENABLE_DISCUSSION_SERVICE value affects the contents of urls.py, + # so we need to call super.setUp() which reloads urls.py (because # of the UrlResetMixin) - - # This setting is cleaned up at the end of the test by @override_settings, which - # restores all of the old settings - settings.MITX_FEATURES['ENABLE_DISCUSSION_SERVICE'] = True - super(ViewsTestCase, self).setUp() # create a course From ed57e7e555c87b3a798209cb7ba7e7ea32584bd0 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Thu, 27 Jun 2013 10:51:23 -0400 Subject: [PATCH 375/375] Used dependency to ensure that REPORT_DIR exists before cleaning. This allows the build to pass in Jenkins --- rakelib/tests.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rakelib/tests.rake b/rakelib/tests.rake index 0ca7c5c1e9..2bbe3a6ad8 100644 --- a/rakelib/tests.rake +++ b/rakelib/tests.rake @@ -53,7 +53,7 @@ task :clean_test_files do sh("git clean -fqdx test_root") end -task :clean_reports_dir do +task :clean_reports_dir => REPORT_DIR do desc "Clean coverage files, to ensure that we don't use stale data to generate reports." # We delete the files but preserve the directory structure