Merge branch 'master' of github.com:MITx/mitx into jarv/prod-requirements
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -23,4 +23,5 @@ reports/
|
||||
*.egg-info
|
||||
Gemfile.lock
|
||||
.env/
|
||||
|
||||
lms/static/sass/*.css
|
||||
cms/static/sass/*.css
|
||||
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "askbot"]
|
||||
path = askbot
|
||||
url = git@github.com:MITx/askbot-devel.git
|
||||
@@ -33,7 +33,7 @@ load-plugins=
|
||||
# can either give multiple identifier separated by comma (,) or put this option
|
||||
# multiple time (only on the command line, not in the configuration file where
|
||||
# it should appear only once).
|
||||
#disable=
|
||||
disable=E1102,W0142
|
||||
|
||||
|
||||
[REPORTS]
|
||||
@@ -82,7 +82,7 @@ zope=no
|
||||
# List of members which are set dynamically and missed by pylint inference
|
||||
# system, and so shouldn't trigger E0201 when accessed. Python regular
|
||||
# expressions are accepted.
|
||||
generated-members=REQUEST,acl_users,aq_parent
|
||||
generated-members=REQUEST,acl_users,aq_parent,objects,DoesNotExist,can_read,can_write,get_url,size
|
||||
|
||||
|
||||
[BASIC]
|
||||
|
||||
2
Gemfile
2
Gemfile
@@ -1,5 +1,5 @@
|
||||
source :rubygems
|
||||
|
||||
ruby "1.9.3"
|
||||
gem 'rake'
|
||||
gem 'sass', '3.1.15'
|
||||
gem 'bourbon', '~> 1.3.6'
|
||||
|
||||
1
askbot
Submodule
1
askbot
Submodule
Submodule askbot added at 1c3381046c
@@ -1,34 +0,0 @@
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.xml import XMLModuleStore
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def import_from_xml(data_dir, course_dirs=None):
|
||||
"""
|
||||
Import the specified xml data_dir into the django defined modulestore,
|
||||
using org and course as the location org and course.
|
||||
"""
|
||||
module_store = XMLModuleStore(
|
||||
data_dir,
|
||||
default_class='xmodule.raw_module.RawDescriptor',
|
||||
eager=True,
|
||||
course_dirs=course_dirs
|
||||
)
|
||||
for module in module_store.modules.itervalues():
|
||||
|
||||
# TODO (cpennington): This forces import to overrite the same items.
|
||||
# This should in the future create new revisions of the items on import
|
||||
try:
|
||||
modulestore().create_item(module.location)
|
||||
except:
|
||||
log.exception('Item already exists at %s' % module.location.url())
|
||||
pass
|
||||
if 'data' in module.definition:
|
||||
modulestore().update_item(module.location, module.definition['data'])
|
||||
if 'children' in module.definition:
|
||||
modulestore().update_children(module.location, module.definition['children'])
|
||||
modulestore().update_metadata(module.location, dict(module.metadata))
|
||||
|
||||
return module_store
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
###
|
||||
### One-off script for importing courseware form XML format
|
||||
### Script for importing courseware from XML format
|
||||
###
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from contentstore import import_from_xml
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
|
||||
unnamed_modules = 0
|
||||
|
||||
@@ -21,4 +23,7 @@ class Command(BaseCommand):
|
||||
course_dirs = args[1:]
|
||||
else:
|
||||
course_dirs = None
|
||||
import_from_xml(data_dir, course_dirs)
|
||||
print "Importing. Data_dir={data}, course_dirs={courses}".format(
|
||||
data=data_dir,
|
||||
courses=course_dirs)
|
||||
import_from_xml(modulestore(), data_dir, course_dirs)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import json
|
||||
from django.test.client import Client
|
||||
from django.test import TestCase
|
||||
from django.test.client import Client
|
||||
from mock import patch, Mock
|
||||
from override_settings import override_settings
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from path import path
|
||||
|
||||
from student.models import Registration
|
||||
from django.contrib.auth.models import User
|
||||
from xmodule.modulestore.django import modulestore
|
||||
import xmodule.modulestore.django
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
import copy
|
||||
|
||||
|
||||
def parse_json(response):
|
||||
@@ -19,23 +25,84 @@ 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):
|
||||
|
||||
class ContentStoreTestCase(TestCase):
|
||||
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(email, pw)
|
||||
data = parse_json(resp)
|
||||
self.assertTrue(data['success'])
|
||||
return resp
|
||||
|
||||
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(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 = 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(email).is_active)
|
||||
|
||||
|
||||
class AuthTestCase(ContentStoreTestCase):
|
||||
"""Check that various permissions-related things work"""
|
||||
|
||||
def setUp(self):
|
||||
self.email = 'a@b.com'
|
||||
self.pw = 'xyz'
|
||||
self.username = 'testuser'
|
||||
self.client = Client()
|
||||
|
||||
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 = (
|
||||
@@ -53,67 +120,10 @@ class AuthTestCase(TestCase):
|
||||
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)
|
||||
|
||||
@@ -121,7 +131,7 @@ class AuthTestCase(TestCase):
|
||||
resp = self._login(self.email, self.pw)
|
||||
data = parse_json(resp)
|
||||
self.assertFalse(data['success'])
|
||||
|
||||
|
||||
self.activate_user(self.email)
|
||||
|
||||
# Now login should work
|
||||
@@ -144,6 +154,9 @@ class AuthTestCase(TestCase):
|
||||
# need an activated user
|
||||
self.test_create_account()
|
||||
|
||||
# Create a new session
|
||||
self.client = Client()
|
||||
|
||||
# Not logged in. Should redirect to login.
|
||||
print 'Not logged in'
|
||||
for page in auth_pages:
|
||||
@@ -157,7 +170,6 @@ class AuthTestCase(TestCase):
|
||||
for page in simple_auth_pages:
|
||||
print "Checking '{0}'".format(page)
|
||||
self.check_page_get(page, expected=200)
|
||||
|
||||
|
||||
def test_index_auth(self):
|
||||
|
||||
@@ -166,3 +178,34 @@ class AuthTestCase(TestCase):
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
|
||||
# Logged in should work.
|
||||
|
||||
TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
|
||||
TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data')
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MODULESTORE)
|
||||
class EditTestCase(ContentStoreTestCase):
|
||||
"""Check that editing functionality works on example courses"""
|
||||
|
||||
def setUp(self):
|
||||
email = 'edit@test.com'
|
||||
password = 'foo'
|
||||
self.create_account('edittest', email, password)
|
||||
self.activate_user(email)
|
||||
self.login(email, password)
|
||||
xmodule.modulestore.django._MODULESTORES = {}
|
||||
xmodule.modulestore.django.modulestore().collection.drop()
|
||||
|
||||
def check_edit_item(self, test_course_name):
|
||||
import_from_xml(modulestore(), 'common/test/data/', [test_course_name])
|
||||
|
||||
for descriptor in modulestore().get_items(Location(None, None, None, None, None)):
|
||||
print "Checking ", descriptor.location.url()
|
||||
print descriptor.__class__, descriptor.location
|
||||
resp = self.client.get(reverse('edit_item'), {'id': descriptor.location.url()})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
def test_edit_item_toy(self):
|
||||
self.check_edit_item('toy')
|
||||
|
||||
def test_edit_item_full(self):
|
||||
self.check_edit_item('full')
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
from util.json_request import expect_json
|
||||
import json
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
|
||||
from django.http import HttpResponse
|
||||
from django.http import HttpResponse, Http404
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.context_processors import csrf
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from django.core.urlresolvers import reverse
|
||||
from fs.osfs import OSFS
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.x_module import ModuleSystem
|
||||
from github_sync import export_to_github
|
||||
from static_replace import replace_urls
|
||||
|
||||
from mitxmako.shortcuts import render_to_response
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule_modifiers import replace_static_urls, wrap_xmodule
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from functools import partial
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ==== Public views ==================================================
|
||||
|
||||
@@ -22,7 +31,8 @@ def signup(request):
|
||||
Display the signup form.
|
||||
"""
|
||||
csrf_token = csrf(request)['csrf_token']
|
||||
return render_to_response('signup.html', {'csrf': csrf_token })
|
||||
return render_to_response('signup.html', {'csrf': csrf_token})
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def login_page(request):
|
||||
@@ -30,13 +40,17 @@ def login_page(request):
|
||||
Display the login form.
|
||||
"""
|
||||
csrf_token = csrf(request)['csrf_token']
|
||||
return render_to_response('login.html', {'csrf': 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):
|
||||
"""
|
||||
List all courses available to the logged in user
|
||||
"""
|
||||
courses = modulestore().get_items(['i4x', None, None, 'course', None])
|
||||
return render_to_response('index.html', {
|
||||
'courses': [(course.metadata['display_name'],
|
||||
@@ -47,6 +61,7 @@ def index(request):
|
||||
for course in courses]
|
||||
})
|
||||
|
||||
|
||||
# ==== Views with per-item permissions================================
|
||||
|
||||
def has_access(user, location):
|
||||
@@ -54,32 +69,47 @@ def has_access(user, location):
|
||||
# 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):
|
||||
"""
|
||||
Display an editable course overview.
|
||||
|
||||
org, course, name: Attributes of the Location for the item to edit
|
||||
"""
|
||||
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(location)
|
||||
weeks = course.get_children()
|
||||
return render_to_response('course_index.html', {'weeks': weeks})
|
||||
|
||||
|
||||
@login_required
|
||||
def edit_item(request):
|
||||
"""
|
||||
Display an editing page for the specified module.
|
||||
|
||||
Expects a GET request with the parameter 'id'.
|
||||
|
||||
id: A Location URL
|
||||
"""
|
||||
# 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)
|
||||
item.get_html = wrap_xmodule(item.get_html, item, "xmodule_edit.html")
|
||||
return render_to_response('unit.html', {
|
||||
'contents': item.get_html(),
|
||||
'js_module': item.js_module_name(),
|
||||
'js_module': item.js_module_name,
|
||||
'category': item.category,
|
||||
'name': item.name,
|
||||
'previews': get_module_previews(request, item),
|
||||
})
|
||||
|
||||
|
||||
@@ -98,6 +128,149 @@ def user_author_string(user):
|
||||
last=l,
|
||||
email=user.email)
|
||||
|
||||
|
||||
@login_required
|
||||
def preview_dispatch(request, preview_id, location, dispatch=None):
|
||||
"""
|
||||
Dispatch an AJAX action to a preview XModule
|
||||
|
||||
Expects a POST request, and passes the arguments to the module
|
||||
|
||||
preview_id (str): An identifier specifying which preview this module is used for
|
||||
location: The Location of the module to dispatch to
|
||||
dispatch: The action to execute
|
||||
"""
|
||||
|
||||
instance_state, shared_state = load_preview_state(request, preview_id, location)
|
||||
descriptor = modulestore().get_item(location)
|
||||
instance = load_preview_module(request, preview_id, descriptor, instance_state, shared_state)
|
||||
# Let the module handle the AJAX
|
||||
try:
|
||||
ajax_return = instance.handle_ajax(dispatch, request.POST)
|
||||
except NotFoundError:
|
||||
log.exception("Module indicating to user that request doesn't exist")
|
||||
raise Http404
|
||||
except:
|
||||
log.exception("error processing ajax call")
|
||||
raise
|
||||
|
||||
save_preview_state(request, preview_id, location, instance.get_instance_state(), instance.get_shared_state())
|
||||
return HttpResponse(ajax_return)
|
||||
|
||||
|
||||
def load_preview_state(request, preview_id, location):
|
||||
"""
|
||||
Load the state of a preview module from the request
|
||||
|
||||
preview_id (str): An identifier specifying which preview this module is used for
|
||||
location: The Location of the module to dispatch to
|
||||
"""
|
||||
if 'preview_states' not in request.session:
|
||||
request.session['preview_states'] = defaultdict(dict)
|
||||
|
||||
instance_state = request.session['preview_states'][preview_id, location].get('instance')
|
||||
shared_state = request.session['preview_states'][preview_id, location].get('shared')
|
||||
|
||||
return instance_state, shared_state
|
||||
|
||||
|
||||
def save_preview_state(request, preview_id, location, instance_state, shared_state):
|
||||
"""
|
||||
Load the state of a preview module to the request
|
||||
|
||||
preview_id (str): An identifier specifying which preview this module is used for
|
||||
location: The Location of the module to dispatch to
|
||||
instance_state: The instance state to save
|
||||
shared_state: The shared state to save
|
||||
"""
|
||||
if 'preview_states' not in request.session:
|
||||
request.session['preview_states'] = defaultdict(dict)
|
||||
|
||||
request.session['preview_states'][preview_id, location]['instance'] = instance_state
|
||||
request.session['preview_states'][preview_id, location]['shared'] = shared_state
|
||||
|
||||
|
||||
def render_from_lms(template_name, dictionary, context=None, namespace='main'):
|
||||
"""
|
||||
Render a template using the LMS MAKO_TEMPLATES
|
||||
"""
|
||||
return render_to_string(template_name, dictionary, context, namespace="lms." + namespace)
|
||||
|
||||
|
||||
def preview_module_system(request, preview_id, descriptor):
|
||||
"""
|
||||
Returns a ModuleSystem for the specified descriptor that is specialized for
|
||||
rendering module previews.
|
||||
|
||||
request: The active django request
|
||||
preview_id (str): An identifier specifying which preview this module is used for
|
||||
descriptor: An XModuleDescriptor
|
||||
"""
|
||||
return ModuleSystem(
|
||||
ajax_url=reverse('preview_dispatch', args=[preview_id, descriptor.location.url(), '']),
|
||||
# TODO (cpennington): Do we want to track how instructors are using the preview problems?
|
||||
track_function=lambda type, event: None,
|
||||
filestore=descriptor.system.resources_fs,
|
||||
get_module=partial(get_preview_module, request, preview_id),
|
||||
render_template=render_from_lms,
|
||||
debug=True,
|
||||
replace_urls=replace_urls,
|
||||
# TODO (vshnayder): All CMS users get staff view by default
|
||||
# is that what we want?
|
||||
is_staff=True,
|
||||
)
|
||||
|
||||
|
||||
def get_preview_module(request, preview_id, location):
|
||||
"""
|
||||
Returns a preview XModule at the specified location. The preview_data is chosen arbitrarily
|
||||
from the set of preview data for the descriptor specified by Location
|
||||
|
||||
request: The active django request
|
||||
preview_id (str): An identifier specifying which preview this module is used for
|
||||
location: A Location
|
||||
"""
|
||||
descriptor = modulestore().get_item(location)
|
||||
instance_state, shared_state = descriptor.get_sample_state()[0]
|
||||
return load_preview_module(request, preview_id, descriptor, instance_state, shared_state)
|
||||
|
||||
|
||||
def load_preview_module(request, preview_id, descriptor, instance_state, shared_state):
|
||||
"""
|
||||
Return a preview XModule instantiated from the supplied descriptor, instance_state, and shared_state
|
||||
|
||||
request: The active django request
|
||||
preview_id (str): An identifier specifying which preview this module is used for
|
||||
descriptor: An XModuleDescriptor
|
||||
instance_state: An instance state string
|
||||
shared_state: A shared state string
|
||||
"""
|
||||
system = preview_module_system(request, preview_id, descriptor)
|
||||
module = descriptor.xmodule_constructor(system)(instance_state, shared_state)
|
||||
module.get_html = replace_static_urls(
|
||||
wrap_xmodule(module.get_html, module, "xmodule_display.html"),
|
||||
module.metadata['data_dir']
|
||||
)
|
||||
save_preview_state(request, preview_id, descriptor.location.url(),
|
||||
module.get_instance_state(), module.get_shared_state())
|
||||
|
||||
return module
|
||||
|
||||
|
||||
def get_module_previews(request, descriptor):
|
||||
"""
|
||||
Returns a list of preview XModule html contents. One preview is returned for each
|
||||
pair of states returned by get_sample_state() for the supplied descriptor.
|
||||
|
||||
descriptor: An XModuleDescriptor
|
||||
"""
|
||||
preview_html = []
|
||||
for idx, (instance_state, shared_state) in enumerate(descriptor.get_sample_state()):
|
||||
module = load_preview_module(request, str(idx), descriptor, instance_state, shared_state)
|
||||
preview_html.append(module.get_html())
|
||||
return preview_html
|
||||
|
||||
|
||||
@login_required
|
||||
@expect_json
|
||||
def save_item(request):
|
||||
@@ -118,4 +291,7 @@ def save_item(request):
|
||||
author_string = user_author_string(request.user)
|
||||
export_to_github(course, "CMS Edit", author_string)
|
||||
|
||||
return HttpResponse(json.dumps({}))
|
||||
descriptor = modulestore().get_item(item_location)
|
||||
preview_html = get_module_previews(request, descriptor)
|
||||
|
||||
return HttpResponse(json.dumps(preview_html))
|
||||
|
||||
@@ -5,36 +5,77 @@ from django.conf import settings
|
||||
from fs.osfs import OSFS
|
||||
from git import Repo, PushInfo
|
||||
|
||||
from contentstore import import_from_xml
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from collections import namedtuple
|
||||
|
||||
from .exceptions import GithubSyncError
|
||||
from .exceptions import GithubSyncError, InvalidRepo
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
RepoSettings = namedtuple('RepoSettings', 'path branch origin')
|
||||
|
||||
def import_from_github(repo_settings):
|
||||
"""
|
||||
Imports data into the modulestore based on the XML stored on github
|
||||
|
||||
repo_settings is a dictionary with the following keys:
|
||||
path: file system path to the local git repo
|
||||
branch: name of the branch to track on github
|
||||
def sync_all_with_github():
|
||||
"""
|
||||
repo_path = repo_settings['path']
|
||||
data_dir, course_dir = os.path.split(repo_path)
|
||||
Sync all defined repositories from github
|
||||
"""
|
||||
for repo_name in settings.REPOS:
|
||||
sync_with_github(load_repo_settings(repo_name))
|
||||
|
||||
|
||||
def sync_with_github(repo_settings):
|
||||
"""
|
||||
Sync specified repository from github
|
||||
|
||||
repo_settings: A RepoSettings defining which repo to sync
|
||||
"""
|
||||
revision, course = import_from_github(repo_settings)
|
||||
export_to_github(course, "Changes from cms import of revision %s" % revision, "CMS <cms@edx.org>")
|
||||
|
||||
|
||||
def setup_repo(repo_settings):
|
||||
"""
|
||||
Reset the local github repo specified by repo_settings
|
||||
|
||||
repo_settings (RepoSettings): The settings for the repo to reset
|
||||
"""
|
||||
course_dir = repo_settings.path
|
||||
repo_path = settings.GITHUB_REPO_ROOT / course_dir
|
||||
|
||||
if not os.path.isdir(repo_path):
|
||||
Repo.clone_from(repo_settings['origin'], repo_path)
|
||||
Repo.clone_from(repo_settings.origin, repo_path)
|
||||
|
||||
git_repo = Repo(repo_path)
|
||||
origin = git_repo.remotes.origin
|
||||
origin.fetch()
|
||||
|
||||
# Do a hard reset to the remote branch so that we have a clean import
|
||||
git_repo.git.checkout(repo_settings['branch'])
|
||||
git_repo.head.reset('origin/%s' % repo_settings['branch'], index=True, working_tree=True)
|
||||
module_store = import_from_xml(data_dir, course_dirs=[course_dir])
|
||||
git_repo.git.checkout(repo_settings.branch)
|
||||
|
||||
return git_repo
|
||||
|
||||
|
||||
def load_repo_settings(course_dir):
|
||||
"""
|
||||
Returns the repo_settings for the course stored in course_dir
|
||||
"""
|
||||
if course_dir not in settings.REPOS:
|
||||
raise InvalidRepo(course_dir)
|
||||
|
||||
return RepoSettings(course_dir, **settings.REPOS[course_dir])
|
||||
|
||||
|
||||
def import_from_github(repo_settings):
|
||||
"""
|
||||
Imports data into the modulestore based on the XML stored on github
|
||||
"""
|
||||
course_dir = repo_settings.path
|
||||
git_repo = setup_repo(repo_settings)
|
||||
git_repo.head.reset('origin/%s' % repo_settings.branch, index=True, working_tree=True)
|
||||
|
||||
module_store = import_from_xml(modulestore(),
|
||||
settings.GITHUB_REPO_ROOT, course_dirs=[course_dir])
|
||||
return git_repo.head.commit.hexsha, module_store.courses[course_dir]
|
||||
|
||||
|
||||
@@ -44,21 +85,23 @@ def export_to_github(course, commit_message, author_str=None):
|
||||
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)
|
||||
course_dir = course.metadata.get('data_dir', course.location.course)
|
||||
repo_settings = load_repo_settings(course_dir)
|
||||
git_repo = setup_repo(repo_settings)
|
||||
|
||||
fs = OSFS(git_repo.working_dir)
|
||||
xml = course.export_to_xml(fs)
|
||||
|
||||
with fs.open('course.xml', 'w') as course_xml:
|
||||
course_xml.write(xml)
|
||||
|
||||
git_repo = Repo(repo_path)
|
||||
if git_repo.is_dirty():
|
||||
git_repo.git.add(A=True)
|
||||
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()
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
class GithubSyncError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidRepo(Exception):
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
###
|
||||
### Script for syncing CMS with defined github repos
|
||||
###
|
||||
|
||||
from django.core.management.base import NoArgsCommand
|
||||
from github_sync import sync_all_with_github
|
||||
|
||||
|
||||
class Command(NoArgsCommand):
|
||||
help = \
|
||||
'''Sync the CMS with the defined github repos'''
|
||||
|
||||
def handle_noargs(self, **options):
|
||||
sync_all_with_github()
|
||||
@@ -1,45 +1,48 @@
|
||||
from django.test import TestCase
|
||||
from path import path
|
||||
import shutil
|
||||
import os
|
||||
from github_sync import import_from_github, export_to_github
|
||||
from github_sync import (
|
||||
import_from_github, export_to_github, load_repo_settings,
|
||||
sync_all_with_github, sync_with_github
|
||||
)
|
||||
from git import Repo
|
||||
from django.conf import settings
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore import Location
|
||||
from override_settings import override_settings
|
||||
from github_sync.exceptions import GithubSyncError
|
||||
from mock import patch, Mock
|
||||
|
||||
REPO_DIR = settings.GITHUB_REPO_ROOT / 'local_repo'
|
||||
WORKING_DIR = path(settings.TEST_ROOT)
|
||||
REMOTE_DIR = WORKING_DIR / 'remote_repo'
|
||||
|
||||
|
||||
@override_settings(DATA_DIR=path('test_root'))
|
||||
@override_settings(REPOS={
|
||||
'local_repo': {
|
||||
'origin': REMOTE_DIR,
|
||||
'branch': 'master',
|
||||
}
|
||||
})
|
||||
class GithubSyncTestCase(TestCase):
|
||||
|
||||
def cleanup(self):
|
||||
shutil.rmtree(self.repo_dir, ignore_errors=True)
|
||||
shutil.rmtree(self.remote_dir, ignore_errors=True)
|
||||
shutil.rmtree(REPO_DIR, ignore_errors=True)
|
||||
shutil.rmtree(REMOTE_DIR, ignore_errors=True)
|
||||
modulestore().collection.drop()
|
||||
|
||||
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)
|
||||
shutil.copytree('common/test/data/toy', REMOTE_DIR)
|
||||
|
||||
remote = Repo.init(self.remote_dir)
|
||||
remote = Repo.init(REMOTE_DIR)
|
||||
remote.git.add(A=True)
|
||||
remote.git.commit(m='Initial commit')
|
||||
remote.git.config("receive.denyCurrentBranch", "ignore")
|
||||
|
||||
modulestore().collection.drop()
|
||||
|
||||
self.import_revision, self.import_course = import_from_github({
|
||||
'path': self.repo_dir,
|
||||
'origin': self.remote_dir,
|
||||
'branch': 'master',
|
||||
})
|
||||
self.import_revision, self.import_course = import_from_github(load_repo_settings('local_repo'))
|
||||
|
||||
def tearDown(self):
|
||||
self.cleanup()
|
||||
@@ -48,7 +51,7 @@ class GithubSyncTestCase(TestCase):
|
||||
"""
|
||||
Test that importing from github will create a repo if the repo doesn't already exist
|
||||
"""
|
||||
self.assertEquals(1, len(Repo(self.repo_dir).head.reference.log()))
|
||||
self.assertEquals(1, len(Repo(REPO_DIR).head.reference.log()))
|
||||
|
||||
def test_import_contents(self):
|
||||
"""
|
||||
@@ -56,17 +59,30 @@ class GithubSyncTestCase(TestCase):
|
||||
"""
|
||||
self.assertEquals('Toy Course', self.import_course.metadata['display_name'])
|
||||
self.assertIn(
|
||||
Location('i4x://edx/local_repo/chapter/Overview'),
|
||||
Location('i4x://edX/toy/chapter/Overview'),
|
||||
[child.location for child in self.import_course.get_children()])
|
||||
self.assertEquals(1, len(self.import_course.get_children()))
|
||||
|
||||
@patch('github_sync.sync_with_github')
|
||||
def test_sync_all_with_github(self, sync_with_github):
|
||||
sync_all_with_github()
|
||||
sync_with_github.assert_called_with(load_repo_settings('local_repo'))
|
||||
|
||||
def test_sync_with_github(self):
|
||||
with patch('github_sync.import_from_github', Mock(return_value=(Mock(), Mock()))) as import_from_github:
|
||||
with patch('github_sync.export_to_github') as export_to_github:
|
||||
settings = load_repo_settings('local_repo')
|
||||
sync_with_github(settings)
|
||||
import_from_github.assert_called_with(settings)
|
||||
export_to_github.assert_called
|
||||
|
||||
@override_settings(MITX_FEATURES={'GITHUB_PUSH': False})
|
||||
def test_export_no_pash(self):
|
||||
"""
|
||||
Test that with the GITHUB_PUSH feature disabled, no content is pushed to the remote
|
||||
"""
|
||||
export_to_github(self.import_course, 'Test no-push')
|
||||
self.assertEquals(1, Repo(self.remote_dir).head.commit.count())
|
||||
self.assertEquals(1, Repo(REMOTE_DIR).head.commit.count())
|
||||
|
||||
@override_settings(MITX_FEATURES={'GITHUB_PUSH': True})
|
||||
def test_export_push(self):
|
||||
@@ -75,7 +91,7 @@ class GithubSyncTestCase(TestCase):
|
||||
"""
|
||||
self.import_course.metadata['display_name'] = 'Changed display name'
|
||||
export_to_github(self.import_course, 'Test push')
|
||||
self.assertEquals(2, Repo(self.remote_dir).head.commit.count())
|
||||
self.assertEquals(2, Repo(REMOTE_DIR).head.commit.count())
|
||||
|
||||
@override_settings(MITX_FEATURES={'GITHUB_PUSH': True})
|
||||
def test_export_conflict(self):
|
||||
@@ -84,7 +100,7 @@ class GithubSyncTestCase(TestCase):
|
||||
"""
|
||||
self.import_course.metadata['display_name'] = 'Changed display name'
|
||||
|
||||
remote = Repo(self.remote_dir)
|
||||
remote = Repo(REMOTE_DIR)
|
||||
remote.git.commit(allow_empty=True, m="Testing conflict commit")
|
||||
|
||||
self.assertRaises(GithubSyncError, export_to_github, self.import_course, 'Test push')
|
||||
|
||||
@@ -1,53 +1,43 @@
|
||||
import json
|
||||
from django.test.client import Client
|
||||
from django.test import TestCase
|
||||
from mock import patch, Mock
|
||||
from mock import patch
|
||||
from override_settings import override_settings
|
||||
from django.conf import settings
|
||||
from github_sync import load_repo_settings
|
||||
|
||||
|
||||
@override_settings(REPOS={'repo': {'path': 'path', 'branch': 'branch'}})
|
||||
@override_settings(REPOS={'repo': {'branch': 'branch', 'origin': 'origin'}})
|
||||
class PostReceiveTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
|
||||
@patch('github_sync.views.export_to_github')
|
||||
@patch('github_sync.views.import_from_github')
|
||||
def test_non_branch(self, import_from_github, export_to_github):
|
||||
@patch('github_sync.views.sync_with_github')
|
||||
def test_non_branch(self, sync_with_github):
|
||||
self.client.post('/github_service_hook', {'payload': json.dumps({
|
||||
'ref': 'refs/tags/foo'})
|
||||
})
|
||||
self.assertFalse(import_from_github.called)
|
||||
self.assertFalse(export_to_github.called)
|
||||
self.assertFalse(sync_with_github.called)
|
||||
|
||||
@patch('github_sync.views.export_to_github')
|
||||
@patch('github_sync.views.import_from_github')
|
||||
def test_non_watched_repo(self, import_from_github, export_to_github):
|
||||
@patch('github_sync.views.sync_with_github')
|
||||
def test_non_watched_repo(self, sync_with_github):
|
||||
self.client.post('/github_service_hook', {'payload': json.dumps({
|
||||
'ref': 'refs/heads/branch',
|
||||
'repository': {'name': 'bad_repo'}})
|
||||
})
|
||||
self.assertFalse(import_from_github.called)
|
||||
self.assertFalse(export_to_github.called)
|
||||
self.assertFalse(sync_with_github.called)
|
||||
|
||||
@patch('github_sync.views.export_to_github')
|
||||
@patch('github_sync.views.import_from_github')
|
||||
def test_non_tracked_branch(self, import_from_github, export_to_github):
|
||||
@patch('github_sync.views.sync_with_github')
|
||||
def test_non_tracked_branch(self, sync_with_github):
|
||||
self.client.post('/github_service_hook', {'payload': json.dumps({
|
||||
'ref': 'refs/heads/non_branch',
|
||||
'repository': {'name': 'repo'}})
|
||||
})
|
||||
self.assertFalse(import_from_github.called)
|
||||
self.assertFalse(export_to_github.called)
|
||||
self.assertFalse(sync_with_github.called)
|
||||
|
||||
@patch('github_sync.views.export_to_github')
|
||||
@patch('github_sync.views.import_from_github', return_value=(Mock(), Mock()))
|
||||
def test_tracked_branch(self, import_from_github, export_to_github):
|
||||
@patch('github_sync.views.sync_with_github')
|
||||
def test_tracked_branch(self, sync_with_github):
|
||||
self.client.post('/github_service_hook', {'payload': json.dumps({
|
||||
'ref': 'refs/heads/branch',
|
||||
'repository': {'name': 'repo'}})
|
||||
})
|
||||
import_from_github.assert_called_with(settings.REPOS['repo'])
|
||||
mock_revision, mock_course = import_from_github.return_value
|
||||
export_to_github.assert_called_with(mock_course, 'path', "Changes from cms import of revision %s" % mock_revision)
|
||||
|
||||
sync_with_github.assert_called_with(load_repo_settings('repo'))
|
||||
|
||||
@@ -5,7 +5,7 @@ from django.http import HttpResponse
|
||||
from django.conf import settings
|
||||
from django_future.csrf import csrf_exempt
|
||||
|
||||
from . import import_from_github, export_to_github
|
||||
from . import sync_with_github, load_repo_settings
|
||||
|
||||
log = logging.getLogger()
|
||||
|
||||
@@ -40,13 +40,12 @@ def github_post_receive(request):
|
||||
log.info('No repository matching %s found' % repo_name)
|
||||
return HttpResponse('No Repo Found')
|
||||
|
||||
repo = settings.REPOS[repo_name]
|
||||
repo = load_repo_settings(repo_name)
|
||||
|
||||
if repo['branch'] != branch_name:
|
||||
if repo.branch != branch_name:
|
||||
log.info('Ignoring changes to non-tracked branch %s in repo %s' % (branch_name, repo_name))
|
||||
return HttpResponse('Ignoring non-tracked branch')
|
||||
|
||||
revision, course = import_from_github(repo)
|
||||
export_to_github(course, repo['path'], "Changes from cms import of revision %s" % revision)
|
||||
sync_with_github(repo)
|
||||
|
||||
return HttpResponse('Push received')
|
||||
|
||||
48
cms/envs/aws.py
Normal file
48
cms/envs/aws.py
Normal file
@@ -0,0 +1,48 @@
|
||||
"""
|
||||
This is the default template for our main set of AWS servers.
|
||||
"""
|
||||
import json
|
||||
|
||||
from .logsettings import get_logger_config
|
||||
from .common import *
|
||||
|
||||
############################### ALWAYS THE SAME ################################
|
||||
DEBUG = False
|
||||
TEMPLATE_DEBUG = False
|
||||
|
||||
EMAIL_BACKEND = 'django_ses.SESBackend'
|
||||
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
|
||||
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto.S3BotoStorage'
|
||||
|
||||
########################### NON-SECURE ENV CONFIG ##############################
|
||||
# Things like server locations, ports, etc.
|
||||
with open(ENV_ROOT / "cms.env.json") as env_file:
|
||||
ENV_TOKENS = json.load(env_file)
|
||||
|
||||
SITE_NAME = ENV_TOKENS['SITE_NAME']
|
||||
|
||||
LOG_DIR = ENV_TOKENS['LOG_DIR']
|
||||
|
||||
CACHES = ENV_TOKENS['CACHES']
|
||||
|
||||
for feature, value in ENV_TOKENS.get('MITX_FEATURES', {}).items():
|
||||
MITX_FEATURES[feature] = value
|
||||
|
||||
LOGGING = get_logger_config(LOG_DIR,
|
||||
logging_env=ENV_TOKENS['LOGGING_ENV'],
|
||||
syslog_addr=(ENV_TOKENS['SYSLOG_SERVER'], 514),
|
||||
debug=False)
|
||||
|
||||
with open(ENV_ROOT / "repos.json") as repos_file:
|
||||
REPOS = json.load(repos_file)
|
||||
|
||||
|
||||
############################## SECURE AUTH ITEMS ###############################
|
||||
# Secret things: passwords, access keys, etc.
|
||||
with open(ENV_ROOT / "cms.auth.json") as auth_file:
|
||||
AUTH_TOKENS = json.load(auth_file)
|
||||
|
||||
AWS_ACCESS_KEY_ID = AUTH_TOKENS["AWS_ACCESS_KEY_ID"]
|
||||
AWS_SECRET_ACCESS_KEY = AUTH_TOKENS["AWS_SECRET_ACCESS_KEY"]
|
||||
DATABASES = AUTH_TOKENS['DATABASES']
|
||||
MODULESTORE = AUTH_TOKENS['MODULESTORE']
|
||||
@@ -25,6 +25,9 @@ import os.path
|
||||
import os
|
||||
import errno
|
||||
import glob2
|
||||
import lms.envs.common
|
||||
import hashlib
|
||||
from collections import defaultdict
|
||||
from path import path
|
||||
|
||||
############################ FEATURE CONFIGURATION #############################
|
||||
@@ -43,10 +46,8 @@ PROJECT_ROOT = path(__file__).abspath().dirname().dirname() # /mitx/cms
|
||||
REPO_ROOT = PROJECT_ROOT.dirname()
|
||||
COMMON_ROOT = REPO_ROOT / "common"
|
||||
ENV_ROOT = REPO_ROOT.dirname() # virtualenv dir /mitx is in
|
||||
COURSES_ROOT = ENV_ROOT / "data"
|
||||
|
||||
# FIXME: To support multiple courses, we should walk the courses dir at startup
|
||||
DATA_DIR = COURSES_ROOT
|
||||
GITHUB_REPO_ROOT = ENV_ROOT / "data"
|
||||
|
||||
sys.path.append(REPO_ROOT)
|
||||
sys.path.append(PROJECT_ROOT / 'djangoapps')
|
||||
@@ -61,9 +62,13 @@ MAKO_MODULE_DIR = tempfile.mkdtemp('mako')
|
||||
MAKO_TEMPLATES = {}
|
||||
MAKO_TEMPLATES['main'] = [
|
||||
PROJECT_ROOT / 'templates',
|
||||
COMMON_ROOT / 'templates',
|
||||
COMMON_ROOT / 'djangoapps' / 'pipeline_mako' / 'templates'
|
||||
]
|
||||
|
||||
for namespace, template_dirs in lms.envs.common.MAKO_TEMPLATES.iteritems():
|
||||
MAKO_TEMPLATES['lms.' + namespace] = template_dirs
|
||||
|
||||
TEMPLATE_DIRS = (
|
||||
PROJECT_ROOT / "templates",
|
||||
)
|
||||
@@ -132,26 +137,32 @@ IGNORABLE_404_ENDS = ('favicon.ico')
|
||||
|
||||
# Email
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||
DEFAULT_FROM_EMAIL = 'registration@mitx.mit.edu'
|
||||
DEFAULT_FEEDBACK_EMAIL = 'feedback@mitx.mit.edu'
|
||||
DEFAULT_FROM_EMAIL = 'registration@edx.org'
|
||||
DEFAULT_FEEDBACK_EMAIL = 'feedback@edx.org'
|
||||
ADMINS = (
|
||||
('MITx Admins', 'admin@mitx.mit.edu'),
|
||||
('edX Admins', 'admin@edx.org'),
|
||||
)
|
||||
MANAGERS = ADMINS
|
||||
|
||||
# Static content
|
||||
STATIC_URL = '/static/'
|
||||
ADMIN_MEDIA_PREFIX = '/static/admin/'
|
||||
STATIC_ROOT = ENV_ROOT / "staticfiles"
|
||||
STATIC_ROOT = ENV_ROOT / "staticfiles"
|
||||
|
||||
# FIXME: We should iterate through the courses we have, adding the static
|
||||
# contents for each of them. (Right now we just use symlinks.)
|
||||
STATICFILES_DIRS = [
|
||||
COMMON_ROOT / "static",
|
||||
PROJECT_ROOT / "static",
|
||||
|
||||
# This is how you would use the textbook images locally
|
||||
# ("book", ENV_ROOT / "book_images")
|
||||
]
|
||||
if os.path.isdir(GITHUB_REPO_ROOT):
|
||||
STATICFILES_DIRS += [
|
||||
# TODO (cpennington): When courses aren't loaded from github, remove this
|
||||
(course_dir, GITHUB_REPO_ROOT / course_dir)
|
||||
for course_dir in os.listdir(GITHUB_REPO_ROOT)
|
||||
if os.path.isdir(GITHUB_REPO_ROOT / course_dir)
|
||||
]
|
||||
|
||||
# Locale/Internationalization
|
||||
TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
|
||||
@@ -166,51 +177,98 @@ MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage'
|
||||
|
||||
STATICFILES_STORAGE = 'pipeline.storage.PipelineCachedStorage'
|
||||
|
||||
# Load javascript and css from all of the available descriptors, and
|
||||
# prep it for use in pipeline js
|
||||
from xmodule.x_module import XModuleDescriptor
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
js_file_dir = PROJECT_ROOT / "static" / "coffee" / "module"
|
||||
css_file_dir = PROJECT_ROOT / "static" / "sass" / "module"
|
||||
module_styles_path = css_file_dir / "_module-styles.scss"
|
||||
|
||||
for dir_ in (js_file_dir, css_file_dir):
|
||||
try:
|
||||
os.makedirs(dir_)
|
||||
except OSError as exc:
|
||||
if exc.errno == errno.EEXIST:
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
|
||||
js_fragments = set()
|
||||
css_fragments = defaultdict(set)
|
||||
for descriptor in XModuleDescriptor.load_classes() + [RawDescriptor]:
|
||||
descriptor_js = descriptor.get_javascript()
|
||||
module_js = descriptor.module_class.get_javascript()
|
||||
|
||||
for filetype in ('coffee', 'js'):
|
||||
for idx, fragment in enumerate(descriptor_js.get(filetype, []) + module_js.get(filetype, [])):
|
||||
js_fragments.add((idx, filetype, fragment))
|
||||
|
||||
for class_ in (descriptor, descriptor.module_class):
|
||||
fragments = class_.get_css()
|
||||
for filetype in ('sass', 'scss', 'css'):
|
||||
for idx, fragment in enumerate(fragments.get(filetype, [])):
|
||||
css_fragments[idx, filetype, fragment].add(class_.__name__)
|
||||
|
||||
module_js_sources = []
|
||||
for idx, filetype, fragment in sorted(js_fragments):
|
||||
path = js_file_dir / "{idx}-{hash}.{type}".format(
|
||||
idx=idx,
|
||||
hash=hashlib.md5(fragment).hexdigest(),
|
||||
type=filetype)
|
||||
with open(path, 'w') as js_file:
|
||||
js_file.write(fragment)
|
||||
module_js_sources.append(path.replace(PROJECT_ROOT / "static/", ""))
|
||||
|
||||
css_imports = defaultdict(set)
|
||||
for (idx, filetype, fragment), classes in sorted(css_fragments.items()):
|
||||
fragment_name = "{idx}-{hash}.{type}".format(
|
||||
idx=idx,
|
||||
hash=hashlib.md5(fragment).hexdigest(),
|
||||
type=filetype)
|
||||
# Prepend _ so that sass just includes the files into a single file
|
||||
with open(css_file_dir / '_' + fragment_name, 'w') as js_file:
|
||||
js_file.write(fragment)
|
||||
|
||||
for class_ in classes:
|
||||
css_imports[class_].add(fragment_name)
|
||||
|
||||
with open(module_styles_path, 'w') as module_styles:
|
||||
for class_, fragment_names in css_imports.items():
|
||||
imports = "\n".join('@import "{0}";'.format(name) for name in fragment_names)
|
||||
module_styles.write(""".xmodule_{class_} {{ {imports} }}""".format(
|
||||
class_=class_, imports=imports
|
||||
))
|
||||
|
||||
PIPELINE_CSS = {
|
||||
'base-style': {
|
||||
'source_filenames': ['sass/base-style.scss'],
|
||||
'output_filename': 'css/base-style.css',
|
||||
'output_filename': 'css/cms-base-style.css',
|
||||
},
|
||||
}
|
||||
|
||||
PIPELINE_ALWAYS_RECOMPILE = ['sass/base-style.scss']
|
||||
|
||||
from xmodule.x_module import XModuleDescriptor
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
js_file_dir = PROJECT_ROOT / "static" / "coffee" / "module"
|
||||
try:
|
||||
os.makedirs(js_file_dir)
|
||||
except OSError as exc:
|
||||
if exc.errno == errno.EEXIST:
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
|
||||
module_js_sources = []
|
||||
for xmodule in XModuleDescriptor.load_classes() + [RawDescriptor]:
|
||||
js = xmodule.get_javascript()
|
||||
for filetype in ('coffee', 'js'):
|
||||
for idx, fragment in enumerate(js.get(filetype, [])):
|
||||
path = os.path.join(js_file_dir, "{name}.{idx}.{type}".format(
|
||||
name=xmodule.__name__,
|
||||
idx=idx,
|
||||
type=filetype))
|
||||
with open(path, 'w') as js_file:
|
||||
js_file.write(fragment)
|
||||
module_js_sources.append(path.replace(PROJECT_ROOT / "static/", ""))
|
||||
|
||||
PIPELINE_JS = {
|
||||
'main': {
|
||||
'source_filenames': [pth.replace(PROJECT_ROOT / 'static/', '') for pth in glob2.glob(PROJECT_ROOT / 'static/coffee/src/**/*.coffee')],
|
||||
'output_filename': 'js/application.js',
|
||||
'source_filenames': [
|
||||
pth.replace(COMMON_ROOT / 'static/', '')
|
||||
for pth
|
||||
in glob2.glob(COMMON_ROOT / 'static/coffee/src/**/*.coffee')
|
||||
] + [
|
||||
pth.replace(PROJECT_ROOT / 'static/', '')
|
||||
for pth
|
||||
in glob2.glob(PROJECT_ROOT / 'static/coffee/src/**/*.coffee')
|
||||
],
|
||||
'output_filename': 'js/cms-application.js',
|
||||
},
|
||||
'module-js': {
|
||||
'source_filenames': module_js_sources,
|
||||
'output_filename': 'js/modules.js',
|
||||
'output_filename': 'js/cms-modules.js',
|
||||
},
|
||||
'spec': {
|
||||
'source_filenames': [pth.replace(PROJECT_ROOT / 'static/', '') for pth in glob2.glob(PROJECT_ROOT / 'static/coffee/spec/**/*.coffee')],
|
||||
'output_filename': 'js/spec.js'
|
||||
'output_filename': 'js/cms-spec.js'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,6 +309,7 @@ INSTALLED_APPS = (
|
||||
|
||||
# For CMS
|
||||
'contentstore',
|
||||
'github_sync',
|
||||
'student', # misleading name due to sharing with lms
|
||||
|
||||
# For asset pipelining
|
||||
|
||||
@@ -18,6 +18,7 @@ MODULESTORE = {
|
||||
'host': 'localhost',
|
||||
'db': 'xmodule',
|
||||
'collection': 'modulestore',
|
||||
'fs_root': GITHUB_REPO_ROOT,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,38 +32,24 @@ DATABASES = {
|
||||
|
||||
REPOS = {
|
||||
'edx4edx': {
|
||||
'path': DATA_DIR / "edx4edx",
|
||||
'org': 'edx',
|
||||
'course': 'edx4edx',
|
||||
'branch': 'for_cms',
|
||||
'branch': 'master',
|
||||
'origin': 'git@github.com:MITx/edx4edx.git',
|
||||
},
|
||||
'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',
|
||||
'content-mit-6002x': {
|
||||
'branch': 'master',
|
||||
#'origin': 'git@github.com:MITx/6002x-fall-2012.git',
|
||||
'origin': 'git@github.com:MITx/content-mit-6002x.git',
|
||||
},
|
||||
'6.00x': {
|
||||
'path': DATA_DIR / '6.00x',
|
||||
'org': 'mit.edu',
|
||||
'course': '6.00x',
|
||||
'branch': 'for_cms',
|
||||
'branch': 'master',
|
||||
'origin': 'git@github.com:MITx/6.00x.git',
|
||||
},
|
||||
'7.00x': {
|
||||
'path': DATA_DIR / '7.00x',
|
||||
'org': 'mit.edu',
|
||||
'course': '7.00x',
|
||||
'branch': 'for_cms',
|
||||
'branch': 'master',
|
||||
'origin': 'git@github.com:MITx/7.00x.git',
|
||||
},
|
||||
'3.091x': {
|
||||
'path': DATA_DIR / '3.091x',
|
||||
'org': 'mit.edu',
|
||||
'course': '3.091x',
|
||||
'branch': 'for_cms',
|
||||
'branch': 'master',
|
||||
'origin': 'git@github.com:MITx/3.091x.git',
|
||||
},
|
||||
}
|
||||
@@ -89,3 +76,6 @@ CACHES = {
|
||||
'KEY_FUNCTION': 'util.memcache.safe_key',
|
||||
}
|
||||
}
|
||||
|
||||
# Make the keyedcache startup warnings go away
|
||||
CACHE_TIMEOUT = 0
|
||||
|
||||
95
cms/envs/logsettings.py
Normal file
95
cms/envs/logsettings.py
Normal file
@@ -0,0 +1,95 @@
|
||||
import os
|
||||
import os.path
|
||||
import platform
|
||||
import sys
|
||||
|
||||
def get_logger_config(log_dir,
|
||||
logging_env="no_env",
|
||||
tracking_filename=None,
|
||||
syslog_addr=None,
|
||||
debug=False):
|
||||
"""Return the appropriate logging config dictionary. You should assign the
|
||||
result of this to the LOGGING var in your settings. The reason it's done
|
||||
this way instead of registering directly is because I didn't want to worry
|
||||
about resetting the logging state if this is called multiple times when
|
||||
settings are extended."""
|
||||
|
||||
# If we're given an explicit place to put tracking logs, we do that (say for
|
||||
# debugging). However, logging is not safe for multiple processes hitting
|
||||
# the same file. So if it's left blank, we dynamically create the filename
|
||||
# based on the PID of this worker process.
|
||||
if tracking_filename:
|
||||
tracking_file_loc = os.path.join(log_dir, tracking_filename)
|
||||
else:
|
||||
pid = os.getpid() # So we can log which process is creating the log
|
||||
tracking_file_loc = os.path.join(log_dir, "tracking_{0}.log".format(pid))
|
||||
|
||||
hostname = platform.node().split(".")[0]
|
||||
syslog_format = ("[%(name)s][env:{logging_env}] %(levelname)s [{hostname} " +
|
||||
" %(process)d] [%(filename)s:%(lineno)d] - %(message)s").format(
|
||||
logging_env=logging_env, hostname=hostname)
|
||||
|
||||
handlers = ['console'] if debug else ['console', 'syslogger', 'newrelic']
|
||||
|
||||
return {
|
||||
'version': 1,
|
||||
'formatters' : {
|
||||
'standard' : {
|
||||
'format' : '%(asctime)s %(levelname)s %(process)d [%(name)s] %(filename)s:%(lineno)d - %(message)s',
|
||||
},
|
||||
'syslog_format' : { 'format' : syslog_format },
|
||||
'raw' : { 'format' : '%(message)s' },
|
||||
},
|
||||
'handlers' : {
|
||||
'console' : {
|
||||
'level' : 'DEBUG' if debug else 'INFO',
|
||||
'class' : 'logging.StreamHandler',
|
||||
'formatter' : 'standard',
|
||||
'stream' : sys.stdout,
|
||||
},
|
||||
'syslogger' : {
|
||||
'level' : 'INFO',
|
||||
'class' : 'logging.handlers.SysLogHandler',
|
||||
'address' : syslog_addr,
|
||||
'formatter' : 'syslog_format',
|
||||
},
|
||||
'tracking' : {
|
||||
'level' : 'DEBUG',
|
||||
'class' : 'logging.handlers.WatchedFileHandler',
|
||||
'filename' : tracking_file_loc,
|
||||
'formatter' : 'raw',
|
||||
},
|
||||
'newrelic' : {
|
||||
'level': 'ERROR',
|
||||
'class': 'newrelic_logging.NewRelicHandler',
|
||||
'formatter': 'raw',
|
||||
}
|
||||
},
|
||||
'loggers' : {
|
||||
'django' : {
|
||||
'handlers' : handlers,
|
||||
'propagate' : True,
|
||||
'level' : 'INFO'
|
||||
},
|
||||
'tracking' : {
|
||||
'handlers' : ['tracking'],
|
||||
'level' : 'DEBUG',
|
||||
'propagate' : False,
|
||||
},
|
||||
'' : {
|
||||
'handlers' : handlers,
|
||||
'level' : 'DEBUG',
|
||||
'propagate' : False
|
||||
},
|
||||
'mitx' : {
|
||||
'handlers' : handlers,
|
||||
'level' : 'DEBUG',
|
||||
'propagate' : False
|
||||
},
|
||||
'keyedcache' : {
|
||||
'handlers' : handlers,
|
||||
'level' : 'DEBUG',
|
||||
'propagate' : False
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,21 @@ TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
|
||||
TEST_ROOT = path('test_root')
|
||||
|
||||
# Want static files in the same dir for running on jenkins.
|
||||
STATIC_ROOT = TEST_ROOT / "staticfiles"
|
||||
STATIC_ROOT = TEST_ROOT / "staticfiles"
|
||||
|
||||
GITHUB_REPO_ROOT = TEST_ROOT / "data"
|
||||
COMMON_TEST_DATA_ROOT = COMMON_ROOT / "test" / "data"
|
||||
|
||||
# TODO (cpennington): We need to figure out how envs/test.py can inject things into common.py so that we don't have to repeat this sort of thing
|
||||
STATICFILES_DIRS = [
|
||||
COMMON_ROOT / "static",
|
||||
PROJECT_ROOT / "static",
|
||||
]
|
||||
STATICFILES_DIRS += [
|
||||
(course_dir, COMMON_TEST_DATA_ROOT / course_dir)
|
||||
for course_dir in os.listdir(COMMON_TEST_DATA_ROOT)
|
||||
if os.path.isdir(COMMON_TEST_DATA_ROOT / course_dir)
|
||||
]
|
||||
|
||||
MODULESTORE = {
|
||||
'default': {
|
||||
@@ -33,6 +46,7 @@ MODULESTORE = {
|
||||
'host': 'localhost',
|
||||
'db': 'test_xmodule',
|
||||
'collection': 'modulestore',
|
||||
'fs_root': GITHUB_REPO_ROOT,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
AjaxPrefix.addAjaxPrefix(jQuery, -> CMS.prefix)
|
||||
|
||||
@CMS =
|
||||
Models: {}
|
||||
Views: {}
|
||||
|
||||
prefix: $("meta[name='path_prefix']").attr('content')
|
||||
|
||||
viewStack: []
|
||||
|
||||
start: (el) ->
|
||||
@@ -31,5 +35,13 @@ $ ->
|
||||
|
||||
$.ajaxSetup
|
||||
headers : { 'X-CSRFToken': $.cookie 'csrftoken' }
|
||||
dataType: 'json'
|
||||
|
||||
window.onTouchBasedDevice = ->
|
||||
navigator.userAgent.match /iPhone|iPod|iPad/i
|
||||
|
||||
$('body').addClass 'touch-based-device' if onTouchBasedDevice()
|
||||
|
||||
|
||||
CMS.start($('section.main-container'))
|
||||
|
||||
|
||||
@@ -4,10 +4,7 @@ class CMS.Models.Module extends Backbone.Model
|
||||
data: ''
|
||||
|
||||
loadModule: (element) ->
|
||||
try
|
||||
@module = new window[@get('type')](element)
|
||||
catch TypeError
|
||||
console.error "Unable to load #{@get('type')}." if console
|
||||
@module = XModule.loadModule($(element).find('.xmodule_edit'))
|
||||
|
||||
editUrl: ->
|
||||
"/edit_item?#{$.param(id: @get('id'))}"
|
||||
|
||||
@@ -4,4 +4,10 @@ class CMS.Views.Module extends Backbone.View
|
||||
|
||||
edit: (event) =>
|
||||
event.preventDefault()
|
||||
CMS.replaceView(new CMS.Views.ModuleEdit(model: new CMS.Models.Module(id: @$el.data('id'), type: @$el.data('type'))))
|
||||
previewType = @$el.data('preview-type')
|
||||
moduleType = @$el.data('type')
|
||||
CMS.replaceView new CMS.Views.ModuleEdit
|
||||
model: new CMS.Models.Module
|
||||
id: @$el.data('id')
|
||||
type: if moduleType == 'None' then null else moduleType
|
||||
previewType: if previewType == 'None' then null else previewType
|
||||
|
||||
@@ -11,11 +11,21 @@ class CMS.Views.ModuleEdit extends Backbone.View
|
||||
@$el.load @model.editUrl(), =>
|
||||
@model.loadModule(@el)
|
||||
|
||||
# Load preview modules
|
||||
XModule.loadModules('display')
|
||||
|
||||
save: (event) ->
|
||||
event.preventDefault()
|
||||
@model.save().success(->
|
||||
@model.save().done((previews) =>
|
||||
alert("Your changes have been saved.")
|
||||
).error(->
|
||||
previews_section = @$el.find('.previews').empty()
|
||||
$.each(previews, (idx, preview) =>
|
||||
preview_wrapper = $('<section/>', class: 'preview').append preview
|
||||
previews_section.append preview_wrapper
|
||||
)
|
||||
|
||||
XModule.loadModules('display')
|
||||
).fail(->
|
||||
alert("There was an error saving your changes. Please try again.")
|
||||
)
|
||||
|
||||
@@ -25,4 +35,10 @@ class CMS.Views.ModuleEdit extends Backbone.View
|
||||
|
||||
editSubmodule: (event) ->
|
||||
event.preventDefault()
|
||||
CMS.pushView(new CMS.Views.ModuleEdit(model: new CMS.Models.Module(id: $(event.target).data('id'), type: $(event.target).data('type'))))
|
||||
previewType = $(event.target).data('preview-type')
|
||||
moduleType = $(event.target).data('type')
|
||||
CMS.pushView new CMS.Views.ModuleEdit
|
||||
model: new CMS.Models.Module
|
||||
id: $(event.target).data('id')
|
||||
type: if moduleType == 'None' then null else moduleType
|
||||
previewType: if previewType == 'None' then null else previewType
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
class @Unit
|
||||
constructor: (@element_id, @module_id) ->
|
||||
@module = new window[$("##{@element_id}").attr('class')] 'module-html'
|
||||
|
||||
$("##{@element_id} .save-update").click (event) =>
|
||||
event.preventDefault()
|
||||
$.post("/save_item", {
|
||||
id: @module_id
|
||||
data: JSON.stringify(@module.save())
|
||||
})
|
||||
|
||||
$("##{@element_id} .cancel").click (event) =>
|
||||
event.preventDefault()
|
||||
CMS.edit_item(@module_id)
|
||||
|
||||
BIN
cms/static/img/content-types/module.png
Normal file
BIN
cms/static/img/content-types/module.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
cms/static/img/menu.png
Normal file
BIN
cms/static/img/menu.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 95 B |
BIN
cms/static/img/noise.png
Normal file
BIN
cms/static/img/noise.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
1
cms/static/sass/.gitignore
vendored
1
cms/static/sass/.gitignore
vendored
@@ -1 +1,2 @@
|
||||
*.css
|
||||
module
|
||||
|
||||
@@ -5,11 +5,21 @@ $body-font-family: "Open Sans", "Lucida Grande", "Lucida Sans Unicode", "Lucida
|
||||
$body-font-size: 14px;
|
||||
$body-line-height: 20px;
|
||||
|
||||
$light-blue: #f0f8fa;
|
||||
$light-blue: #f0f7fd;
|
||||
$dark-blue: #50545c;
|
||||
$bright-blue: #3c8ebf;
|
||||
$orange: #f96e5b;
|
||||
$yellow: #fff8af;
|
||||
$cream: #F6EFD4;
|
||||
$mit-red: #933;
|
||||
|
||||
@mixin hide-text {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
color: transparent;
|
||||
font: 0/0 a;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
// Base html styles
|
||||
html {
|
||||
@@ -32,14 +42,18 @@ input {
|
||||
|
||||
button, input[type="submit"], .button {
|
||||
background-color: $orange;
|
||||
border: 0;
|
||||
border: 1px solid darken($orange, 15%);
|
||||
@include border-radius(4px);
|
||||
@include box-shadow(inset 0 0 0 1px adjust-hue($orange, 20%), 0 1px 0 #fff);
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
padding: 8px 10px;
|
||||
@include linear-gradient(adjust-hue($orange, 8%), $orange);
|
||||
padding: 6px 20px;
|
||||
text-shadow: 0 1px 0 darken($orange, 10%);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
&:hover {
|
||||
background-color: shade($orange, 10%);
|
||||
&:hover, &:focus {
|
||||
@include box-shadow(inset 0 0 6px 1px adjust-hue($orange, 30%));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,10 +134,10 @@ textarea {
|
||||
}
|
||||
}
|
||||
|
||||
.wip {
|
||||
outline: 1px solid #f00 !important;
|
||||
position: relative;
|
||||
}
|
||||
// .wip {
|
||||
// outline: 1px solid #f00 !important;
|
||||
// position: relative;
|
||||
// }
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
section.cal {
|
||||
@include box-sizing(border-box);
|
||||
@include clearfix;
|
||||
padding: 25px;
|
||||
padding: 20px;
|
||||
|
||||
> header {
|
||||
display: none;
|
||||
@include clearfix;
|
||||
margin-bottom: 10px;
|
||||
opacity: .4;
|
||||
@include transition;
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
@@ -70,12 +72,15 @@ section.cal {
|
||||
ol {
|
||||
list-style: none;
|
||||
@include clearfix;
|
||||
border-left: 1px solid lighten($dark-blue, 40%);
|
||||
border-top: 1px solid lighten($dark-blue, 40%);
|
||||
border: 1px solid lighten( $dark-blue , 30% );
|
||||
background: #FFF;
|
||||
width: 100%;
|
||||
@include box-sizing(border-box);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@include box-shadow(0 0 5px lighten($dark-blue, 45%));
|
||||
@include border-radius(3px);
|
||||
overflow: hidden;
|
||||
|
||||
> li {
|
||||
border-right: 1px solid lighten($dark-blue, 40%);
|
||||
@@ -84,6 +89,7 @@ section.cal {
|
||||
float: left;
|
||||
width: flex-grid(3) + ((flex-gutter() * 3) / 4);
|
||||
background-color: $light-blue;
|
||||
@include box-shadow(inset 0 0 0 1px lighten($light-blue, 8%));
|
||||
|
||||
&:hover {
|
||||
li.create-module {
|
||||
@@ -91,6 +97,10 @@ section.cal {
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(4n) {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
header {
|
||||
border-bottom: 1px solid lighten($dark-blue, 40%);
|
||||
@include box-shadow(0 2px 2px $light-blue);
|
||||
@@ -128,6 +138,7 @@ section.cal {
|
||||
color: #888;
|
||||
border-bottom: 0;
|
||||
font-size: 12px;
|
||||
@include box-shadow(none);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,9 +149,11 @@ section.cal {
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
border-bottom: 1px solid darken($light-blue, 8%);
|
||||
position: relative;
|
||||
border-bottom: 1px solid darken($light-blue, 6%);
|
||||
// @include box-shadow(0 1px 0 lighten($light-blue, 4%));
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($yellow, 14%);
|
||||
@@ -314,16 +327,13 @@ section.cal {
|
||||
@include box-sizing(border-box);
|
||||
opacity: .4;
|
||||
@include transition();
|
||||
background: darken($light-blue, 2%);
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
width: flex-grid(5) + flex-gutter();
|
||||
background-color: transparent;
|
||||
|
||||
+ section.main-content {
|
||||
width: flex-grid(7);
|
||||
opacity: .6;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,6 +350,7 @@ section.cal {
|
||||
display: block;
|
||||
|
||||
li {
|
||||
|
||||
ul {
|
||||
display: inline;
|
||||
}
|
||||
@@ -351,6 +362,7 @@ section.cal {
|
||||
li {
|
||||
@include box-sizing(border-box);
|
||||
width: 100%;
|
||||
border-right: 0;
|
||||
|
||||
&.create-module {
|
||||
display: none;
|
||||
|
||||
@@ -53,3 +53,13 @@
|
||||
@extend .content-type;
|
||||
background-image: url('../img/content-types/chapter.png');
|
||||
}
|
||||
|
||||
.module a:first-child {
|
||||
@extend .content-type;
|
||||
background-image: url('../img/content-types/module.png');
|
||||
}
|
||||
|
||||
.module a:first-child {
|
||||
@extend .content-type;
|
||||
background-image: url('../img/content-types/module.png');
|
||||
}
|
||||
|
||||
80
cms/static/sass/_index.scss
Normal file
80
cms/static/sass/_index.scss
Normal file
@@ -0,0 +1,80 @@
|
||||
body.index {
|
||||
> header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
> h1 {
|
||||
font-weight: 300;
|
||||
color: lighten($dark-blue, 40%);
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
max-width: 600px;
|
||||
text-align: center;
|
||||
margin: 80px auto 30px;
|
||||
}
|
||||
|
||||
section.main-container {
|
||||
border-right: 3px;
|
||||
background: #FFF;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
@include box-sizing(border-box);
|
||||
border: 1px solid lighten( $dark-blue , 30% );
|
||||
@include border-radius(3px);
|
||||
overflow: hidden;
|
||||
@include bounce-in-animation(.8s);
|
||||
|
||||
header {
|
||||
border-bottom: 1px solid lighten($dark-blue, 50%);
|
||||
@include linear-gradient(#fff, lighten($dark-blue, 62%));
|
||||
@include clearfix();
|
||||
@include box-shadow( 0 2px 0 $light-blue, inset 0 -1px 0 #fff);
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
|
||||
h1 {
|
||||
font-size: 14px;
|
||||
padding: 8px 20px;
|
||||
float: left;
|
||||
color: $dark-blue;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
float: right;
|
||||
padding: 8px 20px;
|
||||
border-left: 1px solid lighten($dark-blue, 50%);
|
||||
@include box-shadow( inset -1px 0 0 #fff);
|
||||
font-weight: bold;
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
color: $dark-blue;
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
border-bottom: 1px solid lighten($dark-blue, 50%);
|
||||
|
||||
a {
|
||||
display: block;
|
||||
padding: 10px 20px;
|
||||
|
||||
&:hover {
|
||||
color: $dark-blue;
|
||||
background: lighten($yellow, 10%);
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
27
cms/static/sass/_keyframes.scss
Normal file
27
cms/static/sass/_keyframes.scss
Normal file
@@ -0,0 +1,27 @@
|
||||
@mixin bounce-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
@include transform(scale(.3));
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
@include transform(scale(1.05));
|
||||
}
|
||||
|
||||
100% {
|
||||
@include transform(scale(1));
|
||||
}
|
||||
}
|
||||
|
||||
@-moz-keyframes bounce-in { @include bounce-in(); }
|
||||
@-webkit-keyframes bounce-in { @include bounce-in(); }
|
||||
@-o-keyframes bounce-in { @include bounce-in(); }
|
||||
@keyframes bounce-in { @include bounce-in();}
|
||||
|
||||
@mixin bounce-in-animation($duration, $timing: ease-in-out) {
|
||||
@include animation-name(bounce-in);
|
||||
@include animation-duration($duration);
|
||||
@include animation-timing-function($timing);
|
||||
@include animation-fill-mode(both);
|
||||
}
|
||||
@@ -2,6 +2,8 @@ body {
|
||||
@include clearfix();
|
||||
height: 100%;
|
||||
font: 14px $body-font-family;
|
||||
background-color: lighten($dark-blue, 62%);
|
||||
background-image: url('/static/img/noise.png');
|
||||
|
||||
> section {
|
||||
display: table;
|
||||
@@ -11,28 +13,53 @@ body {
|
||||
|
||||
> header {
|
||||
background: $dark-blue;
|
||||
@include background-image(url('/static/img/noise.png'), linear-gradient(lighten($dark-blue, 10%), $dark-blue));
|
||||
border-bottom: 1px solid darken($dark-blue, 15%);
|
||||
@include box-shadow(inset 0 -1px 0 lighten($dark-blue, 10%));
|
||||
@include box-sizing(border-box);
|
||||
color: #fff;
|
||||
display: block;
|
||||
float: none;
|
||||
padding: 8px 25px;
|
||||
padding: 0 20px;
|
||||
text-shadow: 0 -1px 0 darken($dark-blue, 15%);
|
||||
width: 100%;
|
||||
@include box-sizing(border-box);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
nav {
|
||||
@include clearfix;
|
||||
|
||||
h2 {
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
> a {
|
||||
@include hide-text;
|
||||
background: url('/static/img/menu.png') 0 center no-repeat;
|
||||
border-right: 1px solid darken($dark-blue, 10%);
|
||||
@include box-shadow(1px 0 0 lighten($dark-blue, 10%));
|
||||
display: block;
|
||||
float: left;
|
||||
margin: 0 15px 0 0;
|
||||
height: 19px;
|
||||
padding: 8px 10px 8px 0;
|
||||
width: 14px;
|
||||
|
||||
&:hover, &:focus {
|
||||
opacity: .7;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
border-right: 1px solid darken($dark-blue, 10%);
|
||||
@include box-shadow(1px 0 0 lighten($dark-blue, 10%));
|
||||
float: left;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
text-transform: uppercase;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
a {
|
||||
color: #fff;
|
||||
padding: 8px 20px;
|
||||
display: block;
|
||||
|
||||
&:hover {
|
||||
color: rgba(#fff, .6);
|
||||
background-color: rgba(darken($dark-blue, 15%), .5);
|
||||
color: $yellow;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,21 +75,35 @@ body {
|
||||
ul {
|
||||
float: left;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@include clearfix;
|
||||
|
||||
&.user-nav {
|
||||
float: right;
|
||||
border-left: 1px solid darken($dark-blue, 10%);
|
||||
}
|
||||
|
||||
li {
|
||||
@include inline-block();
|
||||
border-right: 1px solid darken($dark-blue, 10%);
|
||||
float: left;
|
||||
@include box-shadow(1px 0 0 lighten($dark-blue, 10%));
|
||||
|
||||
a {
|
||||
padding: 8px 10px;
|
||||
padding: 8px 20px;
|
||||
display: block;
|
||||
margin: -8px 0;
|
||||
|
||||
&:hover {
|
||||
background-color: darken($dark-blue, 15%);
|
||||
background-color: rgba(darken($dark-blue, 15%), .5);
|
||||
color: $yellow;
|
||||
}
|
||||
|
||||
&.new-module {
|
||||
&:before {
|
||||
@include inline-block;
|
||||
content: "+";
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,8 +117,9 @@ body {
|
||||
@include box-sizing(border-box);
|
||||
width: flex-grid(9) + flex-gutter();
|
||||
float: left;
|
||||
@include box-shadow( -2px 0 0 darken($light-blue, 3%));
|
||||
@include box-shadow( -2px 0 0 lighten($dark-blue, 55%));
|
||||
@include transition();
|
||||
background: #FFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
section#unit-wrapper {
|
||||
section.filters {
|
||||
@include clearfix;
|
||||
display: none;
|
||||
opacity: .4;
|
||||
margin-bottom: 10px;
|
||||
@include transition;
|
||||
@@ -52,22 +53,22 @@ section#unit-wrapper {
|
||||
display: table;
|
||||
border: 1px solid lighten($dark-blue, 40%);
|
||||
width: 100%;
|
||||
@include border-radius(3px);
|
||||
@include box-shadow(0 0 4px lighten($dark-blue, 50%));
|
||||
|
||||
section {
|
||||
header {
|
||||
background: #fff;
|
||||
padding: 6px;
|
||||
border-bottom: 1px solid lighten($dark-blue, 60%);
|
||||
border-top: 1px solid lighten($dark-blue, 60%);
|
||||
margin-top: -1px;
|
||||
@include clearfix;
|
||||
|
||||
h2 {
|
||||
color: $bright-blue;
|
||||
float: left;
|
||||
font-size: 12px;
|
||||
// float: left;
|
||||
font-size: 14px;
|
||||
letter-spacing: 1px;
|
||||
line-height: 19px;
|
||||
// line-height: 20px;
|
||||
text-transform: uppercase;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -172,7 +173,6 @@ section#unit-wrapper {
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
border-bottom: 1px solid darken($light-blue, 8%);
|
||||
background: $light-blue;
|
||||
|
||||
&:last-child {
|
||||
@@ -181,6 +181,7 @@ section#unit-wrapper {
|
||||
|
||||
&.new-module a {
|
||||
background-color: darken($light-blue, 2%);
|
||||
border-bottom: 1px solid darken($light-blue, 8%);
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($yellow, 10%);
|
||||
@@ -199,6 +200,7 @@ section#unit-wrapper {
|
||||
li {
|
||||
padding: 6px;
|
||||
border-collapse: collapse;
|
||||
border-bottom: 1px solid darken($light-blue, 8%);
|
||||
position: relative;
|
||||
|
||||
&:last-child {
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
section#unit-wrapper {
|
||||
> header {
|
||||
border-bottom: 2px solid $dark-blue;
|
||||
border-bottom: 1px solid lighten($dark-blue, 50%);
|
||||
@include linear-gradient(#fff, lighten($dark-blue, 62%));
|
||||
@include clearfix();
|
||||
@include box-shadow( 0 2px 0 darken($light-blue, 3%));
|
||||
padding: 6px 20px;
|
||||
@include box-shadow( 0 2px 0 $light-blue, inset 0 -1px 0 #fff);
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
|
||||
section {
|
||||
float: left;
|
||||
padding: 10px 20px;
|
||||
|
||||
h1 {
|
||||
font-size: 16px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
font-size: 18px;
|
||||
@include inline-block();
|
||||
color: $bright-blue;
|
||||
color: $dark-blue;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -22,32 +22,41 @@ section#unit-wrapper {
|
||||
margin: 0;
|
||||
|
||||
a {
|
||||
text-indent: -9999px;
|
||||
@include inline-block();
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
float: right;
|
||||
@include clearfix;
|
||||
color: #666;
|
||||
float: right;
|
||||
padding: 6px 20px;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
@include inline-block;
|
||||
|
||||
&.cancel {
|
||||
margin-right: 20px;
|
||||
font-style: italic;
|
||||
font-size: 12px;
|
||||
}
|
||||
&.cancel {
|
||||
margin-right: 20px;
|
||||
font-style: italic;
|
||||
font-size: 12px;
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
&.save-update {
|
||||
@extend .button;
|
||||
margin: -6px -21px -6px 0;
|
||||
}
|
||||
&.save-update {
|
||||
padding: 6px 20px;
|
||||
@include border-radius(3px);
|
||||
border: 1px solid lighten($dark-blue, 40%);
|
||||
@include box-shadow(inset 0 0 0 1px #fff);
|
||||
color: $dark-blue;
|
||||
@include linear-gradient(lighten($dark-blue, 60%), lighten($dark-blue, 55%));
|
||||
|
||||
&:hover, &:focus {
|
||||
@include linear-gradient(lighten($dark-blue, 58%), lighten($dark-blue, 53%));
|
||||
@include box-shadow(inset 0 0 6px 1px #fff);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
@import 'bourbon/bourbon';
|
||||
@import 'vendor/normalize';
|
||||
@import 'keyframes';
|
||||
|
||||
@import 'base', 'layout', 'content-types';
|
||||
@import 'calendar';
|
||||
@import 'section', 'unit';
|
||||
@import 'section', 'unit', 'index';
|
||||
|
||||
@import 'module/module-styles.scss';
|
||||
|
||||
@@ -6,38 +6,35 @@
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
|
||||
% if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']:
|
||||
<%static:css group='base-style'/>
|
||||
% else:
|
||||
<link rel="stylesheet" href="${static.url('css/base-style.css')}">
|
||||
% endif
|
||||
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/markitup/skins/simple/style.css')}" />
|
||||
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/markitup/sets/wiki/style.css')}" />
|
||||
<title><%block name="title"></%block></title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
|
||||
<meta name="path_prefix" content="${MITX_ROOT_URL}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body class="<%block name='bodyclass'></%block>">
|
||||
|
||||
<%include file="widgets/header.html"/>
|
||||
<%include file="courseware_vendor_js.html"/>
|
||||
|
||||
<script type="text/javascript" src="${static.url('js/vendor/jquery.min.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/json2.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/backbone-min.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/markitup/jquery.markitup.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/markitup/sets/wiki/set.js')}"></script>
|
||||
% if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']:
|
||||
<%static:js group='main'/>
|
||||
% else:
|
||||
<script src="${ STATIC_URL }/js/main.js"></script>
|
||||
% endif
|
||||
|
||||
<%static:js group='module-js'/>
|
||||
<script src="${static.url('js/vendor/jquery.inlineedit.js')}"></script>
|
||||
<script src="${static.url('js/vendor/jquery.cookie.js')}"></script>
|
||||
<script src="${static.url('js/vendor/jquery.leanModal.min.js')}"></script>
|
||||
<script src="${static.url('js/vendor/jquery.tablednd.js')}"></script>
|
||||
<script type="text/javascript">
|
||||
document.write('\x3Cscript type="text/javascript" src="' +
|
||||
document.location.protocol + '//www.youtube.com/player_api">\x3C/script>');
|
||||
</script>
|
||||
|
||||
<%block name="content"></%block>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<%inherit file="base.html" />
|
||||
<%block name="title">Course Manager</%block>
|
||||
<%include file="widgets/header.html"/>
|
||||
|
||||
<%block name="content">
|
||||
<section class="main-container">
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
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:
|
||||
Thank you for signing up for edX! To activate your account,
|
||||
please 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 }
|
||||
http://${ site }/activate/${ key }
|
||||
% endif
|
||||
|
||||
If you didn't request this, you don't need to do anything; you won't
|
||||
|
||||
@@ -1 +1 @@
|
||||
Your account for edX's on-line ${course_title} course
|
||||
Your account for edX
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
<%inherit file="base.html" />
|
||||
<%block name="bodyclass">index</%block>
|
||||
<%block name="title">Courses</%block>
|
||||
|
||||
<%block name="content">
|
||||
<h1>edX Course Management</h1>
|
||||
|
||||
<section class="main-container">
|
||||
<header>
|
||||
<h1>Courses</h1>
|
||||
<a href="#" class="wip">+</a>
|
||||
</header>
|
||||
|
||||
<ol>
|
||||
%for course, url in courses:
|
||||
<li><a href="${url}">${course}</a></li>
|
||||
|
||||
30
cms/templates/registration/activation_complete.html
Normal file
30
cms/templates/registration/activation_complete.html
Normal file
@@ -0,0 +1,30 @@
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<%inherit file="../base.html" />
|
||||
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
|
||||
<section class="container activation">
|
||||
|
||||
<section class="message">
|
||||
%if not already_active:
|
||||
<h1 class="valid">Activation Complete!</h1>
|
||||
%else:
|
||||
<h1>Account already active!</h1>
|
||||
%endif
|
||||
<hr class="horizontal-divider">
|
||||
|
||||
<p>
|
||||
%if not already_active:
|
||||
Thanks for activating your account.
|
||||
%else:
|
||||
This account has already been activated.
|
||||
%endif
|
||||
|
||||
%if user_logged_in:
|
||||
Visit your <a href="${reverse('dashboard')}">dashboard</a> to see your courses.
|
||||
%else:
|
||||
You can now <a href="#login-modal" rel="leanModal">login</a>.
|
||||
%endif
|
||||
</p>
|
||||
</section>
|
||||
</section>
|
||||
@@ -10,10 +10,10 @@
|
||||
<hr>
|
||||
</header>
|
||||
|
||||
<div id="enroll">
|
||||
<div id="register">
|
||||
|
||||
<form id="enroll_form" method="post">
|
||||
<div id="enroll_error" name="enroll_error"></div>
|
||||
<form id="register_form" method="post">
|
||||
<div id="register_error" name="register_error"></div>
|
||||
<label>E-mail</label>
|
||||
<input name="email" type="email" placeholder="E-mail">
|
||||
<label>Password</label>
|
||||
@@ -64,17 +64,17 @@
|
||||
});
|
||||
}
|
||||
|
||||
$('form#enroll_form').submit(function(e) {
|
||||
$('form#register_form').submit(function(e) {
|
||||
e.preventDefault();
|
||||
var submit_data = $('#enroll_form').serialize();
|
||||
var submit_data = $('#register_form').serialize();
|
||||
|
||||
postJSON('/create_account',
|
||||
submit_data,
|
||||
function(json) {
|
||||
if(json.success) {
|
||||
$('#enroll').html(json.value);
|
||||
$('#register').html(json.value);
|
||||
} else {
|
||||
$('#enroll_error').html(json.value).stop().css("background-color", "#933").animate({ backgroundColor: "#333"}, 2000);
|
||||
$('#register_error').html(json.value).stop().css("background-color", "#933").animate({ backgroundColor: "#333"}, 2000);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<section id="unit-wrapper" class="${js_module}">
|
||||
<section id="unit-wrapper">
|
||||
<header>
|
||||
<section>
|
||||
<h1 class="editable">${name}</h1>
|
||||
@@ -12,6 +12,47 @@
|
||||
</header>
|
||||
|
||||
<section>
|
||||
<section class="meta wip">
|
||||
<section class="status-settings">
|
||||
<ul>
|
||||
<li><a href="#" class="current">Scrap</a></li>
|
||||
<li><a href="#">Draft</a></li>
|
||||
<li><a href="#">Proofed</a></li>
|
||||
<li><a href="#">Published</a></li>
|
||||
</ul>
|
||||
<a href="#" class="settings">Settings</a>
|
||||
</section>
|
||||
<section class="author">
|
||||
<dl>
|
||||
<dt>Last modified:</dt>
|
||||
<dd>mm/dd/yy</dd>
|
||||
<dt>By</dt>
|
||||
<dd>Anant Agarwal</dd>
|
||||
</dl>
|
||||
</section>
|
||||
<section class="tags">
|
||||
<div>
|
||||
<h2>Tags:</h2>
|
||||
<p class="editable">Click to edit</p>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Goal</h2>
|
||||
<p class="editable">Click to edit</p>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
${contents}
|
||||
<section class="previews">
|
||||
% for preview in previews:
|
||||
<section class="preview">
|
||||
${preview}
|
||||
</section>
|
||||
% endfor
|
||||
</section>
|
||||
<div class="actions wip">
|
||||
<a href="" class="save-update">Save & Update</a>
|
||||
<a href="#" class="cancel">Cancel</a>
|
||||
</div>
|
||||
<%include file="widgets/notes.html"/>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<header>
|
||||
<nav>
|
||||
<h2><a href="/">edX CMS: TODO:-course-name-here</a></h2>
|
||||
<a href="/">Home</a>
|
||||
<h2><a href="#">edX CMS: TODO:-course-name-here</a></h2>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="#" class="new-module wip">New Module</a>
|
||||
<a href="#" class="new-module wip">Module</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="new-module wip">New Unit</a>
|
||||
<a href="#" class="new-module wip">Unit</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
@@ -1,45 +1,3 @@
|
||||
<section class="html-edit">
|
||||
<section class="meta wip">
|
||||
|
||||
<section class="status-settings">
|
||||
<ul>
|
||||
<li><a href="#" class="current">Scrap</a></li>
|
||||
<li><a href="#">Draft</a></li>
|
||||
<li><a href="#">Proofed</a></li>
|
||||
<li><a href="#">Published</a></li>
|
||||
</ul>
|
||||
<a href="#" class="settings">Settings</a>
|
||||
</section>
|
||||
|
||||
<section class="author">
|
||||
<dl>
|
||||
<dt>Last modified:</dt>
|
||||
<dd>mm/dd/yy</dd>
|
||||
<dt>By</dt>
|
||||
<dd>Anant Agarwal</dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="tags">
|
||||
<div>
|
||||
<h2>Tags:</h2>
|
||||
<p class="editable">Click to edit</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Goal</h2>
|
||||
<p class="editable">Click to edit</p>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<textarea name="" class="edit-box" rows="8" cols="40">${data}</textarea>
|
||||
<div class="preview">${data}</div>
|
||||
|
||||
<div class="actions">
|
||||
<a href="" class="save-update">Save & Update</a>
|
||||
<a href="#" class="cancel">Cancel</a>
|
||||
</div>
|
||||
|
||||
<%include file="notes.html"/>
|
||||
</section>
|
||||
|
||||
@@ -55,7 +55,11 @@
|
||||
|
||||
<ul class="modules">
|
||||
% for module in week.get_children():
|
||||
<li class="module" data-id="${module.location.url()}" data-type="${module.js_module_name()}">
|
||||
<li class="module"
|
||||
data-id="${module.location.url()}"
|
||||
data-type="${module.js_module_name}"
|
||||
data-preview-type="${module.module_class.js_module_name}">
|
||||
|
||||
<a href="#" class="module-edit">${module.name}</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
<section class="problem-edit">
|
||||
<section class="meta">
|
||||
<section class="status-settings">
|
||||
<ul>
|
||||
<li><a href="#" class="current">Scrap</a></li>
|
||||
<li><a href="#">Draft</a></li>
|
||||
<li><a href="#">Proofed</a></li>
|
||||
<li><a href="#">Published</a></li>
|
||||
</ul>
|
||||
<a href="#" class="settings">Settings</a>
|
||||
</section>
|
||||
|
||||
<section class="author">
|
||||
<dl>
|
||||
<dt>Last modified:</dt>
|
||||
<dd>mm/dd/yy</dd>
|
||||
<dt>By</dt>
|
||||
<dd>Anant Agarwal</dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="tags">
|
||||
<div>
|
||||
<h2>Tags:</h2>
|
||||
<p class="editable">Click to edit</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Goal</h2>
|
||||
<p class="editable">Click to edit</p>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<textarea name="" id= rows="8" cols="40">Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.</textarea>
|
||||
<div class="preview">
|
||||
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<a href="#" class="cancel">Cancel</a>
|
||||
<a href="" class="save-update">Save & Update</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<%include file="notes.html"/>
|
||||
</section>
|
||||
@@ -1,51 +0,0 @@
|
||||
<section class="problem-new">
|
||||
<header>
|
||||
<a href="#" class="cancel">Cancel</a>
|
||||
<a href="#" class="save-update">Save & Update</a>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
<header>
|
||||
<h1 class="editable">New Problem</h1>
|
||||
|
||||
<section class="status-settings">
|
||||
<ul>
|
||||
<li><a href="#" class="current">Scrap</a></li>
|
||||
<li><a href="#">Draft</a></li>
|
||||
<li><a href="#">Proofed</a></li>
|
||||
<li><a href="#">Published</a></li>
|
||||
</ul>
|
||||
<a href="#" class="settings">Settings</a>
|
||||
|
||||
<select name="" id="">
|
||||
<option>Global</option>
|
||||
</select>
|
||||
</section>
|
||||
<section class="meta">
|
||||
<div>
|
||||
<h2>Tags:</h2>
|
||||
<p class="editable">Click to edit</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Goal</h2>
|
||||
<p class="editable">Click to edit</p>
|
||||
</div>
|
||||
</section>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
<textarea name="" id= rows="8" cols="40"></textarea>
|
||||
<div class="preview">
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="notes">
|
||||
<h2>Add notes</h2>
|
||||
<textarea name="" id= rows="8" cols="40"></textarea>
|
||||
<input type="submit" name="" id="" value="post" />
|
||||
</section>
|
||||
|
||||
<a href="" class="save-update">Save & Update</a>
|
||||
</section>
|
||||
</section>
|
||||
@@ -1,45 +1,3 @@
|
||||
<section class="raw-edit">
|
||||
<section class="meta wip">
|
||||
|
||||
<section class="status-settings">
|
||||
<ul>
|
||||
<li><a href="#" class="current">Scrap</a></li>
|
||||
<li><a href="#">Draft</a></li>
|
||||
<li><a href="#">Proofed</a></li>
|
||||
<li><a href="#">Published</a></li>
|
||||
</ul>
|
||||
<a href="#" class="settings">Settings</a>
|
||||
</section>
|
||||
|
||||
<section class="author">
|
||||
<dl>
|
||||
<dt>Last modified:</dt>
|
||||
<dd>mm/dd/yy</dd>
|
||||
<dt>By</dt>
|
||||
<dd>Anant Agarwal</dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="tags">
|
||||
<div>
|
||||
<h2>Tags:</h2>
|
||||
<p class="editable">Click to edit</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Goal</h2>
|
||||
<p class="editable">Click to edit</p>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<textarea name="" class="edit-box" rows="8" cols="40">${data}</textarea>
|
||||
<pre class="preview">${data | h}</pre>
|
||||
|
||||
<div class="actions wip">
|
||||
<a href="" class="save-update">Save & Update</a>
|
||||
<a href="#" class="cancel">Cancel</a>
|
||||
</div>
|
||||
|
||||
<%include file="notes.html"/>
|
||||
<textarea name="" class="edit-box" rows="8" cols="40">${data | h}</textarea>
|
||||
</section>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<li>
|
||||
<img src="http://placehold.it/300x180" alt="" /><h5>Video-file-name</h5>
|
||||
</li>
|
||||
@@ -36,7 +36,10 @@
|
||||
<ol>
|
||||
% for child in module.get_children():
|
||||
<li class="${module.category}">
|
||||
<a href="#" class="module-edit" data-id="${child.location.url()}" data-type="${child.js_module_name()}">${child.name}</a>
|
||||
<a href="#" class="module-edit"
|
||||
data-id="${child.location.url()}"
|
||||
data-type="${child.js_module_name}"
|
||||
data-preview-type="${child.module_class.js_module_name}">${child.name}</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
%endfor
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
<section class="sequence-edit">
|
||||
<header>
|
||||
<div class="week">
|
||||
<h2><a href="">Week 1</a></h2>
|
||||
<ul>
|
||||
<li>
|
||||
<p class="editable"><strong>Goal title:</strong> This is the goal body and is where the goal will be further explained</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="editable">Lecture sequence</h1>
|
||||
<p><strong>Group type:</strong> Ordered Sequence</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="content">
|
||||
<section class="filters">
|
||||
<ul>
|
||||
<li>
|
||||
<label for="">Sort by</label>
|
||||
<select>
|
||||
<option value="">Recently Modified</option>
|
||||
</select>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<label for="">Display</label>
|
||||
<select>
|
||||
<option value="">All content</option>
|
||||
</select>
|
||||
</li>
|
||||
<li>
|
||||
<select>
|
||||
<option value="">Internal Only</option>
|
||||
</select>
|
||||
</li>
|
||||
|
||||
<li class="advanced">
|
||||
<a href="#">Advanced filters</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<input type="search" name="" id="" value="" />
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<div>
|
||||
<section class="modules">
|
||||
<ol>
|
||||
<li>
|
||||
<ol>
|
||||
<li>
|
||||
<a href="" class="problem-edit">Problem title 11</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="sequence-edit">Problem Group</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="problem-edit">Problem title 14</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="video-edit">Video 3</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
<li class="group">
|
||||
<header>
|
||||
<h3>
|
||||
<a href="#" class="problem-edit">Problem group</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</h3>
|
||||
</header>
|
||||
<ol>
|
||||
<li>
|
||||
<a href="#" class="problem-edit">Problem title 11</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="problem-edit">Problem title 11</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="problem-edit">Problem title 11</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
</ol>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="problem-edit">Problem title 13</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="problem-edit">Problem title 14</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="video-edit">Video 3</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="" class="problem-edit">Problem title 11</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="sequence-edit">Problem Group</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="problem-edit">Problem title 14</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="video-edit">Video 3</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
</ol>
|
||||
</li>
|
||||
|
||||
<!-- <li class="new-module"> -->
|
||||
<!-- <%include file="new-module.html"/> -->
|
||||
<!-- </li> -->
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section class="scratch-pad">
|
||||
<ol>
|
||||
<li>
|
||||
<header>
|
||||
<h2>Section Scratch</h2>
|
||||
</header>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="#" class="problem-edit">Problem title 11</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="problem-edit">Problem title 13 </a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="problem-edit"> Problem title 14</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="" class="video-edit">Video 3</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<header>
|
||||
<h2>Course Scratch</h2>
|
||||
</header>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<a href="#" class="problem-edit">Problem title 11</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="problem-edit">Problem title 13 </a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="problem-edit"> Problem title 14</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="" class="video-edit">Video 3</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<!-- <li class="new-module"> -->
|
||||
<!-- <%include file="new-module.html"/> -->
|
||||
<!-- </li> -->
|
||||
</ol>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
@@ -14,6 +14,8 @@ urlpatterns = ('',
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)$',
|
||||
'contentstore.views.course_index', name='course_index'),
|
||||
url(r'^github_service_hook$', 'github_sync.views.github_post_receive'),
|
||||
url(r'^preview/modx/(?P<preview_id>[^/]*)/(?P<location>.*?)/(?P<dispatch>[^/]*)$',
|
||||
'contentstore.views.preview_dispatch', name='preview_dispatch')
|
||||
)
|
||||
|
||||
# User creation and updating views
|
||||
|
||||
@@ -13,6 +13,7 @@ from django.db import DEFAULT_DB_ALIAS
|
||||
|
||||
from . import app_settings
|
||||
|
||||
|
||||
def get_instance(model, instance_or_pk, timeout=None, using=None):
|
||||
"""
|
||||
Returns the ``model`` instance with a primary key of ``instance_or_pk``.
|
||||
@@ -87,6 +88,7 @@ def get_instance(model, instance_or_pk, timeout=None, using=None):
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
def delete_instance(model, *instance_or_pk):
|
||||
"""
|
||||
Purges the cache keys for the instances of this model.
|
||||
@@ -94,6 +96,7 @@ def delete_instance(model, *instance_or_pk):
|
||||
|
||||
cache.delete_many([instance_key(model, x) for x in instance_or_pk])
|
||||
|
||||
|
||||
def instance_key(model, instance_or_pk):
|
||||
"""
|
||||
Returns the cache key for this (model, instance) pair.
|
||||
|
||||
@@ -84,6 +84,7 @@ from django.contrib.auth.middleware import AuthenticationMiddleware
|
||||
|
||||
from .model import cache_model
|
||||
|
||||
|
||||
class CacheBackedAuthenticationMiddleware(AuthenticationMiddleware):
|
||||
def __init__(self):
|
||||
cache_model(User)
|
||||
|
||||
@@ -58,6 +58,7 @@ from django.db.models.signals import post_save, post_delete
|
||||
|
||||
from .core import get_instance, delete_instance
|
||||
|
||||
|
||||
def cache_model(model, timeout=None):
|
||||
if hasattr(model, 'get_cached'):
|
||||
# Already patched
|
||||
|
||||
@@ -74,6 +74,7 @@ from django.db.models.signals import post_save, post_delete
|
||||
|
||||
from .core import get_instance, delete_instance
|
||||
|
||||
|
||||
def cache_relation(descriptor, timeout=None):
|
||||
rel = descriptor.related
|
||||
related_name = '%s_cache' % rel.field.related_query_name()
|
||||
|
||||
@@ -5,6 +5,7 @@ from django.template import resolve_variable
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
class CacheNode(Node):
|
||||
def __init__(self, nodelist, expire_time, key):
|
||||
self.nodelist = nodelist
|
||||
@@ -21,6 +22,7 @@ class CacheNode(Node):
|
||||
cache.set(key, value, expire_time)
|
||||
return value
|
||||
|
||||
|
||||
@register.tag
|
||||
def cachedeterministic(parser, token):
|
||||
"""
|
||||
@@ -42,6 +44,7 @@ def cachedeterministic(parser, token):
|
||||
raise TemplateSyntaxError(u"'%r' tag requires 2 arguments." % tokens[0])
|
||||
return CacheNode(nodelist, tokens[1], tokens[2])
|
||||
|
||||
|
||||
class ShowIfCachedNode(Node):
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
@@ -50,6 +53,7 @@ class ShowIfCachedNode(Node):
|
||||
key = resolve_variable(self.key, context)
|
||||
return cache.get(key) or ''
|
||||
|
||||
|
||||
@register.tag
|
||||
def showifcached(parser, token):
|
||||
"""
|
||||
|
||||
@@ -60,6 +60,7 @@ def csrf_response_exempt(view_func):
|
||||
PendingDeprecationWarning)
|
||||
return view_func
|
||||
|
||||
|
||||
def csrf_view_exempt(view_func):
|
||||
"""
|
||||
Marks a view function as being exempt from CSRF view protection.
|
||||
@@ -68,6 +69,7 @@ def csrf_view_exempt(view_func):
|
||||
PendingDeprecationWarning)
|
||||
return csrf_exempt(view_func)
|
||||
|
||||
|
||||
def csrf_exempt(view_func):
|
||||
"""
|
||||
Marks a view function as being exempt from the CSRF view protection.
|
||||
|
||||
@@ -6,6 +6,7 @@ from pipeline.conf import settings
|
||||
from pipeline.packager import Packager
|
||||
from pipeline.utils import guess_type
|
||||
|
||||
|
||||
def compressed_css(package_name):
|
||||
package = settings.PIPELINE_CSS.get(package_name, {})
|
||||
if package:
|
||||
@@ -20,6 +21,7 @@ def compressed_css(package_name):
|
||||
paths = packager.compile(package.paths)
|
||||
return render_individual_css(package, paths)
|
||||
|
||||
|
||||
def render_css(package, path):
|
||||
template_name = package.template_name or "mako/css.html"
|
||||
context = package.extra_context
|
||||
@@ -29,6 +31,7 @@ def render_css(package, path):
|
||||
})
|
||||
return render_to_string(template_name, context)
|
||||
|
||||
|
||||
def render_individual_css(package, paths):
|
||||
tags = [render_css(package, path) for path in paths]
|
||||
return '\n'.join(tags)
|
||||
@@ -49,6 +52,7 @@ def compressed_js(package_name):
|
||||
templates = packager.pack_templates(package)
|
||||
return render_individual_js(package, paths, templates)
|
||||
|
||||
|
||||
def render_js(package, path):
|
||||
template_name = package.template_name or "mako/js.html"
|
||||
context = package.extra_context
|
||||
@@ -58,6 +62,7 @@ def render_js(package, path):
|
||||
})
|
||||
return render_to_string(template_name, context)
|
||||
|
||||
|
||||
def render_inline_js(package, js):
|
||||
context = package.extra_context
|
||||
context.update({
|
||||
@@ -65,6 +70,7 @@ def render_inline_js(package, js):
|
||||
})
|
||||
return render_to_string("mako/inline_js.html", context)
|
||||
|
||||
|
||||
def render_individual_js(package, paths, templates=None):
|
||||
tags = [render_js(package, js) for js in paths]
|
||||
if templates:
|
||||
|
||||
@@ -1,10 +1,25 @@
|
||||
<%!
|
||||
from staticfiles.storage import staticfiles_storage
|
||||
from pipeline_mako import compressed_css, compressed_js
|
||||
from static_replace import replace_urls
|
||||
%>
|
||||
|
||||
<%def name='url(file)'>${staticfiles_storage.url(file)}</%def>
|
||||
<%def name='css(group)'>${compressed_css(group)}</%def>
|
||||
<%def name='js(group)'>${compressed_js(group)}</%def>
|
||||
<%def name='replace_urls(text)'>${replace_urls(text)}</%def>
|
||||
|
||||
<%def name='css(group)'>
|
||||
% if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']:
|
||||
${compressed_css(group)}
|
||||
% else:
|
||||
% for filename in settings.PIPELINE_CSS[group]['source_filenames']:
|
||||
<link rel="stylesheet" href="${staticfiles_storage.url(filename.replace('.scss', '.css'))}" type="text/css" media="all" / >
|
||||
% endfor
|
||||
%endif
|
||||
</%def>
|
||||
<%def name='js(group)'>
|
||||
% if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']:
|
||||
${compressed_js(group)}
|
||||
% else:
|
||||
% for filename in settings.PIPELINE_JS[group]['source_filenames']:
|
||||
<script type="text/javascript" src="${staticfiles_storage.url(filename.replace('.coffee', '.js'))}"></script>
|
||||
% endfor
|
||||
%endif
|
||||
</%def>
|
||||
|
||||
@@ -15,4 +15,3 @@ admin.site.register(CourseEnrollment)
|
||||
admin.site.register(Registration)
|
||||
|
||||
admin.site.register(PendingNameChange)
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
##
|
||||
## One-off script to export 6.002x users into the edX framework
|
||||
##
|
||||
## Could be modified to be general by:
|
||||
## * Changing user_keys and up_keys to handle dates more cleanly
|
||||
## * Providing a generic set of tables, rather than just users and user profiles
|
||||
## * Handling certificates and grades
|
||||
## * Handling merge/forks of UserProfile.meta
|
||||
|
||||
|
||||
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
|
||||
|
||||
import mitxmako.middleware as middleware
|
||||
|
||||
middleware.MakoMiddleware()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = \
|
||||
'''Exports all users and user profiles.
|
||||
Caveat: Should be looked over before any run
|
||||
for schema changes.
|
||||
|
||||
Current version grabs user_keys from
|
||||
django.contrib.auth.models.User and up_keys
|
||||
from student.userprofile. '''
|
||||
|
||||
def handle(self, *args, **options):
|
||||
users = list(User.objects.all())
|
||||
user_profiles = list(UserProfile.objects.all())
|
||||
user_profile_dict = dict([(up.user_id, up) for up in user_profiles])
|
||||
|
||||
user_tuples = [(user_profile_dict[u.id], u) for u in users if u.id in user_profile_dict]
|
||||
|
||||
user_keys = ['id', 'username', 'email', 'password', 'is_staff',
|
||||
'is_active', 'is_superuser', 'last_login', 'date_joined',
|
||||
'password']
|
||||
up_keys = ['language', 'location', 'meta', 'name', 'id', 'user_id']
|
||||
|
||||
def extract_dict(keys, object):
|
||||
d = {}
|
||||
for key in keys:
|
||||
item = object.__getattribute__(key)
|
||||
if type(item) == datetime.datetime:
|
||||
item = item.isoformat()
|
||||
d[key] = item
|
||||
return d
|
||||
|
||||
extracted = [{'up':extract_dict(up_keys, t[0]), 'u':extract_dict(user_keys, t[1])} for t in user_tuples]
|
||||
fp = open('transfer_users.txt', 'w')
|
||||
json.dump(extracted, fp)
|
||||
fp.close()
|
||||
@@ -0,0 +1,68 @@
|
||||
##
|
||||
## One-off script to import 6.002x users into the edX framework
|
||||
## 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
|
||||
|
||||
import mitxmako.middleware as middleware
|
||||
|
||||
middleware.MakoMiddleware()
|
||||
|
||||
|
||||
def import_user(u):
|
||||
user_info = u['u']
|
||||
up_info = u['up']
|
||||
|
||||
# HACK to handle dates
|
||||
user_info['last_login'] = dateutil.parser.parse(user_info['last_login'])
|
||||
user_info['date_joined'] = dateutil.parser.parse(user_info['date_joined'])
|
||||
|
||||
user_keys = ['id', 'username', 'email', 'password', 'is_staff',
|
||||
'is_active', 'is_superuser', 'last_login', 'date_joined',
|
||||
'password']
|
||||
up_keys = ['language', 'location', 'meta', 'name', 'id', 'user_id']
|
||||
|
||||
u = User()
|
||||
for key in user_keys:
|
||||
u.__setattr__(key, user_info[key])
|
||||
u.save()
|
||||
|
||||
up = UserProfile()
|
||||
up.user = u
|
||||
for key in up_keys:
|
||||
up.__setattr__(key, up_info[key])
|
||||
up.save()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = \
|
||||
'''Exports all users and user profiles.
|
||||
Caveat: Should be looked over before any run
|
||||
for schema changes.
|
||||
|
||||
Current version grabs user_keys from
|
||||
django.contrib.auth.models.User and up_keys
|
||||
from student.userprofile. '''
|
||||
|
||||
def handle(self, *args, **options):
|
||||
extracted = json.load(open('transfer_users.txt'))
|
||||
n = 0
|
||||
for u in extracted:
|
||||
import_user(u)
|
||||
if n % 100 == 0:
|
||||
print n
|
||||
n = n + 1
|
||||
@@ -17,29 +17,32 @@ import json
|
||||
|
||||
middleware.MakoMiddleware()
|
||||
|
||||
|
||||
def group_from_value(groups, v):
|
||||
''' Given group: (('a',0.3),('b',0.4),('c',0.3)) And random value
|
||||
in [0,1], return the associated group (in the above case, return
|
||||
'a' if v<0.3, 'b' if 0.3<=v<0.7, and 'c' if v>0.7
|
||||
'''
|
||||
sum = 0
|
||||
for (g,p) in groups:
|
||||
for (g, p) in groups:
|
||||
sum = sum + p
|
||||
if sum > v:
|
||||
return g
|
||||
return g # For round-off errors
|
||||
return g # For round-off errors
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = \
|
||||
''' Assign users to test groups. Takes a list
|
||||
of groups:
|
||||
of groups:
|
||||
a:0.3,b:0.4,c:0.3 file.txt "Testing something"
|
||||
Will assign each user to group a, b, or c with
|
||||
probability 0.3, 0.4, 0.3. Probabilities must
|
||||
add up to 1.
|
||||
probability 0.3, 0.4, 0.3. Probabilities must
|
||||
add up to 1.
|
||||
|
||||
Will log what happened to file.txt.
|
||||
'''
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if len(args) != 3:
|
||||
print "Invalid number of options"
|
||||
@@ -47,13 +50,13 @@ Will log what happened to file.txt.
|
||||
|
||||
# Extract groups from string
|
||||
group_strs = [x.split(':') for x in args[0].split(',')]
|
||||
groups = [(group,float(value)) for group,value in group_strs]
|
||||
groups = [(group, float(value)) for group, value in group_strs]
|
||||
print "Groups", groups
|
||||
|
||||
## Confirm group probabilities add up to 1
|
||||
total = sum(zip(*groups)[1])
|
||||
print "Total:", total
|
||||
if abs(total-1)>0.01:
|
||||
if abs(total - 1) > 0.01:
|
||||
print "Total not 1"
|
||||
sys.exit(-1)
|
||||
|
||||
@@ -65,15 +68,15 @@ Will log what happened to file.txt.
|
||||
|
||||
group_objects = {}
|
||||
|
||||
f = open(args[1],"a+")
|
||||
f = open(args[1], "a+")
|
||||
|
||||
## Create groups
|
||||
for group in dict(groups):
|
||||
utg = UserTestGroup()
|
||||
utg.name=group
|
||||
utg.description = json.dumps({"description":args[2]},
|
||||
{"time":datetime.datetime.utcnow().isoformat()})
|
||||
group_objects[group]=utg
|
||||
utg.name = group
|
||||
utg.description = json.dumps({"description": args[2]},
|
||||
{"time": datetime.datetime.utcnow().isoformat()})
|
||||
group_objects[group] = utg
|
||||
group_objects[group].save()
|
||||
|
||||
## Assign groups
|
||||
@@ -83,11 +86,11 @@ Will log what happened to file.txt.
|
||||
if count % 1000 == 0:
|
||||
print count
|
||||
count = count + 1
|
||||
v = random.uniform(0,1)
|
||||
group = group_from_value(groups,v)
|
||||
v = random.uniform(0, 1)
|
||||
group = group_from_value(groups, v)
|
||||
group_objects[group].users.add(user)
|
||||
f.write("Assigned user {name} ({id}) to {group}\n".format(name=user.username,
|
||||
id=user.id,
|
||||
f.write("Assigned user {name} ({id}) to {group}\n".format(name=user.username,
|
||||
id=user.id,
|
||||
group=group))
|
||||
|
||||
## Save groups
|
||||
|
||||
@@ -10,9 +10,11 @@ import mitxmako.middleware as middleware
|
||||
|
||||
middleware.MakoMiddleware()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = \
|
||||
''' Extract an e-mail list of all active students. '''
|
||||
|
||||
def handle(self, *args, **options):
|
||||
#text = open(args[0]).read()
|
||||
#subject = open(args[1]).read()
|
||||
|
||||
@@ -10,18 +10,20 @@ import mitxmako.middleware as middleware
|
||||
|
||||
middleware.MakoMiddleware()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = \
|
||||
'''Sends an e-mail to all users. Takes a single
|
||||
'''Sends an e-mail to all users. Takes a single
|
||||
parameter -- name of e-mail template -- located
|
||||
in templates/email. Adds a .txt for the message
|
||||
body, and an _subject.txt for the subject. '''
|
||||
|
||||
def handle(self, *args, **options):
|
||||
#text = open(args[0]).read()
|
||||
#subject = open(args[1]).read()
|
||||
users = User.objects.all()
|
||||
text = middleware.lookup['main'].get_template('email/'+args[0]+".txt").render()
|
||||
subject = middleware.lookup['main'].get_template('email/'+args[0]+"_subject.txt").render().strip()
|
||||
text = middleware.lookup['main'].get_template('email/' + args[0] + ".txt").render()
|
||||
subject = middleware.lookup['main'].get_template('email/' + args[0] + "_subject.txt").render().strip()
|
||||
for user in users:
|
||||
if user.is_active:
|
||||
user.email_user(subject, text)
|
||||
|
||||
@@ -16,16 +16,18 @@ import datetime
|
||||
|
||||
middleware.MakoMiddleware()
|
||||
|
||||
|
||||
def chunks(l, n):
|
||||
""" Yield successive n-sized chunks from l.
|
||||
"""
|
||||
for i in xrange(0, len(l), n):
|
||||
yield l[i:i+n]
|
||||
yield l[i:i + n]
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = \
|
||||
'''Sends an e-mail to all users in a text file.
|
||||
E.g.
|
||||
'''Sends an e-mail to all users in a text file.
|
||||
E.g.
|
||||
manage.py userlist.txt message logfile.txt rate
|
||||
userlist.txt -- list of all users
|
||||
message -- prefix for template with message
|
||||
@@ -35,28 +37,28 @@ rate -- messages per second
|
||||
log_file = None
|
||||
|
||||
def hard_log(self, text):
|
||||
self.log_file.write(datetime.datetime.utcnow().isoformat()+' -- '+text+'\n')
|
||||
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()]
|
||||
|
||||
message = middleware.lookup['main'].get_template('emails/'+message_base+"_body.txt").render()
|
||||
subject = middleware.lookup['main'].get_template('emails/'+message_base+"_subject.txt").render().strip()
|
||||
message = middleware.lookup['main'].get_template('emails/' + message_base + "_body.txt").render()
|
||||
subject = middleware.lookup['main'].get_template('emails/' + message_base + "_subject.txt").render().strip()
|
||||
rate = int(ratestr)
|
||||
|
||||
self.log_file = open(logfilename, "a+", buffering = 0)
|
||||
|
||||
i=0
|
||||
self.log_file = open(logfilename, "a+", buffering=0)
|
||||
|
||||
i = 0
|
||||
for users in chunks(users, rate):
|
||||
emails = [ (subject, message, settings.DEFAULT_FROM_EMAIL, [u]) for u in users ]
|
||||
emails = [(subject, message, settings.DEFAULT_FROM_EMAIL, [u]) for u in users]
|
||||
self.hard_log(" ".join(users))
|
||||
send_mass_mail( emails, fail_silently = False )
|
||||
send_mass_mail(emails, fail_silently=False)
|
||||
time.sleep(1)
|
||||
print datetime.datetime.utcnow().isoformat(), i
|
||||
i = i+len(users)
|
||||
i = i + len(users)
|
||||
# Emergency interruptor
|
||||
if os.path.exists("/tmp/stopemails.txt"):
|
||||
self.log_file.close()
|
||||
|
||||
@@ -13,26 +13,28 @@ from student.models import UserProfile
|
||||
|
||||
middleware.MakoMiddleware()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = \
|
||||
''' Extract full user information into a JSON file.
|
||||
''' Extract full user information into a JSON file.
|
||||
Pass a single filename.'''
|
||||
|
||||
def handle(self, *args, **options):
|
||||
f = open(args[0],'w')
|
||||
f = open(args[0], 'w')
|
||||
#text = open(args[0]).read()
|
||||
#subject = open(args[1]).read()
|
||||
users = User.objects.all()
|
||||
|
||||
l = []
|
||||
for user in users:
|
||||
up = UserProfile.objects.get(user = user)
|
||||
d = { 'username':user.username,
|
||||
'email':user.email,
|
||||
'is_active':user.is_active,
|
||||
'joined':user.date_joined.isoformat(),
|
||||
'name':up.name,
|
||||
'language':up.language,
|
||||
'location':up.location}
|
||||
up = UserProfile.objects.get(user=user)
|
||||
d = {'username': user.username,
|
||||
'email': user.email,
|
||||
'is_active': user.is_active,
|
||||
'joined': user.date_joined.isoformat(),
|
||||
'name': up.name,
|
||||
'language': up.language,
|
||||
'location': up.location}
|
||||
l.append(d)
|
||||
json.dump(l,f)
|
||||
json.dump(l, f)
|
||||
f.close()
|
||||
|
||||
@@ -4,10 +4,11 @@ from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
|
||||
|
||||
# Adding model 'UserProfile'
|
||||
db.create_table('auth_userprofile', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
@@ -28,16 +29,14 @@ class Migration(SchemaMigration):
|
||||
))
|
||||
db.send_create_signal('student', ['Registration'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
|
||||
# Deleting model 'UserProfile'
|
||||
db.delete_table('auth_userprofile')
|
||||
|
||||
# Deleting model 'Registration'
|
||||
db.delete_table('auth_registration')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
|
||||
@@ -4,10 +4,11 @@ from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
|
||||
|
||||
# Changing field 'UserProfile.name'
|
||||
db.alter_column('auth_userprofile', 'name', self.gf('django.db.models.fields.CharField')(max_length=255))
|
||||
|
||||
@@ -32,9 +33,8 @@ class Migration(SchemaMigration):
|
||||
# Adding index on 'UserProfile', fields ['location']
|
||||
db.create_index('auth_userprofile', ['location'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
|
||||
# Removing index on 'UserProfile', fields ['location']
|
||||
db.delete_index('auth_userprofile', ['location'])
|
||||
|
||||
@@ -59,7 +59,6 @@ class Migration(SchemaMigration):
|
||||
# Changing field 'UserProfile.location'
|
||||
db.alter_column('auth_userprofile', 'location', self.gf('django.db.models.fields.TextField')())
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
|
||||
@@ -4,10 +4,11 @@ from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
|
||||
|
||||
# Adding model 'UserTestGroup'
|
||||
db.create_table('student_usertestgroup', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
@@ -24,16 +25,14 @@ class Migration(SchemaMigration):
|
||||
))
|
||||
db.create_unique('student_usertestgroup_users', ['usertestgroup_id', 'user_id'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
|
||||
# Deleting model 'UserTestGroup'
|
||||
db.delete_table('student_usertestgroup')
|
||||
|
||||
# Removing M2M table for field users on 'UserTestGroup'
|
||||
db.delete_table('student_usertestgroup_users')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
|
||||
@@ -4,18 +4,17 @@ from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
db.execute("create unique index email on auth_user (email)")
|
||||
pass
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
db.execute("drop index email on auth_user")
|
||||
pass
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
|
||||
@@ -4,10 +4,11 @@ from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
|
||||
|
||||
# Adding model 'PendingEmailChange'
|
||||
db.create_table('student_pendingemailchange', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
@@ -29,9 +30,8 @@ class Migration(SchemaMigration):
|
||||
# Changing field 'UserProfile.user'
|
||||
db.alter_column('auth_userprofile', 'user_id', self.gf('django.db.models.fields.related.OneToOneField')(unique=True, to=orm['auth.User']))
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
|
||||
# Deleting model 'PendingEmailChange'
|
||||
db.delete_table('student_pendingemailchange')
|
||||
|
||||
@@ -41,7 +41,6 @@ class Migration(SchemaMigration):
|
||||
# Changing field 'UserProfile.user'
|
||||
db.alter_column('auth_userprofile', 'user_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], unique=True))
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
|
||||
@@ -4,20 +4,19 @@ from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
|
||||
|
||||
# Changing field 'UserProfile.meta'
|
||||
db.alter_column('auth_userprofile', 'meta', self.gf('django.db.models.fields.TextField')())
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
|
||||
# Changing field 'UserProfile.meta'
|
||||
db.alter_column('auth_userprofile', 'meta', self.gf('django.db.models.fields.CharField')(max_length=255))
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
|
||||
@@ -4,6 +4,7 @@ from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
@@ -16,12 +17,10 @@ class Migration(SchemaMigration):
|
||||
ALTER TABLE student_usertestgroup_users CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci;
|
||||
""")
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Although this migration can't be undone, it is okay for it to be run backwards because it doesn't add/remove any fields
|
||||
pass
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
|
||||
@@ -16,12 +16,10 @@ class Migration(SchemaMigration):
|
||||
))
|
||||
db.send_create_signal('student', ['CourseRegistration'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting model 'CourseRegistration'
|
||||
db.delete_table('student_courseregistration')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
@@ -129,4 +127,4 @@ class Migration(SchemaMigration):
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
complete_apps = ['student']
|
||||
|
||||
@@ -19,7 +19,6 @@ class Migration(SchemaMigration):
|
||||
))
|
||||
db.send_create_signal('student', ['CourseEnrollment'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Adding model 'CourseRegistration'
|
||||
db.create_table('student_courseregistration', (
|
||||
@@ -32,7 +31,6 @@ class Migration(SchemaMigration):
|
||||
# Deleting model 'CourseEnrollment'
|
||||
db.delete_table('student_courseenrollment')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
@@ -140,4 +138,4 @@ class Migration(SchemaMigration):
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
complete_apps = ['student']
|
||||
|
||||
@@ -124,4 +124,4 @@ class Migration(SchemaMigration):
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
complete_apps = ['student']
|
||||
|
||||
@@ -8,20 +8,22 @@ from django.db import models
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Removing unique constraint on 'CourseEnrollment', fields ['user']
|
||||
db.delete_unique('student_courseenrollment', ['user_id'])
|
||||
|
||||
|
||||
# Changing field 'CourseEnrollment.user'
|
||||
db.alter_column('student_courseenrollment', 'user_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User']))
|
||||
# This table is dropped in a subsequent migration. This migration was causing problems when using InnoDB,
|
||||
# so we are just dropping it.
|
||||
pass
|
||||
# # Removing unique constraint on 'CourseEnrollment', fields ['user']
|
||||
# db.delete_unique('student_courseenrollment', ['user_id'])
|
||||
#
|
||||
#
|
||||
# # Changing field 'CourseEnrollment.user'
|
||||
# db.alter_column('student_courseenrollment', 'user_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User']))
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
# Changing field 'CourseEnrollment.user'
|
||||
db.alter_column('student_courseenrollment', 'user_id', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['auth.User'], unique=True))
|
||||
# Adding unique constraint on 'CourseEnrollment', fields ['user']
|
||||
db.create_unique('student_courseenrollment', ['user_id'])
|
||||
|
||||
pass
|
||||
# # Changing field 'CourseEnrollment.user'
|
||||
# db.alter_column('student_courseenrollment', 'user_id', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['auth.User'], unique=True))
|
||||
# # Adding unique constraint on 'CourseEnrollment', fields ['user']
|
||||
# db.create_unique('student_courseenrollment', ['user_id'])
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
@@ -130,4 +132,4 @@ class Migration(SchemaMigration):
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
complete_apps = ['student']
|
||||
|
||||
@@ -38,7 +38,6 @@ class Migration(SchemaMigration):
|
||||
self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting field 'UserProfile.gender'
|
||||
db.delete_column('auth_userprofile', 'gender')
|
||||
@@ -58,7 +57,6 @@ class Migration(SchemaMigration):
|
||||
# Deleting field 'UserProfile.occupation'
|
||||
db.delete_column('auth_userprofile', 'occupation')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
@@ -172,4 +170,4 @@ class Migration(SchemaMigration):
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
complete_apps = ['student']
|
||||
|
||||
@@ -130,4 +130,4 @@ class Migration(SchemaMigration):
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
complete_apps = ['student']
|
||||
|
||||
@@ -11,7 +11,6 @@ class Migration(SchemaMigration):
|
||||
# Deleting model 'CourseEnrollment'
|
||||
db.delete_table('student_courseenrollment')
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Adding model 'CourseEnrollment'
|
||||
db.create_table('student_courseenrollment', (
|
||||
@@ -21,7 +20,6 @@ class Migration(SchemaMigration):
|
||||
))
|
||||
db.send_create_signal('student', ['CourseEnrollment'])
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
@@ -129,4 +127,4 @@ class Migration(SchemaMigration):
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
complete_apps = ['student']
|
||||
|
||||
@@ -19,7 +19,6 @@ class Migration(SchemaMigration):
|
||||
# Adding unique constraint on 'CourseEnrollment', fields ['user', 'course_id']
|
||||
db.create_unique('student_courseenrollment', ['user_id', 'course_id'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Removing unique constraint on 'CourseEnrollment', fields ['user', 'course_id']
|
||||
db.delete_unique('student_courseenrollment', ['user_id', 'course_id'])
|
||||
@@ -27,7 +26,6 @@ class Migration(SchemaMigration):
|
||||
# Deleting model 'CourseEnrollment'
|
||||
db.delete_table('student_courseenrollment')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
@@ -141,4 +139,4 @@ class Migration(SchemaMigration):
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
complete_apps = ['student']
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Adding field 'CourseEnrollment.date'
|
||||
db.add_column('student_courseenrollment', 'date',
|
||||
self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, null=True, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
# Changing field 'UserProfile.country'
|
||||
db.alter_column('auth_userprofile', 'country', self.gf('django_countries.fields.CountryField')(max_length=2, null=True))
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting field 'CourseEnrollment.date'
|
||||
db.delete_column('student_courseenrollment', 'date')
|
||||
|
||||
# Changing field 'UserProfile.country'
|
||||
db.alter_column('auth_userprofile', 'country', self.gf('django.db.models.fields.CharField')(max_length=255, null=True))
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}),
|
||||
'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}),
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
|
||||
'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
|
||||
'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}),
|
||||
'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
|
||||
'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
|
||||
'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}),
|
||||
'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'student.courseenrollment': {
|
||||
'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'},
|
||||
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'student.pendingemailchange': {
|
||||
'Meta': {'object_name': 'PendingEmailChange'},
|
||||
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'new_email': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||
},
|
||||
'student.pendingnamechange': {
|
||||
'Meta': {'object_name': 'PendingNameChange'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'new_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'rationale': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||
},
|
||||
'student.registration': {
|
||||
'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"},
|
||||
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||
},
|
||||
'student.userprofile': {
|
||||
'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"},
|
||||
'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'null': 'True', 'blank': 'True'}),
|
||||
'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}),
|
||||
'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'gender': ('django.db.models.fields.CharField', [], {'max_length': '6', 'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'occupation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||
'telephone_number': ('django.db.models.fields.CharField', [], {'max_length': '25', 'null': 'True', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"})
|
||||
},
|
||||
'student.usertestgroup': {
|
||||
'Meta': {'object_name': 'UserTestGroup'},
|
||||
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
|
||||
'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
@@ -8,14 +8,12 @@ from django.db import models
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
|
||||
# Changing field 'UserProfile.country'
|
||||
db.alter_column('auth_userprofile', 'country', self.gf('django_countries.fields.CountryField')(max_length=2, null=True))
|
||||
# Rename 'date' field to 'created'
|
||||
db.rename_column('student_courseenrollment', 'date', 'created')
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
# Changing field 'UserProfile.country'
|
||||
db.alter_column('auth_userprofile', 'country', self.gf('django.db.models.fields.CharField')(max_length=255, null=True))
|
||||
# Rename 'created' field to 'date'
|
||||
db.rename_column('student_courseenrollment', 'created', 'date')
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
@@ -80,8 +78,9 @@ class Migration(SchemaMigration):
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'student.courseenrollment': {
|
||||
'Meta': {'object_name': 'CourseEnrollment'},
|
||||
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'},
|
||||
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
@@ -115,7 +114,7 @@ class Migration(SchemaMigration):
|
||||
'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'meta': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'occupation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||
'telephone_number': ('django.db.models.fields.CharField', [], {'max_length': '25', 'null': 'True', 'blank': 'True'}),
|
||||
@@ -130,4 +129,4 @@ class Migration(SchemaMigration):
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
complete_apps = ['student']
|
||||
@@ -8,14 +8,12 @@ from django.db import models
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
|
||||
# Changing field 'UserProfile.location'
|
||||
db.alter_column('auth_userprofile', 'location', self.gf('django.db.models.fields.CharField')(max_length=255, null=True))
|
||||
# Adding index on 'CourseEnrollment', fields ['created']
|
||||
db.create_index('student_courseenrollment', ['created'])
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
# Changing field 'UserProfile.location'
|
||||
db.alter_column('auth_userprofile', 'location', self.gf('django.db.models.fields.CharField')(default='', max_length=255))
|
||||
# Removing index on 'CourseEnrollment', fields ['created']
|
||||
db.delete_index('student_courseenrollment', ['created'])
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
@@ -80,8 +78,9 @@ class Migration(SchemaMigration):
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'student.courseenrollment': {
|
||||
'Meta': {'object_name': 'CourseEnrollment'},
|
||||
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'},
|
||||
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
@@ -113,9 +112,9 @@ class Migration(SchemaMigration):
|
||||
'gender': ('django.db.models.fields.CharField', [], {'max_length': '6', 'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||
'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'meta': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'occupation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||
'telephone_number': ('django.db.models.fields.CharField', [], {'max_length': '25', 'null': 'True', 'blank': 'True'}),
|
||||
@@ -130,4 +129,4 @@ class Migration(SchemaMigration):
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
complete_apps = ['student']
|
||||
@@ -0,0 +1,187 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Deleting field 'UserProfile.occupation'
|
||||
db.delete_column('auth_userprofile', 'occupation')
|
||||
|
||||
# Deleting field 'UserProfile.telephone_number'
|
||||
db.delete_column('auth_userprofile', 'telephone_number')
|
||||
|
||||
# Deleting field 'UserProfile.date_of_birth'
|
||||
db.delete_column('auth_userprofile', 'date_of_birth')
|
||||
|
||||
# Deleting field 'UserProfile.country'
|
||||
db.delete_column('auth_userprofile', 'country')
|
||||
|
||||
# Adding field 'UserProfile.year_of_birth'
|
||||
db.add_column('auth_userprofile', 'year_of_birth',
|
||||
self.gf('django.db.models.fields.IntegerField')(db_index=True, null=True, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
# Adding field 'UserProfile.level_of_education'
|
||||
db.add_column('auth_userprofile', 'level_of_education',
|
||||
self.gf('django.db.models.fields.CharField')(db_index=True, max_length=6, null=True, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
# Adding field 'UserProfile.goals'
|
||||
db.add_column('auth_userprofile', 'goals',
|
||||
self.gf('django.db.models.fields.TextField')(null=True, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
# Adding index on 'UserProfile', fields ['gender']
|
||||
db.create_index('auth_userprofile', ['gender'])
|
||||
|
||||
def backwards(self, orm):
|
||||
# Removing index on 'UserProfile', fields ['gender']
|
||||
db.delete_index('auth_userprofile', ['gender'])
|
||||
|
||||
# Adding field 'UserProfile.occupation'
|
||||
db.add_column('auth_userprofile', 'occupation',
|
||||
self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
# Adding field 'UserProfile.telephone_number'
|
||||
db.add_column('auth_userprofile', 'telephone_number',
|
||||
self.gf('django.db.models.fields.CharField')(max_length=25, null=True, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
# Adding field 'UserProfile.date_of_birth'
|
||||
db.add_column('auth_userprofile', 'date_of_birth',
|
||||
self.gf('django.db.models.fields.DateField')(null=True, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
# Adding field 'UserProfile.country'
|
||||
db.add_column('auth_userprofile', 'country',
|
||||
self.gf('django_countries.fields.CountryField')(max_length=2, null=True, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
# Deleting field 'UserProfile.year_of_birth'
|
||||
db.delete_column('auth_userprofile', 'year_of_birth')
|
||||
|
||||
# Deleting field 'UserProfile.level_of_education'
|
||||
db.delete_column('auth_userprofile', 'level_of_education')
|
||||
|
||||
# Deleting field 'UserProfile.goals'
|
||||
db.delete_column('auth_userprofile', 'goals')
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}),
|
||||
'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}),
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
|
||||
'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
|
||||
'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}),
|
||||
'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
|
||||
'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
|
||||
'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
|
||||
'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}),
|
||||
'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'student.courseenrollment': {
|
||||
'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'},
|
||||
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'student.pendingemailchange': {
|
||||
'Meta': {'object_name': 'PendingEmailChange'},
|
||||
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'new_email': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||
},
|
||||
'student.pendingnamechange': {
|
||||
'Meta': {'object_name': 'PendingNameChange'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'new_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'rationale': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||
},
|
||||
'student.registration': {
|
||||
'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"},
|
||||
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'})
|
||||
},
|
||||
'student.userprofile': {
|
||||
'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"},
|
||||
'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}),
|
||||
'gender': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}),
|
||||
'goals': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'level_of_education': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}),
|
||||
'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"}),
|
||||
'year_of_birth': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
|
||||
},
|
||||
'student.usertestgroup': {
|
||||
'Meta': {'object_name': 'UserTestGroup'},
|
||||
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
|
||||
'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
@@ -5,39 +5,60 @@ If you make changes to this model, be sure to create an appropriate migration
|
||||
file and check it in at the same time as your model changes. To do that,
|
||||
|
||||
1. Go to the mitx dir
|
||||
2. ./manage.py schemamigration user --auto description_of_your_change
|
||||
3. Add the migration file created in mitx/courseware/migrations/
|
||||
2. django-admin.py schemamigration student --auto --settings=lms.envs.dev --pythonpath=. description_of_your_change
|
||||
3. Add the migration file created in mitx/common/djangoapps/student/migrations/
|
||||
"""
|
||||
from datetime import datetime
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
import json
|
||||
from django_countries import CountryField
|
||||
|
||||
#from cache_toolbox import cache_model, cache_relation
|
||||
|
||||
|
||||
class UserProfile(models.Model):
|
||||
class Meta:
|
||||
db_table = "auth_userprofile"
|
||||
|
||||
GENDER_CHOICES = (('m', 'Male'), ('f', 'Female'), ('o', 'Other'))
|
||||
|
||||
## CRITICAL TODO/SECURITY
|
||||
# Sanitize all fields.
|
||||
# This is not visible to other users, but could introduce holes later
|
||||
user = models.OneToOneField(User, unique=True, db_index=True, related_name='profile')
|
||||
name = models.CharField(blank=True, max_length=255, db_index=True)
|
||||
language = models.CharField(blank=True, max_length=255, db_index=True)
|
||||
location = models.CharField(blank=True, max_length=255, db_index=True) # TODO: What are we doing with this?
|
||||
meta = models.TextField(blank=True) # JSON dictionary for future expansion
|
||||
|
||||
meta = models.TextField(blank=True) # JSON dictionary for future expansion
|
||||
courseware = models.CharField(blank=True, max_length=255, default='course.xml')
|
||||
gender = models.CharField(blank=True, null=True, max_length=6, choices=GENDER_CHOICES)
|
||||
date_of_birth = models.DateField(blank=True, null=True)
|
||||
|
||||
# Location is no longer used, but is held here for backwards compatibility
|
||||
# for users imported from our first class.
|
||||
language = models.CharField(blank=True, max_length=255, db_index=True)
|
||||
location = models.CharField(blank=True, max_length=255, db_index=True)
|
||||
|
||||
# Optional demographic data we started capturing from Fall 2012
|
||||
this_year = datetime.now().year
|
||||
VALID_YEARS = range(this_year, this_year - 120, -1)
|
||||
year_of_birth = models.IntegerField(blank=True, null=True, db_index=True)
|
||||
GENDER_CHOICES = (('m', 'Male'), ('f', 'Female'), ('o', 'Other'))
|
||||
gender = models.CharField(blank=True, null=True, max_length=6, db_index=True,
|
||||
choices=GENDER_CHOICES)
|
||||
LEVEL_OF_EDUCATION_CHOICES = (('p_se', 'Doctorate in science or engineering'),
|
||||
('p_oth', 'Doctorate in another field'),
|
||||
('m', "Master's or professional degree"),
|
||||
('b', "Bachelor's degree"),
|
||||
('hs', "Secondary/high school"),
|
||||
('jhs', "Junior secondary/junior high/middle school"),
|
||||
('el', "Elementary/primary school"),
|
||||
('none', "None"),
|
||||
('other', "Other"))
|
||||
level_of_education = models.CharField(
|
||||
blank=True, null=True, max_length=6, db_index=True,
|
||||
choices=LEVEL_OF_EDUCATION_CHOICES
|
||||
)
|
||||
mailing_address = models.TextField(blank=True, null=True)
|
||||
country = CountryField(blank=True, null=True)
|
||||
telephone_number = models.CharField(blank=True, null=True, max_length=25)
|
||||
occupation = models.CharField(blank=True, null=True, max_length=255)
|
||||
goals = models.TextField(blank=True, null=True)
|
||||
|
||||
def get_meta(self):
|
||||
js_str = self.meta
|
||||
@@ -48,9 +69,10 @@ class UserProfile(models.Model):
|
||||
|
||||
return js_str
|
||||
|
||||
def set_meta(self,js):
|
||||
def set_meta(self, js):
|
||||
self.meta = json.dumps(js)
|
||||
|
||||
|
||||
## TODO: Should be renamed to generic UserGroup, and possibly
|
||||
# Given an optional field for type of group
|
||||
class UserTestGroup(models.Model):
|
||||
@@ -58,6 +80,7 @@ class UserTestGroup(models.Model):
|
||||
name = models.CharField(blank=False, max_length=32, db_index=True)
|
||||
description = models.TextField(blank=True)
|
||||
|
||||
|
||||
class Registration(models.Model):
|
||||
''' Allows us to wait for e-mail before user is registered. A
|
||||
registration profile is created when the user creates an
|
||||
@@ -71,8 +94,8 @@ class Registration(models.Model):
|
||||
|
||||
def register(self, user):
|
||||
# MINOR TODO: Switch to crypto-secure key
|
||||
self.activation_key=uuid.uuid4().hex
|
||||
self.user=user
|
||||
self.activation_key = uuid.uuid4().hex
|
||||
self.user = user
|
||||
self.save()
|
||||
|
||||
def activate(self):
|
||||
@@ -80,20 +103,25 @@ class Registration(models.Model):
|
||||
self.user.save()
|
||||
#self.delete()
|
||||
|
||||
|
||||
class PendingNameChange(models.Model):
|
||||
user = models.OneToOneField(User, unique=True, db_index=True)
|
||||
new_name = models.CharField(blank=True, max_length=255)
|
||||
rationale = models.CharField(blank=True, max_length=1024)
|
||||
|
||||
|
||||
class PendingEmailChange(models.Model):
|
||||
user = models.OneToOneField(User, unique=True, db_index=True)
|
||||
new_email = models.CharField(blank=True, max_length=255, db_index=True)
|
||||
activation_key = models.CharField(('activation key'), max_length=32, unique=True, db_index=True)
|
||||
|
||||
|
||||
class CourseEnrollment(models.Model):
|
||||
user = models.ForeignKey(User)
|
||||
course_id = models.CharField(max_length=255, db_index=True)
|
||||
|
||||
|
||||
created = models.DateTimeField(auto_now_add=True, null=True, db_index=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = (('user', 'course_id'), )
|
||||
|
||||
@@ -101,38 +129,45 @@ class CourseEnrollment(models.Model):
|
||||
|
||||
#### Helper methods for use from python manage.py shell.
|
||||
|
||||
|
||||
def get_user(email):
|
||||
u = User.objects.get(email = email)
|
||||
up = UserProfile.objects.get(user = u)
|
||||
return u,up
|
||||
u = User.objects.get(email=email)
|
||||
up = UserProfile.objects.get(user=u)
|
||||
return u, up
|
||||
|
||||
|
||||
def user_info(email):
|
||||
u,up = get_user(email)
|
||||
u, up = get_user(email)
|
||||
print "User id", u.id
|
||||
print "Username", u.username
|
||||
print "E-mail", u.email
|
||||
print "Name", up.name
|
||||
print "Location", up.location
|
||||
print "Language", up.language
|
||||
return u,up
|
||||
return u, up
|
||||
|
||||
|
||||
def change_email(old_email, new_email):
|
||||
u = User.objects.get(email = old_email)
|
||||
u = User.objects.get(email=old_email)
|
||||
u.email = new_email
|
||||
u.save()
|
||||
|
||||
|
||||
def change_name(email, new_name):
|
||||
u,up = get_user(email)
|
||||
u, up = get_user(email)
|
||||
up.name = new_name
|
||||
up.save()
|
||||
|
||||
|
||||
def user_count():
|
||||
print "All users", User.objects.all().count()
|
||||
print "Active users", User.objects.filter(is_active = True).count()
|
||||
print "Active users", User.objects.filter(is_active=True).count()
|
||||
return User.objects.all().count()
|
||||
|
||||
|
||||
def active_user_count():
|
||||
return User.objects.filter(is_active = True).count()
|
||||
return User.objects.filter(is_active=True).count()
|
||||
|
||||
|
||||
def create_group(name, description):
|
||||
utg = UserTestGroup()
|
||||
@@ -140,29 +175,31 @@ def create_group(name, description):
|
||||
utg.description = description
|
||||
utg.save()
|
||||
|
||||
|
||||
def add_user_to_group(user, group):
|
||||
utg = UserTestGroup.objects.get(name = group)
|
||||
utg.users.add(User.objects.get(username = user))
|
||||
utg = UserTestGroup.objects.get(name=group)
|
||||
utg.users.add(User.objects.get(username=user))
|
||||
utg.save()
|
||||
|
||||
|
||||
def remove_user_from_group(user, group):
|
||||
utg = UserTestGroup.objects.get(name = group)
|
||||
utg.users.remove(User.objects.get(username = user))
|
||||
utg = UserTestGroup.objects.get(name=group)
|
||||
utg.users.remove(User.objects.get(username=user))
|
||||
utg.save()
|
||||
|
||||
default_groups = {'email_future_courses' : 'Receive e-mails about future MITx courses',
|
||||
'email_helpers' : 'Receive e-mails about how to help with MITx',
|
||||
'mitx_unenroll' : 'Fully unenrolled -- no further communications',
|
||||
'6002x_unenroll' : 'Took and dropped 6002x'}
|
||||
default_groups = {'email_future_courses': 'Receive e-mails about future MITx courses',
|
||||
'email_helpers': 'Receive e-mails about how to help with MITx',
|
||||
'mitx_unenroll': 'Fully unenrolled -- no further communications',
|
||||
'6002x_unenroll': 'Took and dropped 6002x'}
|
||||
|
||||
|
||||
def add_user_to_default_group(user, group):
|
||||
try:
|
||||
utg = UserTestGroup.objects.get(name = group)
|
||||
utg = UserTestGroup.objects.get(name=group)
|
||||
except UserTestGroup.DoesNotExist:
|
||||
utg = UserTestGroup()
|
||||
utg.name = group
|
||||
utg.description = default_groups[group]
|
||||
utg.save()
|
||||
utg.users.add(User.objects.get(username = user))
|
||||
utg.users.add(User.objects.get(username=user))
|
||||
utg.save()
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import uuid
|
||||
import feedparser
|
||||
import urllib
|
||||
import itertools
|
||||
from collections import defaultdict
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import logout, authenticate, login
|
||||
@@ -22,21 +23,24 @@ from django.http import HttpResponse, Http404
|
||||
from django.shortcuts import redirect
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
from django.core.urlresolvers import reverse
|
||||
from BeautifulSoup import BeautifulSoup
|
||||
#from BeautifulSoup import BeautifulSoup
|
||||
from bs4 import BeautifulSoup
|
||||
from django.core.cache import cache
|
||||
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from student.models import Registration, UserProfile, PendingNameChange, PendingEmailChange, CourseEnrollment
|
||||
from util.cache import cache_if_anonymous
|
||||
from util.cache import cache_if_anonymous
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
|
||||
from models import Registration, UserProfile, PendingNameChange, PendingEmailChange, CourseEnrollment
|
||||
from datetime import date
|
||||
from collections import namedtuple
|
||||
|
||||
log = logging.getLogger("mitx.student")
|
||||
|
||||
Article = namedtuple('Article', 'title url author image deck publication publish_date')
|
||||
|
||||
def csrf_token(context):
|
||||
''' A csrf token that can be included in a form.
|
||||
@@ -70,27 +74,41 @@ def index(request):
|
||||
for entry in entries:
|
||||
soup = BeautifulSoup(entry.description)
|
||||
entry.image = soup.img['src'] if soup.img else None
|
||||
entry.summary = soup.getText()
|
||||
|
||||
courses = modulestore().get_courses()
|
||||
universities = dict()
|
||||
for university, group in itertools.groupby(courses, lambda course: course.org):
|
||||
universities.setdefault(university, [])
|
||||
[universities[university].append(course) for course in group]
|
||||
universities = defaultdict(list)
|
||||
courses = sorted(modulestore().get_courses(), key=lambda course: course.number)
|
||||
for course in courses:
|
||||
universities[course.org].append(course)
|
||||
|
||||
return render_to_response('index.html', {'universities': universities, 'entries': entries})
|
||||
|
||||
|
||||
def course_from_id(id):
|
||||
course_loc = CourseDescriptor.id_to_location(id)
|
||||
return modulestore().get_item(course_loc)
|
||||
|
||||
|
||||
def press(request):
|
||||
json_articles = cache.get("student_press_json_articles")
|
||||
if json_articles == None:
|
||||
if hasattr(settings, 'RSS_URL'):
|
||||
content = urllib.urlopen(settings.PRESS_URL).read()
|
||||
json_articles = json.loads(content)
|
||||
else:
|
||||
content = open(settings.PROJECT_ROOT / "templates" / "press.json").read()
|
||||
json_articles = json.loads(content)
|
||||
cache.set("student_press_json_articles", json_articles)
|
||||
articles = [Article(**article) for article in json_articles]
|
||||
return render_to_response('static_templates/press.html', {'articles': articles})
|
||||
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def dashboard(request):
|
||||
csrf_token = csrf(request)['csrf_token']
|
||||
user = request.user
|
||||
enrollments = CourseEnrollment.objects.filter(user=user)
|
||||
|
||||
def course_from_id(id):
|
||||
course_loc = CourseDescriptor.id_to_location(id)
|
||||
return modulestore().get_item(course_loc)
|
||||
|
||||
# Build our courses list for the user, but ignore any courses that no longer
|
||||
# exist (because the course IDs have changed). Still, we don't delete those
|
||||
# enrollments, because it could have been a data push snafu.
|
||||
@@ -102,17 +120,80 @@ def dashboard(request):
|
||||
log.error("User {0} enrolled in non-existant course {1}"
|
||||
.format(user.username, enrollment.course_id))
|
||||
|
||||
context = {'csrf': csrf_token, 'courses': courses}
|
||||
message = ""
|
||||
if not user.is_active:
|
||||
message = render_to_string('registration/activate_account_notice.html', {'email': user.email})
|
||||
|
||||
context = {'courses': courses, 'message': message}
|
||||
return render_to_response('dashboard.html', context)
|
||||
|
||||
|
||||
def try_change_enrollment(request):
|
||||
"""
|
||||
This method calls change_enrollment if the necessary POST
|
||||
parameters are present, but does not return anything. It
|
||||
simply logs the result or exception. This is usually
|
||||
called after a registration or login, as secondary action.
|
||||
It should not interrupt a successful registration or login.
|
||||
"""
|
||||
if 'enrollment_action' in request.POST:
|
||||
try:
|
||||
enrollment_output = change_enrollment(request)
|
||||
# There isn't really a way to display the results to the user, so we just log it
|
||||
# We expect the enrollment to be a success, and will show up on the dashboard anyway
|
||||
log.info("Attempted to automatically enroll after login. Results: {0}".format(enrollment_output))
|
||||
except Exception, e:
|
||||
log.exception("Exception automatically enrolling after login: {0}".format(str(e)))
|
||||
|
||||
|
||||
@login_required
|
||||
def change_enrollment_view(request):
|
||||
return HttpResponse(json.dumps(change_enrollment(request)))
|
||||
|
||||
|
||||
def change_enrollment(request):
|
||||
if request.method != "POST":
|
||||
raise Http404
|
||||
|
||||
action = request.POST.get("enrollment_action", "")
|
||||
user = request.user
|
||||
course_id = request.POST.get("course_id", None)
|
||||
if course_id == None:
|
||||
return HttpResponse(json.dumps({'success': False, 'error': 'There was an error receiving the course id.'}))
|
||||
|
||||
if action == "enroll":
|
||||
# Make sure the course exists
|
||||
# We don't do this check on unenroll, or a bad course id can't be unenrolled from
|
||||
try:
|
||||
course = course_from_id(course_id)
|
||||
except ItemNotFoundError:
|
||||
log.error("User {0} tried to enroll in non-existant course {1}"
|
||||
.format(user.username, enrollment.course_id))
|
||||
return {'success': False, 'error': 'The course requested does not exist.'}
|
||||
|
||||
enrollment, created = CourseEnrollment.objects.get_or_create(user=user, course_id=course.id)
|
||||
return {'success': True}
|
||||
|
||||
elif action == "unenroll":
|
||||
try:
|
||||
enrollment = CourseEnrollment.objects.get(user=user, course_id=course_id)
|
||||
enrollment.delete()
|
||||
return {'success': True}
|
||||
except CourseEnrollment.DoesNotExist:
|
||||
return {'success': False, 'error': 'You are not enrolled for this course.'}
|
||||
else:
|
||||
return {'success': False, 'error': 'Invalid enrollment_action.'}
|
||||
|
||||
return {'success': False, 'error': 'We weren\'t able to unenroll you. Please try again.'}
|
||||
|
||||
|
||||
# Need different levels of logging
|
||||
@ensure_csrf_cookie
|
||||
def login_user(request, error=""):
|
||||
''' AJAX request to log in the user. '''
|
||||
if 'email' not in request.POST or 'password' not in request.POST:
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': 'Invalid login'})) # TODO: User error message
|
||||
'value': 'There was an error receiving your login information. Please email us.'})) # TODO: User error message
|
||||
|
||||
email = request.POST['email']
|
||||
password = request.POST['password']
|
||||
@@ -121,20 +202,20 @@ def login_user(request, error=""):
|
||||
except User.DoesNotExist:
|
||||
log.warning("Login failed - Unknown user email: {0}".format(email))
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': 'Invalid login'})) # TODO: User error message
|
||||
'value': 'Email or password is incorrect.'})) # TODO: User error message
|
||||
|
||||
username = user.username
|
||||
user = authenticate(username=username, password=password)
|
||||
if user is None:
|
||||
log.warning("Login failed - password for {0} is invalid".format(email))
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': 'Invalid login'}))
|
||||
'value': 'Email or password is incorrect.'}))
|
||||
|
||||
if user is not None and user.is_active:
|
||||
try:
|
||||
login(request, user)
|
||||
if request.POST.get('remember') == 'true':
|
||||
request.session.set_expiry(None) # or change to 604800 for 7 days
|
||||
request.session.set_expiry(None) # or change to 604800 for 7 days
|
||||
log.debug("Setting user session to never expire")
|
||||
else:
|
||||
request.session.set_expiry(0)
|
||||
@@ -143,11 +224,15 @@ def login_user(request, error=""):
|
||||
log.exception(e)
|
||||
|
||||
log.info("Login success - {0} ({1})".format(username, email))
|
||||
return HttpResponse(json.dumps({'success':True}))
|
||||
|
||||
try_change_enrollment(request)
|
||||
|
||||
return HttpResponse(json.dumps({'success': True}))
|
||||
|
||||
log.warning("Login failed - Account not active for user {0}".format(username))
|
||||
return HttpResponse(json.dumps({'success':False,
|
||||
'error': 'Account not active. Check your e-mail.'}))
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'value': 'This account has not been activated. Please check your e-mail for the activation instructions.'}))
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def logout_user(request):
|
||||
@@ -155,23 +240,25 @@ def logout_user(request):
|
||||
logout(request)
|
||||
return redirect('/')
|
||||
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def change_setting(request):
|
||||
''' JSON call to change a profile setting: Right now, location
|
||||
'''
|
||||
up = UserProfile.objects.get(user=request.user) #request.user.profile_cache
|
||||
up = UserProfile.objects.get(user=request.user) # request.user.profile_cache
|
||||
if 'location' in request.POST:
|
||||
up.location=request.POST['location']
|
||||
up.location = request.POST['location']
|
||||
up.save()
|
||||
|
||||
return HttpResponse(json.dumps({'success':True,
|
||||
'location':up.location,}))
|
||||
return HttpResponse(json.dumps({'success': True,
|
||||
'location': up.location, }))
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def create_account(request, post_override=None):
|
||||
''' JSON call to enroll in the course. '''
|
||||
js={'success':False}
|
||||
js = {'success': False}
|
||||
|
||||
post_vars = post_override if post_override else request.POST
|
||||
|
||||
@@ -179,15 +266,17 @@ def create_account(request, post_override=None):
|
||||
for a in ['username', 'email', 'password', 'name']:
|
||||
if a not in post_vars:
|
||||
js['value'] = "Error (401 {field}). E-mail us.".format(field=a)
|
||||
js['field'] = a
|
||||
return HttpResponse(json.dumps(js))
|
||||
|
||||
if post_vars.get('honor_code', 'false') != u'true':
|
||||
js['value']="To enroll, you must follow the honor code.".format(field=a)
|
||||
js['value'] = "To enroll, you must follow the honor code.".format(field=a)
|
||||
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['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.
|
||||
@@ -197,25 +286,28 @@ def create_account(request, post_override=None):
|
||||
# TODO: Check password is sane
|
||||
for a in ['username', 'email', 'name', 'password', 'terms_of_service', 'honor_code']:
|
||||
if len(post_vars[a]) < 2:
|
||||
error_str = {'username' : 'Username of length 2 or greater',
|
||||
'email' : 'Properly formatted e-mail',
|
||||
'name' : 'Your legal name ',
|
||||
'password': 'Valid password ',
|
||||
'terms_of_service': 'Accepting Terms of Service',
|
||||
'honor_code': 'Agreeing to the Honor Code'}
|
||||
js['value']="{field} is required.".format(field=error_str[a])
|
||||
error_str = {'username': 'Username must be minimum of two characters long.',
|
||||
'email': 'A properly formatted e-mail is required.',
|
||||
'name': 'Your legal name must be a minimum of two characters long.',
|
||||
'password': 'A valid password is required.',
|
||||
'terms_of_service': 'Accepting Terms of Service is required.',
|
||||
'honor_code': 'Agreeing to the Honor Code is required.'}
|
||||
js['value'] = error_str[a]
|
||||
js['field'] = a
|
||||
return HttpResponse(json.dumps(js))
|
||||
|
||||
try:
|
||||
validate_email(post_vars['email'])
|
||||
except ValidationError:
|
||||
js['value']="Valid e-mail is required.".format(field=a)
|
||||
js['value'] = "Valid e-mail is required.".format(field=a)
|
||||
js['field'] = 'email'
|
||||
return HttpResponse(json.dumps(js))
|
||||
|
||||
try:
|
||||
validate_slug(post_vars['username'])
|
||||
except ValidationError:
|
||||
js['value']="Username should only consist of A-Z and 0-9.".format(field=a)
|
||||
js['value'] = "Username should only consist of A-Z and 0-9.".format(field=a)
|
||||
js['field'] = 'username'
|
||||
return HttpResponse(json.dumps(js))
|
||||
|
||||
u = User(username=post_vars['username'],
|
||||
@@ -231,10 +323,12 @@ def create_account(request, post_override=None):
|
||||
# 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."
|
||||
js['field'] = 'username'
|
||||
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."
|
||||
js['field'] = 'email'
|
||||
return HttpResponse(json.dumps(js))
|
||||
|
||||
raise
|
||||
@@ -243,19 +337,21 @@ def create_account(request, post_override=None):
|
||||
|
||||
up = UserProfile(user=u)
|
||||
up.name = post_vars['name']
|
||||
up.country = post_vars['country']
|
||||
up.gender = post_vars['gender']
|
||||
up.mailing_address = post_vars['mailing_address']
|
||||
up.level_of_education = post_vars.get('level_of_education')
|
||||
up.gender = post_vars.get('gender')
|
||||
up.mailing_address = post_vars.get('mailing_address')
|
||||
up.goals = post_vars.get('goals')
|
||||
|
||||
date_fields = ['date_of_birth__year', 'date_of_birth__month', 'date_of_birth__day']
|
||||
if all(len(post_vars[field]) > 0 for field in date_fields):
|
||||
up.date_of_birth = date(int(post_vars['date_of_birth__year']),
|
||||
int(post_vars['date_of_birth__month']),
|
||||
int(post_vars['date_of_birth__day']))
|
||||
try:
|
||||
up.year_of_birth = int(post_vars['year_of_birth'])
|
||||
except (ValueError, KeyError):
|
||||
up.year_of_birth = None # If they give us garbage, just ignore it instead
|
||||
# of asking them to put an integer.
|
||||
try:
|
||||
up.save()
|
||||
except Exception:
|
||||
log.exception("UserProfile creation failed for user {0}.".format(u.id))
|
||||
|
||||
up.save()
|
||||
|
||||
# TODO (vshnayder): the LMS should probably allow signups without a particular course too
|
||||
d = {'name': post_vars['name'],
|
||||
'key': r.activation_key,
|
||||
}
|
||||
@@ -268,7 +364,7 @@ def create_account(request, post_override=None):
|
||||
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)
|
||||
@@ -277,48 +373,59 @@ def create_account(request, post_override=None):
|
||||
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']})}
|
||||
# Immediately after a user creates an account, we log them in. They are only
|
||||
# logged in until they close the browser. They can't log in again until they click
|
||||
# the activation link from the email.
|
||||
login_user = authenticate(username=post_vars['username'], password=post_vars['password'])
|
||||
login(request, login_user)
|
||||
request.session.set_expiry(0)
|
||||
|
||||
try_change_enrollment(request)
|
||||
|
||||
js = {'success': True}
|
||||
return HttpResponse(json.dumps(js), mimetype="application/json")
|
||||
|
||||
|
||||
def create_random_account(create_account_function):
|
||||
|
||||
def id_generator(size=6, chars=string.ascii_uppercase + string.ascii_lowercase + string.digits):
|
||||
return ''.join(random.choice(chars) for x in range(size))
|
||||
|
||||
def inner_create_random_account(request):
|
||||
post_override= {'username' : "random_" + id_generator(),
|
||||
'email' : id_generator(size=10, chars=string.ascii_lowercase) + "_dummy_test@mitx.mit.edu",
|
||||
'password' : id_generator(),
|
||||
'location' : id_generator(size=5, chars=string.ascii_uppercase),
|
||||
'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',}
|
||||
post_override = {'username': "random_" + id_generator(),
|
||||
'email': id_generator(size=10, chars=string.ascii_lowercase) + "_dummy_test@mitx.mit.edu",
|
||||
'password': id_generator(),
|
||||
'location': id_generator(size=5, chars=string.ascii_uppercase),
|
||||
'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', }
|
||||
|
||||
return create_account_function(request, post_override = post_override)
|
||||
return create_account_function(request, post_override=post_override)
|
||||
|
||||
return inner_create_random_account
|
||||
|
||||
if settings.GENERATE_RANDOM_USER_CREDENTIALS:
|
||||
create_account = create_random_account(create_account)
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def activate_account(request, key):
|
||||
''' When link in activation e-mail is clicked
|
||||
'''
|
||||
r=Registration.objects.filter(activation_key=key)
|
||||
if len(r)==1:
|
||||
r = Registration.objects.filter(activation_key=key)
|
||||
if len(r) == 1:
|
||||
user_logged_in = request.user.is_authenticated()
|
||||
already_active = True
|
||||
if not r[0].user.is_active:
|
||||
r[0].activate()
|
||||
resp = render_to_response("activation_complete.html",{'csrf':csrf(request)['csrf_token']})
|
||||
return resp
|
||||
resp = render_to_response("activation_active.html",{'csrf':csrf(request)['csrf_token']})
|
||||
already_active = False
|
||||
resp = render_to_response("registration/activation_complete.html", {'user_logged_in': user_logged_in, 'already_active': already_active})
|
||||
return resp
|
||||
if len(r)==0:
|
||||
return render_to_response("activation_invalid.html",{'csrf':csrf(request)['csrf_token']})
|
||||
if len(r) == 0:
|
||||
return render_to_response("registration/activation_invalid.html", {'csrf': csrf(request)['csrf_token']})
|
||||
return HttpResponse("Unknown error. Please e-mail us to let us know how it happened.")
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def password_reset(request):
|
||||
''' Attempts to send a password reset e-mail. '''
|
||||
@@ -326,43 +433,45 @@ def password_reset(request):
|
||||
raise Http404
|
||||
form = PasswordResetForm(request.POST)
|
||||
if form.is_valid():
|
||||
form.save( use_https = request.is_secure(),
|
||||
from_email = settings.DEFAULT_FROM_EMAIL,
|
||||
request = request )
|
||||
form.save(use_https = request.is_secure(),
|
||||
from_email = settings.DEFAULT_FROM_EMAIL,
|
||||
request = request,
|
||||
domain_override = settings.SITE_NAME)
|
||||
return HttpResponse(json.dumps({'success':True,
|
||||
'value': render_to_string('registration/password_reset_done.html', {})}))
|
||||
else:
|
||||
return HttpResponse(json.dumps({'success':False,
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'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')
|
||||
user = User.objects.get(email='email')
|
||||
except User.DoesNotExist:
|
||||
return HttpResponse(json.dumps({'success':False,
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': 'No inactive user with this e-mail exists'}))
|
||||
|
||||
if user.is_active:
|
||||
return HttpResponse(json.dumps({'success':False,
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': 'User is already active'}))
|
||||
|
||||
reg = Registration.objects.get(user = user)
|
||||
reg = Registration.objects.get(user=user)
|
||||
reg.register(user)
|
||||
|
||||
d={'name':UserProfile.get(user = user).name,
|
||||
'key':r.activation_key}
|
||||
d = {'name': UserProfile.get(user=user).name,
|
||||
'key': r.activation_key}
|
||||
|
||||
subject = render_to_string('reactivation_email_subject.txt',d)
|
||||
subject = render_to_string('reactivation_email_subject.txt', d)
|
||||
subject = ''.join(subject.splitlines())
|
||||
message = render_to_string('reactivation_email.txt',d)
|
||||
message = render_to_string('reactivation_email.txt', d)
|
||||
|
||||
res=u.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
|
||||
res = u.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
|
||||
|
||||
return HttpResponse(json.dumps({'success':True}))
|
||||
return HttpResponse(json.dumps({'success': True}))
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@@ -376,26 +485,26 @@ def change_email_request(request):
|
||||
user = request.user
|
||||
|
||||
if not user.check_password(request.POST['password']):
|
||||
return HttpResponse(json.dumps({'success':False,
|
||||
'error':'Invalid password'}))
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': 'Invalid password'}))
|
||||
|
||||
new_email = request.POST['new_email']
|
||||
try:
|
||||
validate_email(new_email)
|
||||
except ValidationError:
|
||||
return HttpResponse(json.dumps({'success':False,
|
||||
'error':'Valid e-mail address required.'}))
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': 'Valid e-mail address required.'}))
|
||||
|
||||
if len(User.objects.filter(email = new_email)) != 0:
|
||||
if len(User.objects.filter(email=new_email)) != 0:
|
||||
## CRITICAL TODO: Handle case sensitivity for e-mails
|
||||
return HttpResponse(json.dumps({'success':False,
|
||||
'error':'An account with this e-mail already exists.'}))
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': 'An account with this e-mail already exists.'}))
|
||||
|
||||
pec_list = PendingEmailChange.objects.filter(user = request.user)
|
||||
pec_list = PendingEmailChange.objects.filter(user=request.user)
|
||||
if len(pec_list) == 0:
|
||||
pec = PendingEmailChange()
|
||||
pec.user = user
|
||||
else :
|
||||
else:
|
||||
pec = pec_list[0]
|
||||
|
||||
pec.new_email = request.POST['new_email']
|
||||
@@ -404,20 +513,21 @@ def change_email_request(request):
|
||||
|
||||
if pec.new_email == user.email:
|
||||
pec.delete()
|
||||
return HttpResponse(json.dumps({'success':False,
|
||||
'error':'Old email is the same as the new email.'}))
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': 'Old email is the same as the new email.'}))
|
||||
|
||||
d = {'key':pec.activation_key,
|
||||
'old_email' : user.email,
|
||||
'new_email' : pec.new_email}
|
||||
d = {'key': pec.activation_key,
|
||||
'old_email': user.email,
|
||||
'new_email': pec.new_email}
|
||||
|
||||
subject = render_to_string('emails/email_change_subject.txt',d)
|
||||
subject = render_to_string('emails/email_change_subject.txt', d)
|
||||
subject = ''.join(subject.splitlines())
|
||||
message = render_to_string('emails/email_change.txt',d)
|
||||
message = render_to_string('emails/email_change.txt', d)
|
||||
|
||||
res=send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [pec.new_email])
|
||||
res = send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [pec.new_email])
|
||||
|
||||
return HttpResponse(json.dumps({'success': True}))
|
||||
|
||||
return HttpResponse(json.dumps({'success':True}))
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def confirm_email_change(request, key):
|
||||
@@ -425,22 +535,21 @@ def confirm_email_change(request, key):
|
||||
link is clicked. We confirm with the old e-mail, and update
|
||||
'''
|
||||
try:
|
||||
pec=PendingEmailChange.objects.get(activation_key=key)
|
||||
pec = PendingEmailChange.objects.get(activation_key=key)
|
||||
except PendingEmailChange.DoesNotExist:
|
||||
return render_to_response("invalid_email_key.html", {})
|
||||
|
||||
user = pec.user
|
||||
d = {'old_email' : user.email,
|
||||
'new_email' : pec.new_email}
|
||||
d = {'old_email': user.email,
|
||||
'new_email': pec.new_email}
|
||||
|
||||
if len(User.objects.filter(email = pec.new_email)) != 0:
|
||||
if len(User.objects.filter(email=pec.new_email)) != 0:
|
||||
return render_to_response("email_exists.html", d)
|
||||
|
||||
|
||||
subject = render_to_string('emails/email_change_subject.txt',d)
|
||||
subject = render_to_string('emails/email_change_subject.txt', d)
|
||||
subject = ''.join(subject.splitlines())
|
||||
message = render_to_string('emails/confirm_email_change.txt',d)
|
||||
up = UserProfile.objects.get( user = user )
|
||||
message = render_to_string('emails/confirm_email_change.txt', d)
|
||||
up = UserProfile.objects.get(user=user)
|
||||
meta = up.get_meta()
|
||||
if 'old_emails' not in meta:
|
||||
meta['old_emails'] = []
|
||||
@@ -454,6 +563,7 @@ def confirm_email_change(request, key):
|
||||
|
||||
return render_to_response("email_change_successful.html", d)
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def change_name_request(request):
|
||||
''' Log a request for a new name. '''
|
||||
@@ -461,18 +571,18 @@ def change_name_request(request):
|
||||
raise Http404
|
||||
|
||||
try:
|
||||
pnc = PendingNameChange.objects.get(user = request.user)
|
||||
pnc = PendingNameChange.objects.get(user=request.user)
|
||||
except PendingNameChange.DoesNotExist:
|
||||
pnc = PendingNameChange()
|
||||
pnc.user = request.user
|
||||
pnc.new_name = request.POST['new_name']
|
||||
pnc.rationale = request.POST['rationale']
|
||||
if len(pnc.new_name)<2:
|
||||
return HttpResponse(json.dumps({'success':False,'error':'Name required'}))
|
||||
if len(pnc.rationale)<2:
|
||||
return HttpResponse(json.dumps({'success':False,'error':'Rationale required'}))
|
||||
if len(pnc.new_name) < 2:
|
||||
return HttpResponse(json.dumps({'success': False, 'error': 'Name required'}))
|
||||
if len(pnc.rationale) < 2:
|
||||
return HttpResponse(json.dumps({'success': False, 'error': 'Rationale required'}))
|
||||
pnc.save()
|
||||
return HttpResponse(json.dumps({'success':True}))
|
||||
return HttpResponse(json.dumps({'success': True}))
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@@ -532,4 +642,3 @@ def accept_name_change(request):
|
||||
pnc.delete()
|
||||
|
||||
return HttpResponse(json.dumps({'success': True}))
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from django.conf import settings
|
||||
|
||||
import views
|
||||
|
||||
|
||||
class TrackMiddleware:
|
||||
def process_request(self, request):
|
||||
try:
|
||||
@@ -11,10 +12,34 @@ class TrackMiddleware:
|
||||
# names/passwords.
|
||||
if request.META['PATH_INFO'] in ['/event', '/login']:
|
||||
return
|
||||
|
||||
event = { 'GET' : dict(request.GET),
|
||||
'POST' : dict(request.POST)}
|
||||
|
||||
|
||||
# Removes passwords from the tracking logs
|
||||
# WARNING: This list needs to be changed whenever we change
|
||||
# password handling functionality.
|
||||
#
|
||||
# As of the time of this comment, only 'password' is used
|
||||
# The rest are there for future extension.
|
||||
#
|
||||
# Passwords should never be sent as GET requests, but
|
||||
# this can happen due to older browser bugs. We censor
|
||||
# this too.
|
||||
#
|
||||
# We should manually confirm no passwords make it into log
|
||||
# files when we change this.
|
||||
|
||||
censored_strings = ['password', 'newpassword', 'new_password',
|
||||
'oldpassword', 'old_password']
|
||||
post_dict = dict(request.POST)
|
||||
get_dict = dict(request.GET)
|
||||
for string in censored_strings:
|
||||
if string in post_dict:
|
||||
post_dict[string] = '*' * 8
|
||||
if string in get_dict:
|
||||
get_dict[string] = '*' * 8
|
||||
|
||||
event = {'GET': dict(get_dict),
|
||||
'POST': dict(post_dict)}
|
||||
|
||||
# TODO: Confirm no large file uploads
|
||||
event = json.dumps(event)
|
||||
event = event[:512]
|
||||
|
||||
@@ -10,61 +10,64 @@ from django.conf import settings
|
||||
|
||||
log = logging.getLogger("tracking")
|
||||
|
||||
|
||||
def log_event(event):
|
||||
event_str = json.dumps(event)
|
||||
log.info(event_str[:settings.TRACK_MAX_EVENT])
|
||||
|
||||
|
||||
def user_track(request):
|
||||
try: # TODO: Do the same for many of the optional META parameters
|
||||
try: # TODO: Do the same for many of the optional META parameters
|
||||
username = request.user.username
|
||||
except:
|
||||
except:
|
||||
username = "anonymous"
|
||||
|
||||
try:
|
||||
scookie = request.META['HTTP_COOKIE'] # Get cookies
|
||||
scookie = ";".join([c.split('=')[1] for c in scookie.split(";") if "sessionid" in c]).strip() # Extract session ID
|
||||
except:
|
||||
try:
|
||||
scookie = request.META['HTTP_COOKIE'] # Get cookies
|
||||
scookie = ";".join([c.split('=')[1] for c in scookie.split(";") if "sessionid" in c]).strip() # Extract session ID
|
||||
except:
|
||||
scookie = ""
|
||||
|
||||
try:
|
||||
try:
|
||||
agent = request.META['HTTP_USER_AGENT']
|
||||
except:
|
||||
except:
|
||||
agent = ''
|
||||
|
||||
# TODO: Move a bunch of this into log_event
|
||||
event = {
|
||||
"username" : username,
|
||||
"session" : scookie,
|
||||
"ip" : request.META['REMOTE_ADDR'],
|
||||
"event_source" : "browser",
|
||||
"event_type" : request.GET['event_type'],
|
||||
"event" : request.GET['event'],
|
||||
"agent" : agent,
|
||||
"page" : request.GET['page'],
|
||||
"username": username,
|
||||
"session": scookie,
|
||||
"ip": request.META['REMOTE_ADDR'],
|
||||
"event_source": "browser",
|
||||
"event_type": request.GET['event_type'],
|
||||
"event": request.GET['event'],
|
||||
"agent": agent,
|
||||
"page": request.GET['page'],
|
||||
"time": datetime.datetime.utcnow().isoformat(),
|
||||
}
|
||||
log_event(event)
|
||||
return HttpResponse('success')
|
||||
|
||||
|
||||
def server_track(request, event_type, event, page=None):
|
||||
try:
|
||||
try:
|
||||
username = request.user.username
|
||||
except:
|
||||
except:
|
||||
username = "anonymous"
|
||||
|
||||
try:
|
||||
try:
|
||||
agent = request.META['HTTP_USER_AGENT']
|
||||
except:
|
||||
except:
|
||||
agent = ''
|
||||
|
||||
event = {
|
||||
"username" : username,
|
||||
"ip" : request.META['REMOTE_ADDR'],
|
||||
"event_source" : "server",
|
||||
"event_type" : event_type,
|
||||
"event" : event,
|
||||
"agent" : agent,
|
||||
"page" : page,
|
||||
"username": username,
|
||||
"ip": request.META['REMOTE_ADDR'],
|
||||
"event_source": "server",
|
||||
"event_type": event_type,
|
||||
"event": event,
|
||||
"agent": agent,
|
||||
"page": page,
|
||||
"time": datetime.datetime.utcnow().isoformat(),
|
||||
}
|
||||
log_event(event)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
This module aims to give a little more fine-tuned control of caching and cache
|
||||
invalidation. Import these instead of django.core.cache.
|
||||
|
||||
Note that 'default' is being preserved for user session caching, which we're
|
||||
Note that 'default' is being preserved for user session caching, which we're
|
||||
not migrating so as not to inconvenience users by logging them all out.
|
||||
"""
|
||||
from functools import wraps
|
||||
@@ -16,26 +16,27 @@ try:
|
||||
except Exception:
|
||||
cache = cache.cache
|
||||
|
||||
|
||||
def cache_if_anonymous(view_func):
|
||||
"""
|
||||
Many of the pages in edX are identical when the user is not logged
|
||||
in, but should not be cached when the user is logged in (because
|
||||
of the navigation bar at the top with the username).
|
||||
|
||||
in, but should not be cached when the user is logged in (because
|
||||
of the navigation bar at the top with the username).
|
||||
|
||||
The django middleware cache does not handle this correctly, because
|
||||
we access the session to put the csrf token in the header. This adds
|
||||
the cookie to the vary header, and so every page is cached seperately
|
||||
for each user (because each user has a different csrf token).
|
||||
|
||||
|
||||
Note that this decorator should only be used on views that do not
|
||||
contain the csrftoken within the html. The csrf token can be included
|
||||
in the header by ordering the decorators as such:
|
||||
|
||||
|
||||
@ensure_csrftoken
|
||||
@cache_if_anonymous
|
||||
def myView(request):
|
||||
"""
|
||||
|
||||
|
||||
@wraps(view_func)
|
||||
def _decorated(request, *args, **kwargs):
|
||||
if not request.user.is_authenticated():
|
||||
@@ -45,12 +46,12 @@ def cache_if_anonymous(view_func):
|
||||
if not response:
|
||||
response = view_func(request, *args, **kwargs)
|
||||
cache.set(cache_key, response, 60 * 3)
|
||||
|
||||
|
||||
return response
|
||||
|
||||
|
||||
else:
|
||||
#Don't use the cache
|
||||
return view_func(request, *args, **kwargs)
|
||||
|
||||
|
||||
return _decorated
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ from functools import wraps
|
||||
import copy
|
||||
import json
|
||||
|
||||
|
||||
def expect_json(view_function):
|
||||
@wraps(view_function)
|
||||
def expect_json_with_cloned_request(request, *args, **kwargs):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user