diff --git a/cms/djangoapps/contentstore/tests/__init__.py b/cms/djangoapps/contentstore/tests/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/cms/djangoapps/contentstore/tests/__init__.py @@ -0,0 +1 @@ + diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py new file mode 100644 index 0000000000..0542d4cf03 --- /dev/null +++ b/cms/djangoapps/contentstore/tests/tests.py @@ -0,0 +1,168 @@ +import json +from django.test.client import Client +from django.test import TestCase +from mock import patch, Mock +from override_settings import override_settings +from django.conf import settings +from django.core.urlresolvers import reverse + +from student.models import Registration +from django.contrib.auth.models import User + + +def parse_json(response): + """Parse response, which is assumed to be json""" + return json.loads(response.content) + + +def user(email): + '''look up a user by email''' + return User.objects.get(email=email) + +def registration(email): + '''look up registration object by email''' + return Registration.objects.get(user__email=email) + +class AuthTestCase(TestCase): + """Check that various permissions-related things work""" + + def setUp(self): + self.email = 'a@b.com' + self.pw = 'xyz' + self.username = 'testuser' + + def check_page_get(self, url, expected): + resp = self.client.get(url) + self.assertEqual(resp.status_code, expected) + return resp + + def test_public_pages_load(self): + """Make sure pages that don't require login load without error.""" + pages = ( + reverse('login'), + reverse('signup'), + ) + for page in pages: + print "Checking '{0}'".format(page) + self.check_page_get(page, 200) + + def test_create_account_errors(self): + # No post data -- should fail + resp = self.client.post('/create_account', {}) + self.assertEqual(resp.status_code, 200) + data = parse_json(resp) + self.assertEqual(data['success'], False) + + def _create_account(self, username, email, pw): + '''Try to create an account. No error checking''' + resp = self.client.post('/create_account', { + 'username': username, + 'email': email, + 'password': pw, + 'location' : 'home', + 'language' : 'Franglish', + 'name' : 'Fred Weasley', + 'terms_of_service' : 'true', + 'honor_code' : 'true'}) + return resp + + def create_account(self, username, email, pw): + '''Create the account and check that it worked''' + resp = self._create_account(username, email, pw) + 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(user(self.email).is_active) + + return resp + + def _activate_user(self, email): + '''look up the user's activation key in the db, then hit the activate view. + No error checking''' + activation_key = registration(email).activation_key + + # and now we try to activate + resp = self.client.get(reverse('activate', kwargs={'key': activation_key})) + 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(user(self.email).is_active) + + def test_create_account(self): + self.create_account(self.username, self.email, self.pw) + self.activate_user(self.email) + + + def _login(self, email, pw): + '''Login. View should always return 200. The success/fail is in the + returned json''' + resp = self.client.post(reverse('login_post'), + {'email': email, 'password': pw}) + self.assertEqual(resp.status_code, 200) + return resp + + + def login(self, email, pw): + '''Login, check that it worked.''' + resp = self._login(self.email, self.pw) + data = parse_json(resp) + self.assertTrue(data['success']) + return resp + + def test_login(self): + self.create_account(self.username, self.email, self.pw) + + # Not activated yet. Login should fail. + resp = self._login(self.email, self.pw) + data = parse_json(resp) + self.assertFalse(data['success']) + + self.activate_user(self.email) + + # Now login should work + self.login(self.email, self.pw) + + def test_private_pages_auth(self): + """Make sure pages that do require login work.""" + auth_pages = ( + reverse('index'), + reverse('edit_item'), + reverse('save_item'), + ) + + # These are pages that should just load when the user is logged in + # (no data needed) + simple_auth_pages = ( + reverse('index'), + ) + + # need an activated user + self.test_create_account() + + # Not logged in. Should redirect to login. + print 'Not logged in' + for page in auth_pages: + print "Checking '{0}'".format(page) + self.check_page_get(page, expected=302) + + # Logged in should work. + self.login(self.email, self.pw) + + print 'Logged in' + for page in simple_auth_pages: + print "Checking '{0}'".format(page) + self.check_page_get(page, expected=200) + + + def test_index_auth(self): + + # not logged in. Should return a redirect. + resp = self.client.get(reverse('index')) + self.assertEqual(resp.status_code, 302) + + # Logged in should work. diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 46d7c24d2d..4013af2e89 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -2,16 +2,39 @@ from util.json_request import expect_json import json from django.http import HttpResponse +from django.contrib.auth.decorators import login_required +from django.core.context_processors import csrf from django_future.csrf import ensure_csrf_cookie -from fs.osfs import OSFS from django.core.urlresolvers import reverse +from fs.osfs import OSFS + from xmodule.modulestore import Location from github_sync import export_to_github from mitxmako.shortcuts import render_to_response from xmodule.modulestore.django import modulestore +# ==== Public views ================================================== +@ensure_csrf_cookie +def signup(request): + """ + Display the signup form. + """ + csrf_token = csrf(request)['csrf_token'] + return render_to_response('signup.html', {'csrf': csrf_token }) + +@ensure_csrf_cookie +def login_page(request): + """ + Display the login form. + """ + csrf_token = csrf(request)['csrf_token'] + return render_to_response('login.html', {'csrf': csrf_token }) + +# ==== Views for any logged-in user ================================== + +@login_required @ensure_csrf_cookie def index(request): courses = modulestore().get_items(['i4x', None, None, 'course', None]) @@ -24,18 +47,34 @@ def index(request): for course in courses] }) +# ==== Views with per-item permissions================================ +def has_access(user, location): + '''Return True if user allowed to access this piece of data''' + # TODO (vshnayder): actually check perms + return user.is_active and user.is_authenticated + +@login_required @ensure_csrf_cookie def course_index(request, org, course, name): + location = ['i4x', org, course, 'course', name] + if not has_access(request.user, location): + raise Http404 # TODO (vshnayder): better error + # TODO (cpennington): These need to be read in from the active user - course = modulestore().get_item(['i4x', org, course, 'course', name]) + course = modulestore().get_item(location) weeks = course.get_children() return render_to_response('course_index.html', {'weeks': weeks}) - +@login_required def edit_item(request): - item_id = request.GET['id'] - item = modulestore().get_item(item_id) + # TODO (vshnayder): change name from id to location in coffee+html as well. + item_location = request.GET['id'] + print item_location, request.GET + if not has_access(request.user, item_location): + raise Http404 # TODO (vshnayder): better error + + item = modulestore().get_item(item_location) return render_to_response('unit.html', { 'contents': item.get_html(), 'js_module': item.js_module_name(), @@ -44,18 +83,39 @@ def edit_item(request): }) +def user_author_string(user): + '''Get an author string for commits by this user. Format: + first last . + + If the first and last names are blank, uses the username instead. + Assumes that the email is not blank. + ''' + f = user.first_name + l = user.last_name + if f == '' and l == '': + f = user.username + return '{first} {last} <{email}>'.format(first=f, + last=l, + email=user.email) + +@login_required @expect_json def save_item(request): - item_id = request.POST['id'] + item_location = request.POST['id'] + if not has_access(request.user, item_location): + raise Http404 # TODO (vshnayder): better error + data = json.loads(request.POST['data']) - modulestore().update_item(item_id, data) + modulestore().update_item(item_location, data) # Export the course back to github # This uses wildcarding to find the course, which requires handling # multiple courses returned, but there should only ever be one - course_location = Location(item_id)._replace(category='course', name=None) + course_location = Location(item_location)._replace( + category='course', name=None) courses = modulestore().get_items(course_location, depth=None) for course in courses: - export_to_github(course, "CMS Edit") + author_string = user_author_string(request.user) + export_to_github(course, "CMS Edit", author_string) return HttpResponse(json.dumps({})) diff --git a/cms/djangoapps/github_sync/__init__.py b/cms/djangoapps/github_sync/__init__.py index b8d9dbd683..9f490119af 100644 --- a/cms/djangoapps/github_sync/__init__.py +++ b/cms/djangoapps/github_sync/__init__.py @@ -38,7 +38,12 @@ def import_from_github(repo_settings): return git_repo.head.commit.hexsha, module_store.courses[course_dir] -def export_to_github(course, commit_message): +def export_to_github(course, commit_message, author_str=None): + ''' + Commit any changes to the specified course with given commit message, + and push to github (if MITX_FEATURES['GITHUB_PUSH'] is True). + If author_str is specified, uses it in the commit. + ''' repo_path = settings.DATA_DIR / course.metadata.get('course_dir', course.location.course) fs = OSFS(repo_path) xml = course.export_to_xml(fs) @@ -49,8 +54,11 @@ def export_to_github(course, commit_message): git_repo = Repo(repo_path) if git_repo.is_dirty(): git_repo.git.add(A=True) - git_repo.git.commit(m=commit_message) - + if author_str is not None: + git_repo.git.commit(m=commit_message, author=author_str) + else: + git_repo.git.commit(m=commit_message) + origin = git_repo.remotes.origin if settings.MITX_FEATURES['GITHUB_PUSH']: push_infos = origin.push() diff --git a/cms/djangoapps/github_sync/tests/__init__.py b/cms/djangoapps/github_sync/tests/__init__.py index b644328dd2..452904ffff 100644 --- a/cms/djangoapps/github_sync/tests/__init__.py +++ b/cms/djangoapps/github_sync/tests/__init__.py @@ -1,6 +1,7 @@ from django.test import TestCase from path import path import shutil +import os from github_sync import import_from_github, export_to_github from git import Repo from django.conf import settings @@ -13,10 +14,18 @@ from github_sync.exceptions import GithubSyncError @override_settings(DATA_DIR=path('test_root')) class GithubSyncTestCase(TestCase): + def cleanup(self): + shutil.rmtree(self.repo_dir, ignore_errors=True) + shutil.rmtree(self.remote_dir, ignore_errors=True) + def setUp(self): self.working_dir = path(settings.TEST_ROOT) self.repo_dir = self.working_dir / 'local_repo' self.remote_dir = self.working_dir / 'remote_repo' + + # make sure there's no stale data lying around + self.cleanup() + shutil.copytree('common/test/data/toy', self.remote_dir) remote = Repo.init(self.remote_dir) @@ -33,8 +42,7 @@ class GithubSyncTestCase(TestCase): }) def tearDown(self): - shutil.rmtree(self.repo_dir) - shutil.rmtree(self.remote_dir) + self.cleanup() def test_initialize_repo(self): """ diff --git a/cms/djangoapps/github_sync/views.py b/cms/djangoapps/github_sync/views.py index 8bf654f430..e4cae6cad8 100644 --- a/cms/djangoapps/github_sync/views.py +++ b/cms/djangoapps/github_sync/views.py @@ -49,4 +49,4 @@ def github_post_receive(request): revision, course = import_from_github(repo) export_to_github(course, repo['path'], "Changes from cms import of revision %s" % revision) - return HttpResponse('Push recieved') + return HttpResponse('Push received') diff --git a/cms/envs/common.py b/cms/envs/common.py index 8d3e2672da..0196ddc9e5 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -34,6 +34,10 @@ MITX_FEATURES = { 'GITHUB_PUSH': False, } +# needed to use lms student app +GENERATE_RANDOM_USER_CREDENTIALS = False + + ############################# SET PATH INFORMATION ############################# PROJECT_ROOT = path(__file__).abspath().dirname().dirname() # /mitx/cms REPO_ROOT = PROJECT_ROOT.dirname() @@ -66,6 +70,10 @@ TEMPLATE_DIRS = ( MITX_ROOT_URL = '' +LOGIN_REDIRECT_URL = MITX_ROOT_URL + '/login' +LOGIN_URL = MITX_ROOT_URL + '/login' + + TEMPLATE_CONTEXT_PROCESSORS = ( 'django.core.context_processors.request', 'django.core.context_processors.static', @@ -97,7 +105,7 @@ MIDDLEWARE_CLASSES = ( 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', - # Instead of AuthenticationMiddleware, we use a cached backed version + # Instead of AuthenticationMiddleware, we use a cache-backed version 'cache_toolbox.middleware.CacheBackedAuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', @@ -214,7 +222,7 @@ PIPELINE_COMPILERS = [ PIPELINE_SASS_ARGUMENTS = '-t compressed -r {proj_dir}/static/sass/bourbon/lib/bourbon.rb'.format(proj_dir=PROJECT_ROOT) PIPELINE_CSS_COMPRESSOR = None -PIPELINE_JS_COMPRESSOR = 'pipeline.compressors.yui.YUICompressor' +PIPELINE_JS_COMPRESSOR = None STATICFILES_IGNORE_PATTERNS = ( "sass/*", @@ -239,9 +247,11 @@ INSTALLED_APPS = ( 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.messages', + 'south', # For CMS 'contentstore', + 'student', # misleading name due to sharing with lms # For asset pipelining 'pipeline', diff --git a/cms/envs/dev.py b/cms/envs/dev.py index 465d542c5b..40139a8a22 100644 --- a/cms/envs/dev.py +++ b/cms/envs/dev.py @@ -25,43 +25,41 @@ MODULESTORE = { DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': ENV_ROOT / "db" / "mitx.db", + 'NAME': ENV_ROOT / "db" / "cms.db", } } -REPO_ROOT = ENV_ROOT / "content" - REPOS = { 'edx4edx': { - 'path': REPO_ROOT / "edx4edx", + 'path': DATA_DIR / "edx4edx", 'org': 'edx', 'course': 'edx4edx', 'branch': 'for_cms', 'origin': 'git@github.com:MITx/edx4edx.git', }, '6002x-fall-2012': { - 'path': REPO_ROOT / '6002x-fall-2012', + 'path': DATA_DIR / '6002x-fall-2012', 'org': 'mit.edu', 'course': '6.002x', 'branch': 'for_cms', 'origin': 'git@github.com:MITx/6002x-fall-2012.git', }, '6.00x': { - 'path': REPO_ROOT / '6.00x', + 'path': DATA_DIR / '6.00x', 'org': 'mit.edu', 'course': '6.00x', 'branch': 'for_cms', 'origin': 'git@github.com:MITx/6.00x.git', }, '7.00x': { - 'path': REPO_ROOT / '7.00x', + 'path': DATA_DIR / '7.00x', 'org': 'mit.edu', 'course': '7.00x', 'branch': 'for_cms', 'origin': 'git@github.com:MITx/7.00x.git', }, '3.091x': { - 'path': REPO_ROOT / '3.091x', + 'path': DATA_DIR / '3.091x', 'org': 'mit.edu', 'course': '3.091x', 'branch': 'for_cms', diff --git a/cms/envs/test.py b/cms/envs/test.py index 927e2af987..2a867af91f 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -9,6 +9,8 @@ sessions. Assumes structure: """ from .common import * import os +from path import path + # Nose Test Runner INSTALLED_APPS += ('django_nose',) @@ -17,7 +19,11 @@ for app in os.listdir(PROJECT_ROOT / 'djangoapps'): NOSE_ARGS += ['--cover-package', app] TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' -TEST_ROOT = 'test_root' +TEST_ROOT = path('test_root') + +# Want static files in the same dir for running on jenkins. +STATIC_ROOT = TEST_ROOT / "staticfiles" + MODULESTORE = { 'default': { @@ -34,7 +40,7 @@ MODULESTORE = { DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': ENV_ROOT / "db" / "mitx.db", + 'NAME': ENV_ROOT / "db" / "cms.db", } } diff --git a/cms/static/js/main.js b/cms/static/js/main.js index 2d72edc4bf..4a9b5d2374 100644 --- a/cms/static/js/main.js +++ b/cms/static/js/main.js @@ -80,6 +80,6 @@ $(document).ready(function(){ $('section.problem-edit').show(); return false; }); - + }); diff --git a/cms/static/sass/_content-types.scss b/cms/static/sass/_content-types.scss index 6df113df78..587646fb39 100644 --- a/cms/static/sass/_content-types.scss +++ b/cms/static/sass/_content-types.scss @@ -6,50 +6,50 @@ .videosequence a:first-child { @extend .content-type; - background-image: url('/static/img/content-types/videosequence.png'); + background-image: url('../img/content-types/videosequence.png'); } .video a:first-child { @extend .content-type; - background-image: url('/static/img/content-types/video.png'); + background-image: url('../img/content-types/video.png'); } .problemset a:first-child { @extend .content-type; - background-image: url('/static/img/content-types/problemset.png'); + background-image: url('../img/content-types/problemset.png'); } .problem a:first-child { @extend .content-type; - background-image: url('/static/img/content-types/problem.png'); + background-image: url('../img/content-types/problem.png'); } .lab a:first-child { @extend .content-type; - background-image: url('/static/img/content-types/lab.png'); + background-image: url('../img/content-types/lab.png'); } .tab a:first-child { @extend .content-type; - background-image: url('/static/img/content-types/lab.png'); + background-image: url('../img/content-types/lab.png'); } .html a:first-child { @extend .content-type; - background-image: url('/static/img/content-types/html.png'); + background-image: url('../img/content-types/html.png'); } .vertical a:first-child { @extend .content-type; - background-image: url('/static/img/content-types/vertical.png'); + background-image: url('../img/content-types/vertical.png'); } .sequential a:first-child { @extend .content-type; - background-image: url('/static/img/content-types/sequential.png'); + background-image: url('../img/content-types/sequential.png'); } .chapter a:first-child { @extend .content-type; - background-image: url('/static/img/content-types/chapter.png'); + background-image: url('../img/content-types/chapter.png'); } diff --git a/cms/templates/activation_active.html b/cms/templates/activation_active.html new file mode 100644 index 0000000000..07d3a37969 --- /dev/null +++ b/cms/templates/activation_active.html @@ -0,0 +1,14 @@ +<%inherit file="base.html" /> + +<%block name="content"> + +
+
+ +
+

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 new file mode 100644 index 0000000000..5d9437ccb3 --- /dev/null +++ b/cms/templates/activation_complete.html @@ -0,0 +1,12 @@ +<%inherit file="base.html" /> + +<%block name="content"> + +
+
+

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 new file mode 100644 index 0000000000..c4eb16875b --- /dev/null +++ b/cms/templates/activation_invalid.html @@ -0,0 +1,16 @@ +<%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.

