From 15de4120acc713ce3e319b4b75f58828e1069b16 Mon Sep 17 00:00:00 2001 From: marco Date: Thu, 11 Apr 2013 23:20:49 -0400 Subject: [PATCH 001/995] blankslate edited, header bar now with home button --- lms/static/sass/_discussion.scss | 89 +++++++++++++++---- .../discussion/_filter_dropdown.html | 8 +- .../discussion/_thread_list_template.html | 4 + lms/templates/discussion/index.html | 12 ++- 4 files changed, 93 insertions(+), 20 deletions(-) diff --git a/lms/static/sass/_discussion.scss b/lms/static/sass/_discussion.scss index 2f044ca5a3..8b7e30179d 100644 --- a/lms/static/sass/_discussion.scss +++ b/lms/static/sass/_discussion.scss @@ -199,7 +199,7 @@ body.discussion { z-index: 9999; width: 100%; @include box-sizing(border-box); - background: #737373; + background: #797979; border: 1px solid #333; box-shadow: 0 2px 50px rgba(0, 0, 0, .4); } @@ -710,7 +710,7 @@ body.discussion { border-radius: 3px 0 0 0; - .browse, + .home, .browse, .search { position: relative; float: left; @@ -723,9 +723,29 @@ body.discussion { &:hover { background-color: #e9e9e9; } + } - &.is-open { - width: 80%; + .home { + border-radius: 3px 0 0 0; + box-shadow: -1px 0 0 #aaa inset; + cursor: pointer; + + .home-icon { + display: block; + position: absolute; + top: 30%; + left: 30%; + z-index: 100; + width: 25px; + height: 25px; + //margin-left: -17px; + background: url(../images/home-discussion-icon.png) no-repeat; + opacity: 1; + @include transition(none); + } + + .home-btn { + //nothing here yet } } @@ -734,6 +754,7 @@ body.discussion { box-shadow: -1px 0 0 #aaa inset; &.is-open { + width:60%; .browse-topic-drop-btn span { opacity: 1; } @@ -774,6 +795,11 @@ body.discussion { &.is-open { cursor: auto; + width: 60%; + + .home { + width:0%; + } .post-search { padding: 0 10px; @@ -801,7 +827,7 @@ body.discussion { z-index: 50; width: 100%; height: 100%; - border-radius: 3px 0 0 0; + border-radius: 0 0 0 0; border: 1px solid transparent; text-align: center; overflow: hidden; @@ -820,6 +846,9 @@ body.discussion { opacity: 0; @include transition(opacity .2s); } + .drop-arrow { + font-size:16px; + } } .browse-topic-drop-icon { @@ -843,7 +872,7 @@ body.discussion { left: -1px; z-index: 9999; width: 100%; - background: #737373; + background: #797979; border: 1px solid #4b4b4b; border-left: none; border-radius: 0 0 3px 3px; @@ -852,8 +881,16 @@ body.discussion { .browse-topic-drop-menu { max-height: 400px; overflow-y: scroll; + + .drop-menu-meta-category span, + .drop-menu-parent-category span { + margin: 10px 0; + font-size: 14px; + font-weight: 700; + } } + ul { position: inline; } @@ -866,7 +903,7 @@ body.discussion { display: block; padding: 0 20px; border-top: 1px solid #5f5f5f; - font-size: 14px; + font-size: 12px; font-weight: 700; line-height: 22px; color: #fff; @@ -885,7 +922,7 @@ body.discussion { .board-name { float: left; width: 80%; - margin: 13px 0; + margin: 5px 0; color: #fff; } @@ -903,14 +940,14 @@ body.discussion { li li { a { padding-left: 44px; - background: url(../images/nested-icon.png) no-repeat 22px 14px; + background: url(../images/nested-icon.png) no-repeat 22px 5px; } } li li li { a { padding-left: 68px; - background: url(../images/nested-icon.png) no-repeat 46px 14px; + background: url(../images/nested-icon.png) no-repeat 46px 5px; } } } @@ -981,7 +1018,7 @@ body.discussion { min-height: 27px; border-bottom: 1px solid #a3a3a3; @include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0)); - background-color: #aeaeae; + background-color: #aaaaaa; box-shadow: 0 1px 0 rgba(255, 255, 255, .2) inset; span, @@ -1270,10 +1307,32 @@ body.discussion { } } - .blank-slate h1 { - margin-top: 195px; - text-align: center; - color: #ccc; + .blank-slate { + //nothing here + .section { + border-bottom: 1px solid #ccc; + margin-top: 15px; + } + .home-header { + //nothing here + } + + .home-title { + font-size: 18px; + color: #000; + margin-bottom: 5px; + } + .home-description { + font-size: 12px; + line-height: 1; + margin-bottom: 10px; + } + .home-stats { + //nothing + } + .home-emailsettings { + //nothing here + } } .blank-slate, diff --git a/lms/templates/discussion/_filter_dropdown.html b/lms/templates/discussion/_filter_dropdown.html index fef4abb11f..1f59d4235d 100644 --- a/lms/templates/discussion/_filter_dropdown.html +++ b/lms/templates/discussion/_filter_dropdown.html @@ -11,12 +11,12 @@ <%def name="render_entry(entries, entry)"> -
  • ${entry}
  • +
  • ${entry}
  • <%def name="render_category(categories, category)">
  • - ${category} + ${category} @@ -29,12 +29,12 @@ diff --git a/lms/templates/extauth_failure.html b/lms/templates/extauth_failure.html index fa53ab1084..330c63e604 100644 --- a/lms/templates/extauth_failure.html +++ b/lms/templates/extauth_failure.html @@ -2,10 +2,10 @@ "http://www.w3.org/TR/html4/strict.dtd"> - OpenID failed + External Authentication failed -

    OpenID failed

    +

    External Authentication failed

    ${message}

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

    Welcome ${extauth_email}

    +

    Welcome ${extauth_id}

    Enter a public username:

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

    Registration Help

    + % if has_extauth_info is UNDEFINED: +

    Already registered?

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

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

    Welcome ${extauth_email}


    +

    Welcome ${extauth_id}


    Enter a public username:

    - + - + + + % if ask_for_email: + + + % endif + + + % if ask_for_fullname: + + + % endif + % endif diff --git a/lms/urls.py b/lms/urls.py index 1d34ebf3af..6a4819aedb 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -363,6 +363,21 @@ if settings.MITX_FEATURES.get('AUTH_USE_OPENID'): url(r'^openid/logo.gif$', 'django_openid_auth.views.logo', name='openid-logo'), ) +if settings.MITX_FEATURES.get('AUTH_USE_SHIB'): + urlpatterns += ( + url(r'^shib-login/$', 'external_auth.views.shib_login', name='shib-login'), + ) + +if settings.MITX_FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD'): + urlpatterns += ( + url(r'^course_specific_login/(?P[^/]+/[^/]+/[^/]+)/$', + 'external_auth.views.course_specific_login', name='course-specific-login'), + url(r'^course_specific_register/(?P[^/]+/[^/]+/[^/]+)/$', + 'external_auth.views.course_specific_register', name='course-specific-register'), + + ) + + if settings.MITX_FEATURES.get('AUTH_USE_OPENID_PROVIDER'): urlpatterns += ( url(r'^openid/provider/login/$', 'external_auth.views.provider_login', name='openid-provider-login'), diff --git a/lms/wsgi_apache.py b/lms/wsgi_apache_lms.py similarity index 53% rename from lms/wsgi_apache.py rename to lms/wsgi_apache_lms.py index e2d8a23dc0..0f9950ca41 100644 --- a/lms/wsgi_apache.py +++ b/lms/wsgi_apache_lms.py @@ -1,15 +1,12 @@ import os os.environ.setdefault("DJANGO_SETTINGS_MODULE", "lms.envs.aws") +os.environ.setdefault("SERVICE_VARIANT", "lms") + # This application object is used by the development server # as well as any WSGI server configured to use this file. -from django.core.wsgi import WSGIHandler -_application = WSGIHandler() - -def application(environ, start_response): - #copy SERVICE_VARIANT from apache environ to os environ - os.environ.setdefault("SERVICE_VARIANT", environ.get("SERVICE_VARIANT", "lms")) - return _application(environ, start_response) +from django.core.wsgi import get_wsgi_application +application = get_wsgi_application() from django.conf import settings from xmodule.modulestore.django import modulestore From a39a384ed22e9fe82e10293b146c1b14a3f7e787 Mon Sep 17 00:00:00 2001 From: Jason Bau Date: Fri, 14 Jun 2013 15:07:44 -0700 Subject: [PATCH 077/995] Handle the case where an existing user has email returned by shib By linking the users --- common/djangoapps/external_auth/views.py | 39 +++++++++++++++++++++--- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/common/djangoapps/external_auth/views.py b/common/djangoapps/external_auth/views.py index 8288b27ec9..d4a0b56293 100644 --- a/common/djangoapps/external_auth/views.py +++ b/common/djangoapps/external_auth/views.py @@ -147,11 +147,42 @@ def external_login_or_signup(request, internal_user = eamap.user if internal_user is None: - log.debug('No user for %s yet, doing signup' % eamap.external_email) - return signup(request, eamap) + if settings.MITX_FEATURES.get('AUTH_USE_SHIB'): + # if we are using shib, try to link accounts using email + try: + link_user = User.objects.get(email=eamap.external_email) + if not ExternalAuthMap.objects.filter(user=link_user).exists(): + # if there's no pre-existing linked eamap, we link the user + eamap.user = link_user + eamap.save() + internal_user = link_user + log.debug('Linking existing account for %s' % eamap.external_email) + # now pass through to log in + else: + # otherwise, set external_email to '' to ask for a new one at user signup + eamap.external_email = '' + eamap.save() + log.debug('User with external login found for %s, asking for new email during signup' % email) + return signup(request, eamap) + except User.DoesNotExist: + log.debug('No user for %s yet, doing signup' % eamap.external_email) + return signup(request, eamap) + else: + log.debug('No user for %s yet, doing signup' % eamap.external_email) + return signup(request, eamap) - uname = internal_user.username - user = authenticate(username=uname, password=eamap.internal_password) + # We trust shib's authentication, so no need to authenticate using the password again + if settings.MITX_FEATURES.get('AUTH_USE_SHIB'): + user = internal_user + # Assuming this 'AUTHENTICATION_BACKENDS' is set in settings, which I think is safe + if settings.AUTHENTICATION_BACKENDS: + auth_backend = settings.AUTHENTICATION_BACKENDS[0] + else: + auth_backend = 'django.contrib.auth.backends.ModelBackend' + user.backend = auth_backend + else: + uname = internal_user.username + user = authenticate(username=uname, password=eamap.internal_password) if user is None: log.warning("External Auth Login failed for %s / %s" % (uname, eamap.internal_password)) From ca649d3c33a8c660d6bf06eba75d17649b311589 Mon Sep 17 00:00:00 2001 From: Jason Bau Date: Fri, 14 Jun 2013 22:12:35 -0700 Subject: [PATCH 078/995] Turn off Agreement to Terms of Service for Stanford shib As stipulated by Stanford's office of general counsel --- common/djangoapps/external_auth/views.py | 5 +++++ common/djangoapps/student/views.py | 20 +++++++++++++++----- lms/templates/register.html | 5 +++++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/common/djangoapps/external_auth/views.py b/common/djangoapps/external_auth/views.py index d4a0b56293..097cdefe77 100644 --- a/common/djangoapps/external_auth/views.py +++ b/common/djangoapps/external_auth/views.py @@ -239,8 +239,13 @@ def signup(request, eamap=None): 'extauth_email': eamap.external_email, 'extauth_username': username, 'extauth_name': eamap.external_name, + 'ask_for_tos': True, } + # Can't have terms of service for Stanford users, according to Stanford's Office of General Counsel + if settings.MITX_FEATURES['AUTH_USE_SHIB'] and ('stanford' in eamap.external_domain): + context['ask_for_tos'] = False + # detect if full name is blank and ask for it from user context['ask_for_fullname'] = eamap.external_name.strip() == '' diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 98587cd782..8bc29fa671 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -613,17 +613,27 @@ def create_account(request, post_override=None): js['field'] = 'honor_code' return HttpResponse(json.dumps(js)) - if post_vars.get('terms_of_service', 'false') != u'true': - js['value'] = "You must accept the terms of service.".format(field=a) - js['field'] = 'terms_of_service' - return HttpResponse(json.dumps(js)) + # Can't have terms of service for Stanford users, according to Stanford's Office of General Counsel + if settings.MITX_FEATURES.get("AUTH_USE_SHIB") and DoExternalAuth and ("stanford" in eamap.external_domain): + pass + else: + if post_vars.get('terms_of_service', 'false') != u'true': + js['value'] = "You must accept the terms of service.".format(field=a) + js['field'] = 'terms_of_service' + return HttpResponse(json.dumps(js)) # Confirm appropriate fields are there. # TODO: Check e-mail format is correct. # TODO: Confirm e-mail is not from a generic domain (mailinator, etc.)? Not sure if # this is a good idea # TODO: Check password is sane - for a in ['username', 'email', 'name', 'password', 'terms_of_service', 'honor_code']: + + required_post_vars = ['username', 'email', 'name', 'password', 'terms_of_service', 'honor_code'] + if settings.MITX_FEATURES.get("AUTH_USE_SHIB") and DoExternalAuth and ("stanford" in eamap.external_domain): + # Can't have terms of service for Stanford users, according to Stanford's Office of General Counsel + required_post_vars = ['username', 'email', 'name', 'password', 'honor_code'] + + for a in required_post_vars: if len(post_vars[a]) < 2: error_str = {'username': 'Username must be minimum of two characters long.', 'email': 'A properly formatted e-mail is required.', diff --git a/lms/templates/register.html b/lms/templates/register.html index 2cad6955eb..1a42d402e5 100644 --- a/lms/templates/register.html +++ b/lms/templates/register.html @@ -231,11 +231,16 @@
    1. + + % if has_extauth_info is UNDEFINED or ask_for_tos : +
      + % endif +
      <% From 084160c1c9b76e0c09eb6221591503f9e1b1e3f2 Mon Sep 17 00:00:00 2001 From: Jason Bau Date: Wed, 19 Jun 2013 00:16:41 -0700 Subject: [PATCH 079/995] Finishing up tests/modifications per @ormsbee feedback --- .../external_auth/tests/test_shib.py | 196 +++++++++++------- common/djangoapps/external_auth/views.py | 49 +++-- common/djangoapps/student/views.py | 7 +- 3 files changed, 148 insertions(+), 104 deletions(-) diff --git a/common/djangoapps/external_auth/tests/test_shib.py b/common/djangoapps/external_auth/tests/test_shib.py index f342aa4c74..e5059e5635 100644 --- a/common/djangoapps/external_auth/tests/test_shib.py +++ b/common/djangoapps/external_auth/tests/test_shib.py @@ -1,3 +1,4 @@ +# coding=utf-8 """ Tests for Shibboleth Authentication @jbau @@ -6,11 +7,12 @@ import unittest from django.conf import settings from django.http import HttpResponseRedirect -from django.test.client import RequestFactory +from django.test.client import RequestFactory, Client as DjangoTestClient from django.test.utils import override_settings from django.core.urlresolvers import reverse from django.contrib.auth.models import AnonymousUser, User from django.contrib.sessions.backends.base import SessionBase +from django.utils.importlib import import_module from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase @@ -34,23 +36,27 @@ from student.tests.factories import UserFactory IDP = 'https://idp.stanford.edu/' REMOTE_USER = 'test_user@stanford.edu' MAILS = [None, '', 'test_user@stanford.edu'] -GIVENNAMES = [None, '', 'Jason', 'jason; John; bob'] # At Stanford, the givenNames can be a list delimited by ';' -SNS = [None, '', 'Bau', 'bau; smith'] # At Stanford, the sns can be a list delimited by ';' +GIVENNAMES = [None, '', 'Jason', 'jasön; John; bob'] # At Stanford, the givenNames can be a list delimited by ';' +SNS = [None, '', 'Bau', '包; smith'] # At Stanford, the sns can be a list delimited by ';' def gen_all_identities(): - """A generator for all combinations of identity inputs""" + """ + A generator for all combinations of test inputs. + Each generated item is a dict that represents what a shib IDP + could potentially pass to django via request.META, i.e. + setting (or not) request.META['givenName'], etc. + """ def _build_identity_dict(mail, given_name, surname): """ Helper function to return a dict of test identity """ - meta_dict = {} - meta_dict.update({'Shib-Identity-Provider': IDP, - 'REMOTE_USER': REMOTE_USER}) + meta_dict = {'Shib-Identity-Provider': IDP, + 'REMOTE_USER': REMOTE_USER} if mail is not None: - meta_dict.update({'mail': mail}) + meta_dict['mail'] = mail if given_name is not None: - meta_dict.update({'givenName': given_name}) + meta_dict['givenName'] = given_name if surname is not None: - meta_dict.update({'sn': surname}) + meta_dict['sn'] = surname return meta_dict for mail in MAILS: @@ -59,48 +65,84 @@ def gen_all_identities(): yield _build_identity_dict(mail, given_name, surname) -@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE, SESSION_ENGINE='django.contrib.sessions.backends.cache') class ShibSPTest(ModuleStoreTestCase): """ Tests for the Shibboleth SP, which communicates via request.META (Apache environment variables set by mod_shib) """ - factory = RequestFactory() + request_factory = RequestFactory() def setUp(self): self.store = modulestore() + @unittest.skipUnless(settings.MITX_FEATURES.get('AUTH_USE_SHIB'), True) + def test_exception_shib_login(self): + """ + Tests that we get the error page when there is no REMOTE_USER + or Shib-Identity-Provider in request.META + """ + no_remote_user_request = self.request_factory.get('/shib-login') + no_remote_user_request.META.update({'Shib-Identity-Provider': IDP}) + no_remote_user_response = shib_login(no_remote_user_request) + self.assertEqual(no_remote_user_response.status_code, 403) + self.assertIn("identity server did not return your ID information", no_remote_user_response.content) + + no_idp_request = self.request_factory.get('/shib-login') + no_idp_request.META.update({'REMOTE_USER': REMOTE_USER}) + no_idp_response = shib_login(no_idp_request) + self.assertEqual(no_idp_response.status_code, 403) + self.assertIn("identity server did not return your ID information", no_idp_response.content) + + @unittest.skipUnless(settings.MITX_FEATURES.get('AUTH_USE_SHIB'), True) def test_shib_login(self): """ - Tests that a user with a shib ExternalAuthMap gets logged in while when - shib-login is called, while a user without such gets the registration form. + Tests that: + * shib credentials that match an existing ExternalAuthMap with a linked user logs the user in + * shib credentials that match an existing ExternalAuthMap without a linked user and also match the email + of an existing user without an existing ExternalAuthMap links the two and log the user in + * shib credentials that match an existing ExternalAuthMap without a linked user and also match the email + of an existing user that already has an ExternalAuthMap causes an error (403) + * shib credentials that do not match an existing ExternalAuthMap causes the registration form to appear """ - student = UserFactory.create() - extauth = ExternalAuthMap(external_id='testuser@stanford.edu', + user_w_map = UserFactory.create(email='withmap@stanford.edu') + extauth = ExternalAuthMap(external_id='withmap@stanford.edu', external_email='', external_domain='shib:https://idp.stanford.edu/', external_credentials="", - user=student) - student.save() + user=user_w_map) + user_wo_map = UserFactory.create(email='womap@stanford.edu') + user_w_map.save() + user_wo_map.save() extauth.save() idps = ['https://idp.stanford.edu/', 'https://someother.idp.com/'] - remote_users = ['testuser@stanford.edu', 'testuser2@someother_idp.com'] + remote_users = ['withmap@stanford.edu', 'womap@stanford.edu', 'testuser2@someother_idp.com'] for idp in idps: for remote_user in remote_users: - request = self.factory.get('/shib-login') - request.session = SessionBase() # empty session + request = self.request_factory.get('/shib-login') + request.session = import_module(settings.SESSION_ENGINE).SessionStore() # empty session request.META.update({'Shib-Identity-Provider': idp, - 'REMOTE_USER': remote_user}) + 'REMOTE_USER': remote_user, + 'mail': remote_user}) request.user = AnonymousUser() response = shib_login(request) - if idp == "https://idp.stanford.edu" and remote_user == 'testuser@stanford.edu': + if idp == "https://idp.stanford.edu/" and remote_user == 'withmap@stanford.edu': self.assertIsInstance(response, HttpResponseRedirect) - self.assertEqual(request.user, student) + self.assertEqual(request.user, user_w_map) self.assertEqual(response['Location'], '/') + elif idp == "https://idp.stanford.edu/" and remote_user == 'womap@stanford.edu': + self.assertIsNotNone(ExternalAuthMap.objects.get(user=user_wo_map)) + self.assertIsInstance(response, HttpResponseRedirect) + self.assertEqual(request.user, user_wo_map) + self.assertEqual(response['Location'], '/') + elif idp == "https://someother.idp.com/" and remote_user in \ + ['withmap@stanford.edu', 'womap@stanford.edu']: + self.assertEqual(response.status_code, 403) + self.assertIn("You have already created an account using an external login", response.content) else: self.assertEqual(response.status_code, 200) self.assertContains(response, "Register for") @@ -113,10 +155,9 @@ class ShibSPTest(ModuleStoreTestCase): Uses django test client for its session support """ for identity in gen_all_identities(): - self.client.logout() - request_kwargs = {'path': '/shib-login/', 'data': {}, 'follow': False} - request_kwargs.update(identity) - response = self.client.get(**request_kwargs) # identity k/v pairs will show up in request.META + client = DjangoTestClient() + # identity k/v pairs will show up in request.META + response = client.get(path='/shib-login/', data={}, follow=False, **identity) self.assertEquals(response.status_code, 200) mail_input_HTML = '<input class="" id="email" type="email" name="email"' @@ -124,8 +165,8 @@ class ShibSPTest(ModuleStoreTestCase): self.assertContains(response, mail_input_HTML) else: self.assertNotContains(response, mail_input_HTML) - sn_empty = identity.get('sn', '') == '' - given_name_empty = identity.get('givenName', '') == '' + sn_empty = not identity.get('sn') + given_name_empty = not identity.get('givenName') fullname_input_HTML = '<input id="name" type="text" name="name"' if sn_empty and given_name_empty: self.assertContains(response, fullname_input_HTML) @@ -133,7 +174,7 @@ class ShibSPTest(ModuleStoreTestCase): self.assertNotContains(response, fullname_input_HTML) #clean up b/c we don't want existing ExternalAuthMap for the next run - self.client.session['ExternalAuthMap'].delete() + client.session['ExternalAuthMap'].delete() @unittest.skipUnless(settings.MITX_FEATURES.get('AUTH_USE_SHIB'), True) def test_registration_formSubmit(self): @@ -146,10 +187,8 @@ class ShibSPTest(ModuleStoreTestCase): """ for identity in gen_all_identities(): #First we pop the registration form - self.client.logout() - request1_kwargs = {'path': '/shib-login/', 'data': {}, 'follow': False} - request1_kwargs.update(identity) - response1 = self.client.get(**request1_kwargs) + client = DjangoTestClient() + response1 = client.get(path='/shib-login/', data={}, follow=False, **identity) #Then we have the user answer the registration form postvars = {'email': 'post_email@stanford.edu', 'username': 'post_username', @@ -158,8 +197,8 @@ class ShibSPTest(ModuleStoreTestCase): 'terms_of_service': 'true', 'honor_code': 'true'} #use RequestFactory instead of TestClient here because we want access to request.user - request2 = self.factory.post('/create_account', data=postvars) - request2.session = self.client.session + request2 = self.request_factory.post('/create_account', data=postvars) + request2.session = client.session request2.user = AnonymousUser() response2 = create_account(request2) @@ -177,13 +216,12 @@ class ShibSPTest(ModuleStoreTestCase): #check that the created user profile has the right name, either taken from shib or user input profile = UserProfile.objects.get(user=user) - sn_empty = identity.get('sn', '') == '' - given_name_empty = identity.get('givenName', '') == '' + sn_empty = not identity.get('sn') + given_name_empty = not identity.get('givenName') if sn_empty and given_name_empty: self.assertEqual(profile.name, postvars['name']) else: self.assertEqual(profile.name, request2.session['ExternalAuthMap'].external_name) - #clean up for next loop request2.session['ExternalAuthMap'].delete() UserProfile.objects.filter(user=user).delete() @@ -206,12 +244,12 @@ class ShibSPTest(ModuleStoreTestCase): self.store.update_metadata(course.location.url(), metadata) #setting location to test that GET params get passed through - login_request = self.factory.get('/course_specific_login/MITx/999/Robot_Super_Course' + - '?course_id=MITx/999/Robot_Super_Course' + - '&enrollment_action=enroll') - reg_request = self.factory.get('/course_specific_register/MITx/999/Robot_Super_Course' + - '?course_id=MITx/999/course/Robot_Super_Course' + - '&enrollment_action=enroll') + login_request = self.request_factory.get('/course_specific_login/MITx/999/Robot_Super_Course' + + '?course_id=MITx/999/Robot_Super_Course' + + '&enrollment_action=enroll') + reg_request = self.request_factory.get('/course_specific_register/MITx/999/Robot_Super_Course' + + '?course_id=MITx/999/course/Robot_Super_Course' + + '&enrollment_action=enroll') login_response = course_specific_login(login_request, 'MITx/999/Robot_Super_Course') reg_response = course_specific_register(login_request, 'MITx/999/Robot_Super_Course') @@ -241,12 +279,12 @@ class ShibSPTest(ModuleStoreTestCase): # Now test for non-existent course #setting location to test that GET params get passed through - login_request = self.factory.get('/course_specific_login/DNE/DNE/DNE' + - '?course_id=DNE/DNE/DNE' + - '&enrollment_action=enroll') - reg_request = self.factory.get('/course_specific_register/DNE/DNE/DNE' + - '?course_id=DNE/DNE/DNE/Robot_Super_Course' + - '&enrollment_action=enroll') + login_request = self.request_factory.get('/course_specific_login/DNE/DNE/DNE' + + '?course_id=DNE/DNE/DNE' + + '&enrollment_action=enroll') + reg_request = self.request_factory.get('/course_specific_register/DNE/DNE/DNE' + + '?course_id=DNE/DNE/DNE/Robot_Super_Course' + + '&enrollment_action=enroll') login_response = course_specific_login(login_request, 'DNE/DNE/DNE') reg_response = course_specific_register(login_request, 'DNE/DNE/DNE') @@ -270,54 +308,54 @@ class ShibSPTest(ModuleStoreTestCase): """ #create 2 course, one with limited enrollment one without - course1 = CourseFactory.create(org='Stanford', number='123', display_name='Shib Only') - course1.enrollment_domain = 'shib:https://idp.stanford.edu/' - metadata = own_metadata(course1) - metadata['enrollment_domain'] = course1.enrollment_domain - self.store.update_metadata(course1.location.url(), metadata) + shib_course = CourseFactory.create(org='Stanford', number='123', display_name='Shib Only') + shib_course.enrollment_domain = 'shib:https://idp.stanford.edu/' + metadata = own_metadata(shib_course) + metadata['enrollment_domain'] = shib_course.enrollment_domain + self.store.update_metadata(shib_course.location.url(), metadata) - course2 = CourseFactory.create(org='MITx', number='999', display_name='Robot Super Course') - course2.enrollment_domain = '' - metadata = own_metadata(course2) - metadata['enrollment_domain'] = course2.enrollment_domain - self.store.update_metadata(course2.location.url(), metadata) + open_enroll_course = CourseFactory.create(org='MITx', number='999', display_name='Robot Super Course') + open_enroll_course.enrollment_domain = '' + metadata = own_metadata(open_enroll_course) + metadata['enrollment_domain'] = open_enroll_course.enrollment_domain + self.store.update_metadata(open_enroll_course.location.url(), metadata) - # create 3 kinds of students, external_auth matching course1, external_auth not matching, no external auth - student1 = UserFactory.create() - student1.save() + # create 3 kinds of students, external_auth matching shib_course, external_auth not matching, no external auth + shib_student = UserFactory.create() + shib_student.save() extauth = ExternalAuthMap(external_id='testuser@stanford.edu', external_email='', external_domain='shib:https://idp.stanford.edu/', external_credentials="", - user=student1) + user=shib_student) extauth.save() - student2 = UserFactory.create() - student2.username = "teststudent2" - student2.email = "teststudent2@other.edu" - student2.save() + other_ext_student = UserFactory.create() + other_ext_student.username = "teststudent2" + other_ext_student.email = "teststudent2@other.edu" + other_ext_student.save() extauth = ExternalAuthMap(external_id='testuser1@other.edu', external_email='', external_domain='shib:https://other.edu/', external_credentials="", - user=student2) + user=other_ext_student) extauth.save() - student3 = UserFactory.create() - student3.username = "teststudent3" - student3.email = "teststudent3@gmail.com" - student3.save() + int_student = UserFactory.create() + int_student.username = "teststudent3" + int_student.email = "teststudent3@gmail.com" + int_student.save() #Tests the two case for courses, limited and not - for course in [course1, course2]: - for student in [student1, student2, student3]: - request = self.factory.post('/change_enrollment') + for course in [shib_course, open_enroll_course]: + for student in [shib_student, other_ext_student, int_student]: + request = self.request_factory.post('/change_enrollment') request.POST.update({'enrollment_action': 'enroll', 'course_id': course.id}) request.user = student response = change_enrollment(request) #if course is not limited or student has correct shib extauth then enrollment should be allowed - if course is course2 or student is student1: + if course is open_enroll_course or student is shib_student: self.assertEqual(response.status_code, 200) self.assertEqual(CourseEnrollment.objects.filter(user=student, course_id=course.id).count(), 1) #clean up diff --git a/common/djangoapps/external_auth/views.py b/common/djangoapps/external_auth/views.py index 097cdefe77..1ae8edfc52 100644 --- a/common/djangoapps/external_auth/views.py +++ b/common/djangoapps/external_auth/views.py @@ -145,6 +145,7 @@ def external_login_or_signup(request, eamap.save() + log.info("External_Auth login_or_signup for %s : %s : %s : %s" % (external_domain, external_id, email, fullname)) internal_user = eamap.user if internal_user is None: if settings.MITX_FEATURES.get('AUTH_USE_SHIB'): @@ -156,19 +157,21 @@ def external_login_or_signup(request, eamap.user = link_user eamap.save() internal_user = link_user - log.debug('Linking existing account for %s' % eamap.external_email) + log.info('SHIB: Linking existing account for %s' % eamap.external_email) # now pass through to log in else: - # otherwise, set external_email to '' to ask for a new one at user signup - eamap.external_email = '' - eamap.save() - log.debug('User with external login found for %s, asking for new email during signup' % email) - return signup(request, eamap) + # otherwise, there must have been an error, b/c we've already linked a user with these external + # creds + failure_msg = _(dedent(""" + You have already created an account using an external login like WebAuth or Shibboleth. + Please contact %s for support """ + % getattr(settings, 'TECH_SUPPORT_EMAIL', 'techsupport@class.stanford.edu'))) + return default_render_failure(request, failure_msg) except User.DoesNotExist: - log.debug('No user for %s yet, doing signup' % eamap.external_email) + log.info('SHIB: No user for %s yet, doing signup' % eamap.external_email) return signup(request, eamap) else: - log.debug('No user for %s yet, doing signup' % eamap.external_email) + log.info('No user for %s yet, doing signup' % eamap.external_email) return signup(request, eamap) # We trust shib's authentication, so no need to authenticate using the password again @@ -180,6 +183,7 @@ def external_login_or_signup(request, else: auth_backend = 'django.contrib.auth.backends.ModelBackend' user.backend = auth_backend + log.info('SHIB: Logging in linked user %s' % user.email) else: uname = internal_user.username user = authenticate(username=uname, password=eamap.internal_password) @@ -193,14 +197,13 @@ def external_login_or_signup(request, # TODO: improve error page msg = 'Account not yet activated: please look for link in your email' return default_render_failure(request, msg) - login(request, user) request.session.set_expiry(0) # Now to try enrollment # Need to special case Shibboleth here because it logs in via a GET. # testing request.method for extra paranoia - if 'shib:' in external_domain and request.method == 'GET': + if settings.MITX_FEATURES.get('AUTH_USE_SHIB') and 'shib:' in external_domain and request.method == 'GET': enroll_request = make_shib_enrollment_request(request) student_views.try_change_enrollment(enroll_request) else: @@ -256,7 +259,7 @@ def signup(request, eamap=None): except ValidationError: context['ask_for_email'] = True - log.debug('Doing signup for %s' % eamap.external_email) + log.info('EXTAUTH: Doing signup for %s' % eamap.external_id) return student_views.register_user(request, extra_context=context) @@ -370,7 +373,7 @@ def ssl_login(request): # ----------------------------------------------------------------------------- # Shibboleth (Stanford and others. Uses *Apache* environment variables) # ----------------------------------------------------------------------------- -def shib_login(request, retfun=None): +def shib_login(request): """ Uses Apache's REMOTE_USER environment variable as the external id. This in turn typically uses EduPersonPrincipalName @@ -384,29 +387,31 @@ def shib_login(request, retfun=None): """)) if not request.META.get('REMOTE_USER'): + log.exception("SHIB: no REMOTE_USER found in request.META") + return default_render_failure(request, shib_error_msg) + elif not request.META.get('Shib-Identity-Provider'): + log.exception("SHIB: no Shib-Identity-Provider in request.META") return default_render_failure(request, shib_error_msg) else: #if we get here, the user has authenticated properly - attrs = ['REMOTE_USER', 'givenName', 'sn', 'mail', - 'Shib-Identity-Provider'] - shib = {} - - for attr in attrs: - shib[attr] = request.META.get(attr, '') + shib = {attr: request.META.get(attr, '') + for attr in ['REMOTE_USER', 'givenName', 'sn', 'mail', 'Shib-Identity-Provider']} #Clean up first name, last name, and email address #TODO: Make this less hardcoded re: format, but split will work #even if ";" is not present since we are accessing 1st element - shib['sn'] = shib['sn'].split(";")[0].strip().capitalize() - shib['givenName'] = shib['givenName'].split(";")[0].strip().capitalize() + shib['sn'] = shib['sn'].split(";")[0].strip().capitalize().decode('utf-8') + shib['givenName'] = shib['givenName'].split(";")[0].strip().capitalize().decode('utf-8') + + log.info("SHIB creds returned: %r" % shib) return external_login_or_signup(request, external_id=shib['REMOTE_USER'], external_domain="shib:" + shib['Shib-Identity-Provider'], credentials=shib, email=shib['mail'], - fullname="%s %s" % (shib['givenName'], shib['sn']), - retfun=retfun) + fullname=u'%s %s' % (shib['givenName'], shib['sn']), + ) def make_shib_enrollment_request(request): diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 8bc29fa671..0aac873c03 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -599,7 +599,7 @@ def create_account(request, post_override=None): password = eamap.internal_password post_vars = dict(post_vars.items()) post_vars.update(dict(email=email, name=name, password=password)) - log.debug('extauth test: post_vars = %s' % post_vars) + log.info('In create_account with external_auth: post_vars = %s' % post_vars) # Confirm we have a properly formed request for a in ['username', 'email', 'password', 'name']: @@ -699,10 +699,11 @@ def create_account(request, post_override=None): eamap.user = login_user eamap.dtsignup = datetime.datetime.now(UTC) eamap.save() - log.debug('Updated ExternalAuthMap for %s to be %s' % (post_vars['username'], eamap)) + log.info("User registered with external_auth %s" % post_vars['username']) + log.info('Updated ExternalAuthMap for %s to be %s' % (post_vars['username'], eamap)) if settings.MITX_FEATURES.get('BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH'): - log.debug('bypassing activation email') + log.info('bypassing activation email') login_user.is_active = True login_user.save() From 03605ab686f1e567a0c9ac5868f701039ffc6123 Mon Sep 17 00:00:00 2001 From: Calen Pennington <cale@edx.org> Date: Wed, 19 Jun 2013 09:01:20 -0400 Subject: [PATCH 080/995] Don't print error messages if log/db/data directories already exist --- scripts/create-dev-env.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/create-dev-env.sh b/scripts/create-dev-env.sh index ede86b123a..1f3e078107 100755 --- a/scripts/create-dev-env.sh +++ b/scripts/create-dev-env.sh @@ -495,9 +495,9 @@ pip install argcomplete cd $BASE/edx-platform bundle install -mkdir "$BASE/log" || true -mkdir "$BASE/db" || true -mkdir "$BASE/data" || true +mkdir -p "$BASE/log" +mkdir -p "$BASE/db" +mkdir -p "$BASE/data" rake django-admin[syncdb] rake django-admin[migrate] From 2de645599a1a173a8d247c92d79b7e0c9738f31f Mon Sep 17 00:00:00 2001 From: Calen Pennington <cale@edx.org> Date: Wed, 19 Jun 2013 09:01:30 -0400 Subject: [PATCH 081/995] Remove trailing whitespace --- scripts/create-dev-env.sh | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/scripts/create-dev-env.sh b/scripts/create-dev-env.sh index 1f3e078107..cc0efbef0d 100755 --- a/scripts/create-dev-env.sh +++ b/scripts/create-dev-env.sh @@ -73,7 +73,7 @@ change_git_push_defaults() { #Set git push defaults to upstream rather than master output "Changing git defaults" git config --global push.default upstream - + } clone_repos() { @@ -206,10 +206,10 @@ case `uname -s` in distro=`lsb_release -cs` case $distro in - wheezy|jessie|maya|olivia|nadia|precise|quantal) + wheezy|jessie|maya|olivia|nadia|precise|quantal) warning " Debian support is not fully debugged. Assuming you have standard - development packages already working like scipy rvm, the + development packages already working like scipy rvm, the installation should go fine, but this is still a work in progress. Please report issues you have and let us know if you are able to figure @@ -218,7 +218,7 @@ case `uname -s` in Press return to continue or control-C to abort" read dummy - sudo apt-get install git ;; + sudo apt-get install git ;; squeeze|lisa|katya|oneiric|natty|raring) warning " It seems like you're using $distro which has been deprecated. @@ -231,7 +231,7 @@ case `uname -s` in Press return to continue or control-C to abort" read dummy sudo apt-get install git - ;; + ;; *) error "Unsupported distribution - $distro" @@ -283,7 +283,7 @@ clone_repos if [[ -d $BASE/edx-platform/scripts ]]; then output "Installing system-level dependencies" bash $BASE/edx-platform/scripts/install-system-req.sh -else +else error "It appears that our directory structure has changed and somebody failed to update this script. raise an issue on Github and someone should fix it." exit 1 @@ -314,14 +314,14 @@ case `uname -s` in [Ll]inux) warning "Setting up rvm on linux. This is a known pain point. If the script fails here - refer to the following stack overflow question: + refer to the following stack overflow question: http://stackoverflow.com/questions/9056008/installed-ruby-1-9-3-with-rvm-but-command-line-doesnt-show-ruby-v/9056395#9056395" sudo apt-get --purge remove ruby-rvm sudo rm -rf /usr/share/ruby-rvm /etc/rvmrc /etc/profile.d/rvm.sh curl -sL https://get.rvm.io | bash -s stable --ruby --autolibs=enable --auto-dotfiles ;; esac - + # Ensure we have RVM available as a shell function so that it can mess # with the environment and set everything up properly. The RVM install From fc6043876a7652a7fde90a1985cc2d5a71e5f383 Mon Sep 17 00:00:00 2001 From: Calen Pennington <cale@edx.org> Date: Wed, 19 Jun 2013 09:01:52 -0400 Subject: [PATCH 082/995] Install python and node prerequisites before trying to run django-admin --- scripts/create-dev-env.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/create-dev-env.sh b/scripts/create-dev-env.sh index cc0efbef0d..d3b7715904 100755 --- a/scripts/create-dev-env.sh +++ b/scripts/create-dev-env.sh @@ -494,6 +494,7 @@ cd $BASE pip install argcomplete cd $BASE/edx-platform bundle install +rake install_prereqs mkdir -p "$BASE/log" mkdir -p "$BASE/db" From 7b074424b540db5ad9b2c0c5840de04162680134 Mon Sep 17 00:00:00 2001 From: David Ormsbee <dave@edx.org> Date: Wed, 19 Jun 2013 14:39:02 -0400 Subject: [PATCH 083/995] Initialize MakoMiddleware manually during certificate grading runs. Without this, problems fail to load because they can't find how to render themselves, and the certificate generation grading run will get an inaccurately low count of the possible points a user could get (anything they didn't see will be omitted), inflating their grade during certificate calculation and making it inconsistent with their Progress page. --- lms/djangoapps/certificates/queue.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/certificates/queue.py b/lms/djangoapps/certificates/queue.py index b4632ce9ab..af1037f903 100644 --- a/lms/djangoapps/certificates/queue.py +++ b/lms/djangoapps/certificates/queue.py @@ -3,6 +3,7 @@ from certificates.models import certificate_status_for_student from certificates.models import CertificateStatuses as status from certificates.models import CertificateWhitelist +from mitxmako.middleware import MakoMiddleware from courseware import grades, courses from django.test.client import RequestFactory from capa.xqueue_interface import XQueueInterface @@ -51,6 +52,14 @@ class XQueueCertInterface(object): """ def __init__(self, request=None): + # MakoMiddleware Note: + # Line below has the side-effect of writing to a module level lookup + # table that will allow problems to render themselves. If this is not + # present, problems that a student hasn't seen will error when loading, + # causing the grading system to under-count the possible score and + # inflate their grade. This dependency is bad and was probably recently + # introduced. This is the bandage until we can trace the root cause. + m = MakoMiddleware() # Get basic auth (username/password) for # xqueue connection if it's in the settings @@ -161,6 +170,10 @@ class XQueueCertInterface(object): cert, created = GeneratedCertificate.objects.get_or_create( user=student, course_id=course_id) + # Needed + self.request.user = student + self.request.session = {} + grade = grades.grade(student, self.request, course) is_whitelisted = self.whitelist.filter( user=student, course_id=course_id, whitelist=True).exists() @@ -211,5 +224,5 @@ class XQueueCertInterface(object): (error, msg) = self.xqueue_interface.send_to_queue( header=xheader, body=json.dumps(contents)) if error: - logger.critical('Unable to add a request to the queue') + logger.critical('Unable to add a request to the queue: {} {}'.format(error, msg)) raise Exception('Unable to send queue message') From 142762c1374dbafd8cb1264cd334a9cf0d1d148d Mon Sep 17 00:00:00 2001 From: Ned Batchelder <ned@nedbatchelder.com> Date: Fri, 14 Jun 2013 15:03:57 -0400 Subject: [PATCH 084/995] Rearrange pylintrc a little bit. --- pylintrc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pylintrc b/pylintrc index d4085379b4..0d4ce31185 100644 --- a/pylintrc +++ b/pylintrc @@ -36,8 +36,9 @@ load-plugins= disable= # Never going to use these # C0301: Line too long -# W0142: Used * or ** magic # W0141: Used builtin function 'map' +# W0142: Used * or ** magic + C0301,W0141,W0142, # Might use these when the code is in better shape # C0302: Too many lines in module @@ -50,7 +51,7 @@ disable= # R0912: Too many branches # R0913: Too many arguments # R0914: Too many local variables - C0301,C0302,W0141,W0142,R0201,R0901,R0902,R0903,R0904,R0911,R0912,R0913,R0914 + C0302,R0201,R0901,R0902,R0903,R0904,R0911,R0912,R0913,R0914 [REPORTS] From c53fff9ff4a12bac48425623a0ff455acaf0dead Mon Sep 17 00:00:00 2001 From: Ned Batchelder <ned@nedbatchelder.com> Date: Fri, 14 Jun 2013 15:24:29 -0400 Subject: [PATCH 085/995] Tell pylint to shut up about us telling it to shut up. --- pylintrc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pylintrc b/pylintrc index 0d4ce31185..af958e4af4 100644 --- a/pylintrc +++ b/pylintrc @@ -35,10 +35,11 @@ load-plugins= # it should appear only once). disable= # Never going to use these +# I0011: Locally disabling W0232 # C0301: Line too long # W0141: Used builtin function 'map' # W0142: Used * or ** magic - C0301,W0141,W0142, + I0011,C0301,W0141,W0142, # Might use these when the code is in better shape # C0302: Too many lines in module From 181b1e979b22a119220c4e211f86f7f9d4cdf38c Mon Sep 17 00:00:00 2001 From: Ned Batchelder <ned@nedbatchelder.com> Date: Fri, 14 Jun 2013 13:30:47 -0400 Subject: [PATCH 086/995] Remove unused imports from common, as reported by pylint. --- common/djangoapps/cache_toolbox/core.py | 1 - common/djangoapps/course_groups/cohorts.py | 3 +-- common/djangoapps/course_groups/views.py | 10 ++-------- common/djangoapps/mitxmako/makoloader.py | 1 - common/djangoapps/status/status.py | 1 - .../student/management/commands/6002exportusers.py | 5 ----- .../student/management/commands/6002importusers.py | 6 ------ .../student/management/commands/assigngroups.py | 5 ----- .../management/commands/create_random_users.py | 4 +--- .../student/management/commands/emaillist.py | 5 ----- .../student/management/commands/massemail.py | 5 ----- .../student/management/commands/massemailtxt.py | 3 --- .../student/management/commands/pearson_dump.py | 2 +- .../management/commands/pearson_import_conf_zip.py | 5 +---- .../management/commands/tests/test_pearson.py | 2 +- .../student/management/commands/userinfo.py | 5 ----- common/djangoapps/student/views.py | 8 ++------ common/djangoapps/terrain/browser.py | 6 +++--- common/djangoapps/terrain/course_helpers.py | 3 +-- common/djangoapps/terrain/steps.py | 2 +- common/djangoapps/track/middleware.py | 2 -- common/djangoapps/util/models.py | 2 -- common/djangoapps/util/tests/test_memcache.py | 1 - common/djangoapps/util/tests/test_submit_feedback.py | 1 - common/djangoapps/util/views.py | 12 ++---------- common/lib/capa/capa/checker.py | 1 - common/lib/capa/capa/customrender.py | 2 -- common/lib/capa/capa/responsetypes.py | 1 - common/lib/capa/capa/tests/test_html_render.py | 1 - common/lib/capa/capa/util.py | 2 +- common/lib/chem/chem/chemcalc.py | 11 +---------- common/lib/symmath/symmath/formula.py | 4 +--- common/lib/symmath/symmath/symmath_check.py | 4 ---- .../xmodule/xmodule/modulestore/tests/factories.py | 1 - .../xmodule/modulestore/tests/test_modulestore.py | 4 ++-- .../xmodule/xmodule/modulestore/tests/test_mongo.py | 1 - .../combined_open_ended_modulev1.py | 3 --- .../grading_service_module.py | 1 - .../open_ended_image_submission.py | 2 -- .../open_ended_grading_classes/open_ended_module.py | 2 -- .../open_ended_grading_classes/openendedchild.py | 6 ------ common/lib/xmodule/xmodule/progress.py | 1 - common/lib/xmodule/xmodule/schematic_module.py | 1 - common/lib/xmodule/xmodule/template_module.py | 1 - common/lib/xmodule/xmodule/tests/test_html_module.py | 1 - .../lib/xmodule/xmodule/tests/test_peer_grading.py | 4 ---- .../xmodule/xmodule/tests/test_randomize_module.py | 10 +--------- common/lib/xmodule/xmodule/tests/test_stringify.py | 2 +- .../xmodule/xmodule/tests/test_util_open_ended.py | 2 +- common/lib/xmodule/xmodule/timelimit_module.py | 1 - 50 files changed, 24 insertions(+), 145 deletions(-) diff --git a/common/djangoapps/cache_toolbox/core.py b/common/djangoapps/cache_toolbox/core.py index a9c7002aa6..9a7be940b8 100644 --- a/common/djangoapps/cache_toolbox/core.py +++ b/common/djangoapps/cache_toolbox/core.py @@ -12,7 +12,6 @@ from django.core.cache import cache from django.db import DEFAULT_DB_ALIAS from . import app_settings -from xmodule.contentstore.content import StaticContent def get_instance(model, instance_or_pk, timeout=None, using=None): diff --git a/common/djangoapps/course_groups/cohorts.py b/common/djangoapps/course_groups/cohorts.py index 7924012bfe..d2c7e3a782 100644 --- a/common/djangoapps/course_groups/cohorts.py +++ b/common/djangoapps/course_groups/cohorts.py @@ -3,7 +3,6 @@ This file contains the logic for cohort groups, as exposed internally to the forums, and to the cohort admin views. """ -from django.contrib.auth.models import User from django.http import Http404 import logging import random @@ -27,7 +26,7 @@ def local_random(): """ # ironic, isn't it? global _local_random - + if _local_random is None: _local_random = random.Random() diff --git a/common/djangoapps/course_groups/views.py b/common/djangoapps/course_groups/views.py index 6d5ac43fb0..764f6c301d 100644 --- a/common/djangoapps/course_groups/views.py +++ b/common/djangoapps/course_groups/views.py @@ -1,24 +1,18 @@ from django_future.csrf import ensure_csrf_cookie -from django.contrib.auth.decorators import login_required from django.views.decorators.http import require_POST from django.contrib.auth.models import User -from django.core.context_processors import csrf from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.core.urlresolvers import reverse -from django.http import HttpResponse, HttpResponseForbidden, Http404 -from django.shortcuts import redirect +from django.http import HttpResponse import json import logging import re from courseware.courses import get_course_with_access -from mitxmako.shortcuts import render_to_response, render_to_string +from mitxmako.shortcuts import render_to_response -from .models import CourseUserGroup from . import cohorts -import track.views - log = logging.getLogger(__name__) diff --git a/common/djangoapps/mitxmako/makoloader.py b/common/djangoapps/mitxmako/makoloader.py index 6b6b31d464..06ae2219e6 100644 --- a/common/djangoapps/mitxmako/makoloader.py +++ b/common/djangoapps/mitxmako/makoloader.py @@ -7,7 +7,6 @@ from django.template.loaders.filesystem import Loader as FilesystemLoader from django.template.loaders.app_directories import Loader as AppDirectoriesLoader from mitxmako.template import Template -import mitxmako.middleware import tempdir diff --git a/common/djangoapps/status/status.py b/common/djangoapps/status/status.py index deacd9c631..b3ffd6a84c 100644 --- a/common/djangoapps/status/status.py +++ b/common/djangoapps/status/status.py @@ -6,7 +6,6 @@ from django.conf import settings import json import logging import os -import sys log = logging.getLogger(__name__) diff --git a/common/djangoapps/student/management/commands/6002exportusers.py b/common/djangoapps/student/management/commands/6002exportusers.py index 31d8092d3f..a92bb0a60c 100644 --- a/common/djangoapps/student/management/commands/6002exportusers.py +++ b/common/djangoapps/student/management/commands/6002exportusers.py @@ -11,12 +11,7 @@ import datetime import json -import os.path - -from lxml import etree - from django.core.management.base import BaseCommand -from django.conf import settings from django.contrib.auth.models import User from student.models import UserProfile diff --git a/common/djangoapps/student/management/commands/6002importusers.py b/common/djangoapps/student/management/commands/6002importusers.py index 64be84d910..1f98bd7522 100644 --- a/common/djangoapps/student/management/commands/6002importusers.py +++ b/common/djangoapps/student/management/commands/6002importusers.py @@ -3,17 +3,11 @@ ## See export for more info -import datetime import json import dateutil.parser -import os.path - -from lxml import etree - from django.core.management.base import BaseCommand -from django.conf import settings from django.contrib.auth.models import User from student.models import UserProfile diff --git a/common/djangoapps/student/management/commands/assigngroups.py b/common/djangoapps/student/management/commands/assigngroups.py index 5269c8690e..3e36bf3129 100644 --- a/common/djangoapps/student/management/commands/assigngroups.py +++ b/common/djangoapps/student/management/commands/assigngroups.py @@ -1,9 +1,4 @@ -import os.path - -from lxml import etree - from django.core.management.base import BaseCommand -from django.conf import settings from django.contrib.auth.models import User import mitxmako.middleware as middleware diff --git a/common/djangoapps/student/management/commands/create_random_users.py b/common/djangoapps/student/management/commands/create_random_users.py index 70374d02f2..3000c86601 100644 --- a/common/djangoapps/student/management/commands/create_random_users.py +++ b/common/djangoapps/student/management/commands/create_random_users.py @@ -2,9 +2,7 @@ ## A script to create some dummy users from django.core.management.base import BaseCommand -from django.conf import settings -from django.contrib.auth.models import User -from student.models import UserProfile, CourseEnrollment +from student.models import CourseEnrollment from student.views import _do_create_account, get_random_post_override diff --git a/common/djangoapps/student/management/commands/emaillist.py b/common/djangoapps/student/management/commands/emaillist.py index 4011c41bd2..d3911927ac 100644 --- a/common/djangoapps/student/management/commands/emaillist.py +++ b/common/djangoapps/student/management/commands/emaillist.py @@ -1,9 +1,4 @@ -import os.path - -from lxml import etree - from django.core.management.base import BaseCommand -from django.conf import settings from django.contrib.auth.models import User import mitxmako.middleware as middleware diff --git a/common/djangoapps/student/management/commands/massemail.py b/common/djangoapps/student/management/commands/massemail.py index c6f6e5f6d4..1bb65fd169 100644 --- a/common/djangoapps/student/management/commands/massemail.py +++ b/common/djangoapps/student/management/commands/massemail.py @@ -1,9 +1,4 @@ -import os.path - -from lxml import etree - from django.core.management.base import BaseCommand -from django.conf import settings from django.contrib.auth.models import User import mitxmako.middleware as middleware diff --git a/common/djangoapps/student/management/commands/massemailtxt.py b/common/djangoapps/student/management/commands/massemailtxt.py index 4ea75f972b..fec354e974 100644 --- a/common/djangoapps/student/management/commands/massemailtxt.py +++ b/common/djangoapps/student/management/commands/massemailtxt.py @@ -1,11 +1,8 @@ import os.path import time -from lxml import etree - from django.core.management.base import BaseCommand from django.conf import settings -from django.contrib.auth.models import User import mitxmako.middleware as middleware diff --git a/common/djangoapps/student/management/commands/pearson_dump.py b/common/djangoapps/student/management/commands/pearson_dump.py index 2aade8cf5f..0c9e215f77 100644 --- a/common/djangoapps/student/management/commands/pearson_dump.py +++ b/common/djangoapps/student/management/commands/pearson_dump.py @@ -2,7 +2,7 @@ from optparse import make_option from json import dump from datetime import datetime -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import BaseCommand from student.models import TestCenterRegistration diff --git a/common/djangoapps/student/management/commands/pearson_import_conf_zip.py b/common/djangoapps/student/management/commands/pearson_import_conf_zip.py index 2339383719..1e06a0931a 100644 --- a/common/djangoapps/student/management/commands/pearson_import_conf_zip.py +++ b/common/djangoapps/student/management/commands/pearson_import_conf_zip.py @@ -3,11 +3,8 @@ import csv from zipfile import ZipFile, is_zipfile from time import strptime, strftime -from collections import OrderedDict from datetime import datetime -from os.path import isdir -from optparse import make_option -from dogapi import dog_http_api, dog_stats_api +from dogapi import dog_http_api from django.core.management.base import BaseCommand, CommandError from django.conf import settings diff --git a/common/djangoapps/student/management/commands/tests/test_pearson.py b/common/djangoapps/student/management/commands/tests/test_pearson.py index 65d628fba0..ca6e20673b 100644 --- a/common/djangoapps/student/management/commands/tests/test_pearson.py +++ b/common/djangoapps/student/management/commands/tests/test_pearson.py @@ -14,7 +14,7 @@ from django.test import TestCase from django.core.management import call_command from nose.plugins.skip import SkipTest -from student.models import User, TestCenterRegistration, TestCenterUser, get_testcenter_registration +from student.models import User, TestCenterUser, get_testcenter_registration log = logging.getLogger(__name__) diff --git a/common/djangoapps/student/management/commands/userinfo.py b/common/djangoapps/student/management/commands/userinfo.py index e458995284..5467db1733 100644 --- a/common/djangoapps/student/management/commands/userinfo.py +++ b/common/djangoapps/student/management/commands/userinfo.py @@ -1,9 +1,4 @@ -import os.path - -from lxml import etree - from django.core.management.base import BaseCommand -from django.conf import settings from django.contrib.auth.models import User import mitxmako.middleware as middleware diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index f129f1b4b1..de3e52b080 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -4,7 +4,6 @@ import json import logging import random import string -import sys import urllib import uuid import time @@ -20,9 +19,9 @@ from django.core.mail import send_mail from django.core.urlresolvers import reverse from django.core.validators import validate_email, validate_slug, ValidationError from django.db import IntegrityError, transaction -from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseNotAllowed, HttpResponseRedirect, Http404 +from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseNotAllowed, Http404 from django.shortcuts import redirect -from django_future.csrf import ensure_csrf_cookie, csrf_exempt +from django_future.csrf import ensure_csrf_cookie from django.utils.http import cookie_date from mitxmako.shortcuts import render_to_response, render_to_string @@ -39,14 +38,11 @@ from certificates.models import CertificateStatuses, certificate_status_for_stud from xmodule.course_module import CourseDescriptor from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.django import modulestore -from xmodule.modulestore import Location from collections import namedtuple from courseware.courses import get_courses, sort_by_announcement from courseware.access import has_access -from courseware.views import get_module_for_descriptor, jump_to -from courseware.model_data import ModelDataCache from statsd import statsd from pytz import UTC diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index d2a9480b35..d237edc4b7 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -4,7 +4,6 @@ Browser set up for acceptance tests. #pylint: disable=E1101 #pylint: disable=W0613 -#pylint: disable=W0611 from lettuce import before, after, world from splinter.browser import Browser @@ -15,8 +14,9 @@ from selenium.common.exceptions import WebDriverException # Let the LMS and CMS do their one-time setup # For example, setting up mongo caches -from lms import one_time_startup -from cms import one_time_startup +# These names aren't used, but do important work on import. +from lms import one_time_startup # pylint: disable=W0611 +from cms import one_time_startup # pylint: disable=W0611 # There is an import issue when using django-staticfiles with lettuce # Lettuce assumes that we are using django.contrib.staticfiles, diff --git a/common/djangoapps/terrain/course_helpers.py b/common/djangoapps/terrain/course_helpers.py index fc666d7904..fbc9409e7b 100644 --- a/common/djangoapps/terrain/course_helpers.py +++ b/common/djangoapps/terrain/course_helpers.py @@ -1,7 +1,7 @@ # pylint: disable=C0111 # pylint: disable=W0621 -from lettuce import world, step +from lettuce import world from .factories import * from django.conf import settings from django.http import HttpRequest @@ -15,7 +15,6 @@ from xmodule.templates import update_templates from bs4 import BeautifulSoup import os.path from urllib import quote_plus -from lettuce.django import django_url @world.absorb diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index 6e512982b7..f31be894f9 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -15,7 +15,7 @@ from lettuce import world, step from .course_helpers import * from .ui_helpers import * from lettuce.django import django_url -from nose.tools import assert_equals, assert_in +from nose.tools import assert_equals from logging import getLogger logger = getLogger(__name__) diff --git a/common/djangoapps/track/middleware.py b/common/djangoapps/track/middleware.py index 52d914aeef..7fc02d9969 100644 --- a/common/djangoapps/track/middleware.py +++ b/common/djangoapps/track/middleware.py @@ -1,7 +1,5 @@ import json -from django.conf import settings - import views diff --git a/common/djangoapps/util/models.py b/common/djangoapps/util/models.py index 71a8362390..6b20219993 100644 --- a/common/djangoapps/util/models.py +++ b/common/djangoapps/util/models.py @@ -1,3 +1 @@ -from django.db import models - # Create your models here. diff --git a/common/djangoapps/util/tests/test_memcache.py b/common/djangoapps/util/tests/test_memcache.py index de8d352c38..60b3a0d0cc 100644 --- a/common/djangoapps/util/tests/test_memcache.py +++ b/common/djangoapps/util/tests/test_memcache.py @@ -4,7 +4,6 @@ Tests for memcache in util app from django.test import TestCase from django.core.cache import get_cache -from django.conf import settings from util.memcache import safe_key diff --git a/common/djangoapps/util/tests/test_submit_feedback.py b/common/djangoapps/util/tests/test_submit_feedback.py index b66d3d642b..6461ffa8b7 100644 --- a/common/djangoapps/util/tests/test_submit_feedback.py +++ b/common/djangoapps/util/tests/test_submit_feedback.py @@ -1,6 +1,5 @@ """Tests for the Zendesk""" -from django.conf import settings from django.contrib.auth.models import AnonymousUser from django.http import Http404 from django.test import TestCase diff --git a/common/djangoapps/util/views.py b/common/djangoapps/util/views.py index aa592d25e8..851202caec 100644 --- a/common/djangoapps/util/views.py +++ b/common/djangoapps/util/views.py @@ -1,20 +1,12 @@ -import datetime import json import logging -import pprint import sys from django.conf import settings -from django.contrib.auth.models import User -from django.core.context_processors import csrf -from django.core.mail import send_mail from django.core.validators import ValidationError, validate_email -from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed, HttpResponseServerError -from django.shortcuts import redirect -from django_future.csrf import ensure_csrf_cookie +from django.http import Http404, HttpResponse, HttpResponseNotAllowed from dogapi import dog_stats_api -from mitxmako.shortcuts import render_to_response, render_to_string -from urllib import urlencode +from mitxmako.shortcuts import render_to_response import zendesk import calc diff --git a/common/lib/capa/capa/checker.py b/common/lib/capa/capa/checker.py index 15358aac9e..87cf68d230 100755 --- a/common/lib/capa/capa/checker.py +++ b/common/lib/capa/capa/checker.py @@ -10,7 +10,6 @@ import sys from path import path from cStringIO import StringIO -from collections import defaultdict from .calc import UndefinedVariable from .capa_problem import LoncapaProblem diff --git a/common/lib/capa/capa/customrender.py b/common/lib/capa/capa/customrender.py index 60d3ce578b..9d7ff719ac 100644 --- a/common/lib/capa/capa/customrender.py +++ b/common/lib/capa/capa/customrender.py @@ -10,8 +10,6 @@ from .registry import TagRegistry import logging import re -import shlex # for splitting quoted strings -import json from lxml import etree import xml.sax.saxutils as saxutils diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 80227490da..be70e3866c 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -11,7 +11,6 @@ Used by capa_problem.py # standard library imports import abc import cgi -import hashlib import inspect import json import logging diff --git a/common/lib/capa/capa/tests/test_html_render.py b/common/lib/capa/capa/tests/test_html_render.py index 62605b48f5..9bc326d7b9 100644 --- a/common/lib/capa/capa/tests/test_html_render.py +++ b/common/lib/capa/capa/tests/test_html_render.py @@ -2,7 +2,6 @@ import unittest from lxml import etree import os import textwrap -import json import mock diff --git a/common/lib/capa/capa/util.py b/common/lib/capa/capa/util.py index ec43da6093..433e99171d 100644 --- a/common/lib/capa/capa/util.py +++ b/common/lib/capa/capa/util.py @@ -1,4 +1,4 @@ -from calc import evaluator, UndefinedVariable +from calc import evaluator from cmath import isinf #----------------------------------------------------------------------------- diff --git a/common/lib/chem/chem/chemcalc.py b/common/lib/chem/chem/chemcalc.py index 5b80005044..612e63c0f0 100644 --- a/common/lib/chem/chem/chemcalc.py +++ b/common/lib/chem/chem/chemcalc.py @@ -1,16 +1,7 @@ from __future__ import division -import copy from fractions import Fraction -import logging -import math -import operator -import re -import numpy -import numbers -import scipy.constants -from pyparsing import (Literal, Keyword, Word, nums, StringEnd, Optional, - Forward, OneOrMore, ParseException) +from pyparsing import (Literal, StringEnd, OneOrMore, ParseException) import nltk from nltk.tree import Tree diff --git a/common/lib/symmath/symmath/formula.py b/common/lib/symmath/symmath/formula.py index 8369baa27c..a926d9ae45 100644 --- a/common/lib/symmath/symmath/formula.py +++ b/common/lib/symmath/symmath/formula.py @@ -10,7 +10,6 @@ # Provides sympy representation. import os -import sys import string import re import logging @@ -25,8 +24,7 @@ from sympy.physics.quantum.state import * # from sympy.core.operations import LatticeOp # import sympy.physics.quantum.qubit -import urllib -from xml.sax.saxutils import escape, unescape +from xml.sax.saxutils import unescape import sympy import unicodedata from lxml import etree diff --git a/common/lib/symmath/symmath/symmath_check.py b/common/lib/symmath/symmath/symmath_check.py index 65a17883f5..3f09ebf659 100644 --- a/common/lib/symmath/symmath/symmath_check.py +++ b/common/lib/symmath/symmath/symmath_check.py @@ -8,10 +8,6 @@ # # Takes in math expressions given as Presentation MathML (from ASCIIMathML), converts to Content MathML using SnuggleTeX -import os -import sys -import string -import re import traceback from .formula import * import logging diff --git a/common/lib/xmodule/xmodule/modulestore/tests/factories.py b/common/lib/xmodule/xmodule/modulestore/tests/factories.py index 99c5ec2c91..0a62849d8d 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/factories.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/factories.py @@ -1,5 +1,4 @@ from factory import Factory, lazy_attribute_sequence, lazy_attribute -from time import gmtime from uuid import uuid4 from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_modulestore.py b/common/lib/xmodule/xmodule/modulestore/tests/test_modulestore.py index 469eedac05..1e2035075a 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_modulestore.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_modulestore.py @@ -1,6 +1,6 @@ -from nose.tools import assert_equals, assert_raises, assert_not_equals, with_setup +from nose.tools import assert_equals, assert_raises -from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem +from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.search import path_to_location diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py index 07e6124537..c5ef0d751a 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py @@ -1,6 +1,5 @@ import pymongo -from mock import Mock from nose.tools import assert_equals, assert_raises, assert_not_equals, assert_false from pprint import pprint diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py index 01be4c61ab..9fc438d4c0 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py @@ -1,13 +1,10 @@ import json import logging from lxml import etree -from lxml.html import rewrite_links from xmodule.timeinfo import TimeInfo from xmodule.capa_module import ComplexEncoder -from xmodule.editing_module import EditingDescriptor from xmodule.progress import Progress from xmodule.stringify import stringify_children -from xmodule.xml_module import XmlDescriptor import self_assessment_module import open_ended_module from .combined_open_ended_rubric import CombinedOpenEndedRubric, GRADER_TYPE_IMAGE_DICT, HUMAN_GRADER_TYPE, LEGEND_LIST diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/grading_service_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/grading_service_module.py index 3e3f943cd7..6857876703 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/grading_service_module.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/grading_service_module.py @@ -3,7 +3,6 @@ import json import logging import requests from requests.exceptions import RequestException, ConnectionError, HTTPError -import sys from .combined_open_ended_rubric import CombinedOpenEndedRubric from lxml import etree diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_image_submission.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_image_submission.py index 2eb9502269..ea5c3b3527 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_image_submission.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_image_submission.py @@ -14,9 +14,7 @@ from urlparse import urlparse import requests from boto.s3.connection import S3Connection from boto.s3.key import Key -import pickle import logging -import re log = logging.getLogger(__name__) diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py index 1e5b1b233b..2ac55a8318 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py @@ -11,10 +11,8 @@ from lxml import etree import capa.xqueue_interface as xqueue_interface from xmodule.capa_module import ComplexEncoder -from xmodule.editing_module import EditingDescriptor from xmodule.progress import Progress from xmodule.stringify import stringify_children -from xmodule.xml_module import XmlDescriptor from capa.util import * import openendedchild diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py index b5d4e1b676..4f524d2cd7 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py @@ -3,14 +3,8 @@ import logging from lxml.html.clean import Cleaner, autolink_html import re -from xmodule.capa_module import ComplexEncoder import open_ended_image_submission -from xmodule.editing_module import EditingDescriptor -from xmodule.html_checker import check_html from xmodule.progress import Progress -from xmodule.stringify import stringify_children -from xmodule.xml_module import XmlDescriptor -from xmodule.modulestore import Location from capa.util import * from .peer_grading_service import PeerGradingService, MockPeerGradingService import controller_query_service diff --git a/common/lib/xmodule/xmodule/progress.py b/common/lib/xmodule/xmodule/progress.py index 7adbb02646..bad5105fd0 100644 --- a/common/lib/xmodule/xmodule/progress.py +++ b/common/lib/xmodule/xmodule/progress.py @@ -13,7 +13,6 @@ For most subclassing needs, you should only need to reimplement frac() and __str__(). ''' -from collections import namedtuple import numbers diff --git a/common/lib/xmodule/xmodule/schematic_module.py b/common/lib/xmodule/xmodule/schematic_module.py index d15d629c24..83bcc5351d 100644 --- a/common/lib/xmodule/xmodule/schematic_module.py +++ b/common/lib/xmodule/xmodule/schematic_module.py @@ -1,4 +1,3 @@ -import json from .x_module import XModule, XModuleDescriptor diff --git a/common/lib/xmodule/xmodule/template_module.py b/common/lib/xmodule/xmodule/template_module.py index 9a9666c0b6..bf8f616913 100644 --- a/common/lib/xmodule/xmodule/template_module.py +++ b/common/lib/xmodule/xmodule/template_module.py @@ -3,7 +3,6 @@ from xmodule.raw_module import RawDescriptor from lxml import etree from mako.template import Template from xmodule.modulestore.django import modulestore -import logging class CustomTagModule(XModule): diff --git a/common/lib/xmodule/xmodule/tests/test_html_module.py b/common/lib/xmodule/xmodule/tests/test_html_module.py index e0a49ed98f..4fe0242378 100644 --- a/common/lib/xmodule/xmodule/tests/test_html_module.py +++ b/common/lib/xmodule/xmodule/tests/test_html_module.py @@ -3,7 +3,6 @@ import unittest from mock import Mock from xmodule.html_module import HtmlModule -from xmodule.modulestore import Location from . import get_test_system diff --git a/common/lib/xmodule/xmodule/tests/test_peer_grading.py b/common/lib/xmodule/xmodule/tests/test_peer_grading.py index 3e1a578118..c386f77e9b 100644 --- a/common/lib/xmodule/xmodule/tests/test_peer_grading.py +++ b/common/lib/xmodule/xmodule/tests/test_peer_grading.py @@ -2,10 +2,6 @@ import unittest from xmodule.modulestore import Location from .import get_test_system from test_util_open_ended import MockQueryDict, DummyModulestore -import json - -from xmodule.peer_grading_module import PeerGradingModule, PeerGradingDescriptor -from xmodule.open_ended_grading_classes.grading_service_module import GradingServiceError import logging diff --git a/common/lib/xmodule/xmodule/tests/test_randomize_module.py b/common/lib/xmodule/xmodule/tests/test_randomize_module.py index 81935c4013..81ba45b56c 100644 --- a/common/lib/xmodule/xmodule/tests/test_randomize_module.py +++ b/common/lib/xmodule/xmodule/tests/test_randomize_module.py @@ -1,11 +1,6 @@ import unittest -from time import strptime -from fs.memoryfs import MemoryFS - -from mock import Mock, patch - -from xmodule.modulestore.xml import ImportSystem, XMLModuleStore +from .test_course_module import DummySystem as DummyImportSystem ORG = 'test_org' COURSE = 'test_course' @@ -13,9 +8,6 @@ COURSE = 'test_course' START = '2013-01-01T01:00:00' -from .test_course_module import DummySystem as DummyImportSystem - - class RandomizeModuleTestCase(unittest.TestCase): """Make sure the randomize module works""" @staticmethod diff --git a/common/lib/xmodule/xmodule/tests/test_stringify.py b/common/lib/xmodule/xmodule/tests/test_stringify.py index e44b93b0b8..6c2e44eed5 100644 --- a/common/lib/xmodule/xmodule/tests/test_stringify.py +++ b/common/lib/xmodule/xmodule/tests/test_stringify.py @@ -1,4 +1,4 @@ -from nose.tools import assert_equals, assert_true, assert_false +from nose.tools import assert_equals from lxml import etree from xmodule.stringify import stringify_children diff --git a/common/lib/xmodule/xmodule/tests/test_util_open_ended.py b/common/lib/xmodule/xmodule/tests/test_util_open_ended.py index 9dbb17ae2f..63fb4631c9 100644 --- a/common/lib/xmodule/xmodule/tests/test_util_open_ended.py +++ b/common/lib/xmodule/xmodule/tests/test_util_open_ended.py @@ -1,6 +1,6 @@ from .import get_test_system from xmodule.modulestore import Location -from xmodule.modulestore.xml import ImportSystem, XMLModuleStore +from xmodule.modulestore.xml import XMLModuleStore from xmodule.tests.test_export import DATA_DIR OPEN_ENDED_GRADING_INTERFACE = { diff --git a/common/lib/xmodule/xmodule/timelimit_module.py b/common/lib/xmodule/xmodule/timelimit_module.py index 6be14e7574..9446176f01 100644 --- a/common/lib/xmodule/xmodule/timelimit_module.py +++ b/common/lib/xmodule/xmodule/timelimit_module.py @@ -1,4 +1,3 @@ -import json import logging from lxml import etree From df6d3f9b2f9f9023b4f2710c8c1ee5c05aeef9b1 Mon Sep 17 00:00:00 2001 From: Ned Batchelder <ned@nedbatchelder.com> Date: Mon, 17 Jun 2013 20:43:18 -0400 Subject: [PATCH 087/995] Fix strings that should be raw. --- cms/djangoapps/models/settings/course_details.py | 4 ++-- common/djangoapps/student/management/commands/set_staff.py | 2 +- common/djangoapps/student/views.py | 6 +++--- common/djangoapps/terrain/steps.py | 2 +- common/lib/capa/capa/capa_problem.py | 4 ++-- common/lib/capa/capa/customrender.py | 4 ++-- common/lib/capa/capa/inputtypes.py | 2 +- common/lib/capa/capa/responsetypes.py | 5 ++--- common/lib/symmath/symmath/formula.py | 4 ++-- common/lib/xmodule/xmodule/modulestore/xml.py | 2 +- common/lib/xmodule/xmodule/tests/test_stringify.py | 2 +- lms/djangoapps/course_wiki/views.py | 2 +- lms/djangoapps/foldit/views.py | 2 +- 13 files changed, 20 insertions(+), 21 deletions(-) diff --git a/cms/djangoapps/models/settings/course_details.py b/cms/djangoapps/models/settings/course_details.py index 3f0c87917a..884a4e4fef 100644 --- a/cms/djangoapps/models/settings/course_details.py +++ b/cms/djangoapps/models/settings/course_details.py @@ -153,9 +153,9 @@ class CourseDetails(object): if not raw_video: return None - keystring_matcher = re.search('(?<=embed/)[a-zA-Z0-9_-]+', raw_video) + keystring_matcher = re.search(r'(?<=embed/)[a-zA-Z0-9_-]+', raw_video) if keystring_matcher is None: - keystring_matcher = re.search('<?=\d+:[a-zA-Z0-9_-]+', raw_video) + keystring_matcher = re.search(r'<?=\d+:[a-zA-Z0-9_-]+', raw_video) if keystring_matcher: return keystring_matcher.group(0) diff --git a/common/djangoapps/student/management/commands/set_staff.py b/common/djangoapps/student/management/commands/set_staff.py index 30d0483f50..869e37f13b 100644 --- a/common/djangoapps/student/management/commands/set_staff.py +++ b/common/djangoapps/student/management/commands/set_staff.py @@ -26,7 +26,7 @@ class Command(BaseCommand): raise CommandError('Usage is set_staff {0}'.format(self.args)) for user in args: - if re.match('[^@]+@[^@]+\.[^@]+', user): + if re.match(r'[^@]+@[^@]+\.[^@]+', user): try: v = User.objects.get(email=user) except: diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index de3e52b080..4da7b9d789 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -3,6 +3,7 @@ import feedparser import json import logging import random +import re import string import urllib import uuid @@ -95,9 +96,8 @@ def course_from_id(course_id): course_loc = CourseDescriptor.id_to_location(course_id) return modulestore().get_instance(course_id, course_loc) -import re -day_pattern = re.compile('\s\d+,\s') -multimonth_pattern = re.compile('\s?\-\s?\S+\s') +day_pattern = re.compile(r'\s\d+,\s') +multimonth_pattern = re.compile(r'\s?\-\s?\S+\s') def get_date_for_press(publish_date): diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index f31be894f9..e69476a5b7 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -21,7 +21,7 @@ from logging import getLogger logger = getLogger(__name__) -@step(u'I wait (?:for )?"(\d+)" seconds?$') +@step(r'I wait (?:for )?"(\d+)" seconds?$') def wait(step, seconds): world.wait(seconds) diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index 2a9f3d82a3..d620bac60a 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -103,8 +103,8 @@ class LoncapaProblem(object): self.input_state = state.get('input_state', {}) # Convert startouttext and endouttext to proper <text></text> - problem_text = re.sub("startouttext\s*/", "text", problem_text) - problem_text = re.sub("endouttext\s*/", "/text", problem_text) + problem_text = re.sub(r"startouttext\s*/", "text", problem_text) + problem_text = re.sub(r"endouttext\s*/", "/text", problem_text) self.problem_text = problem_text # parse problem XML file into an element tree diff --git a/common/lib/capa/capa/customrender.py b/common/lib/capa/capa/customrender.py index 9d7ff719ac..f7d586c9d5 100644 --- a/common/lib/capa/capa/customrender.py +++ b/common/lib/capa/capa/customrender.py @@ -26,7 +26,7 @@ class MathRenderer(object): tags = ['math'] def __init__(self, system, xml): - ''' + r''' Render math using latex-like formatting. Examples: @@ -41,7 +41,7 @@ class MathRenderer(object): self.system = system self.xml = xml - mathstr = re.sub('\$(.*)\$', r'[mathjaxinline]\1[/mathjaxinline]', xml.text) + mathstr = re.sub(r'\$(.*)\$', r'[mathjaxinline]\1[/mathjaxinline]', xml.text) mtag = 'mathjax' if not r'\displaystyle' in mathstr: mtag += 'inline' diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 446b832dd7..f026568da1 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -856,7 +856,7 @@ class ImageInput(InputTypeBase): """ if value is of the form [x,y] then parse it and send along coordinates of previous answer """ - m = re.match('\[([0-9]+),([0-9]+)]', + m = re.match(r'\[([0-9]+),([0-9]+)]', self.value.strip().replace(' ', '')) if m: # Note: we subtract 15 to compensate for the size of the dot on the screen. diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index be70e3866c..97319bdb9e 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1902,8 +1902,7 @@ class ImageResponse(LoncapaResponse): if not given: # No answer to parse. Mark as incorrect and move on continue # parse given answer - m = re.match( - '\[([0-9]+),([0-9]+)]', given.strip().replace(' ', '')) + m = re.match(r'\[([0-9]+),([0-9]+)]', given.strip().replace(' ', '')) if not m: raise Exception('[capamodule.capa.responsetypes.imageinput] ' 'error grading %s (input=%s)' % (aid, given)) @@ -1918,7 +1917,7 @@ class ImageResponse(LoncapaResponse): # parse expected answer # TODO: Compile regexp on file load m = re.match( - '[\(\[]([0-9]+),([0-9]+)[\)\]]-[\(\[]([0-9]+),([0-9]+)[\)\]]', + r'[\(\[]([0-9]+),([0-9]+)[\)\]]-[\(\[]([0-9]+),([0-9]+)[\)\]]', solution_rectangle.strip().replace(' ', '')) if not m: msg = 'Error in problem specification! cannot parse rectangle in %s' % ( diff --git a/common/lib/symmath/symmath/formula.py b/common/lib/symmath/symmath/formula.py index a926d9ae45..ca4e20ace3 100644 --- a/common/lib/symmath/symmath/formula.py +++ b/common/lib/symmath/symmath/formula.py @@ -50,7 +50,7 @@ class dot(sympy.operations.LatticeOp): # my dot product def _print_dot(self, expr): - return '{((%s) \cdot (%s))}' % (expr.args[0], expr.args[1]) + return r'{((%s) \cdot (%s))}' % (expr.args[0], expr.args[1]) LatexPrinter._print_dot = _print_dot @@ -202,7 +202,7 @@ class formula(object): return xml def preprocess_pmathml(self, xml): - ''' + r''' Pre-process presentation MathML from ASCIIMathML to make it more acceptable for SnuggleTeX, and also to accomodate some sympy conventions (eg hat(i) for \hat{i}). diff --git a/common/lib/xmodule/xmodule/modulestore/xml.py b/common/lib/xmodule/xmodule/modulestore/xml.py index a704fc2ae8..ef5fa617de 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml.py +++ b/common/lib/xmodule/xmodule/modulestore/xml.py @@ -38,7 +38,7 @@ log = logging.getLogger(__name__) # into the cms from xml def clean_out_mako_templating(xml_string): xml_string = xml_string.replace('%include', 'include') - xml_string = re.sub("(?m)^\s*%.*$", '', xml_string) + xml_string = re.sub(r"(?m)^\s*%.*$", '', xml_string) return xml_string diff --git a/common/lib/xmodule/xmodule/tests/test_stringify.py b/common/lib/xmodule/xmodule/tests/test_stringify.py index 6c2e44eed5..49852ee233 100644 --- a/common/lib/xmodule/xmodule/tests/test_stringify.py +++ b/common/lib/xmodule/xmodule/tests/test_stringify.py @@ -12,7 +12,7 @@ def test_stringify(): def test_stringify_again(): - html = """<html name="Voltage Source Answer" >A voltage source is non-linear! + html = r"""<html name="Voltage Source Answer" >A voltage source is non-linear! <div align="center"> <img src="/static/images/circuits/voltage-source.png"/> \(V=V_C\) diff --git a/lms/djangoapps/course_wiki/views.py b/lms/djangoapps/course_wiki/views.py index 6ab106ed70..74ef7d4a74 100644 --- a/lms/djangoapps/course_wiki/views.py +++ b/lms/djangoapps/course_wiki/views.py @@ -49,7 +49,7 @@ def course_wiki_redirect(request, course_id): if not course_slug: log.exception("This course is improperly configured. The slug cannot be empty.") valid_slug = False - if re.match('^[-\w\.]+$', course_slug) is None: + if re.match(r'^[-\w\.]+$', course_slug) is None: log.exception("This course is improperly configured. The slug can only contain letters, numbers, periods or hyphens.") valid_slug = False diff --git a/lms/djangoapps/foldit/views.py b/lms/djangoapps/foldit/views.py index da361a2a82..76d9bfff98 100644 --- a/lms/djangoapps/foldit/views.py +++ b/lms/djangoapps/foldit/views.py @@ -46,7 +46,7 @@ def foldit_ops(request): # To allow for fixes without breaking this, the regex should only # match unquoted strings, a = re.compile(r':([a-zA-Z]*),') - puzzle_scores_json = re.sub(a, ':"\g<1>",', puzzle_scores_json) + puzzle_scores_json = re.sub(a, r':"\g<1>",', puzzle_scores_json) puzzle_scores = json.loads(puzzle_scores_json) responses.append(save_scores(request.user, puzzle_scores)) From 61b53713d2e1d40b6f15a2a51c6dd8e303f04e27 Mon Sep 17 00:00:00 2001 From: Ned Batchelder <ned@nedbatchelder.com> Date: Tue, 18 Jun 2013 22:29:53 -0400 Subject: [PATCH 088/995] Remove unused imports from lms, as detected by pylint. --- lms/djangoapps/circuit/models.py | 3 --- lms/djangoapps/circuit/views.py | 5 +---- lms/djangoapps/courseware/access.py | 1 - lms/djangoapps/courseware/courses.py | 6 ------ .../courseware/management/commands/clean_xml.py | 3 --- .../courseware/management/commands/metadata_to_json.py | 1 - lms/djangoapps/courseware/module_render.py | 1 - lms/djangoapps/courseware/tabs.py | 7 ------- lms/djangoapps/courseware/tests/test_access.py | 2 +- lms/djangoapps/courseware/tests/test_masquerade.py | 2 +- lms/djangoapps/courseware/tests/test_module_render.py | 1 - lms/djangoapps/dashboard/models.py | 2 -- lms/djangoapps/dashboard/views.py | 5 +---- lms/djangoapps/debug/models.py | 2 -- lms/djangoapps/debug/views.py | 2 +- lms/djangoapps/django_comment_client/helpers.py | 3 --- .../management/commands/show_permissions.py | 1 - lms/djangoapps/django_comment_client/mustache_helpers.py | 1 - lms/djangoapps/django_comment_client/permissions.py | 5 ----- lms/djangoapps/django_comment_client/tests/test_models.py | 3 +-- lms/djangoapps/foldit/models.py | 3 --- lms/djangoapps/foldit/tests.py | 1 - .../instructor/management/commands/compute_grades.py | 8 -------- lms/djangoapps/instructor/offline_gradecalc.py | 6 +----- lms/djangoapps/instructor/tests/test_enrollment.py | 1 - lms/djangoapps/instructor/tests/test_gradebook.py | 4 +--- lms/djangoapps/instructor/tests/test_xss.py | 1 - lms/djangoapps/instructor_task/tests/test_integration.py | 1 - lms/djangoapps/instructor_task/tests/test_tasks.py | 2 +- .../management/commands/generate_serial_numbers.py | 2 -- .../licenses/management/commands/import_serial_numbers.py | 1 - .../lms_migration/management/commands/create_groups.py | 5 +---- .../lms_migration/management/commands/create_user.py | 1 - .../management/commands/manage_course_groups.py | 8 -------- lms/djangoapps/lms_migration/migrate.py | 1 - lms/djangoapps/notes/tests.py | 2 -- lms/djangoapps/notes/views.py | 1 - lms/djangoapps/open_ended_grading/staff_grading.py | 1 - lms/djangoapps/open_ended_grading/tests.py | 1 - lms/djangoapps/open_ended_grading/views.py | 1 - .../management/commands/init_psychometrics.py | 4 ---- lms/djangoapps/static_template_view/models.py | 2 -- lms/djangoapps/staticbook/models.py | 2 -- lms/djangoapps/staticbook/views.py | 1 - lms/envs/common.py | 2 +- lms/envs/dev_edx4edx.py | 1 - lms/envs/dev_ike.py | 1 - lms/lib/comment_client/comment_client.py | 2 ++ lms/lib/perfstats/models.py | 2 -- lms/one_time_startup.py | 3 +-- lms/urls.py | 3 ++- 51 files changed, 16 insertions(+), 114 deletions(-) diff --git a/lms/djangoapps/circuit/models.py b/lms/djangoapps/circuit/models.py index 21a70bcb25..8da678f08a 100644 --- a/lms/djangoapps/circuit/models.py +++ b/lms/djangoapps/circuit/models.py @@ -1,7 +1,4 @@ -import uuid - from django.db import models -from django.contrib.auth.models import User class ServerCircuit(models.Model): diff --git a/lms/djangoapps/circuit/views.py b/lms/djangoapps/circuit/views.py index 40a31a2e3a..cc85c2a452 100644 --- a/lms/djangoapps/circuit/views.py +++ b/lms/djangoapps/circuit/views.py @@ -1,13 +1,10 @@ import json -import os import xml.etree.ElementTree -from django.conf import settings from django.http import Http404 from django.http import HttpResponse -from django.shortcuts import redirect -from mitxmako.shortcuts import render_to_response, render_to_string +from mitxmako.shortcuts import render_to_response from .models import ServerCircuit diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py index 07987a8edf..e25f44b939 100644 --- a/lms/djangoapps/courseware/access.py +++ b/lms/djangoapps/courseware/access.py @@ -2,7 +2,6 @@ Ideally, it will be the only place that needs to know about any special settings like DISABLE_START_DATES""" import logging -import time from datetime import datetime, timedelta from functools import partial diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index 3e1162bc03..71c9630964 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -1,14 +1,9 @@ from collections import defaultdict from fs.errors import ResourceNotFoundError -from functools import wraps import logging import inspect -from lxml.html import rewrite_links - from path import path -from django.conf import settings -from django.core.urlresolvers import reverse from django.http import Http404 from .module_render import get_module @@ -18,7 +13,6 @@ from xmodule.modulestore.django import modulestore from xmodule.contentstore.content import StaticContent from xmodule.modulestore.xml import XMLModuleStore from xmodule.modulestore.exceptions import ItemNotFoundError -from xmodule.x_module import XModule from courseware.model_data import ModelDataCache from static_replace import replace_static_urls from courseware.access import has_access diff --git a/lms/djangoapps/courseware/management/commands/clean_xml.py b/lms/djangoapps/courseware/management/commands/clean_xml.py index 1989361b85..45674f66e0 100644 --- a/lms/djangoapps/courseware/management/commands/clean_xml.py +++ b/lms/djangoapps/courseware/management/commands/clean_xml.py @@ -2,15 +2,12 @@ import os import sys import traceback -from filecmp import dircmp from fs.osfs import OSFS from path import path -from lxml import etree from django.core.management.base import BaseCommand from xmodule.modulestore.xml import XMLModuleStore -from xmodule.errortracker import make_error_tracker def traverse_tree(course): diff --git a/lms/djangoapps/courseware/management/commands/metadata_to_json.py b/lms/djangoapps/courseware/management/commands/metadata_to_json.py index 58d087c316..a910db7028 100644 --- a/lms/djangoapps/courseware/management/commands/metadata_to_json.py +++ b/lms/djangoapps/courseware/management/commands/metadata_to_json.py @@ -2,7 +2,6 @@ A script to walk a course xml tree, generate a dictionary of all the metadata, and print it out as a json dict. """ -import os import sys import json diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index ab0306ed2e..3ffb1d1b1d 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -1,6 +1,5 @@ import json import logging -import pyparsing import re import sys import static_replace diff --git a/lms/djangoapps/courseware/tabs.py b/lms/djangoapps/courseware/tabs.py index 42b1c05743..149542c344 100644 --- a/lms/djangoapps/courseware/tabs.py +++ b/lms/djangoapps/courseware/tabs.py @@ -11,23 +11,16 @@ actually generates the CourseTab. from collections import namedtuple import logging -import json from django.conf import settings from django.core.urlresolvers import reverse -from fs.errors import ResourceNotFoundError - from courseware.access import has_access -from lxml.html import rewrite_links from .module_render import get_module from courseware.access import has_access from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore -from xmodule.modulestore.xml import XMLModuleStore -from xmodule.x_module import XModule -from student.models import unique_id_for_user from courseware.model_data import ModelDataCache from open_ended_grading import open_ended_notifications diff --git a/lms/djangoapps/courseware/tests/test_access.py b/lms/djangoapps/courseware/tests/test_access.py index 34d064971f..f93fa0d659 100644 --- a/lms/djangoapps/courseware/tests/test_access.py +++ b/lms/djangoapps/courseware/tests/test_access.py @@ -1,4 +1,4 @@ -from mock import Mock, patch +from mock import Mock from django.test import TestCase diff --git a/lms/djangoapps/courseware/tests/test_masquerade.py b/lms/djangoapps/courseware/tests/test_masquerade.py index f9ddf88b5f..47d437a316 100644 --- a/lms/djangoapps/courseware/tests/test_masquerade.py +++ b/lms/djangoapps/courseware/tests/test_masquerade.py @@ -12,7 +12,7 @@ from django.test.utils import override_settings from django.core.urlresolvers import reverse -from django.contrib.auth.models import User, Group +from django.contrib.auth.models import Group from courseware.access import _course_staff_group_name from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE, get_user from xmodule.modulestore.django import modulestore diff --git a/lms/djangoapps/courseware/tests/test_module_render.py b/lms/djangoapps/courseware/tests/test_module_render.py index 94ab4b7e94..775b6ff0fc 100644 --- a/lms/djangoapps/courseware/tests/test_module_render.py +++ b/lms/djangoapps/courseware/tests/test_module_render.py @@ -8,7 +8,6 @@ from django.test import TestCase from django.test.client import RequestFactory from django.test.utils import override_settings -from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.django import modulestore import courseware.module_render as render from courseware.tests.tests import LoginEnrollmentTestCase diff --git a/lms/djangoapps/dashboard/models.py b/lms/djangoapps/dashboard/models.py index 71a8362390..6b20219993 100644 --- a/lms/djangoapps/dashboard/models.py +++ b/lms/djangoapps/dashboard/models.py @@ -1,3 +1 @@ -from django.db import models - # Create your models here. diff --git a/lms/djangoapps/dashboard/views.py b/lms/djangoapps/dashboard/views.py index 266e769db5..e04588fff4 100644 --- a/lms/djangoapps/dashboard/views.py +++ b/lms/djangoapps/dashboard/views.py @@ -1,11 +1,8 @@ -# Create your views here. -import json -from datetime import datetime from django.http import Http404 from mitxmako.shortcuts import render_to_response from django.db import connection -from student.models import CourseEnrollment, CourseEnrollmentAllowed +from student.models import CourseEnrollment from django.contrib.auth.models import User diff --git a/lms/djangoapps/debug/models.py b/lms/djangoapps/debug/models.py index 71a8362390..6b20219993 100644 --- a/lms/djangoapps/debug/models.py +++ b/lms/djangoapps/debug/models.py @@ -1,3 +1 @@ -from django.db import models - # Create your models here. diff --git a/lms/djangoapps/debug/views.py b/lms/djangoapps/debug/views.py index c1d4155fdd..317ebcada9 100644 --- a/lms/djangoapps/debug/views.py +++ b/lms/djangoapps/debug/views.py @@ -5,7 +5,7 @@ import traceback from django.http import Http404 from django.contrib.auth.decorators import login_required -from django_future.csrf import ensure_csrf_cookie, csrf_exempt +from django_future.csrf import ensure_csrf_cookie from mitxmako.shortcuts import render_to_response from codejail.safe_exec import safe_exec diff --git a/lms/djangoapps/django_comment_client/helpers.py b/lms/djangoapps/django_comment_client/helpers.py index fbe7a2401b..a8a51ad95c 100644 --- a/lms/djangoapps/django_comment_client/helpers.py +++ b/lms/djangoapps/django_comment_client/helpers.py @@ -1,8 +1,5 @@ -from django.core.urlresolvers import reverse from django.conf import settings -from mitxmako.shortcuts import render_to_string from .mustache_helpers import mustache_helpers -from django.core.urlresolvers import reverse from functools import partial from .utils import * diff --git a/lms/djangoapps/django_comment_client/management/commands/show_permissions.py b/lms/djangoapps/django_comment_client/management/commands/show_permissions.py index 67fc29ea97..f24f183193 100644 --- a/lms/djangoapps/django_comment_client/management/commands/show_permissions.py +++ b/lms/djangoapps/django_comment_client/management/commands/show_permissions.py @@ -1,5 +1,4 @@ from django.core.management.base import BaseCommand, CommandError -from django_comment_common.models import Permission, Role from django.contrib.auth.models import User diff --git a/lms/djangoapps/django_comment_client/mustache_helpers.py b/lms/djangoapps/django_comment_client/mustache_helpers.py index 5743dba9cb..adaf26c9e0 100644 --- a/lms/djangoapps/django_comment_client/mustache_helpers.py +++ b/lms/djangoapps/django_comment_client/mustache_helpers.py @@ -1,7 +1,6 @@ from .utils import url_for_tags as _url_for_tags import django.core.urlresolvers as urlresolvers -import urllib import sys import inspect diff --git a/lms/djangoapps/django_comment_client/permissions.py b/lms/djangoapps/django_comment_client/permissions.py index 1a523a170a..b868d46e36 100644 --- a/lms/djangoapps/django_comment_client/permissions.py +++ b/lms/djangoapps/django_comment_client/permissions.py @@ -1,8 +1,3 @@ -from django_comment_common.models import Role, Permission -from django.db.models.signals import post_save -from django.dispatch import receiver -from student.models import CourseEnrollment - import logging from util.cache import cache from django.core import cache diff --git a/lms/djangoapps/django_comment_client/tests/test_models.py b/lms/djangoapps/django_comment_client/tests/test_models.py index e45c883931..6d46df113a 100644 --- a/lms/djangoapps/django_comment_client/tests/test_models.py +++ b/lms/djangoapps/django_comment_client/tests/test_models.py @@ -1,5 +1,4 @@ import django_comment_common.models as models -import django_comment_client.permissions as permissions from django.test import TestCase @@ -44,7 +43,7 @@ class RoleClassTestCase(TestCase): class PermissionClassTestCase(TestCase): def setUp(self): - self.permission = permissions.Permission.objects.get_or_create(name="test")[0] + self.permission = models.Permission.objects.get_or_create(name="test")[0] def testUnicode(self): self.assertEqual(str(self.permission), "test") diff --git a/lms/djangoapps/foldit/models.py b/lms/djangoapps/foldit/models.py index 0dce956756..c0ef553d7e 100644 --- a/lms/djangoapps/foldit/models.py +++ b/lms/djangoapps/foldit/models.py @@ -1,11 +1,8 @@ import logging -from django.conf import settings from django.contrib.auth.models import User from django.db import models -from student.models import unique_id_for_user - log = logging.getLogger(__name__) diff --git a/lms/djangoapps/foldit/tests.py b/lms/djangoapps/foldit/tests.py index 9928f596be..0c55049cb6 100644 --- a/lms/djangoapps/foldit/tests.py +++ b/lms/djangoapps/foldit/tests.py @@ -5,7 +5,6 @@ from functools import partial from django.contrib.auth.models import User from django.test import TestCase from django.test.client import RequestFactory -from django.conf import settings from django.core.urlresolvers import reverse from foldit.views import foldit_ops, verify_code diff --git a/lms/djangoapps/instructor/management/commands/compute_grades.py b/lms/djangoapps/instructor/management/commands/compute_grades.py index 92db04f09a..4518450e39 100644 --- a/lms/djangoapps/instructor/management/commands/compute_grades.py +++ b/lms/djangoapps/instructor/management/commands/compute_grades.py @@ -3,18 +3,10 @@ # django management command: dump grades to csv files # for use by batch processes -import os -import sys -import string -import datetime -import json - -#import student.models from instructor.offline_gradecalc import * from courseware.courses import get_course_by_id from xmodule.modulestore.django import modulestore -from django.conf import settings from django.core.management.base import BaseCommand diff --git a/lms/djangoapps/instructor/offline_gradecalc.py b/lms/djangoapps/instructor/offline_gradecalc.py index 8182c4e58a..fe5b95c3b9 100644 --- a/lms/djangoapps/instructor/offline_gradecalc.py +++ b/lms/djangoapps/instructor/offline_gradecalc.py @@ -6,16 +6,12 @@ # The grades are stored in the OfflineComputedGrade table of the courseware model. import json -import logging import time -import courseware.models - -from collections import namedtuple from json import JSONEncoder from courseware import grades, models from courseware.courses import get_course_by_id -from django.contrib.auth.models import User, Group +from django.contrib.auth.models import User class MyEncoder(JSONEncoder): diff --git a/lms/djangoapps/instructor/tests/test_enrollment.py b/lms/djangoapps/instructor/tests/test_enrollment.py index ce5f2d2e50..3ce82b700b 100644 --- a/lms/djangoapps/instructor/tests/test_enrollment.py +++ b/lms/djangoapps/instructor/tests/test_enrollment.py @@ -9,7 +9,6 @@ from django.core.urlresolvers import reverse from courseware.access import _course_staff_group_name from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE, get_user from xmodule.modulestore.django import modulestore -import xmodule.modulestore.django from student.models import CourseEnrollment, CourseEnrollmentAllowed from instructor.views import get_and_clean_student_list diff --git a/lms/djangoapps/instructor/tests/test_gradebook.py b/lms/djangoapps/instructor/tests/test_gradebook.py index 4b1d22b594..3d0a1b09b8 100644 --- a/lms/djangoapps/instructor/tests/test_gradebook.py +++ b/lms/djangoapps/instructor/tests/test_gradebook.py @@ -2,13 +2,11 @@ Tests of the instructor dashboard gradebook """ -from django.test import TestCase from django.test.utils import override_settings from django.core.urlresolvers import reverse from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory -from student.tests.factories import UserFactory, CourseEnrollmentFactory, UserProfileFactory, AdminFactory +from student.tests.factories import UserFactory, CourseEnrollmentFactory, AdminFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -from mock import patch, DEFAULT from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE from capa.tests.response_xml_factory import StringResponseXMLFactory from courseware.tests.factories import StudentModuleFactory diff --git a/lms/djangoapps/instructor/tests/test_xss.py b/lms/djangoapps/instructor/tests/test_xss.py index d6b8adc908..87bd2ee16b 100644 --- a/lms/djangoapps/instructor/tests/test_xss.py +++ b/lms/djangoapps/instructor/tests/test_xss.py @@ -3,7 +3,6 @@ Tests of various instructor dashboard features that include lists of students """ from django.conf import settings -from django.test import TestCase from django.test.client import RequestFactory from django.test.utils import override_settings from markupsafe import escape diff --git a/lms/djangoapps/instructor_task/tests/test_integration.py b/lms/djangoapps/instructor_task/tests/test_integration.py index d7a81a5b39..5a17e32329 100644 --- a/lms/djangoapps/instructor_task/tests/test_integration.py +++ b/lms/djangoapps/instructor_task/tests/test_integration.py @@ -17,7 +17,6 @@ from django.core.urlresolvers import reverse from capa.tests.response_xml_factory import (CodeResponseXMLFactory, CustomResponseXMLFactory) from xmodule.modulestore.tests.factories import ItemFactory -from xmodule.modulestore.exceptions import ItemNotFoundError from courseware.model_data import StudentModule diff --git a/lms/djangoapps/instructor_task/tests/test_tasks.py b/lms/djangoapps/instructor_task/tests/test_tasks.py index 9eb81a98c9..c59a7065ae 100644 --- a/lms/djangoapps/instructor_task/tests/test_tasks.py +++ b/lms/djangoapps/instructor_task/tests/test_tasks.py @@ -19,7 +19,7 @@ from courseware.tests.factories import StudentModuleFactory from student.tests.factories import UserFactory from instructor_task.models import InstructorTask -from instructor_task.tests.test_base import InstructorTaskModuleTestCase, TEST_COURSE_ORG, TEST_COURSE_NUMBER +from instructor_task.tests.test_base import InstructorTaskModuleTestCase from instructor_task.tests.factories import InstructorTaskFactory from instructor_task.tasks import rescore_problem, reset_problem_attempts, delete_problem_state from instructor_task.tasks_helper import UpdateProblemModuleStateError, update_problem_module_state diff --git a/lms/djangoapps/licenses/management/commands/generate_serial_numbers.py b/lms/djangoapps/licenses/management/commands/generate_serial_numbers.py index 7c6b0d310e..4409f1cb45 100644 --- a/lms/djangoapps/licenses/management/commands/generate_serial_numbers.py +++ b/lms/djangoapps/licenses/management/commands/generate_serial_numbers.py @@ -1,6 +1,4 @@ -import os.path from uuid import uuid4 -from optparse import make_option from django.utils.html import escape from django.core.management.base import BaseCommand, CommandError diff --git a/lms/djangoapps/licenses/management/commands/import_serial_numbers.py b/lms/djangoapps/licenses/management/commands/import_serial_numbers.py index a3a8c0bad1..0a08ea83d3 100644 --- a/lms/djangoapps/licenses/management/commands/import_serial_numbers.py +++ b/lms/djangoapps/licenses/management/commands/import_serial_numbers.py @@ -1,5 +1,4 @@ import os.path -from optparse import make_option from django.utils.html import escape from django.core.management.base import BaseCommand, CommandError diff --git a/lms/djangoapps/lms_migration/management/commands/create_groups.py b/lms/djangoapps/lms_migration/management/commands/create_groups.py index 95c9e4238b..6cdc032278 100644 --- a/lms/djangoapps/lms_migration/management/commands/create_groups.py +++ b/lms/djangoapps/lms_migration/management/commands/create_groups.py @@ -5,13 +5,10 @@ # Create all staff_* groups for classes in data directory. import os -import sys -import string -import re from django.core.management.base import BaseCommand from django.conf import settings -from django.contrib.auth.models import User, Group +from django.contrib.auth.models import Group from path import path from lxml import etree diff --git a/lms/djangoapps/lms_migration/management/commands/create_user.py b/lms/djangoapps/lms_migration/management/commands/create_user.py index ca0e1a756f..87abf4f73a 100644 --- a/lms/djangoapps/lms_migration/management/commands/create_user.py +++ b/lms/djangoapps/lms_migration/management/commands/create_user.py @@ -7,7 +7,6 @@ import os import sys import string -import re import datetime from getpass import getpass import json diff --git a/lms/djangoapps/lms_migration/management/commands/manage_course_groups.py b/lms/djangoapps/lms_migration/management/commands/manage_course_groups.py index b63ef7859b..3c87762624 100644 --- a/lms/djangoapps/lms_migration/management/commands/manage_course_groups.py +++ b/lms/djangoapps/lms_migration/management/commands/manage_course_groups.py @@ -4,17 +4,9 @@ # # interactively list and edit membership in course staff and instructor groups -import os -import sys -import string import re -import datetime -from getpass import getpass -import json -import readline from django.core.management.base import BaseCommand -from django.conf import settings from django.contrib.auth.models import User, Group #----------------------------------------------------------------------------- diff --git a/lms/djangoapps/lms_migration/migrate.py b/lms/djangoapps/lms_migration/migrate.py index a677383035..3768b557ed 100644 --- a/lms/djangoapps/lms_migration/migrate.py +++ b/lms/djangoapps/lms_migration/migrate.py @@ -5,7 +5,6 @@ import json import logging import os -from pprint import pprint import xmodule.modulestore.django as xmodule_django from xmodule.modulestore.django import modulestore diff --git a/lms/djangoapps/notes/tests.py b/lms/djangoapps/notes/tests.py index a7609b91ac..21b5cd7b36 100644 --- a/lms/djangoapps/notes/tests.py +++ b/lms/djangoapps/notes/tests.py @@ -9,9 +9,7 @@ from django.contrib.auth.models import User from django.core.exceptions import ValidationError import collections -import unittest import json -import logging from . import utils, api, models diff --git a/lms/djangoapps/notes/views.py b/lms/djangoapps/notes/views.py index 654d7fb31d..01671b7ccd 100644 --- a/lms/djangoapps/notes/views.py +++ b/lms/djangoapps/notes/views.py @@ -4,7 +4,6 @@ from mitxmako.shortcuts import render_to_response from courseware.courses import get_course_with_access from notes.models import Note from notes.utils import notes_enabled_for_course -import json @login_required diff --git a/lms/djangoapps/open_ended_grading/staff_grading.py b/lms/djangoapps/open_ended_grading/staff_grading.py index fad5268294..3ea55f1df0 100644 --- a/lms/djangoapps/open_ended_grading/staff_grading.py +++ b/lms/djangoapps/open_ended_grading/staff_grading.py @@ -5,7 +5,6 @@ LMS part of instructor grading: - calls the instructor grading service """ -import json import logging log = logging.getLogger(__name__) diff --git a/lms/djangoapps/open_ended_grading/tests.py b/lms/djangoapps/open_ended_grading/tests.py index 3b6c992881..99b8b1a929 100644 --- a/lms/djangoapps/open_ended_grading/tests.py +++ b/lms/djangoapps/open_ended_grading/tests.py @@ -9,7 +9,6 @@ from mock import MagicMock, patch, Mock from django.core.urlresolvers import reverse from django.contrib.auth.models import Group -from django.http import HttpResponse from django.conf import settings from mitxmako.shortcuts import render_to_string diff --git a/lms/djangoapps/open_ended_grading/views.py b/lms/djangoapps/open_ended_grading/views.py index a914e434a9..7cf5aaf024 100644 --- a/lms/djangoapps/open_ended_grading/views.py +++ b/lms/djangoapps/open_ended_grading/views.py @@ -1,7 +1,6 @@ # Grading Views import logging -import urllib from django.conf import settings from django.views.decorators.cache import cache_control diff --git a/lms/djangoapps/psychometrics/management/commands/init_psychometrics.py b/lms/djangoapps/psychometrics/management/commands/init_psychometrics.py index 53f6e17e9d..87e62f4a2c 100644 --- a/lms/djangoapps/psychometrics/management/commands/init_psychometrics.py +++ b/lms/djangoapps/psychometrics/management/commands/init_psychometrics.py @@ -2,10 +2,6 @@ # # generate pyschometrics data from tracking logs and student module data -import os -import sys -import string -import datetime import json from courseware.models import * diff --git a/lms/djangoapps/static_template_view/models.py b/lms/djangoapps/static_template_view/models.py index 71a8362390..6b20219993 100644 --- a/lms/djangoapps/static_template_view/models.py +++ b/lms/djangoapps/static_template_view/models.py @@ -1,3 +1 @@ -from django.db import models - # Create your models here. diff --git a/lms/djangoapps/staticbook/models.py b/lms/djangoapps/staticbook/models.py index 71a8362390..6b20219993 100644 --- a/lms/djangoapps/staticbook/models.py +++ b/lms/djangoapps/staticbook/models.py @@ -1,3 +1 @@ -from django.db import models - # Create your models here. diff --git a/lms/djangoapps/staticbook/views.py b/lms/djangoapps/staticbook/views.py index 6d3dcbd5ca..fcfba9e22c 100644 --- a/lms/djangoapps/staticbook/views.py +++ b/lms/djangoapps/staticbook/views.py @@ -1,6 +1,5 @@ from django.contrib.auth.decorators import login_required from django.http import Http404 -from django.core.urlresolvers import reverse from mitxmako.shortcuts import render_to_response from courseware.access import has_access diff --git a/lms/envs/common.py b/lms/envs/common.py index cc45739562..0eb931e308 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -305,7 +305,7 @@ COURSES_WITH_UNSAFE_CODE = [] ############################ SIGNAL HANDLERS ################################ # This is imported to register the exception signal handling that logs exceptions -import monitoring.exceptions # noqa +import monitoring.exceptions # noqa # pylint: disable=W0611 ############################### DJANGO BUILT-INS ############################### # Change DEBUG/TEMPLATE_DEBUG in your environment settings files, not here diff --git a/lms/envs/dev_edx4edx.py b/lms/envs/dev_edx4edx.py index c90f369bc6..13a66ed1e8 100644 --- a/lms/envs/dev_edx4edx.py +++ b/lms/envs/dev_edx4edx.py @@ -18,7 +18,6 @@ if 'eecs1' in socket.gethostname(): MITX_ROOT_URL = '/mitx2' from .common import * -from logsettings import get_logger_config from .dev import * if 'eecs1' in socket.gethostname(): diff --git a/lms/envs/dev_ike.py b/lms/envs/dev_ike.py index 3f54b11d1e..50bbfff096 100644 --- a/lms/envs/dev_ike.py +++ b/lms/envs/dev_ike.py @@ -13,7 +13,6 @@ sessions. Assumes structure: # pylint: disable=W0401, W0614 from .common import * -from logsettings import get_logger_config from .dev import * import socket diff --git a/lms/lib/comment_client/comment_client.py b/lms/lib/comment_client/comment_client.py index 9b1a0baee2..d91c5ea47f 100644 --- a/lms/lib/comment_client/comment_client.py +++ b/lms/lib/comment_client/comment_client.py @@ -1,3 +1,5 @@ +# Import other classes here so they can be imported from here. +# pylint: disable=W0611 from .comment import Comment from .thread import Thread from .user import User diff --git a/lms/lib/perfstats/models.py b/lms/lib/perfstats/models.py index 71a8362390..6b20219993 100644 --- a/lms/lib/perfstats/models.py +++ b/lms/lib/perfstats/models.py @@ -1,3 +1 @@ -from django.db import models - # Create your models here. diff --git a/lms/one_time_startup.py b/lms/one_time_startup.py index e1b1f79444..e10ec06685 100644 --- a/lms/one_time_startup.py +++ b/lms/one_time_startup.py @@ -1,10 +1,9 @@ -import logging from dogapi import dog_http_api, dog_stats_api from django.conf import settings from xmodule.modulestore.django import modulestore from request_cache.middleware import RequestCache -from django.core.cache import get_cache, InvalidCacheBackendError +from django.core.cache import get_cache cache = get_cache('mongo_metadata_inheritance') for store_name in settings.MODULESTORE: diff --git a/lms/urls.py b/lms/urls.py index 1d34ebf3af..80f1224837 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -3,7 +3,8 @@ from django.conf.urls import patterns, include, url from django.contrib import admin from django.conf.urls.static import static -from . import one_time_startup +# Not used, the work is done in the imported module. +from . import one_time_startup # pylint: disable=W0611 import django.contrib.auth.views From 645d847bb116eeb273423852a2814891e3d4b66a Mon Sep 17 00:00:00 2001 From: Ned Batchelder <ned@nedbatchelder.com> Date: Wed, 19 Jun 2013 12:42:13 -0400 Subject: [PATCH 089/995] Remove unused imports from cms, as detected by pylint. --- cms/djangoapps/contentstore/tests/test_item.py | 1 - cms/djangoapps/models/settings/course_metadata.py | 1 - cms/envs/common.py | 4 ++-- cms/envs/dev_ike.py | 2 -- cms/urls.py | 3 +++ 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_item.py b/cms/djangoapps/contentstore/tests/test_item.py index 07264cdc30..1831a5769a 100644 --- a/cms/djangoapps/contentstore/tests/test_item.py +++ b/cms/djangoapps/contentstore/tests/test_item.py @@ -1,4 +1,3 @@ -from contentstore.utils import get_modulestore, get_url_reverse from contentstore.tests.test_course_settings import CourseTestCase from xmodule.modulestore.tests.factories import CourseFactory from django.core.urlresolvers import reverse diff --git a/cms/djangoapps/models/settings/course_metadata.py b/cms/djangoapps/models/settings/course_metadata.py index 708e79f0a3..937ba56f69 100644 --- a/cms/djangoapps/models/settings/course_metadata.py +++ b/cms/djangoapps/models/settings/course_metadata.py @@ -1,6 +1,5 @@ from xmodule.modulestore import Location from contentstore.utils import get_modulestore -from xmodule.x_module import XModuleDescriptor from xmodule.modulestore.inheritance import own_metadata from xblock.core import Scope from xmodule.course_module import CourseDescriptor diff --git a/cms/envs/common.py b/cms/envs/common.py index d7c9e6bb90..da3f39ea49 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -21,7 +21,7 @@ Longer TODO: # We intentionally define lots of variables that aren't used, and # want to import all variables from base settings files -# pylint: disable=W0401, W0614 +# pylint: disable=W0401, W0611, W0614 import sys import lms.envs.common @@ -155,7 +155,7 @@ MIDDLEWARE_CLASSES = ( ############################ SIGNAL HANDLERS ################################ # This is imported to register the exception signal handling that logs exceptions -import monitoring.exceptions # noqa +import monitoring.exceptions # noqa # pylint: disable=W0611 ############################ DJANGO_BUILTINS ################################ # Change DEBUG/TEMPLATE_DEBUG in your environment settings files, not here diff --git a/cms/envs/dev_ike.py b/cms/envs/dev_ike.py index 0c798b68aa..6e67f78f36 100644 --- a/cms/envs/dev_ike.py +++ b/cms/envs/dev_ike.py @@ -7,9 +7,7 @@ # FORCE_SCRIPT_NAME = '/cms' from .common import * -from logsettings import get_logger_config from .dev import * -import socket MITX_FEATURES['AUTH_USE_MIT_CERTIFICATES'] = True diff --git a/cms/urls.py b/cms/urls.py index a9a7f0a68a..d04c311161 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -1,5 +1,8 @@ from django.conf import settings from django.conf.urls import patterns, include, url + +# Import this file so it can do its work, even though we don't use the name. +# pylint: disable=W0611 from . import one_time_startup # Uncomment the next two lines to enable the admin: From e08215e62aa2938d767b4058bc7f5c4aed0d429c Mon Sep 17 00:00:00 2001 From: Julian Arni <julian@edx.org> Date: Wed, 15 May 2013 14:05:31 -0400 Subject: [PATCH 090/995] JSinput input type --- common/lib/capa/capa/inputtypes.py | 62 ++++++ common/lib/capa/capa/responsetypes.py | 2 +- common/lib/capa/capa/templates/jsinput.html | 64 ++++++ .../xmodule/js/src/capa/display.coffee | 5 + common/static/css/capa/jsinput_css.css | 0 common/static/js/capa/jsinput.js | 197 ++++++++++++++++++ common/static/js/test/jsinput/jsinput.js | 16 ++ .../static/js/test/jsinput/mainfixture.html | 103 +++++++++ .../src/pip-delete-this-directory.txt | 5 + 9 files changed, 453 insertions(+), 1 deletion(-) create mode 100644 common/lib/capa/capa/templates/jsinput.html create mode 100644 common/static/css/capa/jsinput_css.css create mode 100644 common/static/js/capa/jsinput.js create mode 100644 common/static/js/test/jsinput/jsinput.js create mode 100644 common/static/js/test/jsinput/mainfixture.html create mode 100644 requirements/src/pip-delete-this-directory.txt diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 65280d6d29..963062a263 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -451,6 +451,68 @@ class JavascriptInput(InputTypeBase): registry.register(JavascriptInput) + +#----------------------------------------------------------------------------- + + +class JSInput(InputTypeBase): + """ + DO NOT USE! HAS NOT BEEN TESTED BEYOND 700X PROBLEMS, AND MAY CHANGE IN + BACKWARDS-INCOMPATIBLE WAYS. + Inputtype for general javascript inputs. Intended to be used with + customresponse. + Loads in a sandboxed iframe to help prevent css and js conflicts between + frame and top-level window. + + iframe sandbox whitelist: + - allow-scripts + - allow-popups + - allow-forms + - allow-pointer-lock + + This in turn means that the iframe cannot directly access the top-level + window elements. + Example: + + <jsinput html_file="/static/test.html" + gradefn="grade" + height="500" + width="400"/> + + See the documentation in the /doc/public folder for more information. + """ + + template = "jsinput.html" + tags = ['jsinput'] + + @classmethod + def get_attributes(cls): + """ + Register the attributes. + """ + return [Attribute('params', None), # extra iframe params + Attribute('html_file', None), + Attribute('gradefn', "gradefn"), + Attribute('get_statefn', None), # Function to call in iframe + # to get current state. + Attribute('set_statefn', None), # Function to call iframe to + # set state + Attribute('width', "400"), # iframe width + Attribute('height', "300")] # iframe height + + + + def _extra_context(self): + context = { + 'applet_loader': '/static/js/capa/jsinput.js', + 'saved_state': self.value + } + + return context + + + +registry.register(JSInput) #----------------------------------------------------------------------------- class TextLine(InputTypeBase): diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 0fa50079de..f5c15260de 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -929,7 +929,7 @@ class CustomResponse(LoncapaResponse): 'chemicalequationinput', 'vsepr_input', 'drag_and_drop_input', 'editamoleculeinput', 'designprotein2dinput', 'editageneinput', - 'annotationinput'] + 'annotationinput', 'jsinput'] def setup_response(self): xml = self.xml diff --git a/common/lib/capa/capa/templates/jsinput.html b/common/lib/capa/capa/templates/jsinput.html new file mode 100644 index 0000000000..ec5d32b5c2 --- /dev/null +++ b/common/lib/capa/capa/templates/jsinput.html @@ -0,0 +1,64 @@ +<section id="inputtype_${id}" class="jsinput" + data="${gradefn}" + % if saved_state: + data-stored="${saved_state|x}" + % endif + % if get_statefn: + data-getstate="${get_statefn}" + % endif + % if set_statefn: + data-setstate="${set_statefn}" + % endif + > + + + <div class="script_placeholder" data-src="${applet_loader}"/> + % if status == 'unsubmitted': + <div class="unanswered" id="status_${id}"> + % elif status == 'correct': + <div class="correct" id="status_${id}"> + % elif status == 'incorrect': + <div class="incorrect" id="status_${id}"> + % elif status == 'incomplete': + <div class="incorrect" id="status_${id}"> + % endif + + <iframe name="iframe_${id}" + id="iframe_${id}" + sandbox="allow-scripts allow-popups allow-same-origin allow-forms allow-pointer-lock" + seamless="seamless" + frameborder="0" + src="${html_file}" + height="${height}" + width="${width}" + /> + <input type="hidden" name="input_${id}" id="input_${id}" + waitfor="" + value="${value|h}"/> + + <br/> + <p id="answer_${id}" class="answer"></p> + + <p class="status"> + % if status == 'unsubmitted': + unanswered + % elif status == 'correct': + correct + % elif status == 'incorrect': + incorrect + % elif status == 'incomplete': + incomplete + % endif + </p> + <br/> <br/> + + <div class="error_message" style="padding: 5px 5px 5px 5px; background-color:#FA6666; height:60px;width:400px; display: none"></div> + + % if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']: + </div> + % endif + + % if msg: + <span class="message">${msg|n}</span> + % endif +</section> diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index 70704ab247..5e0e3b9760 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -140,6 +140,11 @@ class @Problem check_fd: => Logger.log 'problem_check', @answers + # If some function wants to be called before sending the answer to the + # server, give it a chance to do so. + if $('input[waitfor]').length != 0 + ($(lcall).data("waitfor").call() for lcall in $('input[waitfor]')) + @refreshAnswers() # If there are no file inputs in the problem, we can fall back on @check if $('input:file').length == 0 @check() diff --git a/common/static/css/capa/jsinput_css.css b/common/static/css/capa/jsinput_css.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/static/js/capa/jsinput.js b/common/static/js/capa/jsinput.js new file mode 100644 index 0000000000..ff6a8aa68b --- /dev/null +++ b/common/static/js/capa/jsinput.js @@ -0,0 +1,197 @@ +(function (jsinput, undefined) { + // Initialize js inputs on current page. + // N.B.: No library assumptions about the iframe can be made (including, + // most relevantly, jquery). Keep in mind what happens in which context + // when modifying this file. + + // First time this function was called? + var isFirst = typeof(jsinput.jsinputarr) != 'undefined'; + + // Use this array to keep track of the elements that have already been + // initialized. + jsinput.jsinputarr = jsinput.jsinputarr || []; + if (isFirst) { + jsinput.jsinputarr.exists = function (id) { + this.filter(function(e, i, a) { + return e.id = id; + }); + }; + } + + function jsinputConstructor(spec) { + // Define an class that will be instantiated for each.jsinput element + // of the DOM + + // 'that' is the object returned by the constructor. It has a single + // public method, "update", which updates the hidden input field. + var that = {}; + + /* Private methods */ + + var sect = $(spec.elem).parent().find('section[class="jsinput"]'); + // Get the hidden input field to pass to customresponse + function inputfield() { + var parent = $(spec.elem).parent(); + return parent.find('input[id^="input_"]'); + } + + // Get the grade function name + function getgradefn() { + return $(sect).attr("data"); + } + + // Get state getter + function getgetstate() { + return $(sect).attr("data-getstate"); + } + // Get state setter + function getsetstate() { + var gss = $(sect).attr("data-setstate"); + return gss; + } + // Get stored state + function getstoredstate() { + return $(sect).attr("data-stored"); + } + + // Put the return value of gradefn in the hidden inputfield. + // If passed an argument, does not call gradefn, and instead directly + // updates the inputfield with the passed value. + var update = function (answer) { + + var ans; + ans = $(spec.elem). + find('iframe[name^="iframe_"]'). + get(0). // jquery might not be available in the iframe + contentWindow[gradefn](); + // setstate presumes getstate, so don't getstate unless setstate is + // defined. + if (getgetstate() && getsetstate()) { + var state, store; + state = $(spec.elem). + find('iframe[name^="iframe_"]'). + get(0). + contentWindow[getgetstate()](); + store = { + answer: ans, + state: state + }; + inputfield().val(JSON.stringify(store)); + } else { + inputfield().val(ans); + } + return; + }; + + // Find the update button, and bind the update function to its click + // event. + function updateHandler() { + var updatebutton = $(spec.elem). + find('button[class="update"]').get(0); + $(updatebutton).click(update); + } + + + + /* Public methods */ + + that.update = update; + + + + /* Initialization */ + + jsinput.jsinputarr.push(that); + + // Put the update function as the value of the inputfield's "waitfor" + // attribute so that it is called when the check button is clicked. + function bindCheck() { + inputfield().data('waitfor', that.update); + return; + } + + var gradefn = getgradefn(); + + if (spec.passive === false) { + updateHandler(); + bindCheck(); + // Check whether application takes in state and there is a saved + // state to give it + if (getsetstate() && getstoredstate()) { + console.log("Using stored state..."); + var sval; + if (typeof(getstoredstate()) === "object") { + sval = getstoredstate()["state"]; + } else { + sval = getstoredstate(); + } + $(spec.elem). + find('iframe[name^="iframe_"]'). + get(0). + contentWindow[getsetstate()](sval); + } + } else { + // NOT CURRENTLY SUPPORTED + // If set up to passively receive updates (intercept a function's + // return value whenever the function is called) add an event + // listener that listens to messages that match "that"'s id. + // Decorate the iframe gradefn with updateDecorator. + iframe.contentWindow[gradefn] = updateDecorator(iframe.contentWindow[gradefn]); + iframe.contentWindow.addEventListener('message', function (e) { + var id = e.data[0], + msg = e.data[1]; + if (id === spec.id) { update(msg); } + }); + } + + + return that; + } + + function updateDecorator(fn, id) { + // NOT CURRENTLY SUPPORTED + // Simple function decorator that posts the output of a function to the + // parent iframe before returning the original function's value. + // Can be used to decorate one or more gradefn (instead of using an + // explicit "Update" button) when gradefn is automatically called as part + // of an application's natural behavior. + // The id argument is used to specify which of the instances of jsinput on + // the parent page the message is being posted to. + return function () { + var result = fn.apply(null, arguments); + window.parent.contentWindow.postMessage([id, result], document.referrer); + return result; + }; + } + + function walkDOM() { + // Find all jsinput elements, and create a jsinput object for each one + var all = $(document).find('section[class="jsinput"]'); + var newid; + all.each(function() { + // Get just the mako variable 'id' from the id attribute + newid = $(this).attr("id").replace(/^inputtype_/, ""); + var newJsElem = jsinputConstructor({ + id: newid, + elem: this, + passive: false + }); + }); + } + + // TODO: Inject css into, and retrieve frame size from, the iframe (for non + // "seamless"-supporting browsers). + //var iframeInjection = { + //injectStyles : function (style) { + //$(document.body).css(style); + //}, + //sendMySize : function () { + //var height = html.height, + //width = html.width; + //window.parent.postMessage(['height', height], '*'); + //window.parent.postMessage(['width', width], '*'); + //} + //}; + + setTimeout(walkDOM, 200); +})(window.jsinput = window.jsinput || {}) diff --git a/common/static/js/test/jsinput/jsinput.js b/common/static/js/test/jsinput/jsinput.js new file mode 100644 index 0000000000..75d9e44d09 --- /dev/null +++ b/common/static/js/test/jsinput/jsinput.js @@ -0,0 +1,16 @@ +describe("jsinput test", function () { + + beforeEach(function () { + $('#fixture').remove(); + $.ajax({ + async: false, + url: 'mainfixture.html', + success: function(data) { + $('body').append($(data)); + } + }); + }); + + it("") +} + ) diff --git a/common/static/js/test/jsinput/mainfixture.html b/common/static/js/test/jsinput/mainfixture.html new file mode 100644 index 0000000000..accf4997e1 --- /dev/null +++ b/common/static/js/test/jsinput/mainfixture.html @@ -0,0 +1,103 @@ +<html> +<head> +<title> JSinput jasmine test + + +
      + +
      + + + +
      + +

      + +

      +

      +

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

      + +

      +

      +

      + + + +
      +
      + + diff --git a/requirements/src/pip-delete-this-directory.txt b/requirements/src/pip-delete-this-directory.txt new file mode 100644 index 0000000000..c8883ea99f --- /dev/null +++ b/requirements/src/pip-delete-this-directory.txt @@ -0,0 +1,5 @@ +This file is placed here by pip to indicate the source was put +here by pip. + +Once this package is successfully installed this source code will be +deleted (unless you remove this file). From 3c55a1e95d0bc912d457848f097e15590ae19760 Mon Sep 17 00:00:00 2001 From: Julian Arni Date: Wed, 19 Jun 2013 17:35:10 -0400 Subject: [PATCH 091/995] Add sphinx documentation for jsinput --- doc/public/course_data_formats/jsinput.rst | 143 +++++++++++++++++++++ doc/public/index.rst | 1 + 2 files changed, 144 insertions(+) create mode 100644 doc/public/course_data_formats/jsinput.rst diff --git a/doc/public/course_data_formats/jsinput.rst b/doc/public/course_data_formats/jsinput.rst new file mode 100644 index 0000000000..5252a5dd0c --- /dev/null +++ b/doc/public/course_data_formats/jsinput.rst @@ -0,0 +1,143 @@ +############################################################################## +JS Input +############################################################################## + + **NOTE** + *Do not use this feature yet! Its attributes and behaviors may change + without any concern for backwards compatibility. Moreover, it has only been + tested in a very limited context. If you absolutely must, contact Julian + (julian@edx.org). When the feature stabilizes, this note will be removed.* + +This document explains how to write a JSInput input type. JSInput is meant to +allow problem authors to easily turn working standalone HTML files into +problems that can be integrated into the edX platform. Since it's aim is +flexibility, it can be seen as the input and client-side equivalent of +CustomResponse. + +A JSInput input creates an iframe into a static HTML page, and passes the +return value of author-specified functions to the enclosing response type +(generally CustomResponse). JSInput can also stored and retrieve state. + +****************************************************************************** +Format +****************************************************************************** + +A jsinput problem looks like this: + +.. code-block:: xml + + + + + + + + +The accepted attributes are: + +============== ============== ========= ========== +Attribute Name Value Type Required? Default +============== ============== ========= ========== +html_file Url string Yes None +gradefn Function name Yes `gradefn` +set_statefn Function name No None +get_statefn Function name No None +height Integer No `500` +width Integer No `400` +============== ============== ========= ========== + +****************************************************************************** +Required Attributes +****************************************************************************** + +============================================================================== +html_file +============================================================================== + +The `html_file` attribute specifies what html file the iframe will point to. This +should be located in the content directory. + +The iframe is created using the sandbox attribute; while popups, scripts, and +pointer locks are allowed, the iframe cannot access its parent's attributes. + +The html file should contain a top-level function for the gradefn function. To +check whether the gradefn will be accessible to JSInput, check that, in the +console,:: + window["`gradefn`"] +Returns the right thing. + +Aside from that, more or less anything goes. Note that currently there is no +support for inheriting css or javascript from the parent (aside from the +Chrome-only `seamless` attribute, which is set to true by default). + +============================================================================== +gradefn +============================================================================== + +The `gradefn` attribute specifies the name of the function that will be called +when a user clicks on the "Check" button, and which should return the student's +answer. This answer will (unless both the get_statefn and set_statefn +attributes are also used) be passed as a string to the enclosing response type. +In the customresponse example above, this means cfn will be passed this answer +as `ans`. + +**IMPORTANT** : the `gradefn` function should not be at all asynchronous, since +this could result in the student's latest answer not being passed correctly. +Moreover, the function should also return promptly, since currently the student +has no indication that her answer is being calculated/produced. + +****************************************************************************** +Option Attributes +****************************************************************************** + +The `height` and `width` attributes are straightforward: they specify the +height and width of the iframe. Both are limited by the enclosing DOM elements, +so for instance there is an implicit max-width of around 900. + +In the future, JSInput may attempt to make these dimensions match the html +file's dimensions (up to the aforementioned limits), but currently it defaults +to `500` and `400` for `height` and `width`, respectively. + +============================================================================== +set_statefn +============================================================================== + +Sometimes a problem author will want information about a student's previous +answers ("state") to be saved and reloaded. If the attribute `set_statefn` is +used, the function given as its value will be passed the state as a string +argument whenever there is a state, and the student returns to a problem. It is +the responsibility of the function to then use this state approriately. + +The state that is passed is: + +1. The previous output of `gradefn` (i.e., the previous answer) if + `get_statefn` is not defined. +2. The previous output of `get_statefn` (see below) otherwise. + +It is the responsibility of the iframe to do proper verification of the +argument that it receives via `set_statefn`. + +============================================================================== +get_statefn +============================================================================== + +Sometimes the state and the answer are quite different. For instance, a problem +that involves using a javascript program that allows the student to alter a +molecule may grade based on the molecule's hidrophobicity, but from the +hidrophobicity it might be incapable of restoring the state. In that case, a +*separate* state may be stored and loaded by `set_statefn`. Note that if +`get_statefn` is defined, the answer (i.e., what is passed to the enclosing +response type) will be a json string with the following format:: + { + answer: `[answer string]` + state: `[state string]` + } + +It is the responsibility of the enclosing response type to then parse this as +json. diff --git a/doc/public/index.rst b/doc/public/index.rst index 064b3ff443..abc9978aeb 100644 --- a/doc/public/index.rst +++ b/doc/public/index.rst @@ -28,6 +28,7 @@ Specific Problem Types course_data_formats/conditional_module/conditional_module.rst course_data_formats/word_cloud/word_cloud.rst course_data_formats/custom_response.rst + course_data_formats/jsinput.rst Internal Data Formats From 49fba9e84d757fef6e444f7003eac3634cea5194 Mon Sep 17 00:00:00 2001 From: Julian Arni Date: Wed, 19 Jun 2013 17:54:28 -0400 Subject: [PATCH 092/995] Remove obnoxious pip file --- requirements/src/pip-delete-this-directory.txt | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 requirements/src/pip-delete-this-directory.txt diff --git a/requirements/src/pip-delete-this-directory.txt b/requirements/src/pip-delete-this-directory.txt deleted file mode 100644 index c8883ea99f..0000000000 --- a/requirements/src/pip-delete-this-directory.txt +++ /dev/null @@ -1,5 +0,0 @@ -This file is placed here by pip to indicate the source was put -here by pip. - -Once this package is successfully installed this source code will be -deleted (unless you remove this file). From bc2f7b96eccb638844d9e3488525c9e71e6126f4 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 19 Jun 2013 22:31:05 -0400 Subject: [PATCH 093/995] Remove a redundant pylint suppression. --- cms/envs/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/envs/common.py b/cms/envs/common.py index da3f39ea49..7f4c106e6d 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -155,7 +155,7 @@ MIDDLEWARE_CLASSES = ( ############################ SIGNAL HANDLERS ################################ # This is imported to register the exception signal handling that logs exceptions -import monitoring.exceptions # noqa # pylint: disable=W0611 +import monitoring.exceptions # noqa ############################ DJANGO_BUILTINS ################################ # Change DEBUG/TEMPLATE_DEBUG in your environment settings files, not here From e775852ee4c6c4ea8c5f5660b44b9b50f5cba8d6 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 19 Jun 2013 22:44:55 -0400 Subject: [PATCH 094/995] Make lms and cms more similar. --- lms/envs/common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lms/envs/common.py b/lms/envs/common.py index 0eb931e308..f9bfa878dd 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -21,7 +21,7 @@ Longer TODO: # We intentionally define lots of variables that aren't used, and # want to import all variables from base settings files -# pylint: disable=W0401, W0614 +# pylint: disable=W0401, W0611, W0614 import sys import os @@ -305,7 +305,7 @@ COURSES_WITH_UNSAFE_CODE = [] ############################ SIGNAL HANDLERS ################################ # This is imported to register the exception signal handling that logs exceptions -import monitoring.exceptions # noqa # pylint: disable=W0611 +import monitoring.exceptions # noqa ############################### DJANGO BUILT-INS ############################### # Change DEBUG/TEMPLATE_DEBUG in your environment settings files, not here From 448ca26cdf23e848a0adca25a7ed29bd7e1de9e4 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 20 Jun 2013 09:06:29 -0400 Subject: [PATCH 095/995] Remove simplewiki from the codebase --- doc/overview.md | 5 - docs/source/lms.rst | 28 - .../multicourse/multicourse_settings.py | 2 +- lms/djangoapps/simplewiki/__init__.py | 9 - lms/djangoapps/simplewiki/admin.py | 70 -- lms/djangoapps/simplewiki/mdx_circuit.py | 72 -- lms/djangoapps/simplewiki/mdx_image.py | 71 -- lms/djangoapps/simplewiki/mdx_mathjax.py | 30 - lms/djangoapps/simplewiki/mdx_video.py | 289 ------- lms/djangoapps/simplewiki/mdx_wikipath.py | 96 --- .../simplewiki/migrations/0001_initial.py | 216 ----- .../migrations/0002_unique_slugs.py | 136 --- ...article_parent__add_field_article_names.py | 161 ---- .../0004_multicourse_data_migration.py | 134 --- .../0005_auto__add_unique_namespace_name.py | 129 --- .../simplewiki/migrations/0006_auto.py | 129 --- .../simplewiki/migrations/0007_auto.py | 129 --- .../simplewiki/migrations/__init__.py | 0 lms/djangoapps/simplewiki/models.py | 387 --------- .../simplewiki/templatetags/__init__.py | 0 .../templatetags/simplewiki_utils.py | 20 - lms/djangoapps/simplewiki/tests.py | 23 - lms/djangoapps/simplewiki/urls.py | 19 - lms/djangoapps/simplewiki/usage.txt | 800 ------------------ lms/djangoapps/simplewiki/views.py | 552 ------------ lms/djangoapps/simplewiki/wiki_settings.py | 111 --- lms/envs/common.py | 1 - lms/templates/simplewiki/simplewiki_base.html | 164 ---- lms/templates/simplewiki/simplewiki_edit.html | 76 -- .../simplewiki/simplewiki_error.html | 79 -- .../simplewiki/simplewiki_history.html | 92 -- .../simplewiki/simplewiki_instructions.html | 24 - .../simplewiki/simplewiki_revision_feed.html | 63 -- .../simplewiki/simplewiki_searchresults.html | 34 - .../simplewiki_updateprogressbar.html | 37 - lms/templates/simplewiki/simplewiki_view.html | 15 - 36 files changed, 1 insertion(+), 4202 deletions(-) delete mode 100644 lms/djangoapps/simplewiki/__init__.py delete mode 100644 lms/djangoapps/simplewiki/admin.py delete mode 100755 lms/djangoapps/simplewiki/mdx_circuit.py delete mode 100755 lms/djangoapps/simplewiki/mdx_image.py delete mode 100644 lms/djangoapps/simplewiki/mdx_mathjax.py delete mode 100755 lms/djangoapps/simplewiki/mdx_video.py delete mode 100755 lms/djangoapps/simplewiki/mdx_wikipath.py delete mode 100644 lms/djangoapps/simplewiki/migrations/0001_initial.py delete mode 100644 lms/djangoapps/simplewiki/migrations/0002_unique_slugs.py delete mode 100644 lms/djangoapps/simplewiki/migrations/0003_auto__add_namespace__del_field_article_parent__add_field_article_names.py delete mode 100644 lms/djangoapps/simplewiki/migrations/0004_multicourse_data_migration.py delete mode 100644 lms/djangoapps/simplewiki/migrations/0005_auto__add_unique_namespace_name.py delete mode 100644 lms/djangoapps/simplewiki/migrations/0006_auto.py delete mode 100644 lms/djangoapps/simplewiki/migrations/0007_auto.py delete mode 100644 lms/djangoapps/simplewiki/migrations/__init__.py delete mode 100644 lms/djangoapps/simplewiki/models.py delete mode 100644 lms/djangoapps/simplewiki/templatetags/__init__.py delete mode 100644 lms/djangoapps/simplewiki/templatetags/simplewiki_utils.py delete mode 100644 lms/djangoapps/simplewiki/tests.py delete mode 100644 lms/djangoapps/simplewiki/urls.py delete mode 100644 lms/djangoapps/simplewiki/usage.txt delete mode 100644 lms/djangoapps/simplewiki/views.py delete mode 100644 lms/djangoapps/simplewiki/wiki_settings.py delete mode 100644 lms/templates/simplewiki/simplewiki_base.html delete mode 100644 lms/templates/simplewiki/simplewiki_edit.html delete mode 100644 lms/templates/simplewiki/simplewiki_error.html delete mode 100644 lms/templates/simplewiki/simplewiki_history.html delete mode 100644 lms/templates/simplewiki/simplewiki_instructions.html delete mode 100644 lms/templates/simplewiki/simplewiki_revision_feed.html delete mode 100644 lms/templates/simplewiki/simplewiki_searchresults.html delete mode 100644 lms/templates/simplewiki/simplewiki_updateprogressbar.html delete mode 100644 lms/templates/simplewiki/simplewiki_view.html diff --git a/doc/overview.md b/doc/overview.md index 4d074dfaf3..31ddd011ff 100644 --- a/doc/overview.md +++ b/doc/overview.md @@ -122,11 +122,6 @@ In production, the django `collectstatic` command recompiles everything and puts In development, we don't use collectstatic, instead accessing the files in place. The auto-compilation is run via `common/djangoapps/pipeline_mako/templates/static_content.html`. Details: templates include `<%namespace name='static' file='static_content.html'/>`, then something like `<%static:css group='application'/>` to call the functions in `common/djangoapps/pipeline_mako/__init__.py`, which call the `django-pipeline` compilers. -### Other modules - -- Wiki -- in `lms/djangoapps/simplewiki`. Has some markdown extentions for embedding circuits, videos, etc. - - ## Testing See `testing.md`. diff --git a/docs/source/lms.rst b/docs/source/lms.rst index 36622114ab..6548cd71a0 100644 --- a/docs/source/lms.rst +++ b/docs/source/lms.rst @@ -314,34 +314,6 @@ Psychoanalyze :members: :show-inheritance: -Simple wiki -=========== - -.. automodule:: simplewiki - :members: - :show-inheritance: - -Models ------- - -.. automodule:: simplewiki.models - :members: - :show-inheritance: - -Views ------ - -.. automodule:: simplewiki.views - :members: - :show-inheritance: - -Tests ------ - -.. automodule:: simplewiki.tests - :members: - :show-inheritance: - Static template view ==================== diff --git a/lms/djangoapps/multicourse/multicourse_settings.py b/lms/djangoapps/multicourse/multicourse_settings.py index c3df167ad8..de445dc0e1 100644 --- a/lms/djangoapps/multicourse/multicourse_settings.py +++ b/lms/djangoapps/multicourse/multicourse_settings.py @@ -10,7 +10,7 @@ # keys being the COURSE_NAME (spaces ok), and the value being a dict of # parameter,value pairs. The required parameters are: # -# - number : course number (used in the simplewiki pages) +# - number : course number (used in the wiki pages) # - title : humanized descriptive course title # # Optional parameters: diff --git a/lms/djangoapps/simplewiki/__init__.py b/lms/djangoapps/simplewiki/__init__.py deleted file mode 100644 index 9f9c332419..0000000000 --- a/lms/djangoapps/simplewiki/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Source: django-simplewiki. GPL license. - -import os -import sys - -# allow mdx_* parsers to be just dropped in the simplewiki folder -module_path = os.path.abspath(os.path.dirname(__file__)) -if module_path not in sys.path: - sys.path.append(module_path) diff --git a/lms/djangoapps/simplewiki/admin.py b/lms/djangoapps/simplewiki/admin.py deleted file mode 100644 index 2ba6405956..0000000000 --- a/lms/djangoapps/simplewiki/admin.py +++ /dev/null @@ -1,70 +0,0 @@ -# Source: django-simplewiki. GPL license. - -from django import forms -from django.contrib import admin -from django.utils.translation import ugettext as _ - -from .models import Article, Revision, Permission, ArticleAttachment - - -class RevisionInline(admin.TabularInline): - model = Revision - extra = 1 - - -class RevisionAdmin(admin.ModelAdmin): - list_display = ('article', '__unicode__', 'revision_date', 'revision_user', 'revision_text') - search_fields = ('article', 'counter') - - -class AttachmentAdmin(admin.ModelAdmin): - list_display = ('article', '__unicode__', 'uploaded_on', 'uploaded_by') - - -class ArticleAdminForm(forms.ModelForm): - def clean(self): - cleaned_data = self.cleaned_data - if cleaned_data.get("slug").startswith('_'): - raise forms.ValidationError(_('Slug cannot start with _ character.' - 'Reserved for internal use.')) - if not self.instance.pk: - parent = cleaned_data.get("parent") - slug = cleaned_data.get("slug") - if Article.objects.filter(slug__exact=slug, parent__exact=parent): - raise forms.ValidationError(_('Article slug and parent must be ' - 'unique together.')) - return cleaned_data - - class Meta: - model = Article - - -class ArticleAdmin(admin.ModelAdmin): - list_display = ('created_by', 'slug', 'modified_on', 'namespace') - search_fields = ('slug',) - prepopulated_fields = {'slug': ('title',)} - inlines = [RevisionInline] - form = ArticleAdminForm - save_on_top = True - - def formfield_for_foreignkey(self, db_field, request, **kwargs): - if db_field.name == 'current_revision': - # Try to determine the id of the article being edited - id = request.path.split('/') - import re - if len(id) > 0 and re.match(r"\d+", id[-2]): - kwargs["queryset"] = Revision.objects.filter(article=id[-2]) - return db_field.formfield(**kwargs) - else: - db_field.editable = False - return db_field.formfield(**kwargs) - return super(ArticleAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs) - - -class PermissionAdmin(admin.ModelAdmin): - search_fields = ('article', 'counter') - -admin.site.register(Article, ArticleAdmin) -admin.site.register(Revision, RevisionAdmin) -admin.site.register(Permission, PermissionAdmin) -admin.site.register(ArticleAttachment, AttachmentAdmin) diff --git a/lms/djangoapps/simplewiki/mdx_circuit.py b/lms/djangoapps/simplewiki/mdx_circuit.py deleted file mode 100755 index 4ec7501341..0000000000 --- a/lms/djangoapps/simplewiki/mdx_circuit.py +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env python -''' -Image Circuit Extension for Python-Markdown -====================================== - - -Any single line beginning with circuit-schematic: and followed by data (which should be json data, but this -is not enforced at this level) will be displayed as a circuit schematic. This is simply an input element with -the value set to the data. It is left to javascript on the page to render that input as a circuit schematic. - -ex: -circuit-schematic:[["r",[128,48,0],{"r":"1","_json_":0},["2","1"]],["view",0,0,2,null,null,null,null,null,null,null],["dc",{"0":0,"1":1,"I(_3)":-1}]] - -(This is a schematic with a single one-ohm resistor. Note that this data is not meant to be user-editable.) - -''' -import markdown -import re - -from django.utils.html import escape - -try: - # Markdown 2.1.0 changed from 2.0.3. We try importing the new version first, - # but import the 2.0.3 version if it fails - from markdown.util import etree -except: - from markdown import etree - - -class CircuitExtension(markdown.Extension): - def __init__(self, configs): - for key, value in configs: - self.setConfig(key, value) - - def extendMarkdown(self, md, md_globals): - ## Because Markdown treats contigous lines as one block of text, it is hard to match - ## a regex that must occupy the whole line (like the circuit regex). This is why we have - ## a preprocessor that inspects the lines and replaces the matched lines with text that is - ## easier to match - md.preprocessors.add('circuit', CircuitPreprocessor(md), "_begin") - - pattern = CircuitLink(r'processed-schematic:(?P.*?)processed-schematic-end') - pattern.md = md - pattern.ext = self - md.inlinePatterns.add('circuit', pattern, ".*)$') - - def run(self, lines): - def convertLine(line): - m = self.preRegex.match(line) - if m: - return 'processed-schematic:{0}processed-schematic-end'.format(m.group('data')) - else: - return line - - return [convertLine(line) for line in lines] - - -class CircuitLink(markdown.inlinepatterns.Pattern): - def handleMatch(self, m): - data = m.group('data') - data = escape(data) - return etree.fromstring("
      ") - - -def makeExtension(configs=None): - to_return = CircuitExtension(configs=configs) - print "circuit returning ", to_return - return to_return diff --git a/lms/djangoapps/simplewiki/mdx_image.py b/lms/djangoapps/simplewiki/mdx_image.py deleted file mode 100755 index af0413f841..0000000000 --- a/lms/djangoapps/simplewiki/mdx_image.py +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env python -''' -Image Embedding Extension for Python-Markdown -====================================== - -Converts lone links to embedded images, provided the file extension is allowed. - -Ex: - http://www.ericfehse.net/media/img/ef/blog/django-pony.jpg - becomes - - - mypic.jpg becomes - -Requires Python-Markdown 1.6+ -''' - -import simplewiki.settings as settings - -import markdown -try: - # Markdown 2.1.0 changed from 2.0.3. We try importing the new version first, - # but import the 2.0.3 version if it fails - from markdown.util import etree -except: - from markdown import etree - - -class ImageExtension(markdown.Extension): - def __init__(self, configs): - for key, value in configs: - self.setConfig(key, value) - - def add_inline(self, md, name, klass, re): - pattern = klass(re) - pattern.md = md - pattern.ext = self - md.inlinePatterns.add(name, pattern, "([^:/?#])+://)?(?P([^/?#]*)/)?(?P[^?#]*\.(?P[^?#]{3,4}))(?:\?([^#]*))?(?:#(.*))?$') - - -class ImageLink(markdown.inlinepatterns.Pattern): - def handleMatch(self, m): - img = etree.Element('img') - proto = m.group('proto') or "http://" - domain = m.group('domain') - path = m.group('path') - ext = m.group('ext') - - # A fixer upper - if ext.lower() in settings.WIKI_IMAGE_EXTENSIONS: - if domain: - src = proto + domain + path - elif path: - # We need a nice way to source local attachments... - src = "/wiki/media/" + path + ".upload" - else: - src = '' - img.set('src', src) - return img - - -def makeExtension(configs=None): - return ImageExtension(configs=configs) - -if __name__ == "__main__": - import doctest - doctest.testmod() diff --git a/lms/djangoapps/simplewiki/mdx_mathjax.py b/lms/djangoapps/simplewiki/mdx_mathjax.py deleted file mode 100644 index b14803744b..0000000000 --- a/lms/djangoapps/simplewiki/mdx_mathjax.py +++ /dev/null @@ -1,30 +0,0 @@ -# Source: https://github.com/mayoff/python-markdown-mathjax - -import markdown -try: - # Markdown 2.1.0 changed from 2.0.3. We try importing the new version first, - # but import the 2.0.3 version if it fails - from markdown.util import etree, AtomicString -except: - from markdown import etree, AtomicString - - -class MathJaxPattern(markdown.inlinepatterns.Pattern): - - def __init__(self): - markdown.inlinepatterns.Pattern.__init__(self, r'(?>> import markdown - -Test Metacafe - ->>> s = "http://www.metacafe.com/watch/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room/" ->>> markdown.markdown(s, ['video']) -u'

      ' - - -Test Metacafe with arguments - ->>> markdown.markdown(s, ['video(metacafe_width=500,metacafe_height=425)']) -u'

      ' - - -Test Link To Metacafe - ->>> s = "[Metacafe link](http://www.metacafe.com/watch/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room/)" ->>> markdown.markdown(s, ['video']) -u'

      Metacafe link

      ' - - -Test Markdown Escaping - ->>> s = "\\http://www.metacafe.com/watch/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room/" ->>> markdown.markdown(s, ['video']) -u'

      http://www.metacafe.com/watch/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room/

      ' ->>> s = "`http://www.metacafe.com/watch/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room/`" ->>> markdown.markdown(s, ['video']) -u'

      http://www.metacafe.com/watch/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room/

      ' - - -Test Youtube - ->>> s = "http://www.youtube.com/watch?v=u1mA-0w8XPo&hd=1&fs=1&feature=PlayList&p=34C6046F7FEACFD3&playnext=1&playnext_from=PL&index=1" ->>> markdown.markdown(s, ['video']) -u'

      ' - - -Test Youtube with argument - ->>> markdown.markdown(s, ['video(youtube_width=200,youtube_height=100)']) -u'

      ' - - -Test Youtube Link - ->>> s = "[Youtube link](http://www.youtube.com/watch?v=u1mA-0w8XPo&feature=PlayList&p=34C6046F7FEACFD3&playnext=1&playnext_from=PL&index=1)" ->>> markdown.markdown(s, ['video']) -u'

      Youtube link

      ' - - -Test Dailymotion - ->>> s = "http://www.dailymotion.com/relevance/search/ut2004/video/x3kv65_ut2004-ownage_videogames" ->>> markdown.markdown(s, ['video']) -u'

      ' - - -Test Dailymotion again (Dailymotion and their crazy URLs) - ->>> s = "http://www.dailymotion.com/us/video/x8qak3_iron-man-vs-bruce-lee_fun" ->>> markdown.markdown(s, ['video']) -u'

      ' - - -Test Yahoo! Video - ->>> s = "http://video.yahoo.com/watch/1981791/4769603" ->>> markdown.markdown(s, ['video']) -u'

      ' - - -Test Veoh Video - ->>> s = "http://www.veoh.com/search/videos/q/mario#watch%3De129555XxCZanYD" ->>> markdown.markdown(s, ['video']) -u'

      ' - - -Test Veoh Video Again (More fun URLs) - ->>> s = "http://www.veoh.com/group/BigCatRescuers#watch%3Dv16771056hFtSBYEr" ->>> markdown.markdown(s, ['video']) -u'

      ' - - -Test Veoh Video Yet Again (Even more fun URLs) - ->>> s = "http://www.veoh.com/browse/videos/category/anime/watch/v181645607JyXPWcQ" ->>> markdown.markdown(s, ['video']) -u'

      ' - - -Test Vimeo Video - ->>> s = "http://www.vimeo.com/1496152" ->>> markdown.markdown(s, ['video']) -u'

      ' - -Test Vimeo Video with some GET values - ->>> s = "http://vimeo.com/1496152?test=test" ->>> markdown.markdown(s, ['video']) -u'

      ' - -Test Blip.tv - ->>> s = "http://blip.tv/file/get/Pycon-PlenarySprintIntro563.flv" ->>> markdown.markdown(s, ['video']) -u'

      ' - -Test Gametrailers - ->>> s = "http://www.gametrailers.com/video/console-comparison-borderlands/58079" ->>> markdown.markdown(s, ['video']) -u'

      ' -""" - -import markdown -try: - # Markdown 2.1.0 changed from 2.0.3. We try importing the new version first, - # but import the 2.0.3 version if it fails - from markdown.util import etree -except: - from markdown import etree - - -version = "0.1.6" - - -class VideoExtension(markdown.Extension): - def __init__(self, configs): - self.config = { - 'bliptv_width': ['480', 'Width for Blip.tv videos'], - 'bliptv_height': ['300', 'Height for Blip.tv videos'], - 'dailymotion_width': ['480', 'Width for Dailymotion videos'], - 'dailymotion_height': ['405', 'Height for Dailymotion videos'], - 'gametrailers_width': ['480', 'Width for Gametrailers videos'], - 'gametrailers_height': ['392', 'Height for Gametrailers videos'], - 'metacafe_width': ['498', 'Width for Metacafe videos'], - 'metacafe_height': ['423', 'Height for Metacafe videos'], - 'veoh_width': ['410', 'Width for Veoh videos'], - 'veoh_height': ['341', 'Height for Veoh videos'], - 'vimeo_width': ['400', 'Width for Vimeo videos'], - 'vimeo_height': ['321', 'Height for Vimeo videos'], - 'yahoo_width': ['512', 'Width for Yahoo! videos'], - 'yahoo_height': ['322', 'Height for Yahoo! videos'], - 'youtube_width': ['425', 'Width for Youtube videos'], - 'youtube_height': ['344', 'Height for Youtube videos'], - } - - # Override defaults with user settings - for key, value in configs: - self.setConfig(key, value) - - def add_inline(self, md, name, klass, re): - pattern = klass(re) - pattern.md = md - pattern.ext = self - md.inlinePatterns.add(name, pattern, "\S+.flv)') - self.add_inline(md, 'dailymotion', Dailymotion, - r'([^(]|^)http://www\.dailymotion\.com/(?P\S+)') - self.add_inline(md, 'gametrailers', Gametrailers, - r'([^(]|^)http://www.gametrailers.com/video/[a-z0-9-]+/(?P\d+)') - self.add_inline(md, 'metacafe', Metacafe, - r'([^(]|^)http://www\.metacafe\.com/watch/(?P\S+)/') - self.add_inline(md, 'veoh', Veoh, - r'([^(]|^)http://www\.veoh\.com/\S*(#watch%3D|watch/)(?P\w+)') - self.add_inline(md, 'vimeo', Vimeo, - r'([^(]|^)http://(www.|)vimeo\.com/(?P\d+)\S*') - self.add_inline(md, 'yahoo', Yahoo, - r'([^(]|^)http://video\.yahoo\.com/watch/(?P\d+)/(?P\d+)') - self.add_inline(md, 'youtube', Youtube, - r'([^(]|^)http://www\.youtube\.com/watch\?\S*v=(?P[A-Za-z0-9_&=-]+)\S*') - - -class Bliptv(markdown.inlinepatterns.Pattern): - def handleMatch(self, m): - url = 'http://blip.tv/scripts/flash/showplayer.swf?file=http://blip.tv/file/get/%s' % m.group('bliptvfile') - width = self.ext.config['bliptv_width'][0] - height = self.ext.config['bliptv_height'][0] - return flash_object(url, width, height) - - -class Dailymotion(markdown.inlinepatterns.Pattern): - def handleMatch(self, m): - url = 'http://www.dailymotion.com/swf/%s' % m.group('dailymotionid').split('/')[-1] - width = self.ext.config['dailymotion_width'][0] - height = self.ext.config['dailymotion_height'][0] - return flash_object(url, width, height) - - -class Gametrailers(markdown.inlinepatterns.Pattern): - def handleMatch(self, m): - url = 'http://www.gametrailers.com/remote_wrap.php?mid=%s' % \ - m.group('gametrailersid').split('/')[-1] - width = self.ext.config['gametrailers_width'][0] - height = self.ext.config['gametrailers_height'][0] - return flash_object(url, width, height) - - -class Metacafe(markdown.inlinepatterns.Pattern): - def handleMatch(self, m): - url = 'http://www.metacafe.com/fplayer/%s.swf' % m.group('metacafeid') - width = self.ext.config['metacafe_width'][0] - height = self.ext.config['metacafe_height'][0] - return flash_object(url, width, height) - - -class Veoh(markdown.inlinepatterns.Pattern): - def handleMatch(self, m): - url = 'http://www.veoh.com/videodetails2.swf?permalinkId=%s' % m.group('veohid') - width = self.ext.config['veoh_width'][0] - height = self.ext.config['veoh_height'][0] - return flash_object(url, width, height) - - -class Vimeo(markdown.inlinepatterns.Pattern): - def handleMatch(self, m): - url = 'http://vimeo.com/moogaloop.swf?clip_id=%s&server=vimeo.com' % m.group('vimeoid') - width = self.ext.config['vimeo_width'][0] - height = self.ext.config['vimeo_height'][0] - return flash_object(url, width, height) - - -class Yahoo(markdown.inlinepatterns.Pattern): - def handleMatch(self, m): - url = "http://d.yimg.com/static.video.yahoo.com/yep/YV_YEP.swf?ver=2.2.40" - width = self.ext.config['yahoo_width'][0] - height = self.ext.config['yahoo_height'][0] - obj = flash_object(url, width, height) - param = etree.Element('param') - param.set('name', 'flashVars') - param.set('value', "id=%s&vid=%s" % (m.group('yahooid'), - m.group('yahoovid'))) - obj.append(param) - return obj - - -class Youtube(markdown.inlinepatterns.Pattern): - def handleMatch(self, m): - url = 'http://www.youtube.com/v/%s' % m.group('youtubeargs') - width = self.ext.config['youtube_width'][0] - height = self.ext.config['youtube_height'][0] - return flash_object(url, width, height) - - -def flash_object(url, width, height): - obj = etree.Element('object') - obj.set('type', 'application/x-shockwave-flash') - obj.set('width', width) - obj.set('height', height) - obj.set('data', url) - param = etree.Element('param') - param.set('name', 'movie') - param.set('value', url) - obj.append(param) - param = etree.Element('param') - param.set('name', 'allowFullScreen') - param.set('value', 'true') - obj.append(param) - #param = etree.Element('param') - #param.set('name', 'allowScriptAccess') - #param.set('value', 'sameDomain') - #obj.append(param) - return obj - - -def makeExtension(configs=None): - return VideoExtension(configs=configs) - -if __name__ == "__main__": - import doctest - doctest.testmod() diff --git a/lms/djangoapps/simplewiki/mdx_wikipath.py b/lms/djangoapps/simplewiki/mdx_wikipath.py deleted file mode 100755 index 17c2b65591..0000000000 --- a/lms/djangoapps/simplewiki/mdx_wikipath.py +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env python - -''' -Wikipath Extension for Python-Markdown -====================================== - -Converts [Link Name](wiki:ArticleName) to relative links pointing to article. Requires Python-Markdown 2.0+ - -Basic usage: - - >>> import markdown - >>> text = "Some text with a [Link Name](wiki:ArticleName)." - >>> html = markdown.markdown(text, ['wikipath(base_url="/wiki/view/")']) - >>> html - u'

      Some text with a Link Name.

      ' - -Dependencies: -* [Python 2.3+](http://python.org) -* [Markdown 2.0+](http://www.freewisdom.org/projects/python-markdown/) -''' - - -import markdown -try: - # Markdown 2.1.0 changed from 2.0.3. We try importing the new version first, - # but import the 2.0.3 version if it fails - from markdown.util import etree -except: - from markdown import etree - - -class WikiPathExtension(markdown.Extension): - def __init__(self, configs): - # set extension defaults - self.config = { - 'default_namespace': ['edX', 'Default namespace for when one isn\'t specified.'], - 'html_class': ['wikipath', 'CSS hook. Leave blank for none.'] - } - - # Override defaults with user settings - for key, value in configs: - # self.config[key][0] = value - self.setConfig(key, value) - - def extendMarkdown(self, md, md_globals): - self.md = md - - # append to end of inline patterns - WIKI_RE = r'\[(?P.+?)\]\(wiki:(?P[a-zA-Z\d/_-]*)\)' - wikiPathPattern = WikiPath(WIKI_RE, self.config) - wikiPathPattern.md = md - md.inlinePatterns.add('wikipath', wikiPathPattern, " 1 and fname[-1].lower() in WIKI_IMAGE_EXTENSIONS: - return True - return False - - def get_thumb(self): - return self.get_thumb_impl(*WIKI_IMAGE_THUMB_SIZE) - - def get_thumb_small(self): - return self.get_thumb_impl(*WIKI_IMAGE_THUMB_SIZE_SMALL) - - def mk_thumbs(self): - self.mk_thumb(*WIKI_IMAGE_THUMB_SIZE, **{'force': True}) - self.mk_thumb(*WIKI_IMAGE_THUMB_SIZE_SMALL, **{'force': True}) - - def mk_thumb(self, width, height, force=False): - """Requires Python Imaging Library (PIL)""" - if not self.get_size(): - return False - - if not self.is_image(): - return False - - base_path = os.path.dirname(self.file.path) - orig_name = self.filename().split('.') - thumb_filename = "%s__thumb__%d_%d.%s" % ('.'.join(orig_name[:-1]), width, height, orig_name[-1]) - thumb_filepath = "%s%s%s" % (base_path, os.sep, thumb_filename) - - if force or not os.path.exists(thumb_filepath): - try: - import Image - img = Image.open(self.file.path) - img.thumbnail((width, height), Image.ANTIALIAS) - img.save(thumb_filepath) - except IOError: - return False - - return True - - def get_thumb_impl(self, width, height): - """Requires Python Imaging Library (PIL)""" - - if not self.get_size(): - return False - - if not self.is_image(): - return False - - self.mk_thumb(width, height) - - orig_name = self.filename().split('.') - thumb_filename = "%s__thumb__%d_%d.%s" % ('.'.join(orig_name[:-1]), width, height, orig_name[-1]) - thumb_url = settings.MEDIA_URL + WIKI_ATTACHMENTS + self.article.get_url() + '/' + thumb_filename - - return thumb_url - - def __unicode__(self): - return self.filename() - - -class Revision(models.Model): - - article = models.ForeignKey(Article, verbose_name=_('Article')) - revision_text = models.CharField(max_length=255, blank=True, null=True, - verbose_name=_('Description of change')) - revision_user = models.ForeignKey(User, verbose_name=_('Modified by'), - blank=True, null=True, related_name='wiki_revision_user') - revision_date = models.DateTimeField(auto_now_add=True, verbose_name=_('Revision date')) - contents = models.TextField(verbose_name=_('Contents (Use MarkDown format)')) - contents_parsed = models.TextField(editable=False, blank=True, null=True) - counter = models.IntegerField(verbose_name=_('Revision#'), default=1, editable=False) - previous_revision = models.ForeignKey('self', blank=True, null=True, editable=False) - - # Deleted has three values. 0 is normal, non-deleted. 1 is if it was deleted by a normal user. It should - # be a NEW revision, so that it appears in the history. 2 is a special flag that can be applied or removed - # from a normal revision. It means it has been admin-deleted, and can only been seen by an admin. It doesn't - # show up in the history. - deleted = models.IntegerField(verbose_name=_('Deleted group'), default=0) - - def get_user(self): - return self.revision_user if self.revision_user else _('Anonymous') - - # Called after the deleted fied has been changed (between 0 and 2). This bypasses the normal checks put in - # save that update the revision or reject the save if contents haven't changed - def adminSetDeleted(self, deleted): - self.deleted = deleted - super(Revision, self).save() - - def save(self, **kwargs): - # Check if contents have changed... if not, silently ignore save - if self.article and self.article.current_revision: - if self.deleted == 0 and self.article.current_revision.contents == self.contents: - return - else: - import datetime - self.article.modified_on = datetime.datetime.now(UTC) - self.article.save() - - # Increment counter according to previous revision - previous_revision = Revision.objects.filter(article=self.article).order_by('-counter') - if previous_revision.count() > 0: - if previous_revision.count() > previous_revision[0].counter: - self.counter = previous_revision.count() + 1 - else: - self.counter = previous_revision[0].counter + 1 - else: - self.counter = 1 - if (self.article.current_revision and self.article.current_revision.deleted == 0): - self.previous_revision = self.article.current_revision - - # Create pre-parsed contents - no need to parse on-the-fly - ext = WIKI_MARKDOWN_EXTENSIONS - ext += ["wikipath(default_namespace=%s)" % self.article.namespace.name] - self.contents_parsed = markdown(self.contents, - extensions=ext, - safe_mode='escape',) - super(Revision, self).save(**kwargs) - - def delete(self, **kwargs): - """If a current revision is deleted, then regress to the previous - revision or insert a stub, if no other revisions are available""" - article = self.article - if article.current_revision == self: - prev_revision = Revision.objects.filter(article__exact=article, - pk__not=self.pk).order_by('-counter') - if prev_revision: - article.current_revision = prev_revision[0] - article.save() - else: - r = Revision(article=article, - revision_user=article.created_by) - r.contents = unicode(_('Auto-generated stub')) - r.revision_text = unicode(_('Auto-generated stub')) - r.save() - article.current_revision = r - article.save() - super(Revision, self).delete(**kwargs) - - def get_diff(self): - if (self.deleted == 1): - yield "Article Deletion" - return - - if self.previous_revision: - previous = self.previous_revision.contents.splitlines(1) - else: - previous = [] - - # Todo: difflib.HtmlDiff would look pretty for our history pages! - diff = difflib.unified_diff(previous, self.contents.splitlines(1)) - # let's skip the preamble - diff.next(); diff.next(); diff.next() - - for d in diff: - yield d - - def __unicode__(self): - return "r%d" % self.counter - - class Meta: - verbose_name = _('article revision') - verbose_name_plural = _('article revisions') - - -class Permission(models.Model): - permission_name = models.CharField(max_length=255, verbose_name=_('Permission name')) - can_write = models.ManyToManyField(User, blank=True, null=True, related_name='write', - help_text=_('Select none to grant anonymous access.')) - can_read = models.ManyToManyField(User, blank=True, null=True, related_name='read', - help_text=_('Select none to grant anonymous access.')) - - def __unicode__(self): - return self.permission_name - - class Meta: - verbose_name = _('Article permission') - verbose_name_plural = _('Article permissions') - - -class RevisionForm(forms.ModelForm): - contents = forms.CharField(label=_('Contents'), widget=forms.Textarea(attrs={'rows': 8, 'cols': 50})) - - class Meta: - model = Revision - fields = ['contents', 'revision_text'] - - -class RevisionFormWithTitle(forms.ModelForm): - title = forms.CharField(label=_('Title')) - - class Meta: - model = Revision - fields = ['title', 'contents', 'revision_text'] - - -class CreateArticleForm(RevisionForm): - title = forms.CharField(label=_('Title')) - - class Meta: - model = Revision - fields = ['title', 'contents', ] - - -def set_revision(sender, *args, **kwargs): - """Signal handler to ensure that a new revision is always chosen as the - current revision - automatically. It simplifies stuff greatly. Also - stores previous revision for diff-purposes""" - instance = kwargs['instance'] - created = kwargs['created'] - if created and instance.article: - instance.article.current_revision = instance - instance.article.save() - -signals.post_save.connect(set_revision, Revision) diff --git a/lms/djangoapps/simplewiki/templatetags/__init__.py b/lms/djangoapps/simplewiki/templatetags/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/lms/djangoapps/simplewiki/templatetags/simplewiki_utils.py b/lms/djangoapps/simplewiki/templatetags/simplewiki_utils.py deleted file mode 100644 index 6325aeb2bd..0000000000 --- a/lms/djangoapps/simplewiki/templatetags/simplewiki_utils.py +++ /dev/null @@ -1,20 +0,0 @@ -from django import template -from django.conf import settings -from django.template.defaultfilters import stringfilter -from django.utils.http import urlquote as django_urlquote - -from simplewiki.wiki_settings import * - -register = template.Library() - - -@register.filter() -def prepend_media_url(value): - """Prepend user defined media root to url""" - return settings.MEDIA_URL + value - - -@register.filter() -def urlquote(value): - """Prepend user defined media root to url""" - return django_urlquote(value) diff --git a/lms/djangoapps/simplewiki/tests.py b/lms/djangoapps/simplewiki/tests.py deleted file mode 100644 index 6b60485805..0000000000 --- a/lms/djangoapps/simplewiki/tests.py +++ /dev/null @@ -1,23 +0,0 @@ -""" -This file demonstrates two different styles of tests (one doctest and one -unittest). These will both pass when you run "manage.py test". - -Replace these with more appropriate tests for your application. -""" - -from django.test import TestCase - - -class SimpleTest(TestCase): - def test_basic_addition(self): - """ - Tests that 1 + 1 always equals 2. - """ - self.failUnlessEqual(1 + 1, 2) - -__test__ = {"doctest": """ -Another way to test that 1 + 1 is equal to 2. - ->>> 1 + 1 == 2 -True -"""} diff --git a/lms/djangoapps/simplewiki/urls.py b/lms/djangoapps/simplewiki/urls.py deleted file mode 100644 index 629b753654..0000000000 --- a/lms/djangoapps/simplewiki/urls.py +++ /dev/null @@ -1,19 +0,0 @@ -from django.conf.urls import patterns, url - -namespace_regex = r"[a-zA-Z\d._-]+" -article_slug = r'/(?P' + namespace_regex + r'/[a-zA-Z\d_-]*)' -namespace = r'/(?P' + namespace_regex + r')' - -urlpatterns = patterns('', # nopep8 - url(r'^$', 'simplewiki.views.root_redirect', name='wiki_root'), - url(r'^view' + article_slug, 'simplewiki.views.view', name='wiki_view'), - url(r'^view_revision/(?P[0-9]+)' + article_slug, 'simplewiki.views.view_revision', name='wiki_view_revision'), - url(r'^edit' + article_slug, 'simplewiki.views.edit', name='wiki_edit'), - url(r'^create' + article_slug, 'simplewiki.views.create', name='wiki_create'), - url(r'^history' + article_slug + r'(?:/(?P[0-9]+))?$', 'simplewiki.views.history', name='wiki_history'), - url(r'^search_related' + article_slug, 'simplewiki.views.search_add_related', name='search_related'), - url(r'^random/?$', 'simplewiki.views.random_article', name='wiki_random'), - url(r'^revision_feed' + namespace + r'/(?P[0-9]+)?$', 'simplewiki.views.revision_feed', name='wiki_revision_feed'), - url(r'^search' + namespace + r'?$', 'simplewiki.views.search_articles', name='wiki_search_articles'), - url(r'^list' + namespace + r'?$', 'simplewiki.views.search_articles', name='wiki_list_articles'), # Just an alias for the search, but you usually don't submit a search term -) diff --git a/lms/djangoapps/simplewiki/usage.txt b/lms/djangoapps/simplewiki/usage.txt deleted file mode 100644 index 4a74ffaf8e..0000000000 --- a/lms/djangoapps/simplewiki/usage.txt +++ /dev/null @@ -1,800 +0,0 @@ -# Markdown: Syntax - -[TOC] - -## Overview - -### Philosophy - -Markdown is intended to be as easy-to-read and easy-to-write as is feasible. - -Readability, however, is emphasized above all else. A Markdown-formatted -document should be publishable as-is, as plain text, without looking -like it's been marked up with tags or formatting instructions. While -Markdown's syntax has been influenced by several existing text-to-HTML -filters -- including [Setext] [1], [atx] [2], [Textile] [3], [reStructuredText] [4], -[Grutatext] [5], and [EtText] [6] -- the single biggest source of -inspiration for Markdown's syntax is the format of plain text email. - - [1]: http://docutils.sourceforge.net/mirror/setext.html - [2]: http://www.aaronsw.com/2002/atx/ - [3]: http://textism.com/tools/textile/ - [4]: http://docutils.sourceforge.net/rst.html - [5]: http://www.triptico.com/software/grutatxt.html - [6]: http://ettext.taint.org/doc/ - -To this end, Markdown's syntax is comprised entirely of punctuation -characters, which punctuation characters have been carefully chosen so -as to look like what they mean. E.g., asterisks around a word actually -look like \*emphasis\*. Markdown lists look like, well, lists. Even -blockquotes look like quoted passages of text, assuming you've ever -used email. - -### Automatic Escaping for Special Characters - -In HTML, there are two characters that demand special treatment: `<` -and `&`. Left angle brackets are used to start tags; ampersands are -used to denote HTML entities. If you want to use them as literal -characters, you must escape them as entities, e.g. `<`, and -`&`. - -Ampersands in particular are bedeviling for web writers. If you want to -write about 'AT&T', you need to write '`AT&T`'. You even need to -escape ampersands within URLs. Thus, if you want to link to: - - http://images.google.com/images?num=30&q=larry+bird - -you need to encode the URL as: - - http://images.google.com/images?num=30&q=larry+bird - -in your anchor tag `href` attribute. Needless to say, this is easy to -forget, and is probably the single most common source of HTML validation -errors in otherwise well-marked-up web sites. - -Markdown allows you to use these characters naturally, taking care of -all the necessary escaping for you. If you use an ampersand as part of -an HTML entity, it remains unchanged; otherwise it will be translated -into `&`. - -So, if you want to include a copyright symbol in your article, you can write: - - © - -and Markdown will leave it alone. But if you write: - - AT&T - -Markdown will translate it to: - - AT&T - -Similarly, because Markdown supports [inline HTML](#html), if you use -angle brackets as delimiters for HTML tags, Markdown will treat them as -such. But if you write: - - 4 < 5 - -Markdown will translate it to: - - 4 < 5 - -However, inside Markdown code spans and blocks, angle brackets and -ampersands are *always* encoded automatically. This makes it easy to use -Markdown to write about HTML code. (As opposed to raw HTML, which is a -terrible format for writing about HTML syntax, because every single `<` -and `&` in your example code needs to be escaped.) - - -* * * - - -## Block Elements - -### Paragraphs and Line Breaks - -A paragraph is simply one or more consecutive lines of text, separated -by one or more blank lines. (A blank line is any line that looks like a -blank line -- a line containing nothing but spaces or tabs is considered -blank.) Normal paragraphs should not be indented with spaces or tabs. - -The implication of the "one or more consecutive lines of text" rule is -that Markdown supports "hard-wrapped" text paragraphs. This differs -significantly from most other text-to-HTML formatters (including Movable -Type's "Convert Line Breaks" option) which translate every line break -character in a paragraph into a `
      ` tag. - -When you *do* want to insert a `
      ` break tag using Markdown, you -end a line with two or more spaces, then type return. - -Yes, this takes a tad more effort to create a `
      `, but a simplistic -"every line break is a `
      `" rule wouldn't work for Markdown. -Markdown's email-style [blockquoting][bq] and multi-paragraph [list items][l] -work best -- and look better -- when you format them with hard breaks. - - [bq]: #blockquote - [l]: #list - -### Headers - -Markdown supports two styles of headers, [Setext] [1] and [atx] [2]. - -Setext-style headers are "underlined" using equal signs (for first-level -headers) and dashes (for second-level headers). For example: - - This is an H1 - ============= - - This is an H2 - ------------- - - This is an H3 - _____________ - -Any number of underlining `=`'s or `-`'s will work. - -Atx-style headers use 1-6 hash characters at the start of the line, -corresponding to header levels 1-6. For example: - - # This is an H1 - - ## This is an H2 - - ###### This is an H6 - -Optionally, you may "close" atx-style headers. This is purely -cosmetic -- you can use this if you think it looks better. The -closing hashes don't even need to match the number of hashes -used to open the header. (The number of opening hashes -determines the header level.) : - - # This is an H1 # - - ## This is an H2 ## - - ### This is an H3 ###### - - -### Blockquotes - -Markdown uses email-style `>` characters for blockquoting. If you're -familiar with quoting passages of text in an email message, then you -know how to create a blockquote in Markdown. It looks best if you hard -wrap the text and put a `>` before every line: - - > This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet, - > consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus. - > Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus. - > - > Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse - > id sem consectetuer libero luctus adipiscing. - -Markdown allows you to be lazy and only put the `>` before the first -line of a hard-wrapped paragraph: - - > This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet, - consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus. - Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus. - - > Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse - id sem consectetuer libero luctus adipiscing. - -Blockquotes can be nested (i.e. a blockquote-in-a-blockquote) by -adding additional levels of `>`: - - > This is the first level of quoting. - > - > > This is nested blockquote. - > - > Back to the first level. - -Blockquotes can contain other Markdown elements, including headers, lists, -and code blocks: - - > ## This is a header. - > - > 1. This is the first list item. - > 2. This is the second list item. - > - > Here's some example code: - > - > return shell_exec("echo $input | $markdown_script"); - -Any decent text editor should make email-style quoting easy. For -example, with BBEdit, you can make a selection and choose Increase -Quote Level from the Text menu. - - -### Lists - -Markdown supports ordered (numbered) and unordered (bulleted) lists. - -Unordered lists use asterisks, pluses, and hyphens -- interchangably --- as list markers: - - * Red - * Green - * Blue - -is equivalent to: - - + Red - + Green - + Blue - -and: - - - Red - - Green - - Blue - -Ordered lists use numbers followed by periods: - - 1. Bird - 2. McHale - 3. Parish - -It's important to note that the actual numbers you use to mark the -list have no effect on the HTML output Markdown produces. The HTML -Markdown produces from the above list is: - -
        -
      1. Bird
      2. -
      3. McHale
      4. -
      5. Parish
      6. -
      - -If you instead wrote the list in Markdown like this: - - 1. Bird - 1. McHale - 1. Parish - -or even: - - 3. Bird - 1. McHale - 8. Parish - -you'd get the exact same HTML output. The point is, if you want to, -you can use ordinal numbers in your ordered Markdown lists, so that -the numbers in your source match the numbers in your published HTML. -But if you want to be lazy, you don't have to. - -If you do use lazy list numbering, however, you should still start the -list with the number 1. At some point in the future, Markdown may support -starting ordered lists at an arbitrary number. - -List markers typically start at the left margin, but may be indented by -up to three spaces. List markers must be followed by one or more spaces -or a tab. - -To make lists look nice, you can wrap items with hanging indents: - - * Lorem ipsum dolor sit amet, consectetuer adipiscing elit. - Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi, - viverra nec, fringilla in, laoreet vitae, risus. - * Donec sit amet nisl. Aliquam semper ipsum sit amet velit. - Suspendisse id sem consectetuer libero luctus adipiscing. - -But if you want to be lazy, you don't have to: - - * Lorem ipsum dolor sit amet, consectetuer adipiscing elit. - Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi, - viverra nec, fringilla in, laoreet vitae, risus. - * Donec sit amet nisl. Aliquam semper ipsum sit amet velit. - Suspendisse id sem consectetuer libero luctus adipiscing. - -If list items are separated by blank lines, Markdown will wrap the -items in `

      ` tags in the HTML output. For example, this input: - - * Bird - * Magic - -will turn into: - -

        -
      • Bird
      • -
      • Magic
      • -
      - -But this: - - * Bird - - * Magic - -will turn into: - -
        -
      • Bird

      • -
      • Magic

      • -
      - -List items may consist of multiple paragraphs. Each subsequent -paragraph in a list item must be indented by either 4 spaces -or one tab: - - 1. This is a list item with two paragraphs. Lorem ipsum dolor - sit amet, consectetuer adipiscing elit. Aliquam hendrerit - mi posuere lectus. - - Vestibulum enim wisi, viverra nec, fringilla in, laoreet - vitae, risus. Donec sit amet nisl. Aliquam semper ipsum - sit amet velit. - - 2. Suspendisse id sem consectetuer libero luctus adipiscing. - -It looks nice if you indent every line of the subsequent -paragraphs, but here again, Markdown will allow you to be -lazy: - - * This is a list item with two paragraphs. - - This is the second paragraph in the list item. You're - only required to indent the first line. Lorem ipsum dolor - sit amet, consectetuer adipiscing elit. - - * Another item in the same list. - -To put a blockquote within a list item, the blockquote's `>` -delimiters need to be indented: - - * A list item with a blockquote: - - > This is a blockquote - > inside a list item. - -To put a code block within a list item, the code block needs -to be indented *twice* -- 8 spaces or two tabs: - - * A list item with a code block: - - - - -It's worth noting that it's possible to trigger an ordered list by -accident, by writing something like this: - - 1986. What a great season. - -In other words, a *number-period-space* sequence at the beginning of a -line. To avoid this, you can backslash-escape the period: - - 1986\. What a great season. - - - -### Code Blocks - -Pre-formatted code blocks are used for writing about programming or -markup source code. Rather than forming normal paragraphs, the lines -of a code block are interpreted literally. Markdown wraps a code block -in both `
      ` and `` tags.
      -
      -To produce a code block in Markdown, simply indent every line of the
      -block by at least 4 spaces or 1 tab. For example, given this input:
      -
      -    This is a normal paragraph:
      -
      -        This is a code block.
      -
      -Markdown will generate:
      -
      -    

      This is a normal paragraph:

      - -
      This is a code block.
      -    
      - -One level of indentation -- 4 spaces or 1 tab -- is removed from each -line of the code block. For example, this: - - Here is an example of AppleScript: - - tell application "Foo" - beep - end tell - -will turn into: - -

      Here is an example of AppleScript:

      - -
      tell application "Foo"
      -        beep
      -    end tell
      -    
      - -A code block continues until it reaches a line that is not indented -(or the end of the article). - -Within a code block, ampersands (`&`) and angle brackets (`<` and `>`) -are automatically converted into HTML entities. This makes it very -easy to include example HTML source code using Markdown -- just paste -it and indent it, and Markdown will handle the hassle of encoding the -ampersands and angle brackets. For example, this: - - - -will turn into: - -
      <div class="footer">
      -        &copy; 2004 Foo Corporation
      -    </div>
      -    
      - -Regular Markdown syntax is not processed within code blocks. E.g., -asterisks are just literal asterisks within a code block. This means -it's also easy to use Markdown to write about Markdown's own syntax. - - - -### Horizontal Rules - -You can produce a horizontal rule tag (`
      `) by placing three or -more hyphens, asterisks, or underscores on a line by themselves. If you -wish, you may use spaces between the hyphens or asterisks. Each of the -following lines will produce a horizontal rule: - - * * * - - *** - - ***** - - - - - - - --------------------------------------- - - -## Span Elements - -### Links - -Markdown supports two style of links: *inline* and *reference*. - -In both styles, the link text is delimited by [square brackets]. - -To create an inline link, use a set of regular parentheses immediately -after the link text's closing square bracket. Inside the parentheses, -put the URL where you want the link to point, along with an *optional* -title for the link, surrounded in quotes. For example: - - This is [an example](http://example.com/ "Title") inline link. - - [This link](http://example.net/) has no title attribute. - -Will produce: - -

      This is - an example inline link.

      - -

      This link has no - title attribute.

      - -If you're referring to a local resource on the same server, you can -use relative paths: - - See my [About](/about/) page for details. - -Reference-style links use a second set of square brackets, inside -which you place a label of your choosing to identify the link: - - This is [an example][id] reference-style link. - -You can optionally use a space to separate the sets of brackets: - - This is [an example] [id] reference-style link. - -Then, anywhere in the document, you define your link label like this, -on a line by itself: - - [id]: http://example.com/ "Optional Title Here" - -That is: - -* Square brackets containing the link identifier (optionally - indented from the left margin using up to three spaces); -* followed by a colon; -* followed by one or more spaces (or tabs); -* followed by the URL for the link; -* optionally followed by a title attribute for the link, enclosed - in double or single quotes, or enclosed in parentheses. - -The following three link definitions are equivalent: - - [foo]: http://example.com/ "Optional Title Here" - [foo]: http://example.com/ 'Optional Title Here' - [foo]: http://example.com/ (Optional Title Here) - -**Note:** There is a known bug in Markdown.pl 1.0.1 which prevents -single quotes from being used to delimit link titles. - -The link URL may, optionally, be surrounded by angle brackets: - - [id]: "Optional Title Here" - -You can put the title attribute on the next line and use extra spaces -or tabs for padding, which tends to look better with longer URLs: - - [id]: http://example.com/longish/path/to/resource/here - "Optional Title Here" - -Link definitions are only used for creating links during Markdown -processing, and are stripped from your document in the HTML output. - -Link definition names may consist of letters, numbers, spaces, and -punctuation -- but they are *not* case sensitive. E.g. these two -links: - - [link text][a] - [link text][A] - -are equivalent. - -The *implicit link name* shortcut allows you to omit the name of the -link, in which case the link text itself is used as the name. -Just use an empty set of square brackets -- e.g., to link the word -"Google" to the google.com web site, you could simply write: - - [Google][] - -And then define the link: - - [Google]: http://google.com/ - -Because link names may contain spaces, this shortcut even works for -multiple words in the link text: - - Visit [Daring Fireball][] for more information. - -And then define the link: - - [Daring Fireball]: http://daringfireball.net/ - -Link definitions can be placed anywhere in your Markdown document. I -tend to put them immediately after each paragraph in which they're -used, but if you want, you can put them all at the end of your -document, sort of like footnotes. - -Here's an example of reference links in action: - - I get 10 times more traffic from [Google] [1] than from - [Yahoo] [2] or [MSN] [3]. - - [1]: http://google.com/ "Google" - [2]: http://search.yahoo.com/ "Yahoo Search" - [3]: http://search.msn.com/ "MSN Search" - -Using the implicit link name shortcut, you could instead write: - - I get 10 times more traffic from [Google][] than from - [Yahoo][] or [MSN][]. - - [google]: http://google.com/ "Google" - [yahoo]: http://search.yahoo.com/ "Yahoo Search" - [msn]: http://search.msn.com/ "MSN Search" - -Both of the above examples will produce the following HTML output: - -

      I get 10 times more traffic from Google than from - Yahoo - or MSN.

      - -For comparison, here is the same paragraph written using -Markdown's inline link style: - - I get 10 times more traffic from [Google](http://google.com/ "Google") - than from [Yahoo](http://search.yahoo.com/ "Yahoo Search") or - [MSN](http://search.msn.com/ "MSN Search"). - -The point of reference-style links is not that they're easier to -write. The point is that with reference-style links, your document -source is vastly more readable. Compare the above examples: using -reference-style links, the paragraph itself is only 81 characters -long; with inline-style links, it's 176 characters; and as raw HTML, -it's 234 characters. In the raw HTML, there's more markup than there -is text. - -With Markdown's reference-style links, a source document much more -closely resembles the final output, as rendered in a browser. By -allowing you to move the markup-related metadata out of the paragraph, -you can add links without interrupting the narrative flow of your -prose. - -### Emphasis - -Markdown treats asterisks (`*`) and underscores (`_`) as indicators of -emphasis. Text wrapped with one `*` or `_` will be wrapped with an -HTML `` tag; double `*`'s or `_`'s will be wrapped with an HTML -`` tag. E.g., this input: - - *single asterisks* - - _single underscores_ - - **double asterisks** - - __double underscores__ - -will produce: - - single asterisks - - single underscores - - double asterisks - - double underscores - -You can use whichever style you prefer; the lone restriction is that -the same character must be used to open and close an emphasis span. - -Emphasis can be used in the middle of a word: - - un*frigging*believable - -But if you surround an `*` or `_` with spaces, it'll be treated as a -literal asterisk or underscore. - -To produce a literal asterisk or underscore at a position where it -would otherwise be used as an emphasis delimiter, you can backslash -escape it: - - \*this text is surrounded by literal asterisks\* - - -### Code - -To indicate a span of code, wrap it with backtick quotes (`` ` ``). -Unlike a pre-formatted code block, a code span indicates code within a -normal paragraph. For example: - - Use the `printf()` function. - -will produce: - -

      Use the printf() function.

      - -To include a literal backtick character within a code span, you can use -multiple backticks as the opening and closing delimiters: - - ``There is a literal backtick (`) here.`` - -which will produce this: - -

      There is a literal backtick (`) here.

      - -The backtick delimiters surrounding a code span may include spaces -- -one after the opening, one before the closing. This allows you to place -literal backtick characters at the beginning or end of a code span: - - A single backtick in a code span: `` ` `` - - A backtick-delimited string in a code span: `` `foo` `` - -will produce: - -

      A single backtick in a code span: `

      - -

      A backtick-delimited string in a code span: `foo`

      - -With a code span, ampersands and angle brackets are encoded as HTML -entities automatically, which makes it easy to include example HTML -tags. Markdown will turn this: - - Please don't use any `` tags. - -into: - -

      Please don't use any <blink> tags.

      - -You can write this: - - `—` is the decimal-encoded equivalent of `—`. - -to produce: - -

      &#8212; is the decimal-encoded - equivalent of &mdash;.

      - - -### Images - -Admittedly, it's fairly difficult to devise a "natural" syntax for -placing images into a plain text document format. - -Markdown uses an image syntax that is intended to resemble the syntax -for links, allowing for two styles: *inline* and *reference*. - -Inline image syntax looks like this: - - ![Alt text](/path/to/img.jpg) - - ![Alt text](/path/to/img.jpg "Optional title") - -That is: - -* An exclamation mark: `!`; -* followed by a set of square brackets, containing the `alt` - attribute text for the image; -* followed by a set of parentheses, containing the URL or path to - the image, and an optional `title` attribute enclosed in double - or single quotes. - -Reference-style image syntax looks like this: - - ![Alt text][id] - -Where "id" is the name of a defined image reference. Image references -are defined using syntax identical to link references: - - [id]: url/to/image "Optional title attribute" - -As of this writing, Markdown has no syntax for specifying the -dimensions of an image; if this is important to you, you can simply -use regular HTML `` tags. - - -## Miscellaneous - -### Automatic Links - -Markdown supports a shortcut style for creating "automatic" links for URLs and email addresses: simply surround the URL or email address with angle brackets. What this means is that if you want to show the actual text of a URL or email address, and also have it be a clickable link, you can do this: - - - -Markdown will turn this into: - - http://example.com/ - -Automatic links for email addresses work similarly, except that -Markdown will also perform a bit of randomized decimal and hex -entity-encoding to help obscure your address from address-harvesting -spambots. For example, Markdown will turn this: - - - -into something like this: - - address@exa - mple.com - -which will render in a browser as a clickable link to "address@example.com". - -(This sort of entity-encoding trick will indeed fool many, if not -most, address-harvesting bots, but it definitely won't fool all of -them. It's better than nothing, but an address published in this way -will probably eventually start receiving spam.) - - - -### Backslash Escapes - -Markdown allows you to use backslash escapes to generate literal -characters which would otherwise have special meaning in Markdown's -formatting syntax. For example, if you wanted to surround a word -with literal asterisks (instead of an HTML `` tag), you can use -backslashes before the asterisks, like this: - - \*literal asterisks\* - -Markdown provides backslash escapes for the following characters: - - \ backslash - ` backtick - * asterisk - _ underscore - {} curly braces - [] square brackets - () parentheses - # hash mark - + plus sign - - minus sign (hyphen) - . dot - ! exclamation mark - diff --git a/lms/djangoapps/simplewiki/views.py b/lms/djangoapps/simplewiki/views.py deleted file mode 100644 index a84fac6e7d..0000000000 --- a/lms/djangoapps/simplewiki/views.py +++ /dev/null @@ -1,552 +0,0 @@ -# -*- coding: utf-8 -*- -from django.conf import settings as settings -from django.contrib.auth.decorators import login_required -from django.core.context_processors import csrf -from django.core.urlresolvers import reverse -from django.db.models import Q -from django.http import HttpResponse, HttpResponseRedirect, Http404 -from django.utils import simplejson -from django.utils.translation import ugettext_lazy as _ -from mitxmako.shortcuts import render_to_response - -from courseware.courses import get_opt_course_with_access -from courseware.access import has_access -from xmodule.course_module import CourseDescriptor -from xmodule.modulestore.django import modulestore - -from .models import Revision, Article, Namespace, CreateArticleForm, RevisionFormWithTitle, RevisionForm -import wiki_settings - - -def wiki_reverse(wiki_page, article=None, course=None, namespace=None, args=[], kwargs={}): - kwargs = dict(kwargs) # TODO: Figure out why if I don't do this kwargs sometimes contains {'article_path'} - if not 'course_id' in kwargs and course: - kwargs['course_id'] = course.id - if not 'article_path' in kwargs and article: - kwargs['article_path'] = article.get_path() - if not 'namespace' in kwargs and namespace: - kwargs['namespace'] = namespace - return reverse(wiki_page, kwargs=kwargs, args=args) - - -def update_template_dictionary(dictionary, request=None, course=None, article=None, revision=None): - if article: - dictionary['wiki_article'] = article - dictionary['wiki_title'] = article.title # TODO: What is the title when viewing the article in a course? - if not course and 'namespace' not in dictionary: - dictionary['namespace'] = article.namespace.name - - if course: - dictionary['course'] = course - if 'namespace' not in dictionary: - dictionary['namespace'] = "edX" - else: - dictionary['course'] = None - - if revision: - dictionary['wiki_article_revision'] = revision - dictionary['wiki_current_revision_deleted'] = not (revision.deleted == 0) - - if request: - dictionary.update(csrf(request)) - - if request and course: - dictionary['staff_access'] = has_access(request.user, course, 'staff') - else: - dictionary['staff_access'] = False - - -def view(request, article_path, course_id=None): - course = get_opt_course_with_access(request.user, course_id, 'load') - - (article, err) = get_article(request, article_path, course) - if err: - return err - - perm_err = check_permissions(request, article, course, check_read=True, check_deleted=True) - if perm_err: - return perm_err - - d = {} - update_template_dictionary(d, request, course, article, article.current_revision) - return render_to_response('simplewiki/simplewiki_view.html', d) - - -def view_revision(request, revision_number, article_path, course_id=None): - course = get_opt_course_with_access(request.user, course_id, 'load') - - (article, err) = get_article(request, article_path, course) - if err: - return err - - try: - revision = Revision.objects.get(counter=int(revision_number), article=article) - except: - d = {'wiki_err_norevision': revision_number} - update_template_dictionary(d, request, course, article) - return render_to_response('simplewiki/simplewiki_error.html', d) - - perm_err = check_permissions(request, article, course, check_read=True, check_deleted=True, revision=revision) - if perm_err: - return perm_err - - d = {} - update_template_dictionary(d, request, course, article, revision) - - return render_to_response('simplewiki/simplewiki_view.html', d) - - -def root_redirect(request, course_id=None): - course = get_opt_course_with_access(request.user, course_id, 'load') - - #TODO: Add a default namespace to settings. - namespace = "edX" - - try: - root = Article.get_root(namespace) - return HttpResponseRedirect(reverse('wiki_view', kwargs={'course_id': course_id, 'article_path': root.get_path()})) - except: - # If the root is not found, we probably are loading this class for the first time - # We should make sure the namespace exists so the root article can be created. - Namespace.ensure_namespace(namespace) - - err = not_found(request, namespace + '/', course) - return err - - -def create(request, article_path, course_id=None): - course = get_opt_course_with_access(request.user, course_id, 'load') - - article_path_components = article_path.split('/') - - # Ensure the namespace exists - if not len(article_path_components) >= 1 or len(article_path_components[0]) == 0: - d = {'wiki_err_no_namespace': True} - update_template_dictionary(d, request, course) - return render_to_response('simplewiki/simplewiki_error.html', d) - - namespace = None - try: - namespace = Namespace.objects.get(name__exact=article_path_components[0]) - except Namespace.DoesNotExist, ValueError: - d = {'wiki_err_bad_namespace': True} - update_template_dictionary(d, request, course) - return render_to_response('simplewiki/simplewiki_error.html', d) - - # See if the article already exists - article_slug = article_path_components[1] if len(article_path_components) >= 2 else '' - #TODO: Make sure the slug only contains legal characters (which is already done a bit by the url regex) - - try: - existing_article = Article.objects.get(namespace=namespace, slug__exact=article_slug) - #It already exists, so we just redirect to view the article - return HttpResponseRedirect(wiki_reverse("wiki_view", existing_article, course)) - except Article.DoesNotExist: - #This is good. The article doesn't exist - pass - - #TODO: Once we have permissions for namespaces, we should check for create permissions - #check_permissions(request, #namespace#, check_locked=False, check_write=True, check_deleted=True) - - if request.method == 'POST': - f = CreateArticleForm(request.POST) - if f.is_valid(): - article = Article() - article.slug = article_slug - if not request.user.is_anonymous(): - article.created_by = request.user - article.title = f.cleaned_data.get('title') - article.namespace = namespace - a = article.save() - new_revision = f.save(commit=False) - if not request.user.is_anonymous(): - new_revision.revision_user = request.user - new_revision.article = article - new_revision.save() - - return HttpResponseRedirect(wiki_reverse("wiki_view", article, course)) - else: - f = CreateArticleForm(initial={'title': request.GET.get('wiki_article_name', article_slug), - 'contents': _('Headline\n===\n\n')}) - - d = {'wiki_form': f, 'create_article': True, 'namespace': namespace.name} - update_template_dictionary(d, request, course) - - return render_to_response('simplewiki/simplewiki_edit.html', d) - - -def edit(request, article_path, course_id=None): - course = get_opt_course_with_access(request.user, course_id, 'load') - - (article, err) = get_article(request, article_path, course) - if err: - return err - - # Check write permissions - perm_err = check_permissions(request, article, course, check_write=True, check_locked=True, check_deleted=False) - if perm_err: - return perm_err - - if wiki_settings.WIKI_ALLOW_TITLE_EDIT: - EditForm = RevisionFormWithTitle - else: - EditForm = RevisionForm - - if request.method == 'POST': - f = EditForm(request.POST) - if f.is_valid(): - new_revision = f.save(commit=False) - new_revision.article = article - - if request.POST.__contains__('delete'): - if (article.current_revision.deleted == 1): # This article has already been deleted. Redirect - return HttpResponseRedirect(wiki_reverse('wiki_view', article, course)) - new_revision.contents = "" - new_revision.deleted = 1 - elif not new_revision.get_diff(): - return HttpResponseRedirect(wiki_reverse('wiki_view', article, course)) - - if not request.user.is_anonymous(): - new_revision.revision_user = request.user - new_revision.save() - if wiki_settings.WIKI_ALLOW_TITLE_EDIT: - new_revision.article.title = f.cleaned_data['title'] - new_revision.article.save() - return HttpResponseRedirect(wiki_reverse('wiki_view', article, course)) - else: - startContents = article.current_revision.contents if (article.current_revision.deleted == 0) else 'Headline\n===\n\n' - - f = EditForm({'contents': startContents, 'title': article.title}) - - d = {'wiki_form': f} - update_template_dictionary(d, request, course, article) - return render_to_response('simplewiki/simplewiki_edit.html', d) - - -def history(request, article_path, page=1, course_id=None): - course = get_opt_course_with_access(request.user, course_id, 'load') - - (article, err) = get_article(request, article_path, course) - if err: - return err - - perm_err = check_permissions(request, article, course, check_read=True, check_deleted=False) - if perm_err: - return perm_err - - page_size = 10 - - if page is None: - page = 1 - try: - p = int(page) - except ValueError: - p = 1 - - history = Revision.objects.filter(article__exact=article).order_by('-counter').select_related('previous_revision__counter', 'revision_user', 'wiki_article') - - if request.method == 'POST': - if request.POST.__contains__('revision'): # They selected a version, but they can be either deleting or changing the version - perm_err = check_permissions(request, article, course, check_write=True, check_locked=True) - if perm_err: - return perm_err - - redirectURL = wiki_reverse('wiki_view', article, course) - try: - r = int(request.POST['revision']) - revision = Revision.objects.get(id=r) - if request.POST.__contains__('change'): - article.current_revision = revision - article.save() - elif request.POST.__contains__('view'): - redirectURL = wiki_reverse('wiki_view_revision', course=course, - kwargs={'revision_number': revision.counter, 'article_path': article.get_path()}) - #The rese of these are admin functions - elif request.POST.__contains__('delete') and request.user.is_superuser: - if (revision.deleted == 0): - revision.adminSetDeleted(2) - elif request.POST.__contains__('restore') and request.user.is_superuser: - if (revision.deleted == 2): - revision.adminSetDeleted(0) - elif request.POST.__contains__('delete_all') and request.user.is_superuser: - Revision.objects.filter(article__exact=article, deleted=0).update(deleted=2) - elif request.POST.__contains__('lock_article'): - article.locked = not article.locked - article.save() - except Exception as e: - print str(e) - pass - finally: - return HttpResponseRedirect(redirectURL) - # - # - # - # - # - # %else: - # - # - - page_count = (history.count() + (page_size - 1)) / page_size - if p > page_count: - p = 1 - beginItem = (p - 1) * page_size - - next_page = p + 1 if page_count > p else None - prev_page = p - 1 if p > 1 else None - - d = {'wiki_page': p, - 'wiki_next_page': next_page, - 'wiki_prev_page': prev_page, - 'wiki_history': history[beginItem:beginItem + page_size], - 'show_delete_revision': request.user.is_superuser} - update_template_dictionary(d, request, course, article) - - return render_to_response('simplewiki/simplewiki_history.html', d) - - -def revision_feed(request, page=1, namespace=None, course_id=None): - course = get_opt_course_with_access(request.user, course_id, 'load') - - page_size = 10 - - if page is None: - page = 1 - try: - p = int(page) - except ValueError: - p = 1 - - history = Revision.objects.order_by('-revision_date').select_related('revision_user', 'article', 'previous_revision') - - page_count = (history.count() + (page_size - 1)) / page_size - if p > page_count: - p = 1 - beginItem = (p - 1) * page_size - - next_page = p + 1 if page_count > p else None - prev_page = p - 1 if p > 1 else None - - d = {'wiki_page': p, - 'wiki_next_page': next_page, - 'wiki_prev_page': prev_page, - 'wiki_history': history[beginItem:beginItem + page_size], - 'show_delete_revision': request.user.is_superuser, - 'namespace': namespace} - update_template_dictionary(d, request, course) - - return render_to_response('simplewiki/simplewiki_revision_feed.html', d) - - -def search_articles(request, namespace=None, course_id=None): - course = get_opt_course_with_access(request.user, course_id, 'load') - - # blampe: We should check for the presence of other popular django search - # apps and use those if possible. Only fall back on this as a last resort. - # Adding some context to results (eg where matches were) would also be nice. - - # todo: maybe do some perm checking here - - if request.method == 'GET': - querystring = request.GET.get('value', '').strip() - else: - querystring = "" - - results = Article.objects.all() - if namespace: - results = results.filter(namespace__name__exact=namespace) - - if request.user.is_superuser: - results = results.order_by('current_revision__deleted') - else: - results = results.filter(current_revision__deleted=0) - - if querystring: - for queryword in querystring.split(): - # Basic negation is as fancy as we get right now - if queryword[0] == '-' and len(queryword) > 1: - results._search = lambda x: results.exclude(x) - queryword = queryword[1:] - else: - results._search = lambda x: results.filter(x) - - results = results._search(Q(current_revision__contents__icontains=queryword) | \ - Q(title__icontains=queryword)) - - results = results.select_related('current_revision__deleted', 'namespace') - - results = sorted(results, key=lambda article: (article.current_revision.deleted, article.get_path().lower())) - - if len(results) == 1 and querystring: - return HttpResponseRedirect(wiki_reverse('wiki_view', article=results[0], course=course)) - else: - d = {'wiki_search_results': results, - 'wiki_search_query': querystring, - 'namespace': namespace} - update_template_dictionary(d, request, course) - return render_to_response('simplewiki/simplewiki_searchresults.html', d) - - -def search_add_related(request, course_id, slug, namespace): - course = get_opt_course_with_access(request.user, course_id, 'load') - - (article, err) = get_article(request, slug, namespace if namespace else course_id) - if err: - return err - - perm_err = check_permissions(request, article, course, check_read=True) - if perm_err: - return perm_err - - search_string = request.GET.get('query', None) - self_pk = request.GET.get('self', None) - if search_string: - results = [] - related = Article.objects.filter(title__istartswith=search_string) - others = article.related.all() - if self_pk: - related = related.exclude(pk=self_pk) - if others: - related = related.exclude(related__in=others) - related = related.order_by('title')[:10] - for item in related: - results.append({'id': str(item.id), - 'value': item.title, - 'info': item.get_url()}) - else: - results = [] - - json = simplejson.dumps({'results': results}) - return HttpResponse(json, mimetype='application/json') - - -def add_related(request, course_id, slug, namespace): - course = get_opt_course_with_access(request.user, course_id, 'load') - - (article, err) = get_article(request, slug, namespace if namespace else course_id) - if err: - return err - - perm_err = check_permissions(request, article, course, check_write=True, check_locked=True) - if perm_err: - return perm_err - - try: - related_id = request.POST['id'] - rel = Article.objects.get(id=related_id) - has_already = article.related.filter(id=related_id).count() - if has_already == 0 and not rel == article: - article.related.add(rel) - article.save() - except: - pass - finally: - return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),))) - - -def remove_related(request, course_id, namespace, slug, related_id): - course = get_opt_course_with_access(request.user, course_id, 'load') - - (article, err) = get_article(request, slug, namespace if namespace else course_id) - - if err: - return err - - perm_err = check_permissions(request, article, course, check_write=True, check_locked=True) - if perm_err: - return perm_err - - try: - rel_id = int(related_id) - rel = Article.objects.get(id=rel_id) - article.related.remove(rel) - article.save() - except: - pass - finally: - return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),))) - - -def random_article(request, course_id=None): - course = get_opt_course_with_access(request.user, course_id, 'load') - - from random import randint - num_arts = Article.objects.count() - article = Article.objects.all()[randint(0, num_arts - 1)] - return HttpResponseRedirect(wiki_reverse('wiki_view', article, course)) - - -def not_found(request, article_path, course): - """Generate a NOT FOUND message for some URL""" - d = {'wiki_err_notfound': True, - 'article_path': article_path, - 'namespace': "edX"} - update_template_dictionary(d, request, course) - return render_to_response('simplewiki/simplewiki_error.html', d) - - -def get_article(request, article_path, course): - err = None - article = None - - try: - article = Article.get_article(article_path) - except Article.DoesNotExist, ValueError: - err = not_found(request, article_path, course) - - return (article, err) - - -def check_permissions(request, article, course, check_read=False, check_write=False, check_locked=False, check_deleted=False, revision=None): - read_err = check_read and not article.can_read(request.user) - - write_err = check_write and not article.can_write(request.user) - - locked_err = check_locked and article.locked - - if revision is None: - revision = article.current_revision - deleted_err = check_deleted and not (revision.deleted == 0) - if (request.user.is_superuser): - deleted_err = False - locked_err = False - - if read_err or write_err or locked_err or deleted_err: - d = {'wiki_article': article, - 'wiki_err_noread': read_err, - 'wiki_err_nowrite': write_err, - 'wiki_err_locked': locked_err, - 'wiki_err_deleted': deleted_err, } - update_template_dictionary(d, request, course) - # TODO: Make this a little less jarring by just displaying an error - # on the current page? (no such redirect happens for an anon upload yet) - # benjaoming: I think this is the nicest way of displaying an error, but - # these errors shouldn't occur, but rather be prevented on the other pages. - return render_to_response('simplewiki/simplewiki_error.html', d) - else: - return None - -#################### -# LOGIN PROTECTION # -#################### - - -if wiki_settings.WIKI_REQUIRE_LOGIN_VIEW: - view = login_required(view) - history = login_required(history) - search_articles = login_required(search_articles) - root_redirect = login_required(root_redirect) - revision_feed = login_required(revision_feed) - random_article = login_required(random_article) - search_add_related = login_required(search_add_related) - not_found = login_required(not_found) - view_revision = login_required(view_revision) - -if wiki_settings.WIKI_REQUIRE_LOGIN_EDIT: - create = login_required(create) - edit = login_required(edit) - add_related = login_required(add_related) - remove_related = login_required(remove_related) - -if wiki_settings.WIKI_CONTEXT_PREPROCESSORS: - settings.TEMPLATE_CONTEXT_PROCESSORS += wiki_settings.WIKI_CONTEXT_PREPROCESSORS diff --git a/lms/djangoapps/simplewiki/wiki_settings.py b/lms/djangoapps/simplewiki/wiki_settings.py deleted file mode 100644 index 6054ab1909..0000000000 --- a/lms/djangoapps/simplewiki/wiki_settings.py +++ /dev/null @@ -1,111 +0,0 @@ -from django.utils.translation import ugettext_lazy as _ -from django.conf import settings - -# Default settings.. overwrite in your own settings.py - -# Planned feature. -WIKI_USE_MARKUP_WIDGET = True - -#################### -# LOGIN PROTECTION # -#################### -# Before setting the below parameters, please note that permissions can -# be set in the django permission system on individual articles and their -# child articles. In this way you can add a user group and give them -# special permissions, be it on the root article or some other. Permissions -# are inherited on lower levels. - -# Adds standard django login protection for viewing -WIKI_REQUIRE_LOGIN_VIEW = getattr(settings, 'SIMPLE_WIKI_REQUIRE_LOGIN_VIEW', - True) - -# Adds standard django login protection for editing -WIKI_REQUIRE_LOGIN_EDIT = getattr(settings, 'SIMPLE_WIKI_REQUIRE_LOGIN_EDIT', - True) - -#################### -# ATTACHMENTS # -#################### - -# This should be a directory that's writable for the web server. -# It's relative to the MEDIA_ROOT. -WIKI_ATTACHMENTS = getattr(settings, 'SIMPLE_WIKI_ATTACHMENTS', - 'simplewiki/attachments/') - -# If false, attachments will completely disappear -WIKI_ALLOW_ATTACHMENTS = getattr(settings, 'SIMPLE_WIKI_ALLOW_ATTACHMENTS', - False) - -# If WIKI_REQUIRE_LOGIN_EDIT is False, then attachments can still be disallowed -WIKI_ALLOW_ANON_ATTACHMENTS = getattr(settings, 'SIMPLE_WIKI_ALLOW_ANON_ATTACHMENTS', False) - -# Attachments are automatically stored with a dummy extension and delivered -# back to the user with their original extension. -# This setting does not add server security, but might add user security -# if set -- or force users to use standard formats, which might also -# be a good idea. -# Example: ('pdf', 'doc', 'gif', 'jpeg', 'jpg', 'png') -WIKI_ATTACHMENTS_ALLOWED_EXTENSIONS = getattr(settings, 'SIMPLE_WIKI_ATTACHMENTS_ALLOWED_EXTENSIONS', - None) - -# At the moment this variable should not be modified, because -# it breaks compatibility with the normal Django FileField and uploading -# from the admin interface. -WIKI_ATTACHMENTS_ROOT = settings.MEDIA_ROOT - -# Bytes! Default: 1 MB. -WIKI_ATTACHMENTS_MAX = getattr(settings, 'SIMPLE_WIKI_ATTACHMENTS_MAX', - 1 * 1024 * 1024) - -# Allow users to edit titles of pages -# (warning! titles are not maintained in the revision system.) -WIKI_ALLOW_TITLE_EDIT = getattr(settings, 'SIMPLE_WIKI_ALLOW_TITLE_EDIT', False) - -# Global context processors -# These are appended to TEMPLATE_CONTEXT_PROCESSORS in your Django settings -# whenever the wiki is in use. It can be used as a simple, but effective -# way of extending simplewiki without touching original code (and thus keeping -# everything easily maintainable) -WIKI_CONTEXT_PREPROCESSORS = getattr(settings, 'SIMPLE_WIKI_CONTEXT_PREPROCESSORS', - ()) - -#################### -# AESTHETICS # -#################### - -# List of extensions to be used by Markdown. Custom extensions (i.e., with file -# names of mdx_*.py) can be dropped into the simplewiki (or project) directory -# and then added to this list to be utilized. Wiki is enabled automatically. -# -# For more information, see -# http://www.freewisdom.org/projects/python-markdown/Available_Extensions -WIKI_MARKDOWN_EXTENSIONS = getattr(settings, 'SIMPLE_WIKI_MARKDOWN_EXTENSIONS', - ['footnotes', - 'tables', - 'headerid', - 'fenced_code', - 'def_list', - #'codehilite', #This was throwing errors - 'abbr', - 'toc', - 'mathjax', - 'video', # In-line embedding for YouTube, etc. - 'circuit', - ]) - - -WIKI_IMAGE_EXTENSIONS = getattr(settings, - 'SIMPLE_WIKI_IMAGE_EXTENSIONS', - ('jpg', 'jpeg', 'gif', 'png', 'tiff', 'bmp')) -# Planned features -WIKI_PAGE_WIDTH = getattr(settings, - 'SIMPLE_WIKI_PAGE_WIDTH', "100%") - -WIKI_PAGE_ALIGN = getattr(settings, - 'SIMPLE_WIKI_PAGE_ALIGN', "center") - -WIKI_IMAGE_THUMB_SIZE = getattr(settings, - 'SIMPLE_WIKI_IMAGE_THUMB_SIZE', (200, 150)) - -WIKI_IMAGE_THUMB_SIZE_SMALL = getattr(settings, - 'SIMPLE_WIKI_IMAGE_THUMB_SIZE_SMALL', (100, 100)) diff --git a/lms/envs/common.py b/lms/envs/common.py index 076528e91e..6c027f39f6 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -689,7 +689,6 @@ INSTALLED_APPS = ( 'student', 'static_template_view', 'staticbook', - 'simplewiki', 'track', 'util', 'certificates', diff --git a/lms/templates/simplewiki/simplewiki_base.html b/lms/templates/simplewiki/simplewiki_base.html deleted file mode 100644 index e19d8d61ca..0000000000 --- a/lms/templates/simplewiki/simplewiki_base.html +++ /dev/null @@ -1,164 +0,0 @@ -##This file is based on the template from the SimpleWiki source which carries the GPL license - -<%inherit file="../main.html"/> -<%namespace name='static' file='../static_content.html'/> - -<%block name="headextra"> - <%static:css group='course'/> - - -<%! - from django.core.urlresolvers import reverse - from simplewiki.views import wiki_reverse -%> - -<%block name="js_extra"> - - -## TODO (cpennington): Remove this when we have a good way for modules to specify js to load on the page -## and in the wiki - - - - - - - <%block name="wiki_head"/> - - - -<%block name="bodyextra"> - -%if course: -<%include file="/courseware/course_navigation.html" args="active_page='wiki'" /> -%endif - -
      -
      - <%block name="wiki_panel"> -
      -

      Course Wiki

      -
        -
      • -

        - All Articles -

        -
      • - -
      • -

        - Create Article -

        - -
        - <% - baseURL = wiki_reverse("wiki_create", course=course, kwargs={"article_path" : namespace + "/" }) - %> -
        -
        - - -
        -
          -
        • - -
        • -
        -
        -
        -
      • - - -
      - -
      - - -
      - %if wiki_article is not UNDEFINED: -
      - %if wiki_article.locked: -

      This article has been locked

      - %endif -

      Last modified: ${wiki_article.modified_on.strftime("%b %d, %Y, %I:%M %p")}

      - %endif - - %if wiki_article is not UNDEFINED: - -
      - %endif - - <%block name="wiki_page_title"/> - <%block name="wiki_body"/> -
      -
      -
      - diff --git a/lms/templates/simplewiki/simplewiki_edit.html b/lms/templates/simplewiki/simplewiki_edit.html deleted file mode 100644 index 0381a21857..0000000000 --- a/lms/templates/simplewiki/simplewiki_edit.html +++ /dev/null @@ -1,76 +0,0 @@ -##This file is based on the template from the SimpleWiki source which carries the GPL license - -<%inherit file="simplewiki_base.html"/> - -<%block name="title"> - -%if create_article: -Wiki – Create Article – MITx 6.002x -%else: -${"Edit " + wiki_title + " - " if wiki_title is not UNDEFINED else ""}MITx 6.002x Wiki -%endif - - -<%block name="wiki_page_title"> -%if create_article: -

      Create article

      -%else: -

      ${ wiki_article.title }

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

      Oops...

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

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

      -%elif wiki_err_no_namespace is not UNDEFINED and wiki_err_no_namespace: -

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

      -%elif wiki_err_bad_namespace is not UNDEFINED and wiki_err_bad_namespace: -

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

      -%elif wiki_err_locked is not UNDEFINED and wiki_err_locked: -

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

      -%elif wiki_err_noread is not UNDEFINED and wiki_err_noread: -

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

      -%elif wiki_err_nowrite is not UNDEFINED and wiki_err_nowrite: -

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

      -%elif wiki_err_noanon is not UNDEFINED and wiki_err_noanon: -

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

      -%elif wiki_err_create is not UNDEFINED and wiki_err_create: -

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

      -%elif wiki_err_encode is not UNDEFINED and wiki_err_encode: -

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

      -%elif wiki_err_deleted is not UNDEFINED and wiki_err_deleted: -

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

      -%elif wiki_err_norevision is not UNDEFINED: -

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

      -%else: -

      - An error has occured. -

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

      -${ wiki_article.title } -

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

      MITx Additions:

      -

      circuit-schematic:

      -

      $LaTeX Math Expression$

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

      [Article Name](wiki:ArticleName)

      - -

      Useful examples:

      -

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

      -

      Huge Header -
      ====

      -

      Smaller Header -
      -------

      -

      *emphasis* or _emphasis_

      -

      **strong** or __strong__

      -

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

      -

      1. Ordered -
      2. List

      - -

      Need more help? There are several useful guides online.

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

      Revision Feed - Page ${wiki_page}

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

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

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

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

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

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

      Courses loaded in the modulestore

      ' html += '
        ' for cdir, course in def_ms.courses.items(): - html += '
      1. %s (%s)
      2. ' % (settings.MITX_ROOT_URL, - escape(cdir), - escape(cdir), - course.location.url()) + html += '
      3. %s (%s)
      4. ' % ( + settings.MITX_ROOT_URL, + escape(cdir), + escape(cdir), + course.location.url() + ) html += '
      ' #---------------------------------------- diff --git a/lms/djangoapps/open_ended_grading/staff_grading_service.py b/lms/djangoapps/open_ended_grading/staff_grading_service.py index 2c611b4481..6b2b4707bb 100644 --- a/lms/djangoapps/open_ended_grading/staff_grading_service.py +++ b/lms/djangoapps/open_ended_grading/staff_grading_service.py @@ -270,8 +270,10 @@ def get_problem_list(request, course_id): mimetype="application/json") except GradingServiceError: #This is a dev_facing_error - log.exception("Error from staff grading service in open ended grading. server url: {0}" - .format(staff_grading_service().url)) + log.exception( + "Error from staff grading service in open " + "ended grading. server url: {0}".format(staff_grading_service().url) + ) #This is a staff_facing_error return HttpResponse(json.dumps({'success': False, 'error': STAFF_ERROR_MESSAGE})) @@ -285,8 +287,10 @@ def _get_next(course_id, grader_id, location): return staff_grading_service().get_next(course_id, location, grader_id) except GradingServiceError: #This is a dev facing error - log.exception("Error from staff grading service in open ended grading. server url: {0}" - .format(staff_grading_service().url)) + log.exception( + "Error from staff grading service in open " + "ended grading. server url: {0}".format(staff_grading_service().url) + ) #This is a staff_facing_error return json.dumps({'success': False, 'error': STAFF_ERROR_MESSAGE}) diff --git a/lms/djangoapps/static_template_view/tests.py b/lms/djangoapps/static_template_view/tests.py index 9cd5502d5d..813a94e294 100644 --- a/lms/djangoapps/static_template_view/tests.py +++ b/lms/djangoapps/static_template_view/tests.py @@ -21,23 +21,24 @@ class SimpleTest(TestCase): """ # since I had to remap files, pedantically test all press releases # published to date. Decent positive test while we're at it. - all_releases = ["/press/mit-and-harvard-announce-edx", - "/press/uc-berkeley-joins-edx", - "/press/edX-announces-proctored-exam-testing", - "/press/elsevier-collaborates-with-edx", - "/press/ut-joins-edx", - "/press/cengage-to-provide-book-content", - "/press/gates-foundation-announcement", - "/press/wellesley-college-joins-edx", - "/press/georgetown-joins-edx", - "/press/spring-courses", - "/press/lewin-course-announcement", - "/press/bostonx-announcement", - "/press/eric-lander-secret-of-life", - "/press/edx-expands-internationally", - "/press/xblock_announcement", - "/press/stanford-to-work-with-edx", - ] + all_releases = [ + "/press/mit-and-harvard-announce-edx", + "/press/uc-berkeley-joins-edx", + "/press/edX-announces-proctored-exam-testing", + "/press/elsevier-collaborates-with-edx", + "/press/ut-joins-edx", + "/press/cengage-to-provide-book-content", + "/press/gates-foundation-announcement", + "/press/wellesley-college-joins-edx", + "/press/georgetown-joins-edx", + "/press/spring-courses", + "/press/lewin-course-announcement", + "/press/bostonx-announcement", + "/press/eric-lander-secret-of-life", + "/press/edx-expands-internationally", + "/press/xblock_announcement", + "/press/stanford-to-work-with-edx", + ] for rel in all_releases: response = self.client.get(rel) @@ -55,7 +56,7 @@ class SimpleTest(TestCase): response = self.client.get("/press/../homework.html") self.assertEqual(response.status_code, 404) - # "." in is ascii 2E + # "." in is ascii 2E response = self.client.get("/press/%2E%2E/homework.html") self.assertEqual(response.status_code, 404) diff --git a/lms/djangoapps/static_template_view/views.py b/lms/djangoapps/static_template_view/views.py index 56a7f32780..e5a8c43ca8 100644 --- a/lms/djangoapps/static_template_view/views.py +++ b/lms/djangoapps/static_template_view/views.py @@ -15,10 +15,11 @@ from util.cache import cache_if_anonymous valid_templates = [] if settings.STATIC_GRAB: - valid_templates = valid_templates + ['server-down.html', - 'server-error.html' - 'server-overloaded.html', - ] + valid_templates = valid_templates + [ + 'server-down.html', + 'server-error.html' + 'server-overloaded.html', + ] def index(request, template): diff --git a/lms/lib/comment_client/legacy.py b/lms/lib/comment_client/legacy.py index fbf66a09fd..de7ce201ce 100644 --- a/lms/lib/comment_client/legacy.py +++ b/lms/lib/comment_client/legacy.py @@ -5,16 +5,14 @@ def delete_threads(commentable_id, *args, **kwargs): def get_threads(commentable_id, recursive=False, query_params={}, *args, **kwargs): default_params = {'page': 1, 'per_page': 20, 'recursive': recursive} attributes = dict(default_params.items() + query_params.items()) - response = _perform_request('get', _url_for_threads(commentable_id), \ - attributes, *args, **kwargs) + response = _perform_request('get', _url_for_threads(commentable_id), attributes, *args, **kwargs) return response.get('collection', []), response.get('page', 1), response.get('num_pages', 1) def search_threads(course_id, recursive=False, query_params={}, *args, **kwargs): default_params = {'page': 1, 'per_page': 20, 'course_id': course_id, 'recursive': recursive} attributes = dict(default_params.items() + query_params.items()) - response = _perform_request('get', _url_for_search_threads(), \ - attributes, *args, **kwargs) + response = _perform_request('get', _url_for_search_threads(), attributes, *args, **kwargs) return response.get('collection', []), response.get('page', 1), response.get('num_pages', 1) From 033b974047822ecd9b4921e04a6eac79e7485db3 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 20 Jun 2013 10:54:43 -0400 Subject: [PATCH 097/995] Fixed flakey show answer test --- lms/djangoapps/courseware/features/problems.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index 0278ee9b42..094d495b53 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -135,12 +135,10 @@ def action_button_present(_step, buttonname, doesnt_appear): @step(u'the button with the label "([^"]*)" does( not)? appear') def button_with_label_present(step, buttonname, doesnt_appear): - button_css = 'button span.show-label' - elem = world.css_find(button_css).first if doesnt_appear: - assert_not_equal(elem.text, buttonname) + world.browser.is_text_not_present(buttonname, wait_time=5) else: - assert_equal(elem.text, buttonname) + world.browser.is_text_present(buttonname, wait_time=5) @step(u'My "([^"]*)" answer is marked "([^"]*)"') From 1e51bd7314d3580f0d3d0b122c6b6f71c3cc20e9 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 20 Jun 2013 10:58:11 -0400 Subject: [PATCH 098/995] Fixed flakey navigation tests by changing css Now only check for the css that appears when the accordion is done --- lms/djangoapps/courseware/features/navigation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/courseware/features/navigation.py b/lms/djangoapps/courseware/features/navigation.py index e0f82f9251..88c540b232 100644 --- a/lms/djangoapps/courseware/features/navigation.py +++ b/lms/djangoapps/courseware/features/navigation.py @@ -83,9 +83,9 @@ def click_on_section(step, section): world.css_click(section_css) subid = "ui-accordion-accordion-panel-" + str(int(section) - 1) - subsection_css = 'ul[id="%s"]> li > a' % subid + subsection_css = 'ul.ui-accordion-content-active[id=\'%s\'][aria-expanded=\'true\']> li > a' % subid #for some reason needed to do it in two steps - world.css_find(subsection_css).click() + world.css_click(subsection_css) @step(u'I click on subsection "([^"]*)"$') From a8789dced903ff6236974fc268305379cd2ce057 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 20 Jun 2013 11:14:17 -0400 Subject: [PATCH 099/995] Fixed flakey check box by making a css_check function that behaves like css_click As a result, changed inputfield to return the css, and all of the element.fill() to css_fill --- common/djangoapps/terrain/ui_helpers.py | 32 +++++++++++++++++++ .../courseware/features/problems_setup.py | 32 +++++++++---------- 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py index 8e4330d940..9c837cbd0d 100644 --- a/common/djangoapps/terrain/ui_helpers.py +++ b/common/djangoapps/terrain/ui_helpers.py @@ -89,6 +89,38 @@ def css_click(css_selector, index=0, attempts=5, success_condition=lambda:True): return result +@world.absorb +def css_check(css_selector, index=0, attempts=5, success_condition=lambda: True): + """ + Checks a check box based on a CSS selector, retrying if it initially fails. + + This function handles errors that may be thrown if the component cannot be clicked on. + However, there are cases where an error may not be thrown, and yet the operation did not + actually succeed. For those cases, a success_condition lambda can be supplied to verify that the check worked. + + This function will return True if the check worked (taking into account both errors and the optional + success_condition). + """ + assert is_css_present(css_selector) + attempt = 0 + result = False + while attempt < attempts: + try: + world.css_find(css_selector)[index].check() + if success_condition(): + result = True + break + except WebDriverException: + # Occasionally, MathJax or other JavaScript can cover up + # an element temporarily. + # If this happens, wait a second, then try again + world.wait(1) + attempt += 1 + except: + attempt += 1 + return result + + @world.absorb def css_click_at(css, x=10, y=10): ''' diff --git a/lms/djangoapps/courseware/features/problems_setup.py b/lms/djangoapps/courseware/features/problems_setup.py index ce343bb853..b8f817f933 100644 --- a/lms/djangoapps/courseware/features/problems_setup.py +++ b/lms/djangoapps/courseware/features/problems_setup.py @@ -142,34 +142,34 @@ def answer_problem(problem_type, correctness): elif problem_type == "multiple choice": if correctness == 'correct': - inputfield('multiple choice', choice='choice_2').check() + world.css_check(inputfield('multiple choice', choice='choice_2')) else: - inputfield('multiple choice', choice='choice_1').check() + world.css_check(inputfield('multiple choice', choice='choice_1')) elif problem_type == "checkbox": if correctness == 'correct': - inputfield('checkbox', choice='choice_0').check() - inputfield('checkbox', choice='choice_2').check() + world.css_check(inputfield('checkbox', choice='choice_0')) + world.css_check(inputfield('checkbox', choice='choice_2')) else: - inputfield('checkbox', choice='choice_3').check() + world.css_check(inputfield('checkbox', choice='choice_3')) elif problem_type == 'radio': if correctness == 'correct': - inputfield('radio', choice='choice_2').check() + world.css_check(inputfield('radio', choice='choice_2')) else: - inputfield('radio', choice='choice_1').check() + world.css_check(inputfield('radio', choice='choice_1')) elif problem_type == 'string': textvalue = 'correct string' if correctness == 'correct' else 'incorrect' - inputfield('string').fill(textvalue) + world.css_fill(inputfield('string'), textvalue) elif problem_type == 'numerical': textvalue = "pi + 1" if correctness == 'correct' else str(random.randint(-2, 2)) - inputfield('numerical').fill(textvalue) + world.css_fill(inputfield('numerical'), textvalue) elif problem_type == 'formula': textvalue = "x^2+2*x+y" if correctness == 'correct' else 'x^2' - inputfield('formula').fill(textvalue) + world.css_fill(inputfield('formula'), textvalue) elif problem_type == 'script': # Correct answer is any two integers that sum to 10 @@ -181,8 +181,8 @@ def answer_problem(problem_type, correctness): if correctness == 'incorrect': second_addend += random.randint(1, 10) - inputfield('script', input_num=1).fill(str(first_addend)) - inputfield('script', input_num=2).fill(str(second_addend)) + world.css_fill(inputfield('script', input_num=1), str(first_addend)) + world.css_fill(inputfield('script', input_num=2), str(second_addend)) elif problem_type == 'code': # The fake xqueue server is configured to respond @@ -281,7 +281,7 @@ def add_problem_to_course(course, problem_type, extraMeta=None): def inputfield(problem_type, choice=None, input_num=1): - """ Return the element for *problem_type*. + """ Return the css element for *problem_type*. For example, if problem_type is 'string', return the text field for the string problem in the test course. @@ -299,7 +299,7 @@ def inputfield(problem_type, choice=None, input_num=1): assert world.is_css_present(sel) # Retrieve the input element - return world.browser.find_by_css(sel) + return sel def assert_checked(problem_type, choices): @@ -312,7 +312,7 @@ def assert_checked(problem_type, choices): all_choices = ['choice_0', 'choice_1', 'choice_2', 'choice_3'] for this_choice in all_choices: - element = inputfield(problem_type, choice=this_choice) + element = world.css_find(inputfield(problem_type, choice=this_choice)) if this_choice in choices: assert element.checked @@ -321,5 +321,5 @@ def assert_checked(problem_type, choices): def assert_textfield(problem_type, expected_text, input_num=1): - element = inputfield(problem_type, input_num=input_num) + element = world.css_find(inputfield(problem_type, input_num=input_num)) assert element.value == expected_text From c77eb4fd4cb41a74d1c6de5d11579c15c4627b9c Mon Sep 17 00:00:00 2001 From: Peter Fogg Date: Thu, 20 Jun 2013 11:14:33 -0400 Subject: [PATCH 100/995] Fix VideoAlpha acceptance test step definitions to not clash with Video module. --- lms/djangoapps/courseware/features/videoalpha.feature | 4 ++-- lms/djangoapps/courseware/features/videoalpha.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lms/djangoapps/courseware/features/videoalpha.feature b/lms/djangoapps/courseware/features/videoalpha.feature index 2a0acb0f9b..8cff43f45f 100644 --- a/lms/djangoapps/courseware/features/videoalpha.feature +++ b/lms/djangoapps/courseware/features/videoalpha.feature @@ -2,5 +2,5 @@ Feature: Video Alpha component As a student, I want to view course videos in LMS. Scenario: Autoplay is enabled in LMS - Given the course has a Video component - Then when I view the video it has autoplay enabled + Given the course has a Video Alpha component + Then when I view the Video Alpha it has autoplay enabled diff --git a/lms/djangoapps/courseware/features/videoalpha.py b/lms/djangoapps/courseware/features/videoalpha.py index cabf8c681f..4cc1581839 100644 --- a/lms/djangoapps/courseware/features/videoalpha.py +++ b/lms/djangoapps/courseware/features/videoalpha.py @@ -9,12 +9,12 @@ from common import TEST_COURSE_NAME, TEST_SECTION_NAME, i_am_registered_for_the_ ############### ACTIONS #################### -@step('when I view the video it has autoplay enabled') +@step('when I view the Video Alpha it has autoplay enabled') def does_autoplay(step): assert(world.css_find('.videoalpha')[0]['data-autoplay'] == 'True') -@step('the course has a Video component') +@step('the course has a Video Alpha component') def view_videoalpha(step): coursename = TEST_COURSE_NAME.replace(' ', '_') i_am_registered_for_the_course(step, coursename) From 4f78c1977f2256fc832514256f4f967040f7eaff Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Wed, 19 Jun 2013 10:59:24 -0400 Subject: [PATCH 101/995] Allow error messages with non-ascii characters to be handled correctly Also, add a test for this behavior. --- CHANGELOG.rst | 2 ++ common/lib/xmodule/xmodule/capa_module.py | 2 +- .../xmodule/xmodule/tests/test_capa_module.py | 28 +++++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 206be44c87..6a79757c0f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -42,6 +42,8 @@ setting now run entirely outside the Python sandbox. Blades: Added tests for Video Alpha player. +Common: Have the capa module handle unicode better (especially errors) + Blades: Video Alpha bug fix for speed changing to 1.0 in Firefox. Blades: Additional event tracking added to Video Alpha: fullscreen switch, show/hide diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index d9f7fc61aa..85c935c9e7 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -781,7 +781,7 @@ class CapaModule(CapaFields, XModule): # Otherwise, display just an error message, # without a stack trace else: - msg = "Error: %s" % str(inst.message) + msg = u"Error: {msg}".format(msg=inst.message) return {'success': msg} diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py index 696ef58268..85e69cabc1 100644 --- a/common/lib/xmodule/xmodule/tests/test_capa_module.py +++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """Tests of the Capa XModule""" #pylint: disable=C0111 #pylint: disable=R0904 @@ -520,6 +521,33 @@ class CapaModuleTest(unittest.TestCase): # Expect that the number of attempts is NOT incremented self.assertEqual(module.attempts, 1) + def test_check_problem_error_nonascii(self): + + # Try each exception that capa_module should handle + for exception_class in [StudentInputError, + LoncapaProblemError, + ResponseError]: + + # Create the module + module = CapaFactory.create(attempts=1) + + # Ensure that the user is NOT staff + module.system.user_is_staff = False + + # Simulate answering a problem that raises the exception + with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade: + mock_grade.side_effect = exception_class(u"ȧƈƈḗƞŧḗḓ ŧḗẋŧ ƒǿř ŧḗşŧīƞɠ") + + get_request_dict = {CapaFactory.input_key(): '3.14'} + result = module.check_problem(get_request_dict) + + # Expect an AJAX alert message in 'success' + expected_msg = u'Error: ȧƈƈḗƞŧḗḓ ŧḗẋŧ ƒǿř ŧḗşŧīƞɠ' + self.assertEqual(expected_msg, result['success']) + + # Expect that the number of attempts is NOT incremented + self.assertEqual(module.attempts, 1) + def test_check_problem_error_with_staff_user(self): # Try each exception that capa module should handle From 401dd550e477ca0616313f85aa2f64d64dc88a2b Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Tue, 18 Jun 2013 13:14:52 -0400 Subject: [PATCH 102/995] Convert many byte strings to unicode; change string formatting --- common/lib/calc/calc.py | 2 +- common/lib/xmodule/xmodule/capa_module.py | 49 +++++++++++++---------- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/common/lib/calc/calc.py b/common/lib/calc/calc.py index f0934a9ed5..bbfd9545f6 100644 --- a/common/lib/calc/calc.py +++ b/common/lib/calc/calc.py @@ -93,7 +93,7 @@ def check_variables(string, variables): Pyparsing uses a left-to-right parser, which makes a more elegant approach pretty hopeless. """ - general_whitespace = re.compile('[^\\w]+') + general_whitespace = re.compile('[^\\w]+') # TODO consider non-ascii # List of all alnums in string possible_variables = re.split(general_whitespace, string) bad_variables = [] diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 85c935c9e7..3bd8331678 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -60,7 +60,7 @@ class Randomization(String): class ComplexEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, complex): - return "{real:.7g}{imag:+.7g}*j".format(real=obj.real, imag=obj.imag) + return u"{real:.7g}{imag:+.7g}*j".format(real=obj.real, imag=obj.imag) return json.JSONEncoder.default(self, obj) @@ -167,7 +167,7 @@ class CapaModule(CapaFields, XModule): self.seed = self.lcp.seed except Exception as err: - msg = 'cannot create LoncapaProblem {loc}: {err}'.format( + msg = u'cannot create LoncapaProblem {loc}: {err}'.format( loc=self.location.url(), err=err) # TODO (vshnayder): do modules need error handlers too? # We shouldn't be switching on DEBUG. @@ -176,12 +176,15 @@ class CapaModule(CapaFields, XModule): # TODO (vshnayder): This logic should be general, not here--and may # want to preserve the data instead of replacing it. # e.g. in the CMS - msg = '

      %s

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

      %s

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

      {msg}

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

      {tb}

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

      Error:

      %s

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

      %s

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

      Error:

      {msg}

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

      {tb}

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

      ${display_name}

      % endif -<<<<<<< HEAD -%if settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']: -
      -
      -
      -
      -
      -
        -
      • -
      • -
        0:00 / 0:00
        -
      • -
      - -
      -
      -
      -
      -%elif settings.MITX_FEATURES.get('USE_YOUTUBE_OBJECT_API') and youtube_id_1_0: - - - - - -%else: -
      -======= %if settings.MITX_FEATURES.get('USE_YOUTUBE_OBJECT_API') and normal_speed_video_id: %else: -
      ->>>>>>> master +
      From 5f0a89bc271aa0f324d8eb35dcb4bc0f7a838ea6 Mon Sep 17 00:00:00 2001 From: cahrens Date: Thu, 20 Jun 2013 13:16:13 -0400 Subject: [PATCH 108/995] Don't mention edge in the e-mail; same text used for edge and edx. --- cms/templates/emails/activation_email.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/templates/emails/activation_email.txt b/cms/templates/emails/activation_email.txt index 5a1d63b670..4badb4ca88 100644 --- a/cms/templates/emails/activation_email.txt +++ b/cms/templates/emails/activation_email.txt @@ -1,4 +1,4 @@ -Thank you for signing up for edX edge! To activate your account, +Thank you for signing up for edX Studio! To activate your account, please copy and paste this address into your web browser's address bar: From 96c4d2877f9e671d9dea3c5455ac65c441550edc Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 20 Jun 2013 14:05:00 -0400 Subject: [PATCH 109/995] Navigation now has a click success condition --- lms/djangoapps/courseware/features/navigation.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/courseware/features/navigation.py b/lms/djangoapps/courseware/features/navigation.py index 88c540b232..d5e56e7460 100644 --- a/lms/djangoapps/courseware/features/navigation.py +++ b/lms/djangoapps/courseware/features/navigation.py @@ -84,8 +84,11 @@ def click_on_section(step, section): subid = "ui-accordion-accordion-panel-" + str(int(section) - 1) subsection_css = 'ul.ui-accordion-content-active[id=\'%s\'][aria-expanded=\'true\']> li > a' % subid + prev_url = world.browser.url + changed_section = lambda: prev_url != world.browser.url + #for some reason needed to do it in two steps - world.css_click(subsection_css) + world.css_click(subsection_css, success_condition=changed_section) @step(u'I click on subsection "([^"]*)"$') From eec095a195b12e44905fbf245d1aded0ce74f7e2 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 20 Jun 2013 15:38:56 -0400 Subject: [PATCH 110/995] Changed naming of attempts to max_attempts and changed css_selector --- common/djangoapps/terrain/ui_helpers.py | 8 ++++---- lms/djangoapps/courseware/features/navigation.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py index 6e711a5137..6adaf5db89 100644 --- a/common/djangoapps/terrain/ui_helpers.py +++ b/common/djangoapps/terrain/ui_helpers.py @@ -58,7 +58,7 @@ def css_find(css, wait_time=5): @world.absorb -def css_click(css_selector, index=0, attempts=5, success_condition=lambda: True): +def css_click(css_selector, index=0, max_attempts=5, success_condition=lambda: True): """ Perform a click on a CSS selector, retrying if it initially fails. @@ -72,7 +72,7 @@ def css_click(css_selector, index=0, attempts=5, success_condition=lambda: True) assert is_css_present(css_selector) attempt = 0 result = False - while attempt < attempts: + while attempt < max_attempts: try: world.css_find(css_selector)[index].click() if success_condition(): @@ -90,7 +90,7 @@ def css_click(css_selector, index=0, attempts=5, success_condition=lambda: True) @world.absorb -def css_check(css_selector, index=0, attempts=5, success_condition=lambda: True): +def css_check(css_selector, index=0, max_attempts=5, success_condition=lambda: True): """ Checks a check box based on a CSS selector, retrying if it initially fails. @@ -104,7 +104,7 @@ def css_check(css_selector, index=0, attempts=5, success_condition=lambda: True) assert is_css_present(css_selector) attempt = 0 result = False - while attempt < attempts: + while attempt < max_attempts: try: world.css_find(css_selector)[index].check() if success_condition(): diff --git a/lms/djangoapps/courseware/features/navigation.py b/lms/djangoapps/courseware/features/navigation.py index d5e56e7460..96d5a3de93 100644 --- a/lms/djangoapps/courseware/features/navigation.py +++ b/lms/djangoapps/courseware/features/navigation.py @@ -83,7 +83,7 @@ def click_on_section(step, section): world.css_click(section_css) subid = "ui-accordion-accordion-panel-" + str(int(section) - 1) - subsection_css = 'ul.ui-accordion-content-active[id=\'%s\'][aria-expanded=\'true\']> li > a' % subid + subsection_css = 'ul.ui-accordion-content-active[id=\'%s\'] > li > a' % subid prev_url = world.browser.url changed_section = lambda: prev_url != world.browser.url From 281f9003894860443c54a7e9b990b77bad71e675 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 20 Jun 2013 15:40:58 -0400 Subject: [PATCH 111/995] Changed format of docstring --- lms/djangoapps/courseware/features/problems_setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/courseware/features/problems_setup.py b/lms/djangoapps/courseware/features/problems_setup.py index b8f817f933..0438b82fa2 100644 --- a/lms/djangoapps/courseware/features/problems_setup.py +++ b/lms/djangoapps/courseware/features/problems_setup.py @@ -281,11 +281,11 @@ def add_problem_to_course(course, problem_type, extraMeta=None): def inputfield(problem_type, choice=None, input_num=1): - """ Return the css element for *problem_type*. + """ Return the css selector for `problem_type`. For example, if problem_type is 'string', return the text field for the string problem in the test course. - *choice* is the name of the checkbox input in a group + `choice` is the name of the checkbox input in a group of checkboxes. """ sel = ("input#input_i4x-edx-model_course-problem-%s_2_%s" % From b9d79aea605722d177dd3d9b56d2bf23b56ec7f1 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 20 Jun 2013 15:55:53 -0400 Subject: [PATCH 112/995] A bunch of pylint fixes and explicit use case for uploads/test --- .../contentstore/features/common.py | 25 ++++++++++--------- .../contentstore/features/course-team.py | 16 ++++++------ .../contentstore/features/course-updates.py | 18 ++++++------- .../contentstore/features/static-pages.py | 10 ++++---- .../contentstore/features/upload.py | 16 ++++++------ common/test/data/uploads/test | 2 +- 6 files changed, 44 insertions(+), 43 deletions(-) diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index 0b7cb11d2a..e126b746c5 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -3,7 +3,6 @@ from lettuce import world, step from nose.tools import assert_true -from nose.tools import assert_equal from auth.authz import get_user_by_email @@ -13,12 +12,13 @@ import time from logging import getLogger logger = getLogger(__name__) -COURSE_NAME = 'Robot Super Course' -COURSE_NUM = '999' -COURSE_ORG = 'MITx' +_COURSE_NAME = 'Robot Super Course' +_COURSE_NUM = '999' +_COURSE_ORG = 'MITx' ########### STEP HELPERS ############## + @step('I (?:visit|access|open) the Studio homepage$') def i_visit_the_studio_homepage(_step): # To make this go to port 8001, put @@ -78,10 +78,11 @@ def create_studio_user( registration.register(studio_user) registration.activate() + def fill_in_course_info( - name=COURSE_NAME, - org=COURSE_ORG, - num=COURSE_NUM): + name=_COURSE_NAME, + org=_COURSE_ORG, + num=_COURSE_NUM): world.css_fill('.new-course-name', name) world.css_fill('.new-course-org', org) world.css_fill('.new-course-number', num) @@ -108,14 +109,14 @@ def log_into_studio( def create_a_course(): - c = world.CourseFactory.create(org=COURSE_ORG, course=COURSE_NUM, display_name=COURSE_NAME) + world.CourseFactory.create(org=_COURSE_ORG, course=_COURSE_NUM, display_name=_COURSE_NAME) # Add the user to the instructor group of the course # so they will have the permissions to see it in studio - g = world.GroupFactory.create(name='instructor_MITx/{course_num}/{course_name}'.format(course_num=COURSE_NUM, course_name=COURSE_NAME.replace(" ", "_"))) - u = get_user_by_email('robot+studio@edx.org') - u.groups.add(g) - u.save() + course = world.GroupFactory.create(name='instructor_MITx/{course_num}/{course_name}'.format(course_num=_COURSE_NUM, course_name=_COURSE_NAME.replace(" ", "_"))) + user = get_user_by_email('robot+studio@edx.org') + user.groups.add(course) + user.save() world.browser.reload() course_link_css = 'span.class-name' diff --git a/cms/djangoapps/contentstore/features/course-team.py b/cms/djangoapps/contentstore/features/course-team.py index 439eccb265..15c9e5169d 100644 --- a/cms/djangoapps/contentstore/features/course-team.py +++ b/cms/djangoapps/contentstore/features/course-team.py @@ -9,19 +9,19 @@ EMAIL_EXTENSION = '@edx.org' @step(u'I am viewing the course team settings') -def view_grading_settings(step): +def view_grading_settings(_step): world.click_course_settings() link_css = 'li.nav-course-settings-team a' world.css_click(link_css) @step(u'The user "([^"]*)" exists$') -def create_other_user(step, name): +def create_other_user(_step, name): create_studio_user(uname=name, password=PASSWORD, email=(name + EMAIL_EXTENSION)) @step(u'I add "([^"]*)" to the course team') -def add_other_user(step, name): +def add_other_user(_step, name): new_user_css = 'a.new-user-button' world.css_click(new_user_css) @@ -34,18 +34,18 @@ def add_other_user(step, name): @step(u'I delete "([^"]*)" from the course team') -def delete_other_user(step, name): +def delete_other_user(_step, name): to_delete_css = '.remove-user[data-id="{name}{extension}"]'.format(name=name, extension=EMAIL_EXTENSION) world.css_click(to_delete_css) @step(u'"([^"]*)" logs in$') -def other_user_login(step, name): +def other_user_login(_step, name): log_into_studio(uname=name, password=PASSWORD, email=name + EMAIL_EXTENSION) @step(u'He does( not)? see the course on his page') -def see_course(step, doesnt_see_course): +def see_course(_step, doesnt_see_course): class_css = '.class-name' all_courses = world.css_find(class_css) all_names = [item.html for item in all_courses] @@ -56,12 +56,12 @@ def see_course(step, doesnt_see_course): @step(u'He cannot delete users') -def cannot_delete(step): +def cannot_delete(_step): to_delete_css = '.remove-user' assert world.is_css_not_present(to_delete_css) @step(u'He cannot add users') -def cannot_add(step): +def cannot_add(_step): add_css = '.new-user' assert world.is_css_not_present(add_css) diff --git a/cms/djangoapps/contentstore/features/course-updates.py b/cms/djangoapps/contentstore/features/course-updates.py index 3bbdd75d26..e742b6a40c 100644 --- a/cms/djangoapps/contentstore/features/course-updates.py +++ b/cms/djangoapps/contentstore/features/course-updates.py @@ -7,7 +7,7 @@ from common import type_in_codemirror @step(u'I go to the course updates page') -def go_to_uploads(step): +def go_to_uploads(_step): menu_css = 'li.nav-course-courseware' uploads_css = '.nav-course-courseware-updates' world.css_click(menu_css) @@ -15,14 +15,14 @@ def go_to_uploads(step): @step(u'I add a new update with the text "([^"]*)"$') -def add_update(step, text): +def add_update(_step, text): update_css = '.new-update-button' world.css_click(update_css) change_text(text) @step(u'I should( not)? see the update "([^"]*)"$') -def check_update(step, doesnt_see_update, text): +def check_update(_step, doesnt_see_update, text): update_css = '.update-contents' update = world.css_find(update_css) if doesnt_see_update: @@ -32,20 +32,20 @@ def check_update(step, doesnt_see_update, text): @step(u'I modify the text to "([^"]*)"$') -def modify_update(step, text): +def modify_update(_step, text): button_css = '.post-preview .edit-button' world.css_click(button_css) change_text(text) @step(u'I delete the update$') -def click_button(step): +def click_button(_step): button_css = '.post-preview .delete-button' world.css_click(button_css) @step(u'I edit the date to "([^"]*)"$') -def change_date(step, new_date): +def change_date(_step, new_date): button_css = '.post-preview .edit-button' world.css_click(button_css) date_css = 'input.date' @@ -58,21 +58,21 @@ def change_date(step, new_date): @step(u'I should see the date "([^"]*)"$') -def check_date(step, date): +def check_date(_step, date): date_css = '.date-display' date_html = world.css_find(date_css) assert date == date_html.html @step(u'I modify the handout to "([^"]*)"$') -def edit_handouts(step, text): +def edit_handouts(_step, text): edit_css = '.course-handouts > .edit-button' world.css_click(edit_css) change_text(text) @step(u'I see the handout "([^"]*)"$') -def check_handout(step, handout): +def check_handout(_step, handout): handout_css = '.handouts-content' handouts = world.css_find(handout_css) assert handout in handouts.html diff --git a/cms/djangoapps/contentstore/features/static-pages.py b/cms/djangoapps/contentstore/features/static-pages.py index 56d12f18aa..23656690fc 100644 --- a/cms/djangoapps/contentstore/features/static-pages.py +++ b/cms/djangoapps/contentstore/features/static-pages.py @@ -6,7 +6,7 @@ from selenium.webdriver.common.keys import Keys @step(u'I go to the static pages page') -def go_to_uploads(step): +def go_to_uploads(_step): menu_css = 'li.nav-course-courseware' uploads_css = '.nav-course-courseware-pages' world.css_find(menu_css).click() @@ -14,13 +14,13 @@ def go_to_uploads(step): @step(u'I add a new page') -def add_page(step): +def add_page(_step): button_css = '.new-button' world.css_find(button_css).click() @step(u'I should( not)? see a "([^"]*)" static page$') -def see_page(step, doesnt, page): +def see_page(_step, doesnt, page): index = get_index(page) if doesnt: assert index == -1 @@ -29,7 +29,7 @@ def see_page(step, doesnt, page): @step(u'I "([^"]*)" the "([^"]*)" page$') -def click_edit_delete(step, edit_delete, page): +def click_edit_delete(_step, edit_delete, page): button_css = '.%s-button' % edit_delete index = get_index(page) assert index != -1 @@ -37,7 +37,7 @@ def click_edit_delete(step, edit_delete, page): @step(u'I change the name to "([^"]*)"$') -def change_name(step, new_name): +def change_name(_step, new_name): settings_css = '#settings-mode' world.css_find(settings_css).click() input_css = '.setting-input' diff --git a/cms/djangoapps/contentstore/features/upload.py b/cms/djangoapps/contentstore/features/upload.py index 9b049ccc78..7ef782ea13 100644 --- a/cms/djangoapps/contentstore/features/upload.py +++ b/cms/djangoapps/contentstore/features/upload.py @@ -13,7 +13,7 @@ HTTP_PREFIX = "http://localhost:8001" @step(u'I go to the files and uploads page') -def go_to_uploads(step): +def go_to_uploads(_step): menu_css = 'li.nav-course-courseware' uploads_css = '.nav-course-courseware-uploads' world.css_find(menu_css).click() @@ -21,7 +21,7 @@ def go_to_uploads(step): @step(u'I upload the file "([^"]*)"$') -def upload_file(step, file_name): +def upload_file(_step, file_name): upload_css = '.upload-button' world.css_find(upload_css).click() @@ -36,7 +36,7 @@ def upload_file(step, file_name): @step(u'I should( not)? see the file "([^"]*)" was uploaded$') -def check_upload(step, do_not_see_file, file_name): +def check_upload(_step, do_not_see_file, file_name): index = get_index(file_name) if do_not_see_file: assert index == -1 @@ -45,13 +45,13 @@ def check_upload(step, do_not_see_file, file_name): @step(u'The url for the file "([^"]*)" is valid$') -def check_url(step, file_name): +def check_url(_step, file_name): r = get_file(file_name) assert r.status_code == 200 @step(u'I delete the file "([^"]*)"$') -def delete_file(step, file_name): +def delete_file(_step, file_name): index = get_index(file_name) assert index != -1 delete_css = ".remove-asset-button" @@ -62,7 +62,7 @@ def delete_file(step, file_name): @step(u'I should see only one "([^"]*)"$') -def no_duplicate(step, file_name): +def no_duplicate(_step, file_name): names_css = '.name-col > a.filename' all_names = world.css_find(names_css) only_one = False @@ -73,7 +73,7 @@ def no_duplicate(step, file_name): @step(u'I can download the correct "([^"]*)" file$') -def check_download(step, file_name): +def check_download(_step, file_name): path = os.path.join(TEST_ROOT, 'uploads/', file_name) with open(os.path.abspath(path), 'r') as cur_file: cur_text = cur_file.read() @@ -83,7 +83,7 @@ def check_download(step, file_name): @step(u'I modify "([^"]*)"$') -def modify_upload(step, file_name): +def modify_upload(_step, file_name): new_text = ''.join(random.choice(string.ascii_uppercase + string.digits) for x in range(10)) path = os.path.join(TEST_ROOT, 'uploads/', file_name) with open(os.path.abspath(path), 'w') as cur_file: diff --git a/common/test/data/uploads/test b/common/test/data/uploads/test index 27bb8ecaac..0424951e34 100644 --- a/common/test/data/uploads/test +++ b/common/test/data/uploads/test @@ -1 +1 @@ -R22VMJ2M \ No newline at end of file +This is an arbitrary file for testing uploads From 34f8c044b8a8f97d9a97e72cd0f7981a3ea0e5f3 Mon Sep 17 00:00:00 2001 From: cahrens Date: Thu, 20 Jun 2013 16:06:00 -0400 Subject: [PATCH 113/995] Pep8 cleaning. --- .../features/advanced-settings.py | 6 +++-- .../contentstore/tests/test_checklists.py | 2 -- .../tests/test_course_settings.py | 22 ++++++++++++++----- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/cms/djangoapps/contentstore/features/advanced-settings.py b/cms/djangoapps/contentstore/features/advanced-settings.py index 473fc20a68..2360baea5a 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.py +++ b/cms/djangoapps/contentstore/features/advanced-settings.py @@ -31,8 +31,10 @@ def press_the_notification_button(step, name): # Save was clicked if either the save notification bar is gone, or we have a error notification # overlaying it (expected in the case of typing Object into display_name). - save_clicked = lambda: world.is_css_not_present('.is-shown.wrapper-notification-warning') or\ - world.is_css_present('.is-shown.wrapper-notification-error') + def save_clicked(): + confirmation_dismissed = world.is_css_not_present('.is-shown.wrapper-notification-warning') + error_showing = world.is_css_present('.is-shown.wrapper-notification-error') + return confirmation_dismissed or error_showing assert_true(world.css_click(css, success_condition=save_clicked), 'Save button not clicked after 5 attempts.') diff --git a/cms/djangoapps/contentstore/tests/test_checklists.py b/cms/djangoapps/contentstore/tests/test_checklists.py index 0e5cd9b884..52e9ba14fe 100644 --- a/cms/djangoapps/contentstore/tests/test_checklists.py +++ b/cms/djangoapps/contentstore/tests/test_checklists.py @@ -19,7 +19,6 @@ class ChecklistTestCase(CourseTestCase): modulestore = get_modulestore(self.course.location) return modulestore.get_item(self.course.location).checklists - def compare_checklists(self, persisted, request): """ Handles url expansion as possible difference and descends into guts @@ -99,7 +98,6 @@ class ChecklistTestCase(CourseTestCase): 'name': self.course.location.name, 'checklist_index': 2}) - def get_first_item(checklist): return checklist['items'][0] diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py index 6b8622f992..5c2a15ac87 100644 --- a/cms/djangoapps/contentstore/tests/test_course_settings.py +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -131,9 +131,14 @@ class CourseDetailsTestCase(CourseTestCase): @override_settings(MKTG_URLS={'ROOT': 'dummy-root'}) def test_marketing_site_fetch(self): - settings_details_url = reverse('settings_details', - kwargs={'org': self.course_location.org, 'name': self.course_location.name, - 'course': self.course_location.course}) + settings_details_url = reverse( + 'settings_details', + kwargs={ + 'org': self.course_location.org, + 'name': self.course_location.name, + 'course': self.course_location.course + } + ) with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': True}): response = self.client.get(settings_details_url) @@ -150,9 +155,14 @@ class CourseDetailsTestCase(CourseTestCase): self.assertNotContains(response, "Requirements") def test_regular_site_fetch(self): - settings_details_url = reverse('settings_details', - kwargs={'org': self.course_location.org, 'name': self.course_location.name, - 'course': self.course_location.course}) + settings_details_url = reverse( + 'settings_details', + kwargs={ + 'org': self.course_location.org, + 'name': self.course_location.name, + 'course': self.course_location.course + } + ) with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': False}): response = self.client.get(settings_details_url) From 106e0aae01931920bdd0465fbe11ace727410e71 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Mon, 10 Jun 2013 09:02:22 -0400 Subject: [PATCH 114/995] Clean tests step now removes stale coverage report files. Added test cleaning to fasttest_* instead of test_*, so that it always gets executed for lms and cms tests. Added comment to `directory` command for tests.rake Separated cleaning of test fixtures from cleaning of reports directory to resolve conflicts with collectstatic. --- rakelib/tests.rake | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/rakelib/tests.rake b/rakelib/tests.rake index 20bd34f4e8..f169d28256 100644 --- a/rakelib/tests.rake +++ b/rakelib/tests.rake @@ -1,6 +1,9 @@ # Set up the clean and clobber tasks CLOBBER.include(REPORT_DIR, 'test_root/*_repo', 'test_root/staticfiles') +# Create the directory to hold coverage reports, if it doesn't already exist. +directory REPORT_DIR + def run_under_coverage(cmd, root) cmd0, cmd_rest = cmd.split(" ", 2) # We use "python -m coverage" so that the proper python will run the importable coverage @@ -45,12 +48,19 @@ task :test_docs do test_sh('rake builddocs[pub]') end -directory REPORT_DIR - task :clean_test_files do + desc "Clean fixture files used by tests" sh("git clean -fqdx test_root") end +task :clean_reports_dir do + desc "Clean coverage files, to ensure that we don't use stale data to generate reports." + # We delete the files but preserve the directory structure + # so that coverage.py has a place to put the reports. + sh("find #{REPORT_DIR} -type f -delete") +end + + TEST_TASK_DIRS = [] [:lms, :cms].each do |system| @@ -58,21 +68,21 @@ TEST_TASK_DIRS = [] # Per System tasks desc "Run all django tests on our djangoapps for the #{system}" - task "test_#{system}", [:test_id] => ["clean_test_files", :predjango, "#{system}:gather_assets:test", "fasttest_#{system}"] + task "test_#{system}", [:test_id] => [:clean_test_files, :predjango, "#{system}:gather_assets:test", "fasttest_#{system}"] # Have a way to run the tests without running collectstatic -- useful when debugging without # messing with static files. - task "fasttest_#{system}", [:test_id] => [report_dir, :install_prereqs, :predjango] do |t, args| + task "fasttest_#{system}", [:test_id] => [report_dir, :clean_reports_dir, :install_prereqs, :predjango] do |t, args| args.with_defaults(:test_id => nil) run_tests(system, report_dir, args.test_id) end # Run acceptance tests desc "Run acceptance tests" - task "test_acceptance_#{system}", [:harvest_args] => ["#{system}:gather_assets:acceptance", "fasttest_acceptance_#{system}"] + task "test_acceptance_#{system}", [:harvest_args] => [:clean_test_files, "#{system}:gather_assets:acceptance", "fasttest_acceptance_#{system}"] desc "Run acceptance tests without collectstatic" - task "fasttest_acceptance_#{system}", [:harvest_args] => ["clean_test_files", :predjango, report_dir] do |t, args| + task "fasttest_acceptance_#{system}", [:harvest_args] => [:clean_reports_dir, :predjango, report_dir] do |t, args| args.with_defaults(:harvest_args => '') run_acceptance_tests(system, report_dir, args.harvest_args) end @@ -88,7 +98,7 @@ Dir["common/lib/*"].select{|lib| File.directory?(lib)}.each do |lib| report_dir = report_dir_path(lib) desc "Run tests for common lib #{lib}" - task "test_#{lib}" => ["clean_test_files", report_dir] do + task "test_#{lib}" => [:clean_reports_dir, report_dir] do ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml") cmd = "nosetests #{lib}" test_sh(run_under_coverage(cmd, lib)) From 70d48e2e9fd20d7eac0289fccb24210d68c8af06 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 20 Jun 2013 16:40:13 -0400 Subject: [PATCH 115/995] Now referencing css element --- .../contentstore/features/course-team.py | 14 +++++----- .../contentstore/features/course-updates.py | 26 +++++++++---------- .../contentstore/features/static-pages.py | 14 +++++----- .../contentstore/features/upload.py | 15 ++++++----- common/test/data/uploads/test | 2 +- 5 files changed, 36 insertions(+), 35 deletions(-) diff --git a/cms/djangoapps/contentstore/features/course-team.py b/cms/djangoapps/contentstore/features/course-team.py index 15c9e5169d..4303d5066c 100644 --- a/cms/djangoapps/contentstore/features/course-team.py +++ b/cms/djangoapps/contentstore/features/course-team.py @@ -2,7 +2,7 @@ #pylint: disable=W0621 from lettuce import world, step -from common import create_studio_user, log_into_studio, COURSE_NAME +from common import create_studio_user, log_into_studio, _COURSE_NAME PASSWORD = 'test' EMAIL_EXTENSION = '@edx.org' @@ -35,7 +35,7 @@ def add_other_user(_step, name): @step(u'I delete "([^"]*)" from the course team') def delete_other_user(_step, name): - to_delete_css = '.remove-user[data-id="{name}{extension}"]'.format(name=name, extension=EMAIL_EXTENSION) + to_delete_css = 'a.remove-user[data-id="{name}{extension}"]'.format(name=name, extension=EMAIL_EXTENSION) world.css_click(to_delete_css) @@ -46,22 +46,22 @@ def other_user_login(_step, name): @step(u'He does( not)? see the course on his page') def see_course(_step, doesnt_see_course): - class_css = '.class-name' + class_css = 'span.class-name' all_courses = world.css_find(class_css) all_names = [item.html for item in all_courses] if doesnt_see_course: - assert not COURSE_NAME in all_names + assert not _COURSE_NAME in all_names else: - assert COURSE_NAME in all_names + assert _COURSE_NAME in all_names @step(u'He cannot delete users') def cannot_delete(_step): - to_delete_css = '.remove-user' + to_delete_css = 'a.remove-user' assert world.is_css_not_present(to_delete_css) @step(u'He cannot add users') def cannot_add(_step): - add_css = '.new-user' + add_css = 'a.new-user' assert world.is_css_not_present(add_css) diff --git a/cms/djangoapps/contentstore/features/course-updates.py b/cms/djangoapps/contentstore/features/course-updates.py index e742b6a40c..d838061698 100644 --- a/cms/djangoapps/contentstore/features/course-updates.py +++ b/cms/djangoapps/contentstore/features/course-updates.py @@ -7,23 +7,23 @@ from common import type_in_codemirror @step(u'I go to the course updates page') -def go_to_uploads(_step): +def go_to_updates(_step): menu_css = 'li.nav-course-courseware' - uploads_css = '.nav-course-courseware-updates' + updates_css = 'li.nav-course-courseware-updates' world.css_click(menu_css) - world.css_click(uploads_css) + world.css_click(updates_css) @step(u'I add a new update with the text "([^"]*)"$') def add_update(_step, text): - update_css = '.new-update-button' + update_css = 'a.new-update-button' world.css_click(update_css) change_text(text) @step(u'I should( not)? see the update "([^"]*)"$') def check_update(_step, doesnt_see_update, text): - update_css = '.update-contents' + update_css = 'div.update-contents' update = world.css_find(update_css) if doesnt_see_update: assert len(update) == 0 or not text in update.html @@ -33,52 +33,52 @@ def check_update(_step, doesnt_see_update, text): @step(u'I modify the text to "([^"]*)"$') def modify_update(_step, text): - button_css = '.post-preview .edit-button' + button_css = 'div.post-preview a.edit-button' world.css_click(button_css) change_text(text) @step(u'I delete the update$') def click_button(_step): - button_css = '.post-preview .delete-button' + button_css = 'div.post-preview a.delete-button' world.css_click(button_css) @step(u'I edit the date to "([^"]*)"$') def change_date(_step, new_date): - button_css = '.post-preview .edit-button' + button_css = 'div.post-preview a.edit-button' world.css_click(button_css) date_css = 'input.date' date = world.css_find(date_css) for i in range(len(date.value)): date._element.send_keys(Keys.END, Keys.BACK_SPACE) date._element.send_keys(new_date) - save_css = '.save-button' + save_css = 'a.save-button' world.css_click(save_css) @step(u'I should see the date "([^"]*)"$') def check_date(_step, date): - date_css = '.date-display' + date_css = 'span.date-display' date_html = world.css_find(date_css) assert date == date_html.html @step(u'I modify the handout to "([^"]*)"$') def edit_handouts(_step, text): - edit_css = '.course-handouts > .edit-button' + edit_css = 'div.course-handouts > a.edit-button' world.css_click(edit_css) change_text(text) @step(u'I see the handout "([^"]*)"$') def check_handout(_step, handout): - handout_css = '.handouts-content' + handout_css = 'div.handouts-content' handouts = world.css_find(handout_css) assert handout in handouts.html def change_text(text): type_in_codemirror(0, text) - save_css = '.save-button' + save_css = 'a.save-button' world.css_click(save_css) diff --git a/cms/djangoapps/contentstore/features/static-pages.py b/cms/djangoapps/contentstore/features/static-pages.py index 23656690fc..a16a3246da 100644 --- a/cms/djangoapps/contentstore/features/static-pages.py +++ b/cms/djangoapps/contentstore/features/static-pages.py @@ -6,16 +6,16 @@ from selenium.webdriver.common.keys import Keys @step(u'I go to the static pages page') -def go_to_uploads(_step): +def go_to_static(_step): menu_css = 'li.nav-course-courseware' - uploads_css = '.nav-course-courseware-pages' + static_css = 'li.nav-course-courseware-pages' world.css_find(menu_css).click() - world.css_find(uploads_css).click() + world.css_find(static_css).click() @step(u'I add a new page') def add_page(_step): - button_css = '.new-button' + button_css = 'a.new-button' world.css_find(button_css).click() @@ -30,7 +30,7 @@ def see_page(_step, doesnt, page): @step(u'I "([^"]*)" the "([^"]*)" page$') def click_edit_delete(_step, edit_delete, page): - button_css = '.%s-button' % edit_delete + button_css = 'a.%s-button' % edit_delete index = get_index(page) assert index != -1 world.css_find(button_css)[index].click() @@ -40,13 +40,13 @@ def click_edit_delete(_step, edit_delete, page): def change_name(_step, new_name): settings_css = '#settings-mode' world.css_find(settings_css).click() - input_css = '.setting-input' + input_css = 'input.setting-input' name_input = world.css_find(input_css) old_name = name_input.value for count in range(len(old_name)): name_input._element.send_keys(Keys.END, Keys.BACK_SPACE) name_input._element.send_keys(new_name) - save_button = '.save-button' + save_button = 'a.save-button' world.css_find(save_button).click() diff --git a/cms/djangoapps/contentstore/features/upload.py b/cms/djangoapps/contentstore/features/upload.py index 7ef782ea13..5bf082c774 100644 --- a/cms/djangoapps/contentstore/features/upload.py +++ b/cms/djangoapps/contentstore/features/upload.py @@ -15,23 +15,23 @@ HTTP_PREFIX = "http://localhost:8001" @step(u'I go to the files and uploads page') def go_to_uploads(_step): menu_css = 'li.nav-course-courseware' - uploads_css = '.nav-course-courseware-uploads' + uploads_css = 'li.nav-course-courseware-uploads' world.css_find(menu_css).click() world.css_find(uploads_css).click() @step(u'I upload the file "([^"]*)"$') def upload_file(_step, file_name): - upload_css = '.upload-button' + upload_css = 'a.upload-button' world.css_find(upload_css).click() - file_css = '.file-input' + file_css = 'input.file-input' upload = world.css_find(file_css) #uploading the file itself path = os.path.join(TEST_ROOT, 'uploads/', file_name) upload._element.send_keys(os.path.abspath(path)) - close_css = '.close-button' + close_css = 'a.close-button' world.css_find(close_css).click() @@ -54,7 +54,8 @@ def check_url(_step, file_name): def delete_file(_step, file_name): index = get_index(file_name) assert index != -1 - delete_css = ".remove-asset-button" + from pdb import set_trace; set_trace() + delete_css = "a.remove-asset-button" world.css_click(delete_css, index=index) prompt_confirm_css = 'li.nav-item > a.action-primary' @@ -63,7 +64,7 @@ def delete_file(_step, file_name): @step(u'I should see only one "([^"]*)"$') def no_duplicate(_step, file_name): - names_css = '.name-col > a.filename' + names_css = 'td.name-col > a.filename' all_names = world.css_find(names_css) only_one = False for i in range(len(all_names)): @@ -91,7 +92,7 @@ def modify_upload(_step, file_name): def get_index(file_name): - names_css = '.name-col > a.filename' + names_css = 'td.name-col > a.filename' all_names = world.css_find(names_css) for i in range(len(all_names)): if file_name == all_names[i].html: diff --git a/common/test/data/uploads/test b/common/test/data/uploads/test index 0424951e34..f019db7176 100644 --- a/common/test/data/uploads/test +++ b/common/test/data/uploads/test @@ -1 +1 @@ -This is an arbitrary file for testing uploads +R2FUIGM88K \ No newline at end of file From 4cb6eb6e19c6b338d095864389e9ac73dffda753 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 20 Jun 2013 10:42:16 -0400 Subject: [PATCH 116/995] Fix pylint violations in xmodule static_content.py --- common/lib/xmodule/xmodule/static_content.py | 31 ++++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/static_content.py b/common/lib/xmodule/xmodule/static_content.py index 7662499c16..2cadd34df1 100755 --- a/common/lib/xmodule/xmodule/static_content.py +++ b/common/lib/xmodule/xmodule/static_content.py @@ -20,22 +20,27 @@ LOG = logging.getLogger(__name__) def write_module_styles(output_root): + """Write all registered XModule css, sass, and scss files to output root.""" return _write_styles('.xmodule_display', output_root, _list_modules()) def write_module_js(output_root): + """Write all registered XModule js and coffee files to output root.""" return _write_js(output_root, _list_modules()) def write_descriptor_styles(output_root): + """Write all registered XModuleDescriptor css, sass, and scss files to output root.""" return _write_styles('.xmodule_edit', output_root, _list_descriptors()) def write_descriptor_js(output_root): + """Write all registered XModuleDescriptor js and coffee files to output root.""" return _write_js(output_root, _list_descriptors()) def _list_descriptors(): + """Return a list of all registered XModuleDescriptor classes.""" return [ desc for desc in [ desc for (_, desc) in XModuleDescriptor.load_classes() @@ -44,6 +49,7 @@ def _list_descriptors(): def _list_modules(): + """Return a list of all registered XModule classes.""" return [ desc.module_class for desc @@ -51,9 +57,10 @@ def _list_modules(): ] -def _ensure_dir(dir_): +def _ensure_dir(directory): + """Ensure that `directory` exists.""" try: - os.makedirs(dir_) + os.makedirs(directory) except OSError as exc: if exc.errno == errno.EEXIST: pass @@ -131,6 +138,19 @@ def _write_js(output_root, classes): def _write_files(output_root, contents, generated_suffix_map=None): + """ + Write file contents to output root. + + Any files not listed in contents that exists in output_root will be deleted, + unless it matches one of the patterns in `generated_suffix_map`. + + output_root (path): The root directory to write the file contents in + contents (dict): A map from filenames to file contents to be written to the output_root + generated_suffix_map (dict): Optional. Maps file suffix to generated file suffix. + For any file in contents, if the suffix matches a key in `generated_suffix_map`, + then the same filename with the suffix replaced by the value from `generated_suffix_map` + will be ignored + """ _ensure_dir(output_root) to_delete = set(file.basename() for file in output_root.files()) - set(contents.keys()) @@ -146,7 +166,12 @@ def _write_files(output_root, contents, generated_suffix_map=None): for filename, file_content in contents.iteritems(): output_file = output_root / filename - if not output_file.isfile() or output_file.read_md5() != hashlib.md5(file_content).digest(): + not_file = not output_file.isfile() + + # not_file is included to short-circuit this check, because + # read_md5 depends on the file already existing + write_file = not_file or output_file.read_md5() != hashlib.md5(file_content).digest() # pylint: disable=E1121 + if write_file: LOG.debug("Writing %s", output_file) output_file.write_bytes(file_content) else: From a52d0beb41a75b493ed5d6d3110790a9edd0c505 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 20 Jun 2013 17:00:46 -0400 Subject: [PATCH 117/995] 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 118/995] Fix typo in output message of rakefile --- rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rakefile b/rakefile index 96bd4c2e96..2cf442bca9 100644 --- a/rakefile +++ b/rakefile @@ -3,7 +3,7 @@ begin require 'rake/clean' require './rakelib/helpers.rb' rescue LoadError => error - puts "Import faild (#{error})" + puts "Import failed (#{error})" puts "Please run `bundle install` to bootstrap ruby dependencies" exit 1 end From aa4e27f77534ad35bece457e097f5226bb95d29a Mon Sep 17 00:00:00 2001 From: Jason Bau Date: Thu, 20 Jun 2013 18:12:06 -0700 Subject: [PATCH 119/995] Shib PR responses to @cpennington and @ormsbee comments * Changed unicode test cases to ascii encoding * Removed 'stanford' hardcoding in TOS logic in lieu of 'SHIB_DISABLE_TOS' MIT_FEATURES flag * made 'external_auth' always an installed_app in lms * log.exception changd to log.error where appropriate But: did not change skipping tests to changing settings, for reasons stated here: https://github.com/edx/edx-platform/pull/67#issuecomment-19790330 --- .../external_auth/tests/test_shib.py | 5 ++-- common/djangoapps/external_auth/views.py | 10 +++++--- common/djangoapps/student/views.py | 25 ++++++++++--------- lms/envs/common.py | 8 ++++++ lms/envs/test.py | 4 +-- 5 files changed, 30 insertions(+), 22 deletions(-) diff --git a/common/djangoapps/external_auth/tests/test_shib.py b/common/djangoapps/external_auth/tests/test_shib.py index e5059e5635..eb05b59afb 100644 --- a/common/djangoapps/external_auth/tests/test_shib.py +++ b/common/djangoapps/external_auth/tests/test_shib.py @@ -1,4 +1,3 @@ -# coding=utf-8 """ Tests for Shibboleth Authentication @jbau @@ -36,8 +35,8 @@ from student.tests.factories import UserFactory IDP = 'https://idp.stanford.edu/' REMOTE_USER = 'test_user@stanford.edu' MAILS = [None, '', 'test_user@stanford.edu'] -GIVENNAMES = [None, '', 'Jason', 'jasön; John; bob'] # At Stanford, the givenNames can be a list delimited by ';' -SNS = [None, '', 'Bau', '包; smith'] # At Stanford, the sns can be a list delimited by ';' +GIVENNAMES = [None, '', 'Jason', 'jas\xc3\xb6n; John; bob'] # At Stanford, the givenNames can be a list delimited by ';' +SNS = [None, '', 'Bau', '\xe5\x8c\x85; smith'] # At Stanford, the sns can be a list delimited by ';' def gen_all_identities(): diff --git a/common/djangoapps/external_auth/views.py b/common/djangoapps/external_auth/views.py index 1ae8edfc52..93ab70debb 100644 --- a/common/djangoapps/external_auth/views.py +++ b/common/djangoapps/external_auth/views.py @@ -245,8 +245,10 @@ def signup(request, eamap=None): 'ask_for_tos': True, } - # Can't have terms of service for Stanford users, according to Stanford's Office of General Counsel - if settings.MITX_FEATURES['AUTH_USE_SHIB'] and ('stanford' in eamap.external_domain): + # Some openEdX instances can't have terms of service for shib users, like + # according to Stanford's Office of General Counsel + if settings.MITX_FEATURES.get('AUTH_USE_SHIB') and settings.MITX_FEATURES.get('SHIB_DISABLE_TOS') and \ + ('shib' in eamap.external_domain): context['ask_for_tos'] = False # detect if full name is blank and ask for it from user @@ -387,10 +389,10 @@ def shib_login(request): """)) if not request.META.get('REMOTE_USER'): - log.exception("SHIB: no REMOTE_USER found in request.META") + log.error("SHIB: no REMOTE_USER found in request.META") return default_render_failure(request, shib_error_msg) elif not request.META.get('Shib-Identity-Provider'): - log.exception("SHIB: no Shib-Identity-Provider in request.META") + log.error("SHIB: no Shib-Identity-Provider in request.META") return default_render_failure(request, shib_error_msg) else: #if we get here, the user has authenticated properly diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 0aac873c03..1a49789a32 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -48,6 +48,8 @@ from courseware.access import has_access from courseware.views import get_module_for_descriptor, jump_to from courseware.model_data import ModelDataCache +from external_auth.models import ExternalAuthMap + from statsd import statsd from pytz import UTC @@ -287,12 +289,10 @@ def dashboard(request): # get info w.r.t ExternalAuthMap external_auth_map = None - if 'external_auth' in settings.INSTALLED_APPS: - from external_auth.models import ExternalAuthMap - try: - external_auth_map = ExternalAuthMap.objects.get(user=user) - except ExternalAuthMap.DoesNotExist: - pass + try: + external_auth_map = ExternalAuthMap.objects.get(user=user) + except ExternalAuthMap.DoesNotExist: + pass context = {'courses': courses, 'message': message, @@ -613,10 +613,12 @@ def create_account(request, post_override=None): js['field'] = 'honor_code' return HttpResponse(json.dumps(js)) - # Can't have terms of service for Stanford users, according to Stanford's Office of General Counsel - if settings.MITX_FEATURES.get("AUTH_USE_SHIB") and DoExternalAuth and ("stanford" in eamap.external_domain): - pass - else: + # Can't have terms of service for certain SHIB users, like at Stanford + tos_not_required = settings.MITX_FEATURES.get("AUTH_USE_SHIB") \ + and settings.MITX_FEATURES.get('SHIB_DISABLE_TOS') \ + and DoExternalAuth and ("shib" in eamap.external_domain) + + if not tos_not_required: if post_vars.get('terms_of_service', 'false') != u'true': js['value'] = "You must accept the terms of service.".format(field=a) js['field'] = 'terms_of_service' @@ -629,8 +631,7 @@ def create_account(request, post_override=None): # TODO: Check password is sane required_post_vars = ['username', 'email', 'name', 'password', 'terms_of_service', 'honor_code'] - if settings.MITX_FEATURES.get("AUTH_USE_SHIB") and DoExternalAuth and ("stanford" in eamap.external_domain): - # Can't have terms of service for Stanford users, according to Stanford's Office of General Counsel + if tos_not_required: required_post_vars = ['username', 'email', 'name', 'password', 'honor_code'] for a in required_post_vars: diff --git a/lms/envs/common.py b/lms/envs/common.py index be570a9796..8a554f5bb9 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -92,6 +92,10 @@ MITX_FEATURES = { 'AUTH_USE_MIT_CERTIFICATES': False, 'AUTH_USE_OPENID_PROVIDER': False, 'AUTH_USE_SHIB': False, + + # This flag disables the requirement of having to agree to the TOS for users registering + # with Shib. Feature was requested by Stanford's office of general counsel + 'SHIB_DISABLE_TOS': False, # Enables ability to restrict enrollment in specific courses by the user account login method 'RESTRICT_ENROLL_BY_REG_METHOD': False, @@ -704,6 +708,10 @@ INSTALLED_APPS = ( 'licenses', 'course_groups', + # External auth (OpenID, shib) + 'external_auth', + 'django_openid_auth', + #For the wiki 'wiki', # The new django-wiki from benjaoming 'django_notify', diff --git a/lms/envs/test.py b/lms/envs/test.py index 3a6c641841..e9b683487e 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -139,6 +139,7 @@ MITX_FEATURES['AUTH_USE_OPENID_PROVIDER'] = True ################################## SHIB ####################################### MITX_FEATURES['AUTH_USE_SHIB'] = True +MITX_FEATURES['SHIB_DISABLE_TOS'] = True MITX_FEATURES['RESTRICT_ENROLL_BY_REG_METHOD'] = True OPENID_CREATE_USERS = False @@ -146,9 +147,6 @@ OPENID_UPDATE_DETAILS_FROM_SREG = True OPENID_USE_AS_ADMIN_LOGIN = False OPENID_PROVIDER_TRUSTED_ROOTS = ['*'] -INSTALLED_APPS += ('external_auth',) -INSTALLED_APPS += ('django_openid_auth',) - ################################# CELERY ###################################### CELERY_ALWAYS_EAGER = True From f109e5b01c0fda46d996cbf0063e3c4af2467a95 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 21 Jun 2013 08:02:23 -0400 Subject: [PATCH 120/995] Changed order of arguments to test tasks so that the report directory is created before it is cleaned. --- rakelib/tests.rake | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rakelib/tests.rake b/rakelib/tests.rake index f169d28256..3cb5e8f4e5 100644 --- a/rakelib/tests.rake +++ b/rakelib/tests.rake @@ -55,6 +55,7 @@ end task :clean_reports_dir do desc "Clean coverage files, to ensure that we don't use stale data to generate reports." + # We delete the files but preserve the directory structure # so that coverage.py has a place to put the reports. sh("find #{REPORT_DIR} -type f -delete") @@ -82,7 +83,7 @@ TEST_TASK_DIRS = [] task "test_acceptance_#{system}", [:harvest_args] => [:clean_test_files, "#{system}:gather_assets:acceptance", "fasttest_acceptance_#{system}"] desc "Run acceptance tests without collectstatic" - task "fasttest_acceptance_#{system}", [:harvest_args] => [:clean_reports_dir, :predjango, report_dir] do |t, args| + task "fasttest_acceptance_#{system}", [:harvest_args] => [report_dir, :clean_reports_dir, :predjango] do |t, args| args.with_defaults(:harvest_args => '') run_acceptance_tests(system, report_dir, args.harvest_args) end @@ -98,7 +99,7 @@ Dir["common/lib/*"].select{|lib| File.directory?(lib)}.each do |lib| report_dir = report_dir_path(lib) desc "Run tests for common lib #{lib}" - task "test_#{lib}" => [:clean_reports_dir, report_dir] do + task "test_#{lib}" => [report_dir, :clean_reports_dir] do ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml") cmd = "nosetests #{lib}" test_sh(run_under_coverage(cmd, lib)) From e4af7287b6f204dc759f1e9a349bf29a6864a025 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Tue, 18 Jun 2013 14:04:43 -0400 Subject: [PATCH 121/995] Initial testing for parallelization --- cms/djangoapps/contentstore/tests/test_contentstore.py | 6 ++++++ cms/envs/test.py | 2 +- .../lib/xmodule/xmodule/modulestore/tests/django_utils.py | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index d24deacecf..86699ef479 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -43,10 +43,13 @@ from django_comment_common.utils import are_permissions_roles_seeded from xmodule.exceptions import InvalidVersionError import datetime from pytz import UTC +#from uuid import uuid4 TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE) TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data') TEST_DATA_MODULESTORE['direct']['OPTIONS']['fs_root'] = path('common/test/data') +TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE) +TEST_DATA_CONTENTSTORE['OPTIONS']['db'] = 'test_xcontent_%s' % 4 #uuid4().hex class MongoCollectionFindWrapper(object): @@ -60,6 +63,7 @@ class MongoCollectionFindWrapper(object): @override_settings(MODULESTORE=TEST_DATA_MODULESTORE) +@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE) class ContentStoreToyCourseTest(ModuleStoreTestCase): """ Tests that rely on the toy courses. @@ -83,6 +87,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): self.client = Client() self.client.login(username=uname, password=password) + def check_components_on_page(self, component_types, expected_types): """ Ensure that the right types end up on the page. @@ -809,6 +814,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): export_to_xml(module_store, content_store, location, root_dir, 'test_export') +@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE) class ContentStoreTest(ModuleStoreTestCase): """ Tests for the CMS ContentStore application. diff --git a/cms/envs/test.py b/cms/envs/test.py index 954a553e10..89813dd937 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -70,7 +70,7 @@ CONTENTSTORE = { 'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore', 'OPTIONS': { 'host': 'localhost', - 'db': 'test_xmodule', + 'db': 'test_xcontent', }, # allow for additional options that can be keyed on a name, e.g. 'trashcan' 'ADDITIONAL_OPTIONS': { diff --git a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py index 04e79ce521..e0e5c1a46f 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py @@ -27,6 +27,7 @@ class ModuleStoreTestCase(TestCase): # Remove everything except templates modulestore.collection.remove(query) + modulestore.collection.drop() @staticmethod def load_templates_if_necessary(): From f90ed69cd792091c9f2d57bce1cbafe0efd51094 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Tue, 18 Jun 2013 15:09:53 -0400 Subject: [PATCH 122/995] move override of MODULESTORE settings into ModuleStore test case class --- cms/djangoapps/contentstore/tests/test_contentstore.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 86699ef479..9c3ec2e3ba 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -45,9 +45,7 @@ import datetime from pytz import UTC #from uuid import uuid4 -TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE) -TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data') -TEST_DATA_MODULESTORE['direct']['OPTIONS']['fs_root'] = path('common/test/data') + TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE) TEST_DATA_CONTENTSTORE['OPTIONS']['db'] = 'test_xcontent_%s' % 4 #uuid4().hex @@ -62,7 +60,7 @@ class MongoCollectionFindWrapper(object): return self.original(query, *args, **kwargs) -@override_settings(MODULESTORE=TEST_DATA_MODULESTORE) +#@override_settings(MODULESTORE=TEST_DATA_MODULESTORE) @override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE) class ContentStoreToyCourseTest(ModuleStoreTestCase): """ @@ -70,6 +68,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): TODO: refactor using CourseFactory so they do not. """ def setUp(self): + + settings.MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data') + settings.MODULESTORE['direct']['OPTIONS']['fs_root'] = path('common/test/data') uname = 'testuser' email = 'test+courses@edx.org' password = 'foo' @@ -88,6 +89,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): self.client.login(username=uname, password=password) + def check_components_on_page(self, component_types, expected_types): """ Ensure that the right types end up on the page. From 51f8c0cfebedb7807b04ef849cfb806a0dcdba0e Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Wed, 19 Jun 2013 11:27:22 -0400 Subject: [PATCH 123/995] Added the beginnings of self cleanup --- .../contentstore/tests/test_contentstore.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 9c3ec2e3ba..46d6a069ce 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -43,11 +43,12 @@ from django_comment_common.utils import are_permissions_roles_seeded from xmodule.exceptions import InvalidVersionError import datetime from pytz import UTC -#from uuid import uuid4 +from uuid import uuid4 +import pymongo TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE) -TEST_DATA_CONTENTSTORE['OPTIONS']['db'] = 'test_xcontent_%s' % 4 #uuid4().hex +TEST_DATA_CONTENTSTORE['OPTIONS']['db'] = 'test_xcontent_%s' % uuid4().hex class MongoCollectionFindWrapper(object): @@ -88,7 +89,10 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): self.client = Client() self.client.login(username=uname, password=password) - + def tearDown(self): + m = pymongo.MongoClient() + m.drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db']) + #contentstore().fs_files.drop() def check_components_on_page(self, component_types, expected_types): """ @@ -449,7 +453,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): content_store = contentstore() trash_store = contentstore('trashcan') module_store = modulestore('direct') - import_from_xml(module_store, 'common/test/data/', ['full'], static_content_store=content_store) # look up original (and thumbnail) in content store, should be there after import @@ -853,6 +856,11 @@ class ContentStoreTest(ModuleStoreTestCase): 'display_name': 'Robot Super Course', } + def tearDown(self): + m = pymongo.MongoClient() + m.drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db']) + #contentstore().fs_files.drop() + def test_create_course(self): """Test new course creation - happy path""" resp = self.client.post(reverse('create_new_course'), self.course_data) From fa18b48f6eaec45bc65f16f9585fa2555462ad55 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 20 Jun 2013 09:05:35 -0400 Subject: [PATCH 124/995] Contentstore singleton is now cleared during teardown --- cms/djangoapps/contentstore/tests/test_contentstore.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 46d6a069ce..b0cbcee032 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -41,6 +41,7 @@ from xmodule.exceptions import NotFoundError from django_comment_common.utils import are_permissions_roles_seeded from xmodule.exceptions import InvalidVersionError +import xmodule.contentstore.django import datetime from pytz import UTC from uuid import uuid4 @@ -92,7 +93,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): def tearDown(self): m = pymongo.MongoClient() m.drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db']) - #contentstore().fs_files.drop() + xmodule.contentstore.django._CONTENTSTORE.clear() def check_components_on_page(self, component_types, expected_types): """ @@ -859,7 +860,7 @@ class ContentStoreTest(ModuleStoreTestCase): def tearDown(self): m = pymongo.MongoClient() m.drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db']) - #contentstore().fs_files.drop() + xmodule.contentstore.django._CONTENTSTORE.clear() def test_create_course(self): """Test new course creation - happy path""" From cb04f9f0b82dfe46777ceb0584fadb656f7b6780 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 20 Jun 2013 17:10:36 -0400 Subject: [PATCH 125/995] Moved port range to rake file --- jenkins/test.sh | 3 --- rakelib/tests.rake | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/jenkins/test.sh b/jenkins/test.sh index e5ac4f6f71..2ff32a9911 100755 --- a/jenkins/test.sh +++ b/jenkins/test.sh @@ -60,9 +60,6 @@ fi export PIP_DOWNLOAD_CACHE=/mnt/pip-cache -# Allow django liveserver tests to use a range of ports -export DJANGO_LIVE_TEST_SERVER_ADDRESS=${DJANGO_LIVE_TEST_SERVER_ADDRESS-localhost:8000-9000} - source /mnt/virtualenvs/"$JOB_NAME"/bin/activate bundle install diff --git a/rakelib/tests.rake b/rakelib/tests.rake index f169d28256..c0592cca7a 100644 --- a/rakelib/tests.rake +++ b/rakelib/tests.rake @@ -16,7 +16,7 @@ def run_tests(system, report_dir, test_id=nil, stop_on_failure=true) ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml") dirs = Dir["common/djangoapps/*"] + Dir["#{system}/djangoapps/*"] test_id = dirs.join(' ') if test_id.nil? or test_id == '' - cmd = django_admin(system, :test, 'test', '--logging-clear-handlers', test_id) + cmd = django_admin(system, :test, 'test', '--logging-clear-handlers', '--liveserver=localhost:8000-9000', test_id) test_sh(run_under_coverage(cmd, system)) end From b8479305797487a54468858073f07a96d0d41aea Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Fri, 21 Jun 2013 09:38:43 -0400 Subject: [PATCH 126/995] Changed a click to css_click and fixed earlier typo --- lms/djangoapps/courseware/features/problems.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index 094d495b53..08c5207303 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -9,7 +9,8 @@ from lettuce import world, step from lettuce.django import django_url from common import i_am_registered_for_the_course, TEST_SECTION_NAME from problems_setup import PROBLEM_DICT, answer_problem, problem_has_answer, add_problem_to_course -from nose.tools import assert_equal, assert_not_equal +from nose.tools import assert_equal + @step(u'I am viewing a "([^"]*)" problem with "([^"]*)" attempt') def view_problem_with_attempts(step, problem_type, attempts): @@ -121,7 +122,7 @@ def press_the_button_with_label(_step, buttonname): button_css = 'button span.show-label' elem = world.css_find(button_css).first assert_equal(elem.text, buttonname) - elem.click() + world.css_click(button_css) @step(u'The "([^"]*)" button does( not)? appear') @@ -136,9 +137,9 @@ def action_button_present(_step, buttonname, doesnt_appear): @step(u'the button with the label "([^"]*)" does( not)? appear') def button_with_label_present(step, buttonname, doesnt_appear): if doesnt_appear: - world.browser.is_text_not_present(buttonname, wait_time=5) + assert world.browser.is_text_not_present(buttonname, wait_time=5) else: - world.browser.is_text_present(buttonname, wait_time=5) + assert world.browser.is_text_present(buttonname, wait_time=5) @step(u'My "([^"]*)" answer is marked "([^"]*)"') From 3d202ffc71631fca47616adff1e66bc0d220a81c Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 21 Jun 2013 10:57:19 -0400 Subject: [PATCH 127/995] Moved generation of pylint/pep8 reports to after running the test suite, so the clean reports command doesn't blow away the reports. --- jenkins/test.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/jenkins/test.sh b/jenkins/test.sh index e5ac4f6f71..c7728ab367 100755 --- a/jenkins/test.sh +++ b/jenkins/test.sh @@ -69,12 +69,14 @@ bundle install rake install_prereqs rake clobber -rake pep8 > pep8.log || cat pep8.log -rake pylint > pylint.log || cat pylint.log # Run the unit tests (use phantomjs for javascript unit tests) rake test +# Generate pylint and pep8 reports +rake pep8 > pep8.log || cat pep8.log +rake pylint > pylint.log || cat pylint.log + # Generate coverage reports rake coverage From 7db93976c5860cf818bc915bb890b1a9c18b6838 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Fri, 21 Jun 2013 11:02:25 -0400 Subject: [PATCH 128/995] PR fixes --- common/lib/xmodule/xmodule/capa_module.py | 9 +++------ .../lib/xmodule/xmodule/tests/test_capa_module.py | 14 ++++++++------ 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index b927106b4a..d740a73946 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -22,7 +22,7 @@ from xblock.core import Scope, String, Boolean, Dict, Integer, Float from .fields import Timedelta, Date from django.utils.timezone import UTC -log = logging.getLogger("mitx.courseware") # pylint: disable=C0103 +log = logging.getLogger("mitx.courseware") # Generate this many different variants of problems with rerandomize=per_student @@ -51,9 +51,6 @@ class Randomization(String): Define a field to store how to randomize a problem. """ def from_json(self, value): - """ - For backward compatability? - """ if value in ("", "true"): return "always" elif value == "false": @@ -865,8 +862,8 @@ class CapaModule(CapaFields, XModule): except Exception as err: if self.system.DEBUG: - msg = "Error checking problem: " + err.message - msg += '\nTraceback:\n' + traceback.format_exc() + msg = u"Error checking problem: {}".format(err.message) + msg += u'\nTraceback:\n{}'.format(traceback.format_exc()) return {'success': msg} raise diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py index 81df686015..c6ffd32e89 100644 --- a/common/lib/xmodule/xmodule/tests/test_capa_module.py +++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py @@ -505,9 +505,10 @@ class CapaModuleTest(unittest.TestCase): def test_check_problem_error(self): # Try each exception that capa_module should handle - for exception_class in [StudentInputError, - LoncapaProblemError, - ResponseError]: + exception_classes = [StudentInputError, + LoncapaProblemError, + ResponseError] + for exception_class in exception_classes: # Create the module module = CapaFactory.create(attempts=1) @@ -532,9 +533,10 @@ class CapaModuleTest(unittest.TestCase): def test_check_problem_error_nonascii(self): # Try each exception that capa_module should handle - for exception_class in [StudentInputError, - LoncapaProblemError, - ResponseError]: + exception_classes = [StudentInputError, + LoncapaProblemError, + ResponseError] + for exception_class in exception_classes: # Create the module module = CapaFactory.create(attempts=1) From 8201ca5e777f6933a8329aab777b86e33b09f843 Mon Sep 17 00:00:00 2001 From: Sarina Canelake Date: Thu, 20 Jun 2013 18:27:45 -0400 Subject: [PATCH 129/995] 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 130/995] 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 9bfddd4891281dfeeb37b2d596c41627908813e2 Mon Sep 17 00:00:00 2001 From: Jean Manuel Nater Date: Fri, 21 Jun 2013 14:05:57 -0400 Subject: [PATCH 131/995] Addressed pull request feedback. --- .../xmodule/modulestore/tests/django_utils.py | 16 + .../xmodule/modulestore/tests/factories.py | 4 + lms/djangoapps/courseware/access.py | 3 +- .../courseware/tests/check_request_code.py | 24 + lms/djangoapps/courseware/tests/helpers.py | 142 +++++ .../courseware/tests/modulestore_config.py | 72 +++ .../courseware/tests/mongo_login_helpers.py | 172 ------ .../tests/test_draft_modulestore.py | 21 + .../courseware/tests/test_masquerade.py | 7 +- .../courseware/tests/test_navigation.py | 449 ++------------- .../tests/test_view_authentication.py | 374 ++++++------- lms/djangoapps/courseware/tests/tests.py | 516 +++++------------- .../instructor/tests/test_download_csv.py | 9 +- .../instructor/tests/test_enrollment.py | 5 +- .../instructor/tests/test_forum_admin.py | 9 +- lms/djangoapps/open_ended_grading/tests.py | 19 +- lms/urls.py | 2 +- 17 files changed, 694 insertions(+), 1150 deletions(-) create mode 100644 lms/djangoapps/courseware/tests/check_request_code.py create mode 100644 lms/djangoapps/courseware/tests/helpers.py create mode 100644 lms/djangoapps/courseware/tests/modulestore_config.py delete mode 100644 lms/djangoapps/courseware/tests/mongo_login_helpers.py create mode 100644 lms/djangoapps/courseware/tests/test_draft_modulestore.py diff --git a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py index 04e79ce521..944b9e5bd4 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py @@ -14,6 +14,22 @@ class ModuleStoreTestCase(TestCase): collection with templates before running the TestCase and drops it they are finished. """ + def update_course(self, course, data): + """ + Updates the version of course in the mongo modulestore + with the metadata in data and returns the updated version. + """ + + store = xmodule.modulestore.django.modulestore() + + store.update_item(course.location, data) + + store.update_metadata(course.location, data) + + updated_course = store.get_instance(course.id, course.location) + + return updated_course + @staticmethod def flush_mongo_except_templates(): ''' diff --git a/common/lib/xmodule/xmodule/modulestore/tests/factories.py b/common/lib/xmodule/xmodule/modulestore/tests/factories.py index b91e9be700..4f63fbc1d2 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/factories.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/factories.py @@ -60,6 +60,8 @@ class XModuleCourseFactory(Factory): if data is not None: store.update_item(new_course.location, data) + '''update_item updates the the course as it exists in the modulestore, but doesn't + update the instance we are working with, so have to refetch the course after updating it.''' new_course = store.get_instance(new_course.id, new_course.location) return new_course @@ -150,6 +152,8 @@ class XModuleItemFactory(Factory): if new_item.location.category not in DETACHED_CATEGORIES: store.update_children(parent_location, parent.children + [new_item.location.url()]) + '''update_children updates the the item as it exists in the modulestore, but doesn't + update the instance we are working with, so have to refetch the item after updating it.''' new_item = store.get_item(new_item.location) return new_item diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py index 9e6a371552..eb732311cf 100644 --- a/lms/djangoapps/courseware/access.py +++ b/lms/djangoapps/courseware/access.py @@ -245,8 +245,7 @@ def _has_access_descriptor(user, descriptor, action, course_context=None): if descriptor.lms.start is not None: now = datetime.now(UTC()) effective_start = _adjust_start_date_for_beta_testers(user, descriptor) - difference = (now - effective_start).total_seconds() - if difference > 3600: + if now > effective_start: # after start date, everyone can see it debug("Allow: now > effective start date") return True diff --git a/lms/djangoapps/courseware/tests/check_request_code.py b/lms/djangoapps/courseware/tests/check_request_code.py new file mode 100644 index 0000000000..1393d2fe17 --- /dev/null +++ b/lms/djangoapps/courseware/tests/check_request_code.py @@ -0,0 +1,24 @@ + + +def check_for_get_code(code, url): + """ + Check that we got the expected code when accessing url via GET. + Returns the response. + """ + resp = self.client.get(url) + self.assertEqual(resp.status_code, code, + "got code %d for url '%s'. Expected code %d" + % (resp.status_code, url, code)) + return resp + + +def check_for_post_code(code, url, data={}): + """ + Check that we got the expected code when accessing url via POST. + Returns the response. + """ + resp = self.client.post(url, data) + self.assertEqual(resp.status_code, code, + "got code %d for url '%s'. Expected code %d" + % (resp.status_code, url, code)) + return resp diff --git a/lms/djangoapps/courseware/tests/helpers.py b/lms/djangoapps/courseware/tests/helpers.py new file mode 100644 index 0000000000..99da5e9061 --- /dev/null +++ b/lms/djangoapps/courseware/tests/helpers.py @@ -0,0 +1,142 @@ +import json + +from django.contrib.auth.models import User +from django.core.urlresolvers import reverse + +from student.models import Registration + +from django.test import TestCase + + +def check_for_get_code(self, code, url): + """ + Check that we got the expected code when accessing url via GET. + Returns the HTTP response. + 'self' is a class that subclasses TestCase. + """ + resp = self.client.get(url) + self.assertEqual(resp.status_code, code, + "got code %d for url '%s'. Expected code %d" + % (resp.status_code, url, code)) + return resp + + +def check_for_post_code(self, code, url, data={}): + """ + Check that we got the expected code when accessing url via POST. + Returns the HTTP response. + 'self' is a class that subclasses TestCase. + """ + resp = self.client.post(url, data) + self.assertEqual(resp.status_code, code, + "got code %d for url '%s'. Expected code %d" + % (resp.status_code, url, code)) + return resp + + +class LoginEnrollmentTestCase(TestCase): + + def setup_user(self): + """ + Create a user account, activate, and log in. + """ + self.email = 'foo@test.com' + self.password = 'bar' + self.username = 'test' + self.create_account(self.username, + self.email, self.password) + self.activate_user(self.email) + self.login(self.email, self.password) + + # ============ User creation and login ============== + + def login(self, email, password): + """ + Login, check that the corresponding view's response has a 200 status code. + """ + resp = resp = self.client.post(reverse('login'), + {'email': email, 'password': password}) + self.assertEqual(resp.status_code, 200) + data = json.loads(resp.content) + self.assertTrue(data['success']) + + def logout(self): + """ + Logout, check that it worked. + Returns an HTTP response which e + """ + resp = self.client.get(reverse('logout'), {}) + # should redirect + self.assertEqual(resp.status_code, 302) + + def create_account(self, username, email, password): + """ + Create the account and check that it worked. + """ + resp = self.client.post(reverse('create_account'), { + 'username': username, + 'email': email, + 'password': password, + 'name': 'username', + 'terms_of_service': 'true', + 'honor_code': 'true', + }) + self.assertEqual(resp.status_code, 200) + data = json.loads(resp.content) + self.assertEqual(data['success'], True) + + # Check both that the user is created, and inactive + self.assertFalse(User.objects.get(email=email).is_active) + + def activate_user(self, email): + """ + Look up the activation key for the user, then hit the activate view. + No error checking. + """ + activation_key = Registration.objects.get(user__email=email).activation_key + + # and now we try to activate + url = reverse('activate', kwargs={'key': activation_key}) + + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + # Now make sure that the user is now actually activated + self.assertTrue(User.objects.get(email=email).is_active) + + def enroll(self, course, verify=False): + """ + Try to enroll and return boolean indicating result. + 'course' is an instance of CourseDescriptor. + 'verify' is an optional parameter specifying whether we + want to verify that the student was successfully enrolled + in the course. + """ + resp = self.client.post(reverse('change_enrollment'), { + 'enrollment_action': 'enroll', + 'course_id': course.id, + }) + print ('Enrollment in %s result status code: %s' + % (course.location.url(), str(resp.status_code))) + result = resp.status_code == 200 + if verify: + self.assertTrue(result) + return result + + # def enroll(self, course): + # """ + # Enroll the currently logged-in user, and check that it worked. + # """ + + # result = self.try_enroll(course) + # self.assertTrue(result) + + def unenroll(self, course): + """ + Unenroll the currently logged-in user, and check that it worked. + 'course' is an instance of CourseDescriptor. + """ + resp = self.client.post('/change_enrollment', { + 'enrollment_action': 'unenroll', + 'course_id': course.id, + }) + self.assertEqual(resp.status_code, 200) diff --git a/lms/djangoapps/courseware/tests/modulestore_config.py b/lms/djangoapps/courseware/tests/modulestore_config.py new file mode 100644 index 0000000000..81d0f4f911 --- /dev/null +++ b/lms/djangoapps/courseware/tests/modulestore_config.py @@ -0,0 +1,72 @@ +from uuid import uuid4 + +from django.conf import settings + + +def mongo_store_config(data_dir): + ''' + Defines default module store using MongoModuleStore + + Use of this config requires mongo to be running + ''' + store = { + 'default': { + 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', + 'OPTIONS': { + 'default_class': 'xmodule.raw_module.RawDescriptor', + 'host': 'localhost', + 'db': 'test_xmodule', + 'collection': 'modulestore_%s' % uuid4().hex, + 'fs_root': data_dir, + 'render_template': 'mitxmako.shortcuts.render_to_string' + } + } + } + store['direct'] = store['default'] + return store + + +def draft_mongo_store_config(data_dir): + '''Defines default module store using DraftMongoModuleStore''' + return { + 'default': { + 'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore', + 'OPTIONS': { + 'default_class': 'xmodule.raw_module.RawDescriptor', + 'host': 'localhost', + 'db': 'test_xmodule', + 'collection': 'modulestore_%s' % uuid4().hex, + 'fs_root': data_dir, + 'render_template': 'mitxmako.shortcuts.render_to_string', + } + }, + 'direct': { + 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', + 'OPTIONS': { + 'default_class': 'xmodule.raw_module.RawDescriptor', + 'host': 'localhost', + 'db': 'test_xmodule', + 'collection': 'modulestore_%s' % uuid4().hex, + 'fs_root': data_dir, + 'render_template': 'mitxmako.shortcuts.render_to_string', + } + } + } + + +def xml_store_config(data_dir): + '''Defines default module store using XMLModuleStore''' + return { + 'default': { + 'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore', + 'OPTIONS': { + 'data_dir': data_dir, + 'default_class': 'xmodule.hidden_module.HiddenDescriptor', + } + } + } + +TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT +TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR) +TEST_DATA_MONGO_MODULESTORE = mongo_store_config(TEST_DATA_DIR) +TEST_DATA_DRAFT_MONGO_MODULESTORE = draft_mongo_store_config(TEST_DATA_DIR) diff --git a/lms/djangoapps/courseware/tests/mongo_login_helpers.py b/lms/djangoapps/courseware/tests/mongo_login_helpers.py deleted file mode 100644 index a329f71d13..0000000000 --- a/lms/djangoapps/courseware/tests/mongo_login_helpers.py +++ /dev/null @@ -1,172 +0,0 @@ -import logging -import json - -from urlparse import urlsplit, urlunsplit - -from django.contrib.auth.models import User -from django.core.urlresolvers import reverse - -from student.models import Registration - -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase - -log = logging.getLogger("mitx." + __name__) - - -def parse_json(response): - """Parse response, which is assumed to be json""" - return json.loads(response.content) - - -def get_user(email): - '''look up a user by email''' - return User.objects.get(email=email) - - -def get_registration(email): - '''look up registration object by email''' - return Registration.objects.get(user__email=email) - - -class MongoLoginHelpers(ModuleStoreTestCase): - - def assertRedirectsNoFollow(self, response, expected_url): - """ - http://devblog.point2.com/2010/04/23/djangos-assertredirects-little-gotcha/ - - Don't check that the redirected-to page loads--there should be other tests for that. - - Some of the code taken from django.test.testcases.py - """ - self.assertEqual(response.status_code, 302, - 'Response status code was %d instead of 302' - % (response.status_code)) - url = response['Location'] - - e_scheme, e_netloc, e_path, e_query, e_fragment = urlsplit(expected_url) - if not (e_scheme or e_netloc): - expected_url = urlunsplit(('http', 'testserver', - e_path, e_query, e_fragment)) - - self.assertEqual(url, expected_url, - "Response redirected to '%s', expected '%s'" % - (url, expected_url)) - - def setup_viewtest_user(self): - '''create a user account, activate, and log in''' - self.viewtest_email = 'view@test.com' - self.viewtest_password = 'foo' - self.viewtest_username = 'viewtest' - self.create_account(self.viewtest_username, - self.viewtest_email, self.viewtest_password) - self.activate_user(self.viewtest_email) - self.login(self.viewtest_email, self.viewtest_password) - - # ============ User creation and login ============== - - def _login(self, email, password): - '''Login. View should always return 200. The success/fail is in the - returned json''' - resp = self.client.post(reverse('login'), - {'email': email, 'password': password}) - self.assertEqual(resp.status_code, 200) - return resp - - def login(self, email, password): - '''Login, check that it worked.''' - resp = self._login(email, password) - data = parse_json(resp) - self.assertTrue(data['success']) - return resp - - def logout(self): - '''Logout, check that it worked.''' - resp = self.client.get(reverse('logout'), {}) - # should redirect - self.assertEqual(resp.status_code, 302) - return resp - - def _create_account(self, username, email, password): - '''Try to create an account. No error checking''' - resp = self.client.post('/create_account', { - 'username': username, - 'email': email, - 'password': password, - 'name': 'Fred Weasley', - 'terms_of_service': 'true', - 'honor_code': 'true', - }) - return resp - - def create_account(self, username, email, password): - '''Create the account and check that it worked''' - resp = self._create_account(username, email, password) - self.assertEqual(resp.status_code, 200) - data = parse_json(resp) - self.assertEqual(data['success'], True) - - # Check both that the user is created, and inactive - self.assertFalse(get_user(email).is_active) - - return resp - - def _activate_user(self, email): - '''Look up the activation key for the user, then hit the activate view. - No error checking''' - activation_key = get_registration(email).activation_key - - # and now we try to activate - url = reverse('activate', kwargs={'key': activation_key}) - resp = self.client.get(url) - return resp - - def activate_user(self, email): - resp = self._activate_user(email) - self.assertEqual(resp.status_code, 200) - # Now make sure that the user is now actually activated - self.assertTrue(get_user(email).is_active) - - def try_enroll(self, course): - """Try to enroll. Return bool success instead of asserting it.""" - resp = self.client.post('/change_enrollment', { - 'enrollment_action': 'enroll', - 'course_id': course.id, - }) - print ('Enrollment in %s result status code: %s' - % (course.location.url(), str(resp.status_code))) - return resp.status_code == 200 - - def enroll(self, course): - """Enroll the currently logged-in user, and check that it worked.""" - result = self.try_enroll(course) - self.assertTrue(result) - - def unenroll(self, course): - """Unenroll the currently logged-in user, and check that it worked.""" - resp = self.client.post('/change_enrollment', { - 'enrollment_action': 'unenroll', - 'course_id': course.id, - }) - self.assertTrue(resp.status_code == 200) - - def check_for_get_code(self, code, url): - """ - Check that we got the expected code when accessing url via GET. - Returns the response. - """ - resp = self.client.get(url) - self.assertEqual(resp.status_code, code, - "got code %d for url '%s'. Expected code %d" - % (resp.status_code, url, code)) - return resp - - def check_for_post_code(self, code, url, data={}): - """ - Check that we got the expected code when accessing url via POST. - Returns the response. - """ - resp = self.client.post(url, data) - self.assertEqual(resp.status_code, code, - "got code %d for url '%s'. Expected code %d" - % (resp.status_code, url, code)) - return resp diff --git a/lms/djangoapps/courseware/tests/test_draft_modulestore.py b/lms/djangoapps/courseware/tests/test_draft_modulestore.py new file mode 100644 index 0000000000..db6d4c45b5 --- /dev/null +++ b/lms/djangoapps/courseware/tests/test_draft_modulestore.py @@ -0,0 +1,21 @@ +from django.test import TestCase +from django.test.utils import override_settings + +from xmodule.modulestore.django import modulestore +from xmodule.modulestore import Location + +from modulestore_config import TEST_DATA_DRAFT_MONGO_MODULESTORE + + +@override_settings(MODULESTORE=TEST_DATA_DRAFT_MONGO_MODULESTORE) +class TestDraftModuleStore(TestCase): + def test_get_items_with_course_items(self): + store = modulestore() + + # fix was to allow get_items() to take the course_id parameter + store.get_items(Location(None, None, 'vertical', None, None), + course_id='abc', depth=0) + + # test success is just getting through the above statement. + # The bug was that 'course_id' argument was + # not allowed to be passed in (i.e. was throwing exception) diff --git a/lms/djangoapps/courseware/tests/test_masquerade.py b/lms/djangoapps/courseware/tests/test_masquerade.py index f9ddf88b5f..4b9a5a578c 100644 --- a/lms/djangoapps/courseware/tests/test_masquerade.py +++ b/lms/djangoapps/courseware/tests/test_masquerade.py @@ -14,11 +14,13 @@ from django.core.urlresolvers import reverse from django.contrib.auth.models import User, Group from courseware.access import _course_staff_group_name -from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE, get_user +from courseware.tests.helpers import LoginEnrollmentTestCase +from courseware.tests.modulestore_config import TEST_DATA_XML_MODULESTORE from xmodule.modulestore.django import modulestore import xmodule.modulestore.django import json + @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) class TestStaffMasqueradeAsStudent(LoginEnrollmentTestCase): ''' @@ -41,7 +43,7 @@ class TestStaffMasqueradeAsStudent(LoginEnrollmentTestCase): def make_instructor(course): group_name = _course_staff_group_name(course.location) g = Group.objects.create(name=group_name) - g.user_set.add(get_user(self.instructor)) + g.user_set.add(User.objects.get(email=self.instructor)) make_instructor(self.graded_course) @@ -67,7 +69,6 @@ class TestStaffMasqueradeAsStudent(LoginEnrollmentTestCase): self.assertTrue(sdebug in resp.content) - def toggle_masquerade(self): ''' Toggle masquerade state diff --git a/lms/djangoapps/courseware/tests/test_navigation.py b/lms/djangoapps/courseware/tests/test_navigation.py index 242379d8ca..9f9bf7ba92 100644 --- a/lms/djangoapps/courseware/tests/test_navigation.py +++ b/lms/djangoapps/courseware/tests/test_navigation.py @@ -1,378 +1,24 @@ -import logging -import json -import random - -from urlparse import urlsplit, urlunsplit -from uuid import uuid4 - -from django.contrib.auth.models import User -from django.test import TestCase -from django.conf import settings from django.core.urlresolvers import reverse from django.test.utils import override_settings -import xmodule.modulestore.django - -from student.models import Registration -from xmodule.error_module import ErrorDescriptor -from xmodule.modulestore.django import modulestore -from xmodule.modulestore import Location -from xmodule.modulestore.xml_importer import import_from_xml -from xmodule.modulestore.xml import XMLModuleStore - from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory -from mongo_login_helpers import * +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -log = logging.getLogger("mitx." + __name__) +import xmodule.modulestore.django - -def parse_json(response): - """Parse response, which is assumed to be json""" - return json.loads(response.content) - - -def get_user(email): - '''look up a user by email''' - return User.objects.get(email=email) - - -def get_registration(email): - '''look up registration object by email''' - return Registration.objects.get(user__email=email) - - -def mongo_store_config(data_dir): - ''' - Defines default module store using MongoModuleStore - - Use of this config requires mongo to be running - ''' - store = { - 'default': { - 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', - 'OPTIONS': { - 'default_class': 'xmodule.raw_module.RawDescriptor', - 'host': 'localhost', - 'db': 'test_xmodule', - 'collection': 'modulestore_%s' % uuid4().hex, - 'fs_root': data_dir, - 'render_template': 'mitxmako.shortcuts.render_to_string', - } - } - } - store['direct'] = store['default'] - return store - - -def xml_store_config(data_dir): - '''Defines default module store using XMLModuleStore''' - return { - 'default': { - 'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore', - 'OPTIONS': { - 'data_dir': data_dir, - 'default_class': 'xmodule.hidden_module.HiddenDescriptor', - } - } - } - -TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT -TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR) -TEST_DATA_MONGO_MODULESTORE = mongo_store_config(TEST_DATA_DIR) - - -class LoginEnrollmentTestCase(TestCase): - - ''' - Base TestCase providing support for user creation, - activation, login, and course enrollment - ''' - - def assertRedirectsNoFollow(self, response, expected_url): - """ - http://devblog.point2.com/2010/04/23/djangos-assertredirects-little-gotcha/ - - Don't check that the redirected-to page loads--there should be other tests for that. - - Some of the code taken from django.test.testcases.py - """ - self.assertEqual(response.status_code, 302, - 'Response status code was %d instead of 302' - % (response.status_code)) - url = response['Location'] - - e_scheme, e_netloc, e_path, e_query, e_fragment = urlsplit(expected_url) - if not (e_scheme or e_netloc): - expected_url = urlunsplit(('http', 'testserver', - e_path, e_query, e_fragment)) - - self.assertEqual(url, expected_url, - "Response redirected to '%s', expected '%s'" % - (url, expected_url)) - - def setup_viewtest_user(self): - '''create a user account, activate, and log in''' - self.viewtest_email = 'view@test.com' - self.viewtest_password = 'foo' - self.viewtest_username = 'viewtest' - self.create_account(self.viewtest_username, - self.viewtest_email, self.viewtest_password) - self.activate_user(self.viewtest_email) - self.login(self.viewtest_email, self.viewtest_password) - - # ============ User creation and login ============== - - def _login(self, email, password): - '''Login. View should always return 200. The success/fail is in the - returned json''' - resp = self.client.post(reverse('login'), - {'email': email, 'password': password}) - self.assertEqual(resp.status_code, 200) - return resp - - def login(self, email, password): - '''Login, check that it worked.''' - resp = self._login(email, password) - data = parse_json(resp) - self.assertTrue(data['success']) - return resp - - def logout(self): - '''Logout, check that it worked.''' - resp = self.client.get(reverse('logout'), {}) - # should redirect - self.assertEqual(resp.status_code, 302) - return resp - - def _create_account(self, username, email, password): - '''Try to create an account. No error checking''' - resp = self.client.post('/create_account', { - 'username': username, - 'email': email, - 'password': password, - 'name': 'Fred Weasley', - 'terms_of_service': 'true', - 'honor_code': 'true', - }) - return resp - - def create_account(self, username, email, password): - '''Create the account and check that it worked''' - resp = self._create_account(username, email, password) - self.assertEqual(resp.status_code, 200) - data = parse_json(resp) - self.assertEqual(data['success'], True) - - # Check both that the user is created, and inactive - self.assertFalse(get_user(email).is_active) - - return resp - - def _activate_user(self, email): - '''Look up the activation key for the user, then hit the activate view. - No error checking''' - activation_key = get_registration(email).activation_key - - # and now we try to activate - url = reverse('activate', kwargs={'key': activation_key}) - resp = self.client.get(url) - return resp - - def activate_user(self, email): - resp = self._activate_user(email) - self.assertEqual(resp.status_code, 200) - # Now make sure that the user is now actually activated - self.assertTrue(get_user(email).is_active) - - def try_enroll(self, course): - """Try to enroll. Return bool success instead of asserting it.""" - resp = self.client.post('/change_enrollment', { - 'enrollment_action': 'enroll', - 'course_id': course.id, - }) - print ('Enrollment in %s result status code: %s' - % (course.location.url(), str(resp.status_code))) - return resp.status_code == 200 - - def enroll(self, course): - """Enroll the currently logged-in user, and check that it worked.""" - result = self.try_enroll(course) - self.assertTrue(result) - - def unenroll(self, course): - """Unenroll the currently logged-in user, and check that it worked.""" - resp = self.client.post('/change_enrollment', { - 'enrollment_action': 'unenroll', - 'course_id': course.id, - }) - self.assertTrue(resp.status_code == 200) - - def check_for_get_code(self, code, url): - """ - Check that we got the expected code when accessing url via GET. - Returns the response. - """ - resp = self.client.get(url) - self.assertEqual(resp.status_code, code, - "got code %d for url '%s'. Expected code %d" - % (resp.status_code, url, code)) - return resp - - def check_for_post_code(self, code, url, data={}): - """ - Check that we got the expected code when accessing url via POST. - Returns the response. - """ - resp = self.client.post(url, data) - self.assertEqual(resp.status_code, code, - "got code %d for url '%s'. Expected code %d" - % (resp.status_code, url, code)) - return resp - - -class ActivateLoginTest(LoginEnrollmentTestCase): - '''Test logging in and logging out''' - def setUp(self): - self.setup_viewtest_user() - - def test_activate_login(self): - '''Test login -- the setup function does all the work''' - pass - - def test_logout(self): - '''Test logout -- setup function does login''' - self.logout() - - -class PageLoaderTestCase(LoginEnrollmentTestCase): - ''' Base class that adds a function to load all pages in a modulestore ''' - - def check_random_page_loads(self, module_store): - ''' - Choose a page in the course randomly, and assert that it loads - ''' - # enroll in the course before trying to access pages - courses = module_store.get_courses() - self.assertEqual(len(courses), 1) - course = courses[0] - self.enroll(course) - course_id = course.id - - # Search for items in the course - # None is treated as a wildcard - course_loc = course.location - location_query = Location(course_loc.tag, course_loc.org, - course_loc.course, None, None, None) - - items = module_store.get_items(location_query) - - if len(items) < 1: - self.fail('Could not retrieve any items from course') - else: - descriptor = random.choice(items) - - # We have ancillary course information now as modules - # and we can't simply use 'jump_to' to view them - if descriptor.location.category == 'about': - self._assert_loads('about_course', - {'course_id': course_id}, - descriptor) - - elif descriptor.location.category == 'static_tab': - kwargs = {'course_id': course_id, - 'tab_slug': descriptor.location.name} - self._assert_loads('static_tab', kwargs, descriptor) - - elif descriptor.location.category == 'course_info': - self._assert_loads('info', {'course_id': course_id}, - descriptor) - - elif descriptor.location.category == 'custom_tag_template': - pass - - else: - - kwargs = {'course_id': course_id, - 'location': descriptor.location.url()} - - self._assert_loads('jump_to', kwargs, descriptor, - expect_redirect=True, - check_content=True) - - def _assert_loads(self, django_url, kwargs, descriptor, - expect_redirect=False, - check_content=False): - ''' - Assert that the url loads correctly. - If expect_redirect, then also check that we were redirected. - If check_content, then check that we don't get - an error message about unavailable modules. - ''' - - url = reverse(django_url, kwargs=kwargs) - response = self.client.get(url, follow=True) - - if response.status_code != 200: - self.fail('Status %d for page %s' % - (response.status_code, descriptor.location.url())) - - if expect_redirect: - self.assertEqual(response.redirect_chain[0][1], 302) - - if check_content: - unavailable_msg = "this module is temporarily unavailable" - self.assertEqual(response.content.find(unavailable_msg), -1) - self.assertFalse(isinstance(descriptor, ErrorDescriptor)) - - -@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) -class TestCoursesLoadTestCase_XmlModulestore(PageLoaderTestCase): - '''Check that all pages in test courses load properly from XML''' - - def setUp(self): - super(TestCoursesLoadTestCase_XmlModulestore, self).setUp() - self.setup_viewtest_user() - xmodule.modulestore.django._MODULESTORES = {} - - def test_toy_course_loads(self): - module_class = 'xmodule.hidden_module.HiddenDescriptor' - module_store = XMLModuleStore(TEST_DATA_DIR, - default_class=module_class, - course_dirs=['toy'], - load_error_modules=True) - - self.check_random_page_loads(module_store) +from helpers import LoginEnrollmentTestCase, check_for_get_code +from modulestore_config import TEST_DATA_MONGO_MODULESTORE @override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) -class TestCoursesLoadTestCase_MongoModulestore(PageLoaderTestCase): - '''Check that all pages in test courses load properly from Mongo''' +class TestNavigation(ModuleStoreTestCase, LoginEnrollmentTestCase): - def setUp(self): - super(TestCoursesLoadTestCase_MongoModulestore, self).setUp() - self.setup_viewtest_user() - xmodule.modulestore.django._MODULESTORES = {} - modulestore().collection.drop() + STUDENT_INFO = [('view@test.com', 'foo'), ('view2@test.com', 'foo')] - def test_toy_course_loads(self): - module_store = modulestore() - import_from_xml(module_store, TEST_DATA_DIR, ['toy']) - self.check_random_page_loads(module_store) - - def test_full_textbooks_loads(self): - module_store = modulestore() - import_from_xml(module_store, TEST_DATA_DIR, ['full']) - - course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None])) - - self.assertGreater(len(course.textbooks), 0) - - -@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) -class TestNavigation(MongoLoginHelpers): - - """Check that navigation state is saved properly""" + """ + Check that navigation state is saved properly. + """ def setUp(self): xmodule.modulestore.django._MODULESTORES = {} @@ -388,52 +34,67 @@ class TestNavigation(MongoLoginHelpers): self.section9 = ItemFactory.create(parent_location=self.chapter9.location, display_name='factory_section') - #Create two accounts - self.student = 'view@test.com' - self.student2 = 'view2@test.com' - self.password = 'foo' - self.create_account('u1', self.student, self.password) - self.create_account('u2', self.student2, self.password) - self.activate_user(self.student) - self.activate_user(self.student2) + # Create student accounts and activate them. + for i in range(len(self.STUDENT_INFO)): + self.create_account('u{0}'.format(i), self.STUDENT_INFO[i][0], self.STUDENT_INFO[i][1]) + self.activate_user(self.STUDENT_INFO[i][0]) - def test_accordion_state(self): - """Make sure that the accordion remembers where you were properly""" - self.login(self.student, self.password) - self.enroll(self.course) - self.enroll(self.full) - - # First request should redirect to ToyVideos + def test_redirects_first_time(self): + """ + Verify that the first time we click on the courseware tab we are + redirected to the 'Welcome' section. + """ + self.login(self.STUDENT_INFO[0][0], self.STUDENT_INFO[0][1]) + self.enroll(self.course, True) + self.enroll(self.full, True) resp = self.client.get(reverse('courseware', kwargs={'course_id': self.course.id})) - # Don't use no-follow, because state should - # only be saved once we actually hit the section self.assertRedirects(resp, reverse( 'courseware_section', kwargs={'course_id': self.course.id, 'chapter': 'Overview', 'section': 'Welcome'})) - # Hitting the couseware tab again should - # redirect to the first chapter: 'Overview' + def test_redirects_second_time(self): + """ + Verify the accordion remembers we've already visited the Welcome section + and redirects correpondingly. + """ + self.login(self.STUDENT_INFO[0][0], self.STUDENT_INFO[0][1]) + self.enroll(self.course, True) + self.enroll(self.full, True) + + self.client.get(reverse('courseware_section', kwargs={'course_id': self.course.id, + 'chapter': 'Overview', + 'section': 'Welcome'})) + resp = self.client.get(reverse('courseware', kwargs={'course_id': self.course.id})) - self.assertRedirectsNoFollow(resp, reverse('courseware_chapter', - kwargs={'course_id': self.course.id, - 'chapter': 'Overview'})) + self.assertRedirects(resp, reverse('courseware_chapter', + kwargs={'course_id': self.course.id, + 'chapter': 'Overview'})) - # Now we directly navigate to a section in a different chapter - self.check_for_get_code(200, reverse('courseware_section', - kwargs={'course_id': self.course.id, - 'chapter': 'factory_chapter', - 'section': 'factory_section'})) + def test_accordion_state(self): + """ + Verify the accordion remembers which chapter you were last viewing. + """ - # And now hitting the courseware tab should redirect to 'secret:magic' + self.login(self.STUDENT_INFO[0][0], self.STUDENT_INFO[0][1]) + self.enroll(self.course, True) + self.enroll(self.full, True) + + # Now we directly navigate to a section in a chapter other than 'Overview'. + check_for_get_code(self, 200, reverse('courseware_section', + kwargs={'course_id': self.course.id, + 'chapter': 'factory_chapter', + 'section': 'factory_section'})) + + # And now hitting the courseware tab should redirect to 'factory_chapter' resp = self.client.get(reverse('courseware', kwargs={'course_id': self.course.id})) - self.assertRedirectsNoFollow(resp, reverse('courseware_chapter', - kwargs={'course_id': self.course.id, - 'chapter': 'factory_chapter'})) + self.assertRedirects(resp, reverse('courseware_chapter', + kwargs={'course_id': self.course.id, + 'chapter': 'factory_chapter'})) diff --git a/lms/djangoapps/courseware/tests/test_view_authentication.py b/lms/djangoapps/courseware/tests/test_view_authentication.py index da4f40e0db..ffae4688bf 100644 --- a/lms/djangoapps/courseware/tests/test_view_authentication.py +++ b/lms/djangoapps/courseware/tests/test_view_authentication.py @@ -1,80 +1,56 @@ -import logging import datetime import pytz import random -from uuid import uuid4 +import xmodule.modulestore.django from django.contrib.auth.models import User, Group from django.conf import settings from django.core.urlresolvers import reverse from django.test.utils import override_settings -import xmodule.modulestore.django - # Need access to internal func to put users in the right group from courseware.access import (has_access, _course_staff_group_name, course_beta_test_group_name) -from mongo_login_helpers import MongoLoginHelpers +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory -log = logging.getLogger("mitx." + __name__) - - -def get_user(email): - '''look up a user by email''' - return User.objects.get(email=email) - - -def update_course(course, data): - """ - Updates the version of course in the mongo modulestore - with the metadata in data and returns the updated version. - """ - - store = xmodule.modulestore.django.modulestore() - - store.update_item(course.location, data) - - store.update_metadata(course.location, data) - - updated_course = store.get_instance(course.id, course.location) - - return updated_course - - -def mongo_store_config(data_dir): - ''' - Defines default module store using MongoModuleStore - - Use of this config requires mongo to be running - ''' - store = { - 'default': { - 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', - 'OPTIONS': { - 'default_class': 'xmodule.raw_module.RawDescriptor', - 'host': 'localhost', - 'db': 'test_xmodule', - 'collection': 'modulestore_%s' % uuid4().hex, - 'fs_root': data_dir, - 'render_template': 'mitxmako.shortcuts.render_to_string', - } - } - } - store['direct'] = store['default'] - return store - - -TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT -TEST_DATA_MONGO_MODULESTORE = mongo_store_config(TEST_DATA_DIR) +from helpers import LoginEnrollmentTestCase, check_for_get_code +from modulestore_config import TEST_DATA_MONGO_MODULESTORE @override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) -class TestViewAuth(MongoLoginHelpers): - """Check that view authentication works properly""" +class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): + """ + Check that view authentication works properly. + """ + + ACCOUNT_INFO = [('view@test.com', 'foo'), ('view2@test.com', 'foo')] + + @classmethod + def _instructor_urls(self, course): + """ + List of urls that only instructors/staff should be able to see. + """ + urls = [reverse(name, kwargs={'course_id': course.id}) for name in ( + 'instructor_dashboard', + 'gradebook', + 'grade_summary',)] + + urls.append(reverse('student_progress', + kwargs={'course_id': course.id, + 'student_id': User.objects.get(email=self.ACCOUNT_INFO[0][0]).id})) + return urls + + @staticmethod + def _reverse_urls(names, course): + """ + Reverse a list of course urls. + """ + return [reverse(name, kwargs={'course_id': course.id}) + for name in names] def setUp(self): xmodule.modulestore.django._MODULESTORES = {} @@ -87,98 +63,105 @@ class TestViewAuth(MongoLoginHelpers): display_name='courseware') self.sub_overview_chapter = ItemFactory.create(parent_location=self.sub_courseware_chapter.location, display_name='Overview') - self.progress_chapter = ItemFactory.create(parent_location=self.course.location, - display_name='progress') - self.info_chapter = ItemFactory.create(parent_location=self.course.location, - display_name='info') self.welcome_section = ItemFactory.create(parent_location=self.overview_chapter.location, display_name='Welcome') - self.somewhere_in_progress = ItemFactory.create(parent_location=self.progress_chapter.location, - display_name='1') - # Create two accounts - self.student = 'view@test.com' - self.instructor = 'view2@test.com' - self.password = 'foo' - self.create_account('u1', self.student, self.password) - self.create_account('u2', self.instructor, self.password) - self.activate_user(self.student) - self.activate_user(self.instructor) + # Create two accounts and activate them. + for i in range(len(self.ACCOUNT_INFO)): + self.create_account('u{0}'.format(i), self.ACCOUNT_INFO[i][0], self.ACCOUNT_INFO[i][1]) + self.activate_user(self.ACCOUNT_INFO[i][0]) - def test_instructor_pages(self): - """Make sure only instructors for the course - or staff can load the instructor - dashboard, the grade views, and student profile pages""" + def test_redirection_unenrolled(self): + """ + Verify unenrolled student is redirected to the 'about' section of the chapter + instead of the 'Welcome' section after clicking on the courseware tab. + """ + + self.login(self.ACCOUNT_INFO[0][0], self.ACCOUNT_INFO[0][1]) + response = self.client.get(reverse('courseware', + kwargs={'course_id': self.course.id})) + self.assertRedirects(response, + reverse('about_course', + args=[self.course.id])) + + def test_redirection_enrolled(self): + """ + Verify enrolled student is redirected to the 'Welcome' section of + the chapter after clicking on the courseware tab. + """ + + self.login(self.ACCOUNT_INFO[0][0], self.ACCOUNT_INFO[0][1]) + self.enroll(self.course) - # First, try with an enrolled student - self.login(self.student, self.password) - # shouldn't work before enroll response = self.client.get(reverse('courseware', kwargs={'course_id': self.course.id})) - self.assertRedirectsNoFollow(response, - reverse('about_course', - args=[self.course.id])) + self.assertRedirects(response, + reverse('courseware_section', + kwargs={'course_id': self.course.id, + 'chapter': 'Overview', + 'section': 'Welcome'})) + + def test_instructor_page_access_nonstaff(self): + """ + Verify non-staff cannot load the instructor + dashboard, the grade views, and student profile pages. + """ + + self.login(self.ACCOUNT_INFO[0][0], self.ACCOUNT_INFO[0][1]) + self.enroll(self.course) self.enroll(self.full) - # should work now -- redirect to first page - response = self.client.get(reverse('courseware', - kwargs={'course_id': self.course.id})) - - self.assertRedirectsNoFollow(response, - reverse('courseware_section', - kwargs={'course_id': self.course.id, - 'chapter': 'Overview', - 'section': 'Welcome'})) - - def instructor_urls(course): - "list of urls that only instructors/staff should be able to see" - urls = [reverse(name, kwargs={'course_id': course.id}) for name in ( - 'instructor_dashboard', - 'gradebook', - 'grade_summary',)] - - urls.append(reverse('student_progress', - kwargs={'course_id': course.id, - 'student_id': get_user(self.student).id})) - return urls # Randomly sample an instructor page - url = random.choice(instructor_urls(self.course) + - instructor_urls(self.full)) + url = random.choice(self._instructor_urls(self.course) + + self._instructor_urls(self.full)) # Shouldn't be able to get to the instructor pages print 'checking for 404 on {0}'.format(url) - self.check_for_get_code(404, url) + check_for_get_code(self, 404, url) - # Make the instructor staff in the toy course + def test_instructor_course_access(self): + """ + Verify instructor can load the instructor dashboard, the grade views, + and student profile pages for their course. + """ + + # Make the instructor staff in self.course group_name = _course_staff_group_name(self.course.location) group = Group.objects.create(name=group_name) - group.user_set.add(get_user(self.instructor)) + group.user_set.add(User.objects.get(email=self.ACCOUNT_INFO[1][0])) - self.logout() - self.login(self.instructor, self.password) + self.login(self.ACCOUNT_INFO[1][0], self.ACCOUNT_INFO[1][1]) - # Now should be able to get to the toy course, but not the full course - url = random.choice(instructor_urls(self.course)) + # Now should be able to get to self.course, but not self.full + url = random.choice(self._instructor_urls(self.course)) print 'checking for 200 on {0}'.format(url) - self.check_for_get_code(200, url) + check_for_get_code(self, 200, url) - url = random.choice(instructor_urls(self.full)) + url = random.choice(self._instructor_urls(self.full)) print 'checking for 404 on {0}'.format(url) - self.check_for_get_code(404, url) + check_for_get_code(self, 404, url) - # now also make the instructor staff - instructor = get_user(self.instructor) + def test_instructor_as_staff_access(self): + """ + Verify the instructor can load staff pages if he is given + staff permissions. + """ + + self.login(self.ACCOUNT_INFO[1][0], self.ACCOUNT_INFO[1][1]) + + # now make the instructor also staff + instructor = User.objects.get(email=self.ACCOUNT_INFO[1][0]) instructor.is_staff = True instructor.save() # and now should be able to load both - url = random.choice(instructor_urls(self.course) + - instructor_urls(self.full)) + url = random.choice(self._instructor_urls(self.course) + + self._instructor_urls(self.full)) print 'checking for 200 on {0}'.format(url) - self.check_for_get_code(200, url) + check_for_get_code(self, 200, url) def run_wrapped(self, test): """ @@ -196,42 +179,47 @@ class TestViewAuth(MongoLoginHelpers): settings.MITX_FEATURES['DISABLE_START_DATES'] = oldDSD def test_dark_launch(self): - """Make sure that before course start, students can't access course - pages, but instructors can""" + """ + Make sure that before course start, students can't access course + pages, but instructors can. + """ self.run_wrapped(self._do_test_dark_launch) def test_enrollment_period(self): - """Check that enrollment periods work""" + """ + Check that enrollment periods work. + """ self.run_wrapped(self._do_test_enrollment_period) def test_beta_period(self): - """Check that beta-test access works""" + """ + Check that beta-test access works. + """ self.run_wrapped(self._do_test_beta_period) def _do_test_dark_launch(self): - """Actually do the test, relying on settings to be right.""" + """ + Actually do the test, relying on settings to be right. + """ # Make courses start in the future now = datetime.datetime.now(pytz.UTC) tomorrow = now + datetime.timedelta(days=1) - self.course.lms.start = tomorrow - self.full.lms.start = tomorrow + course_data = {'start': tomorrow} + full_data = {'start': tomorrow} + self.course = self.update_course(self.course, course_data) + self.full = self.update_course(self.full, full_data) self.assertFalse(self.course.has_started()) self.assertFalse(self.full.has_started()) self.assertFalse(settings.MITX_FEATURES['DISABLE_START_DATES']) - def reverse_urls(names, course): - """Reverse a list of course urls""" - return [reverse(name, kwargs={'course_id': course.id}) - for name in names] - def dark_student_urls(course): """ - list of urls that students should be able to see only + List of urls that students should be able to see only after launch, but staff should see before """ - urls = reverse_urls(['info', 'progress'], course) + urls = self._reverse_urls(['info', 'progress'], course) urls.extend([ reverse('book', kwargs={'course_id': course.id, 'book_index': index}) @@ -241,38 +229,50 @@ class TestViewAuth(MongoLoginHelpers): def light_student_urls(course): """ - list of urls that students should be able to see before + List of urls that students should be able to see before launch. """ - urls = reverse_urls(['about_course'], course) + urls = self._reverse_urls(['about_course'], course) urls.append(reverse('courses')) return urls def instructor_urls(course): - """list of urls that only instructors/staff should be able to see""" - urls = reverse_urls(['instructor_dashboard', - 'gradebook', 'grade_summary'], course) + """ + List of urls that only instructors/staff should be able to see. + """ + urls = self._reverse_urls(['instructor_dashboard', + 'gradebook', 'grade_summary'], course) return urls - def check_non_staff(course): - """Check that access is right for non-staff in course""" + def check_non_staff_light(course): + """ + Check that non-staff have access to light urls. + """ + print '=== Checking non-staff access for {0}'.format(course.id) + + # Randomly sample a light url + url = random.choice(light_student_urls(course)) + print 'checking for 200 on {0}'.format(url) + check_for_get_code(self, 200, url) + + def check_non_staff_dark(course): + """ + Check that non-staff don't have access to dark urls. + """ print '=== Checking non-staff access for {0}'.format(course.id) # Randomly sample a dark url url = random.choice(instructor_urls(course) + dark_student_urls(course) + - reverse_urls(['courseware'], course)) + self._reverse_urls(['courseware'], course)) print 'checking for 404 on {0}'.format(url) - self.check_for_get_code(404, url) - - # Randomly sample a light url - url = random.choice(light_student_urls(course)) - print 'checking for 200 on {0}'.format(url) - self.check_for_get_code(200, url) + check_for_get_code(self, 404, url) def check_staff(course): - """Check that access is right for staff in course""" + """ + Check that access is right for staff in course. + """ print '=== Checking staff access for {0}'.format(course.id) # Randomly sample a url @@ -280,7 +280,7 @@ class TestViewAuth(MongoLoginHelpers): dark_student_urls(course) + light_student_urls(course)) print 'checking for 200 on {0}'.format(url) - self.check_for_get_code(200, url) + check_for_get_code(self, 200, url) # The student progress tab is not accessible to a student # before launch, so the instructor view-as-student feature @@ -290,43 +290,46 @@ class TestViewAuth(MongoLoginHelpers): # user (the student), and the requesting user (the prof) url = reverse('student_progress', kwargs={'course_id': course.id, - 'student_id': get_user(self.student).id}) + 'student_id': User.objects.get(email=self.ACCOUNT_INFO[0][0]).id}) print 'checking for 404 on view-as-student: {0}'.format(url) - self.check_for_get_code(404, url) + check_for_get_code(self, 404, url) # The courseware url should redirect, not 200 - url = reverse_urls(['courseware'], course)[0] - self.check_for_get_code(302, url) + url = self._reverse_urls(['courseware'], course)[0] + check_for_get_code(self, 302, url) # First, try with an enrolled student print '=== Testing student access....' - self.login(self.student, self.password) - self.enroll(self.course) - self.enroll(self.full) + self.login(self.ACCOUNT_INFO[0][0], self.ACCOUNT_INFO[0][1]) + self.enroll(self.course, True) + self.enroll(self.full, True) # shouldn't be able to get to anything except the light pages - check_non_staff(self.course) - check_non_staff(self.full) + check_non_staff_light(self.course) + check_non_staff_dark(self.course) + check_non_staff_light(self.full) + check_non_staff_dark(self.full) print '=== Testing course instructor access....' - # Make the instructor staff in the toy course + # Make the instructor staff in self.course group_name = _course_staff_group_name(self.course.location) group = Group.objects.create(name=group_name) - group.user_set.add(get_user(self.instructor)) + group.user_set.add(User.objects.get(email=self.ACCOUNT_INFO[1][0])) self.logout() - self.login(self.instructor, self.password) + self.login(self.ACCOUNT_INFO[1][0], self.ACCOUNT_INFO[1][1]) # Enroll in the classes---can't see courseware otherwise. - self.enroll(self.course) - self.enroll(self.full) + self.enroll(self.course, True) + self.enroll(self.full, True) # should now be able to get to everything for self.course - check_non_staff(self.full) + check_non_staff_light(self.full) + check_non_staff_dark(self.full) check_staff(self.course) print '=== Testing staff access....' # now also make the instructor staff - instructor = get_user(self.instructor) + instructor = User.objects.get(email=self.ACCOUNT_INFO[1][0]) instructor.is_staff = True instructor.save() @@ -335,7 +338,9 @@ class TestViewAuth(MongoLoginHelpers): check_staff(self.full) def _do_test_enrollment_period(self): - """Actually do the test, relying on settings to be right.""" + """ + Actually do the test, relying on settings to be right. + """ # Make courses start in the future now = datetime.datetime.now(pytz.UTC) @@ -348,42 +353,44 @@ class TestViewAuth(MongoLoginHelpers): print "changing" # self.course's enrollment period hasn't started - self.course = update_course(self.course, course_data) + self.course = self.update_course(self.course, course_data) # full course's has - self.full = update_course(self.full, full_data) + self.full = self.update_course(self.full, full_data) print "login" # First, try with an enrolled student print '=== Testing student access....' - self.login(self.student, self.password) - self.assertFalse(self.try_enroll(self.course)) - self.assertTrue(self.try_enroll(self.full)) + self.login(self.ACCOUNT_INFO[0][0], self.ACCOUNT_INFO[0][1]) + self.assertFalse(self.enroll(self.course)) + self.assertTrue(self.enroll(self.full)) print '=== Testing course instructor access....' - # Make the instructor staff in the toy course + # Make the instructor staff in the self.course group_name = _course_staff_group_name(self.course.location) group = Group.objects.create(name=group_name) - group.user_set.add(get_user(self.instructor)) + group.user_set.add(User.objects.get(email=self.ACCOUNT_INFO[1][0])) print "logout/login" self.logout() - self.login(self.instructor, self.password) - print "Instructor should be able to enroll in toy course" - self.assertTrue(self.try_enroll(self.course)) + self.login(self.ACCOUNT_INFO[1][0], self.ACCOUNT_INFO[1][1]) + print "Instructor should be able to enroll in self.course" + self.assertTrue(self.enroll(self.course)) print '=== Testing staff access....' # now make the instructor global staff, but not in the instructor group - group.user_set.remove(get_user(self.instructor)) - instructor = get_user(self.instructor) + group.user_set.remove(User.objects.get(email=self.ACCOUNT_INFO[1][0])) + instructor = User.objects.get(email=self.ACCOUNT_INFO[1][0]) instructor.is_staff = True instructor.save() # unenroll and try again self.unenroll(self.course) - self.assertTrue(self.try_enroll(self.course)) + self.assertTrue(self.enroll(self.course)) def _do_test_beta_period(self): - """Actually test beta periods, relying on settings to be right.""" + """ + Actually test beta periods, relying on settings to be right. + """ # trust, but verify :) self.assertFalse(settings.MITX_FEATURES['DISABLE_START_DATES']) @@ -391,18 +398,17 @@ class TestViewAuth(MongoLoginHelpers): # Make courses start in the future now = datetime.datetime.now(pytz.UTC) tomorrow = now + datetime.timedelta(days=1) - # nextday = tomorrow + 24 * 3600 - # yesterday = time.time() - 24 * 3600 + course_data = {'start': tomorrow} # self.course's hasn't started - self.course.lms.start = tomorrow + self.course = self.update_course(self.course, course_data) self.assertFalse(self.course.has_started()) # but should be accessible for beta testers self.course.lms.days_early_for_beta = 2 # student user shouldn't see it - student_user = get_user(self.student) + student_user = User.objects.get(email=self.ACCOUNT_INFO[0][0]) self.assertFalse(has_access(student_user, self.course, 'load')) # now add the student to the beta test group diff --git a/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py index 3e39227171..43b190c04b 100644 --- a/lms/djangoapps/courseware/tests/tests.py +++ b/lms/djangoapps/courseware/tests/tests.py @@ -1,275 +1,172 @@ ''' Test for lms courseware app ''' -import logging -import json import random -from urlparse import urlsplit, urlunsplit -from uuid import uuid4 - -from django.contrib.auth.models import User, Group from django.test import TestCase -from django.test.client import RequestFactory -from django.conf import settings from django.core.urlresolvers import reverse from django.test.utils import override_settings import xmodule.modulestore.django -# Need access to internal func to put users in the right group -from courseware import grades -from courseware.model_data import ModelDataCache -from courseware.access import (has_access, _course_staff_group_name, - course_beta_test_group_name) - -from student.models import Registration from xmodule.error_module import ErrorDescriptor from xmodule.modulestore.django import modulestore from xmodule.modulestore import Location from xmodule.modulestore.xml_importer import import_from_xml from xmodule.modulestore.xml import XMLModuleStore -import datetime -from django.utils.timezone import UTC -from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from helpers import LoginEnrollmentTestCase +from modulestore_config import TEST_DATA_DIR, TEST_DATA_XML_MODULESTORE, TEST_DATA_MONGO_MODULESTORE, TEST_DATA_DRAFT_MONGO_MODULESTORE -log = logging.getLogger("mitx." + __name__) +class ActivateLoginTest(LoginEnrollmentTestCase): + """ + Test logging in and logging out. + """ + def setUp(self): + self.setup_user() - -def parse_json(response): - """Parse response, which is assumed to be json""" - return json.loads(response.content) - - -def get_user(email): - '''look up a user by email''' - return User.objects.get(email=email) - - -def get_registration(email): - '''look up registration object by email''' - return Registration.objects.get(user__email=email) - - -def mongo_store_config(data_dir): - ''' - Defines default module store using MongoModuleStore - - Use of this config requires mongo to be running - ''' - store = { - 'default': { - 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', - 'OPTIONS': { - 'default_class': 'xmodule.raw_module.RawDescriptor', - 'host': 'localhost', - 'db': 'test_xmodule', - 'collection': 'modulestore_%s' % uuid4().hex, - 'fs_root': data_dir, - 'render_template': 'mitxmako.shortcuts.render_to_string' - } - } - } - store['direct'] = store['default'] - return store - - -def draft_mongo_store_config(data_dir): - '''Defines default module store using DraftMongoModuleStore''' - return { - 'default': { - 'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore', - 'OPTIONS': { - 'default_class': 'xmodule.raw_module.RawDescriptor', - 'host': 'localhost', - 'db': 'test_xmodule', - 'collection': 'modulestore_%s' % uuid4().hex, - 'fs_root': data_dir, - 'render_template': 'mitxmako.shortcuts.render_to_string', - } - }, - 'direct': { - 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', - 'OPTIONS': { - 'default_class': 'xmodule.raw_module.RawDescriptor', - 'host': 'localhost', - 'db': 'test_xmodule', - 'collection': 'modulestore_%s' % uuid4().hex, - 'fs_root': data_dir, - 'render_template': 'mitxmako.shortcuts.render_to_string', - } - } - } - - -def xml_store_config(data_dir): - '''Defines default module store using XMLModuleStore''' - return { - 'default': { - 'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore', - 'OPTIONS': { - 'data_dir': data_dir, - 'default_class': 'xmodule.hidden_module.HiddenDescriptor', - } - } - } - -TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT -TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR) -TEST_DATA_MONGO_MODULESTORE = mongo_store_config(TEST_DATA_DIR) -TEST_DATA_DRAFT_MONGO_MODULESTORE = draft_mongo_store_config(TEST_DATA_DIR) - - -class LoginEnrollmentTestCase(TestCase): - - ''' - Base TestCase providing support for user creation, - activation, login, and course enrollment - ''' - - def assertRedirectsNoFollow(self, response, expected_url): + def test_activate_login(self): """ - http://devblog.point2.com/2010/04/23/djangos-assertredirects-little-gotcha/ - - Don't check that the redirected-to page loads--there should be other tests for that. - - Some of the code taken from django.test.testcases.py + Test login -- the setup function does all the work. """ - self.assertEqual(response.status_code, 302, - 'Response status code was %d instead of 302' - % (response.status_code)) - url = response['Location'] + pass - e_scheme, e_netloc, e_path, e_query, e_fragment = urlsplit(expected_url) - if not (e_scheme or e_netloc): - expected_url = urlunsplit(('http', 'testserver', - e_path, e_query, e_fragment)) - - self.assertEqual(url, expected_url, - "Response redirected to '%s', expected '%s'" % - (url, expected_url)) - - def setup_viewtest_user(self): - '''create a user account, activate, and log in''' - self.viewtest_email = 'view@test.com' - self.viewtest_password = 'foo' - self.viewtest_username = 'viewtest' - self.create_account(self.viewtest_username, - self.viewtest_email, self.viewtest_password) - self.activate_user(self.viewtest_email) - self.login(self.viewtest_email, self.viewtest_password) - - # ============ User creation and login ============== - - def _login(self, email, password): - '''Login. View should always return 200. The success/fail is in the - returned json''' - resp = self.client.post(reverse('login'), - {'email': email, 'password': password}) - self.assertEqual(resp.status_code, 200) - return resp - - def login(self, email, password): - '''Login, check that it worked.''' - resp = self._login(email, password) - data = parse_json(resp) - self.assertTrue(data['success']) - return resp - - def logout(self): - '''Logout, check that it worked.''' - resp = self.client.get(reverse('logout'), {}) - # should redirect - self.assertEqual(resp.status_code, 302) - return resp - - def _create_account(self, username, email, password): - '''Try to create an account. No error checking''' - resp = self.client.post('/create_account', { - 'username': username, - 'email': email, - 'password': password, - 'name': 'Fred Weasley', - 'terms_of_service': 'true', - 'honor_code': 'true', - }) - return resp - - def create_account(self, username, email, password): - '''Create the account and check that it worked''' - resp = self._create_account(username, email, password) - self.assertEqual(resp.status_code, 200) - data = parse_json(resp) - self.assertEqual(data['success'], True) - - # Check both that the user is created, and inactive - self.assertFalse(get_user(email).is_active) - - return resp - - def _activate_user(self, email): - '''Look up the activation key for the user, then hit the activate view. - No error checking''' - activation_key = get_registration(email).activation_key - - # and now we try to activate - url = reverse('activate', kwargs={'key': activation_key}) - resp = self.client.get(url) - return resp - - def activate_user(self, email): - resp = self._activate_user(email) - self.assertEqual(resp.status_code, 200) - # Now make sure that the user is now actually activated - self.assertTrue(get_user(email).is_active) - - def try_enroll(self, course): - """Try to enroll. Return bool success instead of asserting it.""" - resp = self.client.post('/change_enrollment', { - 'enrollment_action': 'enroll', - 'course_id': course.id, - }) - print ('Enrollment in %s result status code: %s' - % (course.location.url(), str(resp.status_code))) - return resp.status_code == 200 - - def enroll(self, course): - """Enroll the currently logged-in user, and check that it worked.""" - result = self.try_enroll(course) - self.assertTrue(result) - - def unenroll(self, course): - """Unenroll the currently logged-in user, and check that it worked.""" - resp = self.client.post('/change_enrollment', { - 'enrollment_action': 'unenroll', - 'course_id': course.id, - }) - self.assertTrue(resp.status_code == 200) - - def check_for_get_code(self, code, url): + def test_logout(self): """ - Check that we got the expected code when accessing url via GET. - Returns the response. + Test logout -- setup function does login. """ - resp = self.client.get(url) - self.assertEqual(resp.status_code, code, - "got code %d for url '%s'. Expected code %d" - % (resp.status_code, url, code)) - return resp + self.logout() - def check_for_post_code(self, code, url, data={}): + +class PageLoaderTestCase(LoginEnrollmentTestCase): + """ + Base class that adds a function to load all pages in a modulestore. + """ + + def check_random_page_loads(self, module_store): """ - Check that we got the expected code when accessing url via POST. - Returns the response. + Choose a page in the course randomly, and assert that it loads. """ - resp = self.client.post(url, data) - self.assertEqual(resp.status_code, code, - "got code %d for url '%s'. Expected code %d" - % (resp.status_code, url, code)) - return resp + # enroll in the course before trying to access pages + courses = module_store.get_courses() + self.assertEqual(len(courses), 1) + course = courses[0] + self.enroll(course, True) + course_id = course.id + + # Search for items in the course + # None is treated as a wildcard + course_loc = course.location + location_query = Location(course_loc.tag, course_loc.org, + course_loc.course, None, None, None) + + items = module_store.get_items(location_query) + + if len(items) < 1: + self.fail('Could not retrieve any items from course') + else: + descriptor = random.choice(items) + + # We have ancillary course information now as modules + # and we can't simply use 'jump_to' to view them + if descriptor.location.category == 'about': + self._assert_loads('about_course', + {'course_id': course_id}, + descriptor) + + elif descriptor.location.category == 'static_tab': + kwargs = {'course_id': course_id, + 'tab_slug': descriptor.location.name} + self._assert_loads('static_tab', kwargs, descriptor) + + elif descriptor.location.category == 'course_info': + self._assert_loads('info', {'course_id': course_id}, + descriptor) + + elif descriptor.location.category == 'custom_tag_template': + pass + + else: + + kwargs = {'course_id': course_id, + 'location': descriptor.location.url()} + + self._assert_loads('jump_to', kwargs, descriptor, + expect_redirect=True, + check_content=True) + + def _assert_loads(self, django_url, kwargs, descriptor, + expect_redirect=False, + check_content=False): + """ + Assert that the url loads correctly. + If expect_redirect, then also check that we were redirected. + If check_content, then check that we don't get + an error message about unavailable modules. + """ + + url = reverse(django_url, kwargs=kwargs) + response = self.client.get(url, follow=True) + + if response.status_code != 200: + self.fail('Status %d for page %s' % + (response.status_code, descriptor.location.url())) + + if expect_redirect: + self.assertEqual(response.redirect_chain[0][1], 302) + + if check_content: + unavailable_msg = "this module is temporarily unavailable" + self.assertEqual(response.content.find(unavailable_msg), -1) + self.assertFalse(isinstance(descriptor, ErrorDescriptor)) + + +@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) +class TestCoursesLoadTestCase_XmlModulestore(PageLoaderTestCase): + """ + Check that all pages in test courses load properly from XML. + """ + + def setUp(self): + super(TestCoursesLoadTestCase_XmlModulestore, self).setUp() + self.setup_user() + xmodule.modulestore.django._MODULESTORES = {} + + def test_toy_course_loads(self): + module_class = 'xmodule.hidden_module.HiddenDescriptor' + module_store = XMLModuleStore(TEST_DATA_DIR, + default_class=module_class, + course_dirs=['toy'], + load_error_modules=True) + + self.check_random_page_loads(module_store) + + +@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +class TestCoursesLoadTestCase_MongoModulestore(PageLoaderTestCase): + """ + Check that all pages in test courses load properly from Mongo. + """ + + def setUp(self): + super(TestCoursesLoadTestCase_MongoModulestore, self).setUp() + self.setup_user() + xmodule.modulestore.django._MODULESTORES = {} + modulestore().collection.drop() + + def test_toy_course_loads(self): + module_store = modulestore() + import_from_xml(module_store, TEST_DATA_DIR, ['toy']) + self.check_random_page_loads(module_store) + + def test_full_textbooks_loads(self): + module_store = modulestore() + import_from_xml(module_store, TEST_DATA_DIR, ['full']) + + course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None])) + + self.assertGreater(len(course.textbooks), 0) @override_settings(MODULESTORE=TEST_DATA_DRAFT_MONGO_MODULESTORE) @@ -284,134 +181,3 @@ class TestDraftModuleStore(TestCase): # test success is just getting through the above statement. # The bug was that 'course_id' argument was # not allowed to be passed in (i.e. was throwing exception) - - -@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) -class TestSubmittingProblems(LoginEnrollmentTestCase): - """Check that a course gets graded properly""" - - # Subclasses should specify the course slug - course_slug = "UNKNOWN" - course_when = "UNKNOWN" - - def setUp(self): - xmodule.modulestore.django._MODULESTORES = {} - - course_name = "edX/%s/%s" % (self.course_slug, self.course_when) - self.course = modulestore().get_course(course_name) - assert self.course, "Couldn't load course %r" % course_name - - # create a test student - self.student = 'view@test.com' - self.password = 'foo' - self.create_account('u1', self.student, self.password) - self.activate_user(self.student) - self.enroll(self.course) - - self.student_user = get_user(self.student) - - self.factory = RequestFactory() - - def problem_location(self, problem_url_name): - return "i4x://edX/{}/problem/{}".format(self.course_slug, problem_url_name) - - def modx_url(self, problem_location, dispatch): - return reverse( - 'modx_dispatch', - kwargs={ - 'course_id': self.course.id, - 'location': problem_location, - 'dispatch': dispatch, - } - ) - - def submit_question_answer(self, problem_url_name, responses): - """ - Submit answers to a question. - - Responses is a dict mapping problem ids (not sure of the right term) - to answers: - {'2_1': 'Correct', '2_2': 'Incorrect'} - - """ - problem_location = self.problem_location(problem_url_name) - modx_url = self.modx_url(problem_location, 'problem_check') - answer_key_prefix = 'input_i4x-edX-{}-problem-{}_'.format(self.course_slug, problem_url_name) - resp = self.client.post(modx_url, - { (answer_key_prefix + k): v for k, v in responses.items() } - ) - - return resp - - def reset_question_answer(self, problem_url_name): - '''resets specified problem for current user''' - problem_location = self.problem_location(problem_url_name) - modx_url = self.modx_url(problem_location, 'problem_reset') - resp = self.client.post(modx_url) - return resp - - -@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) -class TestSchematicResponse(TestSubmittingProblems): - """Check that we can submit a schematic response, and it answers properly.""" - - course_slug = "embedded_python" - course_when = "2013_Spring" - - def test_schematic(self): - resp = self.submit_question_answer('schematic_problem', - { '2_1': json.dumps( - [['transient', {'Z': [ - [0.0000004, 2.8], - [0.0000009, 2.8], - [0.0000014, 2.8], - [0.0000019, 2.8], - [0.0000024, 2.8], - [0.0000029, 0.2], - [0.0000034, 0.2], - [0.0000039, 0.2] - ]}]] - ) - }) - respdata = json.loads(resp.content) - self.assertEqual(respdata['success'], 'correct') - - self.reset_question_answer('schematic_problem') - resp = self.submit_question_answer('schematic_problem', - { '2_1': json.dumps( - [['transient', {'Z': [ - [0.0000004, 2.8], - [0.0000009, 0.0], # wrong. - [0.0000014, 2.8], - [0.0000019, 2.8], - [0.0000024, 2.8], - [0.0000029, 0.2], - [0.0000034, 0.2], - [0.0000039, 0.2] - ]}]] - ) - }) - respdata = json.loads(resp.content) - self.assertEqual(respdata['success'], 'incorrect') - - def test_check_function(self): - resp = self.submit_question_answer('cfn_problem', {'2_1': "0, 1, 2, 3, 4, 5, 'Outside of loop', 6"}) - respdata = json.loads(resp.content) - self.assertEqual(respdata['success'], 'correct') - - self.reset_question_answer('cfn_problem') - - resp = self.submit_question_answer('cfn_problem', {'2_1': "xyzzy!"}) - respdata = json.loads(resp.content) - self.assertEqual(respdata['success'], 'incorrect') - - def test_computed_answer(self): - resp = self.submit_question_answer('computed_answer', {'2_1': "Xyzzy"}) - respdata = json.loads(resp.content) - self.assertEqual(respdata['success'], 'correct') - - self.reset_question_answer('computed_answer') - - resp = self.submit_question_answer('computed_answer', {'2_1': "NO!"}) - respdata = json.loads(resp.content) - self.assertEqual(respdata['success'], 'incorrect') diff --git a/lms/djangoapps/instructor/tests/test_download_csv.py b/lms/djangoapps/instructor/tests/test_download_csv.py index 29e18eee4d..fd5bd562ba 100644 --- a/lms/djangoapps/instructor/tests/test_download_csv.py +++ b/lms/djangoapps/instructor/tests/test_download_csv.py @@ -11,12 +11,13 @@ django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/inst from django.test.utils import override_settings # Need access to internal func to put users in the right group -from django.contrib.auth.models import Group +from django.contrib.auth.models import Group, User from django.core.urlresolvers import reverse from courseware.access import _course_staff_group_name -from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE, get_user +from courseware.tests.helpers import LoginEnrollmentTestCase +from courseware.tests.modulestore_config import TEST_DATA_XML_MODULESTORE from xmodule.modulestore.django import modulestore import xmodule.modulestore.django @@ -45,7 +46,7 @@ class TestInstructorDashboardGradeDownloadCSV(LoginEnrollmentTestCase): def make_instructor(course): group_name = _course_staff_group_name(course.location) g = Group.objects.create(name=group_name) - g.user_set.add(get_user(self.instructor)) + g.user_set.add(User.objects.get(email=self.instructor)) make_instructor(self.toy) @@ -72,7 +73,7 @@ class TestInstructorDashboardGradeDownloadCSV(LoginEnrollmentTestCase): # All the not-actually-in-the-course hw and labs come from the # default grading policy string in graders.py expected_body = '''"ID","Username","Full Name","edX email","External email","HW 01","HW 02","HW 03","HW 04","HW 05","HW 06","HW 07","HW 08","HW 09","HW 10","HW 11","HW 12","HW Avg","Lab 01","Lab 02","Lab 03","Lab 04","Lab 05","Lab 06","Lab 07","Lab 08","Lab 09","Lab 10","Lab 11","Lab 12","Lab Avg","Midterm","Final" -"2","u2","Fred Weasley","view2@test.com","","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0" +"2","u2","username","view2@test.com","","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0" ''' self.assertEqual(body, expected_body, msg) diff --git a/lms/djangoapps/instructor/tests/test_enrollment.py b/lms/djangoapps/instructor/tests/test_enrollment.py index ce5f2d2e50..e70ccc6ffd 100644 --- a/lms/djangoapps/instructor/tests/test_enrollment.py +++ b/lms/djangoapps/instructor/tests/test_enrollment.py @@ -7,7 +7,8 @@ from django.test.utils import override_settings from django.contrib.auth.models import Group, User from django.core.urlresolvers import reverse from courseware.access import _course_staff_group_name -from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE, get_user +from courseware.tests.helpers import LoginEnrollmentTestCase +from courseware.tests.modulestore_config import TEST_DATA_XML_MODULESTORE from xmodule.modulestore.django import modulestore import xmodule.modulestore.django from student.models import CourseEnrollment, CourseEnrollmentAllowed @@ -40,7 +41,7 @@ class TestInstructorEnrollsStudent(LoginEnrollmentTestCase): def make_instructor(course): group_name = _course_staff_group_name(course.location) g = Group.objects.create(name=group_name) - g.user_set.add(get_user(self.instructor)) + g.user_set.add(User.objects.get(email=self.instructor)) make_instructor(self.toy) diff --git a/lms/djangoapps/instructor/tests/test_forum_admin.py b/lms/djangoapps/instructor/tests/test_forum_admin.py index 7b4e729867..90dadd569e 100644 --- a/lms/djangoapps/instructor/tests/test_forum_admin.py +++ b/lms/djangoapps/instructor/tests/test_forum_admin.py @@ -6,7 +6,7 @@ Unit tests for instructor dashboard forum administration from django.test.utils import override_settings # Need access to internal func to put users in the right group -from django.contrib.auth.models import Group +from django.contrib.auth.models import Group, User from django.core.urlresolvers import reverse from django_comment_common.models import Role, FORUM_ROLE_ADMINISTRATOR, \ @@ -14,7 +14,8 @@ from django_comment_common.models import Role, FORUM_ROLE_ADMINISTRATOR, \ from django_comment_client.utils import has_forum_access from courseware.access import _course_staff_group_name -from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE, get_user +from courseware.tests.helpers import LoginEnrollmentTestCase +from courseware.tests.modulestore_config import TEST_DATA_XML_MODULESTORE from xmodule.modulestore.django import modulestore import xmodule.modulestore.django @@ -55,7 +56,7 @@ class TestInstructorDashboardForumAdmin(LoginEnrollmentTestCase): group_name = _course_staff_group_name(self.toy.location) g = Group.objects.create(name=group_name) - g.user_set.add(get_user(self.instructor)) + g.user_set.add(User.objects.get(email=self.instructor)) self.logout() self.login(self.instructor, self.password) @@ -146,4 +147,4 @@ class TestInstructorDashboardForumAdmin(LoginEnrollmentTestCase): added_roles.append(rolename) added_roles.sort() roles = ', '.join(added_roles) - self.assertTrue(response.content.find('{0}'.format(roles)) >= 0, 'not finding roles "{0}"'.format(roles)) \ No newline at end of file + self.assertTrue(response.content.find('{0}'.format(roles)) >= 0, 'not finding roles "{0}"'.format(roles)) diff --git a/lms/djangoapps/open_ended_grading/tests.py b/lms/djangoapps/open_ended_grading/tests.py index 13d780df12..db19d212a2 100644 --- a/lms/djangoapps/open_ended_grading/tests.py +++ b/lms/djangoapps/open_ended_grading/tests.py @@ -8,8 +8,7 @@ import json from mock import MagicMock, patch, Mock from django.core.urlresolvers import reverse -from django.contrib.auth.models import Group -from django.http import HttpResponse +from django.contrib.auth.models import Group, User from django.conf import settings from mitxmako.shortcuts import render_to_string @@ -21,7 +20,6 @@ from xmodule.x_module import ModuleSystem from open_ended_grading import staff_grading_service, views from courseware.access import _course_staff_group_name -from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE, get_user import logging @@ -31,6 +29,9 @@ from django.test.utils import override_settings from xmodule.tests import test_util_open_ended from courseware.tests import factories +from courseware.tests.modulestore_config import TEST_DATA_XML_MODULESTORE +from courseware.tests.helpers import LoginEnrollmentTestCase, check_for_get_code, check_for_post_code + @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) class TestStaffGradingService(LoginEnrollmentTestCase): @@ -58,7 +59,7 @@ class TestStaffGradingService(LoginEnrollmentTestCase): def make_instructor(course): group_name = _course_staff_group_name(course.location) group = Group.objects.create(name=group_name) - group.user_set.add(get_user(self.instructor)) + group.user_set.add(User.objects.get(email=self.instructor)) make_instructor(self.toy) @@ -75,8 +76,8 @@ class TestStaffGradingService(LoginEnrollmentTestCase): # both get and post should return 404 for view_name in ('staff_grading_get_next', 'staff_grading_save_grade'): url = reverse(view_name, kwargs={'course_id': self.course_id}) - self.check_for_get_code(404, url) - self.check_for_post_code(404, url) + check_for_get_code(self, 404, url) + check_for_post_code(self, 404, url) def test_get_next(self): self.login(self.instructor, self.password) @@ -84,7 +85,7 @@ class TestStaffGradingService(LoginEnrollmentTestCase): url = reverse('staff_grading_get_next', kwargs={'course_id': self.course_id}) data = {'location': self.location} - response = self.check_for_post_code(200, url, data) + response = check_for_post_code(self, 200, url, data) content = json.loads(response.content) @@ -113,7 +114,7 @@ class TestStaffGradingService(LoginEnrollmentTestCase): if skip: data.update({'skipped': True}) - response = self.check_for_post_code(200, url, data) + response = check_for_post_code(self, 200, url, data) content = json.loads(response.content) self.assertTrue(content['success'], str(content)) self.assertEquals(content['submission_id'], self.mock_service.cnt) @@ -130,7 +131,7 @@ class TestStaffGradingService(LoginEnrollmentTestCase): url = reverse('staff_grading_get_problem_list', kwargs={'course_id': self.course_id}) data = {} - response = self.check_for_post_code(200, url, data) + response = check_for_post_code(self, 200, url, data) content = json.loads(response.content) self.assertTrue(content['success'], str(content)) diff --git a/lms/urls.py b/lms/urls.py index 74ac44cf59..2d85fe1e66 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -36,7 +36,7 @@ urlpatterns = ('', # nopep8 url(r'^login_ajax$', 'student.views.login_user', name="login"), url(r'^login_ajax/(?P[^/]*)$', 'student.views.login_user'), url(r'^logout$', 'student.views.logout_user', name='logout'), - url(r'^create_account$', 'student.views.create_account'), + url(r'^create_account$', 'student.views.create_account', name='create_account'), url(r'^activate/(?P[^/]*)$', 'student.views.activate_account', name="activate"), url(r'^begin_exam_registration/(?P[^/]+/[^/]+/[^/]+)$', 'student.views.begin_exam_registration', name="begin_exam_registration"), From e85fa6518227fdfa81cc048109661c9f6354d000 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Fri, 21 Jun 2013 14:24:41 -0400 Subject: [PATCH 132/995] 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 133/995] Make Course Team lettuce tests gender-neutral Because it bothers me, although I don't expect anyone else to care. --- .../contentstore/features/course-team.feature | 30 +++++++++---------- .../contentstore/features/course-team.py | 10 +++---- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/cms/djangoapps/contentstore/features/course-team.feature b/cms/djangoapps/contentstore/features/course-team.feature index 502321c49b..fc1212f398 100644 --- a/cms/djangoapps/contentstore/features/course-team.feature +++ b/cms/djangoapps/contentstore/features/course-team.feature @@ -3,32 +3,32 @@ Feature: Course Team Scenario: Users can add other users Given I have opened a new course in Studio - And The user "abcd" exists + And the user "alice" exists And I am viewing the course team settings - When I add "abcd" to the course team - And "abcd" logs in - Then He does see the course on his page + When I add "alice" to the course team + And "alice" logs in + Then she does see the course on her page Scenario: Added users cannot delete or add other users Given I have opened a new course in Studio - And The user "abcd" exists + And the user "bob" exists And I am viewing the course team settings - When I add "abcd" to the course team - And "abcd" logs in - Then He cannot delete users - And He cannot add users + When I add "bob" to the course team + And "bob" logs in + Then he cannot delete users + And he cannot add users Scenario: Users can delete other users Given I have opened a new course in Studio - And The user "abcd" exists + And the user "carol" exists And I am viewing the course team settings - When I add "abcd" to the course team - And I delete "abcd" from the course team - And "abcd" logs in - Then He does not see the course on his page + When I add "carol" to the course team + And I delete "carol" from the course team + And "carol" logs in + Then she does not see the course on her page Scenario: Users cannot add users that do not exist Given I have opened a new course in Studio And I am viewing the course team settings - When I add "abcd" to the course team + When I add "dennis" to the course team Then I should see "Could not find user by email address" somewhere on the page diff --git a/cms/djangoapps/contentstore/features/course-team.py b/cms/djangoapps/contentstore/features/course-team.py index 4303d5066c..c126773db6 100644 --- a/cms/djangoapps/contentstore/features/course-team.py +++ b/cms/djangoapps/contentstore/features/course-team.py @@ -15,7 +15,7 @@ def view_grading_settings(_step): world.css_click(link_css) -@step(u'The user "([^"]*)" exists$') +@step(u'the user "([^"]*)" exists$') def create_other_user(_step, name): create_studio_user(uname=name, password=PASSWORD, email=(name + EMAIL_EXTENSION)) @@ -44,8 +44,8 @@ def other_user_login(_step, name): log_into_studio(uname=name, password=PASSWORD, email=name + EMAIL_EXTENSION) -@step(u'He does( not)? see the course on his page') -def see_course(_step, doesnt_see_course): +@step(u's?he does( not)? see the course on (his|her) page') +def see_course(_step, doesnt_see_course, gender): class_css = 'span.class-name' all_courses = world.css_find(class_css) all_names = [item.html for item in all_courses] @@ -55,13 +55,13 @@ def see_course(_step, doesnt_see_course): assert _COURSE_NAME in all_names -@step(u'He cannot delete users') +@step(u's?he cannot delete users') def cannot_delete(_step): to_delete_css = 'a.remove-user' assert world.is_css_not_present(to_delete_css) -@step(u'He cannot add users') +@step(u's?he cannot add users') def cannot_add(_step): add_css = 'a.new-user' assert world.is_css_not_present(add_css) From 58fe6d4e8367c570648068d3307cc3105a88edf5 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Fri, 21 Jun 2013 16:17:33 -0400 Subject: [PATCH 134/995] Cleaned up import and comment --- cms/djangoapps/contentstore/tests/test_contentstore.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index b0cbcee032..6d2055d459 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -45,7 +45,7 @@ import xmodule.contentstore.django import datetime from pytz import UTC from uuid import uuid4 -import pymongo +from pymongo import MongoClient TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE) @@ -62,7 +62,6 @@ class MongoCollectionFindWrapper(object): return self.original(query, *args, **kwargs) -#@override_settings(MODULESTORE=TEST_DATA_MODULESTORE) @override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE) class ContentStoreToyCourseTest(ModuleStoreTestCase): """ @@ -91,7 +90,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): self.client.login(username=uname, password=password) def tearDown(self): - m = pymongo.MongoClient() + m = MongoClient() m.drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db']) xmodule.contentstore.django._CONTENTSTORE.clear() @@ -858,7 +857,7 @@ class ContentStoreTest(ModuleStoreTestCase): } def tearDown(self): - m = pymongo.MongoClient() + m = MongoClient() m.drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db']) xmodule.contentstore.django._CONTENTSTORE.clear() From 5e6de488abaa45f765b5aef48a1b36851a673be1 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Fri, 21 Jun 2013 16:28:32 -0400 Subject: [PATCH 135/995] Fixed pylint/pep8 violations --- .../contentstore/tests/test_contentstore.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 6d2055d459..514b631521 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -90,8 +90,8 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): self.client.login(username=uname, password=password) def tearDown(self): - m = MongoClient() - m.drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db']) + mongo = MongoClient() + mongo.drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db']) xmodule.contentstore.django._CONTENTSTORE.clear() def check_components_on_page(self, component_types, expected_types): @@ -414,7 +414,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): self.assertGreater(len(all_assets), 0) # make sure we have some thumbnails in our contentstore - all_thumbnails = content_store.get_all_content_thumbnails_for_course(course_location) + content_store.get_all_content_thumbnails_for_course(course_location) # # cdodge: temporarily comment out assertion on thumbnails because many environments @@ -543,7 +543,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): all_assets = trash_store.get_all_content_for_course(course_location) self.assertEqual(len(all_assets), 0) - all_thumbnails = trash_store.get_all_content_thumbnails_for_course(course_location) self.assertEqual(len(all_thumbnails), 0) @@ -608,7 +607,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): self.assertRaises(InvalidVersionError, draft_store.unpublish, location) - def test_bad_contentstore_request(self): resp = self.client.get('http://localhost:8001/c4x/CDX/123123/asset/&images_circuits_Lab7Solution2.png') self.assertEqual(resp.status_code, 400) @@ -857,8 +855,8 @@ class ContentStoreTest(ModuleStoreTestCase): } def tearDown(self): - m = MongoClient() - m.drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db']) + mongo = MongoClient() + mongo.drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db']) xmodule.contentstore.django._CONTENTSTORE.clear() def test_create_course(self): From bea50efc2651767fd5805f8687a7abafe41824c9 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 21 Jun 2013 14:29:28 -0400 Subject: [PATCH 136/995] Remove the unused smart-accordion lettuce feature, and the code it used. --- common/djangoapps/terrain/course_helpers.py | 46 ----- .../features/smart-accordion.feature | 63 ------- .../courseware/features/smart-accordion.py | 158 ------------------ 3 files changed, 267 deletions(-) delete mode 100644 lms/djangoapps/courseware/features/smart-accordion.feature delete mode 100644 lms/djangoapps/courseware/features/smart-accordion.py diff --git a/common/djangoapps/terrain/course_helpers.py b/common/djangoapps/terrain/course_helpers.py index dfe3803dfd..7da49e6315 100644 --- a/common/djangoapps/terrain/course_helpers.py +++ b/common/djangoapps/terrain/course_helpers.py @@ -13,8 +13,6 @@ from student.models import CourseEnrollment from xmodule.modulestore.django import modulestore from xmodule.contentstore.django import contentstore from xmodule.templates import update_templates -from bs4 import BeautifulSoup -import os.path from urllib import quote_plus @@ -76,50 +74,6 @@ def register_by_course_id(course_id, is_staff=False): CourseEnrollment.objects.get_or_create(user=u, course_id=course_id) - -@world.absorb -def save_the_course_content(path='/tmp'): - html = world.browser.html.encode('ascii', 'ignore') - soup = BeautifulSoup(html) - - # get rid of the header, we only want to compare the body - soup.head.decompose() - - # for now, remove the data-id attributes, because they are - # causing mismatches between cms-master and master - for item in soup.find_all(attrs={'data-id': re.compile('.*')}): - del item['data-id'] - - # we also need to remove them from unrendered problems, - # where they are contained in the text of divs instead of - # in attributes of tags - # Be careful of whether or not it was the last attribute - # and needs a trailing space - for item in soup.find_all(text=re.compile(' data-id=".*?" ')): - s = unicode(item.string) - item.string.replace_with(re.sub(' data-id=".*?" ', ' ', s)) - - for item in soup.find_all(text=re.compile(' data-id=".*?"')): - s = unicode(item.string) - item.string.replace_with(re.sub(' data-id=".*?"', ' ', s)) - - # prettify the html so it will compare better, with - # each HTML tag on its own line - output = soup.prettify() - - # use string slicing to grab everything after 'courseware/' in the URL - u = world.browser.url - section_url = u[u.find('courseware/') + 11:] - - if not os.path.exists(path): - os.makedirs(path) - - filename = '%s.html' % (quote_plus(section_url)) - f = open('%s/%s' % (path, filename), 'w') - f.write(output) - f.close - - @world.absorb def clear_courses(): # Flush and initialize the module store diff --git a/lms/djangoapps/courseware/features/smart-accordion.feature b/lms/djangoapps/courseware/features/smart-accordion.feature deleted file mode 100644 index fc51eca25d..0000000000 --- a/lms/djangoapps/courseware/features/smart-accordion.feature +++ /dev/null @@ -1,63 +0,0 @@ -# Here are all the courses for Fall 2012 -# MITx/3.091x/2012_Fall -# MITx/6.002x/2012_Fall -# MITx/6.00x/2012_Fall -# HarvardX/CS50x/2012 (we will not be testing this, as it is anomolistic) -# HarvardX/PH207x/2012_Fall -# BerkeleyX/CS169.1x/2012_Fall -# BerkeleyX/CS169.2x/2012_Fall -# BerkeleyX/CS184.1x/2012_Fall - -#You can load the courses into your data directory with these cmds: -# git clone https://github.com/MITx/3.091x.git -# git clone https://github.com/MITx/6.00x.git -# git clone https://github.com/MITx/content-mit-6002x.git -# git clone https://github.com/MITx/content-mit-6002x.git -# git clone https://github.com/MITx/content-harvard-id270x.git -# git clone https://github.com/MITx/content-berkeley-cs169x.git -# git clone https://github.com/MITx/content-berkeley-cs169.2x.git -# git clone https://github.com/MITx/content-berkeley-cs184x.git - -Feature: There are courses on the homepage - In order to compared rendered content to the database - As an acceptance test - I want to count all the chapters, sections, and tabs for each course - - # Commenting these all out for now because they don't always run, - # they have too many prerequesites, e.g. the course exists, and - # is within the start and end dates, etc. - - # Scenario: Navigate through course MITx/3.091x/2012_Fall - # Given I am registered for course "MITx/3.091x/2012_Fall" - # And I log in - # Then I verify all the content of each course - - # Scenario: Navigate through course MITx/6.002x/2012_Fall - # Given I am registered for course "MITx/6.002x/2012_Fall" - # And I log in - # Then I verify all the content of each course - - # Scenario: Navigate through course MITx/6.00x/2012_Fall - # Given I am registered for course "MITx/6.00x/2012_Fall" - # And I log in - # Then I verify all the content of each course - - # Scenario: Navigate through course HarvardX/PH207x/2012_Fall - # Given I am registered for course "HarvardX/PH207x/2012_Fall" - # And I log in - # Then I verify all the content of each course - - # Scenario: Navigate through course BerkeleyX/CS169.1x/2012_Fall - # Given I am registered for course "BerkeleyX/CS169.1x/2012_Fall" - # And I log in - # Then I verify all the content of each course - - # Scenario: Navigate through course BerkeleyX/CS169.2x/2012_Fall - # Given I am registered for course "BerkeleyX/CS169.2x/2012_Fall" - # And I log in - # Then I verify all the content of each course - - # Scenario: Navigate through course BerkeleyX/CS184.1x/2012_Fall - # Given I am registered for course "BerkeleyX/CS184.1x/2012_Fall" - # And I log in - # Then I verify all the content of each course diff --git a/lms/djangoapps/courseware/features/smart-accordion.py b/lms/djangoapps/courseware/features/smart-accordion.py deleted file mode 100644 index 63408d7683..0000000000 --- a/lms/djangoapps/courseware/features/smart-accordion.py +++ /dev/null @@ -1,158 +0,0 @@ -#pylint: disable=C0111 -#pylint: disable=W0621 - -from lettuce import world, step -from re import sub -from nose.tools import assert_equals -from xmodule.modulestore.django import modulestore -from common import * - -from logging import getLogger -logger = getLogger(__name__) - - -def check_for_errors(): - e = world.browser.find_by_css('.outside-app') - if len(e) > 0: - assert False, 'there was a server error at %s' % (world.browser.url) - else: - assert True - - -@step(u'I verify all the content of each course') -def i_verify_all_the_content_of_each_course(step): - all_possible_courses = get_courses() - logger.debug('Courses found:') - for c in all_possible_courses: - logger.debug(c.id) - ids = [c.id for c in all_possible_courses] - - # Get a list of all the registered courses - registered_courses = world.browser.find_by_css('article.my-course') - if len(all_possible_courses) < len(registered_courses): - assert False, "user is registered for more courses than are uniquely posssible" - else: - pass - - for test_course in registered_courses: - test_course.css_click('a') - check_for_errors() - - # Get the course. E.g. 'MITx/6.002x/2012_Fall' - current_course = sub('/info', '', sub('.*/courses/', '', world.browser.url)) - validate_course(current_course, ids) - - world.click_link('Courseware') - assert world.is_css_present('accordion') - check_for_errors() - browse_course(current_course) - - # clicking the user link gets you back to the user's home page - world.css_click('.user-link') - check_for_errors() - - -def browse_course(course_id): - - ## count chapters from xml and page and compare - chapters = get_courseware_with_tabs(course_id) - num_chapters = len(chapters) - - rendered_chapters = world.browser.find_by_css('#accordion > nav > div') - num_rendered_chapters = len(rendered_chapters) - - msg = '%d chapters expected, %d chapters found on page for %s' % (num_chapters, num_rendered_chapters, course_id) - #logger.debug(msg) - assert num_chapters == num_rendered_chapters, msg - - chapter_it = 0 - - ## Iterate the chapters - while chapter_it < num_chapters: - - ## click into a chapter - world.browser.find_by_css('#accordion > nav > div')[chapter_it].find_by_tag('h3').click() - - ## look for the "there was a server error" div - check_for_errors() - - ## count sections from xml and page and compare - sections = chapters[chapter_it]['sections'] - num_sections = len(sections) - - rendered_sections = world.browser.find_by_css('#accordion > nav > div')[chapter_it].find_by_tag('li') - num_rendered_sections = len(rendered_sections) - - msg = ('%d sections expected, %d sections found on page, %s - %d - %s' % - (num_sections, num_rendered_sections, course_id, chapter_it, chapters[chapter_it]['chapter_name'])) - #logger.debug(msg) - assert num_sections == num_rendered_sections, msg - - section_it = 0 - - ## Iterate the sections - while section_it < num_sections: - - ## click on a section - world.browser.find_by_css('#accordion > nav > div')[chapter_it].find_by_tag('li')[section_it].find_by_tag('a').click() - - ## sometimes the course-content takes a long time to load - assert world.is_css_present('.course-content') - - ## look for server error div - check_for_errors() - - ## count tabs from xml and page and compare - - ## count the number of tabs. If number of tabs is 0, there won't be anything rendered - ## so we explicitly set rendered_tabs because otherwise find_elements returns a None object with no length - num_tabs = sections[section_it]['clickable_tab_count'] - if num_tabs != 0: - rendered_tabs = world.browser.find_by_css('ol#sequence-list > li') - num_rendered_tabs = len(rendered_tabs) - else: - rendered_tabs = 0 - num_rendered_tabs = 0 - - msg = ('%d tabs expected, %d tabs found, %s - %d - %s' % - (num_tabs, num_rendered_tabs, course_id, section_it, sections[section_it]['section_name'])) - #logger.debug(msg) - - # Save the HTML to a file for later comparison - world.save_the_course_content('/tmp/%s' % course_id) - - assert num_tabs == num_rendered_tabs, msg - - tabs = sections[section_it]['tabs'] - tab_it = 0 - - ## Iterate the tabs - while tab_it < num_tabs: - - rendered_tabs[tab_it].find_by_tag('a').click() - - ## do something with the tab sections[section_it] - # e = world.browser.find_by_css('section.course-content section') - # process_section(e) - tab_children = tabs[tab_it]['children_count'] - tab_class = tabs[tab_it]['class'] - if tab_children != 0: - rendered_items = world.browser.find_by_css('div#seq_content > section > ol > li > section') - num_rendered_items = len(rendered_items) - msg = ('%d items expected, %d items found, %s - %d - %s - tab %d' % - (tab_children, num_rendered_items, course_id, section_it, sections[section_it]['section_name'], tab_it)) - #logger.debug(msg) - assert tab_children == num_rendered_items, msg - - tab_it += 1 - - section_it += 1 - - chapter_it += 1 - - -def validate_course(current_course, ids): - try: - ids.index(current_course) - except: - assert False, "invalid course id %s" % current_course From 3f9a72e6ce805a63d091cc387b44021d079d46c4 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Fri, 21 Jun 2013 16:32:13 -0400 Subject: [PATCH 137/995] Consolidated imports --- cms/djangoapps/contentstore/tests/test_contentstore.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 514b631521..66fead562e 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -23,7 +23,7 @@ from xmodule.modulestore import Location from xmodule.modulestore.store_utilities import clone_course from xmodule.modulestore.store_utilities import delete_course from xmodule.modulestore.django import modulestore -from xmodule.contentstore.django import contentstore +from xmodule.contentstore.django import contentstore, _CONTENTSTORE from xmodule.templates import update_templates from xmodule.modulestore.xml_exporter import export_to_xml from xmodule.modulestore.xml_importer import import_from_xml, perform_xlint @@ -41,7 +41,6 @@ from xmodule.exceptions import NotFoundError from django_comment_common.utils import are_permissions_roles_seeded from xmodule.exceptions import InvalidVersionError -import xmodule.contentstore.django import datetime from pytz import UTC from uuid import uuid4 @@ -92,7 +91,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): def tearDown(self): mongo = MongoClient() mongo.drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db']) - xmodule.contentstore.django._CONTENTSTORE.clear() + _CONTENTSTORE.clear() def check_components_on_page(self, component_types, expected_types): """ @@ -857,7 +856,7 @@ class ContentStoreTest(ModuleStoreTestCase): def tearDown(self): mongo = MongoClient() mongo.drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db']) - xmodule.contentstore.django._CONTENTSTORE.clear() + _CONTENTSTORE.clear() def test_create_course(self): """Test new course creation - happy path""" From e045860cb652686f8ab5bcaff659a636db6f4d32 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 20 Jun 2013 20:36:51 -0400 Subject: [PATCH 138/995] 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 139/995] 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 140/995] 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 141/995] 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 142/995] 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 143/995] 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 144/995] Files that may not exist need F0401 suppressed during import. --- cms/envs/dev.py | 2 +- lms/envs/dev.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cms/envs/dev.py b/cms/envs/dev.py index 07630bdf31..2dcb3640ca 100644 --- a/cms/envs/dev.py +++ b/cms/envs/dev.py @@ -181,6 +181,6 @@ if SEGMENT_IO_KEY: ##################################################################### # Lastly, see if the developer has any local overrides. try: - from .private import * + from .private import * # pylint: disable=F0401 except ImportError: pass diff --git a/lms/envs/dev.py b/lms/envs/dev.py index b1519b77bc..813f9cf32c 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -258,6 +258,6 @@ if SEGMENT_IO_LMS_KEY: ##################################################################### # Lastly, see if the developer has any local overrides. try: - from .private import * + from .private import * # pylint: disable=F0401 except ImportError: pass From fa9a8f4af09a27bd88aeea33a81ec0f5086d9363 Mon Sep 17 00:00:00 2001 From: Julian Arni Date: Fri, 21 Jun 2013 18:00:30 -0400 Subject: [PATCH 145/995] Greater dir naming flexibility. Accepts any dirname for the edx-platform repo. Allows the script to be called from any directory, not just $BASE/edx-platform. --- scripts/create-dev-env.sh | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/scripts/create-dev-env.sh b/scripts/create-dev-env.sh index edb0bcdcae..0816b72d21 100755 --- a/scripts/create-dev-env.sh +++ b/scripts/create-dev-env.sh @@ -98,19 +98,23 @@ clone_repos() { set_base_default() { # if PROJECT_HOME not set # 2 possibilities: this is from cloned repo, or not - # this script is in "./scripts" if a git clone - this_repo=$(cd "${BASH_SOURCE%/*}/.." && pwd) - if [[ "${this_repo##*/}" = "edx-platform" && -d "$this_repo/.git" ]]; then - # set BASE one-up from this_repo; - echo "${this_repo%/*}" + + # See if remote's url is named edx-platform (this works for forks too, but + # not if the name was changed). + cd "$( dirname "${BASH_SOURCE[0]}" )" + this_repo=$(basename $(git ls-remote --get-url 2>/dev/null) 2>/dev/null) || + echo -n "" + + if [[ "x$this_repo" = "xedx-platform.git" ]]; then + # We are in the edx repo and already have git installed. Let git do the + # work of finding base dir: + echo "$(dirname $(git rev-parse --show-toplevel))" else echo "$HOME/edx_all" fi } - - ### START PROG=${0##*/} From 2eefa494b145610eb8b9a2037ee3be7aa0daba51 Mon Sep 17 00:00:00 2001 From: Jason Bau Date: Fri, 21 Jun 2013 23:46:56 -0700 Subject: [PATCH 146/995] Width of labels for multiple-choice capa problems = width of text Stanford got some feedback from our students/faculty that students were making accidental clicks on radio-button capa questions. They would click way to the right of the label text, but it would still select the corresponding input, which caused some students to make unintentional changes to their answers. This was because labels for these inputs were display:block and width:100% Changing these labels to float:left clear:both should fix it. --- lms/static/sass/course/base/_base.scss | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lms/static/sass/course/base/_base.scss b/lms/static/sass/course/base/_base.scss index 6d87b7f554..a1c948d4f5 100644 --- a/lms/static/sass/course/base/_base.scss +++ b/lms/static/sass/course/base/_base.scss @@ -46,6 +46,13 @@ form { } } +form.choicegroup { + label { + clear: both; + float: left; + } +} + textarea, input[type="text"], input[type="email"], From 85b4a4ccab37e14e6f0543f8b7165e667c0768ef Mon Sep 17 00:00:00 2001 From: Alexander Kryklia Date: Sat, 22 Jun 2013 16:13:40 +0300 Subject: [PATCH 147/995] removes choiceresponse wiping after clicking Show Answer --- common/lib/xmodule/xmodule/js/src/capa/display.coffee | 3 --- 1 file changed, 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index 987d20b65a..f773fc81c4 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -364,8 +364,6 @@ class @Problem choicegroup: (element, display, answers) => element = $(element) - element.find('input').attr('disabled', 'disabled') - input_id = element.attr('id').replace(/inputtype_/,'') answer = answers[input_id] for choice in answer @@ -379,7 +377,6 @@ class @Problem inputtypeHideAnswerMethods: choicegroup: (element, display) => element = $(element) - element.find('input').attr('disabled', null) element.find('label').removeClass('choicegroup_correct') javascriptinput: (element, display) => From 4a98e2eda75b1a8b036e4f3f5e035c5049aab776 Mon Sep 17 00:00:00 2001 From: Jason Bau Date: Wed, 5 Jun 2013 23:14:18 -0700 Subject: [PATCH 148/995] Moves user activation away from just clicking on reset password To following the link in the password reset email --- common/djangoapps/student/forms.py | 72 +++++++++++++++++++ common/djangoapps/student/views.py | 31 ++++---- .../registration/password_reset_email.html | 2 +- lms/urls.py | 2 +- 4 files changed, 93 insertions(+), 14 deletions(-) create mode 100644 common/djangoapps/student/forms.py diff --git a/common/djangoapps/student/forms.py b/common/djangoapps/student/forms.py new file mode 100644 index 0000000000..75c89e0a26 --- /dev/null +++ b/common/djangoapps/student/forms.py @@ -0,0 +1,72 @@ +from django import forms +from django.utils.translation import ugettext, ugettext_lazy as _ +from django.template import loader +from django.contrib.auth.models import User +from django.contrib.auth.hashers import UNUSABLE_PASSWORD, is_password_usable, get_hasher +from django.contrib.auth.tokens import default_token_generator +from django.contrib.sites.models import get_current_site +from django.utils.http import int_to_base36 + + + +# This is a literal copy from Django 1.4.5's django.contrib.auth.forms.PasswordResetForm +# I think copy-and-paste here is somewhat better than subclassing and +# just changing the definition of clean_email, because it's less +# likely to be broken by incompatibility with a new django version. +# (If this form is good enough now, a snapshot of it ought to last a while) + +class PasswordResetFormNoActive(forms.Form): + error_messages = { + 'unknown': _("That e-mail address doesn't have an associated " + "user account. Are you sure you've registered?"), + 'unusable': _("The user account associated with this e-mail " + "address cannot reset the password."), + } + email = forms.EmailField(label=_("E-mail"), max_length=75) + + def clean_email(self): + """ + Validates that an active user exists with the given email address. + """ + email = self.cleaned_data["email"] + #The line below contains the only change, removing is_active=True + self.users_cache = User.objects.filter(email__iexact=email) + if not len(self.users_cache): + raise forms.ValidationError(self.error_messages['unknown']) + if any((user.password == UNUSABLE_PASSWORD) + for user in self.users_cache): + raise forms.ValidationError(self.error_messages['unusable']) + return email + + def save(self, domain_override=None, + subject_template_name='registration/password_reset_subject.txt', + email_template_name='registration/password_reset_email.html', + use_https=False, token_generator=default_token_generator, + from_email=None, request=None): + """ + Generates a one-use only link for resetting password and sends to the + user. + """ + from django.core.mail import send_mail + for user in self.users_cache: + if not domain_override: + current_site = get_current_site(request) + site_name = current_site.name + domain = current_site.domain + else: + site_name = domain = domain_override + c = { + 'email': user.email, + 'domain': domain, + 'site_name': site_name, + 'uid': int_to_base36(user.id), + 'user': user, + 'token': token_generator.make_token(user), + 'protocol': use_https and 'https' or 'http', + } + subject = loader.render_to_string(subject_template_name, c) + # Email subject *must not* contain newlines + subject = ''.join(subject.splitlines()) + email = loader.render_to_string(email_template_name, c) + send_mail(subject, email, from_email, [user.email]) + diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index e065333409..50f6d90368 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -11,9 +11,9 @@ import time from django.conf import settings from django.contrib.auth import logout, authenticate, login -from django.contrib.auth.forms import PasswordResetForm from django.contrib.auth.models import User from django.contrib.auth.decorators import login_required +from django.contrib.auth.views import password_reset_confirm from django.core.cache import cache from django.core.context_processors import csrf from django.core.mail import send_mail @@ -24,6 +24,7 @@ from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbid from django.shortcuts import redirect from django_future.csrf import ensure_csrf_cookie from django.utils.http import cookie_date +from django.utils.http import base36_to_int from mitxmako.shortcuts import render_to_response, render_to_string from bs4 import BeautifulSoup @@ -34,6 +35,8 @@ from student.models import (Registration, UserProfile, TestCenterUser, TestCente CourseEnrollment, unique_id_for_user, get_testcenter_registration, CourseEnrollmentAllowed) +from student.forms import PasswordResetFormNoActive + from certificates.models import CertificateStatuses, certificate_status_for_student from xmodule.course_module import CourseDescriptor @@ -962,17 +965,7 @@ def password_reset(request): if request.method != "POST": raise Http404 - # By default, Django doesn't allow Users with is_active = False to reset their passwords, - # but this bites people who signed up a long time ago, never activated, and forgot their - # password. So for their sake, we'll auto-activate a user for whom password_reset is called. - try: - user = User.objects.get(email=request.POST['email']) - user.is_active = True - user.save() - except: - log.exception("Tried to auto-activate user to enable password reset, but failed.") - - form = PasswordResetForm(request.POST) + form = PasswordResetFormNoActive(request.POST) if form.is_valid(): form.save(use_https=request.is_secure(), from_email=settings.DEFAULT_FROM_EMAIL, @@ -984,6 +977,20 @@ def password_reset(request): return HttpResponse(json.dumps({'success': False, 'error': 'Invalid e-mail'})) +def password_reset_confirm_wrapper(request, uidb36=None, token=None): + ''' A wrapper around django.contrib.auth.views.password_reset_confirm. + Needed because we want to set the user as active at this step. + ''' + #cribbed from django.contrib.auth.views.password_reset_confirm + try: + uid_int = base36_to_int(uidb36) + user = User.objects.get(id=uid_int) + user.is_active = True + user.save() + except (ValueError, User.DoesNotExist): + pass + return password_reset_confirm(request, uidb36=uidb36, token=token) + def reactivation_email_for_user(user): try: diff --git a/lms/templates/registration/password_reset_email.html b/lms/templates/registration/password_reset_email.html index bf6c3e0891..68073d9ddd 100644 --- a/lms/templates/registration/password_reset_email.html +++ b/lms/templates/registration/password_reset_email.html @@ -3,7 +3,7 @@ {% trans "Please go to the following page and choose a new password:" %} {% block reset_link %} -https://{{domain}}{% url 'django.contrib.auth.views.password_reset_confirm' uidb36=uid token=token %} +https://{{domain}}{% url 'student.views.password_reset_confirm_wrapper' uidb36=uid token=token %} {% endblock %} If you didn't request this change, you can disregard this email - we have not yet reset your password. diff --git a/lms/urls.py b/lms/urls.py index 52ce539f73..50ce35cde0 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -51,7 +51,7 @@ urlpatterns = ('', # nopep8 url(r'^password_change_done/$', django.contrib.auth.views.password_change_done, name='auth_password_change_done'), url(r'^password_reset_confirm/(?P[0-9A-Za-z]+)-(?P.+)/$', - django.contrib.auth.views.password_reset_confirm, + 'student.views.password_reset_confirm_wrapper', name='auth_password_reset_confirm'), url(r'^password_reset_complete/$', django.contrib.auth.views.password_reset_complete, name='auth_password_reset_complete'), From ad6e7457625965386931bb8d43836d3362034468 Mon Sep 17 00:00:00 2001 From: cahrens Date: Mon, 24 Jun 2013 09:09:05 -0400 Subject: [PATCH 149/995] 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 150/995] 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 634ad8937e2943d5eda6ba50600a5586902c3c10 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 23 May 2013 16:06:08 -0400 Subject: [PATCH 151/995] Tidy up argument parsing in release email script --- scripts/release-email-list.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/release-email-list.sh b/scripts/release-email-list.sh index 92f7a9aef4..64fa7c00d1 100755 --- a/scripts/release-email-list.sh +++ b/scripts/release-email-list.sh @@ -1,7 +1,6 @@ #! /bin/bash -LOG_SPEC="$1..$2" -LOG_CMD="git --no-pager log $LOG_SPEC" +LOG_CMD="git --no-pager log $1..$2" RESPONSIBLE=$(sort -u <($LOG_CMD --format='tformat:%ae' && $LOG_CMD --format='tformat:%ce')) From 3e376bd78031db5a87c890359f1fbc776030404e Mon Sep 17 00:00:00 2001 From: Peter Fogg Date: Mon, 24 Jun 2013 11:06:53 -0400 Subject: [PATCH 152/995] 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 32a0a2d29dbf60713d355fc51f9a6e296af878de Mon Sep 17 00:00:00 2001 From: Jean Manuel Nater Date: Mon, 24 Jun 2013 11:13:59 -0400 Subject: [PATCH 153/995] In the middle of addressing pull request comments. This is a safety commit in case I have to revert some changes I'm about to make. --- .../xmodule/modulestore/tests/django_utils.py | 8 ++- .../xmodule/modulestore/tests/factories.py | 8 +-- .../courseware/tests/check_request_code.py | 24 ------- lms/djangoapps/courseware/tests/helpers.py | 33 ++++----- .../courseware/tests/modulestore_config.py | 16 +++-- .../courseware/tests/test_navigation.py | 16 +++-- .../tests/test_view_authentication.py | 72 +++++++++++++------ 7 files changed, 98 insertions(+), 79 deletions(-) delete mode 100644 lms/djangoapps/courseware/tests/check_request_code.py diff --git a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py index 944b9e5bd4..a2306a5c6b 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py @@ -14,10 +14,16 @@ class ModuleStoreTestCase(TestCase): collection with templates before running the TestCase and drops it they are finished. """ - def update_course(self, course, data): + @staticmethod + def update_course(course, data): """ Updates the version of course in the mongo modulestore with the metadata in data and returns the updated version. + + 'course' is an instance of CourseDescriptor for which we want + to update metadata. + + 'data' is a dictionary with an entry for each CourseField we want to update. """ store = xmodule.modulestore.django.modulestore() diff --git a/common/lib/xmodule/xmodule/modulestore/tests/factories.py b/common/lib/xmodule/xmodule/modulestore/tests/factories.py index 4f63fbc1d2..82ff61204a 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/factories.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/factories.py @@ -60,8 +60,8 @@ class XModuleCourseFactory(Factory): if data is not None: store.update_item(new_course.location, data) - '''update_item updates the the course as it exists in the modulestore, but doesn't - update the instance we are working with, so have to refetch the course after updating it.''' + #update_item updates the the course as it exists in the modulestore, but doesn't + #update the instance we are working with, so have to refetch the course after updating it. new_course = store.get_instance(new_course.id, new_course.location) return new_course @@ -152,8 +152,8 @@ class XModuleItemFactory(Factory): if new_item.location.category not in DETACHED_CATEGORIES: store.update_children(parent_location, parent.children + [new_item.location.url()]) - '''update_children updates the the item as it exists in the modulestore, but doesn't - update the instance we are working with, so have to refetch the item after updating it.''' + #update_children updates the the item as it exists in the modulestore, but doesn't + #update the instance we are working with, so have to refetch the item after updating it. new_item = store.get_item(new_item.location) return new_item diff --git a/lms/djangoapps/courseware/tests/check_request_code.py b/lms/djangoapps/courseware/tests/check_request_code.py deleted file mode 100644 index 1393d2fe17..0000000000 --- a/lms/djangoapps/courseware/tests/check_request_code.py +++ /dev/null @@ -1,24 +0,0 @@ - - -def check_for_get_code(code, url): - """ - Check that we got the expected code when accessing url via GET. - Returns the response. - """ - resp = self.client.get(url) - self.assertEqual(resp.status_code, code, - "got code %d for url '%s'. Expected code %d" - % (resp.status_code, url, code)) - return resp - - -def check_for_post_code(code, url, data={}): - """ - Check that we got the expected code when accessing url via POST. - Returns the response. - """ - resp = self.client.post(url, data) - self.assertEqual(resp.status_code, code, - "got code %d for url '%s'. Expected code %d" - % (resp.status_code, url, code)) - return resp diff --git a/lms/djangoapps/courseware/tests/helpers.py b/lms/djangoapps/courseware/tests/helpers.py index 99da5e9061..ce0603990b 100644 --- a/lms/djangoapps/courseware/tests/helpers.py +++ b/lms/djangoapps/courseware/tests/helpers.py @@ -12,7 +12,12 @@ def check_for_get_code(self, code, url): """ Check that we got the expected code when accessing url via GET. Returns the HTTP response. - 'self' is a class that subclasses TestCase. + + `self` is a class that subclasses TestCase. + + `code` is a status code for HTTP responses. + + `url` is a url pattern for which we have to test the response. """ resp = self.client.get(url) self.assertEqual(resp.status_code, code, @@ -25,7 +30,11 @@ def check_for_post_code(self, code, url, data={}): """ Check that we got the expected code when accessing url via POST. Returns the HTTP response. - 'self' is a class that subclasses TestCase. + `self` is a class that subclasses TestCase. + + `code` is a status code for HTTP responses. + + `url` is a url pattern for which we want to test the response. """ resp = self.client.post(url, data) self.assertEqual(resp.status_code, code, @@ -62,8 +71,8 @@ class LoginEnrollmentTestCase(TestCase): def logout(self): """ - Logout, check that it worked. - Returns an HTTP response which e + Logout; check that the HTTP response code indicates redirection + as expected. """ resp = self.client.get(reverse('logout'), {}) # should redirect @@ -106,8 +115,8 @@ class LoginEnrollmentTestCase(TestCase): def enroll(self, course, verify=False): """ Try to enroll and return boolean indicating result. - 'course' is an instance of CourseDescriptor. - 'verify' is an optional parameter specifying whether we + `course` is an instance of CourseDescriptor. + `verify` is an optional parameter specifying whether we want to verify that the student was successfully enrolled in the course. """ @@ -122,20 +131,12 @@ class LoginEnrollmentTestCase(TestCase): self.assertTrue(result) return result - # def enroll(self, course): - # """ - # Enroll the currently logged-in user, and check that it worked. - # """ - - # result = self.try_enroll(course) - # self.assertTrue(result) - def unenroll(self, course): """ Unenroll the currently logged-in user, and check that it worked. - 'course' is an instance of CourseDescriptor. + `course` is an instance of CourseDescriptor. """ - resp = self.client.post('/change_enrollment', { + resp = self.client.post(reverse('change_enrollment'), { 'enrollment_action': 'unenroll', 'course_id': course.id, }) diff --git a/lms/djangoapps/courseware/tests/modulestore_config.py b/lms/djangoapps/courseware/tests/modulestore_config.py index 81d0f4f911..c3c4ce4e5b 100644 --- a/lms/djangoapps/courseware/tests/modulestore_config.py +++ b/lms/djangoapps/courseware/tests/modulestore_config.py @@ -4,11 +4,11 @@ from django.conf import settings def mongo_store_config(data_dir): - ''' - Defines default module store using MongoModuleStore + """ + Defines default module store using MongoModuleStore. - Use of this config requires mongo to be running - ''' + Use of this config requires mongo to be running. + """ store = { 'default': { 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', @@ -27,7 +27,9 @@ def mongo_store_config(data_dir): def draft_mongo_store_config(data_dir): - '''Defines default module store using DraftMongoModuleStore''' + """ + Defines default module store using DraftMongoModuleStore. + """ return { 'default': { 'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore', @@ -55,7 +57,9 @@ def draft_mongo_store_config(data_dir): def xml_store_config(data_dir): - '''Defines default module store using XMLModuleStore''' + """ + Defines default module store using XMLModuleStore. + """ return { 'default': { 'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore', diff --git a/lms/djangoapps/courseware/tests/test_navigation.py b/lms/djangoapps/courseware/tests/test_navigation.py index 9f9bf7ba92..f4662f2ef5 100644 --- a/lms/djangoapps/courseware/tests/test_navigation.py +++ b/lms/djangoapps/courseware/tests/test_navigation.py @@ -36,15 +36,18 @@ class TestNavigation(ModuleStoreTestCase, LoginEnrollmentTestCase): # Create student accounts and activate them. for i in range(len(self.STUDENT_INFO)): - self.create_account('u{0}'.format(i), self.STUDENT_INFO[i][0], self.STUDENT_INFO[i][1]) - self.activate_user(self.STUDENT_INFO[i][0]) + email, password = self.STUDENT_INFO[i] + username = 'u{0}'.format(i) + self.create_account(username, email, password) + self.activate_user(email) def test_redirects_first_time(self): """ Verify that the first time we click on the courseware tab we are redirected to the 'Welcome' section. """ - self.login(self.STUDENT_INFO[0][0], self.STUDENT_INFO[0][1]) + email, password = self.STUDENT_INFO[0] + self.login(email, password) self.enroll(self.course, True) self.enroll(self.full, True) @@ -61,7 +64,8 @@ class TestNavigation(ModuleStoreTestCase, LoginEnrollmentTestCase): Verify the accordion remembers we've already visited the Welcome section and redirects correpondingly. """ - self.login(self.STUDENT_INFO[0][0], self.STUDENT_INFO[0][1]) + email, password = self.STUDENT_INFO[0] + self.login(email, password) self.enroll(self.course, True) self.enroll(self.full, True) @@ -80,8 +84,8 @@ class TestNavigation(ModuleStoreTestCase, LoginEnrollmentTestCase): """ Verify the accordion remembers which chapter you were last viewing. """ - - self.login(self.STUDENT_INFO[0][0], self.STUDENT_INFO[0][1]) + email, password = self.STUDENT_INFO[0] + self.login(email, password) self.enroll(self.course, True) self.enroll(self.full, True) diff --git a/lms/djangoapps/courseware/tests/test_view_authentication.py b/lms/djangoapps/courseware/tests/test_view_authentication.py index ffae4688bf..5db9847d45 100644 --- a/lms/djangoapps/courseware/tests/test_view_authentication.py +++ b/lms/djangoapps/courseware/tests/test_view_authentication.py @@ -32,28 +32,40 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): @classmethod def _instructor_urls(self, course): """ - List of urls that only instructors/staff should be able to see. + `course` is an instance of CourseDescriptor whose section URLs are to be returned. + + Returns a list of URLs corresponding to sections in the passed in course. """ + urls = [reverse(name, kwargs={'course_id': course.id}) for name in ( 'instructor_dashboard', 'gradebook', 'grade_summary',)] + email, _ = self.ACCOUNT_INFO[0] + student_id = User.objects.get(email=email).id + urls.append(reverse('student_progress', kwargs={'course_id': course.id, - 'student_id': User.objects.get(email=self.ACCOUNT_INFO[0][0]).id})) + 'student_id': student_id})) return urls @staticmethod def _reverse_urls(names, course): """ Reverse a list of course urls. + + `names` is a list of URL names that correspond to sections in a course. + + `course` is the instance of CourseDescriptor whose section URLs are to be returned. + + Returns a list URLs corresponding to section in the passed in course. + """ return [reverse(name, kwargs={'course_id': course.id}) for name in names] def setUp(self): - xmodule.modulestore.django._MODULESTORES = {} self.full = CourseFactory.create(number='666', display_name='Robot_Sub_Course') self.course = CourseFactory.create() @@ -68,8 +80,9 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): # Create two accounts and activate them. for i in range(len(self.ACCOUNT_INFO)): - self.create_account('u{0}'.format(i), self.ACCOUNT_INFO[i][0], self.ACCOUNT_INFO[i][1]) - self.activate_user(self.ACCOUNT_INFO[i][0]) + username, email, password = 'u{0}'.format(i), self.ACCOUNT_INFO[i][0], self.ACCOUNT_INFO[i][1] + self.create_account(username, email, password) + self.activate_user(email) def test_redirection_unenrolled(self): """ @@ -77,7 +90,8 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): instead of the 'Welcome' section after clicking on the courseware tab. """ - self.login(self.ACCOUNT_INFO[0][0], self.ACCOUNT_INFO[0][1]) + email, password = self.ACCOUNT_INFO[0] + self.login(email, password) response = self.client.get(reverse('courseware', kwargs={'course_id': self.course.id})) self.assertRedirects(response, @@ -90,7 +104,8 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): the chapter after clicking on the courseware tab. """ - self.login(self.ACCOUNT_INFO[0][0], self.ACCOUNT_INFO[0][1]) + email, password = self.ACCOUNT_INFO[0] + self.login(email, password) self.enroll(self.course) response = self.client.get(reverse('courseware', @@ -108,7 +123,8 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): dashboard, the grade views, and student profile pages. """ - self.login(self.ACCOUNT_INFO[0][0], self.ACCOUNT_INFO[0][1]) + email, password = self.ACCOUNT_INFO[0] + self.login(email, password) self.enroll(self.course) self.enroll(self.full) @@ -127,12 +143,14 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): and student profile pages for their course. """ + email, password = self.ACCOUNT_INFO[1] + # Make the instructor staff in self.course group_name = _course_staff_group_name(self.course.location) group = Group.objects.create(name=group_name) - group.user_set.add(User.objects.get(email=self.ACCOUNT_INFO[1][0])) + group.user_set.add(User.objects.get(email=email)) - self.login(self.ACCOUNT_INFO[1][0], self.ACCOUNT_INFO[1][1]) + self.login(email, password) # Now should be able to get to self.course, but not self.full url = random.choice(self._instructor_urls(self.course)) @@ -149,10 +167,11 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): staff permissions. """ - self.login(self.ACCOUNT_INFO[1][0], self.ACCOUNT_INFO[1][1]) + email, password = self.ACCOUNT_INFO[1] + self.login(email, password) # now make the instructor also staff - instructor = User.objects.get(email=self.ACCOUNT_INFO[1][0]) + instructor = User.objects.get(email=email) instructor.is_staff = True instructor.save() @@ -202,6 +221,9 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): Actually do the test, relying on settings to be right. """ + student_email, student_password = self.ACCOUNT_INFO[0] + instructor_email, instructor_password = self.ACCOUNT_INFO[1] + # Make courses start in the future now = datetime.datetime.now(pytz.UTC) tomorrow = now + datetime.timedelta(days=1) @@ -300,7 +322,7 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): # First, try with an enrolled student print '=== Testing student access....' - self.login(self.ACCOUNT_INFO[0][0], self.ACCOUNT_INFO[0][1]) + self.login(student_email, student_password) self.enroll(self.course, True) self.enroll(self.full, True) @@ -314,10 +336,10 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): # Make the instructor staff in self.course group_name = _course_staff_group_name(self.course.location) group = Group.objects.create(name=group_name) - group.user_set.add(User.objects.get(email=self.ACCOUNT_INFO[1][0])) + group.user_set.add(User.objects.get(email=instructor_email)) self.logout() - self.login(self.ACCOUNT_INFO[1][0], self.ACCOUNT_INFO[1][1]) + self.login(instructor_email, instructor_password) # Enroll in the classes---can't see courseware otherwise. self.enroll(self.course, True) self.enroll(self.full, True) @@ -329,7 +351,7 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): print '=== Testing staff access....' # now also make the instructor staff - instructor = User.objects.get(email=self.ACCOUNT_INFO[1][0]) + instructor = User.objects.get(email=instructor_email) instructor.is_staff = True instructor.save() @@ -342,6 +364,9 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): Actually do the test, relying on settings to be right. """ + student_email, student_password = self.ACCOUNT_INFO[0] + instructor_email, instructor_password = self.ACCOUNT_INFO[1] + # Make courses start in the future now = datetime.datetime.now(pytz.UTC) tomorrow = now + datetime.timedelta(days=1) @@ -360,7 +385,7 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): print "login" # First, try with an enrolled student print '=== Testing student access....' - self.login(self.ACCOUNT_INFO[0][0], self.ACCOUNT_INFO[0][1]) + self.login(student_email, student_password) self.assertFalse(self.enroll(self.course)) self.assertTrue(self.enroll(self.full)) @@ -368,18 +393,18 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): # Make the instructor staff in the self.course group_name = _course_staff_group_name(self.course.location) group = Group.objects.create(name=group_name) - group.user_set.add(User.objects.get(email=self.ACCOUNT_INFO[1][0])) + group.user_set.add(User.objects.get(email=instructor_email)) print "logout/login" self.logout() - self.login(self.ACCOUNT_INFO[1][0], self.ACCOUNT_INFO[1][1]) + self.login(instructor_email, instructor_password) print "Instructor should be able to enroll in self.course" self.assertTrue(self.enroll(self.course)) print '=== Testing staff access....' # now make the instructor global staff, but not in the instructor group - group.user_set.remove(User.objects.get(email=self.ACCOUNT_INFO[1][0])) - instructor = User.objects.get(email=self.ACCOUNT_INFO[1][0]) + group.user_set.remove(User.objects.get(email=instructor_email)) + instructor = User.objects.get(email=instructor_email) instructor.is_staff = True instructor.save() @@ -392,6 +417,9 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): Actually test beta periods, relying on settings to be right. """ + student_email, student_password = self.ACCOUNT_INFO[0] + instructor_email, instructor_password = self.ACCOUNT_INFO[1] + # trust, but verify :) self.assertFalse(settings.MITX_FEATURES['DISABLE_START_DATES']) @@ -408,7 +436,7 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): self.course.lms.days_early_for_beta = 2 # student user shouldn't see it - student_user = User.objects.get(email=self.ACCOUNT_INFO[0][0]) + student_user = User.objects.get(email=student_email) self.assertFalse(has_access(student_user, self.course, 'load')) # now add the student to the beta test group From 72ffe2d8f243d00679e97b1385475e33a00e181b Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 24 Jun 2013 13:44:48 -0400 Subject: [PATCH 154/995] 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 155/995] 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 156/995] 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 157/995] Update CHANGELOG.rst --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d06cd89621..3dda49928b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,7 +5,7 @@ These are notable changes in edx-platform. This is a rolling list of changes, in roughly chronological order, most recent first. Add your entries at or near the top. Include a label indicating the component affected. -LMS: Small UX fix on capa multiple-choice problems. Make labels only +Blades: Small UX fix on capa multiple-choice problems. Make labels only as wide as the text to reduce accidental choice selections. Studio: Remove XML from the video component editor. All settings are From 83062c0b7dd6b85e6f50ad717ba796c92c5ecb8d Mon Sep 17 00:00:00 2001 From: Jason Bau Date: Mon, 24 Jun 2013 11:54:31 -0700 Subject: [PATCH 158/995] Tests + Now subclass PasswordResetForm instead of copy Changed to subclassing django's PasswordResetForm and overriding clean_password() instead of copy/paste. Less lines to worry about for diff-cover this way =) --- common/djangoapps/student/forms.py | 65 ++------------ common/djangoapps/student/tests/tests.py | 106 ++++++++++++++++++++++- common/djangoapps/student/views.py | 2 +- 3 files changed, 111 insertions(+), 62 deletions(-) diff --git a/common/djangoapps/student/forms.py b/common/djangoapps/student/forms.py index 75c89e0a26..1096092117 100644 --- a/common/djangoapps/student/forms.py +++ b/common/djangoapps/student/forms.py @@ -1,33 +1,15 @@ from django import forms -from django.utils.translation import ugettext, ugettext_lazy as _ -from django.template import loader from django.contrib.auth.models import User -from django.contrib.auth.hashers import UNUSABLE_PASSWORD, is_password_usable, get_hasher -from django.contrib.auth.tokens import default_token_generator -from django.contrib.sites.models import get_current_site -from django.utils.http import int_to_base36 +from django.contrib.auth.forms import PasswordResetForm +from django.contrib.auth.hashers import UNUSABLE_PASSWORD - - -# This is a literal copy from Django 1.4.5's django.contrib.auth.forms.PasswordResetForm -# I think copy-and-paste here is somewhat better than subclassing and -# just changing the definition of clean_email, because it's less -# likely to be broken by incompatibility with a new django version. -# (If this form is good enough now, a snapshot of it ought to last a while) - -class PasswordResetFormNoActive(forms.Form): - error_messages = { - 'unknown': _("That e-mail address doesn't have an associated " - "user account. Are you sure you've registered?"), - 'unusable': _("The user account associated with this e-mail " - "address cannot reset the password."), - } - email = forms.EmailField(label=_("E-mail"), max_length=75) - +class PasswordResetFormNoActive(PasswordResetForm): def clean_email(self): """ - Validates that an active user exists with the given email address. - """ + This is a literal copy from Django 1.4.5's django.contrib.auth.forms.PasswordResetForm + Except removing the requirement of active users + Validates that a user exists with the given email address. + """ email = self.cleaned_data["email"] #The line below contains the only change, removing is_active=True self.users_cache = User.objects.filter(email__iexact=email) @@ -37,36 +19,3 @@ class PasswordResetFormNoActive(forms.Form): for user in self.users_cache): raise forms.ValidationError(self.error_messages['unusable']) return email - - def save(self, domain_override=None, - subject_template_name='registration/password_reset_subject.txt', - email_template_name='registration/password_reset_email.html', - use_https=False, token_generator=default_token_generator, - from_email=None, request=None): - """ - Generates a one-use only link for resetting password and sends to the - user. - """ - from django.core.mail import send_mail - for user in self.users_cache: - if not domain_override: - current_site = get_current_site(request) - site_name = current_site.name - domain = current_site.domain - else: - site_name = domain = domain_override - c = { - 'email': user.email, - 'domain': domain, - 'site_name': site_name, - 'uid': int_to_base36(user.id), - 'user': user, - 'token': token_generator.make_token(user), - 'protocol': use_https and 'https' or 'http', - } - subject = loader.render_to_string(subject_template_name, c) - # Email subject *must not* contain newlines - subject = ''.join(subject.splitlines()) - email = loader.render_to_string(email_template_name, c) - send_mail(subject, email, from_email, [user.email]) - diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py index 4638da44b2..10836122b8 100644 --- a/common/djangoapps/student/tests/tests.py +++ b/common/djangoapps/student/tests/tests.py @@ -5,18 +5,118 @@ when you run "manage.py test". Replace this with more appropriate tests for your application. """ import logging +import json +import re +import unittest +from django import forms +from django.conf import settings from django.test import TestCase -from mock import Mock +from django.test.client import RequestFactory +from django.contrib.auth.models import User +from django.contrib.auth.hashers import UNUSABLE_PASSWORD +from django.template.loader import render_to_string, get_template, TemplateDoesNotExist +from django.core.urlresolvers import is_valid_path + +from mock import Mock, patch +from textwrap import dedent from student.models import unique_id_for_user -from student.views import process_survey_link, _cert_info - +from student.views import process_survey_link, _cert_info, password_reset, password_reset_confirm_wrapper +from student.tests.factories import UserFactory +from student.tests.test_email import mock_render_to_string COURSE_1 = 'edX/toy/2012_Fall' COURSE_2 = 'edx/full/6.002_Spring_2012' log = logging.getLogger(__name__) +try: + get_template('registration/password_reset_email.html') + project_uses_password_reset = True +except TemplateDoesNotExist: + project_uses_password_reset = False + + +class ResetPasswordTests(TestCase): + """ Tests that clicking reset password sends email, and doesn't activate the user + """ + request_factory = RequestFactory() + + def setUp(self): + self.user = UserFactory.create() + self.user.is_active = False + self.user.save() + + self.user_bad_passwd = UserFactory.create() + self.user_bad_passwd.is_active = False + self.user_bad_passwd.password = UNUSABLE_PASSWORD + self.user_bad_passwd.save() + + + @unittest.skipUnless(project_uses_password_reset, dedent("""Skipping Test because CMS has not provided + necessary templates for password reset. If this message is in LMS tests, that is a bug and needs to be fixed.""")) + @patch('student.views.password_reset_confirm') + @patch('django.core.mail.send_mail') + @patch('student.views.render_to_string', Mock(side_effect=mock_render_to_string, autospec=True)) + def test_reset_password_email(self, send_email, reset_confirm): + """Tests sending of reset password email""" + + #First test the bad password user, mainly for diff-cover sake + bad_pwd_req = self.request_factory.post('/password_reset/', {'email': self.user_bad_passwd.email}) + bad_pwd_resp = password_reset(bad_pwd_req) + self.assertEquals(bad_pwd_resp.status_code, 200) + self.assertEquals(bad_pwd_resp.content, json.dumps({'success': False, + 'error': 'Invalid e-mail or user'})) + + #Now test the exception cases with invalid email. + bad_email_req = self.request_factory.post('/password_reset/', {'email': self.user.email+"makeItFail"}) + bad_email_resp = password_reset(bad_email_req) + self.assertEquals(bad_email_resp.status_code, 200) + self.assertEquals(bad_email_resp.content, json.dumps({'success': False, + 'error': 'Invalid e-mail or user'})) + + #Now test the legit case where email should have been sent + good_req = self.request_factory.post('/password_reset/', {'email': self.user.email}) + good_resp = password_reset(good_req) + self.assertEquals(good_resp.status_code, 200) + self.assertEquals(good_resp.content, + json.dumps({'success': True, + 'value': "('registration/password_reset_done.html', [])"})) + + ((subject, msg, from_addr, to_addrs), sm_kwargs) = send_email.call_args + self.assertIn("Password reset", subject) + self.assertIn("You're receiving this e-mail because you requested a password reset", msg) + self.assertEquals(from_addr, settings.DEFAULT_FROM_EMAIL) + self.assertEquals(len(to_addrs), 1) + self.assertIn(self.user.email, to_addrs) + + #test that the user is not active + #it's a bit unsettling that we have to reload the user from the db for this test to work + #but I guess the user is cached here in the instance of ResetPasswordTests + #so the update in the view does not know to update this class. + self.user = User.objects.get(pk=self.user.pk) + self.assertFalse(self.user.is_active) + + #now try to activate the user in the password reset phase + bad_reset_req = self.request_factory.get('/password_reset_confirm/NO-OP/') + bad_reset_resp = password_reset_confirm_wrapper(bad_reset_req, 'NO', 'OP') + (confirm_args, confirm_kwargs) = reset_confirm.call_args + self.assertEquals(confirm_kwargs['uidb36'], 'NO') + self.assertEquals(confirm_kwargs['token'], 'OP') + self.user = User.objects.get(pk=self.user.pk) + self.assertFalse(self.user.is_active) + + reset_match = re.search(r'password_reset_confirm/(?P[0-9A-Za-z]+)-(?P.+)/', msg).groupdict() + good_reset_req = self.request_factory.get('/password_reset_confirm/{0}-{1}/'.format(reset_match['uidb36'], + reset_match['token'])) + good_reset_resp = password_reset_confirm_wrapper(good_reset_req, reset_match['uidb36'], reset_match['token']) + (confirm_args, confirm_kwargs) = reset_confirm.call_args + self.assertEquals(confirm_kwargs['uidb36'], reset_match['uidb36']) + self.assertEquals(confirm_kwargs['token'], reset_match['token']) + self.user = User.objects.get(pk=self.user.pk) + self.assertTrue(self.user.is_active) + + class CourseEndingTest(TestCase): """Test things related to course endings: certificates, surveys, etc""" diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 50f6d90368..7ae460b438 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -975,7 +975,7 @@ def password_reset(request): 'value': render_to_string('registration/password_reset_done.html', {})})) else: return HttpResponse(json.dumps({'success': False, - 'error': 'Invalid e-mail'})) + 'error': 'Invalid e-mail or user'})) def password_reset_confirm_wrapper(request, uidb36=None, token=None): ''' A wrapper around django.contrib.auth.views.password_reset_confirm. From e44ef1a54ebff3d61231ad7cc2e7ccbc5faf5933 Mon Sep 17 00:00:00 2001 From: Jean Manuel Nater Date: Mon, 24 Jun 2013 16:24:09 -0400 Subject: [PATCH 159/995] Removed the use of random.choice() --- .../tests/test_view_authentication.py | 268 ++++++++++-------- 1 file changed, 153 insertions(+), 115 deletions(-) diff --git a/lms/djangoapps/courseware/tests/test_view_authentication.py b/lms/djangoapps/courseware/tests/test_view_authentication.py index 5db9847d45..8e03e2563b 100644 --- a/lms/djangoapps/courseware/tests/test_view_authentication.py +++ b/lms/djangoapps/courseware/tests/test_view_authentication.py @@ -1,8 +1,7 @@ import datetime import pytz -import random -import xmodule.modulestore.django +from mock import patch from django.contrib.auth.models import User, Group from django.conf import settings @@ -65,6 +64,99 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): return [reverse(name, kwargs={'course_id': course.id}) for name in names] + def _dark_student_urls(self, course): + """ + List of urls that students should be able to see only + after launch, but staff should see before + """ + urls = self._reverse_urls(['info', 'progress'], course) + urls.extend([ + reverse('book', kwargs={'course_id': course.id, + 'book_index': index}) + for index, book in enumerate(course.textbooks) + ]) + return urls + + def _light_student_urls(self, course): + """ + List of urls that students should be able to see before + launch. + """ + urls = self._reverse_urls(['about_course'], course) + urls.append(reverse('courses')) + + return urls + + def instructor_urls(self, course): + """ + List of urls that only instructors/staff should be able to see. + """ + urls = self._reverse_urls(['instructor_dashboard', + 'gradebook', 'grade_summary'], course) + return urls + + def _check_non_staff_light(self, course): + """ + Check that non-staff have access to light urls. + + `course` is an instance of CourseDescriptor. + """ + print '=== Checking non-staff access for {0}'.format(course.id) + urls = [reverse('about_course', kwargs={'course_id': course.id}), reverse('courses')] + for url in urls: + print 'checking for 200 on {0}'.format(url) + check_for_get_code(self, 200, url) + + def _check_non_staff_dark(self, course): + """ + Check that non-staff don't have access to dark urls. + """ + print '=== Checking non-staff access for {0}'.format(course.id) + + names = ['courseware', 'instructor_dashboard', 'progress'] + urls = self._reverse_urls(names, course) + urls.extend([ + reverse('book', kwargs={'course_id': course.id, + 'book_index': index}) + for index, book in enumerate(course.textbooks) + ]) + for url in urls: + print 'checking for 404 on {0}'.format(url) + check_for_get_code(self, 404, url) + + def _check_staff(self, course): + """ + Check that access is right for staff in course. + """ + print '=== Checking staff access for {0}'.format(course.id) + + names = ['about_course', 'instructor_dashboard', 'progress'] + urls = self._reverse_urls(names, course) + urls.extend([ + reverse('book', kwargs={'course_id': course.id, + 'book_index': index}) + for index, book in enumerate(course.textbooks) + ]) + for url in urls: + print 'checking for 200 on {0}'.format(url) + check_for_get_code(self, 200, url) + + # The student progress tab is not accessible to a student + # before launch, so the instructor view-as-student feature + # should return a 404 as well. + # TODO (vshnayder): If this is not the behavior we want, will need + # to make access checking smarter and understand both the effective + # user (the student), and the requesting user (the prof) + url = reverse('student_progress', + kwargs={'course_id': course.id, + 'student_id': User.objects.get(email=self.ACCOUNT_INFO[0][0]).id}) + print 'checking for 404 on view-as-student: {0}'.format(url) + check_for_get_code(self, 404, url) + + # The courseware url should redirect, not 200 + url = self._reverse_urls(['courseware'], course)[0] + check_for_get_code(self, 302, url) + def setUp(self): self.full = CourseFactory.create(number='666', display_name='Robot_Sub_Course') @@ -129,13 +221,13 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): self.enroll(self.course) self.enroll(self.full) - # Randomly sample an instructor page - url = random.choice(self._instructor_urls(self.course) + - self._instructor_urls(self.full)) + urls = [reverse('instructor_dashboard', kwargs={'course_id': self.course.id}), + reverse('instructor_dashboard', kwargs={'course_id': self.full.id})] # Shouldn't be able to get to the instructor pages - print 'checking for 404 on {0}'.format(url) - check_for_get_code(self, 404, url) + for url in urls: + print 'checking for 404 on {0}'.format(url) + check_for_get_code(self, 404, url) def test_instructor_course_access(self): """ @@ -153,11 +245,11 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): self.login(email, password) # Now should be able to get to self.course, but not self.full - url = random.choice(self._instructor_urls(self.course)) + url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id}) print 'checking for 200 on {0}'.format(url) check_for_get_code(self, 200, url) - url = random.choice(self._instructor_urls(self.full)) + url = reverse('instructor_dashboard', kwargs={'course_id': self.full.id}) print 'checking for 404 on {0}'.format(url) check_for_get_code(self, 404, url) @@ -176,11 +268,12 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): instructor.save() # and now should be able to load both - url = random.choice(self._instructor_urls(self.course) + - self._instructor_urls(self.full)) + urls = [reverse('instructor_dashboard', kwargs={'course_id': self.course.id}), + reverse('instructor_dashboard', kwargs={'course_id': self.full.id})] - print 'checking for 200 on {0}'.format(url) - check_for_get_code(self, 200, url) + for url in urls: + print 'checking for 200 on {0}'.format(url) + check_for_get_code(self, 200, url) def run_wrapped(self, test): """ @@ -202,7 +295,9 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): Make sure that before course start, students can't access course pages, but instructors can. """ - self.run_wrapped(self._do_test_dark_launch) + self.run_wrapped(self._do_test_dark_launch_enrolled_student) + self.run_wrapped(self._do_test_dark_launch_instructor) + self.run_wrapped(self._do_test_dark_launch_staff) def test_enrollment_period(self): """ @@ -210,19 +305,18 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): """ self.run_wrapped(self._do_test_enrollment_period) - def test_beta_period(self): - """ - Check that beta-test access works. - """ - self.run_wrapped(self._do_test_beta_period) + # def test_beta_period(self): + # """ + # Check that beta-test access works. + # """ + # self.run_wrapped(self._do_test_beta_period) - def _do_test_dark_launch(self): + def _do_test_dark_launch_enrolled_student(self): """ Actually do the test, relying on settings to be right. """ student_email, student_password = self.ACCOUNT_INFO[0] - instructor_email, instructor_password = self.ACCOUNT_INFO[1] # Make courses start in the future now = datetime.datetime.now(pytz.UTC) @@ -236,90 +330,6 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): self.assertFalse(self.full.has_started()) self.assertFalse(settings.MITX_FEATURES['DISABLE_START_DATES']) - def dark_student_urls(course): - """ - List of urls that students should be able to see only - after launch, but staff should see before - """ - urls = self._reverse_urls(['info', 'progress'], course) - urls.extend([ - reverse('book', kwargs={'course_id': course.id, - 'book_index': index}) - for index, book in enumerate(course.textbooks) - ]) - return urls - - def light_student_urls(course): - """ - List of urls that students should be able to see before - launch. - """ - urls = self._reverse_urls(['about_course'], course) - urls.append(reverse('courses')) - - return urls - - def instructor_urls(course): - """ - List of urls that only instructors/staff should be able to see. - """ - urls = self._reverse_urls(['instructor_dashboard', - 'gradebook', 'grade_summary'], course) - return urls - - def check_non_staff_light(course): - """ - Check that non-staff have access to light urls. - """ - print '=== Checking non-staff access for {0}'.format(course.id) - - # Randomly sample a light url - url = random.choice(light_student_urls(course)) - print 'checking for 200 on {0}'.format(url) - check_for_get_code(self, 200, url) - - def check_non_staff_dark(course): - """ - Check that non-staff don't have access to dark urls. - """ - print '=== Checking non-staff access for {0}'.format(course.id) - - # Randomly sample a dark url - url = random.choice(instructor_urls(course) + - dark_student_urls(course) + - self._reverse_urls(['courseware'], course)) - print 'checking for 404 on {0}'.format(url) - check_for_get_code(self, 404, url) - - def check_staff(course): - """ - Check that access is right for staff in course. - """ - print '=== Checking staff access for {0}'.format(course.id) - - # Randomly sample a url - url = random.choice(instructor_urls(course) + - dark_student_urls(course) + - light_student_urls(course)) - print 'checking for 200 on {0}'.format(url) - check_for_get_code(self, 200, url) - - # The student progress tab is not accessible to a student - # before launch, so the instructor view-as-student feature - # should return a 404 as well. - # TODO (vshnayder): If this is not the behavior we want, will need - # to make access checking smarter and understand both the effective - # user (the student), and the requesting user (the prof) - url = reverse('student_progress', - kwargs={'course_id': course.id, - 'student_id': User.objects.get(email=self.ACCOUNT_INFO[0][0]).id}) - print 'checking for 404 on view-as-student: {0}'.format(url) - check_for_get_code(self, 404, url) - - # The courseware url should redirect, not 200 - url = self._reverse_urls(['courseware'], course)[0] - check_for_get_code(self, 302, url) - # First, try with an enrolled student print '=== Testing student access....' self.login(student_email, student_password) @@ -327,17 +337,27 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): self.enroll(self.full, True) # shouldn't be able to get to anything except the light pages - check_non_staff_light(self.course) - check_non_staff_dark(self.course) - check_non_staff_light(self.full) - check_non_staff_dark(self.full) + self._check_non_staff_light(self.course) + self._check_non_staff_dark(self.course) + self._check_non_staff_light(self.full) + self._check_non_staff_dark(self.full) + + def _do_test_dark_launch_instructor(self): + + instructor_email, instructor_password = self.ACCOUNT_INFO[1] + + now = datetime.datetime.now(pytz.UTC) + tomorrow = now + datetime.timedelta(days=1) + course_data = {'start': tomorrow} + full_data = {'start': tomorrow} + self.course = self.update_course(self.course, course_data) + self.full = self.update_course(self.full, full_data) print '=== Testing course instructor access....' # Make the instructor staff in self.course group_name = _course_staff_group_name(self.course.location) group = Group.objects.create(name=group_name) group.user_set.add(User.objects.get(email=instructor_email)) - self.logout() self.login(instructor_email, instructor_password) # Enroll in the classes---can't see courseware otherwise. @@ -345,9 +365,24 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): self.enroll(self.full, True) # should now be able to get to everything for self.course - check_non_staff_light(self.full) - check_non_staff_dark(self.full) - check_staff(self.course) + self._check_non_staff_light(self.full) + self._check_non_staff_dark(self.full) + self._check_staff(self.course) + + def _do_test_dark_launch_staff(self): + + instructor_email, instructor_password = self.ACCOUNT_INFO[1] + + now = datetime.datetime.now(pytz.UTC) + tomorrow = now + datetime.timedelta(days=1) + course_data = {'start': tomorrow} + full_data = {'start': tomorrow} + self.course = self.update_course(self.course, course_data) + self.full = self.update_course(self.full, full_data) + + self.login(instructor_email, instructor_password) + self.enroll(self.course, True) + self.enroll(self.full, True) print '=== Testing staff access....' # now also make the instructor staff @@ -356,8 +391,8 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): instructor.save() # and now should be able to load both - check_staff(self.course) - check_staff(self.full) + self._check_staff(self.course) + self._check_staff(self.full) def _do_test_enrollment_period(self): """ @@ -412,6 +447,9 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): self.unenroll(self.course) self.assertTrue(self.enroll(self.course)) + #from courseware.access import MITX_FEATURES + + #@patch.dict(MITX_FEATURES, {'DISABLE_START_DATES': True}) def _do_test_beta_period(self): """ Actually test beta periods, relying on settings to be right. From 3a8f591fe5280146b66918e55daabd674999b507 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Mon, 24 Jun 2013 16:36:49 -0400 Subject: [PATCH 160/995] Add tests for the diff coverage; fix one hidden unicode bug --- common/lib/xmodule/xmodule/capa_module.py | 2 +- .../xmodule/xmodule/tests/test_capa_module.py | 85 ++++++++++++++++++- 2 files changed, 83 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index d740a73946..bb06912f7a 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -412,7 +412,7 @@ class CapaModule(CapaFields, XModule): `err` is the Exception encountered while rendering the problem HTML. """ - log.exception(err) + log.exception(err.message) # TODO (vshnayder): another switch on DEBUG. if self.system.DEBUG: diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py index c6ffd32e89..1e84174291 100644 --- a/common/lib/xmodule/xmodule/tests/test_capa_module.py +++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py @@ -11,11 +11,12 @@ import datetime from mock import Mock, patch import unittest import random +import json import xmodule -from capa.responsetypes import StudentInputError, \ - LoncapaProblemError, ResponseError -from xmodule.capa_module import CapaModule +from capa.responsetypes import (StudentInputError, LoncapaProblemError, + ResponseError) +from xmodule.capa_module import CapaModule, ComplexEncoder from xmodule.modulestore import Location from django.http import QueryDict @@ -530,6 +531,32 @@ class CapaModuleTest(unittest.TestCase): # Expect that the number of attempts is NOT incremented self.assertEqual(module.attempts, 1) + def test_check_problem_other_errors(self): + """ + Test that errors other than the expected kinds give an appropriate message. + + See also `test_check_problem_error` for the "expected kinds" or errors. + """ + # Create the module + module = CapaFactory.create(attempts=1) + + # Ensure that the user is NOT staff + module.system.user_is_staff = False + + # Ensure that DEBUG is on + module.system.DEBUG = True + + # Simulate answering a problem that raises the exception + with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade: + error_msg = u"Superterrible error happened: ☠" + mock_grade.side_effect = Exception(error_msg) + + get_request_dict = {CapaFactory.input_key(): '3.14'} + result = module.check_problem(get_request_dict) + + # Expect an AJAX alert message in 'success' + self.assertTrue(error_msg in result['success']) + def test_check_problem_error_nonascii(self): # Try each exception that capa_module should handle @@ -1059,6 +1086,33 @@ class CapaModuleTest(unittest.TestCase): # Expect that the module has created a new dummy problem with the error self.assertNotEqual(original_problem, module.lcp) + def test_get_problem_html_error_w_debug(self): + """ + Test the html response when an error occurs with DEBUG on + """ + module = CapaFactory.create() + + # Simulate throwing an exception when the capa problem + # is asked to render itself as HTML + error_msg = u"Superterrible error happened: ☠" + module.lcp.get_html = Mock(side_effect=Exception(error_msg)) + + # Stub out the get_test_system rendering function + module.system.render_template = Mock(return_value="
      Test Template HTML
      ") + + # Make sure DEBUG is on + module.system.DEBUG = True + + # Try to render the module with DEBUG turned on + html = module.get_problem_html() + + self.assertTrue(html is not None) + + # Check the rendering context + render_args, _ = module.system.render_template.call_args + context = render_args[1] + self.assertTrue(error_msg in context['problem']['html']) + def test_random_seed_no_change(self): # Run the test for each possible rerandomize value @@ -1164,3 +1218,28 @@ class CapaModuleTest(unittest.TestCase): for i in range(200): module = CapaFactory.create(rerandomize=rerandomize) assert 0 <= module.seed < 1000 + + @patch('xmodule.capa_module.log') + @patch('xmodule.capa_module.Progress') + def test_get_progress_error(self, mock_progress, mock_log): + """ + Check that an exception given in `Progress` produces a `log.exception` call. + """ + error_types = [TypeError, ValueError] + for error_type in error_types: + mock_progress.side_effect = error_type + module = CapaFactory.create() + self.assertIsNone(module.get_progress()) + mock_log.exception.assert_called_once_with('Got bad progress') + mock_log.reset_mock() + + +class ComplexEncoderTest(unittest.TestCase): + def test_default(self): + """ + Check that complex numbers can be encoded into JSON. + """ + complex_num = 1 - 1j + expected_str = '1-1*j' + json_str = json.dumps(complex_num, cls=ComplexEncoder) + self.assertEqual(expected_str, json_str[1:-1]) # ignore quotes From 74bb976ef50cf0767e5f61abd056f3ea3a65b619 Mon Sep 17 00:00:00 2001 From: Julian Arni Date: Mon, 24 Jun 2013 13:32:30 -0400 Subject: [PATCH 161/995] Abort submission and alter user if gradefn throws an exception --- .../xmodule/js/src/capa/display.coffee | 31 ++++++-- common/static/js/capa/jsinput.js | 76 +++++++++++-------- doc/public/course_data_formats/jsinput.rst | 5 ++ 3 files changed, 77 insertions(+), 35 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index 69e4551b6e..fc4c750b52 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -129,6 +129,30 @@ class @Problem if setupMethod? @inputtypeDisplays[id] = setupMethod(inputtype) + # If some function wants to be called before sending the answer to the + # server, give it a chance to do so. + # + # check_waitfor allows the callee to send alerts if the user's input is + # invalid. To do so, the callee must throw an exception named "Waitfor + # Exception". This and any other errors or exceptions that arise from the + # callee are rethrown and abort the submission. + # + # In order to use this feature, add a 'data-waitfor' attribute to the input, + # and specify the function to be called by the check button before sending + # off @answers + check_waitfor: => + for inp in @inputs + if not ($(inp).attr("data-waitfor")?) + try + $(inp).data("waitfor")() + catch e + if e.name == "Waitfor Exception" + alert e.message + else + alert "Could not grade your answer. The submission was aborted." + throw e + @refreshAnswers() + ### # 'check_fd' uses FormData to allow file submissions in the 'problem_check' dispatch, @@ -140,11 +164,7 @@ class @Problem check_fd: => Logger.log 'problem_check', @answers - # If some function wants to be called before sending the answer to the - # server, give it a chance to do so. - if $('input[waitfor]').length != 0 - ($(lcall).data("waitfor").call() for lcall in $('input[waitfor]')) - @refreshAnswers() + # If there are no file inputs in the problem, we can fall back on @check if $('input:file').length == 0 @check() @@ -217,6 +237,7 @@ class @Problem $.ajaxWithPrefix("#{@url}/problem_check", settings) check: => + @check_waitfor() Logger.log 'problem_check', @answers $.postWithPrefix "#{@url}/problem_check", @answers, (response) => switch response.success diff --git a/common/static/js/capa/jsinput.js b/common/static/js/capa/jsinput.js index ff6a8aa68b..5eb1d3e360 100644 --- a/common/static/js/capa/jsinput.js +++ b/common/static/js/capa/jsinput.js @@ -10,16 +10,15 @@ // Use this array to keep track of the elements that have already been // initialized. jsinput.jsinputarr = jsinput.jsinputarr || []; - if (isFirst) { - jsinput.jsinputarr.exists = function (id) { - this.filter(function(e, i, a) { - return e.id = id; - }); - }; - } + jsinput.jsinputarr.exists = function (id) { + this.filter(function(e, i, a) { + return e.id = id; + }); + }; + function jsinputConstructor(spec) { - // Define an class that will be instantiated for each.jsinput element + // Define an class that will be instantiated for each jsinput element // of the DOM // 'that' is the object returned by the constructor. It has a single @@ -35,6 +34,11 @@ return parent.find('input[id^="input_"]'); } + // For the state and grade functions below, use functions instead of + // storing their return values since we might need to call them + // repeatedly, and they might change (e.g., they might not be defined + // when we first try calling them). + // Get the grade function name function getgradefn() { return $(sect).attr("data"); @@ -54,24 +58,24 @@ return $(sect).attr("data-stored"); } + var thisIFrame = $(spec.elem). + find('iframe[name^="iframe_"]'). + get(0); + + var cWindow = thisIFrame.contentWindow; + // Put the return value of gradefn in the hidden inputfield. // If passed an argument, does not call gradefn, and instead directly // updates the inputfield with the passed value. var update = function (answer) { var ans; - ans = $(spec.elem). - find('iframe[name^="iframe_"]'). - get(0). // jquery might not be available in the iframe - contentWindow[gradefn](); + ans = cWindow[gradefn](); // setstate presumes getstate, so don't getstate unless setstate is // defined. if (getgetstate() && getsetstate()) { var state, store; - state = $(spec.elem). - find('iframe[name^="iframe_"]'). - get(0). - contentWindow[getgetstate()](); + state = cWindow[getgetstate()](); store = { answer: ans, state: state @@ -91,8 +95,6 @@ $(updatebutton).click(update); } - - /* Public methods */ that.update = update; @@ -116,19 +118,30 @@ updateHandler(); bindCheck(); // Check whether application takes in state and there is a saved - // state to give it + // state to give it. If getsetstate is specified but calling it + // fails, wait and try again, since the iframe might still be + // loading. if (getsetstate() && getstoredstate()) { - console.log("Using stored state..."); var sval; if (typeof(getstoredstate()) === "object") { sval = getstoredstate()["state"]; } else { sval = getstoredstate(); } - $(spec.elem). - find('iframe[name^="iframe_"]'). - get(0). - contentWindow[getsetstate()](sval); + function whileloop(n) { + if (n < 10){ + try { + cWindow[getsetstate()](sval); + } catch (err) { + setTimeout(whileloop(n+1), 200); + } + } + else { + console.log("Error: could not set state"); + } + } + whileloop(0); + } } else { // NOT CURRENTLY SUPPORTED @@ -171,11 +184,13 @@ all.each(function() { // Get just the mako variable 'id' from the id attribute newid = $(this).attr("id").replace(/^inputtype_/, ""); - var newJsElem = jsinputConstructor({ - id: newid, - elem: this, - passive: false - }); + if (! jsinput.jsinputarr.exists(newid)){ + var newJsElem = jsinputConstructor({ + id: newid, + elem: this, + passive: false + }); + } }); } @@ -193,5 +208,6 @@ //} //}; - setTimeout(walkDOM, 200); + + setTimeout(walkDOM, 100); })(window.jsinput = window.jsinput || {}) diff --git a/doc/public/course_data_formats/jsinput.rst b/doc/public/course_data_formats/jsinput.rst index 5252a5dd0c..008940e3b7 100644 --- a/doc/public/course_data_formats/jsinput.rst +++ b/doc/public/course_data_formats/jsinput.rst @@ -87,6 +87,11 @@ attributes are also used) be passed as a string to the enclosing response type. In the customresponse example above, this means cfn will be passed this answer as `ans`. +If the `gradefn` function throws an exception when a student attempts to +submit a problem, the submission is aborted, and the student receives a generic +alert. The alert can be customised by making the exception name `Waitfor +Exception`; in that case, the alert message will be the exception message. + **IMPORTANT** : the `gradefn` function should not be at all asynchronous, since this could result in the student's latest answer not being passed correctly. Moreover, the function should also return promptly, since currently the student From d5d495c24d622ca9e1b8b6d0b30164df024230a3 Mon Sep 17 00:00:00 2001 From: Peter Fogg Date: Mon, 24 Jun 2013 16:57:14 -0400 Subject: [PATCH 162/995] Fix acceptance tests expecting outdated CSS. --- cms/djangoapps/contentstore/features/advanced-settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/features/advanced-settings.py b/cms/djangoapps/contentstore/features/advanced-settings.py index 2360baea5a..1661e1c391 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.py +++ b/cms/djangoapps/contentstore/features/advanced-settings.py @@ -27,7 +27,7 @@ def i_am_on_advanced_course_settings(step): @step(u'I press the "([^"]*)" notification button$') def press_the_notification_button(step, name): - css = 'a.%s-button' % name.lower() + css = 'a.action-%s' % name.lower() # Save was clicked if either the save notification bar is gone, or we have a error notification # overlaying it (expected in the case of typing Object into display_name). From 986b63d85d9fda2135d239ad2caf84365b2f80d3 Mon Sep 17 00:00:00 2001 From: Jean Manuel Nater Date: Mon, 24 Jun 2013 17:07:43 -0400 Subject: [PATCH 163/995] Removed run_wrapped() and replaced its functionality with patch.dict(): --- lms/djangoapps/courseware/tests/helpers.py | 4 ++ .../courseware/tests/test_navigation.py | 1 - .../tests/test_view_authentication.py | 64 ++++++++----------- 3 files changed, 29 insertions(+), 40 deletions(-) diff --git a/lms/djangoapps/courseware/tests/helpers.py b/lms/djangoapps/courseware/tests/helpers.py index ce0603990b..1ceeb14433 100644 --- a/lms/djangoapps/courseware/tests/helpers.py +++ b/lms/djangoapps/courseware/tests/helpers.py @@ -44,6 +44,10 @@ def check_for_post_code(self, code, url, data={}): class LoginEnrollmentTestCase(TestCase): + """ + Provides support for user creation, + activation, login, and course enrollment. + """ def setup_user(self): """ diff --git a/lms/djangoapps/courseware/tests/test_navigation.py b/lms/djangoapps/courseware/tests/test_navigation.py index f4662f2ef5..f1aa7f5b31 100644 --- a/lms/djangoapps/courseware/tests/test_navigation.py +++ b/lms/djangoapps/courseware/tests/test_navigation.py @@ -21,7 +21,6 @@ class TestNavigation(ModuleStoreTestCase, LoginEnrollmentTestCase): """ def setUp(self): - xmodule.modulestore.django._MODULESTORES = {} self.course = CourseFactory.create() self.full = CourseFactory.create(display_name='Robot_Sub_Course') diff --git a/lms/djangoapps/courseware/tests/test_view_authentication.py b/lms/djangoapps/courseware/tests/test_view_authentication.py index 8e03e2563b..1edeac58ed 100644 --- a/lms/djangoapps/courseware/tests/test_view_authentication.py +++ b/lms/djangoapps/courseware/tests/test_view_authentication.py @@ -10,7 +10,7 @@ from django.test.utils import override_settings # Need access to internal func to put users in the right group from courseware.access import (has_access, _course_staff_group_name, - course_beta_test_group_name) + course_beta_test_group_name, settings as access_settings) from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase @@ -20,6 +20,7 @@ from helpers import LoginEnrollmentTestCase, check_for_get_code from modulestore_config import TEST_DATA_MONGO_MODULESTORE +#@patch.dict(access_settings.MITX_FEATURES, {'DISABLE_START_DATES': True}) @override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): """ @@ -81,20 +82,13 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): """ List of urls that students should be able to see before launch. + `course` is an instance of CourseDescriptor. """ urls = self._reverse_urls(['about_course'], course) urls.append(reverse('courses')) return urls - def instructor_urls(self, course): - """ - List of urls that only instructors/staff should be able to see. - """ - urls = self._reverse_urls(['instructor_dashboard', - 'gradebook', 'grade_summary'], course) - return urls - def _check_non_staff_light(self, course): """ Check that non-staff have access to light urls. @@ -295,25 +289,12 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): Make sure that before course start, students can't access course pages, but instructors can. """ - self.run_wrapped(self._do_test_dark_launch_enrolled_student) - self.run_wrapped(self._do_test_dark_launch_instructor) - self.run_wrapped(self._do_test_dark_launch_staff) - def test_enrollment_period(self): + @patch.dict(access_settings.MITX_FEATURES, {'DISABLE_START_DATES': False}) + def test_dark_launch_enrolled_student(self): """ - Check that enrollment periods work. - """ - self.run_wrapped(self._do_test_enrollment_period) - - # def test_beta_period(self): - # """ - # Check that beta-test access works. - # """ - # self.run_wrapped(self._do_test_beta_period) - - def _do_test_dark_launch_enrolled_student(self): - """ - Actually do the test, relying on settings to be right. + Make sure that before course start, students can't access course + pages. """ student_email, student_password = self.ACCOUNT_INFO[0] @@ -328,7 +309,6 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): self.assertFalse(self.course.has_started()) self.assertFalse(self.full.has_started()) - self.assertFalse(settings.MITX_FEATURES['DISABLE_START_DATES']) # First, try with an enrolled student print '=== Testing student access....' @@ -342,7 +322,12 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): self._check_non_staff_light(self.full) self._check_non_staff_dark(self.full) - def _do_test_dark_launch_instructor(self): + @patch.dict(access_settings.MITX_FEATURES, {'DISABLE_START_DATES': False}) + def test_dark_launch_instructor(self): + """ + Make sure that before course start instructors can access the + page for their course. + """ instructor_email, instructor_password = self.ACCOUNT_INFO[1] @@ -369,7 +354,12 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): self._check_non_staff_dark(self.full) self._check_staff(self.course) - def _do_test_dark_launch_staff(self): + @patch.dict(access_settings.MITX_FEATURES, {'DISABLE_START_DATES': False}) + def test_dark_launch_staff(self): + """ + Make sure that before course start staff can access + course pages. + """ instructor_email, instructor_password = self.ACCOUNT_INFO[1] @@ -394,9 +384,10 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): self._check_staff(self.course) self._check_staff(self.full) - def _do_test_enrollment_period(self): + @patch.dict(access_settings.MITX_FEATURES, {'DISABLE_START_DATES': False}) + def test_enrollment_period(self): """ - Actually do the test, relying on settings to be right. + Check that enrollment periods work. """ student_email, student_password = self.ACCOUNT_INFO[0] @@ -447,20 +438,15 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): self.unenroll(self.course) self.assertTrue(self.enroll(self.course)) - #from courseware.access import MITX_FEATURES - - #@patch.dict(MITX_FEATURES, {'DISABLE_START_DATES': True}) - def _do_test_beta_period(self): + @patch.dict(access_settings.MITX_FEATURES, {'DISABLE_START_DATES': False}) + def test_beta_period(self): """ - Actually test beta periods, relying on settings to be right. + Check that beta-test access works. """ student_email, student_password = self.ACCOUNT_INFO[0] instructor_email, instructor_password = self.ACCOUNT_INFO[1] - # trust, but verify :) - self.assertFalse(settings.MITX_FEATURES['DISABLE_START_DATES']) - # Make courses start in the future now = datetime.datetime.now(pytz.UTC) tomorrow = now + datetime.timedelta(days=1) From 7fd1190505fde49e24763927721701187bcceaf0 Mon Sep 17 00:00:00 2001 From: Jean Manuel Nater Date: Mon, 24 Jun 2013 17:13:33 -0400 Subject: [PATCH 164/995] Updated a doc string. --- lms/djangoapps/courseware/tests/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/djangoapps/courseware/tests/helpers.py b/lms/djangoapps/courseware/tests/helpers.py index 1ceeb14433..a02a0dfe50 100644 --- a/lms/djangoapps/courseware/tests/helpers.py +++ b/lms/djangoapps/courseware/tests/helpers.py @@ -120,7 +120,7 @@ class LoginEnrollmentTestCase(TestCase): """ Try to enroll and return boolean indicating result. `course` is an instance of CourseDescriptor. - `verify` is an optional parameter specifying whether we + `verify` is an optional boolean parameter specifying whether we want to verify that the student was successfully enrolled in the course. """ From 1b344e4d4d14e7996ffff393a0d6d8d50f6aeae5 Mon Sep 17 00:00:00 2001 From: Jean Manuel Nater Date: Mon, 24 Jun 2013 17:20:59 -0400 Subject: [PATCH 165/995] Removed some unused functions. --- .../tests/test_view_authentication.py | 45 ------------------- 1 file changed, 45 deletions(-) diff --git a/lms/djangoapps/courseware/tests/test_view_authentication.py b/lms/djangoapps/courseware/tests/test_view_authentication.py index 1edeac58ed..6a6c539b60 100644 --- a/lms/djangoapps/courseware/tests/test_view_authentication.py +++ b/lms/djangoapps/courseware/tests/test_view_authentication.py @@ -29,27 +29,6 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): ACCOUNT_INFO = [('view@test.com', 'foo'), ('view2@test.com', 'foo')] - @classmethod - def _instructor_urls(self, course): - """ - `course` is an instance of CourseDescriptor whose section URLs are to be returned. - - Returns a list of URLs corresponding to sections in the passed in course. - """ - - urls = [reverse(name, kwargs={'course_id': course.id}) for name in ( - 'instructor_dashboard', - 'gradebook', - 'grade_summary',)] - - email, _ = self.ACCOUNT_INFO[0] - student_id = User.objects.get(email=email).id - - urls.append(reverse('student_progress', - kwargs={'course_id': course.id, - 'student_id': student_id})) - return urls - @staticmethod def _reverse_urls(names, course): """ @@ -65,30 +44,6 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): return [reverse(name, kwargs={'course_id': course.id}) for name in names] - def _dark_student_urls(self, course): - """ - List of urls that students should be able to see only - after launch, but staff should see before - """ - urls = self._reverse_urls(['info', 'progress'], course) - urls.extend([ - reverse('book', kwargs={'course_id': course.id, - 'book_index': index}) - for index, book in enumerate(course.textbooks) - ]) - return urls - - def _light_student_urls(self, course): - """ - List of urls that students should be able to see before - launch. - `course` is an instance of CourseDescriptor. - """ - urls = self._reverse_urls(['about_course'], course) - urls.append(reverse('courses')) - - return urls - def _check_non_staff_light(self, course): """ Check that non-staff have access to light urls. From c4c68f516b796c8f9ac837ff30fab9ac59126771 Mon Sep 17 00:00:00 2001 From: Jean Manuel Nater Date: Mon, 24 Jun 2013 17:31:49 -0400 Subject: [PATCH 166/995] Removed some unnecessary imports. --- .../courseware/tests/test_navigation.py | 2 -- .../courseware/tests/test_view_authentication.py | 15 --------------- 2 files changed, 17 deletions(-) diff --git a/lms/djangoapps/courseware/tests/test_navigation.py b/lms/djangoapps/courseware/tests/test_navigation.py index f1aa7f5b31..eaeb062504 100644 --- a/lms/djangoapps/courseware/tests/test_navigation.py +++ b/lms/djangoapps/courseware/tests/test_navigation.py @@ -5,8 +5,6 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -import xmodule.modulestore.django - from helpers import LoginEnrollmentTestCase, check_for_get_code from modulestore_config import TEST_DATA_MONGO_MODULESTORE diff --git a/lms/djangoapps/courseware/tests/test_view_authentication.py b/lms/djangoapps/courseware/tests/test_view_authentication.py index 6a6c539b60..b118f99ca2 100644 --- a/lms/djangoapps/courseware/tests/test_view_authentication.py +++ b/lms/djangoapps/courseware/tests/test_view_authentication.py @@ -224,21 +224,6 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase): print 'checking for 200 on {0}'.format(url) check_for_get_code(self, 200, url) - def run_wrapped(self, test): - """ - test.py turns off start dates. Enable them. - Because settings is global, be careful not to mess it up for other tests - (Can't use override_settings because we're only changing part of the - MITX_FEATURES dict) - """ - oldDSD = settings.MITX_FEATURES['DISABLE_START_DATES'] - - try: - settings.MITX_FEATURES['DISABLE_START_DATES'] = False - test() - finally: - settings.MITX_FEATURES['DISABLE_START_DATES'] = oldDSD - def test_dark_launch(self): """ Make sure that before course start, students can't access course From 332a440539928e7bd8d9d9fab3dd8d5f475f4b97 Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Wed, 19 Jun 2013 18:09:18 -0400 Subject: [PATCH 167/995] Enable per-student background tasks. --- lms/djangoapps/instructor_task/api_helper.py | 7 +- lms/djangoapps/instructor_task/models.py | 21 +++++- .../instructor_task/tests/test_api.py | 2 +- .../instructor_task/tests/test_integration.py | 2 +- .../instructor_task/tests/test_tasks.py | 71 +++++++++++++++---- .../instructor_task/tests/test_views.py | 5 +- .../courseware/instructor_dashboard.html | 6 +- 7 files changed, 87 insertions(+), 27 deletions(-) diff --git a/lms/djangoapps/instructor_task/api_helper.py b/lms/djangoapps/instructor_task/api_helper.py index f9febd17d7..2795fd08c1 100644 --- a/lms/djangoapps/instructor_task/api_helper.py +++ b/lms/djangoapps/instructor_task/api_helper.py @@ -2,8 +2,6 @@ import hashlib import json import logging -from django.db import transaction - from celery.result import AsyncResult from celery.states import READY_STATES, SUCCESS, FAILURE, REVOKED @@ -30,7 +28,6 @@ def _task_is_running(course_id, task_type, task_key): return len(runningTasks) > 0 -@transaction.autocommit def _reserve_task(course_id, task_type, task_key, task_input, requester): """ Creates a database entry to indicate that a task is in progress. @@ -39,9 +36,9 @@ def _reserve_task(course_id, task_type, task_key, task_input, requester): Includes the creation of an arbitrary value for task_id, to be submitted with the task call to celery. - Autocommit annotation makes sure the database entry is committed. + The InstructorTask.create method makes sure the InstructorTask entry is committed. When called from any view that is wrapped by TransactionMiddleware, - and thus in a "commit-on-success" transaction, this autocommit here + and thus in a "commit-on-success" transaction, an autocommit buried within here will cause any pending transaction to be committed by a successful save here. Any future database operations will take place in a separate transaction. diff --git a/lms/djangoapps/instructor_task/models.py b/lms/djangoapps/instructor_task/models.py index f01cc4e3ad..b28a9a3d83 100644 --- a/lms/djangoapps/instructor_task/models.py +++ b/lms/djangoapps/instructor_task/models.py @@ -72,6 +72,16 @@ class InstructorTask(models.Model): @classmethod def create(cls, course_id, task_type, task_key, task_input, requester): + """ + Create an instance of InstructorTask. + + The InstructorTask.save_now method makes sure the InstructorTask entry is committed. + When called from any view that is wrapped by TransactionMiddleware, + and thus in a "commit-on-success" transaction, an autocommit buried within here + will cause any pending transaction to be committed by a successful + save here. Any future database operations will take place in a + separate transaction. + """ # create the task_id here, and pass it into celery: task_id = str(uuid4()) @@ -99,7 +109,16 @@ class InstructorTask(models.Model): @transaction.autocommit def save_now(self): - """Writes InstructorTask immediately, ensuring the transaction is committed.""" + """ + Writes InstructorTask immediately, ensuring the transaction is committed. + + Autocommit annotation makes sure the database entry is committed. + When called from any view that is wrapped by TransactionMiddleware, + and thus in a "commit-on-success" transaction, this autocommit here + will cause any pending transaction to be committed by a successful + save here. Any future database operations will take place in a + separate transaction. + """ self.save() @staticmethod diff --git a/lms/djangoapps/instructor_task/tests/test_api.py b/lms/djangoapps/instructor_task/tests/test_api.py index 841fdca8a0..1e40c51c4b 100644 --- a/lms/djangoapps/instructor_task/tests/test_api.py +++ b/lms/djangoapps/instructor_task/tests/test_api.py @@ -22,7 +22,7 @@ from instructor_task.tests.test_base import (InstructorTaskTestCase, class InstructorTaskReportTest(InstructorTaskTestCase): """ - Tests API and view methods that involve the reporting of status for background tasks. + Tests API methods that involve the reporting of status for background tasks. """ def test_get_running_instructor_tasks(self): diff --git a/lms/djangoapps/instructor_task/tests/test_integration.py b/lms/djangoapps/instructor_task/tests/test_integration.py index 5a17e32329..9b56663753 100644 --- a/lms/djangoapps/instructor_task/tests/test_integration.py +++ b/lms/djangoapps/instructor_task/tests/test_integration.py @@ -1,5 +1,5 @@ """ -Integration Tests for LMS instructor-initiated background tasks +Integration Tests for LMS instructor-initiated background tasks. Runs tasks on answers to course problems to validate that code paths actually work. diff --git a/lms/djangoapps/instructor_task/tests/test_tasks.py b/lms/djangoapps/instructor_task/tests/test_tasks.py index c59a7065ae..090c114720 100644 --- a/lms/djangoapps/instructor_task/tests/test_tasks.py +++ b/lms/djangoapps/instructor_task/tests/test_tasks.py @@ -1,5 +1,5 @@ """ -Unit tests for LMS instructor-initiated background tasks, +Unit tests for LMS instructor-initiated background tasks. Runs tasks on answers to course problems to validate that code paths actually work. @@ -7,6 +7,7 @@ paths actually work. """ import json from uuid import uuid4 +from unittest import skip from mock import Mock, patch @@ -62,6 +63,7 @@ class TestInstructorTasks(InstructorTaskModuleTestCase): } def _run_task_with_mock_celery(self, task_function, entry_id, task_id, expected_failure_message=None): + """Submit a task and mock how celery provides a current_task.""" self.current_task = Mock() self.current_task.request = Mock() self.current_task.request.id = task_id @@ -73,7 +75,7 @@ class TestInstructorTasks(InstructorTaskModuleTestCase): return task_function(entry_id, self._get_xmodule_instance_args()) def _test_missing_current_task(self, task_function): - # run without (mock) Celery running + """Check that a task_function fails when celery doesn't provide a current_task.""" task_entry = self._create_input_entry() with self.assertRaises(UpdateProblemModuleStateError): task_function(task_entry.id, self._get_xmodule_instance_args()) @@ -88,7 +90,7 @@ class TestInstructorTasks(InstructorTaskModuleTestCase): self._test_missing_current_task(delete_problem_state) def _test_undefined_problem(self, task_function): - # run with celery, but no problem defined + """Run with celery, but no problem defined.""" task_entry = self._create_input_entry() with self.assertRaises(ItemNotFoundError): self._run_task_with_mock_celery(task_function, task_entry.id, task_entry.task_id) @@ -103,7 +105,7 @@ class TestInstructorTasks(InstructorTaskModuleTestCase): self._test_undefined_problem(delete_problem_state) def _test_run_with_task(self, task_function, action_name, expected_num_updated): - # run with some StudentModules for the problem + """Run a task and check the number of StudentModules processed.""" task_entry = self._create_input_entry() status = self._run_task_with_mock_celery(task_function, task_entry.id, task_entry.task_id) # check return value @@ -118,7 +120,7 @@ class TestInstructorTasks(InstructorTaskModuleTestCase): self.assertEquals(entry.task_state, SUCCESS) def _test_run_with_no_state(self, task_function, action_name): - # run with no StudentModules for the problem + """Run with no StudentModules defined for the current problem.""" self.define_option_problem(PROBLEM_URL_NAME) self._test_run_with_task(task_function, action_name, 0) @@ -185,7 +187,7 @@ class TestInstructorTasks(InstructorTaskModuleTestCase): module_state_key=self.problem_url) def _test_reset_with_student(self, use_email): - # run with some StudentModules for the problem + """Run a reset task for one student, with several StudentModules for the problem defined.""" num_students = 10 initial_attempts = 3 input_state = json.dumps({'attempts': initial_attempts}) @@ -233,8 +235,7 @@ class TestInstructorTasks(InstructorTaskModuleTestCase): self._test_reset_with_student(True) def _test_run_with_failure(self, task_function, expected_message): - # run with no StudentModules for the problem, - # because we will fail before entering the loop. + """Run a task and trigger an artificial failure with give message.""" task_entry = self._create_input_entry() self.define_option_problem(PROBLEM_URL_NAME) with self.assertRaises(TestTaskFailure): @@ -256,8 +257,10 @@ class TestInstructorTasks(InstructorTaskModuleTestCase): self._test_run_with_failure(delete_problem_state, 'We expected this to fail') def _test_run_with_long_error_msg(self, task_function): - # run with an error message that is so long it will require - # truncation (as well as the jettisoning of the traceback). + """ + Run with an error message that is so long it will require + truncation (as well as the jettisoning of the traceback). + """ task_entry = self._create_input_entry() self.define_option_problem(PROBLEM_URL_NAME) expected_message = "x" * 1500 @@ -282,9 +285,11 @@ class TestInstructorTasks(InstructorTaskModuleTestCase): self._test_run_with_long_error_msg(delete_problem_state) def _test_run_with_short_error_msg(self, task_function): - # run with an error message that is short enough to fit - # in the output, but long enough that the traceback won't. - # Confirm that the traceback is truncated. + """ + Run with an error message that is short enough to fit + in the output, but long enough that the traceback won't. + Confirm that the traceback is truncated. + """ task_entry = self._create_input_entry() self.define_option_problem(PROBLEM_URL_NAME) expected_message = "x" * 900 @@ -330,3 +335,43 @@ class TestInstructorTasks(InstructorTaskModuleTestCase): self.assertEquals(output['exception'], 'ValueError') self.assertTrue("Length of task output is too long" in output['message']) self.assertTrue('traceback' not in output) + + @skip + def test_rescoring_unrescorable(self): + # TODO: this test needs to have Mako templates initialized + # to make sure that the creation of an XModule works. + input_state = json.dumps({'done': True}) + num_students = 1 + self._create_students_with_state(num_students, input_state) + task_entry = self._create_input_entry() + with self.assertRaises(UpdateProblemModuleStateError): + self._run_task_with_mock_celery(rescore_problem, task_entry.id, task_entry.task_id) + # check values stored in table: + entry = InstructorTask.objects.get(id=task_entry.id) + output = json.loads(entry.task_output) + self.assertEquals(output['exception'], "UpdateProblemModuleStateError") + self.assertEquals(output['message'], "Specified problem does not support rescoring.") + self.assertGreater(len(output['traceback']), 0) + + @skip + def test_rescoring_success(self): + # TODO: this test needs to have Mako templates initialized + # to make sure that the creation of an XModule works. + input_state = json.dumps({'done': True}) + num_students = 10 + self._create_students_with_state(num_students, input_state) + task_entry = self._create_input_entry() + mock_instance = Mock() + mock_instance.rescore_problem = Mock({'success': 'correct'}) + # TODO: figure out why this mock is not working.... + with patch('courseware.module_render.get_module_for_descriptor_internal') as mock_get_module: + mock_get_module.return_value = mock_instance + self._run_task_with_mock_celery(rescore_problem, task_entry.id, task_entry.task_id) + # check return value + entry = InstructorTask.objects.get(id=task_entry.id) + output = json.loads(entry.task_output) + self.assertEquals(output.get('attempted'), num_students) + self.assertEquals(output.get('updated'), num_students) + self.assertEquals(output.get('total'), num_students) + self.assertEquals(output.get('action_name'), 'rescored') + self.assertGreater('duration_ms', 0) diff --git a/lms/djangoapps/instructor_task/tests/test_views.py b/lms/djangoapps/instructor_task/tests/test_views.py index 9020bf6e60..41de314abd 100644 --- a/lms/djangoapps/instructor_task/tests/test_views.py +++ b/lms/djangoapps/instructor_task/tests/test_views.py @@ -1,6 +1,6 @@ """ -Test for LMS instructor background task queue management +Test for LMS instructor background task views. """ import json from celery.states import SUCCESS, FAILURE, REVOKED, PENDING @@ -18,7 +18,7 @@ from instructor_task.views import instructor_task_status, get_task_completion_in class InstructorTaskReportTest(InstructorTaskTestCase): """ - Tests API and view methods that involve the reporting of status for background tasks. + Tests view methods that involve the reporting of status for background tasks. """ def _get_instructor_task_status(self, task_id): @@ -263,4 +263,3 @@ class InstructorTaskReportTest(InstructorTaskTestCase): succeeded, message = get_task_completion_info(instructor_task) self.assertFalse(succeeded) self.assertEquals(message, "Problem rescored for 2 of 3 students (out of 5)") - diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index ef1eb174fc..d541962906 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -249,7 +249,7 @@ function goto( mode)

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

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

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

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

      From 3e38327f7fdfc5fbd76780a76e51fbc8d1575e25 Mon Sep 17 00:00:00 2001 From: Jason Bau Date: Mon, 24 Jun 2013 17:30:25 -0700 Subject: [PATCH 168/995] External_auth.views.login_or_signup fix codepath that didn't set uname which caused UnboundLocalError. Added tests for this case --- .../external_auth/tests/test_shib.py | 21 ++++++++++++++++--- common/djangoapps/external_auth/views.py | 1 + 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/common/djangoapps/external_auth/tests/test_shib.py b/common/djangoapps/external_auth/tests/test_shib.py index eb05b59afb..e46c9eda8f 100644 --- a/common/djangoapps/external_auth/tests/test_shib.py +++ b/common/djangoapps/external_auth/tests/test_shib.py @@ -98,7 +98,8 @@ class ShibSPTest(ModuleStoreTestCase): def test_shib_login(self): """ Tests that: - * shib credentials that match an existing ExternalAuthMap with a linked user logs the user in + * shib credentials that match an existing ExternalAuthMap with a linked active user logs the user in + * shib credentials that match an existing ExternalAuthMap with a linked inactive user shows error page * shib credentials that match an existing ExternalAuthMap without a linked user and also match the email of an existing user without an existing ExternalAuthMap links the two and log the user in * shib credentials that match an existing ExternalAuthMap without a linked user and also match the email @@ -117,8 +118,19 @@ class ShibSPTest(ModuleStoreTestCase): user_wo_map.save() extauth.save() + inactive_user = UserFactory.create(email='inactive@stanford.edu') + inactive_user.is_active = False + inactive_extauth = ExternalAuthMap(external_id='inactive@stanford.edu', + external_email='', + external_domain='shib:https://idp.stanford.edu/', + external_credentials="", + user=inactive_user) + inactive_user.save() + inactive_extauth.save() + idps = ['https://idp.stanford.edu/', 'https://someother.idp.com/'] - remote_users = ['withmap@stanford.edu', 'womap@stanford.edu', 'testuser2@someother_idp.com'] + remote_users = ['withmap@stanford.edu', 'womap@stanford.edu', + 'testuser2@someother_idp.com', 'inactive@stanford.edu'] for idp in idps: for remote_user in remote_users: @@ -133,13 +145,16 @@ class ShibSPTest(ModuleStoreTestCase): self.assertIsInstance(response, HttpResponseRedirect) self.assertEqual(request.user, user_w_map) self.assertEqual(response['Location'], '/') + elif idp == "https://idp.stanford.edu/" and remote_user == 'inactive@stanford.edu': + self.assertEqual(response.status_code, 403) + self.assertIn("Account not yet activated: please look for link in your email", response.content) elif idp == "https://idp.stanford.edu/" and remote_user == 'womap@stanford.edu': self.assertIsNotNone(ExternalAuthMap.objects.get(user=user_wo_map)) self.assertIsInstance(response, HttpResponseRedirect) self.assertEqual(request.user, user_wo_map) self.assertEqual(response['Location'], '/') elif idp == "https://someother.idp.com/" and remote_user in \ - ['withmap@stanford.edu', 'womap@stanford.edu']: + ['withmap@stanford.edu', 'womap@stanford.edu', 'inactive@stanford.edu']: self.assertEqual(response.status_code, 403) self.assertIn("You have already created an account using an external login", response.content) else: diff --git a/common/djangoapps/external_auth/views.py b/common/djangoapps/external_auth/views.py index 06709eff9e..b0dfbec6b9 100644 --- a/common/djangoapps/external_auth/views.py +++ b/common/djangoapps/external_auth/views.py @@ -176,6 +176,7 @@ def external_login_or_signup(request, # We trust shib's authentication, so no need to authenticate using the password again if settings.MITX_FEATURES.get('AUTH_USE_SHIB'): + uname = internal_user.username user = internal_user # Assuming this 'AUTHENTICATION_BACKENDS' is set in settings, which I think is safe if settings.AUTHENTICATION_BACKENDS: From ddc986f775e5eb2cf5bb30644ae7d934140805cb Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Tue, 25 Jun 2013 11:25:29 -0400 Subject: [PATCH 169/995] Call event.preventDefault() on notification action buttons But allow you to specify that the event should not be prevented --- .../coffee/spec/views/feedback_spec.coffee | 39 +++++++++++++++++++ cms/static/js/views/feedback.js | 14 ++++++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/cms/static/coffee/spec/views/feedback_spec.coffee b/cms/static/coffee/spec/views/feedback_spec.coffee index e5916c5ed3..adec11e2a7 100644 --- a/cms/static/coffee/spec/views/feedback_spec.coffee +++ b/cms/static/coffee/spec/views/feedback_spec.coffee @@ -17,6 +17,16 @@ beforeEach -> return text.test(trimmedText) else return trimmedText.indexOf(text) != -1; + toHaveBeenPrevented: -> + # remove this when we upgrade jasmine-jquery + eventName = @actual.eventName + selector = @actual.selector + @message = -> + [ + "Expected event #{eventName} to have been prevented on #{selector}", + "Expected event #{eventName} not to have been prevented on #{selector}" + ] + return jasmine.JQuery.events.wasPrevented(selector, eventName) describe "CMS.Views.SystemFeedback", -> beforeEach -> @@ -123,6 +133,35 @@ describe "CMS.Views.SystemFeedback click events", -> it "should apply class to secondary action", -> expect(@view.$(".action-secondary")).toHaveClass("cancel-button") + it "should preventDefault on primary action", -> + spyOnEvent(".action-primary", "click") + @view.$(".action-primary").click() + expect("click").toHaveBeenPreventedOn(".action-primary") + + it "should preventDefault on secondary action", -> + spyOnEvent(".action-secondary", "click") + @view.$(".action-secondary").click() + expect("click").toHaveBeenPreventedOn(".action-secondary") + +describe "CMS.Views.SystemFeedback not preventing events", -> + beforeEach -> + @clickSpy = jasmine.createSpy('clickSpy') + @view = new CMS.Views.Alert.Confirmation( + title: "It's all good" + message: "No reason for this alert" + actions: + primary: + text: "Whatever" + click: @clickSpy + preventDefault: false + ) + @view.show() + + it "should not preventDefault", -> + spyOnEvent(".action-primary", "click") + @view.$(".action-primary").click() + expect("click").not.toHaveBeenPreventedOn(".action-primary") + expect(@clickSpy).toHaveBeenCalled() describe "CMS.Views.SystemFeedback multiple secondary actions", -> beforeEach -> diff --git a/cms/static/js/views/feedback.js b/cms/static/js/views/feedback.js index 3f161d5b1f..3bfeeb5af2 100644 --- a/cms/static/js/views/feedback.js +++ b/cms/static/js/views/feedback.js @@ -10,8 +10,12 @@ CMS.Views.SystemFeedback = Backbone.View.extend({ minShown: 0, // length of time after this view has been shown before it can be hidden (milliseconds) maxShown: Infinity // length of time after this view has been shown before it will be automatically hidden (milliseconds) - /* could also have an "actions" hash: here is an example demonstrating - the expected structure + /* Could also have an "actions" hash: here is an example demonstrating + the expected structure. For each action, by default the framework + will call preventDefault on the click event before the function is + run; to make it not do that, just pass `preventDefault: false` in + the action object. + actions: { primary: { "text": "Save", @@ -106,6 +110,9 @@ CMS.Views.SystemFeedback = Backbone.View.extend({ if(!actions) { return; } var primary = actions.primary; if(!primary) { return; } + if(primary.preventDefault !== false) { + event.preventDefault(); + } if(primary.click) { primary.click.call(event.target, this, event); } @@ -121,6 +128,9 @@ CMS.Views.SystemFeedback = Backbone.View.extend({ i = _.indexOf(this.$(".action-secondary"), event.target); } var secondary = secondaryList[i]; + if(secondary.preventDefault !== false) { + event.preventDefault(); + } if(secondary.click) { secondary.click.call(event.target, this, event); } From a9a7f97d9b694078ccc23706d29a1fcb11dcc74a Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Tue, 25 Jun 2013 11:32:45 -0400 Subject: [PATCH 170/995] Update CHANGELOG for per-student problem rescoring. --- CHANGELOG.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index cb8eec738f..21b8c9f90b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,11 @@ These are notable changes in edx-platform. This is a rolling list of changes, in roughly chronological order, most recent first. Add your entries at or near the top. Include a label indicating the component affected. +LMS: Problem rescoring. Added options on the Grades tab of the +Instructor Dashboard to allow a particular student's submission for a +particular problem to be rescored. Provides an option to see a +history of background tasks for a given problem and student. + Blades: Small UX fix on capa multiple-choice problems. Make labels only as wide as the text to reduce accidental choice selections. From 8a9125f121a1b983b33d4c7f8cd16deeee8335cf Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Tue, 25 Jun 2013 11:33:46 -0400 Subject: [PATCH 171/995] Test Mongo database is now unique and destroyed in teardown --- common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py index c5ef0d751a..44e69fb0ed 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py @@ -13,11 +13,12 @@ from xmodule.templates import update_templates from .test_modulestore import check_path_to_location from . import DATA_DIR +from uuid import uuid4 HOST = 'localhost' PORT = 27017 -DB = 'test' +DB = 'test_mongo_%s' % uuid4().hex COLLECTION = 'modulestore' FS_ROOT = DATA_DIR # TODO (vshnayder): will need a real fs_root for testing load_item DEFAULT_CLASS = 'xmodule.raw_module.RawDescriptor' @@ -39,7 +40,8 @@ class TestMongoModuleStore(object): @classmethod def teardownClass(cls): - pass + cls.connection = pymongo.connection.Connection(HOST, PORT) + cls.connection.drop_database(DB) @staticmethod def initdb(): From 3df08dc1c4358e3d926fe73161df3b7064a52c53 Mon Sep 17 00:00:00 2001 From: marco Date: Thu, 11 Apr 2013 23:20:49 -0400 Subject: [PATCH 172/995] blankslate edited, header bar now with home button --- lms/static/sass/_discussion.scss | 89 +++++++++++++++---- .../discussion/_filter_dropdown.html | 8 +- .../discussion/_thread_list_template.html | 4 + lms/templates/discussion/index.html | 12 ++- 4 files changed, 93 insertions(+), 20 deletions(-) diff --git a/lms/static/sass/_discussion.scss b/lms/static/sass/_discussion.scss index ddfe4a88f1..e6d6d4b373 100644 --- a/lms/static/sass/_discussion.scss +++ b/lms/static/sass/_discussion.scss @@ -200,7 +200,7 @@ body.discussion { z-index: 9999; width: 100%; @include box-sizing(border-box); - background: #737373; + background: #797979; border: 1px solid #333; box-shadow: 0 2px 50px rgba(0, 0, 0, .4); } @@ -711,7 +711,7 @@ body.discussion { border-radius: 3px 0 0 0; - .browse, + .home, .browse, .search { position: relative; float: left; @@ -724,9 +724,29 @@ body.discussion { &:hover { background-color: #e9e9e9; } + } - &.is-open { - width: 80%; + .home { + border-radius: 3px 0 0 0; + box-shadow: -1px 0 0 #aaa inset; + cursor: pointer; + + .home-icon { + display: block; + position: absolute; + top: 30%; + left: 30%; + z-index: 100; + width: 25px; + height: 25px; + //margin-left: -17px; + background: url(../images/home-discussion-icon.png) no-repeat; + opacity: 1; + @include transition(none); + } + + .home-btn { + //nothing here yet } } @@ -735,6 +755,7 @@ body.discussion { box-shadow: -1px 0 0 #aaa inset; &.is-open { + width:60%; .browse-topic-drop-btn span { opacity: 1.0; } @@ -775,6 +796,11 @@ body.discussion { &.is-open { cursor: auto; + width: 60%; + + .home { + width:0%; + } .post-search { padding: 0 10px; @@ -802,7 +828,7 @@ body.discussion { z-index: 50; width: 100%; height: 100%; - border-radius: 3px 0 0 0; + border-radius: 0 0 0 0; border: 1px solid transparent; text-align: center; overflow: hidden; @@ -821,6 +847,9 @@ body.discussion { opacity: 0.0; @include transition(opacity .2s); } + .drop-arrow { + font-size:16px; + } } .browse-topic-drop-icon { @@ -844,7 +873,7 @@ body.discussion { left: -1px; z-index: 9999; width: 100%; - background: #737373; + background: #797979; border: 1px solid #4b4b4b; border-left: none; border-radius: 0 0 3px 3px; @@ -853,8 +882,16 @@ body.discussion { .browse-topic-drop-menu { max-height: 400px; overflow-y: scroll; + + .drop-menu-meta-category span, + .drop-menu-parent-category span { + margin: 10px 0; + font-size: 14px; + font-weight: 700; + } } + ul { position: inline; } @@ -867,7 +904,7 @@ body.discussion { display: block; padding: 0 20px; border-top: 1px solid #5f5f5f; - font-size: 14px; + font-size: 12px; font-weight: 700; line-height: 22px; color: #fff; @@ -886,7 +923,7 @@ body.discussion { .board-name { float: left; width: 80%; - margin: 13px 0; + margin: 5px 0; color: #fff; } @@ -904,14 +941,14 @@ body.discussion { li li { a { padding-left: 44px; - background: url(../images/nested-icon.png) no-repeat 22px 14px; + background: url(../images/nested-icon.png) no-repeat 22px 5px; } } li li li { a { padding-left: 68px; - background: url(../images/nested-icon.png) no-repeat 46px 14px; + background: url(../images/nested-icon.png) no-repeat 46px 5px; } } } @@ -982,7 +1019,7 @@ body.discussion { min-height: 27px; border-bottom: 1px solid #a3a3a3; @include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0)); - background-color: #aeaeae; + background-color: #aaaaaa; box-shadow: 0 1px 0 rgba(255, 255, 255, .2) inset; span, @@ -1271,10 +1308,32 @@ body.discussion { } } - .blank-slate h1 { - margin-top: 195px; - text-align: center; - color: #ccc; + .blank-slate { + //nothing here + .section { + border-bottom: 1px solid #ccc; + margin-top: 15px; + } + .home-header { + //nothing here + } + + .home-title { + font-size: 18px; + color: #000; + margin-bottom: 5px; + } + .home-description { + font-size: 12px; + line-height: 1; + margin-bottom: 10px; + } + .home-stats { + //nothing + } + .home-emailsettings { + //nothing here + } } .blank-slate, diff --git a/lms/templates/discussion/_filter_dropdown.html b/lms/templates/discussion/_filter_dropdown.html index dd5b94f910..a191e1b583 100644 --- a/lms/templates/discussion/_filter_dropdown.html +++ b/lms/templates/discussion/_filter_dropdown.html @@ -11,12 +11,12 @@ <%def name="render_entry(entries, entry)"> -

    2. ${entry}
    3. +
    4. ${entry}
    5. <%def name="render_category(categories, category)">
    6. - ${category} + ${category}
        ${render_dropdown(categories[category])}
      @@ -29,7 +29,7 @@
    7. + +
      +

      You haven't added any textbooks to this course yet.

      +

      Add your first textbook

      +
      +
      + + + + From f198b7e297b0f4f8ff065512bd8b9b678814ce43 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Fri, 31 May 2013 13:37:27 -0400 Subject: [PATCH 484/995] Clean up Backbone view logic --- cms/templates/js/upload-dialog.underscore | 7 +++++- cms/templates/textbooks.html | 28 +++++++++++++++-------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/cms/templates/js/upload-dialog.underscore b/cms/templates/js/upload-dialog.underscore index 0270525541..5994cabcb8 100644 --- a/cms/templates/js/upload-dialog.underscore +++ b/cms/templates/js/upload-dialog.underscore @@ -1,4 +1,9 @@ -
    8. diff --git a/cms/templates/js/textbook.underscore b/cms/templates/js/textbook.underscore index 02b37a0121..06459e58ff 100644 --- a/cms/templates/js/textbook.underscore +++ b/cms/templates/js/textbook.underscore @@ -1,21 +1,27 @@
      -
      - <%= gettext("Required information to edit or add a textbook and chapters") %> +
      1. +
        + <%= gettext("Textbook information") %>
        - + " value="<%= name %>"> <%= gettext("the title/name of the text book as you would like your students to see it.") %>
        +
      2. +
        + <%= gettext("Chapter(s) information") %> +
          + +
        -

        <%= gettext("Note: It's best practice to break your course's textbook into multiple chapters to reduce loading times for students. Breaking up textbooks into chapters can also help with students more easily finding a concept or topic-based information.") %>

        -
        +
        diff --git a/cms/templates/js/upload-dialog.underscore b/cms/templates/js/upload-dialog.underscore index 5994cabcb8..20a4b5a13c 100644 --- a/cms/templates/js/upload-dialog.underscore +++ b/cms/templates/js/upload-dialog.underscore @@ -1,16 +1,17 @@ -
      - % if disable_course_creation and settings.MITX_FEATURES.get('STAFF_EMAIL',''):

      ${_('Can I create courses in Studio?')}

      @@ -189,14 +243,12 @@
      % endif - % if not disable_course_creation and course_creator_status == "unrequested":

      ${_('Can I create courses in Studio?')}

      -

      ${_('In order to create courses in Studio, you must have authorship rights to create your own course.')}

      +

      ${_('In order to create courses in Studio, you must have Course Creator privileges to create your own course.')}

      - % elif not disable_course_creation and course_creator_status == "denied":

      ${_('Can I create courses in Studio?')}

      @@ -208,7 +260,6 @@ - % else:
      From b1653f056143216ec0026c2f6efcc30df2e2a680 Mon Sep 17 00:00:00 2001 From: Frances Botsford Date: Thu, 11 Jul 2013 11:19:39 -0400 Subject: [PATCH 693/995] layout cleanup on LMS PDF Textbook viewer --- common/static/css/pdfviewer.css | 3 +- lms/static/sass/course/_textbook.scss | 59 +++++++++++++++++++---- lms/templates/static_pdfbook.html | 67 ++++++++++++++------------- 3 files changed, 87 insertions(+), 42 deletions(-) diff --git a/common/static/css/pdfviewer.css b/common/static/css/pdfviewer.css index 656bc47c29..8b0253261b 100644 --- a/common/static/css/pdfviewer.css +++ b/common/static/css/pdfviewer.css @@ -100,7 +100,7 @@ select { .toolbar { /* position: absolute; */ left: 0; - right: 0; + right: 0; height: 32px; z-index: 9999; cursor: default; @@ -185,6 +185,7 @@ select { margin: 0; } +.splitToolbarButton > .toolbarButton, /*added */ .splitToolbarButton:hover > .toolbarButton, .splitToolbarButton:focus > .toolbarButton, .splitToolbarButton.toggled > .toolbarButton, diff --git a/lms/static/sass/course/_textbook.scss b/lms/static/sass/course/_textbook.scss index bc9da1f43f..08f1a853dc 100644 --- a/lms/static/sass/course/_textbook.scss +++ b/lms/static/sass/course/_textbook.scss @@ -1,13 +1,46 @@ div.book-wrapper { - display: table; - table-layout: fixed; - padding: 1em 8em; + max-width: 1150px; + margin: 0 auto; + width: 100%; + background-color: $white; + #toolbarViewer { + padding: 0 ($baseline/2); + + #toolbarViewerLeft { + display: inline-block; + } + + .outerCenter { + display: inline-block; + float: right !important; + right: auto; + + .innerCenter { + right: auto; + } + + .dropdownToolbarButton { + margin: 3px 2px 4px 0; + } + } + + + } #open_close_accordion { display: none; } + .pdfbook-wrap { + display: table; + width: 100%; + } + + .pdfbook-wrap-inner { + display: table-row; + } + section.book-sidebar { @extend .sidebar; @extend .tran; @@ -44,14 +77,17 @@ div.book-wrapper { li { background: none; border-bottom: 0; - padding-left: lh(); + padding-left: ($baseline/2); a { - padding: 0; @include clearfix; + padding: 0; + color: $link-color; + cursor: pointer; &:hover { background-color: transparent; + color: $link-hover; .page-number { opacity: 1.0; @@ -84,7 +120,7 @@ div.book-wrapper { > li { padding: 5px 6px; - margin: 0 16px 5px 25px; + margin: ($baseline/4) ($baseline/2); } } } @@ -158,18 +194,20 @@ div.book-wrapper { } section.page { - border: 1px solid $border-color; + border-left: 1px solid $border-color; background-color: #fff; position: relative; text-align: center; - padding: lh(); - margin-right:10%; border-radius: 0 3px 3px 0; img { max-width: 100%; } + #viewer { + padding: $baseline; + } + div { text-align: left; line-height: 1.6em; @@ -214,3 +252,6 @@ div.book-wrapper { } } } + + + diff --git a/lms/templates/static_pdfbook.html b/lms/templates/static_pdfbook.html index 565a59977a..a8608b2877 100644 --- a/lms/templates/static_pdfbook.html +++ b/lms/templates/static_pdfbook.html @@ -11,7 +11,7 @@ <%static:js group='courseware'/> - + <%block name="js_extra"> @@ -35,10 +35,10 @@ %if page is not None: options.pageNum = ${page}; %endif - + $('#outerContainer').PDFViewer(options); }); - + <%include file="/courseware/course_navigation.html" args="active_page='pdftextbook/{0}'.format(book_index)" /> @@ -91,40 +91,43 @@
      %if 'chapters' in textbook: -
      -
        - <%def name="print_entry(entry, index_value)"> -
      • - - ${entry.get('title')} - -
      • - +
        +
        +
        +
          + <%def name="print_entry(entry, index_value)"> +
        • + + ${entry.get('title')} + +
        • + - % for (index, entry) in enumerate(textbook['chapters']): - ${print_entry(entry, index+1)} - % endfor -
        -
        + % for (index, entry) in enumerate(textbook['chapters']): + ${print_entry(entry, index+1)} + % endfor +
      +
      %endif -
      +
      - -
      -
      -
      - + +
      +
      + + + From f3837009aff72e8214640e4eefa0ef4185b15de3 Mon Sep 17 00:00:00 2001 From: Peter Fogg Date: Thu, 11 Jul 2013 11:26:17 -0400 Subject: [PATCH 694/995] Remove ellipsis from internationalized notifications. --- cms/static/js/base.js | 2 +- cms/static/js/models/section.js | 2 +- cms/static/js/views/textbook.js | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index f172775f3a..c6645daef0 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -713,7 +713,7 @@ function saveSetSectionScheduleDate(e) { }); var saving = new CMS.Views.Notification.Saving({ - title: gettext("Saving…"), + title: gettext("Saving") + "…", maxShown: 1250 }); saving.show(); diff --git a/cms/static/js/models/section.js b/cms/static/js/models/section.js index f9b20f3244..38acceb76d 100644 --- a/cms/static/js/models/section.js +++ b/cms/static/js/models/section.js @@ -23,7 +23,7 @@ CMS.Models.Section = Backbone.Model.extend({ showNotification: function() { if(!this.msg) { this.msg = new CMS.Views.Notification.Saving({ - title: gettext("Saving…") + title: gettext("Saving") + "…" }); } this.msg.show(); diff --git a/cms/static/js/views/textbook.js b/cms/static/js/views/textbook.js index a5a5473915..3abf09230c 100644 --- a/cms/static/js/views/textbook.js +++ b/cms/static/js/views/textbook.js @@ -35,7 +35,7 @@ CMS.Views.ShowTextbook = Backbone.View.extend({ click: function(view) { view.hide(); var delmsg = new CMS.Views.Notification.Saving({ - title: gettext("Deleting…") + title: gettext("Deleting") + "…" }).show(); textbook.destroy({ complete: function() { @@ -122,7 +122,7 @@ CMS.Views.EditTextbook = Backbone.View.extend({ this.setValues(); if(!this.model.isValid()) { return; } var saving = new CMS.Views.Notification.Saving({ - title: gettext("Saving…") + title: gettext("Saving") + "…" }).show(); var that = this; this.model.save({}, { From 08fe23ac5f0c872b1b5784cb81cdc6f2e1c98a61 Mon Sep 17 00:00:00 2001 From: Frances Botsford Date: Thu, 11 Jul 2013 11:26:59 -0400 Subject: [PATCH 695/995] adjusted the LMS PDF textbook zoom elements positioning --- lms/static/sass/course/_textbook.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lms/static/sass/course/_textbook.scss b/lms/static/sass/course/_textbook.scss index 08f1a853dc..72d73bdb78 100644 --- a/lms/static/sass/course/_textbook.scss +++ b/lms/static/sass/course/_textbook.scss @@ -13,8 +13,8 @@ div.book-wrapper { .outerCenter { display: inline-block; - float: right !important; - right: auto; + position: absolute; + right: ($baseline/2); .innerCenter { right: auto; From 24ab36c87ac3ce148a4831830fce254cdc85b32d Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 11 Jul 2013 09:45:59 -0400 Subject: [PATCH 696/995] Unskip Acceptance Tests --- .../contentstore/features/problem-editor.feature | 12 ------------ .../contentstore/features/subsection.feature | 1 - cms/djangoapps/contentstore/features/video.feature | 1 - 3 files changed, 14 deletions(-) diff --git a/cms/djangoapps/contentstore/features/problem-editor.feature b/cms/djangoapps/contentstore/features/problem-editor.feature index fe876aa4e4..cc1d766d2e 100644 --- a/cms/djangoapps/contentstore/features/problem-editor.feature +++ b/cms/djangoapps/contentstore/features/problem-editor.feature @@ -1,35 +1,30 @@ Feature: Problem Editor As a course author, I want to be able to create problems and edit their settings. - @skip Scenario: User can view metadata Given I have created a Blank Common Problem When I edit and select Settings Then I see five alphabetized settings and their expected values And Edit High Level Source is not visible - @skip Scenario: User can modify String values Given I have created a Blank Common Problem When I edit and select Settings Then I can modify the display name And my display name change is persisted on save - @skip Scenario: User can specify special characters in String values Given I have created a Blank Common Problem When I edit and select Settings Then I can specify special characters in the display name And my special characters and persisted on save - @skip Scenario: User can revert display name to unset Given I have created a Blank Common Problem When I edit and select Settings Then I can revert the display name to unset And my display name is unset on save - @skip Scenario: User can select values in a Select Given I have created a Blank Common Problem When I edit and select Settings @@ -37,7 +32,6 @@ Feature: Problem Editor And my change to randomization is persisted And I can revert to the default value for randomization - @skip Scenario: User can modify float input values Given I have created a Blank Common Problem When I edit and select Settings @@ -45,25 +39,21 @@ Feature: Problem Editor And my change to weight is persisted And I can revert to the default value of unset for weight - @skip Scenario: User cannot type letters in float number field Given I have created a Blank Common Problem When I edit and select Settings Then if I set the weight to "abc", it remains unset - @skip Scenario: User cannot type decimal values integer number field Given I have created a Blank Common Problem When I edit and select Settings Then if I set the max attempts to "2.34", it displays initially as "234", and is persisted as "234" - @skip Scenario: User cannot type out of range values in an integer number field Given I have created a Blank Common Problem When I edit and select Settings Then if I set the max attempts to "-3", it displays initially as "-3", and is persisted as "0" - @skip Scenario: Settings changes are not saved on Cancel Given I have created a Blank Common Problem When I edit and select Settings @@ -71,13 +61,11 @@ Feature: Problem Editor And I can modify the display name Then If I press Cancel my changes are not persisted - @skip Scenario: Edit High Level source is available for LaTeX problem Given I have created a LaTeX Problem When I edit and select Settings Then Edit High Level Source is visible - @skip Scenario: High Level source is persisted for LaTeX problem (bug STUD-280) Given I have created a LaTeX Problem When I edit and compile the High Level Source diff --git a/cms/djangoapps/contentstore/features/subsection.feature b/cms/djangoapps/contentstore/features/subsection.feature index 1dfe5d95f5..a11467e3f9 100644 --- a/cms/djangoapps/contentstore/features/subsection.feature +++ b/cms/djangoapps/contentstore/features/subsection.feature @@ -32,7 +32,6 @@ Feature: Create Subsection And I reload the page Then I see the correct dates - @skip Scenario: Delete a subsection Given I have opened a new course section in Studio And I have added a new subsection diff --git a/cms/djangoapps/contentstore/features/video.feature b/cms/djangoapps/contentstore/features/video.feature index 548ba12a3d..e4caa70ef6 100644 --- a/cms/djangoapps/contentstore/features/video.feature +++ b/cms/djangoapps/contentstore/features/video.feature @@ -18,7 +18,6 @@ Feature: Video Component Given I have created a Video component Then when I view the video it does show the captions - @skip Scenario: Captions are toggled correctly Given I have created a Video component And I have toggled captions From b39edd821cd0907af61889120cf81bd696a83cbe Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Thu, 11 Jul 2013 12:05:07 -0400 Subject: [PATCH 697/995] seems like we need to define COURSES_WITH_UNSAFE_CODE in cms.envs.common.py, although cms.envs.common.py imports lms.envs.common (where this is defined). --- cms/envs/common.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cms/envs/common.py b/cms/envs/common.py index 1207b8fe05..260aa30cd2 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -368,3 +368,5 @@ MKTG_URL_LINK_MAP = { 'HONOR': 'honor', 'PRIVACY': 'privacy_edx', } + +COURSES_WITH_UNSAFE_CODE = [] From 4e015eacf51613163209e76784e38cab1aa75c72 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 11 Jul 2013 12:05:18 -0400 Subject: [PATCH 698/995] Fixed the whitelist test to not use full course --- .../contentstore/tests/test_contentstore.py | 11 ++++------- common/test/data/toy/chapter/html_container.xml | 1 + common/test/data/toy/course/2012_Fall.xml | 1 + common/test/data/toy/html/toylab.html | 13 +++++++++++++ common/test/data/toy/html/toylab.xml | 1 + common/test/data/toy/vertical/vertical_test.xml | 11 ++++++++++- 6 files changed, 30 insertions(+), 8 deletions(-) create mode 100644 common/test/data/toy/chapter/html_container.xml create mode 100644 common/test/data/toy/html/toylab.html create mode 100644 common/test/data/toy/html/toylab.xml diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 7b2b3bddcd..b43ea46d7d 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -351,25 +351,23 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): err_cnt = perform_xlint('common/test/data', ['toy']) self.assertGreater(err_cnt, 0) -#FIX - @override_settings(COURSES_WITH_UNSAFE_CODE=['edX/full/.*']) + @override_settings(COURSES_WITH_UNSAFE_CODE=['edX/toy/.*']) def test_module_preview_in_whitelist(self): ''' Tests the ajax callback to render an XModule ''' direct_store = modulestore('direct') - import_from_xml(direct_store, 'common/test/data/', ['full']) + import_from_xml(direct_store, 'common/test/data/', ['toy']) - html_module_location = Location(['i4x', 'edX', 'full', 'html', 'html_90', None]) + html_module_location = Location(['i4x', 'edX', 'toy', 'html', 'toylab', None]) url = reverse('preview_component', kwargs={'location': html_module_location.url()}) resp = self.client.get(url) self.assertEqual(resp.status_code, 200) - self.assertIn('Inline content', resp.content) # also try a custom response which will trigger the 'is this course in whitelist' logic - problem_module_location = Location(['i4x', 'edX', 'full', 'problem', 'H1P1_Energy', None]) + problem_module_location = Location(['i4x', 'edX', 'toy', 'vertical', 'vertical_test', None]) url = reverse('preview_component', kwargs={'location': problem_module_location.url()}) resp = self.client.get(url) self.assertEqual(resp.status_code, 200) @@ -643,7 +641,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): resp = self.client.get('http://localhost:8001/c4x/CDX/123123/asset/&images_circuits_Lab7Solution2.png') self.assertEqual(resp.status_code, 400) -#FIX def test_delete_course(self): """ This test will import a course, make a draft item, and delete it. This will also assert that the diff --git a/common/test/data/toy/chapter/html_container.xml b/common/test/data/toy/chapter/html_container.xml new file mode 100644 index 0000000000..5691607f3b --- /dev/null +++ b/common/test/data/toy/chapter/html_container.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/common/test/data/toy/course/2012_Fall.xml b/common/test/data/toy/course/2012_Fall.xml index ded9713f17..b96f04042f 100644 --- a/common/test/data/toy/course/2012_Fall.xml +++ b/common/test/data/toy/course/2012_Fall.xml @@ -13,4 +13,5 @@ + diff --git a/common/test/data/toy/html/toylab.html b/common/test/data/toy/html/toylab.html new file mode 100644 index 0000000000..b2a4599cc6 --- /dev/null +++ b/common/test/data/toy/html/toylab.html @@ -0,0 +1,13 @@ +Lab 2A: Superposition Experiment + +<<<<<<< Updated upstream +

      Isn't the toy course great?

      + +

      Let's add some markup that uses non-ascii characters. +For example, we should be able to write words like encyclopædia, or foreign words like français. +Looking beyond latin-1, we should handle math symbols: πr² ≤ ∞. +And it shouldn't matter if we use entities or numeric codes — Ω ≠ π ≡ Ω ≠ π. +

      +======= +

      Isn't the toy course great? — ≤

      +>>>>>>> Stashed changes diff --git a/common/test/data/toy/html/toylab.xml b/common/test/data/toy/html/toylab.xml new file mode 100644 index 0000000000..ab78aeb494 --- /dev/null +++ b/common/test/data/toy/html/toylab.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/common/test/data/toy/vertical/vertical_test.xml b/common/test/data/toy/vertical/vertical_test.xml index c74300f826..72ad1692ba 100644 --- a/common/test/data/toy/vertical/vertical_test.xml +++ b/common/test/data/toy/vertical/vertical_test.xml @@ -4,4 +4,13 @@ Yes No - \ No newline at end of file + + Answer the question + + + + + + From c3ad168b10ebb47af1ca531b3b7fd005af9d34cc Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Thu, 11 Jul 2013 12:12:42 -0400 Subject: [PATCH 699/995] also add some defaulting to the querying of the settings where it defaults to an empty set --- common/djangoapps/util/sandboxing.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/common/djangoapps/util/sandboxing.py b/common/djangoapps/util/sandboxing.py index 7d1c1da06f..2024f8fa27 100644 --- a/common/djangoapps/util/sandboxing.py +++ b/common/djangoapps/util/sandboxing.py @@ -14,7 +14,9 @@ def can_execute_unsafe_code(course_id): """ # To decide if we can run unsafe code, we check the course id against # a list of regexes configured on the server. - for regex in settings.COURSES_WITH_UNSAFE_CODE: + # If this is not defined in the environment variables then default to the most restrictive, which + # is 'no unsafe courses' + for regex in getattr(settings, 'COURSES_WITH_UNSAFE_CODE', []): if re.match(regex, course_id): return True return False From 51c9523f979a0916d3cb89b4c17e765b2ab2a1b6 Mon Sep 17 00:00:00 2001 From: cahrens Date: Thu, 11 Jul 2013 12:36:30 -0400 Subject: [PATCH 700/995] Only send a single variable to index.html about authorship rights. --- cms/templates/index.html | 241 +++++++++++++++++++++++++-------------- 1 file changed, 153 insertions(+), 88 deletions(-) diff --git a/cms/templates/index.html b/cms/templates/index.html index 1df0878bd0..3b7b2d74b4 100644 --- a/cms/templates/index.html +++ b/cms/templates/index.html @@ -34,6 +34,19 @@ +<%block name="jsextra"> + + + <%block name="content">
      @@ -44,12 +57,10 @@

      ${_("Page Actions")}

      - % if user.is_active:
      - - - - - -

      ${_("Welcome, %(name)s!") % dict(name= user.username)}

      + %if len(courses) > 0:

      ${_("Here are all of the courses you currently have access to in Studio:")}

      + + %else: +
      +

      ${_("You currently aren't associated with any Studio Courses.")}

      +
      + %endif
      -
      -
        - %for course, url, lms_link in sorted(courses, key=lambda s: s[0].lower() if s[0] is not None else ''): -
      • - - ${course} - - View Live -
      • - %endfor -
      -
      - - % if not disable_course_creation and course_creator_status != "granted": -
      -

      - ${_('Becoming a Course Author in Studio')} -

      - -
      -
      -

      ${_('edX Studio is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while Course Authorship privileges are granted by edX. Our team will evaluate your request and provide you feedback within 24 hours during the work week.')}

      -
      - - !-- if request is unrequested --> - %if course_creator_status = "unrequested": -
      -

      ${_('Your Authorship Request Status:')}

      - - -
      - - !-- if request is pending --> - %elif course_creator_status = "pending": -
      -

      ${_('Your Authorship Request Status:')}

      - -
      -
      ${_('Your authorship request is:')}
      -
      - - ${_('Pending')} - ${_('Your request is currently being reviewed by edX staff and should be updated shortly.')} -
      -
      -
      - - !-- if request is denied --> - %elif course_creator_status = "denied": -
      -

      ${_('Your Authorship Request Status:')}

      - -
      -
      ${_('Your authorship request is:')}
      -
      - - ${_('Denied')} - ${_('Your request did not meet the criteria/guidelines specified by edX Staff.')} -
      -
      -
      - % endif + %if len(courses) > 0: +
      +
      +
        + %for course, url, lms_link in sorted(courses, key=lambda s: s[0].lower() if s[0] is not None else ''): +
      • + + ${course} + + View Live +
      • + %endfor +
      + + %else: +
      +
      +
      +

      ${_("Are you staff on an existing Studio course?")}

      +
      +

      ${_('You will need to be added to the course in Studio by the course creator. Please get in touch with the course creator or administrator for the specific course you are helping to author.')}

      +
      +
      +
      + + %if course_creator_status == "granted": +
      +
      +

      ${_('Create Your First Course')}

      +
      +

      ${_('Your new course is just a click away!')}

      +
      +
      + + +
      + % endif + +
      % endif + + + %if course_creator_status == "unrequested": +
      +

      + ${_('Becoming a Course Creator in Studio')} +

      + +
      +
      +

      ${_('edX Studio is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by edX. Our team will evaluate your request and provide you feedback within 24 hours during the work week.')}

      +
      + +
      +

      ${_('Your Course Creator Request Status:')}

      + +
      + + + +
      + +
      +
      +
      +
      +
      + + %elif course_creator_status == "denied": +
      +

      + ${_('Your Course Creator Request Status')} +

      + +
      +
      +

      ${_('edX Studio is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by edX. Our team is has completed evaluating your request.')}

      +
      + +
      +

      ${_('Your Course Creator Request Status:')}

      + +
      +
      ${_('Your Course Creator request is:')}
      +
      + + ${_('Denied')} + ${_('Your request did not meet the criteria/guidelines specified by edX Staff.')} +
      +
      +
      +
      +
      + + %elif course_creator_status == "pending": +
      +

      + ${_('Your Course Creator Request Status')} +

      + +
      +
      +

      ${_('edX Studio is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by edX. Our team is currently evaluating your request.')}

      +
      + +
      +

      ${_('Your Course Creator Request Status:')}

      + +
      +
      ${_('Your Course Creator request is:')}
      +
      + + ${_('Pending')} + ${_('Your request is currently being reviewed by edX staff and should be updated shortly.')} +
      +
      +
      +
      +
      + % endif +
      - - % if disable_course_creation and settings.MITX_FEATURES.get('STAFF_EMAIL',''): + % if course_creator_status=='disallowed_for_this_site' and settings.MITX_FEATURES.get('STAFF_EMAIL',''):

      ${_('Can I create courses in Studio?')}

      ${_('In order to create courses in Studio, you must')} ${_("contact edX staff to help you create a course")}

      % endif - - % if not disable_course_creation and course_creator_status = "unrequested": + % if course_creator_status == "unrequested":

      ${_('Can I create courses in Studio?')}

      -

      ${_('In order to create courses in Studio, you must have authorship rights to create your own course.')}

      +

      ${_('In order to create courses in Studio, you must have course creator privileges to create your own course.')}

      - - % elif not disable_course_creation and course_creator_status = "denied": + % elif course_creator_status == "denied":

      ${_('Can I create courses in Studio?')}

      ${_('Your request to author courses in studio has been denied. Please')} ${_('contact edX Staff with further questions')}

      @@ -192,7 +258,6 @@ - % else:
      @@ -204,7 +269,7 @@

      ${_('We need to verify your email address')}

      -

      ${_('Almost there! In order to complete your sign up we need you verify your $emailaddress email address. An activation message and next steps should be waiting for you there.')}

      +

      ${_('Almost there! In order to complete your sign up we need you verify your email address (%(email)s). An activation message and next steps should be waiting for you there.') % dict(email=user.email)}

      From 3715d6c429865f4ef6e869daff1a10f7b9d12fe8 Mon Sep 17 00:00:00 2001 From: cahrens Date: Thu, 11 Jul 2013 12:39:51 -0400 Subject: [PATCH 701/995] Only send a single variable to index.html about authorship rights. --- cms/djangoapps/contentstore/views/user.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/views/user.py b/cms/djangoapps/contentstore/views/user.py index dae0d246a5..dbb2fbddb6 100644 --- a/cms/djangoapps/contentstore/views/user.py +++ b/cms/djangoapps/contentstore/views/user.py @@ -11,6 +11,7 @@ from contentstore.utils import get_url_reverse, get_lms_link_for_item from util.json_request import expect_json, JsonResponse from auth.authz import STAFF_ROLE_NAME, INSTRUCTOR_ROLE_NAME, get_users_in_course_group_by_role from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group +from course_creators.views import get_course_creator_status, add_user_with_status_unrequested from .access import has_access @@ -32,6 +33,18 @@ def index(request): and course.location.name != '') courses = filter(course_filter, courses) + if settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False): + course_creator_status = 'granted' if request.user.is_staff else 'disallowed_for_this_site' + elif settings.MITX_FEATURES.get('ENABLE_CREATOR_GROUP', False): + course_creator_status = get_course_creator_status(request.user) + if course_creator_status is None: + # User not grandfathered in as an existing user, has not previously visited the dashboard page. + # Add the user to the course creator admin table with status 'unrequested'. + add_user_with_status_unrequested(request.user) + course_creator_status = get_course_creator_status(request.user) + else: + course_creator_status = 'granted' + return render_to_response('index.html', { 'new_course_template': Location('i4x', 'edx', 'templates', 'course', 'Empty'), 'courses': [(course.display_name, @@ -39,7 +52,7 @@ def index(request): get_lms_link_for_item(course.location, course_id=course.location.course_id)) for course in courses], 'user': request.user, - 'disable_course_creation': settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff + 'course_creator_status': course_creator_status }) From 59850cb4a3a24e6289222926628de7f4d751856c Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Thu, 11 Jul 2013 12:44:13 -0400 Subject: [PATCH 702/995] Studio: cleans up new course form and course listings display --- cms/static/js/base.js | 6 ++-- cms/static/sass/views/_dashboard.scss | 43 ++++++++++++++++----------- cms/templates/index.html | 25 ++++++++-------- 3 files changed, 41 insertions(+), 33 deletions(-) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index c2f401c77e..a6218541ef 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -577,10 +577,10 @@ function cancelNewSection(e) { function addNewCourse(e) { e.preventDefault(); - $(e.target).hide(); + $(e.target).addClass('disabled'); var $newCourse = $($('#new-course-template').html()); var $cancelButton = $newCourse.find('.new-course-cancel'); - $('.inner-wrapper').prepend($newCourse); + $('.courses').prepend($newCourse); $newCourse.find('.new-course-name').focus().select(); $newCourse.find('form').bind('submit', saveNewCourse); $cancelButton.bind('click', cancelNewCourse); @@ -627,7 +627,7 @@ function saveNewCourse(e) { function cancelNewCourse(e) { e.preventDefault(); - $('.new-course-button').show(); + $('.new-course-button').removeClass('disabled'); $(this).parents('section.new-course').remove(); } diff --git a/cms/static/sass/views/_dashboard.scss b/cms/static/sass/views/_dashboard.scss index b00306e5c5..a2a3c72fb1 100644 --- a/cms/static/sass/views/_dashboard.scss +++ b/cms/static/sass/views/_dashboard.scss @@ -203,7 +203,7 @@ body.dashboard { @extend .t-copy-sub1; position: relative; color: $white; - opacity: 0.65; + opacity: 0.85; } } @@ -249,20 +249,21 @@ body.dashboard { // ==================== - .my-classes { + // course listings + .courses { margin: $baseline 0; } - .class-list { + .list-courses { margin-top: $baseline; border-radius: 3px; - border: 1px solid $gray-d2; + border: 1px solid $gray; background: $white; - box-shadow: 0 1px 2px rgba(0, 0, 0, .1); + box-shadow: 0 1px 2px $shadow-l1; - li { + .course-item { position: relative; - border-bottom: 1px solid $mediumGrey; + border-bottom: 1px solid $gray-l1; &:last-child { border-bottom: none; @@ -302,7 +303,7 @@ body.dashboard { .view-live-button { z-index: 10000; position: absolute; - top: 15px; + top: ($baseline*0.75); right: $baseline; padding: ($baseline/4) ($baseline/2); opacity: 0.0; @@ -316,17 +317,25 @@ body.dashboard { } .new-course { - padding: 15px 25px; - margin-top: 20px; + @include clearfix(); + padding: ($baseline*0.75) ($baseline*1.25); + margin-top: $baseline; border-radius: 3px; - border: 1px solid $darkGrey; - background: #fff; + border: 1px solid $gray; + background: $white; box-shadow: 0 1px 2px rgba(0, 0, 0, .1); - @include clearfix; + + .title { + @extend .t-title4; + font-weight: 600; + margin-bottom: ($baseline/2); + border-bottom: 1px solid $gray-l3; + padding-bottom: ($baseline/2); + } .row { - margin-bottom: 15px; - @include clearfix; + @include clearfix(); + margin-bottom: ($baseline*0.75); } .column { @@ -343,8 +352,8 @@ body.dashboard { } label { + @extend .t-title7; display: block; - font-size: 13px; font-weight: 700; } @@ -355,7 +364,7 @@ body.dashboard { } .new-course-name { - font-size: 19px; + @extend .t-title5; font-weight: 300; } diff --git a/cms/templates/index.html b/cms/templates/index.html index 3b7b2d74b4..74831d1984 100644 --- a/cms/templates/index.html +++ b/cms/templates/index.html @@ -8,6 +8,7 @@ <%block name="header_extras"> + + <%block name="header_extras"> @@ -151,10 +158,8 @@

      ${_('Your Course Creator Request Status:')}

      -
      - - - + +
      diff --git a/cms/urls.py b/cms/urls.py index 56efd1a557..67314178f8 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -12,6 +12,7 @@ admin.autodiscover() urlpatterns = ('', # nopep8 url(r'^$', 'contentstore.views.howitworks', name='homepage'), url(r'^listing', 'contentstore.views.index', name='index'), + url(r'^request_course_creator$', 'contentstore.views.request_course_creator', name='request_course_creator'), url(r'^edit/(?P.*?)$', 'contentstore.views.edit_unit', name='edit_unit'), url(r'^subsection/(?P.*?)$', 'contentstore.views.edit_subsection', name='edit_subsection'), url(r'^preview_component/(?P.*?)$', 'contentstore.views.preview_component', name='preview_component'), From fbe2cde6e53bfa773bcdfde08b393ee53a8d8164 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Thu, 11 Jul 2013 14:31:53 -0400 Subject: [PATCH 707/995] add simple unit test on the defaulting of the settings --- common/djangoapps/util/tests/test_sandboxing.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/common/djangoapps/util/tests/test_sandboxing.py b/common/djangoapps/util/tests/test_sandboxing.py index 4bccac707f..c76132696a 100644 --- a/common/djangoapps/util/tests/test_sandboxing.py +++ b/common/djangoapps/util/tests/test_sandboxing.py @@ -25,3 +25,10 @@ class SandboxingTest(TestCase): """ self.assertTrue(can_execute_unsafe_code('edX/full/2012_Fall')) self.assertTrue(can_execute_unsafe_code('edX/full/2013_Spring')) + + def test_courses_with_unsafe_code_default(self): + """ + Test that the default setting for COURSES_WITH_UNSAFE_CODE is an empty setting, e.g. we don't use @override_settings in these tests + """ + self.assertFalse(can_execute_unsafe_code('edX/full/2012_Fall')) + self.assertFalse(can_execute_unsafe_code('edX/full/2013_Spring')) From 5d300bf36f739db037666d62eed9e695f8ea6d31 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 11 Jul 2013 14:34:41 -0400 Subject: [PATCH 708/995] Fixed the broken unit tests from deleting full --- .../contentstore/tests/test_contentstore.py | 7 ------- common/test/data/toy/chapter/html_container.xml | 1 - common/test/data/toy/course/2012_Fall.xml | 1 - common/test/data/toy/html/toylab.html | 13 ------------- common/test/data/toy/html/toylab.xml | 1 - common/test/data/toy/vertical/vertical_test.xml | 9 --------- 6 files changed, 32 deletions(-) delete mode 100644 common/test/data/toy/chapter/html_container.xml delete mode 100644 common/test/data/toy/html/toylab.html delete mode 100644 common/test/data/toy/html/toylab.xml diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index b43ea46d7d..be122fa1a4 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -359,13 +359,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): direct_store = modulestore('direct') import_from_xml(direct_store, 'common/test/data/', ['toy']) - html_module_location = Location(['i4x', 'edX', 'toy', 'html', 'toylab', None]) - - url = reverse('preview_component', kwargs={'location': html_module_location.url()}) - - resp = self.client.get(url) - self.assertEqual(resp.status_code, 200) - # also try a custom response which will trigger the 'is this course in whitelist' logic problem_module_location = Location(['i4x', 'edX', 'toy', 'vertical', 'vertical_test', None]) url = reverse('preview_component', kwargs={'location': problem_module_location.url()}) diff --git a/common/test/data/toy/chapter/html_container.xml b/common/test/data/toy/chapter/html_container.xml deleted file mode 100644 index 5691607f3b..0000000000 --- a/common/test/data/toy/chapter/html_container.xml +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/common/test/data/toy/course/2012_Fall.xml b/common/test/data/toy/course/2012_Fall.xml index b96f04042f..ded9713f17 100644 --- a/common/test/data/toy/course/2012_Fall.xml +++ b/common/test/data/toy/course/2012_Fall.xml @@ -13,5 +13,4 @@ - diff --git a/common/test/data/toy/html/toylab.html b/common/test/data/toy/html/toylab.html deleted file mode 100644 index b2a4599cc6..0000000000 --- a/common/test/data/toy/html/toylab.html +++ /dev/null @@ -1,13 +0,0 @@ -Lab 2A: Superposition Experiment - -<<<<<<< Updated upstream -

      Isn't the toy course great?

      - -

      Let's add some markup that uses non-ascii characters. -For example, we should be able to write words like encyclopædia, or foreign words like français. -Looking beyond latin-1, we should handle math symbols: πr² ≤ ∞. -And it shouldn't matter if we use entities or numeric codes — Ω ≠ π ≡ Ω ≠ π. -

      -======= -

      Isn't the toy course great? — ≤

      ->>>>>>> Stashed changes diff --git a/common/test/data/toy/html/toylab.xml b/common/test/data/toy/html/toylab.xml deleted file mode 100644 index ab78aeb494..0000000000 --- a/common/test/data/toy/html/toylab.xml +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/common/test/data/toy/vertical/vertical_test.xml b/common/test/data/toy/vertical/vertical_test.xml index 72ad1692ba..e801a4ac86 100644 --- a/common/test/data/toy/vertical/vertical_test.xml +++ b/common/test/data/toy/vertical/vertical_test.xml @@ -4,13 +4,4 @@ Yes No - - Answer the question - - - - - From b4457371a86713051a3c3c72669d3c1e28bc47e7 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Thu, 11 Jul 2013 14:53:02 -0400 Subject: [PATCH 709/995] Studio: revises and syncs up activation message UI with dashboard UI --- cms/templates/activation_active.html | 35 +++++++++++++++------ cms/templates/activation_complete.html | 35 ++++++++++++++++----- cms/templates/activation_invalid.html | 43 +++++++++++++++++++------- 3 files changed, 85 insertions(+), 28 deletions(-) diff --git a/cms/templates/activation_active.html b/cms/templates/activation_active.html index 712c73abf9..51c01f1155 100644 --- a/cms/templates/activation_active.html +++ b/cms/templates/activation_active.html @@ -1,14 +1,31 @@ <%inherit file="base.html" /> <%block name="content"> - -
      -
      - -
      -

      Account already active!

      -

      This account has already been activated. Log in here.

      +
      +
      +

      ${_("Studio Account Activation")}

      +
      -
      - \ No newline at end of file +
      +
      +
      +
      + +
      +
      +

      ${_("Your account is already active")}

      +
      +

      ${_("This account, set up using (%(email)s), has already been activated. Please sign in to start working within edX Studio.") % dict(email=user.email)}

      +
      +
      + + +
      +
      +
      + diff --git a/cms/templates/activation_complete.html b/cms/templates/activation_complete.html index 1e195a632c..d6bdab36dc 100644 --- a/cms/templates/activation_complete.html +++ b/cms/templates/activation_complete.html @@ -1,12 +1,33 @@ <%inherit file="base.html" /> <%block name="content"> - -
      -
      -

      Activation Complete!

      -

      Thanks for activating your account. Log in here.

      +
      +
      +

      ${_("Studio Account Activation")}

      +
      -
      - \ No newline at end of file +
      +
      +
      +
      + +
      +
      +

      ${_("Your account activation is complete!")}

      +
      +

      ${_("Thank you for activating your account. You may now sign in and start using edX Studio to author courses.")}

      +
      +
      + + +
      +
      +
      + + + diff --git a/cms/templates/activation_invalid.html b/cms/templates/activation_invalid.html index c4eb16875b..0c663aa307 100644 --- a/cms/templates/activation_invalid.html +++ b/cms/templates/activation_invalid.html @@ -1,16 +1,35 @@ <%inherit file="base.html" /> <%block name="content"> -
      -
      -

      Activation Invalid

      - -

      Something went wrong. Check to make sure the URL you went to was - correct -- e-mail programs will sometimes split it into two - lines. If you still have issues, e-mail us to let us know what happened - at bugs@mitx.mit.edu.

      - -

      Or you can go back to the home page.

      +
      +
      +

      ${_("Studio Account Activation")}

      +
      -
      - \ No newline at end of file + +
      +
      +
      +
      + +
      +
      +

      ${_('Your account activation is invalid')}

      +
      +

      ${_("We're sorry. Something went wrong with your activation. Check to make sure the URL you went to was correct — e-mail programs will sometimes split it into two lines.")}

      +

      ${_("If you still have issues, contact edX Support. In the meatime, you can also return to")} {_('the Studio homepage.')}

      +
      +
      + + +
      +
      +
      + + + + From c2f0b7d4e9d06cff8a748282abdcf867d57c2f91 Mon Sep 17 00:00:00 2001 From: Peter Fogg Date: Tue, 2 Jul 2013 11:46:26 -0400 Subject: [PATCH 710/995] Fix validation in many cases, and ensure that the notification bar pops up correctly. --- .../models/settings/course_grading_policy.js | 10 +++--- cms/static/js/views/settings/advanced_view.js | 7 ++-- .../js/views/settings/main_settings_view.js | 22 ++++++++---- .../views/settings/settings_grading_view.js | 25 +++++++------- cms/static/js/views/validating_view.js | 34 ++++++++++++++----- 5 files changed, 62 insertions(+), 36 deletions(-) diff --git a/cms/static/js/models/settings/course_grading_policy.js b/cms/static/js/models/settings/course_grading_policy.js index 8162145003..c4cc326e92 100644 --- a/cms/static/js/models/settings/course_grading_policy.js +++ b/cms/static/js/models/settings/course_grading_policy.js @@ -71,7 +71,7 @@ CMS.Models.Settings.CourseGrader = Backbone.Model.extend({ }, validate : function(attrs) { var errors = {}; - if (attrs['type']) { + if (_.has(attrs, 'type')) { if (_.isEmpty(attrs['type'])) { errors.type = "The assignment type must have a name."; } @@ -83,7 +83,7 @@ CMS.Models.Settings.CourseGrader = Backbone.Model.extend({ } } } - if (attrs['weight']) { + if (_.has(attrs, 'weight')) { if (!isFinite(attrs.weight) || /\D+/.test(attrs.weight)) { errors.weight = "Please enter an integer between 0 and 100."; } @@ -97,19 +97,19 @@ CMS.Models.Settings.CourseGrader = Backbone.Model.extend({ // errors.weight = "The weights cannot add to more than 100."; } }} - if (attrs['min_count']) { + if (_.has(attrs, 'min_count')) { if (!isFinite(attrs.min_count) || /\D+/.test(attrs.min_count)) { errors.min_count = "Please enter an integer."; } else attrs.min_count = parseInt(attrs.min_count); } - if (attrs['drop_count']) { + if (_.has(attrs, 'drop_count')) { if (!isFinite(attrs.drop_count) || /\D+/.test(attrs.drop_count)) { errors.drop_count = "Please enter an integer."; } else attrs.drop_count = parseInt(attrs.drop_count); } - if (attrs['min_count'] && attrs['drop_count'] && attrs.drop_count > attrs.min_count) { + if (_.has(attrs, 'min_count') && _.has(attrs, 'drop_count') && attrs.drop_count > attrs.min_count) { errors.drop_count = "Cannot drop more " + attrs.type + " than will assigned."; } if (!_.isEmpty(errors)) return errors; diff --git a/cms/static/js/views/settings/advanced_view.js b/cms/static/js/views/settings/advanced_view.js index 961d9d010b..90e84adf2b 100644 --- a/cms/static/js/views/settings/advanced_view.js +++ b/cms/static/js/views/settings/advanced_view.js @@ -56,6 +56,7 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ CodeMirror.fromTextArea(textarea, { mode: "application/json", lineNumbers: false, lineWrapping: false, onChange: function(instance, changeobj) { + instance.save() // this event's being called even when there's no change :-( if (instance.getValue() !== oldValue) { 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."); @@ -94,8 +95,7 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ } } if (JSONValue !== undefined) { - self.clearValidationErrors(); - self.model.set(key, JSONValue, {validate: true}); + self.model.set(key, JSONValue); } } }); @@ -115,7 +115,8 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ analytics.track('Saved Advanced Settings', { 'course': course_location_analytics }); - } + }, + silent: true }); }, revertView: function() { diff --git a/cms/static/js/views/settings/main_settings_view.js b/cms/static/js/views/settings/main_settings_view.js index 27194dba3b..c378ba2eaf 100644 --- a/cms/static/js/views/settings/main_settings_view.js +++ b/cms/static/js/views/settings/main_settings_view.js @@ -89,7 +89,6 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({ var timefield = $(div).find("input:.time"); var cachethis = this; var setfield = function () { - cachethis.clearValidationErrors(); var date = datefield.datepicker('getDate'); if (date) { var time = timefield.timepicker("getSecondsFromMidnight"); @@ -98,14 +97,16 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({ } var newVal = new Date(date.getTime() + time * 1000); if (!cacheModel.has(fieldName) || cacheModel.get(fieldName).getTime() !== newVal.getTime()) { - cacheModel.set(fieldName, newVal, {validate: true}); + cachethis.clearValidationErrors(); + cachethis.setAndValidate(fieldName, newVal); } } else { // Clear date (note that this clears the time as well, as date and time are linked). // Note also that the validation logic prevents us from clearing the start date // (start date is required by the back end). - cacheModel.set(fieldName, null, {validate: true}); + cachethis.clearValidationErrors(); + cachethis.setAndValidate(fieldName, null); } }; @@ -142,14 +143,13 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({ default: // Everything else is handled by datepickers and CodeMirror. break; } - var self = this; this.showNotificationBar(this.save_message, _.bind(this.saveView, this), _.bind(this.revertView, this)); }, removeSyllabus: function() { - if (this.model.has('syllabus')) this.model.set({'syllabus': null}); + if (this.model.has('syllabus')) this.setAndValidate('syllabus', null); }, assetSyllabus : function() { @@ -184,7 +184,7 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({ cachethis.clearValidationErrors(); var newVal = mirror.getValue(); if (cachethis.model.get(field) != newVal) { - cachethis.model.set(field, newVal); + cachethis.setAndValidate(field, newVal); cachethis.showNotificationBar(cachethis.save_message, _.bind(cachethis.saveView, cachethis), _.bind(cachethis.revertView, cachethis)); @@ -209,6 +209,16 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({ }); }, reset: true}); + }, + setAndValidate: function(attr, value) { + // If we call model.set() with {validate: true}, model fields + // will not be set if validation fails. This puts the UI and + // the model in an inconsistent state, and causes us to not + // see the right validation errors the next time validate() is + // called on the model. So we set *without* validating, then + // call validate ourselves. + this.model.set(attr, value); + this.model.isValid(); } }); diff --git a/cms/static/js/views/settings/settings_grading_view.js b/cms/static/js/views/settings/settings_grading_view.js index e5e5c52086..7329140fa6 100644 --- a/cms/static/js/views/settings/settings_grading_view.js +++ b/cms/static/js/views/settings/settings_grading_view.js @@ -38,6 +38,7 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({ } ); this.listenTo(this.model, 'invalid', this.handleValidationError); + this.listenTo(this.model, 'change', this.showNotificationBar); this.model.get('graders').on('reset', this.render, this); this.model.get('graders').on('add', this.render, this); this.selectorToField = _.invert(this.fieldToSelectorMap); @@ -53,14 +54,18 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({ // Undo the double invocation error. At some point, fix the double invocation $(gradelist).empty(); var gradeCollection = this.model.get('graders'); - // We need to bind the 'remove' event here (rather than in + // We need to bind these events here (rather than in // initialize), or else we can only press the delete button // once due to the graders collection changing when we cancel // our changes. - gradeCollection.on('remove', function() { - this.showNotificationBar(); - this.render(); - }, this); + _.each(['change', 'remove', 'add'], + function (event) { + gradeCollection.on(event, function() { + this.showNotificationBar(); + this.render(); + }, this); + }, + this); gradeCollection.each(function(gradeModel) { $(gradelist).append(self.template({model : gradeModel })); var newEle = gradelist.children().last(); @@ -69,9 +74,6 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({ // Listen in order to rerender when the 'cancel' button is // pressed self.listenTo(newView, 'revert', _.bind(self.render, self)); - self.listenTo(gradeModel, 'change', function() { - self.showNotificationBar(); - }); }); // render the grade cutoffs @@ -89,7 +91,6 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({ addAssignmentType : function(e) { e.preventDefault(); this.model.get('graders').push({}); - this.showNotificationBar(); }, fieldToSelectorMap : { 'grace_period' : 'course-grading-graceperiod' @@ -99,7 +100,6 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({ self.clearValidationErrors(); var newVal = self.model.dateToGracePeriod($(event.currentTarget).timepicker('getTime')); self.model.set('grace_period', newVal, {validate: true}); - self.showNotificationBar(); }, updateModel : function(event) { if (!this.selectorToField[event.currentTarget.id]) return; @@ -112,7 +112,6 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({ this.setField(event); break; } - this.showNotificationBar(); }, // Grade sliders attributes and methods @@ -238,7 +237,6 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({ }, {}), {validate: true}); - this.showNotificationBar(); }, addNewGrade: function(e) { @@ -333,7 +331,8 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({ self.render(); self.renderCutoffBar(); }, - reset: true}); + reset: true, + silent: true}); }, showNotificationBar: function() { // We always call showNotificationBar with the same args, just diff --git a/cms/static/js/views/validating_view.js b/cms/static/js/views/validating_view.js index ede4a0b123..bd47d07808 100644 --- a/cms/static/js/views/validating_view.js +++ b/cms/static/js/views/validating_view.js @@ -45,7 +45,8 @@ CMS.Views.ValidatingView = Backbone.View.extend({ this.clearValidationErrors(); var field = this.selectorToField[event.currentTarget.id]; var newVal = $(event.currentTarget).val(); - this.model.set(field, newVal, {validate: true}); + this.model.set(field, newVal); + this.model.isValid(); return newVal; }, // these should perhaps go into a superclass but lack of event hash inheritance demotivates me @@ -68,9 +69,18 @@ CMS.Views.ValidatingView = Backbone.View.extend({ }, showNotificationBar: function(message, primaryClick, secondaryClick) { + // Show a notification with message. primaryClick is called on + // pressing the save button, and secondaryClick (if it's + // passed, which it may not be) will be called on + // cancel. Takes care of hiding the notification bar at the + // appropriate times. if(this.notificationBarShowing) { return; } + // If we've already saved something, hide the alert. + if(this.saved) { + this.saved.hide(); + } var self = this; this.confirmation = new CMS.Views.Notification.Warning({ title: gettext("You've made some changes"), @@ -93,10 +103,6 @@ CMS.Views.ValidatingView = Backbone.View.extend({ secondaryClick(); } self.model.clear({silent : true}); - /*self.model.fetch({ - success : function() { self.render(); }, - reset: true - });*/ self.confirmation.hide(); self.notificationBarShowing = false; } @@ -114,13 +120,23 @@ CMS.Views.ValidatingView = Backbone.View.extend({ closeIcon: false }); this.saved.show(); + $.smoothScroll({ + offset: 0, + easing: 'swing', + speed: 1000 + }); }, saveView: function() { var self = this; - this.model.save({}, - {success: function() { - self.showSavedBar(); - }}); + this.model.save( + {}, + { + success: function() { + self.showSavedBar(); + }, + silent: true + } + ); } }); From 6e949604885c0a2e6a9f8ec7f22e62ef16d1b680 Mon Sep 17 00:00:00 2001 From: Peter Fogg Date: Tue, 2 Jul 2013 16:50:06 -0400 Subject: [PATCH 711/995] Fix many validation bugs and failing tests. --- .../contentstore/features/common.py | 9 ++- .../features/course-settings.feature | 6 ++ .../contentstore/features/grading.feature | 7 ++ .../contentstore/features/grading.py | 4 - .../coffee/spec/views/feedback_spec.coffee | 6 +- .../models/settings/course_grading_policy.js | 7 +- cms/static/js/views/feedback.js | 16 +++- .../js/views/settings/main_settings_view.js | 76 +++++++++---------- .../views/settings/settings_grading_view.js | 24 ++++-- cms/static/js/views/validating_view.js | 17 ++++- .../sass/elements/_system-feedback.scss | 10 +-- common/static/sass/_mixins-inherited.scss | 12 +-- 12 files changed, 123 insertions(+), 71 deletions(-) diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index 37daebabd9..5a8e02f168 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -68,7 +68,7 @@ def press_the_notification_button(_step, name): error_showing = world.is_css_present('.is-shown.wrapper-notification-error') return confirmation_dismissed or error_showing - assert_true(world.css_click(css, success_condition=button_clicked), '%s button not clicked after 5 attempts.' % name) + world.css_click(css, success_condition=button_clicked), '%s button not clicked after 5 attempts.' % name @step('I change the "(.*)" field to "(.*)"$') @@ -228,6 +228,13 @@ def shows_captions(step, show_captions): assert world.is_css_not_present('.video.closed') +@step('the save button is disabled$') +def save_button_disabled(step): + button_css = '.action-save' + disabled = 'is-disabled' + assert world.css_find(button_css)[0].has_class(disabled) + + def type_in_codemirror(index, text): world.css_click(".CodeMirror", index=index) g = world.css_find("div.CodeMirror.CodeMirror-focused > div > textarea") diff --git a/cms/djangoapps/contentstore/features/course-settings.feature b/cms/djangoapps/contentstore/features/course-settings.feature index cafedd6d97..5c79dc7ee3 100644 --- a/cms/djangoapps/contentstore/features/course-settings.feature +++ b/cms/djangoapps/contentstore/features/course-settings.feature @@ -65,3 +65,9 @@ Feature: Course Settings And I change the course overview And I press the "Save" notification button Then I see a confirmation that my changes have been saved + + Scenario: User cannot save invalid settings + Given I have opened a new course in Studio + When I select Schedule and Details + And I change the "Course Start Date" field to "" + Then the save button is disabled diff --git a/cms/djangoapps/contentstore/features/grading.feature b/cms/djangoapps/contentstore/features/grading.feature index 303cf5db9d..b01d762d73 100644 --- a/cms/djangoapps/contentstore/features/grading.feature +++ b/cms/djangoapps/contentstore/features/grading.feature @@ -77,3 +77,10 @@ Feature: Course Grading When I change assignment type "Homework" to "New Type" And I press the "Save" notification button Then I see a confirmation that my changes have been saved + + Scenario: User cannot save invalid settings + Given I have opened a new course in Studio + And I have populated the course + And I am viewing the grading settings + When I change assignment type "Homework" to "" + Then the save button is disabled diff --git a/cms/djangoapps/contentstore/features/grading.py b/cms/djangoapps/contentstore/features/grading.py index 504dcf7ef3..e75d8f23ad 100644 --- a/cms/djangoapps/contentstore/features/grading.py +++ b/cms/djangoapps/contentstore/features/grading.py @@ -60,8 +60,6 @@ def change_assignment_name(step, old_name, new_name): for count in range(len(old_name)): f._element.send_keys(Keys.END, Keys.BACK_SPACE) f._element.send_keys(new_name) - # Without this, the "you've made changes" notification won't pop up - f._element.send_keys(Keys.ENTER) @step(u'I go back to the main course page') @@ -94,8 +92,6 @@ def add_assignment_type(step, new_name): name_id = '#course-grading-assignment-name' f = world.css_find(name_id)[4] f._element.send_keys(new_name) - # Without this, the "you've made changes" notification won't pop up - f._element.send_keys(Keys.ENTER) @step(u'I have populated the course') diff --git a/cms/static/coffee/spec/views/feedback_spec.coffee b/cms/static/coffee/spec/views/feedback_spec.coffee index 059dd48ef7..1925f890e0 100644 --- a/cms/static/coffee/spec/views/feedback_spec.coffee +++ b/cms/static/coffee/spec/views/feedback_spec.coffee @@ -37,6 +37,10 @@ describe "CMS.Views.SystemFeedback", -> @renderSpy = spyOn(CMS.Views.Alert.Confirmation.prototype, 'render').andCallThrough() @showSpy = spyOn(CMS.Views.Alert.Confirmation.prototype, 'show').andCallThrough() @hideSpy = spyOn(CMS.Views.Alert.Confirmation.prototype, 'hide').andCallThrough() + @clock = sinon.useFakeTimers() + + afterEach -> + @clock.restore() it "requires a type and an intent", -> neither = => @@ -80,8 +84,8 @@ describe "CMS.Views.SystemFeedback", -> it "close button sends a .hide() message", -> view = new CMS.Views.Alert.Confirmation(@options).show() view.$(".action-close").click() - expect(@hideSpy).toHaveBeenCalled() + @clock.tick(900) expect(view.$('.wrapper')).toBeHiding() describe "CMS.Views.Prompt", -> diff --git a/cms/static/js/models/settings/course_grading_policy.js b/cms/static/js/models/settings/course_grading_policy.js index c4cc326e92..3014b39e82 100644 --- a/cms/static/js/models/settings/course_grading_policy.js +++ b/cms/static/js/models/settings/course_grading_policy.js @@ -77,18 +77,19 @@ CMS.Models.Settings.CourseGrader = Backbone.Model.extend({ } else { // FIXME somehow this.collection is unbound sometimes. I can't track down when - var existing = this.collection && this.collection.some(function(other) { return (other != this) && (other.get('type') == attrs['type']);}, this); + var existing = this.collection && this.collection.some(function(other) { return (other.cid != this.cid) && (other.get('type') == attrs['type']);}, this); if (existing) { errors.type = "There's already another assignment type with this name."; } } } if (_.has(attrs, 'weight')) { - if (!isFinite(attrs.weight) || /\D+/.test(attrs.weight)) { + var intWeight = parseInt(attrs.weight); // see if this ensures value saved is int + if (!isFinite(intWeight) || /\D+/.test(attrs.weight) || intWeight < 0 || intWeight > 100) { errors.weight = "Please enter an integer between 0 and 100."; } else { - attrs.weight = parseInt(attrs.weight); // see if this ensures value saved is int + attrs.weight = intWeight; if (this.collection && attrs.weight > 0) { // FIXME b/c saves don't update the models if validation fails, we should // either revert the field value to the one in the model and make them make room diff --git a/cms/static/js/views/feedback.js b/cms/static/js/views/feedback.js index 5b884ad88d..4595a6cab4 100644 --- a/cms/static/js/views/feedback.js +++ b/cms/static/js/views/feedback.js @@ -140,7 +140,21 @@ CMS.Views.SystemFeedback = Backbone.View.extend({ CMS.Views.Alert = CMS.Views.SystemFeedback.extend({ options: $.extend({}, CMS.Views.SystemFeedback.prototype.options, { type: "alert" - }) + }), + slide_speed: 900, + show: function() { + CMS.Views.SystemFeedback.prototype.show.apply(this, arguments); + this.$el.hide(); + this.$el.slideDown(this.slide_speed); + return this; + }, + hide: function () { + this.$el.slideUp({ + duration: this.slide_speed + }); + setTimeout(_.bind(CMS.Views.SystemFeedback.prototype.hide, this, arguments), + this.slideSpeed); + } }); CMS.Views.Notification = CMS.Views.SystemFeedback.extend({ options: $.extend({}, CMS.Views.SystemFeedback.prototype.options, { diff --git a/cms/static/js/views/settings/main_settings_view.js b/cms/static/js/views/settings/main_settings_view.js index c378ba2eaf..5550c550c0 100644 --- a/cms/static/js/views/settings/main_settings_view.js +++ b/cms/static/js/views/settings/main_settings_view.js @@ -3,10 +3,11 @@ if (!CMS.Views['Settings']) CMS.Views.Settings = {}; CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({ // Model class is CMS.Models.Settings.CourseDetails events : { + "input input" : "updateModel", + "input textarea" : "updateModel", + // Leaving change in as fallback for older browsers "change input" : "updateModel", "change textarea" : "updateModel", - 'click .remove-course-syllabus' : "removeSyllabus", - 'click .new-course-syllabus' : 'assetSyllabus', 'click .remove-course-introduction-video' : "removeVideo", 'focus #course-overview' : "codeMirrorize", 'mouseover #timezone' : "updateTime", @@ -28,6 +29,7 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({ this.$el.find('#timezone').html("(" + dateIntrospect.getTimezone() + ")"); this.listenTo(this.model, 'invalid', this.handleValidationError); + this.listenTo(this.model, 'change', this.showNotificationBar); this.selectorToField = _.invert(this.fieldToSelectorMap); }, @@ -37,25 +39,13 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({ this.setupDatePicker('enrollment_start'); this.setupDatePicker('enrollment_end'); - if (this.model.has('syllabus')) { - this.$el.find(this.fieldToSelectorMap['syllabus']).html( - this.fileAnchorTemplate({ - fullpath : this.model.get('syllabus'), - filename: 'syllabus'})); - this.$el.find('.remove-course-syllabus').show(); - } - else { - this.$el.find('#' + this.fieldToSelectorMap['syllabus']).html(""); - this.$el.find('.remove-course-syllabus').hide(); - } - this.$el.find('#' + this.fieldToSelectorMap['overview']).val(this.model.get('overview')); this.codeMirrorize(null, $('#course-overview')[0]); this.$el.find('.current-course-introduction-video iframe').attr('src', this.model.videosourceSample()); + this.$el.find('#' + this.fieldToSelectorMap['intro_video']).val(this.model.get('intro_video') || ''); if (this.model.has('intro_video')) { this.$el.find('.remove-course-introduction-video').show(); - this.$el.find('#' + this.fieldToSelectorMap['intro_video']).val(this.model.get('intro_video')); } else this.$el.find('.remove-course-introduction-video').hide(); @@ -68,7 +58,6 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({ 'end_date' : 'course-end', 'enrollment_start' : 'enrollment-start', 'enrollment_end' : 'enrollment-end', - 'syllabus' : '.current-course-syllabus .doc-filename', 'overview' : 'course-overview', 'intro_video' : 'course-introduction-video', 'effort' : "course-effort" @@ -120,7 +109,13 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({ timefield.on('changeTime', setfield); datefield.datepicker('setDate', this.model.get(fieldName)); - if (this.model.has(fieldName)) timefield.timepicker('setTime', this.model.get(fieldName)); + // timepicker doesn't let us set null, so check that we have a time + if (this.model.has(fieldName)) { + timefield.timepicker('setTime', this.model.get(fieldName)); + } // but reset the field either way + else { + timefield.val(''); + } }, updateModel: function(event) { @@ -129,34 +124,28 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({ this.setField(event); break; // Don't make the user reload the page to check the Youtube ID. + // Wait for a second to load the video, avoiding egregious AJAX calls. case 'course-introduction-video': this.clearValidationErrors(); var previewsource = this.model.set_videosource($(event.currentTarget).val()); - this.$el.find(".current-course-introduction-video iframe").attr("src", previewsource); - if (this.model.has('intro_video')) { - this.$el.find('.remove-course-introduction-video').show(); - } - else { - this.$el.find('.remove-course-introduction-video').hide(); - } + clearTimeout(this.videoTimer); + this.videoTimer = setTimeout(_.bind(function() { + this.$el.find(".current-course-introduction-video iframe").attr("src", previewsource); + if (this.model.has('intro_video')) { + this.$el.find('.remove-course-introduction-video').show(); + } + else { + this.$el.find('.remove-course-introduction-video').hide(); + } + }, this), 1000); break; default: // Everything else is handled by datepickers and CodeMirror. break; } - this.showNotificationBar(this.save_message, - _.bind(this.saveView, this), - _.bind(this.revertView, this)); }, - removeSyllabus: function() { - if (this.model.has('syllabus')) this.setAndValidate('syllabus', null); - }, - - assetSyllabus : function() { - // TODO implement - }, - - removeVideo: function() { + removeVideo: function(event) { + event.preventDefault(); if (this.model.has('intro_video')) { this.model.set_videosource(null); this.$el.find(".current-course-introduction-video iframe").attr("src", ""); @@ -185,9 +174,6 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({ var newVal = mirror.getValue(); if (cachethis.model.get(field) != newVal) { cachethis.setAndValidate(field, newVal); - cachethis.showNotificationBar(cachethis.save_message, - _.bind(cachethis.saveView, cachethis), - _.bind(cachethis.revertView, cachethis)); } } }); @@ -208,7 +194,8 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({ mirror.setValue(self.model.get(field)); }); }, - reset: true}); + reset: true, + silent: true}); }, setAndValidate: function(attr, value) { // If we call model.set() with {validate: true}, model fields @@ -219,6 +206,15 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({ // call validate ourselves. this.model.set(attr, value); this.model.isValid(); + }, + + showNotificationBar: function() { + // We always call showNotificationBar with the same args, just + // delegate to superclass + CMS.Views.ValidatingView.prototype.showNotificationBar.call(this, + this.save_message, + _.bind(this.saveView, this), + _.bind(this.revertView, this)); } }); diff --git a/cms/static/js/views/settings/settings_grading_view.js b/cms/static/js/views/settings/settings_grading_view.js index 7329140fa6..f05261d67f 100644 --- a/cms/static/js/views/settings/settings_grading_view.js +++ b/cms/static/js/views/settings/settings_grading_view.js @@ -3,6 +3,9 @@ if (!CMS.Views['Settings']) CMS.Views.Settings = {}; // ensure the pseudo pkg ex CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({ // Model class is CMS.Models.Settings.CourseGradingPolicy events : { + "input input" : "updateModel", + "input textarea" : "updateModel", + // Leaving change in as fallback for older browsers "change input" : "updateModel", "change textarea" : "updateModel", "change span[contenteditable=true]" : "updateDesignation", @@ -62,7 +65,12 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({ function (event) { gradeCollection.on(event, function() { this.showNotificationBar(); - this.render(); + // Since the change event gets fired every time + // we type in an input field, we don't need to + // (and really shouldn't) rerender the whole view. + if(event !== 'change') { + this.render(); + } }, this); }, this); @@ -110,7 +118,7 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({ default: this.setField(event); - break; + break; } }, @@ -347,6 +355,9 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({ CMS.Views.Settings.GraderView = CMS.Views.ValidatingView.extend({ // Model class is CMS.Models.Settings.CourseGrader events : { + "input input" : "updateModel", + "input textarea" : "updateModel", + // Leaving change in as fallback for older browsers "change input" : "updateModel", "change textarea" : "updateModel", "click .remove-grading-data" : "deleteModel", @@ -370,7 +381,7 @@ CMS.Views.Settings.GraderView = CMS.Views.ValidatingView.extend({ 'drop_count' : 'course-grading-assignment-droppable', 'weight' : 'course-grading-assignment-gradeweight' }, - updateModel : function(event) { + updateModel: function(event) { // HACK to fix model sometimes losing its pointer to the collection [I think I fixed this but leaving // this in out of paranoia. If this error ever happens, the user will get a warning that they cannot // give 2 assignments the same name.] @@ -384,13 +395,14 @@ CMS.Views.Settings.GraderView = CMS.Views.ValidatingView.extend({ this.setField(event); break; case 'course-grading-assignment-name': - var oldName = this.model.get('type'); + // Keep the original name, until we save + this.oldName = this.oldName === undefined ? this.model.get('type') : this.oldName; // If the name has changed, alert the user to change all subsection names. - if (this.setField(event) != oldName && !_.isEmpty(oldName)) { + if (this.setField(event) != this.oldName && !_.isEmpty(this.oldName)) { // overload the error display logic this._cacheValidationErrors.push(event.currentTarget); $(event.currentTarget).parent().append( - this.errorTemplate({message : 'For grading to work, you must change all "' + oldName + + this.errorTemplate({message : 'For grading to work, you must change all "' + this.oldName + '" subsections to "' + this.model.get('type') + '".'})); } break; diff --git a/cms/static/js/views/validating_view.js b/cms/static/js/views/validating_view.js index bd47d07808..09b723e617 100644 --- a/cms/static/js/views/validating_view.js +++ b/cms/static/js/views/validating_view.js @@ -9,7 +9,10 @@ CMS.Views.ValidatingView = Backbone.View.extend({ errorTemplate : _.template('<%= message %>'), + save_title: gettext("You've made some changes"), save_message: gettext("Your changes will not take effect until you save your progress."), + error_title: gettext("You've made some changes, but there are some errors"), + error_message: gettext("Please address the errors on this page first, and then save your progress."), events : { "change input" : "clearValidationErrors", @@ -22,6 +25,7 @@ CMS.Views.ValidatingView = Backbone.View.extend({ _cacheValidationErrors : [], handleValidationError : function(model, error) { + this.clearValidationErrors(); // error is object w/ fields and error strings for (var field in error) { var ele = this.$el.find('#' + this.fieldToSelectorMap[field]); @@ -29,6 +33,11 @@ CMS.Views.ValidatingView = Backbone.View.extend({ this.getInputElements(ele).addClass('error'); $(ele).parent().append(this.errorTemplate({message : error[field]})); } + $('.wrapper-notification-warning').addClass('wrapper-notification-warning-w-errors'); + $('.action-save').addClass('is-disabled'); + // TODO: (pfogg) should this text fade in/out on change? + $('#notification-warning-title').text(this.error_title); + $('#notification-warning-description').text(this.error_message); }, clearValidationErrors : function() { @@ -38,6 +47,10 @@ CMS.Views.ValidatingView = Backbone.View.extend({ this.getInputElements(ele).removeClass('error'); $(ele).nextAll('.message-error').remove(); } + $('.wrapper-notification-warning').removeClass('wrapper-notification-warning-w-errors'); + $('.action-save').removeClass('is-disabled'); + $('#notification-warning-title').text(this.save_title); + $('#notification-warning-description').text(this.save_message); }, setField : function(event) { @@ -83,7 +96,7 @@ CMS.Views.ValidatingView = Backbone.View.extend({ } var self = this; this.confirmation = new CMS.Views.Notification.Warning({ - title: gettext("You've made some changes"), + title: this.save_title, message: message, actions: { primary: { @@ -110,6 +123,8 @@ CMS.Views.ValidatingView = Backbone.View.extend({ }}); this.notificationBarShowing = true; this.confirmation.show(); + // Make sure the bar is in the right state + this.model.isValid(); }, showSavedBar: function(title, message) { diff --git a/cms/static/sass/elements/_system-feedback.scss b/cms/static/sass/elements/_system-feedback.scss index 68e7ed6aeb..29c77bbc5e 100644 --- a/cms/static/sass/elements/_system-feedback.scss +++ b/cms/static/sass/elements/_system-feedback.scss @@ -665,14 +665,8 @@ } } - // alert showing/hiding - .wrapper-alert { - display: none; - - &.is-shown { - display: block; - } - } + // alert showing/hiding done by jQuery + .wrapper-alert { } // notification showing/hiding .wrapper-notification { diff --git a/common/static/sass/_mixins-inherited.scss b/common/static/sass/_mixins-inherited.scss index 82813153fa..5a834fa256 100644 --- a/common/static/sass/_mixins-inherited.scss +++ b/common/static/sass/_mixins-inherited.scss @@ -126,7 +126,7 @@ padding: ($baseline/5) $baseline ($baseline/4); font-weight: 700; - &.disabled { + &.disabled, &.is-disabled { border: 1px solid $gray-l1 !important; border-radius: 3px !important; background: $gray-l1 !important; @@ -157,7 +157,7 @@ color: $white; } - &.disabled { + &.disabled, &.is-disabled { border: 1px solid $green-l3 !important; background: $green-l3 !important; color: $white !important; @@ -178,7 +178,7 @@ color: $white; } - &.disabled { + &.disabled, &.is-disabled { box-shadow: none; border: 1px solid $blue-l3 !important; background: $blue-l3 !important; @@ -199,7 +199,7 @@ color: $white; } - &.disabled { + &.disabled, &.is-disabled { box-shadow: none; border: 1px solid $red-l3 !important; background: $red-l3 !important; @@ -220,7 +220,7 @@ color: $white; } - &.disabled { + &.disabled, &.is-disabled { box-shadow: none; border: 1px solid $pink-l3 !important; background: $pink-l3 !important; @@ -242,7 +242,7 @@ color: $gray-d2; } - &.disabled { + &.disabled, &.is-disabled { border: 1px solid $orange-l3 !important; background: $orange-l2 !important; color: $gray-l1 !important; From c360faed5893c276a409093c648ec4789f094d0c Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Thu, 11 Jul 2013 16:30:30 -0400 Subject: [PATCH 712/995] Studio: revises dashboard course creation button and form interaction --- cms/static/js/base.js | 2 +- cms/templates/index.html | 121 ++++++++++++++++++++------------------- 2 files changed, 63 insertions(+), 60 deletions(-) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index a6218541ef..5042764212 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -576,7 +576,7 @@ function cancelNewSection(e) { function addNewCourse(e) { e.preventDefault(); - + $('.new-course-button').addClass('disabled'); $(e.target).addClass('disabled'); var $newCourse = $($('#new-course-template').html()); var $cancelButton = $newCourse.find('.new-course-cancel'); diff --git a/cms/templates/index.html b/cms/templates/index.html index d182f4c174..dd36f19b44 100644 --- a/cms/templates/index.html +++ b/cms/templates/index.html @@ -113,6 +113,9 @@
      %else: +
      +
      +
      @@ -144,82 +147,82 @@ % endif - %if course_creator_status == "unrequested": -
      -

      - ${_('Becoming a Course Creator in Studio')} -

      + %if course_creator_status == "unrequested": +
      +

      + ${_('Becoming a Course Creator in Studio')} +

      -
      -
      -

      ${_('edX Studio is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by edX. Our team will evaluate your request and provide you feedback within 24 hours during the work week.')}

      -
      +
      +
      +

      ${_('edX Studio is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by edX. Our team will evaluate your request and provide you feedback within 24 hours during the work week.')}

      +
      -
      -

      ${_('Your Course Creator Request Status:')}

      +
      +

      ${_('Your Course Creator Request Status:')}

      - - -
      - -
      - +
      + +
      + +
      +
      +
      -
      - %elif course_creator_status == "denied": -
      -

      - ${_('Your Course Creator Request Status')} -

      + %elif course_creator_status == "denied": +
      +

      + ${_('Your Course Creator Request Status')} +

      -
      -
      -

      ${_('edX Studio is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by edX. Our team is has completed evaluating your request.')}

      -
      +
      +
      +

      ${_('edX Studio is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by edX. Our team is has completed evaluating your request.')}

      +
      -
      -

      ${_('Your Course Creator Request Status:')}

      +
      +

      ${_('Your Course Creator Request Status:')}

      -
      -
      ${_('Your Course Creator request is:')}
      -
      - - ${_('Denied')} - ${_('Your request did not meet the criteria/guidelines specified by edX Staff.')} -
      -
      +
      +
      ${_('Your Course Creator request is:')}
      +
      + + ${_('Denied')} + ${_('Your request did not meet the criteria/guidelines specified by edX Staff.')} +
      +
      +
      -
      - %elif course_creator_status == "pending": -
      -

      - ${_('Your Course Creator Request Status')} -

      + %elif course_creator_status == "pending": +
      +

      + ${_('Your Course Creator Request Status')} +

      -
      -
      -

      ${_('edX Studio is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by edX. Our team is currently evaluating your request.')}

      -
      +
      +
      +

      ${_('edX Studio is a hosted solution for our xConsortium partners and selected guests. Courses for which you are a team member appear above for you to edit, while course creator privileges are granted by edX. Our team is currently evaluating your request.')}

      +
      -
      -

      ${_('Your Course Creator Request Status:')}

      +
      +

      ${_('Your Course Creator Request Status:')}

      -
      -
      ${_('Your Course Creator request is:')}
      -
      - - ${_('Pending')} - ${_('Your request is currently being reviewed by edX staff and should be updated shortly.')} -
      -
      +
      +
      ${_('Your Course Creator request is:')}
      +
      + + ${_('Pending')} + ${_('Your request is currently being reviewed by edX staff and should be updated shortly.')} +
      +
      +
      -
      - % endif + % endif From 047da7a7ce6569ad7185253502e01ba3e9540e39 Mon Sep 17 00:00:00 2001 From: James Tauber Date: Thu, 11 Jul 2013 18:18:22 -0400 Subject: [PATCH 713/995] Use $WORKON_HOME consistently for virtualenv. Some parts of the `create-dev-env/sh` script use `$WORKON_HOME` and others hardcode `$HOME/.virtualenvs` which means if you **don't** put your virtualenvs under `$HOME/.virtualenvs`, the script doesn't work at all. This commit removes the hardcoding of virtualenv location and consistently uses `$WORKON_HOME` everywhere. --- scripts/create-dev-env.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/create-dev-env.sh b/scripts/create-dev-env.sh index 3083ddadba..a28ee1a8b4 100755 --- a/scripts/create-dev-env.sh +++ b/scripts/create-dev-env.sh @@ -404,14 +404,14 @@ fi # Create edX virtualenv and link it to repo # virtualenvwrapper automatically sources the activation script if [[ $systempkgs ]]; then - mkvirtualenv -q -a "$HOME/.virtualenvs" --system-site-packages edx-platform || { + mkvirtualenv -q -a "$WORKON_HOME" --system-site-packages edx-platform || { error "mkvirtualenv exited with a non-zero error" return 1 } else # default behavior for virtualenv>1.7 is # --no-site-packages - mkvirtualenv -q -a "$HOME/.virtualenvs" edx-platform || { + mkvirtualenv -q -a "$WORKON_HOME" edx-platform || { error "mkvirtualenv exited with a non-zero error" return 1 } @@ -443,7 +443,7 @@ fi # building correct version of distribute from source DISTRIBUTE_VER="0.6.28" output "Building Distribute" -SITE_PACKAGES="$HOME/.virtualenvs/edx-platform/lib/python2.7/site-packages" +SITE_PACKAGES="$WORKON_HOME/edx-platform/lib/python2.7/site-packages" cd "$SITE_PACKAGES" curl -sSLO http://pypi.python.org/packages/source/d/distribute/distribute-${DISTRIBUTE_VER}.tar.gz tar -xzvf distribute-${DISTRIBUTE_VER}.tar.gz From 871ab87bf210ef758e9dac1dd1944ec891f1522e Mon Sep 17 00:00:00 2001 From: Peter Fogg Date: Fri, 12 Jul 2013 09:01:03 -0400 Subject: [PATCH 714/995] Fix saving notification CSS. --- cms/djangoapps/contentstore/features/section.py | 2 +- cms/static/sass/elements/_system-feedback.scss | 4 ++-- cms/templates/js/system-feedback.underscore | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cms/djangoapps/contentstore/features/section.py b/cms/djangoapps/contentstore/features/section.py index f41b2dbcd0..93fa0adf3e 100644 --- a/cms/djangoapps/contentstore/features/section.py +++ b/cms/djangoapps/contentstore/features/section.py @@ -44,7 +44,7 @@ def i_save_a_new_section_release_date(_step): @step('I see a "saving" notification') def i_see_a_saving_notification(step): - saving_css = '.wrapper-notification-saving' + saving_css = '.wrapper-notification-mini' assert world.is_css_present(saving_css) diff --git a/cms/static/sass/elements/_system-feedback.scss b/cms/static/sass/elements/_system-feedback.scss index 29c77bbc5e..5022a9f677 100644 --- a/cms/static/sass/elements/_system-feedback.scss +++ b/cms/static/sass/elements/_system-feedback.scss @@ -274,7 +274,7 @@ } } - &.wrapper-notification-saving { + &.wrapper-notification-mini { box-shadow: 0 -1px 3px $shadow, inset 0 3px 1px $pink; } @@ -434,7 +434,7 @@ } } - &.saving { + &.mini { [class^="icon"] { @include animation(rotateCW $tmg-s3 linear infinite); diff --git a/cms/templates/js/system-feedback.underscore b/cms/templates/js/system-feedback.underscore index b8ef1b8dc8..cf4f8479bb 100644 --- a/cms/templates/js/system-feedback.underscore +++ b/cms/templates/js/system-feedback.underscore @@ -1,6 +1,6 @@
      wrapper-<%= type %>-status<% } %>" id="<%= type %>-<%= intent %>" aria-hidden="<% if(obj.shown) { %>false<% } else { %>true<% } %>" aria-labelledby="<%= type %>-<%= intent %>-title" @@ -9,7 +9,7 @@ >
      <% if(obj.icon) { %> - <% var iconClass = {"warning": "warning-sign", "confirmation": "ok", "error": "warning-sign", "announcement": "bullhorn", "step-required": "exclamation-sign", "help": "question-sign", "saving": "cog"} %> + <% var iconClass = {"warning": "warning-sign", "confirmation": "ok", "error": "warning-sign", "announcement": "bullhorn", "step-required": "exclamation-sign", "help": "question-sign", "mini": "cog"} %> <% } %> From d86502bdc1ed0c6bfe2e1de68ce5d7ba7209870e Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Wed, 3 Jul 2013 15:42:55 -0400 Subject: [PATCH 715/995] Added in a retry for checking the class of the video --- cms/djangoapps/contentstore/features/common.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index 5a8e02f168..820c643f9e 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -223,7 +223,13 @@ def shows_captions(step, show_captions): # Prevent cookies from overriding course settings world.browser.cookies.delete('hide_captions') if show_captions == 'does not': - assert world.css_find('.video')[0].has_class('closed') + attempt = 0 + while attempt < 5: + try: + assert world.css_find('.video')[0].has_class('closed') + except: + attempt += 1 + assert_true(attempt < 5, "There was a stale reference exception in accessing the class of the video") else: assert world.is_css_not_present('.video.closed') From 8b0e7c5782db49a2cdc1f92daf8437b1b8346db4 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Wed, 3 Jul 2013 15:57:11 -0400 Subject: [PATCH 716/995] Made a new command css_has_class that will safely check the class and get around stale elements --- cms/djangoapps/contentstore/features/common.py | 8 +------- common/djangoapps/terrain/ui_helpers.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index 820c643f9e..61c24728f4 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -223,13 +223,7 @@ def shows_captions(step, show_captions): # Prevent cookies from overriding course settings world.browser.cookies.delete('hide_captions') if show_captions == 'does not': - attempt = 0 - while attempt < 5: - try: - assert world.css_find('.video')[0].has_class('closed') - except: - attempt += 1 - assert_true(attempt < 5, "There was a stale reference exception in accessing the class of the video") + assert world.css_has_class('.video', 'closed') else: assert world.is_css_not_present('.video.closed') diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py index c238ff862a..a74f15bbd4 100644 --- a/common/djangoapps/terrain/ui_helpers.py +++ b/common/djangoapps/terrain/ui_helpers.py @@ -182,6 +182,19 @@ def css_html(css_selector, index=0, max_attempts=5): return '' +@world.absorb +def css_has_class(css_selector, class_name, index=0, max_attempts=5): + attempt = 0 + found = False + while attempt < max_attempts and not found: + try: + return world.css_find(css_selector)[index].has_class(class_name) + found = True + except: + attempt += 1 + return False + + @world.absorb def css_visible(css_selector): assert is_css_present(css_selector), "{} is not present".format(css_selector) From f1dd81a67a4edc6a25ce65119b832c38402f6634 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Mon, 8 Jul 2013 09:56:35 -0400 Subject: [PATCH 717/995] Refactored LMS so all potential stale element exceptions are avoided --- common/djangoapps/terrain/ui_helpers.py | 76 +++++++++++++++---- lms/djangoapps/courseware/features/login.py | 25 ++++-- .../courseware/features/problems.py | 2 +- .../courseware/features/problems_setup.py | 23 ++++-- .../courseware/features/registration.py | 5 +- lms/djangoapps/courseware/features/signup.py | 30 ++++++-- 6 files changed, 124 insertions(+), 37 deletions(-) diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py index a74f15bbd4..fee96a9938 100644 --- a/common/djangoapps/terrain/ui_helpers.py +++ b/common/djangoapps/terrain/ui_helpers.py @@ -10,6 +10,7 @@ from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from lettuce.django import django_url +from nose.tools import assert_true @world.absorb @@ -142,14 +143,44 @@ def id_click(elem_id): @world.absorb -def css_fill(css_selector, text): - assert is_css_present(css_selector), "{} is not present".format(css_selector) - world.browser.find_by_css(css_selector).first.fill(text) +def css_fill(css_selector, text, index=0, max_attempts=5): + assert is_css_present(css_selector) + attempt = 0 + result = False + while attempt < max_attempts: + try: + world.browser.find_by_css(css_selector)[index].fill(text) + result = True + break + except WebDriverException: + # Occasionally, MathJax or other JavaScript can cover up + # an element temporarily. + # If this happens, wait a second, then try again + world.wait(1) + attempt += 1 + except: + attempt += 1 + assert_true(result, 'Filling {} did not work as expected'.format(css_selector)) @world.absorb -def click_link(partial_text): - world.browser.find_link_by_partial_text(partial_text).first.click() +def click_link(partial_text, index=0, max_attempts=5): + attempt = 0 + result = False + while attempt < max_attempts: + try: + world.browser.find_link_by_partial_text(partial_text)[index].click() + result = True + break + except WebDriverException: + # Occasionally, MathJax or other JavaScript can cover up + # an element temporarily. + # If this happens, wait a second, then try again + world.wait(1) + attempt += 1 + except: + attempt += 1 + assert_true(result, 'Clicking {} did not work as expected'.format(partial_text)) @world.absorb @@ -167,6 +198,21 @@ def css_text(css_selector, index=0): return "" +@world.absorb +def css_value(css_selector, index=0): + + # Wait for the css selector to appear + if world.is_css_present(css_selector): + try: + return world.browser.find_by_css(css_selector)[index].value + except StaleElementReferenceException: + # The DOM was still redrawing. Wait a second and try again. + world.wait(1) + return world.browser.find_by_css(css_selector)[index].value + else: + return "" + + @world.absorb def css_html(css_selector, index=0, max_attempts=5): """ @@ -179,26 +225,30 @@ def css_html(css_selector, index=0, max_attempts=5): return world.browser.find_by_css(css_selector)[index].html except: attempt += 1 - return '' + assert_true(attempt < max_attempts, 'Ran out of attempts to access {}'.format(css_selector)) @world.absorb def css_has_class(css_selector, class_name, index=0, max_attempts=5): attempt = 0 - found = False - while attempt < max_attempts and not found: + while attempt < max_attempts: try: return world.css_find(css_selector)[index].has_class(class_name) - found = True except: attempt += 1 - return False + assert_true(attempt < max_attempts, 'Ran out of attempts to access {}'.format(css_selector)) @world.absorb -def css_visible(css_selector): - assert is_css_present(css_selector), "{} is not present".format(css_selector) - return world.browser.find_by_css(css_selector).visible +def css_visible(css_selector, index=0, max_attempts=5): + assert is_css_present(css_selector) + attempt = 0 + while attempt < max_attempts: + try: + return world.browser.find_by_css(css_selector)[index].visible + except: + attempt += 1 + assert_true(attempt < max_attempts, 'Ran out of attempts to access {}'.format(css_selector)) @world.absorb diff --git a/lms/djangoapps/courseware/features/login.py b/lms/djangoapps/courseware/features/login.py index 857b70fa5d..25260c3256 100644 --- a/lms/djangoapps/courseware/features/login.py +++ b/lms/djangoapps/courseware/features/login.py @@ -3,6 +3,7 @@ from lettuce import step, world from django.contrib.auth.models import User +from nose.tools import assert_true @step('I am an unactivated user$') @@ -19,8 +20,15 @@ def i_am_an_activated_user(step): def i_submit_my_credentials_on_the_login_form(step): fill_in_the_login_form('email', 'robot@edx.org') fill_in_the_login_form('password', 'test') - login_form = world.browser.find_by_css('form#login-form') - login_form.find_by_name('submit').click() + attempt = 0 + while attempt < 5: + try: + login_form = world.browser.find_by_css('form#login-form') + login_form.find_by_name('submit').click() + break + except: + attempt += 1 + assert_true(attempt < 5, 'Login form could not be clicked') @step(u'I should see the login error message "([^"]*)"$') @@ -49,6 +57,13 @@ def user_is_an_activated_user(uname): def fill_in_the_login_form(field, value): - login_form = world.browser.find_by_css('form#login-form') - form_field = login_form.find_by_name(field) - form_field.fill(value) + attempt = 0 + while attempt < 5: + try: + login_form = world.browser.find_by_css('form#login-form') + form_field = login_form.find_by_name(field) + form_field.fill(value) + break + except: + attempt += 1 + assert_true(attempt < 5, 'Login form could not be filled') diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index 39b99214c8..e97533f4db 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -121,7 +121,7 @@ def reset_problem(_step): def press_the_button_with_label(_step, buttonname): button_css = 'button span.show-label' elem = world.css_find(button_css).first - assert_equal(elem.text, buttonname) + world.css_has_text(button_css, elem) world.css_click(button_css) diff --git a/lms/djangoapps/courseware/features/problems_setup.py b/lms/djangoapps/courseware/features/problems_setup.py index 0438b82fa2..96dd3cee40 100644 --- a/lms/djangoapps/courseware/features/problems_setup.py +++ b/lms/djangoapps/courseware/features/problems_setup.py @@ -19,6 +19,7 @@ from capa.tests.response_xml_factory import OptionResponseXMLFactory, \ StringResponseXMLFactory, NumericalResponseXMLFactory, \ FormulaResponseXMLFactory, CustomResponseXMLFactory, \ CodeResponseXMLFactory +from nose.tools import assert_true # Factories from capa.tests.response_xml_factory that we will use @@ -312,14 +313,20 @@ def assert_checked(problem_type, choices): all_choices = ['choice_0', 'choice_1', 'choice_2', 'choice_3'] for this_choice in all_choices: - element = world.css_find(inputfield(problem_type, choice=this_choice)) - - if this_choice in choices: - assert element.checked - else: - assert not element.checked + attempt = 0 + while attempt < 5: + try: + element = world.css_find(inputfield(problem_type, choice=this_choice)) + if this_choice in choices: + assert element.checked + else: + assert not element.checked + break + except: + attempt += 1 + assert_true(attempt < 5, "Could not access {}".format(element)) def assert_textfield(problem_type, expected_text, input_num=1): - element = world.css_find(inputfield(problem_type, input_num=input_num)) - assert element.value == expected_text + element_value = world.css_value(inputfield(problem_type, input_num=input_num)) + assert element_value == expected_text diff --git a/lms/djangoapps/courseware/features/registration.py b/lms/djangoapps/courseware/features/registration.py index 80db4a4b23..e5edb41575 100644 --- a/lms/djangoapps/courseware/features/registration.py +++ b/lms/djangoapps/courseware/features/registration.py @@ -11,10 +11,7 @@ def i_register_for_the_course(_step, course): cleaned_name = TEST_COURSE_NAME.replace(' ', '_') url = django_url('courses/%s/%s/%s/about' % (TEST_COURSE_ORG, course, cleaned_name)) world.browser.visit(url) - - intro_section = world.browser.find_by_css('section.intro') - register_link = intro_section.find_by_css('a.register') - register_link.click() + world.css_click('section.intro a.register') assert world.is_css_present('section.container.dashboard') diff --git a/lms/djangoapps/courseware/features/signup.py b/lms/djangoapps/courseware/features/signup.py index 3dc34d5af8..0dfdae02b1 100644 --- a/lms/djangoapps/courseware/features/signup.py +++ b/lms/djangoapps/courseware/features/signup.py @@ -6,20 +6,38 @@ from lettuce import world, step @step('I fill in "([^"]*)" on the registration form with "([^"]*)"$') def when_i_fill_in_field_on_the_registration_form_with_value(step, field, value): - register_form = world.browser.find_by_css('form#register-form') - form_field = register_form.find_by_name(field) - form_field.fill(value) + attempt = 0 + while attempt < 5: + try: + register_form = world.browser.find_by_css('form#register-form') + form_field = register_form.find_by_name(field) + form_field.fill(value) + break + except: + attempt += 1 @step('I submit the registration form$') def i_press_the_button_on_the_registration_form(step): - register_form = world.browser.find_by_css('form#register-form') - register_form.find_by_name('submit').click() + attempt = 0 + while attempt < 5: + try: + register_form = world.browser.find_by_css('form#register-form') + register_form.find_by_name('submit').click() + break + except: + attempt += 1 @step('I check the checkbox named "([^"]*)"$') def i_check_checkbox(step, checkbox): - world.browser.find_by_name(checkbox).check() + attempt = 0 + while attempt < 5: + try: + world.browser.find_by_name(checkbox).check() + break + except: + attempt += 1 @step('I should see "([^"]*)" in the dashboard banner$') From c53aac459697aebec81dacccc92a712a697ba367 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Mon, 8 Jul 2013 10:51:51 -0400 Subject: [PATCH 718/995] CMS acceptance tests now should not get stale element exception errors --- .../contentstore/features/advanced-settings.py | 4 ++-- .../contentstore/features/checklists.py | 2 +- cms/djangoapps/contentstore/features/common.py | 14 ++++++++++---- .../contentstore/features/course-settings.py | 2 +- .../contentstore/features/course-team.py | 2 +- .../contentstore/features/course-updates.py | 2 +- .../contentstore/features/grading.py | 12 ++++++------ .../contentstore/features/problem-editor.py | 4 ++-- .../contentstore/features/section.py | 4 ++-- cms/djangoapps/contentstore/features/signup.py | 18 ++++++++++++------ .../features/studio-overview-togglesection.py | 10 +++++----- .../contentstore/features/subsection.py | 4 ++-- cms/djangoapps/contentstore/features/video.py | 2 +- 13 files changed, 46 insertions(+), 34 deletions(-) diff --git a/cms/djangoapps/contentstore/features/advanced-settings.py b/cms/djangoapps/contentstore/features/advanced-settings.py index c08216c8e6..cdba6d9cb2 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.py +++ b/cms/djangoapps/contentstore/features/advanced-settings.py @@ -99,7 +99,7 @@ def assert_policy_entries(expected_keys, expected_values): def get_index_of(expected_key): for counter in range(len(world.css_find(KEY_CSS))): # Sometimes get stale reference if I hold on to the array of elements - key = world.css_find(KEY_CSS)[counter].value + key = world.css_value(KEY_CSS, index=counter) if key == expected_key: return counter @@ -108,7 +108,7 @@ def get_index_of(expected_key): def get_display_name_value(): index = get_index_of(DISPLAY_NAME_KEY) - return world.css_find(VALUE_CSS)[index].value + return world.css_value(VALUE_CSS, index=index) def change_display_name_value(step, new_value): diff --git a/cms/djangoapps/contentstore/features/checklists.py b/cms/djangoapps/contentstore/features/checklists.py index fe20fb9b77..13d3ca99b7 100644 --- a/cms/djangoapps/contentstore/features/checklists.py +++ b/cms/djangoapps/contentstore/features/checklists.py @@ -61,7 +61,7 @@ def i_select_a_link_to_the_course_outline(step): @step('I am brought to the course outline page$') def i_am_brought_to_course_outline(step): - assert_in('Course Outline', world.css_find('.outline .page-header')[0].text) + assert_in('Course Outline', world.css_text('.outline .page-header')) assert_equal(1, len(world.browser.windows)) diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index 61c24728f4..4d4e099c8b 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -144,10 +144,16 @@ def log_into_studio( 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(email) - login_form.find_by_name('password').fill(password) - login_form.find_by_name('submit').click() + attempt = 0 + while attempt < 5: + try: + login_form = world.browser.find_by_css('form#login_form') + login_form.find_by_name('email').fill(email) + login_form.find_by_name('password').fill(password) + login_form.find_by_name('submit').click() + break + except: + attempt += 1 assert_true(world.is_css_present('.new-course-button')) diff --git a/cms/djangoapps/contentstore/features/course-settings.py b/cms/djangoapps/contentstore/features/course-settings.py index 53a3fa7870..da72d893cf 100644 --- a/cms/djangoapps/contentstore/features/course-settings.py +++ b/cms/djangoapps/contentstore/features/course-settings.py @@ -162,7 +162,7 @@ def verify_date_or_time(css, date_or_time): """ Verifies date or time field. """ - assert_equal(date_or_time, world.css_find(css).first.value) + assert_equal(date_or_time, world.css_value(css)) def i_see_the_set_dates(): diff --git a/cms/djangoapps/contentstore/features/course-team.py b/cms/djangoapps/contentstore/features/course-team.py index c126773db6..71d9d9fb02 100644 --- a/cms/djangoapps/contentstore/features/course-team.py +++ b/cms/djangoapps/contentstore/features/course-team.py @@ -47,7 +47,7 @@ def other_user_login(_step, name): @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_courses = world.css_find(class_css, wait_time=1) all_names = [item.html for item in all_courses] if doesnt_see_course: assert not _COURSE_NAME in all_names diff --git a/cms/djangoapps/contentstore/features/course-updates.py b/cms/djangoapps/contentstore/features/course-updates.py index e7fbb2f90c..9506191a76 100644 --- a/cms/djangoapps/contentstore/features/course-updates.py +++ b/cms/djangoapps/contentstore/features/course-updates.py @@ -24,7 +24,7 @@ def add_update(_step, text): @step(u'I should( not)? see the update "([^"]*)"$') def check_update(_step, doesnt_see_update, text): update_css = 'div.update-contents' - update = world.css_find(update_css) + update = world.css_find(update_css, wait_time=1) if doesnt_see_update: assert len(update) == 0 or not text in update.html else: diff --git a/cms/djangoapps/contentstore/features/grading.py b/cms/djangoapps/contentstore/features/grading.py index e75d8f23ad..636722502c 100644 --- a/cms/djangoapps/contentstore/features/grading.py +++ b/cms/djangoapps/contentstore/features/grading.py @@ -90,8 +90,8 @@ def add_assignment_type(step, new_name): add_button_css = '.add-grading-data' world.css_click(add_button_css) name_id = '#course-grading-assignment-name' - f = world.css_find(name_id)[4] - f._element.send_keys(new_name) + new_assignment = world.css_find(name_id)[-1] + new_assignment._element.send_keys(new_name) @step(u'I have populated the course') @@ -118,8 +118,8 @@ def i_see_the_assignment_type(_step, name): def get_type_index(name): name_id = '#course-grading-assignment-name' - f = world.css_find(name_id) - for i in range(len(f)): - if f[i].value == name: - return i + all_types = world.css_find(name_id) + for index in range(len(all_types)): + if world.css_value(name_id, index=index) == name: + return index return -1 diff --git a/cms/djangoapps/contentstore/features/problem-editor.py b/cms/djangoapps/contentstore/features/problem-editor.py index 8691a6772e..5d12b23d90 100644 --- a/cms/djangoapps/contentstore/features/problem-editor.py +++ b/cms/djangoapps/contentstore/features/problem-editor.py @@ -169,7 +169,7 @@ def edit_latex_source(step): @step('my change to the High Level Source is persisted') def high_level_source_persisted(step): def verify_text(driver): - return world.css_find('.problem').text == 'hi' + return world.css_text('.problem') == 'hi' world.wait_for(verify_text) @@ -177,7 +177,7 @@ def high_level_source_persisted(step): @step('I view the High Level Source I see my changes') def high_level_source_in_editor(step): open_high_level_source() - assert_equal('hi', world.css_find('.source-edit-box').value) + assert_equal('hi', world.css_value('.source-edit-box')) def verify_high_level_source_links(step, visible): diff --git a/cms/djangoapps/contentstore/features/section.py b/cms/djangoapps/contentstore/features/section.py index 93fa0adf3e..4b69b9b37e 100644 --- a/cms/djangoapps/contentstore/features/section.py +++ b/cms/djangoapps/contentstore/features/section.py @@ -70,7 +70,7 @@ def i_click_to_edit_section_name(_step): def i_see_complete_section_name_with_quote_in_editor(_step): css = '.section-name-edit input[type=text]' assert world.is_css_present(css) - assert_equal(world.browser.find_by_css(css).value, 'Section with "Quote"') + assert_equal(world.css_value(css), 'Section with "Quote"') @step('the section does not exist$') @@ -85,7 +85,7 @@ def i_see_a_release_date_for_my_section(_step): css = 'span.published-status' assert world.is_css_present(css) - status_text = world.browser.find_by_css(css).text + status_text = world.css_text(css) # e.g. 11/06/2012 at 16:25 msg = 'Will Release:' diff --git a/cms/djangoapps/contentstore/features/signup.py b/cms/djangoapps/contentstore/features/signup.py index 398f8d074d..be396c8699 100644 --- a/cms/djangoapps/contentstore/features/signup.py +++ b/cms/djangoapps/contentstore/features/signup.py @@ -7,12 +7,18 @@ from common import * @step('I fill in the registration form$') def i_fill_in_the_registration_form(step): - register_form = world.browser.find_by_css('form#register_form') - register_form.find_by_name('email').fill('robot+studio@edx.org') - register_form.find_by_name('password').fill('test') - register_form.find_by_name('username').fill('robot-studio') - register_form.find_by_name('name').fill('Robot Studio') - register_form.find_by_name('terms_of_service').check() + attempt = 0 + while attempt < 5: + try: + register_form = world.browser.find_by_css('form#register_form') + register_form.find_by_name('email').fill('robot+studio@edx.org') + register_form.find_by_name('password').fill('test') + register_form.find_by_name('username').fill('robot-studio') + register_form.find_by_name('name').fill('Robot Studio') + register_form.find_by_name('terms_of_service').check() + break + except: + attempt += 1 @step('I press the Create My Account button on the registration form$') diff --git a/cms/djangoapps/contentstore/features/studio-overview-togglesection.py b/cms/djangoapps/contentstore/features/studio-overview-togglesection.py index 1fbd965871..9ab17fbdac 100644 --- a/cms/djangoapps/contentstore/features/studio-overview-togglesection.py +++ b/cms/djangoapps/contentstore/features/studio-overview-togglesection.py @@ -92,7 +92,7 @@ def i_expand_a_section(step): def i_see_the_span_with_text(step, text): span_locator = '.toggle-button-sections span' assert_true(world.is_css_present(span_locator)) - assert_equal(world.css_find(span_locator).value, text) + assert_equal(world.css_value(span_locator), text) assert_true(world.css_visible(span_locator)) @@ -108,13 +108,13 @@ def i_do_not_see_the_span_with_text(step, text): def all_sections_are_expanded(step): subsection_locator = 'div.subsection-list' subsections = world.css_find(subsection_locator) - for s in subsections: - assert_true(s.visible) + for index in range(len(subsections)): + assert_true(world.css_visible(subsection_locator, index=index)) @step(u'all sections are collapsed$') def all_sections_are_collapsed(step): subsection_locator = 'div.subsection-list' subsections = world.css_find(subsection_locator) - for s in subsections: - assert_false(s.visible) + for index in range(len(subsections)): + assert_false(world.css_visible(subsection_locator, index=index)) diff --git a/cms/djangoapps/contentstore/features/subsection.py b/cms/djangoapps/contentstore/features/subsection.py index 1134e53280..e280ec615d 100644 --- a/cms/djangoapps/contentstore/features/subsection.py +++ b/cms/djangoapps/contentstore/features/subsection.py @@ -50,7 +50,7 @@ def i_click_to_edit_subsection_name(step): def i_see_complete_subsection_name_with_quote_in_editor(step): css = '.subsection-display-name-input' assert world.is_css_present(css) - assert_equal(world.css_find(css).value, 'Subsection With "Quote"') + assert_equal(world.css_value(css), 'Subsection With "Quote"') @step('I have set a release date and due date in different years$') @@ -69,7 +69,7 @@ def i_mark_it_as_homework(step): @step('I see it marked as Homework$') def i_see_it_marked__as_homework(step): - assert_equal(world.css_find(".status-label").value, 'Homework') + assert_equal(world.css_value(".status-label"), 'Homework') ############ ASSERTIONS ################### diff --git a/cms/djangoapps/contentstore/features/video.py b/cms/djangoapps/contentstore/features/video.py index 190f8e9f1e..cb59193f17 100644 --- a/cms/djangoapps/contentstore/features/video.py +++ b/cms/djangoapps/contentstore/features/video.py @@ -8,7 +8,7 @@ from lettuce import world, step @step('when I view the video it does not have autoplay enabled') def does_not_autoplay(_step): assert world.css_find('.video')[0]['data-autoplay'] == 'False' - assert world.css_find('.video_control')[0].has_class('play') + assert world.css_has_class('.video_control', 'play') @step('creating a video takes a single click') From 2c0c8dfb90cc1e40e827633ed0e274aa980693b5 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Mon, 8 Jul 2013 12:59:37 -0400 Subject: [PATCH 719/995] Fixed structure of css_text and css_value --- common/djangoapps/terrain/ui_helpers.py | 32 ++++++++++++++----------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py index fee96a9938..076384d30c 100644 --- a/common/djangoapps/terrain/ui_helpers.py +++ b/common/djangoapps/terrain/ui_helpers.py @@ -184,31 +184,35 @@ def click_link(partial_text, index=0, max_attempts=5): @world.absorb -def css_text(css_selector, index=0): +def css_text(css_selector, index=0, max_attempts=5): # Wait for the css selector to appear if world.is_css_present(css_selector): - try: - return world.browser.find_by_css(css_selector)[index].text - except StaleElementReferenceException: - # The DOM was still redrawing. Wait a second and try again. - world.wait(1) - return world.browser.find_by_css(css_selector)[index].text + attempt = 0 + while attempt < max_attempts: + try: + return world.browser.find_by_css(css_selector)[index].text + break + except: + attempt += 1 + assert_true(attempt < max_attempts, 'Could not access {}'.format(css_selector)) else: return "" @world.absorb -def css_value(css_selector, index=0): +def css_value(css_selector, index=0, max_attempts=5): # Wait for the css selector to appear if world.is_css_present(css_selector): - try: - return world.browser.find_by_css(css_selector)[index].value - except StaleElementReferenceException: - # The DOM was still redrawing. Wait a second and try again. - world.wait(1) - return world.browser.find_by_css(css_selector)[index].value + attempt = 0 + while attempt < max_attempts: + try: + return world.browser.find_by_css(css_selector)[index].value + break + except: + attempt += 1 + assert_true(attempt < max_attempts, 'Could not access {}'.format(css_selector)) else: return "" From 287b750a22b83889776583c798741d9fffd10b98 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Mon, 8 Jul 2013 15:01:33 -0400 Subject: [PATCH 720/995] Reworded css_text to use css_find --- common/djangoapps/terrain/ui_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py index 076384d30c..fd8bd32e20 100644 --- a/common/djangoapps/terrain/ui_helpers.py +++ b/common/djangoapps/terrain/ui_helpers.py @@ -191,7 +191,7 @@ def css_text(css_selector, index=0, max_attempts=5): attempt = 0 while attempt < max_attempts: try: - return world.browser.find_by_css(css_selector)[index].text + return world.css_find(css_selector, index=index).text break except: attempt += 1 From 1f648d901ec8a49282c70c6088b65ca093f43ac0 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Mon, 8 Jul 2013 15:17:11 -0400 Subject: [PATCH 721/995] Revert "Reworded css_text to use css_find" This reverts commit a7de337fe2d218d6cb9c2f175c6ea71f5ddcb6da. --- common/djangoapps/terrain/ui_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py index fd8bd32e20..076384d30c 100644 --- a/common/djangoapps/terrain/ui_helpers.py +++ b/common/djangoapps/terrain/ui_helpers.py @@ -191,7 +191,7 @@ def css_text(css_selector, index=0, max_attempts=5): attempt = 0 while attempt < max_attempts: try: - return world.css_find(css_selector, index=index).text + return world.browser.find_by_css(css_selector)[index].text break except: attempt += 1 From 1ae86673f3c5f6ec5dd1f0a663693fc15e1cc750 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Mon, 8 Jul 2013 15:35:20 -0400 Subject: [PATCH 722/995] Changed a world.browser.find_by_css to world.css_find --- cms/djangoapps/contentstore/features/signup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/features/signup.py b/cms/djangoapps/contentstore/features/signup.py index be396c8699..68414389e4 100644 --- a/cms/djangoapps/contentstore/features/signup.py +++ b/cms/djangoapps/contentstore/features/signup.py @@ -10,7 +10,7 @@ def i_fill_in_the_registration_form(step): attempt = 0 while attempt < 5: try: - register_form = world.browser.find_by_css('form#register_form') + register_form = world.css_find('form#register_form') register_form.find_by_name('email').fill('robot+studio@edx.org') register_form.find_by_name('password').fill('test') register_form.find_by_name('username').fill('robot-studio') From ebc9fa9e2ab2abc8cfa957b85abe50985bbe6f30 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 11 Jul 2013 11:17:28 -0400 Subject: [PATCH 723/995] Made a retry_on_exception world function that will retry the given function --- .../contentstore/features/common.py | 17 ++++----- .../contentstore/features/signup.py | 20 +++++------ common/djangoapps/terrain/ui_helpers.py | 13 ++++++- lms/djangoapps/courseware/features/login.py | 29 ++++++--------- lms/djangoapps/courseware/features/signup.py | 36 +++++++------------ 5 files changed, 48 insertions(+), 67 deletions(-) diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index 4d4e099c8b..875bc3ced9 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -144,17 +144,12 @@ def log_into_studio( world.is_css_present(signin_css) world.css_click(signin_css) - attempt = 0 - while attempt < 5: - try: - login_form = world.browser.find_by_css('form#login_form') - login_form.find_by_name('email').fill(email) - login_form.find_by_name('password').fill(password) - login_form.find_by_name('submit').click() - break - except: - attempt += 1 - + def fill_login_form(): + login_form = world.browser.find_by_css('form#login_form') + login_form.find_by_name('email').fill(email) + login_form.find_by_name('password').fill(password) + login_form.find_by_name('submit').click() + world.retry_on_exception(fill_login_form) assert_true(world.is_css_present('.new-course-button')) diff --git a/cms/djangoapps/contentstore/features/signup.py b/cms/djangoapps/contentstore/features/signup.py index 68414389e4..49a305f70b 100644 --- a/cms/djangoapps/contentstore/features/signup.py +++ b/cms/djangoapps/contentstore/features/signup.py @@ -7,18 +7,14 @@ from common import * @step('I fill in the registration form$') def i_fill_in_the_registration_form(step): - attempt = 0 - while attempt < 5: - try: - register_form = world.css_find('form#register_form') - register_form.find_by_name('email').fill('robot+studio@edx.org') - register_form.find_by_name('password').fill('test') - register_form.find_by_name('username').fill('robot-studio') - register_form.find_by_name('name').fill('Robot Studio') - register_form.find_by_name('terms_of_service').check() - break - except: - attempt += 1 + def fill_in_reg_form(): + register_form = world.css_find('form#register_form') + register_form.find_by_name('email').fill('robot+studio@edx.org') + register_form.find_by_name('password').fill('test') + register_form.find_by_name('username').fill('robot-studio') + register_form.find_by_name('name').fill('Robot Studio') + register_form.find_by_name('terms_of_service').check() + world.retry_on_exception(fill_in_reg_form) @step('I press the Create My Account button on the registration form$') diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py index 076384d30c..46fe97548a 100644 --- a/common/djangoapps/terrain/ui_helpers.py +++ b/common/djangoapps/terrain/ui_helpers.py @@ -249,7 +249,7 @@ def css_visible(css_selector, index=0, max_attempts=5): attempt = 0 while attempt < max_attempts: try: - return world.browser.find_by_css(css_selector)[index].visible + return except: attempt += 1 assert_true(attempt < max_attempts, 'Ran out of attempts to access {}'.format(css_selector)) @@ -299,3 +299,14 @@ def click_tools(): @world.absorb def is_mac(): return platform.mac_ver()[0] is not '' + + +@world.absorb +def retry_on_exception(func, max_attempts=5): + attempts = 0 + while attempts < max_attempts: + try: + return func() + break + except: + attempts += 1 diff --git a/lms/djangoapps/courseware/features/login.py b/lms/djangoapps/courseware/features/login.py index 25260c3256..6a9a7c9e28 100644 --- a/lms/djangoapps/courseware/features/login.py +++ b/lms/djangoapps/courseware/features/login.py @@ -20,15 +20,11 @@ def i_am_an_activated_user(step): def i_submit_my_credentials_on_the_login_form(step): fill_in_the_login_form('email', 'robot@edx.org') fill_in_the_login_form('password', 'test') - attempt = 0 - while attempt < 5: - try: - login_form = world.browser.find_by_css('form#login-form') - login_form.find_by_name('submit').click() - break - except: - attempt += 1 - assert_true(attempt < 5, 'Login form could not be clicked') + + def submit_login_form(): + login_form = world.browser.find_by_css('form#login-form') + login_form.find_by_name('submit').click() + world.retry_on_excetion(submit_login_form) @step(u'I should see the login error message "([^"]*)"$') @@ -57,13 +53,8 @@ def user_is_an_activated_user(uname): def fill_in_the_login_form(field, value): - attempt = 0 - while attempt < 5: - try: - login_form = world.browser.find_by_css('form#login-form') - form_field = login_form.find_by_name(field) - form_field.fill(value) - break - except: - attempt += 1 - assert_true(attempt < 5, 'Login form could not be filled') + def fill_login_form(): + login_form = world.browser.find_by_css('form#login-form') + form_field = login_form.find_by_name(field) + form_field.fill(value) + world.retry_on_excetion(fill_login_form) diff --git a/lms/djangoapps/courseware/features/signup.py b/lms/djangoapps/courseware/features/signup.py index 0dfdae02b1..ef864aa855 100644 --- a/lms/djangoapps/courseware/features/signup.py +++ b/lms/djangoapps/courseware/features/signup.py @@ -6,38 +6,26 @@ from lettuce import world, step @step('I fill in "([^"]*)" on the registration form with "([^"]*)"$') def when_i_fill_in_field_on_the_registration_form_with_value(step, field, value): - attempt = 0 - while attempt < 5: - try: - register_form = world.browser.find_by_css('form#register-form') - form_field = register_form.find_by_name(field) - form_field.fill(value) - break - except: - attempt += 1 + def fill_in_registration(): + register_form = world.browser.find_by_css('form#register-form') + form_field = register_form.find_by_name(field) + form_field.fill(value) + world.retry_on_exception(fill_in_registration) @step('I submit the registration form$') def i_press_the_button_on_the_registration_form(step): - attempt = 0 - while attempt < 5: - try: - register_form = world.browser.find_by_css('form#register-form') - register_form.find_by_name('submit').click() - break - except: - attempt += 1 + def submit_registration(): + register_form = world.browser.find_by_css('form#register-form') + register_form.find_by_name('submit').click() + world.retry_on_exception(submit_registration) @step('I check the checkbox named "([^"]*)"$') def i_check_checkbox(step, checkbox): - attempt = 0 - while attempt < 5: - try: - world.browser.find_by_name(checkbox).check() - break - except: - attempt += 1 + def check_box(): + world.browser.find_by_name(checkbox).check() + world.retry_on_exception(check_box) @step('I should see "([^"]*)" in the dashboard banner$') From b4036c66cfd59b04adfdeae7466efb54384b2b47 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 11 Jul 2013 16:21:06 -0400 Subject: [PATCH 724/995] Refactored the ui tests to use retry_on_exception --- common/djangoapps/terrain/ui_helpers.py | 86 ++++----------------- lms/djangoapps/courseware/features/login.py | 4 +- 2 files changed, 16 insertions(+), 74 deletions(-) diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py index 46fe97548a..e69f53a5dc 100644 --- a/common/djangoapps/terrain/ui_helpers.py +++ b/common/djangoapps/terrain/ui_helpers.py @@ -145,42 +145,12 @@ def id_click(elem_id): @world.absorb def css_fill(css_selector, text, index=0, max_attempts=5): assert is_css_present(css_selector) - attempt = 0 - result = False - while attempt < max_attempts: - try: - world.browser.find_by_css(css_selector)[index].fill(text) - result = True - break - except WebDriverException: - # Occasionally, MathJax or other JavaScript can cover up - # an element temporarily. - # If this happens, wait a second, then try again - world.wait(1) - attempt += 1 - except: - attempt += 1 - assert_true(result, 'Filling {} did not work as expected'.format(css_selector)) + return world.retry_on_exception(lambda: world.browser.find_by_css(css_selector)[index].fill(text), max_attempts=max_attempts) @world.absorb def click_link(partial_text, index=0, max_attempts=5): - attempt = 0 - result = False - while attempt < max_attempts: - try: - world.browser.find_link_by_partial_text(partial_text)[index].click() - result = True - break - except WebDriverException: - # Occasionally, MathJax or other JavaScript can cover up - # an element temporarily. - # If this happens, wait a second, then try again - world.wait(1) - attempt += 1 - except: - attempt += 1 - assert_true(result, 'Clicking {} did not work as expected'.format(partial_text)) + return world.retry_on_exception(lambda: world.browser.find_link_by_partial_text(partial_text)[index].click(), max_attempts=max_attempts) @world.absorb @@ -188,14 +158,7 @@ def css_text(css_selector, index=0, max_attempts=5): # Wait for the css selector to appear if world.is_css_present(css_selector): - attempt = 0 - while attempt < max_attempts: - try: - return world.browser.find_by_css(css_selector)[index].text - break - except: - attempt += 1 - assert_true(attempt < max_attempts, 'Could not access {}'.format(css_selector)) + return world.retry_on_exception(lambda: world.browser.find_by_css(css_selector)[index].text, max_attempts=max_attempts) else: return "" @@ -205,14 +168,7 @@ def css_value(css_selector, index=0, max_attempts=5): # Wait for the css selector to appear if world.is_css_present(css_selector): - attempt = 0 - while attempt < max_attempts: - try: - return world.browser.find_by_css(css_selector)[index].value - break - except: - attempt += 1 - assert_true(attempt < max_attempts, 'Could not access {}'.format(css_selector)) + return world.retry_on_exception(lambda: world.browser.find_by_css(css_selector)[index].value, max_attempts=max_attempts) else: return "" @@ -223,36 +179,18 @@ def css_html(css_selector, index=0, max_attempts=5): Returns the HTML of a css_selector and will retry if there is a StaleElementReferenceException """ assert is_css_present(css_selector) - attempt = 0 - while attempt < max_attempts: - try: - return world.browser.find_by_css(css_selector)[index].html - except: - attempt += 1 - assert_true(attempt < max_attempts, 'Ran out of attempts to access {}'.format(css_selector)) + return world.retry_on_exception(lambda: world.browser.find_by_css(css_selector)[index].html, max_attempts=max_attempts) @world.absorb def css_has_class(css_selector, class_name, index=0, max_attempts=5): - attempt = 0 - while attempt < max_attempts: - try: - return world.css_find(css_selector)[index].has_class(class_name) - except: - attempt += 1 - assert_true(attempt < max_attempts, 'Ran out of attempts to access {}'.format(css_selector)) + return world.retry_on_exception(lambda: world.css_find(css_selector)[index].has_class(class_name), max_attempts=max_attempts) @world.absorb def css_visible(css_selector, index=0, max_attempts=5): assert is_css_present(css_selector) - attempt = 0 - while attempt < max_attempts: - try: - return - except: - attempt += 1 - assert_true(attempt < max_attempts, 'Ran out of attempts to access {}'.format(css_selector)) + return world.retry_on_exception(lambda: world.browser.find_by_css(css_selector)[index].visible, max_attempts=max_attempts) @world.absorb @@ -303,10 +241,14 @@ def is_mac(): @world.absorb def retry_on_exception(func, max_attempts=5): - attempts = 0 - while attempts < max_attempts: + attempt = 0 + while attempt < max_attempts: try: return func() break + except WebDriverException: + world.wait(1) + attempt += 1 except: - attempts += 1 + attempt += 1 + assert_true(attempt < max_attempts, 'Ran out of attempts to execute {}'.format(func)) diff --git a/lms/djangoapps/courseware/features/login.py b/lms/djangoapps/courseware/features/login.py index 6a9a7c9e28..ed788b1bd8 100644 --- a/lms/djangoapps/courseware/features/login.py +++ b/lms/djangoapps/courseware/features/login.py @@ -24,7 +24,7 @@ def i_submit_my_credentials_on_the_login_form(step): def submit_login_form(): login_form = world.browser.find_by_css('form#login-form') login_form.find_by_name('submit').click() - world.retry_on_excetion(submit_login_form) + world.retry_on_exception(submit_login_form) @step(u'I should see the login error message "([^"]*)"$') @@ -57,4 +57,4 @@ def fill_in_the_login_form(field, value): login_form = world.browser.find_by_css('form#login-form') form_field = login_form.find_by_name(field) form_field.fill(value) - world.retry_on_excetion(fill_login_form) + world.retry_on_exception(fill_login_form) From c37a9e3e23abc934996ae0c221203cdcd4973941 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 11 Jul 2013 16:37:39 -0400 Subject: [PATCH 725/995] Refactored problem_setup to use retry_on_exception --- .../courseware/features/problems_setup.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/lms/djangoapps/courseware/features/problems_setup.py b/lms/djangoapps/courseware/features/problems_setup.py index 96dd3cee40..1805da55d0 100644 --- a/lms/djangoapps/courseware/features/problems_setup.py +++ b/lms/djangoapps/courseware/features/problems_setup.py @@ -313,18 +313,13 @@ def assert_checked(problem_type, choices): all_choices = ['choice_0', 'choice_1', 'choice_2', 'choice_3'] for this_choice in all_choices: - attempt = 0 - while attempt < 5: - try: - element = world.css_find(inputfield(problem_type, choice=this_choice)) - if this_choice in choices: - assert element.checked - else: - assert not element.checked - break - except: - attempt += 1 - assert_true(attempt < 5, "Could not access {}".format(element)) + def check_problem(): + element = world.css_find(inputfield(problem_type, choice=this_choice)) + if this_choice in choices: + assert element.checked + else: + assert not element.checked + world.retry_on_exception(check_problem) def assert_textfield(problem_type, expected_text, input_num=1): From d1661fa6aa5b0614aa4c927978ba73758503e8bf Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Fri, 12 Jul 2013 09:05:51 -0400 Subject: [PATCH 726/995] Refactored recently merged in tests --- cms/djangoapps/contentstore/features/common.py | 2 +- cms/djangoapps/contentstore/features/grading.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index 875bc3ced9..2495ea83b2 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -233,7 +233,7 @@ def shows_captions(step, show_captions): def save_button_disabled(step): button_css = '.action-save' disabled = 'is-disabled' - assert world.css_find(button_css)[0].has_class(disabled) + assert world.css_has_class(button_css, disabled) def type_in_codemirror(index, text): diff --git a/cms/djangoapps/contentstore/features/grading.py b/cms/djangoapps/contentstore/features/grading.py index 636722502c..090ca79096 100644 --- a/cms/djangoapps/contentstore/features/grading.py +++ b/cms/djangoapps/contentstore/features/grading.py @@ -104,8 +104,7 @@ def populate_course(step): def changes_not_persisted(step): reload_the_page(step) name_id = '#course-grading-assignment-name' - ele = world.css_find(name_id)[0] - assert(ele.value == 'Homework') + assert(world.css_value(name_id) == 'Homework') @step(u'I see the assignment type "(.*)"$') From 4ecceb292e272bba960b445adea15a6230112e74 Mon Sep 17 00:00:00 2001 From: cahrens Date: Fri, 12 Jul 2013 09:30:36 -0400 Subject: [PATCH 727/995] Add tests for changing table status without staff permissions. --- cms/djangoapps/course_creators/models.py | 9 ++++--- .../course_creators/tests/test_views.py | 25 +++++++++++++------ cms/djangoapps/course_creators/views.py | 15 ++++++----- 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/cms/djangoapps/course_creators/models.py b/cms/djangoapps/course_creators/models.py index 7645b21575..b370ac423a 100644 --- a/cms/djangoapps/course_creators/models.py +++ b/cms/djangoapps/course_creators/models.py @@ -54,20 +54,23 @@ def post_init_callback(sender, **kwargs): @receiver(post_save, sender=CourseCreator) def post_save_callback(sender, **kwargs): """ - Extend to update state_changed time and modify the course creator group in authz.py. + Extend to update state_changed time and fire event to update course creator group, if appropriate. """ instance = kwargs['instance'] # We only wish to modify the state_changed time if the state has been modified. We don't wish to # modify it for changes to the notes field. if instance.state != instance.orig_state: - if hasattr(instance, 'admin'): + # If either old or new state is 'granted', we must manipulate the course creator + # group maintained by authz. That requires staff permissions (stored admin). + if instance.state == CourseCreator.GRANTED or instance.orig_state == CourseCreator.GRANTED: + assert hasattr(instance, 'admin'), 'Must have stored staff user to change course creator group' update_creator_state.send( sender=sender, caller=instance.admin, user=instance.user, add=instance.state == CourseCreator.GRANTED ) - # TODO: Else must be sure that state change does not switch to or from granted + instance.state_changed = timezone.now() instance.orig_state = instance.state instance.save() diff --git a/cms/djangoapps/course_creators/tests/test_views.py b/cms/djangoapps/course_creators/tests/test_views.py index bd91208b9c..943120f3c0 100644 --- a/cms/djangoapps/course_creators/tests/test_views.py +++ b/cms/djangoapps/course_creators/tests/test_views.py @@ -7,7 +7,7 @@ from django.contrib.auth.models import User from django.core.exceptions import PermissionDenied from course_creators.views import add_user_with_status_unrequested, add_user_with_status_granted -from course_creators.views import get_course_creator_status, update_course_creator_group +from course_creators.views import get_course_creator_status, update_course_creator_group, user_requested_access from course_creators.models import CourseCreator from auth.authz import is_user_in_creator_group import mock @@ -26,14 +26,11 @@ class CourseCreatorView(TestCase): def test_staff_permission_required(self): """ - Tests that add methods and course creator group method must be called with staff permissions. + Tests that any method changing the course creator authz group must be called with staff permissions. """ with self.assertRaises(PermissionDenied): add_user_with_status_granted(self.user, self.user) - with self.assertRaises(PermissionDenied): - add_user_with_status_unrequested(self.user, self.user) - with self.assertRaises(PermissionDenied): update_course_creator_group(self.user, self.user, True) @@ -41,7 +38,7 @@ class CourseCreatorView(TestCase): self.assertIsNone(get_course_creator_status(self.user)) def test_add_unrequested(self): - add_user_with_status_unrequested(self.admin, self.user) + add_user_with_status_unrequested(self.user) self.assertEqual('unrequested', get_course_creator_status(self.user)) # Calling add again will be a no-op (even if state is different). @@ -57,7 +54,7 @@ class CourseCreatorView(TestCase): self.assertEqual('granted', get_course_creator_status(self.user)) # Calling add again will be a no-op (even if state is different). - add_user_with_status_unrequested(self.admin, self.user) + add_user_with_status_unrequested(self.user) self.assertEqual('granted', get_course_creator_status(self.user)) self.assertTrue(is_user_in_creator_group(self.user)) @@ -69,3 +66,17 @@ class CourseCreatorView(TestCase): self.assertTrue(is_user_in_creator_group(self.user)) update_course_creator_group(self.admin, self.user, False) self.assertFalse(is_user_in_creator_group(self.user)) + + def test_user_requested_access(self): + add_user_with_status_unrequested(self.user) + self.assertEqual('unrequested', get_course_creator_status(self.user)) + user_requested_access(self.user) + self.assertEqual('pending', get_course_creator_status(self.user)) + + def test_user_requested_already_granted(self): + add_user_with_status_granted(self.admin, self.user) + self.assertEqual('granted', get_course_creator_status(self.user)) + # Will not "downgrade" to pending because that would require removing the + # user from the authz course creator group (and that can only be done by an admin). + user_requested_access(self.user) + self.assertEqual('granted', get_course_creator_status(self.user)) diff --git a/cms/djangoapps/course_creators/views.py b/cms/djangoapps/course_creators/views.py index 12993d1be4..6b2d91acb2 100644 --- a/cms/djangoapps/course_creators/views.py +++ b/cms/djangoapps/course_creators/views.py @@ -20,10 +20,11 @@ def add_user_with_status_granted(caller, user): """ Adds a user to the course creator table with status 'granted'. - If the user is already in the table, this method is a no-op - (state will not be changed). Caller must have staff permissions. - This method also adds the user to the course creator group maintained by authz.py. + Caller must have staff permissions. + + If the user is already in the table, this method is a no-op + (state will not be changed). """ _add_user(user, CourseCreator.GRANTED) update_course_creator_group(caller, user, True) @@ -64,11 +65,13 @@ def user_requested_access(user): """ User has requested course creator access. - This changes the user state to CourseCreator.PENDING. + This changes the user state to CourseCreator.PENDING, unless the user + state is already CourseCreator.GRANTED, in which case this method is a no-op. """ user = CourseCreator.objects.get(user=user) - user.state = CourseCreator.PENDING - user.save() + if user.state != CourseCreator.GRANTED: + user.state = CourseCreator.PENDING + user.save() def _add_user(user, state): From 84f531af54f91f7aa1ef7e28afc698db778c49c4 Mon Sep 17 00:00:00 2001 From: ichuang Date: Fri, 12 Jul 2013 13:40:55 +0000 Subject: [PATCH 728/995] add export_all_courses management script to cms --- .../management/commands/export.py | 2 +- .../management/commands/export_all_courses.py | 49 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 cms/djangoapps/contentstore/management/commands/export_all_courses.py diff --git a/cms/djangoapps/contentstore/management/commands/export.py b/cms/djangoapps/contentstore/management/commands/export.py index eb7800d46c..0087fdddcb 100644 --- a/cms/djangoapps/contentstore/management/commands/export.py +++ b/cms/djangoapps/contentstore/management/commands/export.py @@ -14,7 +14,7 @@ unnamed_modules = 0 class Command(BaseCommand): - help = 'Import the specified data directory into the default ModuleStore' + help = 'Export the specified data directory into the default ModuleStore' def handle(self, *args, **options): if len(args) != 2: diff --git a/cms/djangoapps/contentstore/management/commands/export_all_courses.py b/cms/djangoapps/contentstore/management/commands/export_all_courses.py new file mode 100644 index 0000000000..978418ba7b --- /dev/null +++ b/cms/djangoapps/contentstore/management/commands/export_all_courses.py @@ -0,0 +1,49 @@ +### +### Script for exporting all courseware from Mongo to a directory +### +import os + +from django.core.management.base import BaseCommand, CommandError +from xmodule.modulestore.xml_exporter import export_to_xml +from xmodule.modulestore.django import modulestore +from xmodule.contentstore.django import contentstore +from xmodule.course_module import CourseDescriptor + + +unnamed_modules = 0 + + +class Command(BaseCommand): + help = 'Export all courses from mongo to the specified data directory' + + def handle(self, *args, **options): + if len(args) != 1: + raise CommandError("import requires one argument: ") + + output_path = args[0] + + cs = contentstore() + ms = modulestore('direct') + root_dir = output_path + courses = ms.get_courses() + + print "%d courses to export:" % len(courses) + cids = [x.id for x in courses] + print cids + + for course_id in cids: + + print "-"*77 + print "Exporting course id = {0} to {1}".format(course_id, output_path) + + if 1: + try: + location = CourseDescriptor.id_to_location(course_id) + course_dir = course_id.replace('/','...') + export_to_xml(ms, cs, location, root_dir, course_dir) + except Exception as err: + print "="*30 + "> Oops, failed to export %s" % course_id + print "Error:" + print err + + From 84c38b4e9a58e6ea2c317212a8fe96dc58195c32 Mon Sep 17 00:00:00 2001 From: ichuang Date: Fri, 12 Jul 2013 09:43:52 -0400 Subject: [PATCH 729/995] fix two typos in export scripts --- cms/djangoapps/contentstore/management/commands/export.py | 2 +- .../contentstore/management/commands/export_all_courses.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cms/djangoapps/contentstore/management/commands/export.py b/cms/djangoapps/contentstore/management/commands/export.py index 0087fdddcb..52a5eb265b 100644 --- a/cms/djangoapps/contentstore/management/commands/export.py +++ b/cms/djangoapps/contentstore/management/commands/export.py @@ -18,7 +18,7 @@ class Command(BaseCommand): def handle(self, *args, **options): if len(args) != 2: - raise CommandError("import requires two arguments: ") + raise CommandError("export requires two arguments: ") course_id = args[0] output_path = args[1] diff --git a/cms/djangoapps/contentstore/management/commands/export_all_courses.py b/cms/djangoapps/contentstore/management/commands/export_all_courses.py index 978418ba7b..5d0adaa315 100644 --- a/cms/djangoapps/contentstore/management/commands/export_all_courses.py +++ b/cms/djangoapps/contentstore/management/commands/export_all_courses.py @@ -18,7 +18,7 @@ class Command(BaseCommand): def handle(self, *args, **options): if len(args) != 1: - raise CommandError("import requires one argument: ") + raise CommandError("export requires one argument: ") output_path = args[0] From 5fd84465e2ec5d8720b14f0def6b75b0cc5a48ca Mon Sep 17 00:00:00 2001 From: Miki Goyal Date: Fri, 12 Jul 2013 10:05:13 -0400 Subject: [PATCH 730/995] Fix alt text for the login page: LMS-600 --- lms/templates/login.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/templates/login.html b/lms/templates/login.html index fd51e429e9..74e1376abf 100644 --- a/lms/templates/login.html +++ b/lms/templates/login.html @@ -80,7 +80,7 @@
      -

      ${_("Log Into Your Account")}

      +

      ${_("PLEASE LOG IN to access your account and courses")}

      From 08b8438dda9a5e6ec2e8b4b3136a189160bf561d Mon Sep 17 00:00:00 2001 From: ichuang Date: Fri, 12 Jul 2013 14:10:16 +0000 Subject: [PATCH 731/995] add extra modulestore() argument to export to make it export drafts also --- .../contentstore/management/commands/export_all_courses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/management/commands/export_all_courses.py b/cms/djangoapps/contentstore/management/commands/export_all_courses.py index 5d0adaa315..3c7c398e94 100644 --- a/cms/djangoapps/contentstore/management/commands/export_all_courses.py +++ b/cms/djangoapps/contentstore/management/commands/export_all_courses.py @@ -40,7 +40,7 @@ class Command(BaseCommand): try: location = CourseDescriptor.id_to_location(course_id) course_dir = course_id.replace('/','...') - export_to_xml(ms, cs, location, root_dir, course_dir) + export_to_xml(ms, cs, location, root_dir, course_dir, modulestore()) except Exception as err: print "="*30 + "> Oops, failed to export %s" % course_id print "Error:" From c53dd97f79827a3cfd818cf90f88f6bc94319cd4 Mon Sep 17 00:00:00 2001 From: ichuang Date: Fri, 12 Jul 2013 14:11:20 +0000 Subject: [PATCH 732/995] add extra modulestore() argument to single export script --- cms/djangoapps/contentstore/management/commands/export.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/management/commands/export.py b/cms/djangoapps/contentstore/management/commands/export.py index 52a5eb265b..90db8750d9 100644 --- a/cms/djangoapps/contentstore/management/commands/export.py +++ b/cms/djangoapps/contentstore/management/commands/export.py @@ -30,4 +30,4 @@ class Command(BaseCommand): root_dir = os.path.dirname(output_path) course_dir = os.path.splitext(os.path.basename(output_path))[0] - export_to_xml(modulestore('direct'), contentstore(), location, root_dir, course_dir) + export_to_xml(modulestore('direct'), contentstore(), location, root_dir, course_dir, modulestore()) From f27ca66f51e555779f1ebba6f5167671d55dd192 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Fri, 12 Jul 2013 10:11:39 -0400 Subject: [PATCH 733/995] Added documentation to new acceptance test features and techniques --- doc/testing.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/doc/testing.md b/doc/testing.md index b40ba30610..96f0dcfd98 100644 --- a/doc/testing.md +++ b/doc/testing.md @@ -175,6 +175,8 @@ Use `rake -T` to get a list of all available subsystems **Troubleshooting**: If you get an error message while running the `rake` task, try running `bundle install` to install the required ruby gems. +Unit tests can be run in parallel to each other and while acceptance tests are running + ### Running Acceptance Tests We use [Lettuce](http://lettuce.it/) for acceptance testing. @@ -203,6 +205,10 @@ To start the debugger on failure, add the `--pdb` option: To run tests faster by not collecting static files, you can use `rake fasttest_acceptance_lms` and `rake fasttest_acceptance_cms`. +Acceptance tests will run on a randomized port and can be run in the background of rake cms and lms or unit tests. +To specify the port, change the LETTUCE_SERVER_PORT constant in cms/envs/acceptance.py and lms/envs/acceptance.py +as well as the port listed in cms/djangoapps/contentstore/feature/upload.py + **Note**: The acceptance tests can *not* currently run in parallel. ## Viewing Test Coverage @@ -230,3 +236,30 @@ When testing problems that use a queue server on AWS (e.g. sandbox-xqueue.edx.or `django-admin.py runserver --settings=lms.envs.dev --pythonpath=. 0.0.0.0:8000` When you connect to the LMS, you need to use the public ip. Use `ifconfig` to figure out the number, and connect e.g. to `http://18.3.4.5:8000/` + + +## Acceptance Test Techniques + +1. Do not assert not if possible for css. Use world.is_css_present and is_css_not_present + Errors can arise if checks for the css are performed before the page finishes loading. + To get around this, there are functions that will wait a period of time for the css to appear + before returning and return immediately if they are there. There is a reverse to this function as well. + It will wait for the css to not appear and returns if it isn't there. + + All css functions can utilize this timeout to ensure that the page is fully loaded + +2. Dealing with alerts + Chrome can hang on javascripts alerts. If a javascript alert/prompt/confirmation is expected, use the step + 'I will confirm all alerts', 'I will cancel all alerts' or 'I will anser all prompts with "(.*)"' before the step + that causes the alert in order to properly deal with it. + +3. Dealing with stale element reference exceptions + These exceptions happen if any part of the page is refreshed in between finding an element and accessing the element. + When possible, use any of the css functions in common/djangoapps/terrain/ui_helpers.py as they will retry the action + in case of this exception. If the functionality is not there, wrap the function with world.retry_on_exception. This function takes in a function and will retry and return the result of the function if there was an exception + +4. Scenario Level Constants + If you want an object to be available for the entire scenario, it can be stored in world.scenario_dict. This object + is a dictionary that gets refreshed at the beginning on the scenario. Currently, the current logged in user and the current created course are stored under 'COURSE' and 'USER'. This will help prevent strings from being hard coded so the + acceptance tests can become more flexible. + From bdca0f1d0e5be6fe15a13d48e4b7e2bd9f55eb1c Mon Sep 17 00:00:00 2001 From: Xavier Antoviaque Date: Fri, 12 Jul 2013 11:15:00 -0300 Subject: [PATCH 734/995] vagrant: Always mount `node_modules` folder after Vagrant NFS mount --- scripts/vagrant-provisioning.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/scripts/vagrant-provisioning.sh b/scripts/vagrant-provisioning.sh index 153bc7a879..d4f627b2c4 100755 --- a/scripts/vagrant-provisioning.sh +++ b/scripts/vagrant-provisioning.sh @@ -59,7 +59,14 @@ chown vagrant.vagrant ~vagrant/.ssh/known_hosts # Node modules require a filesystem with symlinks (Windows support) mkdir -p /opt/edx/node_modules /opt/edx/edx-platform/node_modules -mount -o bind /opt/edx/node_modules /opt/edx/edx-platform/node_modules +([[ -f /etc/fstab ]] && grep '/opt/edx/node_modules' /etc/fstab) || { + echo '/opt/edx/node_modules /opt/edx/edx-platform/node_modules none bind,noauto 0 0' >> /etc/fstab + mount /opt/edx/node_modules +} +# Must be mounted *after* the NFS mount, made manually by Vagrant +([[ -f /etc/cron.d/nodemodules ]] && grep '/opt/edx/node_modules' /etc/cron.d/nodemodules) || { + echo '@reboot root until [ -n "`mount |grep "/opt/edx/edx-platform type"`" ]; do sleep 1; done; mount /opt/edx/node_modules' > /etc/cron.d/nodemodules +} # Force rechecking all prerequisites (could have been fetched outside of the VM) rm -rf /opt/edx/edx-platform/.prereqs_cache From 4c8eb6b0d50a63b859fae3db485bec30e8fa2b75 Mon Sep 17 00:00:00 2001 From: Miki Goyal Date: Fri, 12 Jul 2013 10:24:48 -0400 Subject: [PATCH 735/995] Adding myself to authors file --- AUTHORS | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 697f42b36c..70af9f318d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -79,4 +79,5 @@ Bethany LaPenta Renzo Lucioni Felix Sun Adam Palay -Ian Hoover \ No newline at end of file +Ian Hoover +Mukul Goyal From 35094a968ba8e13ac019b2afd8989c3a14be672f Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Tue, 25 Jun 2013 17:00:26 -0400 Subject: [PATCH 736/995] Beginnings of refactoring the acceptance tests --- .../contentstore/features/common.py | 25 +++++++++------- .../contentstore/features/course-team.py | 6 ++-- .../contentstore/features/courses.py | 2 +- common/djangoapps/terrain/browser.py | 8 +++++ common/djangoapps/terrain/course_helpers.py | 6 ++-- common/djangoapps/terrain/steps.py | 8 ++--- lms/djangoapps/courseware/features/common.py | 30 +++++++++---------- .../courseware/features/navigation.py | 23 +++++++------- .../courseware/features/problems.py | 23 +++++++------- .../courseware/features/registration.py | 5 ++-- lms/djangoapps/courseware/features/video.py | 18 +++++------ 11 files changed, 84 insertions(+), 70 deletions(-) diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index 2495ea83b2..ecfdfce06a 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -14,10 +14,6 @@ logger = getLogger(__name__) from terrain.browser import reset_data -_COURSE_NAME = 'Robot Super Course' -_COURSE_NUM = '999' -_COURSE_ORG = 'MITx' - ########### STEP HELPERS ############## @@ -124,9 +120,9 @@ def create_studio_user( def fill_in_course_info( - name=_COURSE_NAME, - org=_COURSE_ORG, - num=_COURSE_NUM): + name=world.scenario_dict['COURSE_NAME'], + org=world.scenario_dict['COURSE_ORG'], + num=world.scenario_dict['COURSE_NUM']): world.css_fill('.new-course-name', name) world.css_fill('.new-course-org', org) world.css_fill('.new-course-number', num) @@ -151,15 +147,24 @@ def log_into_studio( login_form.find_by_name('submit').click() world.retry_on_exception(fill_login_form) assert_true(world.is_css_present('.new-course-button')) + world.scenario_dict['username'] = uname + world.scenario_dict['userpassword'] = password + world.scenario_dict['useremail'] = email def create_a_course(): - world.CourseFactory.create(org=_COURSE_ORG, course=_COURSE_NUM, display_name=_COURSE_NAME) + world.scenario_dict['COURSE_NAME'] = 'Robot Super Course' + world.scenario_dict['COURSE_NUM'] = '999' + world.scenario_dict['COURSE_ORG'] = 'MITx' + world.CourseFactory.create(org=world.scenario_dict['COURSE_ORG'], course=world.scenario_dict['COURSE_NUM'], display_name=world.scenario_dict['COURSE_NAME']) # Add the user to the instructor group of the course # so they will have the permissions to see it in studio - 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') + course = world.GroupFactory.create(name='instructor_MITx/{course_num}/{course_name}'.format(course_num=world.scenario_dict['COURSE_NUM'], course_name=world.scenario_dict['COURSE_NAME'].replace(" ", "_"))) + if world.scenario_dict['useremail']: + user = get_user_by_email(world.scenario_dict['useremail']) + else: + user = get_user_by_email('robot+studio@edx.org') user.groups.add(course) user.save() world.browser.reload() diff --git a/cms/djangoapps/contentstore/features/course-team.py b/cms/djangoapps/contentstore/features/course-team.py index 71d9d9fb02..ea73253c4d 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 PASSWORD = 'test' EMAIL_EXTENSION = '@edx.org' @@ -50,9 +50,9 @@ def see_course(_step, doesnt_see_course, gender): all_courses = world.css_find(class_css, wait_time=1) all_names = [item.html for item in all_courses] if doesnt_see_course: - assert not _COURSE_NAME in all_names + assert not world.scenario_dict['COURSE_NAME'] in all_names else: - assert _COURSE_NAME in all_names + assert world.scenario_dict['COURSE_NAME'] in all_names @step(u's?he cannot delete users') diff --git a/cms/djangoapps/contentstore/features/courses.py b/cms/djangoapps/contentstore/features/courses.py index 5b279d402f..dd7a1a7a28 100644 --- a/cms/djangoapps/contentstore/features/courses.py +++ b/cms/djangoapps/contentstore/features/courses.py @@ -45,7 +45,7 @@ def courseware_page_has_loaded_in_studio(step): @step('I see the course listed in My Courses$') def i_see_the_course_in_my_courses(step): course_css = 'span.class-name' - assert world.css_has_text(course_css, 'Robot Super Course') + assert world.css_has_text(course_css, world.scenario_dict['COURSE_NAME']) @step('I am on the "([^"]*)" tab$') diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 0c1303ed1a..c338d403d6 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -79,6 +79,7 @@ def initial_setup(server): # Set the browser size to 1280x1024 world.browser.driver.set_window_size(1280, 1024) + #world.absorb({}, 'scenario_dict') @before.each_scenario @@ -89,6 +90,13 @@ def reset_data(scenario): """ LOGGER.debug("Flushing the test database...") call_command('flush', interactive=False) + world.absorb({}, 'scenario_dict') + + +@after.each_scenario +def clear_data(scenario): + world.spew('scenario_dict') + @after.each_scenario diff --git a/common/djangoapps/terrain/course_helpers.py b/common/djangoapps/terrain/course_helpers.py index 7da49e6315..78ba7d2cbc 100644 --- a/common/djangoapps/terrain/course_helpers.py +++ b/common/djangoapps/terrain/course_helpers.py @@ -17,14 +17,14 @@ from urllib import quote_plus @world.absorb -def create_user(uname): +def create_user(uname, password): # If the user already exists, don't try to create it again if len(User.objects.filter(username=uname)) > 0: return portal_user = UserFactory.build(username=uname, email=uname + '@edx.org') - portal_user.set_password('test') + portal_user.set_password(password) portal_user.save() registration = world.RegistrationFactory(user=portal_user) @@ -62,6 +62,8 @@ def log_in(username, password): cookie_dict = {settings.SESSION_COOKIE_NAME: request.session.session_key} world.browser.cookies.delete() world.browser.cookies.add(cookie_dict) + world.scenario_dict['username'] = username + world.scenario_dict['userpassword'] = password @world.absorb diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index e69476a5b7..bd0b448e11 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -93,7 +93,7 @@ def i_log_in(step): @step('I am a logged in user$') def i_am_logged_in_user(step): - world.create_user('robot') + world.create_user('robot', 'test') world.log_in('robot', 'test') @@ -139,7 +139,7 @@ def should_see_in_the_page(step, doesnt_appear, text): @step('I am logged in$') def i_am_logged_in(step): - world.create_user('robot') + world.create_user('robot', 'test') world.log_in('robot', 'test') world.browser.visit(django_url('/')) # You should not see the login link @@ -148,12 +148,12 @@ def i_am_logged_in(step): @step(u'I am an edX user$') def i_am_an_edx_user(step): - world.create_user('robot') + world.create_user('robot', 'test') @step(u'User "([^"]*)" is an edX user$') def registered_edx_user(step, uname): - world.create_user(uname) + world.create_user(uname, 'test') @step(u'All dialogs should be closed$') diff --git a/lms/djangoapps/courseware/features/common.py b/lms/djangoapps/courseware/features/common.py index 50679fec6f..3837dbe13d 100644 --- a/lms/djangoapps/courseware/features/common.py +++ b/lms/djangoapps/courseware/features/common.py @@ -18,13 +18,13 @@ from xmodule import seq_module, vertical_module from logging import getLogger logger = getLogger(__name__) -TEST_COURSE_ORG = 'edx' -TEST_COURSE_NAME = 'Test Course' -TEST_SECTION_NAME = 'Test Section' - @step(u'The course "([^"]*)" exists$') def create_course(step, course): + world.scenario_dict['COURSE_NUM'] = course + world.scenario_dict['SECTION_NAME'] = 'Test Section' + world.scenario_dict['COURSE_NAME'] = 'Test Course' + world.scenario_dict['COURSE_ORG'] = 'edx' # First clear the modulestore so we don't try to recreate # the same course twice @@ -34,17 +34,17 @@ def create_course(step, course): # Create the course # We always use the same org and display name, # but vary the course identifier (e.g. 600x or 191x) - course = world.CourseFactory.create(org=TEST_COURSE_ORG, + course = world.CourseFactory.create(org=world.scenario_dict['COURSE_ORG'], number=course, - display_name=TEST_COURSE_NAME) + display_name=world.scenario_dict['COURSE_NAME']) # Add a section to the course to contain problems section = world.ItemFactory.create(parent_location=course.location, - display_name=TEST_SECTION_NAME) + display_name=world.scenario_dict['SECTION_NAME']) problem_section = world.ItemFactory.create(parent_location=section.location, template='i4x://edx/templates/sequential/Empty', - display_name=TEST_SECTION_NAME) + display_name=world.scenario_dict['SECTION_NAME']) @step(u'I am registered for the course "([^"]*)"$') @@ -53,7 +53,7 @@ def i_am_registered_for_the_course(step, course): create_course(step, course) # Create the user - world.create_user('robot') + world.create_user('robot', 'test') u = User.objects.get(username='robot') # If the user is not already enrolled, enroll the user. @@ -71,24 +71,24 @@ def add_tab_to_course(step, course, extra_tab_name): def course_id(course_num): - return "%s/%s/%s" % (TEST_COURSE_ORG, course_num, - TEST_COURSE_NAME.replace(" ", "_")) + return "%s/%s/%s" % (world.scenario_dict['COURSE_ORG'], course_num, + world.scenario_dict['COURSE_NAME'].replace(" ", "_")) def course_location(course_num): return Location(loc_or_tag="i4x", - org=TEST_COURSE_ORG, + org=world.scenario_dict['COURSE_ORG'], course=course_num, category='course', - name=TEST_COURSE_NAME.replace(" ", "_")) + name=world.scenario_dict['COURSE_NAME'].replace(" ", "_")) def section_location(course_num): return Location(loc_or_tag="i4x", - org=TEST_COURSE_ORG, + org=world.scenario_dict['COURSE_ORG'], course=course_num, category='sequential', - name=TEST_SECTION_NAME.replace(" ", "_")) + name=world.scenario_dict['SECTION_NAME'].replace(" ", "_")) def get_courses(): diff --git a/lms/djangoapps/courseware/features/navigation.py b/lms/djangoapps/courseware/features/navigation.py index 96d5a3de93..b2258a8945 100644 --- a/lms/djangoapps/courseware/features/navigation.py +++ b/lms/djangoapps/courseware/features/navigation.py @@ -8,8 +8,6 @@ from student.models import CourseEnrollment from common import course_id, course_location from problems_setup import PROBLEM_DICT -TEST_COURSE_ORG = 'edx' -TEST_COURSE_NAME = 'Test Course' TEST_SECTION_NAME = 'Test Section' TEST_SUBSECTION_NAME = 'Test Subsection' @@ -18,11 +16,11 @@ TEST_SUBSECTION_NAME = 'Test Subsection' def view_course_multiple_sections(step): create_course() # Add a section to the course to contain problems - section1 = world.ItemFactory.create(parent_location=course_location('model_course'), + section1 = world.ItemFactory.create(parent_location=course_location(world.scenario_dict['COURSE_NUM']), display_name=section_name(1)) # Add a section to the course to contain problems - section2 = world.ItemFactory.create(parent_location=course_location('model_course'), + section2 = world.ItemFactory.create(parent_location=course_location(world.scenario_dict['COURSE_NUM']), display_name=section_name(2)) place1 = world.ItemFactory.create(parent_location=section1.location, @@ -44,7 +42,7 @@ def view_course_multiple_subsections(step): create_course() # Add a section to the course to contain problems - section1 = world.ItemFactory.create(parent_location=course_location('model_course'), + section1 = world.ItemFactory.create(parent_location=course_location(world.scenario_dict['COURSE_NUM']), display_name=section_name(1)) place1 = world.ItemFactory.create(parent_location=section1.location, @@ -64,7 +62,7 @@ def view_course_multiple_subsections(step): def view_course_multiple_sequences(step): create_course() # Add a section to the course to contain problems - section1 = world.ItemFactory.create(parent_location=course_location('model_course'), + section1 = world.ItemFactory.create(parent_location=course_location(world.scenario_dict['COURSE_NUM']), display_name=section_name(1)) place1 = world.ItemFactory.create(parent_location=section1.location, @@ -144,16 +142,19 @@ def subsection_name(section): def create_course(): world.clear_courses() - world.CourseFactory.create(org=TEST_COURSE_ORG, - number="model_course", - display_name=TEST_COURSE_NAME) + world.scenario_dict['COURSE_NAME'] = 'Test Course' + world.scenario_dict['COURSE_ORG'] = 'edx' + world.scenario_dict['COURSE_NUM'] = 'model_course' + world.CourseFactory.create(org=world.scenario_dict['COURSE_ORG'], + number=world.scenario_dict['COURSE_NUM'], + display_name=world.scenario_dict['COURSE_NAME']) def create_user_and_visit_course(): - world.create_user('robot') + world.create_user('robot', 'test') u = User.objects.get(username='robot') - CourseEnrollment.objects.get_or_create(user=u, course_id=course_id("model_course")) + CourseEnrollment.objects.get_or_create(user=u, course_id=course_id(world.scenario_dict['COURSE_NUM'])) world.log_in('robot', 'test') chapter_name = (TEST_SECTION_NAME + "1").replace(" ", "_") diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index e97533f4db..c7f34ce683 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -7,7 +7,7 @@ Steps for problem.feature lettuce tests from lettuce import world, step from lettuce.django import django_url -from common import i_am_registered_for_the_course, TEST_SECTION_NAME +from common import i_am_registered_for_the_course from problems_setup import PROBLEM_DICT, answer_problem, problem_has_answer, add_problem_to_course from nose.tools import assert_equal @@ -17,15 +17,14 @@ def view_problem_with_attempts(step, problem_type, attempts): i_am_registered_for_the_course(step, 'model_course') # Ensure that the course has this problem type - add_problem_to_course('model_course', problem_type, {'attempts': attempts}) + add_problem_to_course(world.scenario_dict['COURSE_NUM'], problem_type, {'attempts': attempts}) # Go to the one section in the factory-created course # which should be loaded with the correct problem - chapter_name = TEST_SECTION_NAME.replace(" ", "_") + chapter_name = world.scenario_dict['SECTION_NAME'].replace(" ", "_") section_name = chapter_name - url = django_url('/courses/edx/model_course/Test_Course/courseware/%s/%s' % - (chapter_name, section_name)) - + url = django_url('/courses/%s/%s/%s/courseware/%s/%s' % + (world.scenario_dict['COURSE_ORG'], world.scenario_dict['COURSE_NUM'], world.scenario_dict['COURSE_NAME'].replace(' ', '_'), chapter_name, section_name,)) world.browser.visit(url) @@ -38,10 +37,10 @@ def view_problem_with_show_answer(step, problem_type, answer): # Go to the one section in the factory-created course # which should be loaded with the correct problem - chapter_name = TEST_SECTION_NAME.replace(" ", "_") + chapter_name = world.scenario_dict['SECTION_NAME'].replace(" ", "_") section_name = chapter_name - url = django_url('/courses/edx/model_course/Test_Course/courseware/%s/%s' % - (chapter_name, section_name)) + url = django_url('/courses/%s/%s/%s/courseware/%s/%s' % + (world.scenario_dict['COURSE_ORG'], world.scenario_dict['COURSE_NUM'], world.scenario_dict['COURSE_NAME'].replace(' ', '_'), chapter_name, section_name,)) world.browser.visit(url) @@ -55,10 +54,10 @@ def view_problem(step, problem_type): # Go to the one section in the factory-created course # which should be loaded with the correct problem - chapter_name = TEST_SECTION_NAME.replace(" ", "_") + chapter_name = world.scenario_dict['SECTION_NAME'].replace(" ", "_") section_name = chapter_name - url = django_url('/courses/edx/model_course/Test_Course/courseware/%s/%s' % - (chapter_name, section_name)) + url = django_url('/courses/%s/%s/%s/courseware/%s/%s' % + (world.scenario_dict['COURSE_ORG'], world.scenario_dict['COURSE_NUM'], world.scenario_dict['COURSE_NAME'].replace(' ', '_'), chapter_name, section_name,)) world.browser.visit(url) diff --git a/lms/djangoapps/courseware/features/registration.py b/lms/djangoapps/courseware/features/registration.py index e5edb41575..25dc0bf1cd 100644 --- a/lms/djangoapps/courseware/features/registration.py +++ b/lms/djangoapps/courseware/features/registration.py @@ -3,13 +3,12 @@ from lettuce import world, step from lettuce.django import django_url -from common import TEST_COURSE_ORG, TEST_COURSE_NAME @step('I register for the course "([^"]*)"$') def i_register_for_the_course(_step, course): - cleaned_name = TEST_COURSE_NAME.replace(' ', '_') - url = django_url('courses/%s/%s/%s/about' % (TEST_COURSE_ORG, course, cleaned_name)) + cleaned_name = world.scenario_dict['COURSE_NAME'].replace(' ', '_') + url = django_url('courses/%s/%s/%s/about' % (world.scenario_dict['COURSE_ORG'], course, cleaned_name)) world.browser.visit(url) world.css_click('section.intro a.register') diff --git a/lms/djangoapps/courseware/features/video.py b/lms/djangoapps/courseware/features/video.py index cd1bdcf60f..0a57b4dc72 100644 --- a/lms/djangoapps/courseware/features/video.py +++ b/lms/djangoapps/courseware/features/video.py @@ -2,7 +2,7 @@ from lettuce import world, step from lettuce.django import django_url -from common import TEST_COURSE_NAME, TEST_SECTION_NAME, i_am_registered_for_the_course, section_location +from common import i_am_registered_for_the_course, section_location ############### ACTIONS #################### @@ -14,30 +14,30 @@ def does_autoplay(_step): @step('the course has a Video component') def view_video(_step): - coursename = TEST_COURSE_NAME.replace(' ', '_') + coursename = world.scenario_dict['COURSE_NAME'].replace(' ', '_') i_am_registered_for_the_course(step, coursename) # Make sure we have a video add_video_to_course(coursename) - chapter_name = TEST_SECTION_NAME.replace(" ", "_") + chapter_name = world.scenario_dict['SECTION_NAME'].replace(" ", "_") section_name = chapter_name - url = django_url('/courses/edx/Test_Course/Test_Course/courseware/%s/%s' % - (chapter_name, section_name)) + url = django_url('/courses/%s/%s/%s/courseware/%s/%s' % + (world.scenario_dict['COURSE_ORG'], world.scenario_dict['COURSE_NUM'], world.scenario_dict['COURSE_NAME'].replace(' ', '_'), chapter_name, section_name,)) world.browser.visit(url) @step('the course has a VideoAlpha component') def view_videoalpha(step): - coursename = TEST_COURSE_NAME.replace(' ', '_') + coursename = world.scenario_dict['COURSE_NAME'].replace(' ', '_') i_am_registered_for_the_course(step, coursename) # Make sure we have a videoalpha add_videoalpha_to_course(coursename) - chapter_name = TEST_SECTION_NAME.replace(" ", "_") + chapter_name = world.scenario_dict['SECTION_NAME'].replace(" ", "_") section_name = chapter_name - url = django_url('/courses/edx/Test_Course/Test_Course/courseware/%s/%s' % - (chapter_name, section_name)) + url = django_url('/courses/%s/%s/%s/courseware/%s/%s' % + (world.scenario_dict['COURSE_ORG'], world.scenario_dict['COURSE_NUM'], world.scenario_dict['COURSE_NAME'].replace(' ', '_'), chapter_name, section_name,)) world.browser.visit(url) From 131f1a49c4123889aae6c66d50ac0d2d2fc7ccf8 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Wed, 26 Jun 2013 08:56:48 -0400 Subject: [PATCH 737/995] All lms acceptance tests refactored to use world dictionaries --- lms/djangoapps/courseware/features/video.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lms/djangoapps/courseware/features/video.py b/lms/djangoapps/courseware/features/video.py index 0a57b4dc72..4ea70ef4de 100644 --- a/lms/djangoapps/courseware/features/video.py +++ b/lms/djangoapps/courseware/features/video.py @@ -14,11 +14,11 @@ def does_autoplay(_step): @step('the course has a Video component') def view_video(_step): - coursename = world.scenario_dict['COURSE_NAME'].replace(' ', '_') - i_am_registered_for_the_course(step, coursename) + coursenum = 'test_course' + i_am_registered_for_the_course(step, coursenum) # Make sure we have a video - add_video_to_course(coursename) + add_video_to_course(coursenum) chapter_name = world.scenario_dict['SECTION_NAME'].replace(" ", "_") section_name = chapter_name url = django_url('/courses/%s/%s/%s/courseware/%s/%s' % @@ -29,11 +29,11 @@ def view_video(_step): @step('the course has a VideoAlpha component') def view_videoalpha(step): - coursename = world.scenario_dict['COURSE_NAME'].replace(' ', '_') - i_am_registered_for_the_course(step, coursename) + coursenum = 'test_course' + i_am_registered_for_the_course(step, coursenum) # Make sure we have a videoalpha - add_videoalpha_to_course(coursename) + add_videoalpha_to_course(coursenum) chapter_name = world.scenario_dict['SECTION_NAME'].replace(" ", "_") section_name = chapter_name url = django_url('/courses/%s/%s/%s/courseware/%s/%s' % From 8110307ba936e1242508d7b78e39523ac13f9f45 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Wed, 26 Jun 2013 09:57:06 -0400 Subject: [PATCH 738/995] CMS now refactored to use world dictionary --- cms/djangoapps/contentstore/features/common.py | 6 +++--- cms/djangoapps/contentstore/features/grading.py | 2 +- common/test/data/uploads/test | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index ecfdfce06a..e7e73df020 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -120,9 +120,9 @@ def create_studio_user( def fill_in_course_info( - name=world.scenario_dict['COURSE_NAME'], - org=world.scenario_dict['COURSE_ORG'], - num=world.scenario_dict['COURSE_NUM']): + name='Robot Super Course', + org='MITx', + num='999'): world.css_fill('.new-course-name', name) world.css_fill('.new-course-org', org) world.css_fill('.new-course-number', num) diff --git a/cms/djangoapps/contentstore/features/grading.py b/cms/djangoapps/contentstore/features/grading.py index 090ca79096..e09954a556 100644 --- a/cms/djangoapps/contentstore/features/grading.py +++ b/cms/djangoapps/contentstore/features/grading.py @@ -64,7 +64,7 @@ def change_assignment_name(step, old_name, new_name): @step(u'I go back to the main course page') def main_course_page(step): - main_page_link_css = 'a[href="/MITx/999/course/Robot_Super_Course"]' + main_page_link_css = 'a[href="/%s/%s/course/%s"]' % (world.scenario_dict['COURSE_ORG'], world.scenario_dict['COURSE_NUM'], world.scenario_dict['COURSE_NAME'].replace(' ', '_'),) world.css_click(main_page_link_css) diff --git a/common/test/data/uploads/test b/common/test/data/uploads/test index f019db7176..0424951e34 100644 --- a/common/test/data/uploads/test +++ b/common/test/data/uploads/test @@ -1 +1 @@ -R2FUIGM88K \ No newline at end of file +This is an arbitrary file for testing uploads From 1f1bba55070cf2b672c40603c1eb48b317455435 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 27 Jun 2013 16:32:35 -0400 Subject: [PATCH 739/995] Scenario Dictionary now holds objects --- .../contentstore/features/common.py | 16 ++++------- .../contentstore/features/course-team.py | 4 +-- .../contentstore/features/courses.py | 2 +- .../contentstore/features/grading.py | 2 +- common/djangoapps/terrain/course_helpers.py | 4 +-- lms/djangoapps/courseware/features/common.py | 28 ++++++++----------- .../courseware/features/navigation.py | 17 ++++------- .../courseware/features/problems.py | 13 +++++---- .../courseware/features/registration.py | 4 +-- lms/djangoapps/courseware/features/video.py | 8 +++--- 10 files changed, 42 insertions(+), 56 deletions(-) diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index e7e73df020..dd115f76ef 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -147,22 +147,18 @@ def log_into_studio( login_form.find_by_name('submit').click() world.retry_on_exception(fill_login_form) assert_true(world.is_css_present('.new-course-button')) - world.scenario_dict['username'] = uname - world.scenario_dict['userpassword'] = password - world.scenario_dict['useremail'] = email + world.scenario_dict['USER'] = get_user_by_email(email) def create_a_course(): - world.scenario_dict['COURSE_NAME'] = 'Robot Super Course' - world.scenario_dict['COURSE_NUM'] = '999' - world.scenario_dict['COURSE_ORG'] = 'MITx' - world.CourseFactory.create(org=world.scenario_dict['COURSE_ORG'], course=world.scenario_dict['COURSE_NUM'], display_name=world.scenario_dict['COURSE_NAME']) + world.scenario_dict['COURSE'] = world.CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course') # Add the user to the instructor group of the course # so they will have the permissions to see it in studio - course = world.GroupFactory.create(name='instructor_MITx/{course_num}/{course_name}'.format(course_num=world.scenario_dict['COURSE_NUM'], course_name=world.scenario_dict['COURSE_NAME'].replace(" ", "_"))) - if world.scenario_dict['useremail']: - user = get_user_by_email(world.scenario_dict['useremail']) + + course = world.GroupFactory.create(name='instructor_MITx/{course_num}/{course_name}'.format(course_num=world.scenario_dict['COURSE'].number, course_name=world.scenario_dict['COURSE_NAME'].display_name.replace(" ", "_"))) + if world.scenario_dict['USER']: + user = world.scenario_dict['USER'] else: user = get_user_by_email('robot+studio@edx.org') user.groups.add(course) diff --git a/cms/djangoapps/contentstore/features/course-team.py b/cms/djangoapps/contentstore/features/course-team.py index ea73253c4d..ad5d31977c 100644 --- a/cms/djangoapps/contentstore/features/course-team.py +++ b/cms/djangoapps/contentstore/features/course-team.py @@ -50,9 +50,9 @@ def see_course(_step, doesnt_see_course, gender): all_courses = world.css_find(class_css, wait_time=1) all_names = [item.html for item in all_courses] if doesnt_see_course: - assert not world.scenario_dict['COURSE_NAME'] in all_names + assert not world.scenario_dict['COURSE'].display_name in all_names else: - assert world.scenario_dict['COURSE_NAME'] in all_names + assert world.scenario_dict['COURSE'].display_name in all_names @step(u's?he cannot delete users') diff --git a/cms/djangoapps/contentstore/features/courses.py b/cms/djangoapps/contentstore/features/courses.py index dd7a1a7a28..2feafce361 100644 --- a/cms/djangoapps/contentstore/features/courses.py +++ b/cms/djangoapps/contentstore/features/courses.py @@ -45,7 +45,7 @@ def courseware_page_has_loaded_in_studio(step): @step('I see the course listed in My Courses$') def i_see_the_course_in_my_courses(step): course_css = 'span.class-name' - assert world.css_has_text(course_css, world.scenario_dict['COURSE_NAME']) + assert world.css_has_text(course_css, world.scenario_dict['COURSE'].display_name) @step('I am on the "([^"]*)" tab$') diff --git a/cms/djangoapps/contentstore/features/grading.py b/cms/djangoapps/contentstore/features/grading.py index e09954a556..fdd89564eb 100644 --- a/cms/djangoapps/contentstore/features/grading.py +++ b/cms/djangoapps/contentstore/features/grading.py @@ -64,7 +64,7 @@ def change_assignment_name(step, old_name, new_name): @step(u'I go back to the main course page') def main_course_page(step): - main_page_link_css = 'a[href="/%s/%s/course/%s"]' % (world.scenario_dict['COURSE_ORG'], world.scenario_dict['COURSE_NUM'], world.scenario_dict['COURSE_NAME'].replace(' ', '_'),) + main_page_link_css = 'a[href="/%s/%s/course/%s"]' % (world.scenario_dict['COURSE'].org, world.scenario_dict['COURSE'].number, world.scenario_dict['COURSE'].display_name.replace(' ', '_'),) world.css_click(main_page_link_css) diff --git a/common/djangoapps/terrain/course_helpers.py b/common/djangoapps/terrain/course_helpers.py index 78ba7d2cbc..3cb66e3bee 100644 --- a/common/djangoapps/terrain/course_helpers.py +++ b/common/djangoapps/terrain/course_helpers.py @@ -43,7 +43,7 @@ def log_in(username, password): """ # Authenticate the user - user = authenticate(username=username, password=password) + world.scenario_dict['USER'] = authenticate(username=username, password=password) assert(user is not None and user.is_active) # Send a fake HttpRequest to log the user in @@ -62,8 +62,6 @@ def log_in(username, password): cookie_dict = {settings.SESSION_COOKIE_NAME: request.session.session_key} world.browser.cookies.delete() world.browser.cookies.add(cookie_dict) - world.scenario_dict['username'] = username - world.scenario_dict['userpassword'] = password @world.absorb diff --git a/lms/djangoapps/courseware/features/common.py b/lms/djangoapps/courseware/features/common.py index 3837dbe13d..0aa079ebac 100644 --- a/lms/djangoapps/courseware/features/common.py +++ b/lms/djangoapps/courseware/features/common.py @@ -21,10 +21,6 @@ logger = getLogger(__name__) @step(u'The course "([^"]*)" exists$') def create_course(step, course): - world.scenario_dict['COURSE_NUM'] = course - world.scenario_dict['SECTION_NAME'] = 'Test Section' - world.scenario_dict['COURSE_NAME'] = 'Test Course' - world.scenario_dict['COURSE_ORG'] = 'edx' # First clear the modulestore so we don't try to recreate # the same course twice @@ -34,17 +30,17 @@ def create_course(step, course): # Create the course # We always use the same org and display name, # but vary the course identifier (e.g. 600x or 191x) - course = world.CourseFactory.create(org=world.scenario_dict['COURSE_ORG'], + world.scenario_dict['COURSE'] = world.CourseFactory.create(org='edx', number=course, - display_name=world.scenario_dict['COURSE_NAME']) + display_name='Test Course') # Add a section to the course to contain problems - section = world.ItemFactory.create(parent_location=course.location, - display_name=world.scenario_dict['SECTION_NAME']) + world.scenario_dict['SECTION'] = world.ItemFactory.create(parent_location=world.scenario_dict['COURSE'].location, + display_name='Test Section') - problem_section = world.ItemFactory.create(parent_location=section.location, + problem_section = world.ItemFactory.create(parent_location=world.scenario_dict['SECTION'].location, template='i4x://edx/templates/sequential/Empty', - display_name=world.scenario_dict['SECTION_NAME']) + display_name='Test Section') @step(u'I am registered for the course "([^"]*)"$') @@ -71,24 +67,24 @@ def add_tab_to_course(step, course, extra_tab_name): def course_id(course_num): - return "%s/%s/%s" % (world.scenario_dict['COURSE_ORG'], course_num, - world.scenario_dict['COURSE_NAME'].replace(" ", "_")) + return "%s/%s/%s" % (world.scenario_dict['COURSE'].org, course_num, + world.scenario_dict['COURSE'].display_name.replace(" ", "_")) def course_location(course_num): return Location(loc_or_tag="i4x", - org=world.scenario_dict['COURSE_ORG'], + org=world.scenario_dict['COURSE'].org, course=course_num, category='course', - name=world.scenario_dict['COURSE_NAME'].replace(" ", "_")) + name=world.scenario_dict['COURSE'].display_name.replace(" ", "_")) def section_location(course_num): return Location(loc_or_tag="i4x", - org=world.scenario_dict['COURSE_ORG'], + org=world.scenario_dict['COURSE'].org, course=course_num, category='sequential', - name=world.scenario_dict['SECTION_NAME'].replace(" ", "_")) + name=world.scenario_dict['SECTION'].display_name.replace(" ", "_")) def get_courses(): diff --git a/lms/djangoapps/courseware/features/navigation.py b/lms/djangoapps/courseware/features/navigation.py index b2258a8945..7c2474ae1a 100644 --- a/lms/djangoapps/courseware/features/navigation.py +++ b/lms/djangoapps/courseware/features/navigation.py @@ -16,11 +16,11 @@ TEST_SUBSECTION_NAME = 'Test Subsection' def view_course_multiple_sections(step): create_course() # Add a section to the course to contain problems - section1 = world.ItemFactory.create(parent_location=course_location(world.scenario_dict['COURSE_NUM']), + section1 = world.ItemFactory.create(parent_location=course_location(world.scenario_dict['COURSE'].number), display_name=section_name(1)) # Add a section to the course to contain problems - section2 = world.ItemFactory.create(parent_location=course_location(world.scenario_dict['COURSE_NUM']), + section2 = world.ItemFactory.create(parent_location=course_location(world.scenario_dict['COURSE'].number), display_name=section_name(2)) place1 = world.ItemFactory.create(parent_location=section1.location, @@ -42,7 +42,7 @@ def view_course_multiple_subsections(step): create_course() # Add a section to the course to contain problems - section1 = world.ItemFactory.create(parent_location=course_location(world.scenario_dict['COURSE_NUM']), + section1 = world.ItemFactory.create(parent_location=course_location(world.scenario_dict['COURSE'].number), display_name=section_name(1)) place1 = world.ItemFactory.create(parent_location=section1.location, @@ -62,7 +62,7 @@ def view_course_multiple_subsections(step): def view_course_multiple_sequences(step): create_course() # Add a section to the course to contain problems - section1 = world.ItemFactory.create(parent_location=course_location(world.scenario_dict['COURSE_NUM']), + section1 = world.ItemFactory.create(parent_location=course_location(world.scenario_dict['COURSE'].number), display_name=section_name(1)) place1 = world.ItemFactory.create(parent_location=section1.location, @@ -142,19 +142,14 @@ def subsection_name(section): def create_course(): world.clear_courses() - world.scenario_dict['COURSE_NAME'] = 'Test Course' - world.scenario_dict['COURSE_ORG'] = 'edx' - world.scenario_dict['COURSE_NUM'] = 'model_course' - world.CourseFactory.create(org=world.scenario_dict['COURSE_ORG'], - number=world.scenario_dict['COURSE_NUM'], - display_name=world.scenario_dict['COURSE_NAME']) + world.scenario_dict['COURSE'] = world.CourseFactory.create(org='edx', number='model_course', display_name='Test Course') def create_user_and_visit_course(): world.create_user('robot', 'test') u = User.objects.get(username='robot') - CourseEnrollment.objects.get_or_create(user=u, course_id=course_id(world.scenario_dict['COURSE_NUM'])) + CourseEnrollment.objects.get_or_create(user=u, course_id=course_id(world.scenario_dict['COURSE'].number)) world.log_in('robot', 'test') chapter_name = (TEST_SECTION_NAME + "1").replace(" ", "_") diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index c7f34ce683..bebbf655a2 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -17,14 +17,15 @@ def view_problem_with_attempts(step, problem_type, attempts): i_am_registered_for_the_course(step, 'model_course') # Ensure that the course has this problem type - add_problem_to_course(world.scenario_dict['COURSE_NUM'], problem_type, {'attempts': attempts}) + add_problem_to_course(world.scenario_dict['COURSE'].number, problem_type, {'attempts': attempts}) # Go to the one section in the factory-created course # which should be loaded with the correct problem chapter_name = world.scenario_dict['SECTION_NAME'].replace(" ", "_") section_name = chapter_name url = django_url('/courses/%s/%s/%s/courseware/%s/%s' % - (world.scenario_dict['COURSE_ORG'], world.scenario_dict['COURSE_NUM'], world.scenario_dict['COURSE_NAME'].replace(' ', '_'), chapter_name, section_name,)) + (world.scenario_dict['COURSE'].org, world.scenario_dict['COURSE'].number, world.scenario_dict['COURSE'].display_name.replace(' ', '_'), + chapter_name, section_name,)) world.browser.visit(url) @@ -40,8 +41,8 @@ def view_problem_with_show_answer(step, problem_type, answer): chapter_name = world.scenario_dict['SECTION_NAME'].replace(" ", "_") section_name = chapter_name url = django_url('/courses/%s/%s/%s/courseware/%s/%s' % - (world.scenario_dict['COURSE_ORG'], world.scenario_dict['COURSE_NUM'], world.scenario_dict['COURSE_NAME'].replace(' ', '_'), chapter_name, section_name,)) - + (world.scenario_dict['COURSE'].org, world.scenario_dict['COURSE'].number, world.scenario_dict['COURSE'].display_name.replace(' ', '_'), + chapter_name, section_name,)) world.browser.visit(url) @@ -57,8 +58,8 @@ def view_problem(step, problem_type): chapter_name = world.scenario_dict['SECTION_NAME'].replace(" ", "_") section_name = chapter_name url = django_url('/courses/%s/%s/%s/courseware/%s/%s' % - (world.scenario_dict['COURSE_ORG'], world.scenario_dict['COURSE_NUM'], world.scenario_dict['COURSE_NAME'].replace(' ', '_'), chapter_name, section_name,)) - + (world.scenario_dict['COURSE'].org, world.scenario_dict['COURSE'].number, world.scenario_dict['COURSE'].display_name.replace(' ', '_'), + chapter_name, section_name,)) world.browser.visit(url) diff --git a/lms/djangoapps/courseware/features/registration.py b/lms/djangoapps/courseware/features/registration.py index 25dc0bf1cd..b54416c661 100644 --- a/lms/djangoapps/courseware/features/registration.py +++ b/lms/djangoapps/courseware/features/registration.py @@ -7,8 +7,8 @@ from lettuce.django import django_url @step('I register for the course "([^"]*)"$') def i_register_for_the_course(_step, course): - cleaned_name = world.scenario_dict['COURSE_NAME'].replace(' ', '_') - url = django_url('courses/%s/%s/%s/about' % (world.scenario_dict['COURSE_ORG'], course, cleaned_name)) + cleaned_name = world.scenario_dict['COURSE'].display_name.replace(' ', '_') + url = django_url('courses/%s/%s/%s/about' % (world.scenario_dict['COURSE'].org, course, cleaned_name)) world.browser.visit(url) world.css_click('section.intro a.register') diff --git a/lms/djangoapps/courseware/features/video.py b/lms/djangoapps/courseware/features/video.py index 4ea70ef4de..1be43f9dcb 100644 --- a/lms/djangoapps/courseware/features/video.py +++ b/lms/djangoapps/courseware/features/video.py @@ -22,8 +22,8 @@ def view_video(_step): chapter_name = world.scenario_dict['SECTION_NAME'].replace(" ", "_") section_name = chapter_name url = django_url('/courses/%s/%s/%s/courseware/%s/%s' % - (world.scenario_dict['COURSE_ORG'], world.scenario_dict['COURSE_NUM'], world.scenario_dict['COURSE_NAME'].replace(' ', '_'), chapter_name, section_name,)) - + (world.scenario_dict['COURSE'].org, world.scenario_dict['COURSE'].number, world.scenario_dict['COURSE'].display_name.replace(' ', '_'), + chapter_name, section_name,)) world.browser.visit(url) @@ -37,8 +37,8 @@ def view_videoalpha(step): chapter_name = world.scenario_dict['SECTION_NAME'].replace(" ", "_") section_name = chapter_name url = django_url('/courses/%s/%s/%s/courseware/%s/%s' % - (world.scenario_dict['COURSE_ORG'], world.scenario_dict['COURSE_NUM'], world.scenario_dict['COURSE_NAME'].replace(' ', '_'), chapter_name, section_name,)) - + (world.scenario_dict['COURSE'].org, world.scenario_dict['COURSE'].number, world.scenario_dict['COURSE'].display_name.replace(' ', '_'), + chapter_name, section_name,)) world.browser.visit(url) From 484e217de36b8e857418c324a592fd4ec54db628 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 27 Jun 2013 17:02:48 -0400 Subject: [PATCH 740/995] Fixed some errors --- cms/djangoapps/contentstore/features/common.py | 2 +- common/djangoapps/terrain/course_helpers.py | 4 ++-- lms/djangoapps/courseware/features/problems.py | 6 +++--- lms/djangoapps/courseware/features/video.py | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index dd115f76ef..438059cba3 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -156,7 +156,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 - course = world.GroupFactory.create(name='instructor_MITx/{course_num}/{course_name}'.format(course_num=world.scenario_dict['COURSE'].number, course_name=world.scenario_dict['COURSE_NAME'].display_name.replace(" ", "_"))) + course = world.GroupFactory.create(name='instructor_MITx/{course_num}/{course_name}'.format(course_num=world.scenario_dict['COURSE'].number, course_name=world.scenario_dict['COURSE'].display_name.replace(" ", "_"))) if world.scenario_dict['USER']: user = world.scenario_dict['USER'] else: diff --git a/common/djangoapps/terrain/course_helpers.py b/common/djangoapps/terrain/course_helpers.py index 3cb66e3bee..27bf95099d 100644 --- a/common/djangoapps/terrain/course_helpers.py +++ b/common/djangoapps/terrain/course_helpers.py @@ -44,7 +44,7 @@ def log_in(username, password): # Authenticate the user world.scenario_dict['USER'] = authenticate(username=username, password=password) - assert(user is not None and user.is_active) + assert(world.scenario_dict['USER'] is not None and world.scenario_dict['USER'].is_active) # Send a fake HttpRequest to log the user in # We need to process the request using @@ -53,7 +53,7 @@ def log_in(username, password): request = HttpRequest() SessionMiddleware().process_request(request) AuthenticationMiddleware().process_request(request) - login(request, user) + login(request, world.scenario_dict['USER']) # Save the session request.session.save() diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index bebbf655a2..82bb4959a8 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -21,7 +21,7 @@ def view_problem_with_attempts(step, problem_type, attempts): # Go to the one section in the factory-created course # which should be loaded with the correct problem - chapter_name = world.scenario_dict['SECTION_NAME'].replace(" ", "_") + chapter_name = world.scenario_dict['SECTION'].display_name.replace(" ", "_") section_name = chapter_name url = django_url('/courses/%s/%s/%s/courseware/%s/%s' % (world.scenario_dict['COURSE'].org, world.scenario_dict['COURSE'].number, world.scenario_dict['COURSE'].display_name.replace(' ', '_'), @@ -38,7 +38,7 @@ def view_problem_with_show_answer(step, problem_type, answer): # Go to the one section in the factory-created course # which should be loaded with the correct problem - chapter_name = world.scenario_dict['SECTION_NAME'].replace(" ", "_") + chapter_name = world.scenario_dict['SECTION'].display_name.replace(" ", "_") section_name = chapter_name url = django_url('/courses/%s/%s/%s/courseware/%s/%s' % (world.scenario_dict['COURSE'].org, world.scenario_dict['COURSE'].number, world.scenario_dict['COURSE'].display_name.replace(' ', '_'), @@ -55,7 +55,7 @@ def view_problem(step, problem_type): # Go to the one section in the factory-created course # which should be loaded with the correct problem - chapter_name = world.scenario_dict['SECTION_NAME'].replace(" ", "_") + chapter_name = world.scenario_dict['SECTION'].display_name.replace(" ", "_") section_name = chapter_name url = django_url('/courses/%s/%s/%s/courseware/%s/%s' % (world.scenario_dict['COURSE'].org, world.scenario_dict['COURSE'].number, world.scenario_dict['COURSE'].display_name.replace(' ', '_'), diff --git a/lms/djangoapps/courseware/features/video.py b/lms/djangoapps/courseware/features/video.py index 1be43f9dcb..6b05af51b5 100644 --- a/lms/djangoapps/courseware/features/video.py +++ b/lms/djangoapps/courseware/features/video.py @@ -19,7 +19,7 @@ def view_video(_step): # Make sure we have a video add_video_to_course(coursenum) - chapter_name = world.scenario_dict['SECTION_NAME'].replace(" ", "_") + chapter_name = world.scenario_dict['SECTION'].display_name.replace(" ", "_") section_name = chapter_name url = django_url('/courses/%s/%s/%s/courseware/%s/%s' % (world.scenario_dict['COURSE'].org, world.scenario_dict['COURSE'].number, world.scenario_dict['COURSE'].display_name.replace(' ', '_'), @@ -34,7 +34,7 @@ def view_videoalpha(step): # Make sure we have a videoalpha add_videoalpha_to_course(coursenum) - chapter_name = world.scenario_dict['SECTION_NAME'].replace(" ", "_") + chapter_name = world.scenario_dict['SECTION'].display_name.replace(" ", "_") section_name = chapter_name url = django_url('/courses/%s/%s/%s/courseware/%s/%s' % (world.scenario_dict['COURSE'].org, world.scenario_dict['COURSE'].number, world.scenario_dict['COURSE'].display_name.replace(' ', '_'), From 940113efeeedf410c23481545c6c0fc1bb1be7ef Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Fri, 28 Jun 2013 08:57:25 -0400 Subject: [PATCH 741/995] Fixed long lines --- cms/djangoapps/contentstore/features/common.py | 5 +++-- cms/djangoapps/contentstore/features/grading.py | 4 +++- common/djangoapps/terrain/browser.py | 1 - 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index 438059cba3..cb24af47e0 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -156,8 +156,9 @@ 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 - course = world.GroupFactory.create(name='instructor_MITx/{course_num}/{course_name}'.format(course_num=world.scenario_dict['COURSE'].number, course_name=world.scenario_dict['COURSE'].display_name.replace(" ", "_"))) - if world.scenario_dict['USER']: + course = world.GroupFactory.create(name='instructor_MITx/{}/{}'.format(world.scenario_dict['COURSE'].number, + world.scenario_dict['COURSE'].display_name.replace(" ", "_"))) + if world.scenario_dict.get('USER') is None: user = world.scenario_dict['USER'] else: user = get_user_by_email('robot+studio@edx.org') diff --git a/cms/djangoapps/contentstore/features/grading.py b/cms/djangoapps/contentstore/features/grading.py index fdd89564eb..0b60510bf5 100644 --- a/cms/djangoapps/contentstore/features/grading.py +++ b/cms/djangoapps/contentstore/features/grading.py @@ -64,7 +64,9 @@ def change_assignment_name(step, old_name, new_name): @step(u'I go back to the main course page') def main_course_page(step): - main_page_link_css = 'a[href="/%s/%s/course/%s"]' % (world.scenario_dict['COURSE'].org, world.scenario_dict['COURSE'].number, world.scenario_dict['COURSE'].display_name.replace(' ', '_'),) + main_page_link_css = 'a[href="/%s/%s/course/%s"]' % (world.scenario_dict['COURSE'].org, + world.scenario_dict['COURSE'].number, + world.scenario_dict['COURSE'].display_name.replace(' ', '_'),) world.css_click(main_page_link_css) diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index c338d403d6..ec361a9aff 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -79,7 +79,6 @@ def initial_setup(server): # Set the browser size to 1280x1024 world.browser.driver.set_window_size(1280, 1024) - #world.absorb({}, 'scenario_dict') @before.each_scenario From e62cb45da4242578f4369afdf40b4f47c1474336 Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 8 Jul 2013 15:50:22 +0000 Subject: [PATCH 742/995] tracking in idashboard should only log json-serializable objects --- lms/djangoapps/instructor/views.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 9f9b7a2399..b046bdb762 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -374,7 +374,7 @@ def instructor_dashboard(request, course_id): msg += message if student is not None: progress_url = reverse('student_progress', kwargs={'course_id': course_id, 'student_id': student.id}) - track.views.server_track(request, "get-student-progress-page", {"student": student, "instructor": request.user, "course": course_id}, page="idashboard") + track.views.server_track(request, "get-student-progress-page", {"student": str(student), "instructor": str(request.user), "course": course_id}, page="idashboard") msg += " Progress page for username: {1} with email address: {2}.".format(progress_url, student.username, student.email) #---------------------------------------- @@ -472,7 +472,7 @@ def instructor_dashboard(request, course_id): msg += 'Added {0} to instructor group = {1}'.format(user, group.name) log.debug('staffgrp={0}'.format(group.name)) user.groups.add(group) - track.views.server_track(request, "add-instructor", {"instructor": user}, page="idashboard") + track.views.server_track(request, "add-instructor", {"instructor": str(user)}, page="idashboard") elif action == 'Remove course staff': uname = request.POST['staffuser'] @@ -491,7 +491,7 @@ def instructor_dashboard(request, course_id): msg += 'Removed {0} from instructor group = {1}'.format(user, group.name) log.debug('instructorgrp={0}'.format(group.name)) user.groups.remove(group) - track.views.server_track(request, "remove-instructor", {"instructor": user}, page="idashboard") + track.views.server_track(request, "remove-instructor", {"instructor": str(user)}, page="idashboard") #---------------------------------------- # DataDump @@ -658,7 +658,7 @@ def instructor_dashboard(request, course_id): problem = request.POST['Problem'] nmsg, plots = psychoanalyze.generate_plots_for_problem(problem) msg += nmsg - track.views.server_track(request, "psychometrics-histogram-generation", {"problem": problem}, page="idashboard") + track.views.server_track(request, "psychometrics-histogram-generation", {"problem": str(problem)}, page="idashboard") if idash_mode == 'Psychometrics': problems = psychoanalyze.problems_with_psychometric_data(course_id) @@ -911,7 +911,7 @@ def _add_or_remove_user_group(request, username_or_email, group, group_title, ev else: user.groups.remove(group) event = "add" if do_add else "remove" - track.views.server_track(request, "add-or-remove-user-group", {"event_name": event_name, "user": user, "event": event}, page="idashboard") + track.views.server_track(request, "add-or-remove-user-group", {"event_name": event_name, "user": str(user), "event": event}, page="idashboard") return msg From a3e4f21a9c891735765f96e61fd302e8bb774c17 Mon Sep 17 00:00:00 2001 From: ichuang Date: Tue, 9 Jul 2013 14:24:23 -0400 Subject: [PATCH 743/995] use unicode instead of str for casting track.views args in idashboard --- lms/djangoapps/instructor/views.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index b046bdb762..d133bdaf38 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -374,7 +374,7 @@ def instructor_dashboard(request, course_id): msg += message if student is not None: progress_url = reverse('student_progress', kwargs={'course_id': course_id, 'student_id': student.id}) - track.views.server_track(request, "get-student-progress-page", {"student": str(student), "instructor": str(request.user), "course": course_id}, page="idashboard") + track.views.server_track(request, "get-student-progress-page", {"student": unicode(student), "instructor": unicode(request.user), "course": course_id}, page="idashboard") msg += " Progress page for username: {1} with email address: {2}.".format(progress_url, student.username, student.email) #---------------------------------------- @@ -472,7 +472,7 @@ def instructor_dashboard(request, course_id): msg += 'Added {0} to instructor group = {1}'.format(user, group.name) log.debug('staffgrp={0}'.format(group.name)) user.groups.add(group) - track.views.server_track(request, "add-instructor", {"instructor": str(user)}, page="idashboard") + track.views.server_track(request, "add-instructor", {"instructor": unicode(user)}, page="idashboard") elif action == 'Remove course staff': uname = request.POST['staffuser'] @@ -491,7 +491,7 @@ def instructor_dashboard(request, course_id): msg += 'Removed {0} from instructor group = {1}'.format(user, group.name) log.debug('instructorgrp={0}'.format(group.name)) user.groups.remove(group) - track.views.server_track(request, "remove-instructor", {"instructor": str(user)}, page="idashboard") + track.views.server_track(request, "remove-instructor", {"instructor": unicode(user)}, page="idashboard") #---------------------------------------- # DataDump @@ -658,7 +658,7 @@ def instructor_dashboard(request, course_id): problem = request.POST['Problem'] nmsg, plots = psychoanalyze.generate_plots_for_problem(problem) msg += nmsg - track.views.server_track(request, "psychometrics-histogram-generation", {"problem": str(problem)}, page="idashboard") + track.views.server_track(request, "psychometrics-histogram-generation", {"problem": unicode(problem)}, page="idashboard") if idash_mode == 'Psychometrics': problems = psychoanalyze.problems_with_psychometric_data(course_id) @@ -911,7 +911,7 @@ def _add_or_remove_user_group(request, username_or_email, group, group_title, ev else: user.groups.remove(group) event = "add" if do_add else "remove" - track.views.server_track(request, "add-or-remove-user-group", {"event_name": event_name, "user": str(user), "event": event}, page="idashboard") + track.views.server_track(request, "add-or-remove-user-group", {"event_name": event_name, "user": unicode(user), "event": event}, page="idashboard") return msg From 50bea28d750a8d5bfbe9ff31b5bdef18c2957bdd Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Fri, 12 Jul 2013 11:13:59 -0400 Subject: [PATCH 744/995] Add a 'string literal' serialization method and use it in the lambda function mapping table when the value to xml serialize is of basestring type. Also a couple of drive-by pep8 fixes. --- common/lib/xmodule/xmodule/xml_module.py | 54 ++++++++++++++---------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/common/lib/xmodule/xmodule/xml_module.py b/common/lib/xmodule/xmodule/xml_module.py index c1340a9fc0..edd10288de 100644 --- a/common/lib/xmodule/xmodule/xml_module.py +++ b/common/lib/xmodule/xmodule/xml_module.py @@ -81,13 +81,23 @@ class AttrMap(_AttrMapBase): def serialize_field(value): """ - Return a string version of the value (where value is the JSON-formatted, internally stored value). + Return a string version of the value (where value is the JSON-formatted, internally stored value). - By default, this is the result of calling json.dumps on the input value. - """ + By default, this is the result of calling json.dumps on the input value. + """ return json.dumps(value, cls=EdxJSONEncoder) +def serialize_string_literal(value): + """ + Assert that the value is a base string and - if it is - simply return it + """ + if not isinstance(value, basestring): + raise Exception('Value {0} is not of type basestring!'.format(value)) + + return value + + def deserialize_field(field, value): """ Deserialize the string version to the value stored internally. @@ -126,7 +136,7 @@ class XmlDescriptor(XModuleDescriptor): """ xml_attributes = Dict(help="Map of unhandled xml attributes, used only for storage between import and export", - default={}, scope=Scope.settings) + default={}, scope=Scope.settings) # Extension to append to filename paths filename_extension = 'xml' @@ -141,23 +151,23 @@ class XmlDescriptor(XModuleDescriptor): # understand? And if we do, is this the place? # Related: What's the right behavior for clean_metadata? metadata_attributes = ('format', 'graceperiod', 'showanswer', 'rerandomize', - 'start', 'due', 'graded', 'display_name', 'url_name', 'hide_from_toc', - 'ispublic', # if True, then course is listed for all users; see - 'xqa_key', # for xqaa server access - 'giturl', # url of git server for origin of file - # information about testcenter exams is a dict (of dicts), not a string, - # so it cannot be easily exportable as a course element's attribute. - 'testcenter_info', - # VS[compat] Remove once unused. - 'name', 'slug') + 'start', 'due', 'graded', 'display_name', 'url_name', 'hide_from_toc', + 'ispublic', # if True, then course is listed for all users; see + 'xqa_key', # for xqaa server access + 'giturl', # url of git server for origin of file + # information about testcenter exams is a dict (of dicts), not a string, + # so it cannot be easily exportable as a course element's attribute. + 'testcenter_info', + # VS[compat] Remove once unused. + 'name', 'slug') metadata_to_strip = ('data_dir', - 'tabs', 'grading_policy', 'published_by', 'published_date', - 'discussion_blackouts', 'testcenter_info', - # VS[compat] -- remove the below attrs once everything is in the CMS - 'course', 'org', 'url_name', 'filename', - # Used for storing xml attributes between import and export, for roundtrips - 'xml_attributes') + 'tabs', 'grading_policy', 'published_by', 'published_date', + 'discussion_blackouts', 'testcenter_info', + # VS[compat] -- remove the below attrs once everything is in the CMS + 'course', 'org', 'url_name', 'filename', + # Used for storing xml attributes between import and export, for roundtrips + 'xml_attributes') metadata_to_export_to_policy = ('discussion_topics') @@ -166,7 +176,7 @@ class XmlDescriptor(XModuleDescriptor): for field in set(cls.fields + cls.lms.fields): if field.name == attr: from_xml = lambda val: deserialize_field(field, val) - to_xml = lambda val : serialize_field(val) + to_xml = lambda val: serialize_string_literal(val) if isinstance(val, basestring) else serialize_field(val) return AttrMap(from_xml, to_xml) return AttrMap() @@ -254,7 +264,7 @@ class XmlDescriptor(XModuleDescriptor): definition, children = cls.definition_from_xml(definition_xml, system) if definition_metadata: definition['definition_metadata'] = definition_metadata - definition['filename'] = [ filepath, filename ] + definition['filename'] = [filepath, filename] return definition, children @@ -280,7 +290,6 @@ class XmlDescriptor(XModuleDescriptor): metadata[attr] = attr_map.from_xml(val) return metadata - @classmethod def apply_policy(cls, metadata, policy): """ @@ -374,7 +383,6 @@ class XmlDescriptor(XModuleDescriptor): """ return True - def export_to_xml(self, resource_fs): """ Returns an xml string representing this module, and all modules From 5aae3c69715bb84294354299f26fbcf07388c0d6 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Fri, 12 Jul 2013 11:14:35 -0400 Subject: [PATCH 745/995] add new tests for the string literal serialization --- .../xmodule/xmodule/tests/test_xml_module.py | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/tests/test_xml_module.py b/common/lib/xmodule/xmodule/tests/test_xml_module.py index 6581ce58f6..a853e6ab3f 100644 --- a/common/lib/xmodule/xmodule/tests/test_xml_module.py +++ b/common/lib/xmodule/xmodule/tests/test_xml_module.py @@ -4,7 +4,7 @@ from xmodule.x_module import XModuleFields from xblock.core import Scope, String, Dict, Boolean, Integer, Float, Any, List from xmodule.fields import Date, Timedelta -from xmodule.xml_module import XmlDescriptor, serialize_field, deserialize_field +from xmodule.xml_module import XmlDescriptor, serialize_field, deserialize_field, serialize_string_literal import unittest from .import get_test_system from nose.tools import assert_equals @@ -186,12 +186,27 @@ class TestSerialize(unittest.TestCase): assert_equals('"false"', serialize_field('false')) assert_equals('"fAlse"', serialize_field('fAlse')) assert_equals('"hat box"', serialize_field('hat box')) - assert_equals('{"bar": "hat", "frog": "green"}', serialize_field({'bar': 'hat', 'frog' : 'green'})) + assert_equals('{"bar": "hat", "frog": "green"}', serialize_field({'bar': 'hat', 'frog': 'green'})) assert_equals('[3.5, 5.6]', serialize_field([3.5, 5.6])) assert_equals('["foo", "bar"]', serialize_field(['foo', 'bar'])) assert_equals('"2012-12-31T23:59:59Z"', serialize_field("2012-12-31T23:59:59Z")) assert_equals('"1 day 12 hours 59 minutes 59 seconds"', - serialize_field("1 day 12 hours 59 minutes 59 seconds")) + serialize_field("1 day 12 hours 59 minutes 59 seconds")) + + def test_serialize_string_literal(self): + assert_equals('2', serialize_string_literal('2')) + assert_equals('2.589', serialize_string_literal('2.589')) + assert_equals('false', serialize_string_literal('false')) + assert_equals('fAlse', serialize_string_literal('fAlse')) + assert_equals('hat box', serialize_string_literal('hat box')) + assert_equals('2012-12-31T23:59:59Z', serialize_string_literal("2012-12-31T23:59:59Z")) + assert_equals('1 day 12 hours 59 minutes 59 seconds', + serialize_string_literal("1 day 12 hours 59 minutes 59 seconds")) + + try: + self.assertRaises(serialize_string_literal(2.31)) + except Exception: + pass class TestDeserialize(unittest.TestCase): From d417a78c7493630a2f8ed1d701dd7d78ae266689 Mon Sep 17 00:00:00 2001 From: cahrens Date: Fri, 12 Jul 2013 11:25:12 -0400 Subject: [PATCH 746/995] Tests for course creator status as returned in index page. --- .../contentstore/tests/test_users.py | 186 ++++++++++++++++++ cms/djangoapps/contentstore/views/user.py | 50 +++-- 2 files changed, 217 insertions(+), 19 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_users.py b/cms/djangoapps/contentstore/tests/test_users.py index f945ef50fc..bba4d42f4d 100644 --- a/cms/djangoapps/contentstore/tests/test_users.py +++ b/cms/djangoapps/contentstore/tests/test_users.py @@ -1,6 +1,18 @@ +""" +Tests for user.py. +""" import json +import mock from .utils import CourseTestCase from django.core.urlresolvers import reverse +from contentstore.views.user import _get_course_creator_status +from course_creators.views import add_user_with_status_granted +from course_creators.admin import CourseCreatorAdmin +from course_creators.models import CourseCreator + +from django.http import HttpRequest +from django.contrib.auth.models import User +from django.contrib.admin.sites import AdminSite class UsersTestCase(CourseTestCase): @@ -13,3 +25,177 @@ class UsersTestCase(CourseTestCase): self.assertEqual(resp.status_code, 400) content = json.loads(resp.content) self.assertEqual(content["Status"], "Failed") + + +class IndexCourseCreatorTests(CourseTestCase): + """ + Tests the various permutations of course creator status. + """ + def setUp(self): + super(IndexCourseCreatorTests, self).setUp() + + self.index_url = reverse("index") + self.request_access_url = reverse("request_course_creator") + + # Disable course creation takes precedence over enable creator group. I have enabled the + # latter to make this clear. + self.disable_course_creation = { + "DISABLE_COURSE_CREATION": True, + "ENABLE_CREATOR_GROUP": True, + 'STAFF_EMAIL': 'mark@marky.mark', + } + + self.enable_creator_group = {"ENABLE_CREATOR_GROUP": True} + + def test_get_course_creator_status_disable_creation(self): + # DISABLE_COURSE_CREATION is True (this is the case on edx, where we have a marketing site). + # Only edx staff can create courses. + with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.disable_course_creation): + self.assertTrue(self.user.is_staff) + self.assertEquals('granted', _get_course_creator_status(self.user)) + self._set_user_non_staff() + self.assertFalse(self.user.is_staff) + self.assertEquals('disallowed_for_this_site', _get_course_creator_status(self.user)) + + def test_get_course_creator_status_default_cause(self): + # Neither ENABLE_CREATOR_GROUP nor DISABLE_COURSE_CREATION are enabled. Anyone can create a course. + self.assertEquals('granted', _get_course_creator_status(self.user)) + self._set_user_non_staff() + self.assertEquals('granted', _get_course_creator_status(self.user)) + + def test_get_course_creator_status_creator_group(self): + # ENABLE_CREATOR_GROUP is True. This is the case on edge. + # Only staff members and users who have been granted access can create courses. + with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.enable_creator_group): + # Staff members can always create courses. + self.assertEquals('granted', _get_course_creator_status(self.user)) + # Non-staff must request access. + self._set_user_non_staff() + self.assertEquals('unrequested', _get_course_creator_status(self.user)) + # Staff user requests access. + self.client.post(self.request_access_url) + self.assertEquals('pending', _get_course_creator_status(self.user)) + + def test_get_course_creator_status_creator_group_granted(self): + # ENABLE_CREATOR_GROUP is True. This is the case on edge. + # Check return value for a non-staff user who has been granted access. + with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.enable_creator_group): + # self.user has staff permissions, can call this method. + add_user_with_status_granted(self.user, self.user) + # now make self.user non-staff + self._set_user_non_staff() + self.assertEquals('granted', _get_course_creator_status(self.user)) + + def test_get_course_creator_status_creator_group_denied(self): + # ENABLE_CREATOR_GROUP is True. This is the case on edge. + # Check return value for a non-staff user who has been denied access. + with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.enable_creator_group): + # make self.user non-staff + self._set_user_non_staff() + self._set_user_denied() + self.assertEquals('denied', _get_course_creator_status(self.user)) + + def test_disable_course_creation_enabled_non_staff(self): + # Test index page content when DISABLE_COURSE_CREATION is True, non-staff member. + with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.disable_course_creation): + self._set_user_non_staff() + self._assert_cannot_create() + + def test_disable_course_creation_enabled_staff(self): + # Test index page content when DISABLE_COURSE_CREATION is True, staff member. + with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.disable_course_creation): + resp = self._assert_can_create() + self.assertFalse('Email staff to create course' in resp.content) + + def test_can_create_by_default(self): + # Test index page content with neither ENABLE_CREATOR_GROUP nor DISABLE_COURSE_CREATION enabled. + # Anyone can create a course. + self._assert_can_create() + self._set_user_non_staff() + self._assert_can_create() + + def test_course_creator_group_enabled(self): + # Test index page content with ENABLE_CREATOR_GROUP True. + # Staff can always create a course, others must request access. + with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.enable_creator_group): + # Staff members can always create courses. + self._assert_can_create() + + # Non-staff case. + self._set_user_non_staff() + resp = self._assert_cannot_create() + self.assertTrue(self.request_access_url in resp.content) + + # Now request access. + self.client.post(self.request_access_url) + + # Still cannot create a course, but the "request access button" is no longer there. + resp = self._assert_cannot_create() + self.assertFalse(self.request_access_url in resp.content) + self.assertTrue('has-status is-pending' in resp.content) + + def test_course_creator_group_granted(self): + # Test index page content with ENABLE_CREATOR_GROUP True, non-staff member with access granted. + with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.enable_creator_group): + # self.user has staff permissions, can call this method. + add_user_with_status_granted(self.user, self.user) + # now make self.user non-staff + self._set_user_non_staff() + self._assert_can_create() + + def test_course_creator_group_denied(self): + # Test index page content with ENABLE_CREATOR_GROUP True, non-staff member with access denied. + with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.enable_creator_group): + self._set_user_non_staff() + self._set_user_denied() + resp = self._assert_cannot_create() + self.assertFalse(self.request_access_url in resp.content) + self.assertTrue('has-status is-denied' in resp.content) + + def _assert_can_create(self): + """ + Helper method that posts to the index page and checks that the user can create a course. + + Returns the response from the post. + """ + resp = self.client.post(self.index_url) + self.assertTrue('new-course-button' in resp.content) + self.assertFalse(self.request_access_url in resp.content) + self.assertFalse('Email staff to create course' in resp.content) + return resp + + def _assert_cannot_create(self): + """ + Helper method that posts to the index page and checks that the user cannot create a course. + + Returns the response from the post. + """ + resp = self.client.post(self.index_url) + self.assertFalse('new-course-button' in resp.content) + return resp + + def _set_user_non_staff(self): + """ + Sets user as non-staff. + """ + self.user.is_staff = False + self.user.save() + + def _set_user_denied(self): + """ + Sets course creator status to denied in admin table. + """ + self.table_entry = CourseCreator(user=self.user) + self.table_entry.save() + + self.admin = User.objects.create_user('Mark', 'admin+courses@edx.org', 'foo') + self.admin.is_staff = True + + self.deny_request = HttpRequest() + self.deny_request.user = self.admin + + self.creator_admin = CourseCreatorAdmin(self.table_entry, AdminSite()) + + self.table_entry.state = CourseCreator.DENIED + self.creator_admin.save_model(self.deny_request, self.table_entry, None, True) + diff --git a/cms/djangoapps/contentstore/views/user.py b/cms/djangoapps/contentstore/views/user.py index b2a97d4f01..f8e341f2cd 100644 --- a/cms/djangoapps/contentstore/views/user.py +++ b/cms/djangoapps/contentstore/views/user.py @@ -35,21 +35,6 @@ def index(request): and course.location.name != '') courses = filter(course_filter, courses) - if settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False): - course_creator_status = 'granted' if request.user.is_staff else 'disallowed_for_this_site' - elif settings.MITX_FEATURES.get('ENABLE_CREATOR_GROUP', False): - course_creator_status = get_course_creator_status(request.user) - if course_creator_status is None: - # User not grandfathered in as an existing user, has not previously visited the dashboard page. - # Add the user to the course creator admin table with status 'unrequested'. - add_user_with_status_unrequested(request.user) - course_creator_status = get_course_creator_status(request.user) - else: - course_creator_status = 'granted' - - request_course_creator_url = reverse('request_course_creator') - csrf_token = csrf(request)['csrf_token'] - return render_to_response('index.html', { 'new_course_template': Location('i4x', 'edx', 'templates', 'course', 'Empty'), 'courses': [(course.display_name, @@ -57,9 +42,9 @@ def index(request): get_lms_link_for_item(course.location, course_id=course.location.course_id)) for course in courses], 'user': request.user, - 'request_course_creator_url': request_course_creator_url, - 'course_creator_status': course_creator_status, - 'csrf': csrf_token + 'request_course_creator_url': reverse('request_course_creator'), + 'course_creator_status': _get_course_creator_status(request.user), + 'csrf': csrf(request)['csrf_token'] }) @@ -67,11 +52,13 @@ def index(request): @ensure_csrf_cookie @login_required def request_course_creator(request): + """ + User has requested course creation access. + """ user_requested_access(request.user) return JsonResponse({"Status": "OK"}) - @login_required @ensure_csrf_cookie def manage_users(request, location): @@ -169,3 +156,28 @@ def remove_user(request, location): remove_user_from_course_group(request.user, user, location, STAFF_ROLE_NAME) return JsonResponse({"Status": "OK"}) + + +def _get_course_creator_status(user): + """ + Helper method for returning the course creator status for a particular user, + taking into account the values of DISABLE_COURSE_CREATION and ENABLE_CREATOR_GROUP. + + If the user passed in has not previously visited the index page, it will be + added with status 'unrequested' if the course creator group is in use. + """ + if user.is_staff: + course_creator_status = 'granted' + elif settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False): + course_creator_status = 'disallowed_for_this_site' + elif settings.MITX_FEATURES.get('ENABLE_CREATOR_GROUP', False): + course_creator_status = get_course_creator_status(user) + if course_creator_status is None: + # User not grandfathered in as an existing user, has not previously visited the dashboard page. + # Add the user to the course creator admin table with status 'unrequested'. + add_user_with_status_unrequested(user) + course_creator_status = get_course_creator_status(user) + else: + course_creator_status = 'granted' + + return course_creator_status From ab4012cc594341ac78663bd89f90621cb8c02bd3 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Fri, 12 Jul 2013 11:33:46 -0400 Subject: [PATCH 747/995] fix up some of the test courses which had hardcoded that quot; escaping --- common/test/data/simple/course.xml | 8 ++++---- .../test/data/test_exam_registration/course/2012_Fall.xml | 4 ++-- common/test/data/test_start_date/course/2012_Fall.xml | 4 ++-- common/test/data/toy/chapter/secret/magic.xml | 2 +- common/test/data/toy/course/2012_Fall.xml | 8 ++++---- common/test/data/two_toys/video/Video_Resources.xml | 2 +- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/common/test/data/simple/course.xml b/common/test/data/simple/course.xml index c9bb5ec8b2..529528ca0a 100644 --- a/common/test/data/simple/course.xml +++ b/common/test/data/simple/course.xml @@ -1,13 +1,13 @@ -
      -
      From c75041e168f3db047f0ee32e2245e31196c0b28e Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Fri, 12 Jul 2013 14:24:22 -0400 Subject: [PATCH 753/995] Studio: adds in disabled/submitting state and logic for course creation button --- cms/static/sass/views/_dashboard.scss | 29 ++++++++++++++++- cms/templates/index.html | 46 ++++++++++++++++----------- common/static/sass/_mixins.scss | 2 +- 3 files changed, 57 insertions(+), 20 deletions(-) diff --git a/cms/static/sass/views/_dashboard.scss b/cms/static/sass/views/_dashboard.scss index f64efb184d..a968a2c368 100644 --- a/cms/static/sass/views/_dashboard.scss +++ b/cms/static/sass/views/_dashboard.scss @@ -95,6 +95,7 @@ body.dashboard { @extend .t-action1; @include transform(rotate(45deg)); @include transform-origin(center center); + @include transition(all $tmg-f1 linear 0s); float: right; text-align: right; } @@ -102,7 +103,7 @@ body.dashboard { .ui-toggle-target { @extend .depth1; - @include transition(opacity 0.50s ease-in-out 0s); + @include transition(opacity $tmg-f1 ease-in-out 0s); position: relative; top: -2px; display: none; @@ -156,6 +157,32 @@ body.dashboard { @extend .btn-primary-blue; @extend .t-action3; } + + // specific - request button + .action-request { + position: relative; + overflow: hidden; + + .icon-cog { + @include transition(all $tmg-f1 ease-in-out $tmg-f1); + @include font-size(20); + position: absolute; + top: ($baseline/2); + left: -($baseline); + visibility: hidden; + opacity: 0.0; + } + + &.is-submitting { + padding-left: ($baseline*2); + + .icon-cog { + left: ($baseline*0.75); + visibility: visible; + opacity: 1.0; + } + } + } } .status-update { diff --git a/cms/templates/index.html b/cms/templates/index.html index 7cd660c088..8ef701e3bd 100644 --- a/cms/templates/index.html +++ b/cms/templates/index.html @@ -8,6 +8,28 @@ +<%block name="jsextra"> + + + <%block name="header_extras"> -<%block name="jsextra"> - - - <%block name="content">
      @@ -162,9 +167,14 @@

      ${_('Your Course Creator Request Status:')}

      + +
      +

      There was a problem submitting your request

      +
      +
      - +
      diff --git a/common/static/sass/_mixins.scss b/common/static/sass/_mixins.scss index 64248734c3..f349ccb963 100644 --- a/common/static/sass/_mixins.scss +++ b/common/static/sass/_mixins.scss @@ -125,7 +125,7 @@ } - &.disabled, &[disabled] { + &.disabled, &[disabled], &.is-disabled { cursor: default; pointer-events: none; opacity: 0.5; From b28b4bfb754f2d4569613edf188a2259a83fdeca Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Fri, 12 Jul 2013 14:25:23 -0400 Subject: [PATCH 754/995] Studio: abstracts and revises form/notice UI --- .../sass/elements/_system-feedback.scss | 42 +++++++++++++++++++ cms/static/sass/elements/_system-help.scss | 2 +- cms/static/sass/views/_account.scss | 41 ------------------ 3 files changed, 43 insertions(+), 42 deletions(-) diff --git a/cms/static/sass/elements/_system-feedback.scss b/cms/static/sass/elements/_system-feedback.scss index 5022a9f677..90de604aa8 100644 --- a/cms/static/sass/elements/_system-feedback.scss +++ b/cms/static/sass/elements/_system-feedback.scss @@ -1,4 +1,46 @@ // studio - elements - system feedback +// ==================== + +// messages +.message { + @extend .t-copy-sub1; + display: block; +} + +.message-status { + display: none; + @include border-top-radius(2px); + @include box-sizing(border-box); + border-bottom: 2px solid $yellow-d2; + margin: 0 0 $baseline 0; + padding: ($baseline/2) $baseline; + font-weight: 500; + background: $yellow-d1; + color: $white; + + [class^="icon-"] { + position: relative; + top: 1px; + @include font-size(16); + display: inline-block; + margin-right: ($baseline/2); + } + + .text { + display: inline-block; + } + + &.error { + border-color: shade($red, 50%); + background: tint($red, 20%); + } + + &.is-shown { + display: block; + } +} + + // alerts, notifications, prompts, and status communication // ==================== diff --git a/cms/static/sass/elements/_system-help.scss b/cms/static/sass/elements/_system-help.scss index 0f90d9db5c..3b33946e19 100644 --- a/cms/static/sass/elements/_system-help.scss +++ b/cms/static/sass/elements/_system-help.scss @@ -144,7 +144,7 @@ background-color: $gray-l4; .title { - color: $gray-d3; + color: $gray-d2; } .copy { diff --git a/cms/static/sass/views/_account.scss b/cms/static/sass/views/_account.scss index 53f01eee6d..c2cf139400 100644 --- a/cms/static/sass/views/_account.scss +++ b/cms/static/sass/views/_account.scss @@ -252,44 +252,3 @@ body.signup, body.signin { } } } - -// ==================== - -// messages -.message { - @extend .t-copy-sub1; - display: block; -} - -.message-status { - display: none; - @include border-top-radius(2px); - @include box-sizing(border-box); - border-bottom: 2px solid $yellow-d2; - margin: 0 0 $baseline 0; - padding: ($baseline/2) $baseline; - font-weight: 500; - background: $yellow-d1; - color: $white; - - [class^="icon-"] { - position: relative; - top: 1px; - @include font-size(16); - display: inline-block; - margin-right: ($baseline/2); - } - - .text { - display: inline-block; - } - - &.error { - border-color: shade($red, 50%); - background: tint($red, 20%); - } - - &.is-shown { - display: block; - } -} From ac4e20b4f387a5f86ac903b0041691dd4cec22f1 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Fri, 12 Jul 2013 15:27:36 -0400 Subject: [PATCH 755/995] change serialize_field to check for type. If value is a string, simply return that, otherwise do a json.dumps --- .../xmodule/xmodule/tests/test_xml_module.py | 31 +++++-------------- common/lib/xmodule/xmodule/xml_module.py | 18 ++++------- 2 files changed, 14 insertions(+), 35 deletions(-) diff --git a/common/lib/xmodule/xmodule/tests/test_xml_module.py b/common/lib/xmodule/xmodule/tests/test_xml_module.py index 2010ad2b7e..7ccc71dd96 100644 --- a/common/lib/xmodule/xmodule/tests/test_xml_module.py +++ b/common/lib/xmodule/xmodule/tests/test_xml_module.py @@ -4,7 +4,7 @@ from xmodule.x_module import XModuleFields from xblock.core import Scope, String, Dict, Boolean, Integer, Float, Any, List from xmodule.fields import Date, Timedelta -from xmodule.xml_module import XmlDescriptor, serialize_field, deserialize_field, serialize_string_literal +from xmodule.xml_module import XmlDescriptor, serialize_field, deserialize_field import unittest from .import get_test_system from nose.tools import assert_equals @@ -137,7 +137,6 @@ class EditableMetadataFieldsTest(unittest.TestCase): type='Float', options={'min': 0, 'step': .3} ) - # Start of helper methods def get_xml_editable_fields(self, model_data): system = get_test_system() @@ -179,33 +178,20 @@ class TestSerialize(unittest.TestCase): def test_serialize(self): assert_equals('null', serialize_field(None)) assert_equals('-2', serialize_field(-2)) - assert_equals('"2"', serialize_field('2')) + assert_equals('2', serialize_field('2')) assert_equals('-3.41', serialize_field(-3.41)) - assert_equals('"2.589"', serialize_field('2.589')) + assert_equals('2.589', serialize_field('2.589')) assert_equals('false', serialize_field(False)) - assert_equals('"false"', serialize_field('false')) - assert_equals('"fAlse"', serialize_field('fAlse')) - assert_equals('"hat box"', serialize_field('hat box')) + assert_equals('false', serialize_field('false')) + assert_equals('fAlse', serialize_field('fAlse')) + assert_equals('hat box', serialize_field('hat box')) assert_equals('{"bar": "hat", "frog": "green"}', serialize_field({'bar': 'hat', 'frog': 'green'})) assert_equals('[3.5, 5.6]', serialize_field([3.5, 5.6])) assert_equals('["foo", "bar"]', serialize_field(['foo', 'bar'])) - assert_equals('"2012-12-31T23:59:59Z"', serialize_field("2012-12-31T23:59:59Z")) - assert_equals('"1 day 12 hours 59 minutes 59 seconds"', + assert_equals('2012-12-31T23:59:59Z', serialize_field("2012-12-31T23:59:59Z")) + assert_equals('1 day 12 hours 59 minutes 59 seconds', serialize_field("1 day 12 hours 59 minutes 59 seconds")) - def test_serialize_string_literal(self): - assert_equals('2', serialize_string_literal('2')) - assert_equals('2.589', serialize_string_literal('2.589')) - assert_equals('false', serialize_string_literal('false')) - assert_equals('fAlse', serialize_string_literal('fAlse')) - assert_equals('hat box', serialize_string_literal('hat box')) - assert_equals('2012-12-31T23:59:59Z', serialize_string_literal("2012-12-31T23:59:59Z")) - assert_equals('1 day 12 hours 59 minutes 59 seconds', - serialize_string_literal("1 day 12 hours 59 minutes 59 seconds")) - - # make sure we can't call serialize_string_literal with a non basestring type - with self.assertRaises(TypeError): - self.assertRaises(serialize_string_literal(2.31)) class TestDeserialize(unittest.TestCase): def assertDeserializeEqual(self, expected, arg): @@ -214,7 +200,6 @@ class TestDeserialize(unittest.TestCase): """ assert_equals(expected, deserialize_field(self.test_field(), arg)) - def assertDeserializeNonString(self): """ Asserts input value is returned for None or something that is not a string. diff --git a/common/lib/xmodule/xmodule/xml_module.py b/common/lib/xmodule/xmodule/xml_module.py index 043727d082..0528bbfb6c 100644 --- a/common/lib/xmodule/xmodule/xml_module.py +++ b/common/lib/xmodule/xmodule/xml_module.py @@ -83,21 +83,15 @@ def serialize_field(value): """ Return a string version of the value (where value is the JSON-formatted, internally stored value). - By default, this is the result of calling json.dumps on the input value. + If the value is a string, then we simply return what was passed in. + Otherwise, we return json.dumps on the input value. """ + if isinstance(value, basestring): + return value + return json.dumps(value, cls=EdxJSONEncoder) -def serialize_string_literal(value): - """ - Assert that the value is a base string and - if it is - simply return it - """ - if not isinstance(value, basestring): - raise TypeError('Value {0} is not of type basestring!'.format(value)) - - return value - - def deserialize_field(field, value): """ Deserialize the string version to the value stored internally. @@ -176,7 +170,7 @@ class XmlDescriptor(XModuleDescriptor): for field in set(cls.fields + cls.lms.fields): if field.name == attr: from_xml = lambda val: deserialize_field(field, val) - to_xml = lambda val: serialize_string_literal(val) if isinstance(val, basestring) else serialize_field(val) + to_xml = lambda val: serialize_field(val) return AttrMap(from_xml, to_xml) return AttrMap() From a6f349dab9f9b0694a3e8e98553e11a24756d0c1 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Fri, 12 Jul 2013 09:40:51 -0400 Subject: [PATCH 756/995] Studio i18n --- cms/djangoapps/contentstore/utils.py | 5 +- cms/djangoapps/contentstore/views/tabs.py | 6 +- cms/djangoapps/contentstore/views/user.py | 9 +- cms/static/client_templates/checklist.html | 10 +- .../course_info_handouts.html | 2 +- cms/static/coffee/files.json | 1 + cms/static/js/base.js | 38 ++-- .../js/models/settings/course_details.js | 12 +- .../models/settings/course_grading_policy.js | 12 +- cms/static/js/views/textbook.js | 10 +- cms/templates/404.html | 13 +- cms/templates/500.html | 13 +- cms/templates/activation_active.html | 7 +- cms/templates/activation_complete.html | 7 +- cms/templates/activation_invalid.html | 13 +- cms/templates/asset_index.html | 4 +- cms/templates/base.html | 3 +- cms/templates/checklists.html | 13 +- cms/templates/course_info.html | 13 +- cms/templates/edit-tabs.html | 21 +- cms/templates/edit_subsection.html | 77 ++++--- cms/templates/emails/activation_email.txt | 10 +- .../emails/activation_email_subject.txt | 3 +- cms/templates/error.html | 27 ++- cms/templates/export.html | 32 +-- cms/templates/howitworks.html | 94 +++++---- cms/templates/import.html | 28 +-- cms/templates/index.html | 10 +- cms/templates/login.html | 33 +-- cms/templates/manage_users.html | 13 +- cms/templates/overview.html | 79 +++---- .../registration/activation_complete.html | 13 +- cms/templates/registration/reg_complete.html | 4 +- cms/templates/settings.html | 2 +- cms/templates/settings_advanced.html | 31 +-- .../settings_discussions_faculty.html | 197 +++++++++--------- cms/templates/settings_graders.html | 39 ++-- cms/templates/signup.html | 61 +++--- cms/templates/static-pages.html | 13 +- cms/templates/unit.html | 54 ++--- cms/templates/widgets/header.html | 13 +- cms/templates/widgets/sock.html | 27 +-- cms/urls.py | 2 - common/djangoapps/mitxmako/middleware.py | 1 + common/djangoapps/student/views.py | 68 +++--- common/lib/xmodule/xmodule/course_module.py | 1 - common/lib/xmodule/xmodule/util/date_utils.py | 17 +- 47 files changed, 600 insertions(+), 561 deletions(-) diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 452806fe64..5fa0d949b0 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -9,12 +9,13 @@ import copy import logging import re from xmodule.modulestore.draft import DIRECT_ONLY_CATEGORIES +from django.utils.translation import ugettext as _ log = logging.getLogger(__name__) # In order to instantiate an open ended tab automatically, need to have this data -OPEN_ENDED_PANEL = {"name": "Open Ended Panel", "type": "open_ended"} -NOTES_PANEL = {"name": "My Notes", "type": "notes"} +OPEN_ENDED_PANEL = {"name": _("Open Ended Panel"), "type": "open_ended"} +NOTES_PANEL = {"name": _("My Notes"), "type": "notes"} EXTRA_TAB_PANELS = dict([(p['type'], p) for p in [OPEN_ENDED_PANEL, NOTES_PANEL]]) diff --git a/cms/djangoapps/contentstore/views/tabs.py b/cms/djangoapps/contentstore/views/tabs.py index a7b232e92a..154f9fb55d 100644 --- a/cms/djangoapps/contentstore/views/tabs.py +++ b/cms/djangoapps/contentstore/views/tabs.py @@ -13,7 +13,7 @@ from xmodule.modulestore.django import modulestore from ..utils import get_course_for_item, get_modulestore from .access import get_location_and_verify_access -__all__ = ['edit_tabs', 'reorder_static_tabs', 'static_pages', 'edit_static'] +__all__ = ['edit_tabs', 'reorder_static_tabs', 'static_pages'] def initialize_course_tabs(course): @@ -127,7 +127,3 @@ def static_pages(request, org, course, coursename): return render_to_response('static-pages.html', { 'context_course': course, }) - - -def edit_static(request, org, course, coursename): - return render_to_response('edit-static-page.html', {}) diff --git a/cms/djangoapps/contentstore/views/user.py b/cms/djangoapps/contentstore/views/user.py index dae0d246a5..948ed614d2 100644 --- a/cms/djangoapps/contentstore/views/user.py +++ b/cms/djangoapps/contentstore/views/user.py @@ -2,6 +2,7 @@ from django.conf import settings from django.core.exceptions import PermissionDenied from django.core.urlresolvers import reverse from django.contrib.auth.decorators import login_required +from django.utils.translation import ugettext as _ from django_future.csrf import ensure_csrf_cookie from mitxmako.shortcuts import render_to_response @@ -78,7 +79,7 @@ def add_user(request, location): if not email: msg = { 'Status': 'Failed', - 'ErrMsg': 'Please specify an email address.', + 'ErrMsg': _('Please specify an email address.'), } return JsonResponse(msg, 400) @@ -92,7 +93,7 @@ def add_user(request, location): if user is None: msg = { 'Status': 'Failed', - 'ErrMsg': "Could not find user by email address '{0}'.".format(email), + 'ErrMsg': _("Could not find user by email address '{email}'.").format(email=email), } return JsonResponse(msg, 404) @@ -100,7 +101,7 @@ def add_user(request, location): if not user.is_active: msg = { 'Status': 'Failed', - 'ErrMsg': 'User {0} has registered but has not yet activated his/her account.'.format(email), + 'ErrMsg': _('User {email} has registered but has not yet activated his/her account.').format(email=email), } return JsonResponse(msg, 400) @@ -129,7 +130,7 @@ def remove_user(request, location): if user is None: msg = { 'Status': 'Failed', - 'ErrMsg': "Could not find user by email address '{0}'.".format(email), + 'ErrMsg': _("Could not find user by email address '{email}'.").format(email=email), } return JsonResponse(msg, 404) diff --git a/cms/static/client_templates/checklist.html b/cms/static/client_templates/checklist.html index e985ab9509..5d36264d07 100644 --- a/cms/static/client_templates/checklist.html +++ b/cms/static/client_templates/checklist.html @@ -6,15 +6,15 @@ class="course-checklist" <% } %> id="<%= 'course-checklist' + checklistIndex %>"> - <% var widthPercentage = 'width:' + percentChecked + '%;'; %> - - <%= percentChecked %>% of checklist completed + + <%= _.template(gettext("{number}% of checklists completed"), {number: '' + percentChecked + ''}, {interpolate: /\{(.+?)\}/g}) %> +

      <%= checklistShortDescription %>

      - Tasks Completed: <%= itemsChecked %>/<%= items.length %> + <%= gettext("Tasks Completed:") %> <%= itemsChecked %>/<%= items.length %>
      @@ -47,7 +47,7 @@
    9. - rel="external" title="This link will open in a new browser window/tab" + rel="external" title="<%= gettext("This link will open in a new browser window/tab") %>" <% } %> ><%= item['action_text'] %>
    10. diff --git a/cms/static/client_templates/course_info_handouts.html b/cms/static/client_templates/course_info_handouts.html index 958a1c77d6..cf9fdb5c85 100644 --- a/cms/static/client_templates/course_info_handouts.html +++ b/cms/static/client_templates/course_info_handouts.html @@ -6,7 +6,7 @@ <%= model.get('data') %> <% } else {%> -

      You have no handouts defined

      +

      ${_("You have no handouts defined")}

      <% } %>
      diff --git a/cms/static/coffee/files.json b/cms/static/coffee/files.json index 3c27629f69..3964bee455 100644 --- a/cms/static/coffee/files.json +++ b/cms/static/coffee/files.json @@ -1,5 +1,6 @@ { "static_files": [ + "../jsi18n/", "js/vendor/RequireJS.js", "js/vendor/jquery.min.js", "js/vendor/jquery-ui.min.js", diff --git a/cms/static/js/base.js b/cms/static/js/base.js index d597c2af27..f74a6cb5e4 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -79,10 +79,10 @@ $(document).ready(function() { }); // general link management - new window/tab - $('a[rel="external"]').attr('title', 'This link will open in a new browser window/tab').bind('click', linkNewWindow); + $('a[rel="external"]').attr('title', gettext('This link will open in a new browser window/tab')).bind('click', linkNewWindow); // general link management - lean modal window - $('a[rel="modal"]').attr('title', 'This link will open in a modal window').leanModal({ + $('a[rel="modal"]').attr('title', gettext('This link will open in a modal window')).leanModal({ overlay: 0.50, closeButton: '.action-modal-close' }); @@ -199,8 +199,10 @@ function toggleSections(e) { $section = $('.courseware-section'); sectionCount = $section.length; $button = $(this); - $labelCollapsed = $(' Collapse All Sections'); - $labelExpanded = $(' Expand All Sections'); + $labelCollapsed = $(' ' + + gettext('Collapse All Sections') + ''); + $labelExpanded = $(' ' + + gettext('Expand All Sections') + ''); var buttonLabel = $button.hasClass('is-activated') ? $labelCollapsed : $labelExpanded; $button.toggleClass('is-activated').html(buttonLabel); @@ -326,7 +328,7 @@ function saveSubsection() { $changedInput = null; }, error: function() { - showToastMessage('There has been an error while saving your changes.'); + showToastMessage(gettext('There has been an error while saving your changes.')); } }); } @@ -372,7 +374,7 @@ function deleteSection(e) { } function _deleteItem($el) { - if (!confirm('Are you sure you wish to delete this item. It cannot be reversed!')) return; + if (!confirm(gettext('Are you sure you wish to delete this item. It cannot be reversed!'))) return; var id = $el.data('id'); @@ -599,7 +601,7 @@ function saveNewCourse(e) { var display_name = $newCourse.find('.new-course-name').val(); if (org == '' || number == '' || display_name == '') { - alert('You must specify all fields in order to create a new course.'); + alert(gettext('You must specify all fields in order to create a new course.')); return; } @@ -730,18 +732,16 @@ function saveSetSectionScheduleDate(e) { }) }).success(function() { var $thisSection = $('.courseware-section[data-id="' + id + '"]'); - var format = gettext('Will Release: %(date)s at %(time)s UTC'); - var willReleaseAt = interpolate(format, { - 'date': input_date, - 'time': input_time - }, - true); - $thisSection.find('.section-published-date').html( - '' + willReleaseAt + '' + - '' + gettext('Edit') + ''); + var html = _.template( + '' + + '' + gettext("Will Release:") + '' + + gettext("<%= date %> at <%= time %> UTC") + + '' + + '' + + gettext("Edit") + + '', + {date: input_date, time: input_time, id: id}); + $thisSection.find('.section-published-date').html(html); hideModal(); saving.hide(); }); diff --git a/cms/static/js/models/settings/course_details.js b/cms/static/js/models/settings/course_details.js index 993832f830..d7e11d5689 100644 --- a/cms/static/js/models/settings/course_details.js +++ b/cms/static/js/models/settings/course_details.js @@ -38,23 +38,23 @@ CMS.Models.Settings.CourseDetails = Backbone.Model.extend({ // A bit funny in that the video key validation is asynchronous; so, it won't stop the validation. var errors = {}; if (newattrs.start_date === null) { - errors.start_date = "The course must have an assigned start date."; + errors.start_date = gettext("The course must have an assigned start date."); } if (newattrs.start_date && newattrs.end_date && newattrs.start_date >= newattrs.end_date) { - errors.end_date = "The course end date cannot be before the course start date."; + errors.end_date = gettext("The course end date cannot be before the course start date."); } if (newattrs.start_date && newattrs.enrollment_start && newattrs.start_date < newattrs.enrollment_start) { - errors.enrollment_start = "The course start date cannot be before the enrollment start date."; + errors.enrollment_start = gettext("The course start date cannot be before the enrollment start date."); } if (newattrs.enrollment_start && newattrs.enrollment_end && newattrs.enrollment_start >= newattrs.enrollment_end) { - errors.enrollment_end = "The enrollment start date cannot be after the enrollment end date."; + errors.enrollment_end = gettext("The enrollment start date cannot be after the enrollment end date."); } if (newattrs.end_date && newattrs.enrollment_end && newattrs.end_date < newattrs.enrollment_end) { - errors.enrollment_end = "The enrollment end date cannot be after the course end date."; + errors.enrollment_end = gettext("The enrollment end date cannot be after the course end date."); } if (newattrs.intro_video && newattrs.intro_video !== this.get('intro_video')) { if (this._videokey_illegal_chars.exec(newattrs.intro_video)) { - errors.intro_video = "Key should only contain letters, numbers, _, or -"; + errors.intro_video = gettext("Key should only contain letters, numbers, _, or -"); } // TODO check if key points to a real video using google's youtube api } diff --git a/cms/static/js/models/settings/course_grading_policy.js b/cms/static/js/models/settings/course_grading_policy.js index 3014b39e82..04ae3f4c32 100644 --- a/cms/static/js/models/settings/course_grading_policy.js +++ b/cms/static/js/models/settings/course_grading_policy.js @@ -79,14 +79,14 @@ CMS.Models.Settings.CourseGrader = Backbone.Model.extend({ // FIXME somehow this.collection is unbound sometimes. I can't track down when var existing = this.collection && this.collection.some(function(other) { return (other.cid != this.cid) && (other.get('type') == attrs['type']);}, this); if (existing) { - errors.type = "There's already another assignment type with this name."; + errors.type = gettext("There's already another assignment type with this name."); } } } if (_.has(attrs, 'weight')) { var intWeight = parseInt(attrs.weight); // see if this ensures value saved is int if (!isFinite(intWeight) || /\D+/.test(attrs.weight) || intWeight < 0 || intWeight > 100) { - errors.weight = "Please enter an integer between 0 and 100."; + errors.weight = gettext("Please enter an integer between 0 and 100."); } else { attrs.weight = intWeight; @@ -100,18 +100,20 @@ CMS.Models.Settings.CourseGrader = Backbone.Model.extend({ }} if (_.has(attrs, 'min_count')) { if (!isFinite(attrs.min_count) || /\D+/.test(attrs.min_count)) { - errors.min_count = "Please enter an integer."; + errors.min_count = gettext("Please enter an integer."); } else attrs.min_count = parseInt(attrs.min_count); } if (_.has(attrs, 'drop_count')) { if (!isFinite(attrs.drop_count) || /\D+/.test(attrs.drop_count)) { - errors.drop_count = "Please enter an integer."; + errors.drop_count = gettext("Please enter an integer."); } else attrs.drop_count = parseInt(attrs.drop_count); } if (_.has(attrs, 'min_count') && _.has(attrs, 'drop_count') && attrs.drop_count > attrs.min_count) { - errors.drop_count = "Cannot drop more " + attrs.type + " than will assigned."; + errors.drop_count = _.template( + gettext("Cannot drop more <% attrs.types %> than will assigned."), + attrs, {variable: 'attrs'}); } if (!_.isEmpty(errors)) return errors; } diff --git a/cms/static/js/views/textbook.js b/cms/static/js/views/textbook.js index fe12082c7a..74eaae8601 100644 --- a/cms/static/js/views/textbook.js +++ b/cms/static/js/views/textbook.js @@ -26,8 +26,8 @@ CMS.Views.ShowTextbook = Backbone.View.extend({ if(e && e.preventDefault) { e.preventDefault(); } var textbook = this.model, collection = this.model.collection; var msg = new CMS.Views.Prompt.Warning({ - title: _.str.sprintf(gettext("Delete “%s”?"), - textbook.escape('name')), + title: _.template(gettext("Delete “<%= name %>”?"), + {name: textbook.escape('name')}), message: gettext("Deleting a textbook cannot be undone and once deleted any reference to it in your courseware's navigation will also be removed."), actions: { primary: { @@ -241,8 +241,8 @@ CMS.Views.EditChapter = Backbone.View.extend({ asset_path: this.$("input.chapter-asset-path").val() }); var msg = new CMS.Models.FileUpload({ - title: _.str.sprintf(gettext("Upload a new asset to %s"), - section.escape('name')), + title: _.template(gettext("Upload a new asset to “<%= name %>”"), + {name: section.escape('name')}), message: "Files must be in PDF format." }); var view = new CMS.Views.UploadDialog({model: msg, chapter: this.model}); @@ -260,7 +260,7 @@ CMS.Views.UploadDialog = Backbone.View.extend({ this.listenTo(this.model, "change", this.render); }, render: function() { - var isValid = this.model.isValid() + var isValid = this.model.isValid(); var selectedFile = this.model.get('selectedFile'); var oldInput = this.$("input[type=file]").get(0); this.$el.html(this.template({ diff --git a/cms/templates/404.html b/cms/templates/404.html index a45a223bad..be7a66a31c 100644 --- a/cms/templates/404.html +++ b/cms/templates/404.html @@ -1,14 +1,19 @@ +<%! from django.utils.translation import ugettext as _ %> <%inherit file="base.html" /> -<%block name="title">Page Not Found +<%block name="title">${_("Page Not Found")} <%block name="content">
      -

      Page not found

      -

      The page that you were looking for was not found. Go back to the homepage or let us know about any pages that may have been moved at technical@edx.org.

      +

      ${_("Page not found")}

      +

      ${_('The page that you were looking for was not found.')} + ${_('Go back to the {homepage} or let us know about any pages that may have been moved at {email}.').format( + homepage='homepage', + email='technical@edx.org')} +

      - \ No newline at end of file + diff --git a/cms/templates/500.html b/cms/templates/500.html index 3d18d9dcc5..5d79dd7a16 100644 --- a/cms/templates/500.html +++ b/cms/templates/500.html @@ -1,18 +1,19 @@ +<%! from django.utils.translation import ugettext as _ %> <%inherit file="base.html" /> -<%block name="title">Studio Server Error +<%block name="title">${_("Studio Server Error")} <%block name="content">
      -

      The Studio servers encountered an error

      +

      ${_("The Studio servers encountered an error")}

      - An error occurred in Studio and the page could not be loaded. Please try again in a few moments. - We've logged the error and our staff is currently working to resolve this error as soon as possible. - If the problem persists, please email us at technical@edx.org. + ${_("An error occurred in Studio and the page could not be loaded. Please try again in a few moments.")} + ${_("We've logged the error and our staff is currently working to resolve this error as soon as possible.")} + ${_('If the problem persists, please email us at {email}.').format(email='technical@edx.org')}

      - \ No newline at end of file + diff --git a/cms/templates/activation_active.html b/cms/templates/activation_active.html index 712c73abf9..9a4ebd7e4e 100644 --- a/cms/templates/activation_active.html +++ b/cms/templates/activation_active.html @@ -1,3 +1,4 @@ +<%! from django.utils.translation import ugettext as _ %> <%inherit file="base.html" /> <%block name="content"> @@ -6,9 +7,9 @@
      -

      Account already active!

      -

      This account has already been activated. Log in here.

      +

      ${_("Account already active!")}

      +

      ${_('This account has already been activated.')}${_("Log in here.")}

      - \ No newline at end of file + diff --git a/cms/templates/activation_complete.html b/cms/templates/activation_complete.html index 1e195a632c..d845c5153b 100644 --- a/cms/templates/activation_complete.html +++ b/cms/templates/activation_complete.html @@ -1,12 +1,13 @@ +<%! from django.utils.translation import ugettext as _ %> <%inherit file="base.html" /> <%block name="content">
      -

      Activation Complete!

      -

      Thanks for activating your account. Log in here.

      +

      ${_("Activation Complete!")}

      +

      ${_('Thanks for activating your account.')}${_("Log in here.")}

      - \ No newline at end of file + diff --git a/cms/templates/activation_invalid.html b/cms/templates/activation_invalid.html index c4eb16875b..3ee4e8ec4e 100644 --- a/cms/templates/activation_invalid.html +++ b/cms/templates/activation_invalid.html @@ -1,16 +1,15 @@ +<%! from django.utils.translation import ugettext as _ %> <%inherit file="base.html" /> <%block name="content">
      -

      Activation Invalid

      +

      ${_("Activation Invalid")}

      -

      Something went wrong. Check to make sure the URL you went to was - correct -- e-mail programs will sometimes split it into two - lines. If you still have issues, e-mail us to let us know what happened - at bugs@mitx.mit.edu.

      +

      ${_('Something went wrong. Check to make sure the URL you went to was correct -- e-mail programs will sometimes split it into two lines. If you still have issues, e-mail us to let us know what happened at {email}.').format(email='bugs@mitx.mit.edu')}

      -

      Or you can go back to the home page.

      +

      ${_('Or you can go back to the {link_start}home page{link_end}.').format( + link_start='', link_end='')}

      - \ No newline at end of file + diff --git a/cms/templates/asset_index.html b/cms/templates/asset_index.html index bdad7b7b88..6c92994a6f 100644 --- a/cms/templates/asset_index.html +++ b/cms/templates/asset_index.html @@ -2,7 +2,7 @@ <%! from django.core.urlresolvers import reverse %> <%! from django.utils.translation import ugettext as _ %> <%block name="bodyclass">is-signedin course uploads -<%block name="title">Files & Uploads +<%block name="title">${_("Files & Uploads")} <%namespace name='static' file='static_content.html'/> @@ -48,7 +48,7 @@

      Page Actions

      diff --git a/cms/templates/base.html b/cms/templates/base.html index e58dcdfc60..44ebf59170 100644 --- a/cms/templates/base.html +++ b/cms/templates/base.html @@ -1,3 +1,4 @@ +## -*- coding: utf-8 -*- <%namespace name='static' file='static_content.html'/> @@ -17,6 +18,7 @@ + <%static:css group='base-style'/> @@ -35,7 +37,6 @@ ## javascript - diff --git a/cms/templates/checklists.html b/cms/templates/checklists.html index 6f78e952c0..ad4f29aeb6 100644 --- a/cms/templates/checklists.html +++ b/cms/templates/checklists.html @@ -1,3 +1,4 @@ +<%! from django.utils.translation import ugettext as _ %> <%inherit file="base.html" /> <%! from django.core.urlresolvers import reverse %> <%block name="title">Course Checklists @@ -30,8 +31,8 @@

      - Tools - > Course Checklists + ${_("Tools")} + > ${_("Course Checklists")}

      @@ -40,18 +41,18 @@
      -

      Current Checklists

      +

      ${_("Current Checklists")}

      -children: [] diff --git a/common/lib/xmodule/xmodule/templates/html/empty.yaml b/common/lib/xmodule/xmodule/templates/html/empty.yaml deleted file mode 100644 index 0967ef424b..0000000000 --- a/common/lib/xmodule/xmodule/templates/html/empty.yaml +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/common/lib/xmodule/xmodule/templates/html/everything.yaml b/common/lib/xmodule/xmodule/templates/html/everything.yaml deleted file mode 100644 index 348ce64fa1..0000000000 --- a/common/lib/xmodule/xmodule/templates/html/everything.yaml +++ /dev/null @@ -1,33 +0,0 @@ ---- -metadata: - display_name: Announcement - -data: | -

      Heading of document

      -

      First subheading

      -

      This is a paragraph. It will take care of line breaks for you.

      HTML only parses the location - - of tags for inserting line breaks into your doc, not - line - breaks - you - add - yourself. -

      -

      Links

      -

      You can refer to other parts of the internet with a link, to other parts of your course by prepending your link with /course/

      -

      Now a list:

      -
        -
      • An item
      • -
      • Another item
      • -
      • And yet another
      • -
      -

      This list has an ordering

      -
        -
      1. An item
      2. -
      3. Another item
      4. -
      5. Yet another item
      6. -
      -

      Note, we have a lot of standard edX styles, so please try to avoid any custom styling, and make sure that you make a note of any custom styling that you do yourself so that we can incorporate it into - tools that other people can use.

      -children: [] diff --git a/common/lib/xmodule/xmodule/templates/html/latex_html.yaml b/common/lib/xmodule/xmodule/templates/html/latex_html.yaml index ba5c4b5c06..2db7e98c65 100644 --- a/common/lib/xmodule/xmodule/templates/html/latex_html.yaml +++ b/common/lib/xmodule/xmodule/templates/html/latex_html.yaml @@ -19,4 +19,3 @@ data: | It is very convenient to write complex equations in LaTeX.

      -children: [] diff --git a/common/lib/xmodule/xmodule/templates/peer_grading/default.yaml b/common/lib/xmodule/xmodule/templates/peer_grading/default.yaml deleted file mode 100644 index 0967ef424b..0000000000 --- a/common/lib/xmodule/xmodule/templates/peer_grading/default.yaml +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/common/lib/xmodule/xmodule/templates/problem/circuitschematic.yaml b/common/lib/xmodule/xmodule/templates/problem/circuitschematic.yaml index 3b051f2ba8..1717bb91ad 100644 --- a/common/lib/xmodule/xmodule/templates/problem/circuitschematic.yaml +++ b/common/lib/xmodule/xmodule/templates/problem/circuitschematic.yaml @@ -1,9 +1,9 @@ - --- metadata: display_name: Circuit Schematic Builder rerandomize: never showanswer: finished + markdown: !!null data: | Please make a voltage divider that splits the provided voltage evenly. diff --git a/common/lib/xmodule/xmodule/templates/problem/customgrader.yaml b/common/lib/xmodule/xmodule/templates/problem/customgrader.yaml index 48feef481b..05de74f28c 100644 --- a/common/lib/xmodule/xmodule/templates/problem/customgrader.yaml +++ b/common/lib/xmodule/xmodule/templates/problem/customgrader.yaml @@ -1,50 +1,47 @@ --- metadata: display_name: Custom Python-Evaluated Input - rerandomize: never - showanswer: finished + markdown: !!null data: | - -

      - A custom python-evaluated input problem accepts one or more lines of text input from the - student, and evaluates the inputs for correctness based on evaluation using a - python script embedded within the problem. -

      + +

      + A custom python-evaluated input problem accepts one or more lines of text input from the + student, and evaluates the inputs for correctness based on evaluation using a + python script embedded within the problem. +

      - + -

      Enter two integers which sum to 10:

      - -
      - -
      +

      Enter two integers which sum to 10:

      + +
      + +
      -

      Enter two integers which sum to 20:

      - -
      - -
      - - -
      -

      Explanation

      -

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

      - -
      -
      -
      - -children: [] +

      Enter two integers which sum to 20:

      + +
      + +
      + + +
      +

      Explanation

      +

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

      + +
      +
      +
      diff --git a/common/lib/xmodule/xmodule/templates/problem/empty.yaml b/common/lib/xmodule/xmodule/templates/problem/empty.yaml deleted file mode 100644 index 0967ef424b..0000000000 --- a/common/lib/xmodule/xmodule/templates/problem/empty.yaml +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/common/lib/xmodule/xmodule/templates/problem/emptyadvanced.yaml b/common/lib/xmodule/xmodule/templates/problem/emptyadvanced.yaml deleted file mode 100644 index 3d696ec2fd..0000000000 --- a/common/lib/xmodule/xmodule/templates/problem/emptyadvanced.yaml +++ /dev/null @@ -1,10 +0,0 @@ ---- -metadata: - display_name: Blank Advanced Problem - rerandomize: never - showanswer: finished -data: | - - - -children: [] diff --git a/common/lib/xmodule/xmodule/templates/problem/forumularesponse.yaml b/common/lib/xmodule/xmodule/templates/problem/forumularesponse.yaml index 0401a01c31..4cf877bd1f 100644 --- a/common/lib/xmodule/xmodule/templates/problem/forumularesponse.yaml +++ b/common/lib/xmodule/xmodule/templates/problem/forumularesponse.yaml @@ -3,6 +3,7 @@ metadata: display_name: Math Expression Input rerandomize: never showanswer: finished + markdown: !!null data: |

      @@ -43,5 +44,3 @@ data: |

      - -children: [] diff --git a/common/lib/xmodule/xmodule/templates/problem/imageresponse.yaml b/common/lib/xmodule/xmodule/templates/problem/imageresponse.yaml index ab1f22e3b2..566997671d 100644 --- a/common/lib/xmodule/xmodule/templates/problem/imageresponse.yaml +++ b/common/lib/xmodule/xmodule/templates/problem/imageresponse.yaml @@ -3,6 +3,7 @@ metadata: display_name: Image Mapped Input rerandomize: never showanswer: finished + markdown: !!null data: |

      @@ -21,6 +22,3 @@ data: | - - -children: [] diff --git a/common/lib/xmodule/xmodule/templates/problem/latex_problem.yaml b/common/lib/xmodule/xmodule/templates/problem/latex_problem.yaml index 82d7e8c1ae..097055cfe3 100644 --- a/common/lib/xmodule/xmodule/templates/problem/latex_problem.yaml +++ b/common/lib/xmodule/xmodule/templates/problem/latex_problem.yaml @@ -85,6 +85,7 @@ metadata: can contain equations: $\alpha = \frac{2}{\sqrt{1+\gamma}}$ } This is some text after the showhide example. + markdown: !!null data: | @@ -214,4 +215,3 @@ data: |

      -children: [] diff --git a/common/lib/xmodule/xmodule/templates/problem/multiplechoice.yaml b/common/lib/xmodule/xmodule/templates/problem/multiplechoice.yaml index 10d51de280..202fc03b44 100644 --- a/common/lib/xmodule/xmodule/templates/problem/multiplechoice.yaml +++ b/common/lib/xmodule/xmodule/templates/problem/multiplechoice.yaml @@ -3,33 +3,25 @@ metadata: display_name: Multiple Choice rerandomize: never showanswer: finished - markdown: - "A multiple choice problem presents radio buttons for student input. Students can only select a single + markdown: | + A multiple choice problem presents radio buttons for student input. Students can only select a single option presented. Multiple Choice questions have been the subject of many areas of research due to the early invention and adoption of bubble sheets. - One of the main elements that goes into a good multiple choice question is the existence of good distractors. That is, each of the alternate responses presented to the student should be the result of a plausible mistake that a student might make. - What Apple device competed with the portable CD player? - ( ) The iPad - ( ) Napster - (x) The iPod - ( ) The vegetable peeler - [explanation] The release of the iPod allowed consumers to carry their entire music library with them in a format that did not rely on fragile and energy-intensive spinning disks. [explanation] - " data: |

      @@ -54,4 +46,3 @@ data: | -children: [] diff --git a/common/lib/xmodule/xmodule/templates/problem/numericalresponse.yaml b/common/lib/xmodule/xmodule/templates/problem/numericalresponse.yaml index 548fd94fab..9b2ddec2a7 100644 --- a/common/lib/xmodule/xmodule/templates/problem/numericalresponse.yaml +++ b/common/lib/xmodule/xmodule/templates/problem/numericalresponse.yaml @@ -3,43 +3,33 @@ metadata: display_name: Numerical Input rerandomize: never showanswer: finished - markdown: - "A numerical input problem accepts a line of text input from the + markdown: | + A numerical input problem accepts a line of text input from the student, and evaluates the input for correctness based on its numerical value. - The answer is correct if it is within a specified numerical tolerance of the expected answer. - Enter the numerical value of Pi: - = 3.14159 +- .02 - Enter the approximate value of 502*9: - = 4518 +- 15% - - + Enter the number of fingers on a human hand: - = 5 - [explanation] Pi, or the the ratio between a circle's circumference to its diameter, is an irrational number known to extreme precision. It is value is approximately equal to 3.14. - + Although you can get an exact value by typing 502*9 into a calculator, the result will be close to 500*10, or 5,000. The grader accepts any response within 15% of the true value, 4518, so that you can use any estimation technique that you like. - + If you look at your hand, you can count that you have five fingers. [explanation] - " - data: |

      @@ -83,5 +73,3 @@ data: | - -children: [] diff --git a/common/lib/xmodule/xmodule/templates/problem/optionresponse.yaml b/common/lib/xmodule/xmodule/templates/problem/optionresponse.yaml index c2edfb1cbc..8e59f8ae4d 100644 --- a/common/lib/xmodule/xmodule/templates/problem/optionresponse.yaml +++ b/common/lib/xmodule/xmodule/templates/problem/optionresponse.yaml @@ -3,19 +3,16 @@ metadata: display_name: Dropdown rerandomize: never showanswer: finished - markdown: - "Dropdown problems give a limited set of options for students to respond with, and present those options + markdown: | + Dropdown problems give a limited set of options for students to respond with, and present those options in a format that encourages them to search for a specific answer rather than being immediately presented with options from which to recognize the correct answer. - The answer options and the identification of the correct answer is defined in the optioninput tag. - Translation between Dropdown and __________ is extremely straightforward: - - [[(Multiple Choice), Text Input, Numerical Input, External Response, Image Response]] + [[(Multiple Choice), Text Input, Numerical Input, External Response, Image Response]] [explanation] Multiple Choice also allows students to select from a variety of pre-written responses, although the @@ -23,7 +20,6 @@ metadata: slightly because students are more likely to think of an answer and then search for it rather than relying purely on recognition to answer the question. [explanation] - " data: |

      Dropdown problems give a limited set of options for students to respond with, and present those options @@ -45,4 +41,3 @@ data: | -children: [] diff --git a/common/lib/xmodule/xmodule/templates/problem/problem_with_hint.yaml b/common/lib/xmodule/xmodule/templates/problem/problem_with_hint.yaml index 73a94ed941..0d93cd3c5e 100644 --- a/common/lib/xmodule/xmodule/templates/problem/problem_with_hint.yaml +++ b/common/lib/xmodule/xmodule/templates/problem/problem_with_hint.yaml @@ -46,7 +46,7 @@ metadata: enter your answer in upper or lower case, with or without quotes. \edXabox{type="custom" cfn='test_str' expect='python' hintfn='hint_fn'} - + markdown: !!null data: | @@ -92,4 +92,3 @@ data: |

      -children: [] diff --git a/common/lib/xmodule/xmodule/templates/problem/string_response.yaml b/common/lib/xmodule/xmodule/templates/problem/string_response.yaml index 64e3dc062f..9c59ae3bc2 100644 --- a/common/lib/xmodule/xmodule/templates/problem/string_response.yaml +++ b/common/lib/xmodule/xmodule/templates/problem/string_response.yaml @@ -3,16 +3,13 @@ metadata: display_name: Text Input rerandomize: never showanswer: finished - # Note, the extra newlines are needed to make the yaml parser add blank lines instead of folding - markdown: - "A text input problem accepts a line of text from the + markdown: | + A text input problem accepts a line of text from the student, and evaluates the input for correctness based on an expected answer. - The answer is correct if it matches every character of the expected answer. This can be a problem with international spelling, dates, or anything where the format of the answer is not clear. - Which US state has Lansing as its capital? @@ -23,9 +20,8 @@ metadata: Lansing is the capital of Michigan, although it is not Michgan's largest city, or even the seat of the county in which it resides. [explanation] - " data: | - +

      A text input problem accepts a line of text from the @@ -46,4 +42,3 @@ data: | -children: [] diff --git a/common/lib/xmodule/xmodule/templates/sequence/with_video.yaml b/common/lib/xmodule/xmodule/templates/sequence/with_video.yaml deleted file mode 100644 index a56d44ebff..0000000000 --- a/common/lib/xmodule/xmodule/templates/sequence/with_video.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -metadata: - display_name: Sequence with Video - data_dir: a_made_up_name -data: '' -children: - - 'i4x://edx/templates/video/default' diff --git a/common/lib/xmodule/xmodule/templates/statictab/empty.yaml b/common/lib/xmodule/xmodule/templates/statictab/empty.yaml deleted file mode 100644 index 9e26dfeeb6..0000000000 --- a/common/lib/xmodule/xmodule/templates/statictab/empty.yaml +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/common/lib/xmodule/xmodule/templates/video/default.yaml b/common/lib/xmodule/xmodule/templates/video/default.yaml deleted file mode 100644 index 0967ef424b..0000000000 --- a/common/lib/xmodule/xmodule/templates/video/default.yaml +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml b/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml deleted file mode 100644 index 0967ef424b..0000000000 --- a/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/common/lib/xmodule/xmodule/templates/word_cloud/default.yaml b/common/lib/xmodule/xmodule/templates/word_cloud/default.yaml deleted file mode 100644 index 0967ef424b..0000000000 --- a/common/lib/xmodule/xmodule/templates/word_cloud/default.yaml +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/common/lib/xmodule/xmodule/tests/test_logic.py b/common/lib/xmodule/xmodule/tests/test_logic.py index 9be533885c..5fe7aa2832 100644 --- a/common/lib/xmodule/xmodule/tests/test_logic.py +++ b/common/lib/xmodule/xmodule/tests/test_logic.py @@ -28,7 +28,8 @@ class LogicTest(unittest.TestCase): def setUp(self): class EmptyClass: """Empty object.""" - pass + url_name = '' + category = 'test' self.system = get_test_system() self.descriptor = EmptyClass() diff --git a/common/lib/xmodule/xmodule/tests/test_xml_module.py b/common/lib/xmodule/xmodule/tests/test_xml_module.py index 7ccc71dd96..a277ff2900 100644 --- a/common/lib/xmodule/xmodule/tests/test_xml_module.py +++ b/common/lib/xmodule/xmodule/tests/test_xml_module.py @@ -141,6 +141,7 @@ class EditableMetadataFieldsTest(unittest.TestCase): def get_xml_editable_fields(self, model_data): system = get_test_system() system.render_template = Mock(return_value="

      Test Template HTML
      ") + model_data['category'] = 'test' return XmlDescriptor(runtime=system, model_data=model_data).editable_metadata_fields def get_descriptor(self, model_data): diff --git a/common/lib/xmodule/xmodule/video_module.py b/common/lib/xmodule/xmodule/video_module.py index ebff888f34..381cc9a622 100644 --- a/common/lib/xmodule/xmodule/video_module.py +++ b/common/lib/xmodule/xmodule/video_module.py @@ -97,7 +97,6 @@ class VideoDescriptor(VideoFields, MetadataOnlyEditingDescriptor, RawDescriptor): module_class = VideoModule - template_dir_name = "video" def __init__(self, *args, **kwargs): super(VideoDescriptor, self).__init__(*args, **kwargs) diff --git a/common/lib/xmodule/xmodule/videoalpha_module.py b/common/lib/xmodule/xmodule/videoalpha_module.py index 33945c33fc..d8ed8949f1 100644 --- a/common/lib/xmodule/xmodule/videoalpha_module.py +++ b/common/lib/xmodule/xmodule/videoalpha_module.py @@ -179,4 +179,3 @@ class VideoAlphaModule(VideoAlphaFields, XModule): class VideoAlphaDescriptor(VideoAlphaFields, RawDescriptor): """Descriptor for `VideoAlphaModule`.""" module_class = VideoAlphaModule - template_dir_name = "videoalpha" diff --git a/common/lib/xmodule/xmodule/xml_module.py b/common/lib/xmodule/xmodule/xml_module.py index 0528bbfb6c..882e308c77 100644 --- a/common/lib/xmodule/xmodule/xml_module.py +++ b/common/lib/xmodule/xmodule/xml_module.py @@ -356,6 +356,7 @@ class XmlDescriptor(XModuleDescriptor): if key not in set(f.name for f in cls.fields + cls.lms.fields): model_data['xml_attributes'][key] = value model_data['location'] = location + model_data['category'] = xml_object.tag return cls( system, diff --git a/lms/djangoapps/courseware/features/common.py b/lms/djangoapps/courseware/features/common.py index 0aa079ebac..55e82e0e90 100644 --- a/lms/djangoapps/courseware/features/common.py +++ b/lms/djangoapps/courseware/features/common.py @@ -10,7 +10,6 @@ from django.contrib.auth.models import User from student.models import CourseEnrollment from xmodule.modulestore import Location from xmodule.modulestore.django import _MODULESTORES, modulestore -from xmodule.templates import update_templates from xmodule.course_module import CourseDescriptor from courseware.courses import get_course_by_id from xmodule import seq_module, vertical_module @@ -39,7 +38,7 @@ def create_course(step, course): display_name='Test Section') problem_section = world.ItemFactory.create(parent_location=world.scenario_dict['SECTION'].location, - template='i4x://edx/templates/sequential/Empty', + category='sequential' display_name='Test Section') @@ -62,7 +61,7 @@ def i_am_registered_for_the_course(step, course): @step(u'The course "([^"]*)" has extra tab "([^"]*)"$') def add_tab_to_course(step, course, extra_tab_name): section_item = world.ItemFactory.create(parent_location=course_location(course), - template="i4x://edx/templates/static_tab/Empty", + category="static_tab", display_name=str(extra_tab_name)) diff --git a/lms/djangoapps/courseware/features/navigation.py b/lms/djangoapps/courseware/features/navigation.py index 7c2474ae1a..c87e6122a4 100644 --- a/lms/djangoapps/courseware/features/navigation.py +++ b/lms/djangoapps/courseware/features/navigation.py @@ -24,11 +24,11 @@ def view_course_multiple_sections(step): display_name=section_name(2)) place1 = world.ItemFactory.create(parent_location=section1.location, - template='i4x://edx/templates/sequential/Empty', + category='sequential', display_name=subsection_name(1)) place2 = world.ItemFactory.create(parent_location=section2.location, - template='i4x://edx/templates/sequential/Empty', + category='sequential', display_name=subsection_name(2)) add_problem_to_course_section('model_course', 'multiple choice', place1.location) @@ -46,7 +46,7 @@ def view_course_multiple_subsections(step): display_name=section_name(1)) place1 = world.ItemFactory.create(parent_location=section1.location, - template='i4x://edx/templates/sequential/Empty', + category='sequential', display_name=subsection_name(1)) place2 = world.ItemFactory.create(parent_location=section1.location, @@ -66,7 +66,7 @@ def view_course_multiple_sequences(step): display_name=section_name(1)) place1 = world.ItemFactory.create(parent_location=section1.location, - template='i4x://edx/templates/sequential/Empty', + category='sequential', display_name=subsection_name(1)) add_problem_to_course_section('model_course', 'multiple choice', place1.location) @@ -177,9 +177,8 @@ def add_problem_to_course_section(course, problem_type, parent_location, extraMe # Create a problem item using our generated XML # We set rerandomize=always in the metadata so that the "Reset" button # will appear. - template_name = "i4x://edx/templates/problem/Blank_Common_Problem" world.ItemFactory.create(parent_location=parent_location, - template=template_name, + category='problem', display_name=str(problem_type), data=problem_xml, metadata=metadata) diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py index 82bb4959a8..e0c3c004da 100644 --- a/lms/djangoapps/courseware/features/problems.py +++ b/lms/djangoapps/courseware/features/problems.py @@ -17,7 +17,7 @@ def view_problem_with_attempts(step, problem_type, attempts): i_am_registered_for_the_course(step, 'model_course') # Ensure that the course has this problem type - add_problem_to_course(world.scenario_dict['COURSE'].number, problem_type, {'attempts': attempts}) + add_problem_to_course(world.scenario_dict['COURSE'].number, problem_type, {'max_attempts': attempts}) # Go to the one section in the factory-created course # which should be loaded with the correct problem diff --git a/lms/djangoapps/courseware/features/problems_setup.py b/lms/djangoapps/courseware/features/problems_setup.py index 1805da55d0..6086d7fa5e 100644 --- a/lms/djangoapps/courseware/features/problems_setup.py +++ b/lms/djangoapps/courseware/features/problems_setup.py @@ -273,9 +273,9 @@ def add_problem_to_course(course, problem_type, extraMeta=None): # Create a problem item using our generated XML # We set rerandomize=always in the metadata so that the "Reset" button # will appear. - template_name = "i4x://edx/templates/problem/Blank_Common_Problem" - world.ItemFactory.create(parent_location=section_location(course), - template=template_name, + category_name = "problem" + return world.ItemFactory.create(parent_location=section_location(course), + category=category_name, display_name=str(problem_type), data=problem_xml, metadata=metadata) diff --git a/lms/djangoapps/courseware/features/video.py b/lms/djangoapps/courseware/features/video.py index 6b05af51b5..f95ffd9917 100644 --- a/lms/djangoapps/courseware/features/video.py +++ b/lms/djangoapps/courseware/features/video.py @@ -43,14 +43,13 @@ def view_videoalpha(step): def add_video_to_course(course): - template_name = 'i4x://edx/templates/video/default' world.ItemFactory.create(parent_location=section_location(course), - template=template_name, + category='video', display_name='Video') def add_videoalpha_to_course(course): - template_name = 'i4x://edx/templates/videoalpha/Video_Alpha' + category = 'videoalpha' world.ItemFactory.create(parent_location=section_location(course), - template=template_name, + category=category, display_name='Video Alpha') diff --git a/lms/djangoapps/courseware/tests/__init__.py b/lms/djangoapps/courseware/tests/__init__.py index 0abbaa02cf..31fe376d69 100644 --- a/lms/djangoapps/courseware/tests/__init__.py +++ b/lms/djangoapps/courseware/tests/__init__.py @@ -29,17 +29,17 @@ class BaseTestXmodule(ModuleStoreTestCase): 2. create, enrol and login users for this course; Any xmodule should overwrite only next parameters for test: - 1. TEMPLATE_NAME + 1. CATEGORY 2. DATA 3. MODEL_DATA - This class should not contain any tests, because TEMPLATE_NAME + This class should not contain any tests, because CATEGORY should be defined in child class. """ USER_COUNT = 2 # Data from YAML common/lib/xmodule/xmodule/templates/NAME/default.yaml - TEMPLATE_NAME = "" + CATEGORY = "" DATA = '' MODEL_DATA = {'data': ''} @@ -53,11 +53,11 @@ class BaseTestXmodule(ModuleStoreTestCase): chapter = ItemFactory.create( parent_location=self.course.location, - template="i4x://edx/templates/sequential/Empty", + category="sequential", ) section = ItemFactory.create( parent_location=chapter.location, - template="i4x://edx/templates/sequential/Empty" + category="sequential" ) # username = robot{0}, password = 'test' @@ -71,7 +71,7 @@ class BaseTestXmodule(ModuleStoreTestCase): self.item_descriptor = ItemFactory.create( parent_location=section.location, - template=self.TEMPLATE_NAME, + category=self.CATEGORY, data=self.DATA ) diff --git a/lms/djangoapps/courseware/tests/test_submitting_problems.py b/lms/djangoapps/courseware/tests/test_submitting_problems.py index 83ae7dc73e..7e9b55a4fb 100644 --- a/lms/djangoapps/courseware/tests/test_submitting_problems.py +++ b/lms/djangoapps/courseware/tests/test_submitting_problems.py @@ -130,7 +130,7 @@ class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase): problem = ItemFactory.create( parent_location=section_location, - template=problem_template, + category='problem', data=prob_xml, metadata={'randomize': 'always'}, display_name=name @@ -149,13 +149,13 @@ class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase): if not(hasattr(self, 'chapter')): self.chapter = ItemFactory.create( parent_location=self.course.location, - template="i4x://edx/templates/chapter/Empty", + category='chapter' ) section = ItemFactory.create( parent_location=self.chapter.location, display_name=name, - template="i4x://edx/templates/sequential/Empty", + category='sequential', metadata={'graded': True, 'format': section_format} ) diff --git a/lms/djangoapps/courseware/tests/test_video_mongo.py b/lms/djangoapps/courseware/tests/test_video_mongo.py index a0fdecc77a..829308423c 100644 --- a/lms/djangoapps/courseware/tests/test_video_mongo.py +++ b/lms/djangoapps/courseware/tests/test_video_mongo.py @@ -7,7 +7,7 @@ from . import BaseTestXmodule class TestVideo(BaseTestXmodule): """Integration tests: web client + mongo.""" - TEMPLATE_NAME = "i4x://edx/templates/video/default" + TEMPLATE_NAME = "video" DATA = '