From 911110c6bc4d80cb0452a9789bb0d4c8b4946b64 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Sat, 11 May 2013 16:19:04 -0400 Subject: [PATCH 01/51] 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 02/51] 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 03/51] 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 04/51] 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 05/51] 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 af2416756edaf34f257e3ff20c156202506d13df Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Wed, 5 Jun 2013 17:06:36 -0400 Subject: [PATCH 06/51] 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 665bc6e3dba79485d2fce9683fea32f4a2fb0423 Mon Sep 17 00:00:00 2001 From: Juho Kim Date: Thu, 6 Jun 2013 13:48:20 -0400 Subject: [PATCH 07/51] analytics server setup simplified --- lms/envs/analyticsserver.py | 225 +----------------------------------- 1 file changed, 5 insertions(+), 220 deletions(-) diff --git a/lms/envs/analyticsserver.py b/lms/envs/analyticsserver.py index 9bc6745129..a72d6bedc5 100644 --- a/lms/envs/analyticsserver.py +++ b/lms/envs/analyticsserver.py @@ -17,27 +17,16 @@ 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 +# import json ROOT_URLCONF = 'lms.urls' -import sys +# import sys from .common import * +from .dev 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', @@ -50,6 +39,8 @@ INSTALLED_APPS = INSTALLED_APPS + ( 'djeventstream.httphandler', INSTALLED_ANALYTICS_MODULES = open("../analytics_modules.txt").readlines() INSTALLED_ANALYTICS_MODULES = [x.strip() for x in INSTALLED_ANALYTICS_MODULES if x and len(x)>1] +print "======JUHO==========", INSTALLED_ANALYTICS_MODULES + DJFS = { 'type' : 'osfs', 'directory_root' : '/tmp/djfsmodule', 'url_root' : 'file:///tmp/' @@ -59,209 +50,3 @@ 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 = "" From e161a0a65e811282b38d7b240d2d24db6cc67944 Mon Sep 17 00:00:00 2001 From: Juho Kim Date: Thu, 6 Jun 2013 18:58:20 -0400 Subject: [PATCH 08/51] simplified analyticserver settings --- lms/envs/analyticsserver.py | 11 ++++------- lms/urls.py | 6 +++++- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/lms/envs/analyticsserver.py b/lms/envs/analyticsserver.py index a72d6bedc5..362698da20 100644 --- a/lms/envs/analyticsserver.py +++ b/lms/envs/analyticsserver.py @@ -23,9 +23,8 @@ ROOT_URLCONF = 'lms.urls' # import sys -from .common import * from .dev import * -from logsettings import get_logger_config +# from logsettings import get_logger_config MITX_FEATURES['RUN_AS_ANALYTICS_SERVER_ENABLED'] = True @@ -36,10 +35,9 @@ INSTALLED_APPS = INSTALLED_APPS + ( 'djeventstream.httphandler', 'edinsights.modulefs', ) -INSTALLED_ANALYTICS_MODULES = open("../analytics_modules.txt").readlines() -INSTALLED_ANALYTICS_MODULES = [x.strip() for x in INSTALLED_ANALYTICS_MODULES if x and len(x)>1] - -print "======JUHO==========", INSTALLED_ANALYTICS_MODULES +INSTALLED_ANALYTICS_MODULES = ['edxdataanalytic.edxdataanalytic', 'video_heatmap.video_heatmap'] +# INSTALLED_ANALYTICS_MODULES = open("../analytics_modules.txt").readlines() +# INSTALLED_ANALYTICS_MODULES = [x.strip() for x in INSTALLED_ANALYTICS_MODULES if x and len(x)>1] DJFS = { 'type' : 'osfs', 'directory_root' : '/tmp/djfsmodule', @@ -47,6 +45,5 @@ DJFS = { 'type' : 'osfs', } import djcelery - djcelery.setup_loader() default_optional_kwargs = ['fs','db','query'] diff --git a/lms/urls.py b/lms/urls.py index cec118fe7d..7f5e8aa9f1 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -355,7 +355,11 @@ if settings.MITX_FEATURES.get('ENABLE_SQL_TRACKING_LOGS'): if settings.MITX_FEATURES.get('RUN_AS_ANALYTICS_SERVER_ENABLED'): urlpatterns += ( - url('^', include('edinsights.core.urls')), + # url('^', include('edinsights.core.urls')), + url(r'^view/([A-Za-z_+]+)$', 'edinsights.core.views.handle_view'), + url(r'^query/([A-Za-z_+]+)$', 'edinsights.core.views.handle_query'), + url(r'^schema$', 'edinsights.core.views.schema'), + url(r'^event_properties$', 'edinsights.core.views.event_properties'), ) import edinsights.core.registry From 98b978528b88241e3a923f7212c947b65c95b7f7 Mon Sep 17 00:00:00 2001 From: Juho Kim Date: Fri, 7 Jun 2013 18:54:36 -0400 Subject: [PATCH 09/51] simplified analytics conf --- lms/envs/analyticsserver.py | 30 +++++++++++++++--------------- lms/urls.py | 6 +----- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/lms/envs/analyticsserver.py b/lms/envs/analyticsserver.py index 362698da20..62ae862177 100644 --- a/lms/envs/analyticsserver.py +++ b/lms/envs/analyticsserver.py @@ -9,41 +9,41 @@ 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. +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. +it should only be granted access to read replicas of the databases. """ -# import json ROOT_URLCONF = 'lms.urls' -# import sys - from .dev import * -# from logsettings import get_logger_config MITX_FEATURES['RUN_AS_ANALYTICS_SERVER_ENABLED'] = True -INSTALLED_APPS = INSTALLED_APPS + ( 'djeventstream.httphandler', +INSTALLED_APPS = INSTALLED_APPS + ( + 'djeventstream.httphandler', 'djcelery', 'south', 'edinsights.core', 'edinsights.modulefs', ) -INSTALLED_ANALYTICS_MODULES = ['edxdataanalytic.edxdataanalytic', 'video_heatmap.video_heatmap'] -# INSTALLED_ANALYTICS_MODULES = open("../analytics_modules.txt").readlines() -# INSTALLED_ANALYTICS_MODULES = [x.strip() for x in INSTALLED_ANALYTICS_MODULES if x and len(x)>1] +if os.path.isfile("../analytics_modules.txt"): + INSTALLED_ANALYTICS_MODULES = open("../analytics_modules.txt").readlines() + INSTALLED_ANALYTICS_MODULES = [x.strip() for x in INSTALLED_ANALYTICS_MODULES if x and len(x) > 1] +else: + INSTALLED_ANALYTICS_MODULES = ['edxdataanalytic.edxdataanalytic'] -DJFS = { 'type' : 'osfs', - 'directory_root' : '/tmp/djfsmodule', - 'url_root' : 'file:///tmp/' - } +DJFS = { + 'type': 'osfs', + 'directory_root': '/tmp/djfsmodule', + 'url_root': 'file:///tmp/' +} import djcelery djcelery.setup_loader() -default_optional_kwargs = ['fs','db','query'] +default_optional_kwargs = ['fs', 'db', 'query'] diff --git a/lms/urls.py b/lms/urls.py index 7f5e8aa9f1..6e667b5e03 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -355,11 +355,7 @@ if settings.MITX_FEATURES.get('ENABLE_SQL_TRACKING_LOGS'): if settings.MITX_FEATURES.get('RUN_AS_ANALYTICS_SERVER_ENABLED'): urlpatterns += ( - # url('^', include('edinsights.core.urls')), - url(r'^view/([A-Za-z_+]+)$', 'edinsights.core.views.handle_view'), - url(r'^query/([A-Za-z_+]+)$', 'edinsights.core.views.handle_query'), - url(r'^schema$', 'edinsights.core.views.schema'), - url(r'^event_properties$', 'edinsights.core.views.event_properties'), + url(r'^edinsights_service/', include('edinsights.core.urls')), ) import edinsights.core.registry From 271de4fbc931d686885559fedfbfa72ea96f24a7 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Mon, 10 Jun 2013 14:38:52 -0400 Subject: [PATCH 10/51] Added in testing for course team Also modified the create_course method in common to allow for the possibility to access the course name and etc. from other files. --- .../contentstore/features/common.py | 14 ++-- .../contentstore/features/course-team.feature | 34 ++++++++ .../contentstore/features/course-team.py | 77 +++++++++++++++++++ 3 files changed, 120 insertions(+), 5 deletions(-) create mode 100644 cms/djangoapps/contentstore/features/course-team.feature create mode 100644 cms/djangoapps/contentstore/features/course-team.py diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index 494192ad06..a80fbcbb8f 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -13,6 +13,10 @@ import time from logging import getLogger logger = getLogger(__name__) +COURSE_NAME = 'Robot Super Course' +COURSE_NUM = '999' +COURSE_ORG = 'MITx' + ########### STEP HELPERS ############## @step('I (?:visit|access|open) the Studio homepage$') @@ -75,9 +79,9 @@ def create_studio_user( def fill_in_course_info( - name='Robot Super Course', - org='MITx', - num='101'): + 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) @@ -107,11 +111,11 @@ def log_into_studio( def create_a_course(): - c = world.CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course') + c = 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/999/Robot_Super_Course') + g = world.GroupFactory.create(name='instructor_MITx/%s/%s' % (COURSE_NUM, COURSE_NAME.replace(" ", "_"),)) u = get_user_by_email('robot+studio@edx.org') u.groups.add(g) u.save() diff --git a/cms/djangoapps/contentstore/features/course-team.feature b/cms/djangoapps/contentstore/features/course-team.feature new file mode 100644 index 0000000000..502321c49b --- /dev/null +++ b/cms/djangoapps/contentstore/features/course-team.feature @@ -0,0 +1,34 @@ +Feature: Course Team + As a course author, I want to be able to add others to my team + + Scenario: Users can add other users + Given I have opened a new course in Studio + And The user "abcd" 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 + + Scenario: Added users cannot delete or add other users + Given I have opened a new course in Studio + And The user "abcd" 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 + + Scenario: Users can delete other users + Given I have opened a new course in Studio + And The user "abcd" 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 + + 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 + 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 new file mode 100644 index 0000000000..7bf538157d --- /dev/null +++ b/cms/djangoapps/contentstore/features/course-team.py @@ -0,0 +1,77 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + +from lettuce import world, step +from common import create_studio_user, COURSE_NAME + +PASS = 'test' +EXTENSION = '@edx.org' + + +@step(u'I am viewing the course team settings') +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): + create_studio_user(uname=name, password=PASS, email=(name + EXTENSION)) + + +@step(u'I add "([^"]*)" to the course team') +def add_other_user(step, name): + new_user_css = '.new-user-button' + world.css_find(new_user_css).click() + + email_css = '.email-input' + f = world.css_find(email_css) + f._element.send_keys(name, EXTENSION) + + confirm_css = '#add_user' + world.css_find(confirm_css).click() + + +@step(u'I delete "([^"]*)" from the course team') +def delete_other_user(step, name): + to_delete_css = '.remove-user[data-id="%s%s"]' % (name, EXTENSION,) + world.css_find(to_delete_css).click() + + +@step(u'"([^"]*)" logs in$') +def other_user_login(step, name): + world.browser.cookies.delete() + world.visit('/') + + signin_css = 'a.action-signin' + world.is_css_present(signin_css) + world.css_click(signin_css) + + login_form = world.browser.find_by_css('form#login_form') + login_form.find_by_name('email').fill(name + EXTENSION) + login_form.find_by_name('password').fill(PASS) + login_form.find_by_name('submit').click() + + +@step(u'He does( not)? see the course on his page') +def see_course(step, doesnt): + class_css = '.class-name' + all_courses = world.css_find(class_css) + all_names = [item.html for item in all_courses] + if doesnt: + assert not COURSE_NAME in all_names + else: + assert COURSE_NAME in all_names + + +@step(u'He cannot delete users') +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): + add_css = '.new-user' + assert world.is_css_not_present(add_css) From b7674679d9782fdbb4686857a6f32dffd2f69efe Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Tue, 11 Jun 2013 09:18:16 -0400 Subject: [PATCH 11/51] Tests now use their own contentstore database test.py was changed such that the database being used is test_xcontent course_helpers.clear_courses now calls contentstore().fs_files.drop() such that all content is removed and the databse is started afresh in the same manner as the modulestore --- cms/envs/test.py | 2 +- common/djangoapps/terrain/course_helpers.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/cms/envs/test.py b/cms/envs/test.py index 8a3f9ba158..667b9f3555 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': 'xcontent', + 'db': 'test_xcontent', } } diff --git a/common/djangoapps/terrain/course_helpers.py b/common/djangoapps/terrain/course_helpers.py index cc1f770217..23f932bc3b 100644 --- a/common/djangoapps/terrain/course_helpers.py +++ b/common/djangoapps/terrain/course_helpers.py @@ -11,6 +11,7 @@ from django.contrib.auth.middleware import AuthenticationMiddleware from django.contrib.sessions.middleware import SessionMiddleware from student.models import CourseEnrollment from xmodule.modulestore.django import _MODULESTORES, modulestore +from xmodule.contentstore.django import contentstore from xmodule.templates import update_templates from bs4 import BeautifulSoup import os.path @@ -133,3 +134,4 @@ def clear_courses(): _MODULESTORES = {} modulestore().collection.drop() update_templates(modulestore('direct')) + contentstore().fs_files.drop() From d471bcb72316a51d7761318a0c8211f87dcac90f Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Tue, 11 Jun 2013 10:00:52 -0400 Subject: [PATCH 12/51] Added tests for uploading files TODO: When deleting is available, test deleting uploaded files` --- .../contentstore/features/upload.feature | 15 +++++ .../contentstore/features/upload.py | 57 +++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 cms/djangoapps/contentstore/features/upload.feature create mode 100644 cms/djangoapps/contentstore/features/upload.py diff --git a/cms/djangoapps/contentstore/features/upload.feature b/cms/djangoapps/contentstore/features/upload.feature new file mode 100644 index 0000000000..5717cfb907 --- /dev/null +++ b/cms/djangoapps/contentstore/features/upload.feature @@ -0,0 +1,15 @@ +Feature: Upload Files + As a course author, I want to be able to upload files for my students + + Scenario: Users can upload files + Given I have opened a new course in Studio + And I go to the files and uploads page + When I upload the file "upload.feature" + Then I see the file "upload.feature" was uploaded + + Scenario: Users can update files + Given I have opened a new course in studio + And I go to the files and uploads page + When I upload the file "upload.feature" + And I upload the file "upload.feature" + Then I see only one "upload.feature" diff --git a/cms/djangoapps/contentstore/features/upload.py b/cms/djangoapps/contentstore/features/upload.py new file mode 100644 index 0000000000..6c53741dcc --- /dev/null +++ b/cms/djangoapps/contentstore/features/upload.py @@ -0,0 +1,57 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + +from lettuce import world, step +import os + + +@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' + 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' + world.css_find(upload_css).click() + + file_css = '.file-input' + upload = world.css_find(file_css) + upload._element.send_keys(os.getcwd() + '/' + file_name) + + close_css = '.close-button' + world.css_find(close_css).click() + + +@step(u'I see the file "([^"]*)" was uploaded') +def check_upload(step, file_name): + index = get_index(file_name) + assert index != -1 + + +@step(u'I see only one "([^"]*)"$') +def no_duplicate(step, file_name): + names_css = '.name-col > a.filename' + all_names = world.css_find(names_css) + only_one = False + for i in range(len(all_names)): + if file_name == all_names[i].html: + only_one = not only_one + assert only_one + + +@step(u'I PAUSE') +def pause(step): + from pdb import set_trace; set_trace() + + +def get_index(file_name): + names_css = '.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: + return i + return -1 From c74d76a36b1609e0a2c7b5ed16718fb9cc66d477 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Tue, 11 Jun 2013 10:07:13 -0400 Subject: [PATCH 13/51] Removed test breakpoint --- cms/djangoapps/contentstore/features/upload.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/cms/djangoapps/contentstore/features/upload.py b/cms/djangoapps/contentstore/features/upload.py index 6c53741dcc..d7a6eba999 100644 --- a/cms/djangoapps/contentstore/features/upload.py +++ b/cms/djangoapps/contentstore/features/upload.py @@ -43,11 +43,6 @@ def no_duplicate(step, file_name): assert only_one -@step(u'I PAUSE') -def pause(step): - from pdb import set_trace; set_trace() - - def get_index(file_name): names_css = '.name-col > a.filename' all_names = world.css_find(names_css) From 81b06d5c702cf280368a7f6bc0fd7fcaf15ba3d8 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Tue, 11 Jun 2013 14:34:18 -0400 Subject: [PATCH 14/51] Added tests for static pages Also added in drag and drop helper. Please be careful using this method as it is truly a hack to obtain the visual effects. As of this commit, selenium does not support drag and drop for jQuery sortable items --- .../features/static-pages.feature | 34 +++++++++ .../contentstore/features/static-pages.py | 76 +++++++++++++++++++ common/djangoapps/terrain/ui_helpers.py | 12 +++ 3 files changed, 122 insertions(+) create mode 100644 cms/djangoapps/contentstore/features/static-pages.feature create mode 100644 cms/djangoapps/contentstore/features/static-pages.py diff --git a/cms/djangoapps/contentstore/features/static-pages.feature b/cms/djangoapps/contentstore/features/static-pages.feature new file mode 100644 index 0000000000..99a4e802dd --- /dev/null +++ b/cms/djangoapps/contentstore/features/static-pages.feature @@ -0,0 +1,34 @@ +Feature: Static Pages + As a course author, I want to be able to add static pages + + Scenario: Users can add static pages + Given I have opened a new course in Studio + And I go to the static pages page + When I add a new page + Then I should see a "Empty" static page + + Scenario: Users can delete static pages + Given I have opened a new course in Studio + And I go to the static pages page + And I add a new page + When I will confirm all alerts + And I "delete" the "Empty" page + Then I should not see a "Empty" static page + + Scenario: Users can edit static pages + Given I have opened a new course in Studio + And I go to the static pages page + And I add a new page + When I "edit" the "Empty" page + And I change the name to "New" + Then I should see a "New" static page + + Scenario: Users can reorder static pages + Given I have opened a new course in Studio + And I go to the static pages page + And I add a new page + And I "edit" the "Empty" page + And I change the name to "New" + And I add a new page + When I move "New" after "Empty" + Then I see the order is "Empty New" diff --git a/cms/djangoapps/contentstore/features/static-pages.py b/cms/djangoapps/contentstore/features/static-pages.py new file mode 100644 index 0000000000..2a1110de40 --- /dev/null +++ b/cms/djangoapps/contentstore/features/static-pages.py @@ -0,0 +1,76 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + +from lettuce import world, step +from selenium.webdriver.common.keys import Keys + + +@step(u'I go to the static pages page') +def go_to_uploads(step): + menu_css = 'li.nav-course-courseware' + uploads_css = '.nav-course-courseware-pages' + world.css_find(menu_css).click() + world.css_find(uploads_css).click() + + +@step(u'I add a new page') +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): + index = get_index(page) + if doesnt: + assert index == -1 + else: + assert index != -1 + + +@step(u'I "([^"]*)" the "([^"]*)" page$') +def click_edit_delete(step, edit_delete, page): + button_css = '.%s-button' % edit_delete + index = get_index(page) + assert index != -1 + world.css_find(button_css)[index].click() + + +@step(u'I change the name to "([^"]*)"$') +def change_name(step, new_name): + settings_css = '#settings-mode' + world.css_find(settings_css).click() + input_css = '.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' + world.css_find(save_button).click() + + +@step(u'I move "([^"]*)" after "([^"]*)"$') +def change_list(step, item1, item2): + index1 = get_index(item1) + index2 = get_index(item2) + assert index1 != -1 and index2 != -1 + world.drag_sortable_after(".component", index1, index2, ".ui-sortable") + + +@step(u'I see the order is "([^"]*)"$') +def check_order(step, items): + items = items.split(' ') + name_css = 'section[data-type="HTMLModule"]' + all_elements = world.css_find(name_css) + for i in range(len(items)): + assert all_elements[i].html == '\n %s\n' % items[i] + + +def get_index(name): + page_name_css = 'section[data-type="HTMLModule"]' + all_pages = world.css_find(page_name_css) + for i in range(len(all_pages)): + if all_pages[i].html == '\n %s\n' % name: + return i + return -1 diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py index ecd43eb719..c59d7030ff 100644 --- a/common/djangoapps/terrain/ui_helpers.py +++ b/common/djangoapps/terrain/ui_helpers.py @@ -158,3 +158,15 @@ def click_tools(): tools_css = 'li.nav-course-tools' if world.browser.is_element_present_by_css(tools_css): world.css_click(tools_css) + + +@world.absorb +def drag_sortable_after(item_css, index1, index2, sortable_css): + """ + This is a hack in order to simulate jQuery's sortable list dragging as Selenium cannot currently do it with action_chains + Please note that this is very finnicky with keeping the changes after refreshing the page so be careful when testing persistant changes + Also note that this is mainly for visualization of the sortable list and the true sortable drag and drop fires many events + """ + world.browser.execute_script('$("%(item_css)s:eq(%(index_one)s)").insertAfter($("%(item_css)s:eq(%(index_two)s)"));\ + $("%(sortable_css)s").trigger("sortupdate")' % + {'item_css': item_css, 'index_one': index1, 'index_two': index2, 'sortable_css': sortable_css}) From eb14a07814db2b4ca9f2a240afc9ceac019141b1 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Tue, 11 Jun 2013 16:15:49 -0400 Subject: [PATCH 15/51] Added tests for course updates and handouts --- .../features/course-updates.feature | 44 ++++++++++ .../contentstore/features/course-updates.py | 82 +++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 cms/djangoapps/contentstore/features/course-updates.feature create mode 100644 cms/djangoapps/contentstore/features/course-updates.py diff --git a/cms/djangoapps/contentstore/features/course-updates.feature b/cms/djangoapps/contentstore/features/course-updates.feature new file mode 100644 index 0000000000..7266e4f9a6 --- /dev/null +++ b/cms/djangoapps/contentstore/features/course-updates.feature @@ -0,0 +1,44 @@ +Feature: Course updates + As a course author, I want to be able to provide updates to my students + + Scenario: Users can add updates + Given I have opened a new course in Studio + And I go to the course updates page + When I add a new update + And I make the text "Hello" + Then I should see the update "Hello" + + Scenario: Users can edit updates + Given I have opened a new course in Studio + And I go to the course updates page + When I add a new update + And I make the text "Hello" + And I click the "edit" button + And I make the text "Goodbye" + Then I should see the update "Goodbye" + + Scenario: Users can delete updates + Given I have opened a new course in Studio + And I go to the course updates page + And I add a new update + And I make the text "Hello" + When I will confirm all alerts + And I click the "delete" button + Then I should not see the update "Hello" + + + Scenario: Users can edit update dates + Given I have opened a new course in Studio + And I go to the course updates page + And I add a new update + And I make the text "Hello" + When I click the "edit" button + And I make the date "June 1, 2013" + Then I should see the date "June 1, 2013" + + Scenario: Users can change handouts + Given I have opened a new course in Studio + And I go to the course updates page + When I edit the handouts + And I make the text "
    Test