+
+
+ \ No newline at end of file diff --git a/cms/templates/base.html b/cms/templates/base.html index dbae876eca..935917b11a 100644 --- a/cms/templates/base.html +++ b/cms/templates/base.html @@ -21,14 +21,12 @@ <%include file="widgets/header.html"/> - <%block name="content"> - - + % if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']: <%static:js group='main'/> % else: @@ -40,6 +38,9 @@ + + <%block name="content"> + diff --git a/cms/templates/emails/activation_email.txt b/cms/templates/emails/activation_email.txt new file mode 100644 index 0000000000..eca5effdd6 --- /dev/null +++ b/cms/templates/emails/activation_email.txt @@ -0,0 +1,14 @@ +Someone, hopefully you, signed up for an account for edX's on-line +offering of "${ course_title}" using this email address. If it was +you, and you'd like to activate and use your account, copy and paste +this address into your web browser's address bar: + +% if is_secure: + https://${ site }/activate/${ key } +% else: + http://edx4edx.mitx.mit.edu/activate/${ key } +% endif + +If you didn't request this, you don't need to do anything; you won't +receive any more email from us. Please do not reply to this e-mail; if +you require assistance, check the help section of the edX web site. diff --git a/cms/templates/emails/activation_email_subject.txt b/cms/templates/emails/activation_email_subject.txt new file mode 100644 index 0000000000..c25c006a81 --- /dev/null +++ b/cms/templates/emails/activation_email_subject.txt @@ -0,0 +1 @@ +Your account for edX's on-line ${course_title} course diff --git a/cms/templates/login.html b/cms/templates/login.html index 03ea5f967c..aa493a5c8a 100644 --- a/cms/templates/login.html +++ b/cms/templates/login.html @@ -1,11 +1,76 @@ -
- +<%inherit file="base.html" /> +<%! from django.core.urlresolvers import reverse %> +<%block name="title">Log in - % if next is not None: - - % endif +<%block name="content"> - Username: - Possword: - -
+
+ +
+
+

