From dc85e46315dfb30ed9a3ead1df2c4e38411b7e6f Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 8 Jun 2012 16:55:38 -0400 Subject: [PATCH] Allow login in the cms, and read a particular course from mongo --- .../djangoapps/instructor}/__init__.py | 0 cms/djangoapps/instructor/models.py | 61 ++++++++++++++ cms/djangoapps/instructor/tests.py | 16 ++++ cms/djangoapps/instructor/views.py | 49 ++++++++++++ cms/envs/dev.py | 6 ++ cms/lib/keystore/__init__.py | 80 +++++++++++++++++++ cms/lib/keystore/django.py | 12 +++ cms/lib/keystore/exceptions.py | 7 ++ cms/lib/keystore/mongo.py | 26 ++++++ cms/templates/login.html | 11 +++ cms/urls.py | 5 +- cms/views.py | 11 ++- common/lib/django_future/__init__.py | 0 {lms => common}/lib/django_future/csrf.py | 0 requirements.txt | 1 + 15 files changed, 281 insertions(+), 4 deletions(-) rename {lms/lib/django_future => cms/djangoapps/instructor}/__init__.py (100%) create mode 100644 cms/djangoapps/instructor/models.py create mode 100644 cms/djangoapps/instructor/tests.py create mode 100644 cms/djangoapps/instructor/views.py create mode 100644 cms/lib/keystore/__init__.py create mode 100644 cms/lib/keystore/django.py create mode 100644 cms/lib/keystore/exceptions.py create mode 100644 cms/lib/keystore/mongo.py create mode 100644 cms/templates/login.html create mode 100644 common/lib/django_future/__init__.py rename {lms => common}/lib/django_future/csrf.py (100%) diff --git a/lms/lib/django_future/__init__.py b/cms/djangoapps/instructor/__init__.py similarity index 100% rename from lms/lib/django_future/__init__.py rename to cms/djangoapps/instructor/__init__.py diff --git a/cms/djangoapps/instructor/models.py b/cms/djangoapps/instructor/models.py new file mode 100644 index 0000000000..906aeee2f1 --- /dev/null +++ b/cms/djangoapps/instructor/models.py @@ -0,0 +1,61 @@ +""" +WE'RE USING MIGRATIONS! + +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/ +""" +import uuid + +from django.db import models +from django.contrib.auth.models import User + + +class UserProfile(models.Model): + class Meta: + db_table = "auth_userprofile" + + ## 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) + org = models.CharField(blank=True, max_length=255, db_index=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 + account, but that account is inactive. Once the user clicks + on the activation key, it becomes active. ''' + class Meta: + db_table = "auth_registration" + + user = models.ForeignKey(User, unique=True) + activation_key = models.CharField(('activation key'), max_length=32, unique=True, db_index=True) + + def register(self, user): + # MINOR TODO: Switch to crypto-secure key + self.activation_key = uuid.uuid4().hex + self.user = user + self.save() + + def activate(self): + self.user.is_active = True + 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) diff --git a/cms/djangoapps/instructor/tests.py b/cms/djangoapps/instructor/tests.py new file mode 100644 index 0000000000..501deb776c --- /dev/null +++ b/cms/djangoapps/instructor/tests.py @@ -0,0 +1,16 @@ +""" +This file demonstrates writing tests using the unittest module. These will pass +when you run "manage.py test". + +Replace this with more appropriate tests for your application. +""" + +from django.test import TestCase + + +class SimpleTest(TestCase): + def test_basic_addition(self): + """ + Tests that 1 + 1 always equals 2. + """ + self.assertEqual(1 + 1, 2) diff --git a/cms/djangoapps/instructor/views.py b/cms/djangoapps/instructor/views.py new file mode 100644 index 0000000000..fbb341b468 --- /dev/null +++ b/cms/djangoapps/instructor/views.py @@ -0,0 +1,49 @@ +import logging + +from django.views.decorators.http import require_http_methods, require_POST, require_GET +from django.contrib.auth import logout, authenticate, login +from django.shortcuts import redirect +from mitxmako.shortcuts import render_to_response + +from django_future.csrf import ensure_csrf_cookie + +log = logging.getLogger("mitx.student") + + +@require_http_methods(['GET', 'POST']) +def do_login(request): + if request.method == 'POST': + return post_login(request) + elif request.method == 'GET': + return get_login(request) + + +@require_POST +@ensure_csrf_cookie +def post_login(request): + username = request.POST['username'] + password = request.POST['password'] + user = authenticate(username=username, password=password) + if user is not None: + if user.is_active: + login(request, user) + return redirect(request.POST.get('next', '/')) + else: + raise Exception("Can't log in, account disabled") + else: + raise Exception("Can't log in, invalid authentication") + + +@require_GET +@ensure_csrf_cookie +def get_login(request): + return render_to_response('login.html', { + 'next': request.GET.get('next') + }) + + +@ensure_csrf_cookie +def logout_user(request): + ''' HTTP request to log in the user. Redirects to marketing page''' + logout(request) + return redirect('/') diff --git a/cms/envs/dev.py b/cms/envs/dev.py index 72a5e512c4..f7277b3d3f 100644 --- a/cms/envs/dev.py +++ b/cms/envs/dev.py @@ -6,6 +6,12 @@ from .common import * DEBUG = True TEMPLATE_DEBUG = DEBUG +KEYSTORE = { + 'host': 'localhost', + 'db': 'mongo_base', + 'collection': 'key_store', +} + DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', diff --git a/cms/lib/keystore/__init__.py b/cms/lib/keystore/__init__.py new file mode 100644 index 0000000000..5e6374cf4a --- /dev/null +++ b/cms/lib/keystore/__init__.py @@ -0,0 +1,80 @@ +""" +This module provides an abstraction for working objects that conceptually have +the following attributes: + + location: An identifier for an item, of which there might be many revisions + children: A list of urls for other items required to fully define this object + data: A set of nested data needed to define this object + editor: The editor/owner of the object + parents: Url pointers for objects that this object was derived from + revision: What revision of the item this is +""" + + +class Location(object): + ''' Encodes a location. + Can be: + * String (url) + * Tuple + * Dictionary + ''' + def __init__(self, location): + self.update(location) + + def update(self, location): + if isinstance(location, basestring): + self.tag = location.split('/')[0][:-1] + (self.org, self.course, self.category, self.name) = location.split('/')[2:] + elif isinstance(location, list): + (self.tag, self.org, self.course, self.category, self.name) = location + elif isinstance(location, dict): + self.tag = location['tag'] + self.org = location['org'] + self.course = location['course'] + self.category = location['category'] + self.name = location['name'] + elif isinstance(location, Location): + self.update(location.list()) + + def url(self): + return "i4x://{org}/{course}/{category}/{name}".format(**self.dict()) + + def list(self): + return [self.tag, self.org, self.course, self.category, self.name] + + def dict(self): + return {'tag': self.tag, + 'org': self.org, + 'course': self.course, + 'category': self.category, + 'name': self.name} + + def to_json(self): + return self.dict() + + +class KeyStore(object): + def get_children_for_item(self, location): + """ + Returns the children for the most recent revision of the object + with the specified location. + + If no object is found at that location, raises keystore.exceptions.ItemNotFoundError + """ + raise NotImplementedError + + +class KeyStoreItem(object): + """ + An object from a KeyStore, which can be saved back to that keystore + """ + def __init__(self, location, children, data, editor, parents, revision): + self.location = location + self.children = children + self.data = data + self.editor = editor + self.parents = parents + self.revision = revision + + def save(self): + raise NotImplementedError diff --git a/cms/lib/keystore/django.py b/cms/lib/keystore/django.py new file mode 100644 index 0000000000..b6ffb83b5c --- /dev/null +++ b/cms/lib/keystore/django.py @@ -0,0 +1,12 @@ +""" +Module that provides a connection to the keystore specified in the django settings. + +Passes settings.KEYSTORE as kwargs to MongoKeyStore +""" + +from __future__ import absolute_import + +from django.conf import settings +from .mongo import MongoKeyStore + +keystore = MongoKeyStore(**settings.KEYSTORE) diff --git a/cms/lib/keystore/exceptions.py b/cms/lib/keystore/exceptions.py new file mode 100644 index 0000000000..b66470859f --- /dev/null +++ b/cms/lib/keystore/exceptions.py @@ -0,0 +1,7 @@ +""" +Exceptions thrown by KeyStore objects +""" + + +class ItemNotFoundError(Exception): + pass diff --git a/cms/lib/keystore/mongo.py b/cms/lib/keystore/mongo.py new file mode 100644 index 0000000000..fc190ee098 --- /dev/null +++ b/cms/lib/keystore/mongo.py @@ -0,0 +1,26 @@ +import pymongo +from . import KeyStore +from .exceptions import ItemNotFoundError + + +class MongoKeyStore(KeyStore): + """ + A Mongodb backed KeyStore + """ + def __init__(self, host, db, collection, port=27017): + self.collection = pymongo.connection.Connection( + host=host, + port=port + )[db][collection] + + def get_children_for_item(self, location): + item = self.collection.find_one( + {'location': location.dict()}, + fields={'children': True}, + sort=[('revision', pymongo.ASCENDING)], + ) + + if item is None: + raise ItemNotFoundError() + + return item['children'] diff --git a/cms/templates/login.html b/cms/templates/login.html new file mode 100644 index 0000000000..03ea5f967c --- /dev/null +++ b/cms/templates/login.html @@ -0,0 +1,11 @@ +
+ + + % if next is not None: + + % endif + + Username: + Possword: + +
diff --git a/cms/urls.py b/cms/urls.py index 80e2b19e9d..55d7a1086e 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -4,6 +4,7 @@ from django.conf.urls.defaults import patterns, url # from django.contrib import admin # admin.autodiscover() -urlpatterns = patterns('cms.views', - url(r'^(?P[^/]+)/calendar/', 'calendar', name='calendar'), +urlpatterns = patterns('', + url(r'^(?P[^/]+)/(?P[^/]+)/calendar/', 'cms.views.calendar', name='calendar'), + url(r'^accounts/login/', 'instructor.views.do_login', name='login'), ) diff --git a/cms/views.py b/cms/views.py index d0d4f4871c..c6786b03c4 100644 --- a/cms/views.py +++ b/cms/views.py @@ -1,5 +1,12 @@ from mitxmako.shortcuts import render_to_response +from keystore import Location +from keystore.django import keystore +from django.contrib.auth.decorators import login_required -def calendar(request, course): - return render_to_response('calendar.html', {}) +@login_required +def calendar(request, org, course): + weeks = keystore.get_children_for_item( + Location(['i4x', org, course, 'Course', 'course']) + ) + return render_to_response('calendar.html', {'weeks': weeks}) diff --git a/common/lib/django_future/__init__.py b/common/lib/django_future/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/lib/django_future/csrf.py b/common/lib/django_future/csrf.py similarity index 100% rename from lms/lib/django_future/csrf.py rename to common/lib/django_future/csrf.py diff --git a/requirements.txt b/requirements.txt index 82b28b09cf..5e95e1bf9e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,3 +23,4 @@ requests sympy newrelic glob2 +pymongo