" + Then I see the handout "Test" diff --git a/cms/djangoapps/contentstore/features/course-updates.py b/cms/djangoapps/contentstore/features/course-updates.py new file mode 100644 index 0000000000..4d0b5ef174 --- /dev/null +++ b/cms/djangoapps/contentstore/features/course-updates.py @@ -0,0 +1,82 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + +from lettuce import world, step +from selenium.webdriver.common.keys import Keys + + +@step(u'I go to the course updates page') +def go_to_uploads(step): + menu_css = 'li.nav-course-courseware' + uploads_css = '.nav-course-courseware-updates' + world.css_find(menu_css).click() + world.css_find(uploads_css).click() + + +@step(u'I add a new update') +def add_update(step): + update_css = '.new-update-button' + world.css_find(update_css).click() + + +@step(u'I make the text "([^"]*)"$') +def change_text(step, text): + text_css = 'div.CodeMirror > div > textarea' + prev_css = 'div.CodeMirror-lines > div > div:last-child > pre' + all_lines = world.css_find(prev_css) + all_text = '' + for i in range(len(all_lines)): + all_text = all_lines[i].html + text_area = world.css_find(text_css) + for i in range(len(all_text)): + text_area._element.send_keys(Keys.END, Keys.BACK_SPACE) + text_area._element.send_keys(text) + save_css = '.save-button' + world.css_find(save_css).click() + + +@step(u'I should( not)? see the update "([^"]*)"$') +def check_update(step, doesnt, text): + update_css = '.update-contents' + update = world.css_find(update_css) + if doesnt: + assert len(update) == 0 or not text in update.html + else: + assert text in update.html + + +@step(u'I click the "([^"]*)" button$') +def click_button(step, edit_delete): + button_css = '.post-preview .%s-button' % edit_delete + world.css_find(button_css).click() + + +@step(u'I make the date "([^"]*)"$') +def change_date(step, new_date): + 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' + world.css_find(save_css).click() + + +@step(u'I should see the 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 edit the handouts') +def edit_handouts(step): + edit_css = '.course-handouts > .edit-button' + world.css_find(edit_css).click() + + +@step(u'I see the handout "([^"]*)"$') +def check_handout(step, handout): + handout_css = '.handouts-content' + handouts = world.css_find(handout_css) + assert handout in handouts.html From 0825319cc7c81f675d0304e49f0654c9a249eaef Mon Sep 17 00:00:00 2001 From: Miles Steele Date: Wed, 12 Jun 2013 11:49:35 -0400 Subject: [PATCH 16/51] fix gradebook tests --- lms/djangoapps/instructor/tests/test_gradebook.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lms/djangoapps/instructor/tests/test_gradebook.py b/lms/djangoapps/instructor/tests/test_gradebook.py index 4b1d22b594..49a3d23e39 100644 --- a/lms/djangoapps/instructor/tests/test_gradebook.py +++ b/lms/djangoapps/instructor/tests/test_gradebook.py @@ -18,6 +18,7 @@ from xmodule.modulestore.django import modulestore USER_COUNT = 11 + @override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) class TestGradebook(ModuleStoreTestCase): grading_policy = None @@ -43,10 +44,7 @@ class TestGradebook(ModuleStoreTestCase): metadata={'graded': True, 'format': 'Homework'} ) - self.users = [ - UserFactory.create(username='robot%d' % i, email='robot+test+%d@edx.org' % i) - for i in xrange(USER_COUNT) - ] + self.users = [UserFactory() for _ in xrange(USER_COUNT)] for user in self.users: CourseEnrollmentFactory.create(user=user, course_id=self.course.id) @@ -74,10 +72,11 @@ class TestGradebook(ModuleStoreTestCase): def test_response_code(self): self.assertEquals(self.response.status_code, 200) + class TestDefaultGradingPolicy(TestGradebook): def test_all_users_listed(self): for user in self.users: - self.assertIn(user.username, self.response.content) + self.assertIn(str(user.username), str(self.response.content)) def test_default_policy(self): # Default >= 50% passes, so Users 5-10 should be passing for Homework 1 [6] @@ -94,6 +93,7 @@ class TestDefaultGradingPolicy(TestGradebook): # One use at the top of the page [1] self.assertEquals(293, self.response.content.count('grade_None')) + class TestLetterCutoffPolicy(TestGradebook): grading_policy = { "GRADER": [ From 1b5050c9ac3abab8aaf49223199ebac55de03831 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Wed, 12 Jun 2013 13:25:33 -0400 Subject: [PATCH 17/51] Changed the following: % to format css_find().click() to its actual function A few name changes as well as step changes Removal of drag testing A refactoring of login and create user. Login will no longer create the specified user --- .../contentstore/features/common.py | 6 +- .../contentstore/features/course-team.py | 38 ++---- .../features/course-updates.feature | 23 ++-- .../contentstore/features/course-updates.py | 120 ++++++++++-------- .../contentstore/features/courses.py | 1 + .../features/static-pages.feature | 10 -- .../contentstore/features/static-pages.py | 19 +-- .../features/studio-overview-togglesection.py | 3 +- .../contentstore/features/upload.py | 1 + common/djangoapps/terrain/ui_helpers.py | 12 -- 10 files changed, 94 insertions(+), 139 deletions(-) diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index a80fbcbb8f..aa4ed56a16 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -58,6 +58,7 @@ def i_have_opened_a_new_course(step): ####### HELPER FUNCTIONS ############## def open_new_course(): world.clear_courses() + create_studio_user() log_into_studio() create_a_course() @@ -90,10 +91,7 @@ def fill_in_course_info( def log_into_studio( uname='robot', email='robot+studio@edx.org', - password='test', - is_staff=False): - - create_studio_user(uname=uname, email=email, is_staff=is_staff) + password='test'): world.browser.cookies.delete() world.visit('/') diff --git a/cms/djangoapps/contentstore/features/course-team.py b/cms/djangoapps/contentstore/features/course-team.py index 7bf538157d..cfcd0e053d 100644 --- a/cms/djangoapps/contentstore/features/course-team.py +++ b/cms/djangoapps/contentstore/features/course-team.py @@ -2,10 +2,10 @@ #pylint: disable=W0621 from lettuce import world, step -from common import create_studio_user, COURSE_NAME +from common import create_studio_user, log_into_studio, COURSE_NAME -PASS = 'test' -EXTENSION = '@edx.org' +PASSWORD = 'test' +EMAIL_EXTENSION = '@edx.org' @step(u'I am viewing the course team settings') @@ -17,49 +17,39 @@ def view_grading_settings(step): @step(u'The user "([^"]*)" exists$') def create_other_user(step, name): - create_studio_user(uname=name, password=PASS, email=(name + EXTENSION)) + 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): - new_user_css = '.new-user-button' - world.css_find(new_user_css).click() + new_user_css = 'a.new-user-button' + world.css_click(new_user_css) - email_css = '.email-input' + email_css = 'input.email-input' f = world.css_find(email_css) - f._element.send_keys(name, EXTENSION) + f._element.send_keys(name, EMAIL_EXTENSION) confirm_css = '#add_user' - world.css_find(confirm_css).click() + world.css_click(confirm_css) @step(u'I delete "([^"]*)" from the course team') def delete_other_user(step, name): - to_delete_css = '.remove-user[data-id="%s%s"]' % (name, EXTENSION,) - world.css_find(to_delete_css).click() + 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): - world.browser.cookies.delete() - world.visit('/') - - signin_css = 'a.action-signin' - world.is_css_present(signin_css) - world.css_click(signin_css) - - login_form = world.browser.find_by_css('form#login_form') - login_form.find_by_name('email').fill(name + EXTENSION) - login_form.find_by_name('password').fill(PASS) - login_form.find_by_name('submit').click() + 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): +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] - if doesnt: + if doesnt_see_course: assert not COURSE_NAME in all_names else: assert COURSE_NAME in all_names diff --git a/cms/djangoapps/contentstore/features/course-updates.feature b/cms/djangoapps/contentstore/features/course-updates.feature index 7266e4f9a6..81714c43ae 100644 --- a/cms/djangoapps/contentstore/features/course-updates.feature +++ b/cms/djangoapps/contentstore/features/course-updates.feature @@ -4,41 +4,34 @@ Feature: Course updates Scenario: Users can add updates Given I have opened a new course in Studio And I go to the course updates page - When I add a new update - And I make the text "Hello" + When I add a new update with the text "Hello" Then I should see the update "Hello" Scenario: Users can edit updates Given I have opened a new course in Studio And I go to the course updates page - When I add a new update - And I make the text "Hello" - And I click the "edit" button - And I make the text "Goodbye" + When I add a new update with the text "Hello" + And I modify the text to "Goodbye" Then I should see the update "Goodbye" Scenario: Users can delete updates Given I have opened a new course in Studio And I go to the course updates page - And I add a new update - And I make the text "Hello" + And I add a new update with the text "Hello" When I will confirm all alerts - And I click the "delete" button + And I delete the update Then I should not see the update "Hello" Scenario: Users can edit update dates Given I have opened a new course in Studio And I go to the course updates page - And I add a new update - And I make the text "Hello" - When I click the "edit" button - And I make the date "June 1, 2013" + And I add a new update with the text "Hello" + When I edit the date to "June 1, 2013" Then I should see the date "June 1, 2013" Scenario: Users can change handouts Given I have opened a new course in Studio And I go to the course updates page - When I edit the handouts - And I make the text "
    Test
" + When I modify the handout to "
    Test
" Then I see the handout "Test" diff --git a/cms/djangoapps/contentstore/features/course-updates.py b/cms/djangoapps/contentstore/features/course-updates.py index 4d0b5ef174..dbb9cbc958 100644 --- a/cms/djangoapps/contentstore/features/course-updates.py +++ b/cms/djangoapps/contentstore/features/course-updates.py @@ -9,18 +9,75 @@ from selenium.webdriver.common.keys import Keys def go_to_uploads(step): menu_css = 'li.nav-course-courseware' uploads_css = '.nav-course-courseware-updates' - world.css_find(menu_css).click() - world.css_find(uploads_css).click() + world.css_click(menu_css) + world.css_click(uploads_css) -@step(u'I add a new update') -def add_update(step): +@step(u'I add a new update with the text "([^"]*)"$') +def add_update(step, text): update_css = '.new-update-button' - world.css_find(update_css).click() + world.css_click(update_css) + change_text(text) -@step(u'I make the text "([^"]*)"$') -def change_text(step, text): +@step(u'I should( not)? see the update "([^"]*)"$') +def check_update(step, doesnt_see_update, text): + update_css = '.update-contents' + update = world.css_find(update_css) + if doesnt_see_update: + assert len(update) == 0 or not text in update.html + else: + assert text in update.html + + +@step(u'I modify the text to "([^"]*)"$') +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): + button_css = '.post-preview .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' + 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' + world.css_click(save_css) + + +@step(u'I should see the 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): + 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): + handout_css = '.handouts-content' + handouts = world.css_find(handout_css) + assert handout in handouts.html + + +def change_text(text): text_css = 'div.CodeMirror > div > textarea' prev_css = 'div.CodeMirror-lines > div > div:last-child > pre' all_lines = world.css_find(prev_css) @@ -32,51 +89,4 @@ def change_text(step, text): text_area._element.send_keys(Keys.END, Keys.BACK_SPACE) text_area._element.send_keys(text) save_css = '.save-button' - world.css_find(save_css).click() - - -@step(u'I should( not)? see the update "([^"]*)"$') -def check_update(step, doesnt, text): - update_css = '.update-contents' - update = world.css_find(update_css) - if doesnt: - assert len(update) == 0 or not text in update.html - else: - assert text in update.html - - -@step(u'I click the "([^"]*)" button$') -def click_button(step, edit_delete): - button_css = '.post-preview .%s-button' % edit_delete - world.css_find(button_css).click() - - -@step(u'I make the date "([^"]*)"$') -def change_date(step, new_date): - 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' - world.css_find(save_css).click() - - -@step(u'I should see the 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 edit the handouts') -def edit_handouts(step): - edit_css = '.course-handouts > .edit-button' - world.css_find(edit_css).click() - - -@step(u'I see the handout "([^"]*)"$') -def check_handout(step, handout): - handout_css = '.handouts-content' - handouts = world.css_find(handout_css) - assert handout in handouts.html + world.css_click(save_css) diff --git a/cms/djangoapps/contentstore/features/courses.py b/cms/djangoapps/contentstore/features/courses.py index a3e838a9d1..5b279d402f 100644 --- a/cms/djangoapps/contentstore/features/courses.py +++ b/cms/djangoapps/contentstore/features/courses.py @@ -10,6 +10,7 @@ from common import * @step('There are no courses$') def no_courses(step): world.clear_courses() + create_studio_user() @step('I click the New Course button$') diff --git a/cms/djangoapps/contentstore/features/static-pages.feature b/cms/djangoapps/contentstore/features/static-pages.feature index 99a4e802dd..9997df69f0 100644 --- a/cms/djangoapps/contentstore/features/static-pages.feature +++ b/cms/djangoapps/contentstore/features/static-pages.feature @@ -22,13 +22,3 @@ Feature: Static Pages When I "edit" the "Empty" page And I change the name to "New" Then I should see a "New" static page - - Scenario: Users can reorder static pages - Given I have opened a new course in Studio - And I go to the static pages page - And I add a new page - And I "edit" the "Empty" page - And I change the name to "New" - And I add a new page - When I move "New" after "Empty" - Then I see the order is "Empty New" diff --git a/cms/djangoapps/contentstore/features/static-pages.py b/cms/djangoapps/contentstore/features/static-pages.py index 2a1110de40..56d12f18aa 100644 --- a/cms/djangoapps/contentstore/features/static-pages.py +++ b/cms/djangoapps/contentstore/features/static-pages.py @@ -50,27 +50,10 @@ def change_name(step, new_name): world.css_find(save_button).click() -@step(u'I move "([^"]*)" after "([^"]*)"$') -def change_list(step, item1, item2): - index1 = get_index(item1) - index2 = get_index(item2) - assert index1 != -1 and index2 != -1 - world.drag_sortable_after(".component", index1, index2, ".ui-sortable") - - -@step(u'I see the order is "([^"]*)"$') -def check_order(step, items): - items = items.split(' ') - name_css = 'section[data-type="HTMLModule"]' - all_elements = world.css_find(name_css) - for i in range(len(items)): - assert all_elements[i].html == '\n %s\n' % items[i] - - def get_index(name): page_name_css = 'section[data-type="HTMLModule"]' all_pages = world.css_find(page_name_css) for i in range(len(all_pages)): - if all_pages[i].html == '\n %s\n' % name: + if all_pages[i].html == '\n {name}\n'.format(name=name): return i return -1 diff --git a/cms/djangoapps/contentstore/features/studio-overview-togglesection.py b/cms/djangoapps/contentstore/features/studio-overview-togglesection.py index 3a39f3cc15..3aca2aee92 100644 --- a/cms/djangoapps/contentstore/features/studio-overview-togglesection.py +++ b/cms/djangoapps/contentstore/features/studio-overview-togglesection.py @@ -50,7 +50,8 @@ def have_a_course_with_two_sections(step): @step(u'I navigate to the course overview page$') def navigate_to_the_course_overview_page(step): - log_into_studio(is_staff=True) + create_studio_user(is_staff=True) + log_into_studio() course_locator = '.class-name' world.css_click(course_locator) diff --git a/cms/djangoapps/contentstore/features/upload.py b/cms/djangoapps/contentstore/features/upload.py index d7a6eba999..16e38e3ff7 100644 --- a/cms/djangoapps/contentstore/features/upload.py +++ b/cms/djangoapps/contentstore/features/upload.py @@ -20,6 +20,7 @@ def upload_file(step, file_name): file_css = '.file-input' upload = world.css_find(file_css) + #uploading the file itself upload._element.send_keys(os.getcwd() + '/' + file_name) close_css = '.close-button' diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py index c59d7030ff..ecd43eb719 100644 --- a/common/djangoapps/terrain/ui_helpers.py +++ b/common/djangoapps/terrain/ui_helpers.py @@ -158,15 +158,3 @@ def click_tools(): tools_css = 'li.nav-course-tools' if world.browser.is_element_present_by_css(tools_css): world.css_click(tools_css) - - -@world.absorb -def drag_sortable_after(item_css, index1, index2, sortable_css): - """ - This is a hack in order to simulate jQuery's sortable list dragging as Selenium cannot currently do it with action_chains - Please note that this is very finnicky with keeping the changes after refreshing the page so be careful when testing persistant changes - Also note that this is mainly for visualization of the sortable list and the true sortable drag and drop fires many events - """ - world.browser.execute_script('$("%(item_css)s:eq(%(index_one)s)").insertAfter($("%(item_css)s:eq(%(index_two)s)"));\ - $("%(sortable_css)s").trigger("sortupdate")' % - {'item_css': item_css, 'index_one': index1, 'index_two': index2, 'sortable_css': sortable_css}) From 6d686dd9f61582c4e8cfa898d0fc28cf15c87719 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Wed, 12 Jun 2013 13:32:46 -0400 Subject: [PATCH 18/51] Changed the way the file path was obtained --- cms/djangoapps/contentstore/features/upload.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/features/upload.py b/cms/djangoapps/contentstore/features/upload.py index 16e38e3ff7..47aa58d71b 100644 --- a/cms/djangoapps/contentstore/features/upload.py +++ b/cms/djangoapps/contentstore/features/upload.py @@ -21,7 +21,7 @@ def upload_file(step, file_name): file_css = '.file-input' upload = world.css_find(file_css) #uploading the file itself - upload._element.send_keys(os.getcwd() + '/' + file_name) + upload._element.send_keys(os.path.dirname(__file__) + '/' + file_name) close_css = '.close-button' world.css_find(close_css).click() From 3c43dbb26fec2cb3a8e6d647518867dbc46fc727 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Wed, 12 Jun 2013 13:36:46 -0400 Subject: [PATCH 19/51] Changed the other % to format --- cms/djangoapps/contentstore/features/common.py | 2 +- cms/djangoapps/contentstore/features/course-team.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index aa4ed56a16..8146c4ab99 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -113,7 +113,7 @@ def create_a_course(): # 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/%s/%s' % (COURSE_NUM, COURSE_NAME.replace(" ", "_"),)) + 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() diff --git a/cms/djangoapps/contentstore/features/course-team.py b/cms/djangoapps/contentstore/features/course-team.py index cfcd0e053d..439eccb265 100644 --- a/cms/djangoapps/contentstore/features/course-team.py +++ b/cms/djangoapps/contentstore/features/course-team.py @@ -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 = '.remove-user[data-id="{name}{extension}"]'.format(name=name, extension=EMAIL_EXTENSION) world.css_click(to_delete_css) From 7fb9bfd685067d51b5131efe1118c31f6b4d7016 Mon Sep 17 00:00:00 2001 From: Miles Steele Date: Thu, 13 Jun 2013 10:25:36 -0400 Subject: [PATCH 20/51] fix test string comparison --- lms/djangoapps/instructor/tests/test_gradebook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/djangoapps/instructor/tests/test_gradebook.py b/lms/djangoapps/instructor/tests/test_gradebook.py index 49a3d23e39..f0633f90c0 100644 --- a/lms/djangoapps/instructor/tests/test_gradebook.py +++ b/lms/djangoapps/instructor/tests/test_gradebook.py @@ -76,7 +76,7 @@ class TestGradebook(ModuleStoreTestCase): class TestDefaultGradingPolicy(TestGradebook): def test_all_users_listed(self): for user in self.users: - self.assertIn(str(user.username), str(self.response.content)) + self.assertIn(user.username, unicode(self.response.content, 'utf-8')) def test_default_policy(self): # Default >= 50% passes, so Users 5-10 should be passing for Homework 1 [6] From 40bf2514abce3d995326329332d21e4d0245c959 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 13 Jun 2013 11:00:45 -0400 Subject: [PATCH 21/51] Added tests for downloading files A few notes: 1. Downloads are done through direct requests. This is due to the difficulty of downloading a file to the correct place 2. Modifiying a file will just change the file to a random 10 character string. 3. The page is reloaded in between uploads. This is due to a current caching bug that is in the process of being looked into and will be updated once it is fixed. --- .../contentstore/features/upload.feature | 26 ++++++++--- .../contentstore/features/upload.py | 45 +++++++++++++++++-- common/test/data/uploads/test | 1 + 3 files changed, 64 insertions(+), 8 deletions(-) create mode 100644 common/test/data/uploads/test diff --git a/cms/djangoapps/contentstore/features/upload.feature b/cms/djangoapps/contentstore/features/upload.feature index 5717cfb907..71b754a3e7 100644 --- a/cms/djangoapps/contentstore/features/upload.feature +++ b/cms/djangoapps/contentstore/features/upload.feature @@ -4,12 +4,28 @@ Feature: Upload Files Scenario: Users can upload files Given I have opened a new course in Studio And I go to the files and uploads page - When I upload the file "upload.feature" - Then I see the file "upload.feature" was uploaded + When I upload the file "test" + Then I see the file "test" was uploaded + And The url for the file "test" is valid Scenario: Users can update files Given I have opened a new course in studio And I go to the files and uploads page - When I upload the file "upload.feature" - And I upload the file "upload.feature" - Then I see only one "upload.feature" + When I upload the file "test" + And I upload the file "test" + Then I see only one "test" + + Scenario: Users can download files + Given I have opened a new course in studio + And I go to the files and uploads page + When I upload the file "test" + Then I can download the correct "test" file + + Scenario: Users can download updated files + Given I have opened a new course in studio + And I go to the files and uploads page + When I upload the file "test" + And I modify "test" + And I reload the page + And I upload the file "test" + Then I can download the correct "test" file diff --git a/cms/djangoapps/contentstore/features/upload.py b/cms/djangoapps/contentstore/features/upload.py index 47aa58d71b..acc889ac8a 100644 --- a/cms/djangoapps/contentstore/features/upload.py +++ b/cms/djangoapps/contentstore/features/upload.py @@ -2,7 +2,13 @@ #pylint: disable=W0621 from lettuce import world, step -import os +from django.conf import settings +import requests +import string +import random + +TEST_ROOT = settings.COMMON_TEST_DATA_ROOT +HTTP_PREFIX = "http://localhost:8001" @step(u'I go to the files and uploads page') @@ -21,18 +27,24 @@ def upload_file(step, file_name): file_css = '.file-input' upload = world.css_find(file_css) #uploading the file itself - upload._element.send_keys(os.path.dirname(__file__) + '/' + file_name) + upload._element.send_keys(TEST_ROOT + '/uploads/' + file_name) close_css = '.close-button' world.css_find(close_css).click() -@step(u'I see the file "([^"]*)" was uploaded') +@step(u'I see the file "([^"]*)" was uploaded$') def check_upload(step, file_name): index = get_index(file_name) assert index != -1 +@step(u'The url for the file "([^"]*)" is valid$') +def check_url(step, file_name): + r = get_file(file_name) + assert r.status_code == 200 + + @step(u'I see only one "([^"]*)"$') def no_duplicate(step, file_name): names_css = '.name-col > a.filename' @@ -44,6 +56,24 @@ def no_duplicate(step, file_name): assert only_one +@step(u'I can download the correct "([^"]*)" file$') +def check_download(step, file_name): + cur_file = open(TEST_ROOT + '/uploads/' + file_name, 'r') + cur_text = cur_file.read() + r = get_file(file_name) + downloaded_text = r.text + assert cur_text == downloaded_text + cur_file.close() + + +@step(u'I modify "([^"]*)"$') +def modify_upload(step, file_name): + new_text = ''.join(random.choice(string.ascii_uppercase + string.digits) for x in range(10)) + cur_file = open(TEST_ROOT + '/uploads/' + file_name, 'w') + cur_file.write(new_text) + cur_file.close() + + def get_index(file_name): names_css = '.name-col > a.filename' all_names = world.css_find(names_css) @@ -51,3 +81,12 @@ def get_index(file_name): if file_name == all_names[i].html: return i return -1 + + +def get_file(file_name): + index = get_index(file_name) + assert index != -1 + + url_css = 'input.embeddable-xml-input' + url = world.css_find(url_css)[index].value + return requests.get(HTTP_PREFIX + url) diff --git a/common/test/data/uploads/test b/common/test/data/uploads/test new file mode 100644 index 0000000000..27bb8ecaac --- /dev/null +++ b/common/test/data/uploads/test @@ -0,0 +1 @@ +R22VMJ2M \ No newline at end of file From ef1f523c5576ce421b26b84eba6768a60b5840d5 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Mon, 17 Jun 2013 14:12:42 -0400 Subject: [PATCH 22/51] Removed _MODULESTORE reference that was doing nothing --- common/djangoapps/terrain/course_helpers.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/common/djangoapps/terrain/course_helpers.py b/common/djangoapps/terrain/course_helpers.py index 23f932bc3b..b843d47934 100644 --- a/common/djangoapps/terrain/course_helpers.py +++ b/common/djangoapps/terrain/course_helpers.py @@ -10,7 +10,7 @@ from django.contrib.auth import authenticate, login from django.contrib.auth.middleware import AuthenticationMiddleware from django.contrib.sessions.middleware import SessionMiddleware from student.models import CourseEnrollment -from xmodule.modulestore.django import _MODULESTORES, modulestore +from xmodule.modulestore.django import modulestore from xmodule.contentstore.django import contentstore from xmodule.templates import update_templates from bs4 import BeautifulSoup @@ -112,7 +112,6 @@ def save_the_course_content(path='/tmp'): u = world.browser.url section_url = u[u.find('courseware/') + 11:] - if not os.path.exists(path): os.makedirs(path) @@ -131,7 +130,6 @@ def clear_courses(): # (though it shouldn't), do this manually # from the bash shell to drop it: # $ mongo test_xmodule --eval "db.dropDatabase()" - _MODULESTORES = {} modulestore().collection.drop() update_templates(modulestore('direct')) contentstore().fs_files.drop() From 25fe8e8070abf0bf1351fd9eedf5b6f0e870bf30 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Mon, 17 Jun 2013 15:54:10 -0400 Subject: [PATCH 23/51] Now using context files and os.path --- .../contentstore/features/upload.py | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/cms/djangoapps/contentstore/features/upload.py b/cms/djangoapps/contentstore/features/upload.py index acc889ac8a..386fc81040 100644 --- a/cms/djangoapps/contentstore/features/upload.py +++ b/cms/djangoapps/contentstore/features/upload.py @@ -6,6 +6,7 @@ from django.conf import settings import requests import string import random +import os TEST_ROOT = settings.COMMON_TEST_DATA_ROOT HTTP_PREFIX = "http://localhost:8001" @@ -27,7 +28,8 @@ def upload_file(step, file_name): file_css = '.file-input' upload = world.css_find(file_css) #uploading the file itself - upload._element.send_keys(TEST_ROOT + '/uploads/' + file_name) + path = os.path.join(TEST_ROOT, 'uploads/', file_name) + upload._element.send_keys(os.path.abspath(path)) close_css = '.close-button' world.css_find(close_css).click() @@ -58,20 +60,20 @@ def no_duplicate(step, file_name): @step(u'I can download the correct "([^"]*)" file$') def check_download(step, file_name): - cur_file = open(TEST_ROOT + '/uploads/' + file_name, 'r') - cur_text = cur_file.read() - r = get_file(file_name) - downloaded_text = r.text - assert cur_text == downloaded_text - cur_file.close() + path = os.path.join(TEST_ROOT, 'uploads/', file_name) + with open(os.path.abspath(path), 'r') as cur_file: + cur_text = cur_file.read() + r = get_file(file_name) + downloaded_text = r.text + assert cur_text == downloaded_text @step(u'I modify "([^"]*)"$') def modify_upload(step, file_name): new_text = ''.join(random.choice(string.ascii_uppercase + string.digits) for x in range(10)) - cur_file = open(TEST_ROOT + '/uploads/' + file_name, 'w') - cur_file.write(new_text) - cur_file.close() + path = os.path.join(TEST_ROOT, 'uploads/', file_name) + with open(os.path.abspath(path), 'w') as cur_file: + cur_file.write(new_text) def get_index(file_name): From 15facf8fc0efb1c2cf4224a0e2504418f053d755 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Mon, 17 Jun 2013 16:48:37 -0400 Subject: [PATCH 24/51] Changed change_text to implement new type_in_codemirror function --- .../contentstore/features/course-updates.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/cms/djangoapps/contentstore/features/course-updates.py b/cms/djangoapps/contentstore/features/course-updates.py index dbb9cbc958..3bbdd75d26 100644 --- a/cms/djangoapps/contentstore/features/course-updates.py +++ b/cms/djangoapps/contentstore/features/course-updates.py @@ -3,6 +3,7 @@ from lettuce import world, step from selenium.webdriver.common.keys import Keys +from common import type_in_codemirror @step(u'I go to the course updates page') @@ -78,15 +79,6 @@ def check_handout(step, handout): def change_text(text): - text_css = 'div.CodeMirror > div > textarea' - prev_css = 'div.CodeMirror-lines > div > div:last-child > pre' - all_lines = world.css_find(prev_css) - all_text = '' - for i in range(len(all_lines)): - all_text = all_lines[i].html - text_area = world.css_find(text_css) - for i in range(len(all_text)): - text_area._element.send_keys(Keys.END, Keys.BACK_SPACE) - text_area._element.send_keys(text) + type_in_codemirror(0, text) save_css = '.save-button' world.css_click(save_css) From 650a2ba423907f099492027fd3ef75e4ca641338 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Tue, 18 Jun 2013 11:20:23 -0400 Subject: [PATCH 25/51] Added deletion tests --- .../contentstore/features/upload.feature | 11 ++++++++-- .../contentstore/features/upload.py | 22 +++++++++++++++---- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/cms/djangoapps/contentstore/features/upload.feature b/cms/djangoapps/contentstore/features/upload.feature index 71b754a3e7..b3c1fc2ce3 100644 --- a/cms/djangoapps/contentstore/features/upload.feature +++ b/cms/djangoapps/contentstore/features/upload.feature @@ -5,7 +5,7 @@ Feature: Upload Files Given I have opened a new course in Studio And I go to the files and uploads page When I upload the file "test" - Then I see the file "test" was uploaded + Then I should see the file "test" was uploaded And The url for the file "test" is valid Scenario: Users can update files @@ -13,7 +13,14 @@ Feature: Upload Files And I go to the files and uploads page When I upload the file "test" And I upload the file "test" - Then I see only one "test" + Then I should see only one "test" + + Scenario: Users can delete uploaded files + Given I have opened a new course in studio + And I go to the files and uploads page + When I upload the file "test" + And I delete the file "test" + Then I should not see the file "test" was uploaded Scenario: Users can download files Given I have opened a new course in studio diff --git a/cms/djangoapps/contentstore/features/upload.py b/cms/djangoapps/contentstore/features/upload.py index 386fc81040..9b049ccc78 100644 --- a/cms/djangoapps/contentstore/features/upload.py +++ b/cms/djangoapps/contentstore/features/upload.py @@ -35,10 +35,13 @@ def upload_file(step, file_name): world.css_find(close_css).click() -@step(u'I see the file "([^"]*)" was uploaded$') -def check_upload(step, file_name): +@step(u'I should( not)? see the file "([^"]*)" was uploaded$') +def check_upload(step, do_not_see_file, file_name): index = get_index(file_name) - assert index != -1 + if do_not_see_file: + assert index == -1 + else: + assert index != -1 @step(u'The url for the file "([^"]*)" is valid$') @@ -47,7 +50,18 @@ def check_url(step, file_name): assert r.status_code == 200 -@step(u'I see only one "([^"]*)"$') +@step(u'I delete the file "([^"]*)"$') +def delete_file(step, file_name): + index = get_index(file_name) + assert index != -1 + delete_css = ".remove-asset-button" + world.css_click(delete_css, index=index) + + prompt_confirm_css = 'li.nav-item > a.action-primary' + world.css_click(prompt_confirm_css) + + +@step(u'I should see only one "([^"]*)"$') def no_duplicate(step, file_name): names_css = '.name-col > a.filename' all_names = world.css_find(names_css) From 9dc126271989441a9612f9b506af2652b7290749 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Tue, 18 Jun 2013 13:58:58 -0400 Subject: [PATCH 26/51] This was causing some failures in the unit tests for some reason --- cms/envs/test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/envs/test.py b/cms/envs/test.py index 89813dd937..954a553e10 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_xcontent', + 'db': 'test_xmodule', }, # allow for additional options that can be keyed on a name, e.g. 'trashcan' 'ADDITIONAL_OPTIONS': { From b9d79aea605722d177dd3d9b56d2bf23b56ec7f1 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 20 Jun 2013 15:55:53 -0400 Subject: [PATCH 27/51] 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 70d48e2e9fd20d7eac0289fccb24210d68c8af06 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 20 Jun 2013 16:40:13 -0400 Subject: [PATCH 28/51] 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 a52d0beb41a75b493ed5d6d3110790a9edd0c505 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 20 Jun 2013 17:00:46 -0400 Subject: [PATCH 29/51] 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 30/51] 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 8201ca5e777f6933a8329aab777b86e33b09f843 Mon Sep 17 00:00:00 2001 From: Sarina Canelake Date: Thu, 20 Jun 2013 18:27:45 -0400 Subject: [PATCH 31/51] 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 32/51] 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 33/51] 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 34/51] 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 bea50efc2651767fd5805f8687a7abafe41824c9 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 21 Jun 2013 14:29:28 -0400 Subject: [PATCH 35/51] 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 e045860cb652686f8ab5bcaff659a636db6f4d32 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 20 Jun 2013 20:36:51 -0400 Subject: [PATCH 36/51] 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 37/51] 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 38/51] 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 39/51] 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 40/51] 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 41/51] 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 42/51] 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 2eefa494b145610eb8b9a2037ee3be7aa0daba51 Mon Sep 17 00:00:00 2001 From: Jason Bau Date: Fri, 21 Jun 2013 23:46:56 -0700 Subject: [PATCH 43/51] 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 ad6e7457625965386931bb8d43836d3362034468 Mon Sep 17 00:00:00 2001 From: cahrens Date: Mon, 24 Jun 2013 09:09:05 -0400 Subject: [PATCH 44/51] 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 45/51] 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 46/51] 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 47/51] 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 48/51] 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 49/51] 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 50/51] 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 d5d495c24d622ca9e1b8b6d0b30164df024230a3 Mon Sep 17 00:00:00 2001 From: Peter Fogg Date: Mon, 24 Jun 2013 16:57:14 -0400 Subject: [PATCH 51/51] 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).