Log in

+
+
+ +
+ + + + + +
+ +
+
+ + + + +
+ +
+ + + + diff --git a/cms/templates/registration/reg_complete.html b/cms/templates/registration/reg_complete.html new file mode 100644 index 0000000000..256146f3b4 --- /dev/null +++ b/cms/templates/registration/reg_complete.html @@ -0,0 +1,3 @@ +

Check your email

+

An activation link has been sent to ${ email }, along with +instructions for activating your account.

diff --git a/cms/templates/signup.html b/cms/templates/signup.html new file mode 100644 index 0000000000..d3eedc8070 --- /dev/null +++ b/cms/templates/signup.html @@ -0,0 +1,88 @@ +<%inherit file="base.html" /> +<%block name="title">Sign up + +<%block name="content"> +
+ +
+
+

Sign Up for edX

+
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + +
+ +
+
+ + + +
+ + + +
+ +
+ diff --git a/cms/templates/widgets/header.html b/cms/templates/widgets/header.html index bad314560b..c1c05671fa 100644 --- a/cms/templates/widgets/header.html +++ b/cms/templates/widgets/header.html @@ -1,6 +1,7 @@ +<%! from django.core.urlresolvers import reverse %>
diff --git a/cms/urls.py b/cms/urls.py index eb925a7069..b3dc9a48e9 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -1,6 +1,8 @@ from django.conf import settings from django.conf.urls.defaults import patterns, include, url +import django.contrib.auth.views + # Uncomment the next two lines to enable the admin: # from django.contrib import admin # admin.autodiscover() @@ -9,10 +11,27 @@ urlpatterns = ('', url(r'^$', 'contentstore.views.index', name='index'), url(r'^edit_item$', 'contentstore.views.edit_item', name='edit_item'), url(r'^save_item$', 'contentstore.views.save_item', name='save_item'), - url(r'^(?P[^/]+)/(?P[^/]+)/course/(?P[^/]+)$', 'contentstore.views.course_index', name='course_index'), + url(r'^(?P[^/]+)/(?P[^/]+)/course/(?P[^/]+)$', + 'contentstore.views.course_index', name='course_index'), url(r'^github_service_hook$', 'github_sync.views.github_post_receive'), ) +# User creation and updating views +urlpatterns += ( + url(r'^signup$', 'contentstore.views.signup', name='signup'), + + url(r'^create_account$', 'student.views.create_account'), + url(r'^activate/(?P[^/]*)$', 'student.views.activate_account', name='activate'), + + # form page + url(r'^login$', 'contentstore.views.login_page', name='login'), + # ajax view that actually does the work + url(r'^login_post$', 'student.views.login_user', name='login_post'), + + url(r'^logout$', 'student.views.logout_user', name='logout'), + + ) + if settings.DEBUG: ## Jasmine urlpatterns=urlpatterns + (url(r'^_jasmine/', include('django_jasmine.urls')),) diff --git a/lms/djangoapps/student/__init__.py b/common/djangoapps/student/__init__.py similarity index 100% rename from lms/djangoapps/student/__init__.py rename to common/djangoapps/student/__init__.py diff --git a/lms/djangoapps/student/admin.py b/common/djangoapps/student/admin.py similarity index 100% rename from lms/djangoapps/student/admin.py rename to common/djangoapps/student/admin.py diff --git a/lms/djangoapps/student/management/__init__.py b/common/djangoapps/student/management/__init__.py similarity index 100% rename from lms/djangoapps/student/management/__init__.py rename to common/djangoapps/student/management/__init__.py diff --git a/lms/djangoapps/student/management/commands/__init__.py b/common/djangoapps/student/management/commands/__init__.py similarity index 100% rename from lms/djangoapps/student/management/commands/__init__.py rename to common/djangoapps/student/management/commands/__init__.py diff --git a/lms/djangoapps/student/management/commands/assigngroups.py b/common/djangoapps/student/management/commands/assigngroups.py similarity index 100% rename from lms/djangoapps/student/management/commands/assigngroups.py rename to common/djangoapps/student/management/commands/assigngroups.py diff --git a/lms/djangoapps/student/management/commands/emaillist.py b/common/djangoapps/student/management/commands/emaillist.py similarity index 100% rename from lms/djangoapps/student/management/commands/emaillist.py rename to common/djangoapps/student/management/commands/emaillist.py diff --git a/lms/djangoapps/student/management/commands/massemail.py b/common/djangoapps/student/management/commands/massemail.py similarity index 100% rename from lms/djangoapps/student/management/commands/massemail.py rename to common/djangoapps/student/management/commands/massemail.py diff --git a/lms/djangoapps/student/management/commands/massemailtxt.py b/common/djangoapps/student/management/commands/massemailtxt.py similarity index 100% rename from lms/djangoapps/student/management/commands/massemailtxt.py rename to common/djangoapps/student/management/commands/massemailtxt.py diff --git a/lms/djangoapps/student/management/commands/userinfo.py b/common/djangoapps/student/management/commands/userinfo.py similarity index 100% rename from lms/djangoapps/student/management/commands/userinfo.py rename to common/djangoapps/student/management/commands/userinfo.py diff --git a/lms/djangoapps/student/migrations/0001_initial.py b/common/djangoapps/student/migrations/0001_initial.py similarity index 100% rename from lms/djangoapps/student/migrations/0001_initial.py rename to common/djangoapps/student/migrations/0001_initial.py diff --git a/lms/djangoapps/student/migrations/0002_text_to_varchar_and_indexes.py b/common/djangoapps/student/migrations/0002_text_to_varchar_and_indexes.py similarity index 100% rename from lms/djangoapps/student/migrations/0002_text_to_varchar_and_indexes.py rename to common/djangoapps/student/migrations/0002_text_to_varchar_and_indexes.py diff --git a/lms/djangoapps/student/migrations/0003_auto__add_usertestgroup.py b/common/djangoapps/student/migrations/0003_auto__add_usertestgroup.py similarity index 100% rename from lms/djangoapps/student/migrations/0003_auto__add_usertestgroup.py rename to common/djangoapps/student/migrations/0003_auto__add_usertestgroup.py diff --git a/lms/djangoapps/student/migrations/0004_add_email_index.py b/common/djangoapps/student/migrations/0004_add_email_index.py similarity index 100% rename from lms/djangoapps/student/migrations/0004_add_email_index.py rename to common/djangoapps/student/migrations/0004_add_email_index.py diff --git a/lms/djangoapps/student/migrations/0005_name_change.py b/common/djangoapps/student/migrations/0005_name_change.py similarity index 100% rename from lms/djangoapps/student/migrations/0005_name_change.py rename to common/djangoapps/student/migrations/0005_name_change.py diff --git a/lms/djangoapps/student/migrations/0006_expand_meta_field.py b/common/djangoapps/student/migrations/0006_expand_meta_field.py similarity index 100% rename from lms/djangoapps/student/migrations/0006_expand_meta_field.py rename to common/djangoapps/student/migrations/0006_expand_meta_field.py diff --git a/lms/djangoapps/student/migrations/0007_convert_to_utf8.py b/common/djangoapps/student/migrations/0007_convert_to_utf8.py similarity index 100% rename from lms/djangoapps/student/migrations/0007_convert_to_utf8.py rename to common/djangoapps/student/migrations/0007_convert_to_utf8.py diff --git a/lms/djangoapps/student/migrations/0008__auto__add_courseregistration.py b/common/djangoapps/student/migrations/0008__auto__add_courseregistration.py similarity index 100% rename from lms/djangoapps/student/migrations/0008__auto__add_courseregistration.py rename to common/djangoapps/student/migrations/0008__auto__add_courseregistration.py diff --git a/lms/djangoapps/student/migrations/0009_auto__del_courseregistration__add_courseenrollment.py b/common/djangoapps/student/migrations/0009_auto__del_courseregistration__add_courseenrollment.py similarity index 100% rename from lms/djangoapps/student/migrations/0009_auto__del_courseregistration__add_courseenrollment.py rename to common/djangoapps/student/migrations/0009_auto__del_courseregistration__add_courseenrollment.py diff --git a/lms/djangoapps/student/migrations/0010_auto__chg_field_courseenrollment_course_id.py b/common/djangoapps/student/migrations/0010_auto__chg_field_courseenrollment_course_id.py similarity index 100% rename from lms/djangoapps/student/migrations/0010_auto__chg_field_courseenrollment_course_id.py rename to common/djangoapps/student/migrations/0010_auto__chg_field_courseenrollment_course_id.py diff --git a/lms/djangoapps/student/migrations/0011_auto__chg_field_courseenrollment_user__del_unique_courseenrollment_use.py b/common/djangoapps/student/migrations/0011_auto__chg_field_courseenrollment_user__del_unique_courseenrollment_use.py similarity index 100% rename from lms/djangoapps/student/migrations/0011_auto__chg_field_courseenrollment_user__del_unique_courseenrollment_use.py rename to common/djangoapps/student/migrations/0011_auto__chg_field_courseenrollment_user__del_unique_courseenrollment_use.py diff --git a/lms/djangoapps/student/migrations/0012_auto__add_field_userprofile_gender__add_field_userprofile_date_of_birt.py b/common/djangoapps/student/migrations/0012_auto__add_field_userprofile_gender__add_field_userprofile_date_of_birt.py similarity index 100% rename from lms/djangoapps/student/migrations/0012_auto__add_field_userprofile_gender__add_field_userprofile_date_of_birt.py rename to common/djangoapps/student/migrations/0012_auto__add_field_userprofile_gender__add_field_userprofile_date_of_birt.py diff --git a/lms/djangoapps/student/migrations/__init__.py b/common/djangoapps/student/migrations/__init__.py similarity index 100% rename from lms/djangoapps/student/migrations/__init__.py rename to common/djangoapps/student/migrations/__init__.py diff --git a/lms/djangoapps/student/models.py b/common/djangoapps/student/models.py similarity index 100% rename from lms/djangoapps/student/models.py rename to common/djangoapps/student/models.py diff --git a/cms/djangoapps/contentstore/tests.py b/common/djangoapps/student/tests.py similarity index 100% rename from cms/djangoapps/contentstore/tests.py rename to common/djangoapps/student/tests.py diff --git a/lms/djangoapps/student/views.py b/common/djangoapps/student/views.py similarity index 93% rename from lms/djangoapps/student/views.py rename to common/djangoapps/student/views.py index ae64549f30..a349acbbb7 100644 --- a/lms/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -143,15 +143,15 @@ def create_account(request, post_override=None): # Confirm we have a properly formed request for a in ['username', 'email', 'password', 'location', 'language', 'name']: if a not in post_vars: - js['value']="Error (401 {field}). E-mail us.".format(field=a) + js['value'] = "Error (401 {field}). E-mail us.".format(field=a) return HttpResponse(json.dumps(js)) - if post_vars['honor_code']!=u'true': + if post_vars.get('honor_code', 'false') != u'true': js['value']="To enroll, you must follow the honor code.".format(field=a) return HttpResponse(json.dumps(js)) - if post_vars['terms_of_service']!=u'true': + if post_vars.get('terms_of_service', 'false') != u'true': js['value']="You must accept the terms of service.".format(field=a) return HttpResponse(json.dumps(js)) @@ -161,7 +161,7 @@ def create_account(request, post_override=None): # this is a good idea # TODO: Check password is sane for a in ['username', 'email', 'name', 'password', 'terms_of_service', 'honor_code']: - if len(post_vars[a])<2: + if len(post_vars[a]) < 2: error_str = {'username' : 'Username of length 2 or greater', 'email' : 'Properly formatted e-mail', 'name' : 'Your legal name ', @@ -183,25 +183,23 @@ def create_account(request, post_override=None): js['value']="Username should only consist of A-Z and 0-9.".format(field=a) return HttpResponse(json.dumps(js)) - - - u=User(username=post_vars['username'], - email=post_vars['email'], - is_active=False) + u = User(username=post_vars['username'], + email=post_vars['email'], + is_active=False) u.set_password(post_vars['password']) - r=Registration() + r = Registration() # TODO: Rearrange so that if part of the process fails, the whole process fails. # Right now, we can have e.g. no registration e-mail sent out and a zombie account try: u.save() except IntegrityError: # Figure out the cause of the integrity error - if len(User.objects.filter(username=post_vars['username']))>0: - js['value']="An account with this username already exists." + if len(User.objects.filter(username=post_vars['username'])) > 0: + js['value'] = "An account with this username already exists." return HttpResponse(json.dumps(js)) - if len(User.objects.filter(email=post_vars['email']))>0: - js['value']="An account with this e-mail already exists." + if len(User.objects.filter(email=post_vars['email'])) > 0: + js['value'] = "An account with this e-mail already exists." return HttpResponse(json.dumps(js)) raise @@ -209,36 +207,37 @@ def create_account(request, post_override=None): r.register(u) up = UserProfile(user=u) - up.name=post_vars['name'] - up.language=post_vars['language'] - up.location=post_vars['location'] + up.name = post_vars['name'] + up.language = post_vars['language'] + up.location = post_vars['location'] up.save() - d={'name':post_vars['name'], - 'key':r.activation_key, - 'course_title' : settings.COURSE_TITLE, - } + # TODO (vshnayder): the LMS should probably allow signups without a particular course too + d = {'name': post_vars['name'], + 'key': r.activation_key, + 'course_title': getattr(settings, 'COURSE_TITLE', ''), + } - subject = render_to_string('emails/activation_email_subject.txt',d) + subject = render_to_string('emails/activation_email_subject.txt', d) # Email subject *must not* contain newlines subject = ''.join(subject.splitlines()) - message = render_to_string('emails/activation_email.txt',d) + message = render_to_string('emails/activation_email.txt', d) try: if settings.MITX_FEATURES.get('REROUTE_ACTIVATION_EMAIL'): dest_addr = settings.MITX_FEATURES['REROUTE_ACTIVATION_EMAIL'] - message = "Activation for %s (%s): %s\n" % (u,u.email,up.name) + '-'*80 + '\n\n' + message + message = "Activation for %s (%s): %s\n" % (u,u.email,up.name) + '-' * 80 + '\n\n' + message send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [dest_addr], fail_silently=False) elif not settings.GENERATE_RANDOM_USER_CREDENTIALS: - res=u.email_user(subject, message, settings.DEFAULT_FROM_EMAIL) + res = u.email_user(subject, message, settings.DEFAULT_FROM_EMAIL) except: log.exception(sys.exc_info()) - js['value']='Could not send activation e-mail.' + js['value'] = 'Could not send activation e-mail.' return HttpResponse(json.dumps(js)) - js={'success':True, - 'value':render_to_string('registration/reg_complete.html', {'email':post_vars['email'], - 'csrf':csrf(request)['csrf_token']})} + js={'success': True, + 'value': render_to_string('registration/reg_complete.html', {'email': post_vars['email'], + 'csrf': csrf(request)['csrf_token']})} return HttpResponse(json.dumps(js), mimetype="application/json") def create_random_account(create_account_function): diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index 0dc3f18671..4cffc48bea 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -180,6 +180,15 @@ class LoncapaProblem(object): return {'score': correct, 'total': self.get_max_score()} + def update_score(self, score_msg): + newcmap = CorrectMap() + for responder in self.responders.values(): + if hasattr(responder,'update_score'): # Is this the best way to implement 'update_score' for CodeResponse? + results = responder.update_score(score_msg) + newcmap.update(results) + self.correct_map = newcmap + return newcmap + def grade_answers(self, answers): ''' Grade student responses. Called by capa_module.check_problem. diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 9e8c1e97f5..b645a2faa7 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -18,6 +18,7 @@ import re import requests import traceback import abc +import time # specific library imports from calc import evaluator, UndefinedVariable @@ -693,6 +694,124 @@ class SymbolicResponse(CustomResponse): #----------------------------------------------------------------------------- +class CodeResponse(LoncapaResponse): + ''' + Grade student code using an external server + ''' + + response_tag = 'coderesponse' + allowed_inputfields = ['textline', 'textbox'] + + def setup_response(self): + xml = self.xml + self.url = xml.get('url') or "http://ec2-50-16-59-149.compute-1.amazonaws.com/xqueue/submit/" # FIXME -- hardcoded url + + answer = xml.find('answer') + if answer is not None: + answer_src = answer.get('src') + if answer_src is not None: + self.code = self.system.filesystem.open('src/'+answer_src).read() + else: + self.code = answer.text + else: # no stanza; get code from