Commits

Andi Albrecht committed ccc8370 Draft

Lots of changes to get Rietveld up:

* Add users API.
* Add first port of GQL parser.
* API cleanups.
* Some minor fixes and enhancements.

  • Participants
  • Parent commits 348b196

Comments (0)

Files changed (19)

 Google's App Engine API based on pure Django. The helper makes it easier to
 re-use applications originally designed for the App Engine environment in a
 pure Django environment.
+
+
+Install
+
+ - add 'gae2django' to INSTALLED_APPS
+ - add 'gae2django.middleware.FixRequestUserMiddleware' to MIDDLEWARE_CLASSES
+ - at the top of manage.py add
+     import gae2django
+     gae2django.install()
  - Cleanup API
  - GQL
  - Unittests
+ - Implement datastore.Query
+ - Finish classes in ext.db

File examples/rietveld/Makefile

 default:
 	@echo "Run 'make all' to fetch required sources to run this example."
 
-all: codereview static templates django appengine_hook __init__.py manage.py settings.py dev.db
+all: codereview static templates django gae2django __init__.py manage.py dev.db
+	@echo "Run './manage.py runserver' to run Rietveld."
 
 clean: clean_local clean_external
 
 	rm -rf django
 
 clean_local:
-	unlink appengine_hook
-	rm -f __init__.py settings.py manage.py dev.db
+	unlink gae2django
+	rm -f __init__.py manage.py dev.db
 
-appengine_hook:
-	ln -s ../../appengine_hook .
+gae2django:
+	ln -s ../../gae2django .
 
 __init__.py:
 	touch __init__.py
 manage.py:
 	cp ../../manage.py .
 
-settings.py:
-	sed "s/'appengine_hook',/'appengine_hook',\n    'codereview',/g" ../../settings.py  > settings.py
-
 dev.db:
 	./manage.py syncdb
 

File examples/rietveld/rietveld_helper/__init__.py

+from django import template
+from django.contrib.auth.models import AnonymousUser
+
+from codereview import library
+
+def nickname(email, arg=None):
+    if isinstance(email, AnonymousUser):
+        email = None
+    return library.nickname(email, arg)
+
+# Make filters global
+template.defaultfilters.register.filter('nickname', nickname)

File examples/rietveld/rietveld_helper/middleware.py

+
+from codereview import models
+
+class AddUserToRequestMiddleware(object):
+    """Just add the account..."""
+
+    def process_request(self, request):
+        account = None
+        is_admin = False
+        if not request.user.is_anonymous():
+            account = models.Account.get_account_for_user(request.user)
+            is_admin = request.user.is_superuser()
+        models.Account.current_user_account = account
+        request.user_is_admin = is_admin
+
+    def process_view(self, request, view_func, view_args, view_kwargs):
+        is_rietveld = view_func.__module__.startswith('codereview')
+        user = request.user
+        if is_rietveld or view_func.__module__ == 'django.contrib.auth.views':
+            request.user = None
+        response = view_func(request, *view_args, **view_kwargs)
+        request.user = user
+        return response

File examples/rietveld/rietveld_helper/models.py

+from django.db import models
+
+# Create your models here.

File examples/rietveld/rietveld_helper/templates/registration/login.html

+{% extends "base.html" %}
+
+{% block body %}
+
+{% if form.errors %}
+<p>Your username and password didn't match. Please try again.</p>
+{% endif %}
+
+<form method="post" action=".">
+<table>
+<tr><td>{{ form.username.label_tag }}</td><td>{{ form.username }}</td></tr>
+<tr><td>{{ form.password.label_tag }}</td><td>{{ form.password }}</td></tr>
+</table>
+
+<input type="submit" value="login" />
+<input type="hidden" name="next" value="{{ next }}" />
+</form>
+
+{% endblock %}

File examples/rietveld/rietveld_helper/urls.py

+from django.conf.urls.defaults import *
+from django.contrib import admin
+
+from codereview.urls import urlpatterns
+
+urlpatterns += patterns('',
+        (r'^static/(?P<path>.*)$', 'django.views.static.serve',
+         {'document_root': 'static/'}),
+        (r'^accounts/login/$', 'django.contrib.auth.views.login'),
+        (r'^accounts/logout/$', 'django.contrib.auth.views.logout_then_login'),
+        ('^admin/(.*)', admin.site.root),
+        ('^_ah/admin', 'rietveld_helper.views.admin_redirect'),
+    )

File examples/rietveld/rietveld_helper/views.py

+from django.http import HttpResponseRedirect
+
+def admin_redirect(request):
+    return HttpResponseRedirect('/admin/')

File examples/rietveld/settings.py

+# Django settings for django_gae2django project.
+
+# NOTE: Keep the settings.py in examples directories in sync with this one!
+
+import os
+
+DEBUG = True
+TEMPLATE_DEBUG = DEBUG
+
+ADMINS = (
+    # ('Your Name', 'your_email@domain.com'),
+)
+
+MANAGERS = ADMINS
+
+DATABASE_ENGINE = 'sqlite3'    # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
+DATABASE_NAME = 'dev.db'       # Or path to database file if using sqlite3.
+DATABASE_USER = ''             # Not used with sqlite3.
+DATABASE_PASSWORD = ''         # Not used with sqlite3.
+DATABASE_HOST = ''             # Set to empty string for localhost. Not used with sqlite3.
+DATABASE_PORT = ''             # Set to empty string for default. Not used with sqlite3.
+
+# Local time zone for this installation. Choices can be found here:
+# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
+# although not all choices may be available on all operating systems.
+# If running in a Windows environment this must be set to the same as your
+# system time zone.
+TIME_ZONE = 'America/Chicago'
+
+# Language code for this installation. All choices can be found here:
+# http://www.i18nguy.com/unicode/language-identifiers.html
+LANGUAGE_CODE = 'en-us'
+
+SITE_ID = 1
+
+# If you set this to False, Django will make some optimizations so as not
+# to load the internationalization machinery.
+USE_I18N = True
+
+# Absolute path to the directory that holds media.
+# Example: "/home/media/media.lawrence.com/"
+MEDIA_ROOT = ''
+
+# URL that handles the media served from MEDIA_ROOT. Make sure to use a
+# trailing slash if there is a path component (optional in other cases).
+# Examples: "http://media.lawrence.com", "http://example.com/media/"
+MEDIA_URL = ''
+
+# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
+# trailing slash.
+# Examples: "http://foo.com/media/", "/media/".
+ADMIN_MEDIA_PREFIX = '/media/'
+
+# Make this unique, and don't share it with anybody.
+SECRET_KEY = 'el@4s$*(idwm5-87teftxlksckmy8$tyo7(tm!n-5x)zeuheex'
+
+# List of callables that know how to import templates from various sources.
+TEMPLATE_LOADERS = (
+    'django.template.loaders.filesystem.load_template_source',
+    'django.template.loaders.app_directories.load_template_source',
+#     'django.template.loaders.eggs.load_template_source',
+)
+
+MIDDLEWARE_CLASSES = (
+    'django.middleware.common.CommonMiddleware',
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
+    'gae2django.middleware.FixRequestUserMiddleware',
+    'rietveld_helper.middleware.AddUserToRequestMiddleware',
+    'django.middleware.doc.XViewMiddleware',
+)
+
+ROOT_URLCONF = 'rietveld_helper.urls'
+
+TEMPLATE_DIRS = (
+    os.path.join(os.path.dirname(__file__), 'templates'),
+)
+
+INSTALLED_APPS = (
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.sites',
+    'gae2django',
+    'rietveld_helper',
+    'codereview',
+)
+
+AUTH_PROFILE_MODULE = 'codereview.Account'

File gae2django/__init__.py

 """Provides a pure Django implementation of Google's App Engine API."""
 
 import logging
+import os
 import sys
 
 
 def install():
     """Imports the API and makes it available as 'google.appengine'."""
     import gaeapi
-    sys.modules['google'] = gaeapi
+    sys.modules['google'] = gaeapi
+    sys.modules['gaeapi'] = gaeapi
+    os.environ['SERVER_SOFTWARE'] = 'gae2django drop-in environment'

File gae2django/gaeapi/appengine/api/datastore.py

 class Query(dict):
-    
+
+    def __init__(self, model_class):
+        self._model_cls = model_class
+
+    def filter(self, property_operator, value):
+        raise NotImplementedError
+
+    def order(self, property):
+        raise NotImplementedError
+
+    def ancestor(self, ancestor):
+        raise NotImplementedError
+
+    def get(self):
+        raise NotImplementedError
+
+    def fetch(self, limit, offset=0):
+        raise NotImplementedError
+
+    def count(self, limit):
+        raise NotImplementedError
+
+
+# Copied from google.appengine.api.datastore.
+# Used in ext.gql.GQL
+def _AddOrAppend(dictionary, key, value):
+    """Adds the value to the existing values in the dictionary, if any.
+
+    If dictionary[key] doesn't exist, sets dictionary[key] to value.
+
+    If dictionary[key] is not a list, sets dictionary[key] to [old_value, value].
+
+    If dictionary[key] is a list, appends value to that list.
+
+    Args:
+      dictionary: a dict
+      key, value: anything
+  """
+    if key in dictionary:
+        existing_value = dictionary[key]
+        if isinstance(existing_value, list):
+          existing_value.append(value)
+        else:
+          dictionary[key] = [existing_value, value]
+    else:
+        dictionary[key] = value

File gae2django/gaeapi/appengine/api/mail.py

Empty file added.

File gae2django/gaeapi/appengine/ext/db.py

 import re
 import time
 
-from google.appengine.ext import gql
-
 from django.contrib.auth.models import User
 from django.contrib.contenttypes import generic
 from django.contrib.contenttypes.models import ContentType
         return new_cls
 
 
-class BaseModel(models.Model):
+class Model(models.Model):
 
     __metaclass__ = BaseModelMeta
 
         if 'key' in kwds:
             kwds['gae_key'] = kwds['key']
             del kwds['key']
-        super(BaseModel, self).__init__(*args, **kwds)
+        super(Model, self).__init__(*args, **kwds)
 
     @classmethod
     def get_or_insert(cls, key, **kwds):
                                        % (randrange(0, MAX_SESSION_KEY),
                                           pid, time.time(),
                                           self.__name__)).hexdigest()
-        super(BaseModel, self).save()
+        super(Model, self).save()
 
     @classmethod
     def gql(cls, clause, *args):
 class GqlQuery(object):
 
     def __init__(self, sql, *args):
+        from gaeapi.appengine.ext import gql
         self._sql = sql
         self._gql = gql.GQL(sql)
         self._args = None
             raise StopIteration
 
     def _execute(self):
+        from gaeapi.appengine.ext import gql
         if self._cursor:
             raise Exception, 'Already executed.'
         # Make sql local just for traceback
             self._execute()
         return self._results[offset:limit]
 
+    def count(self, limit):
+        idx = self._idx
+        c = len(list(self._results))
+        self._idx = idx
+        return c
+
 
 @transaction.commit_on_success
 def run_in_transaction(func, *args, **kwds):

File gae2django/gaeapi/appengine/ext/gql/__init__.py

-#!/usr/bin/env python
-#
-# Copyright 2007 Google Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
+# Customized version of Google's GQL class (google.appengine.ext.gql.GQL).
 
-"""GQL -- the SQL-like interface to the datastore.
-
-Defines the GQL-based query class, which is a query mechanism
-for the datastore which provides an alternative model for interacting with
-data stored.
-"""
-
-
-
-
-
-import calendar
 import datetime
 import heapq
 import logging
 import re
 import time
 
-from google.appengine.api import datastore
-from google.appengine.api import datastore_errors
-from google.appengine.api import datastore_types
-from google.appengine.api import users
-
+from gaeapi.appengine.api import datastore
+from gaeapi.appengine.api import users
+from gaeapi.appengine.ext import db
 
 LOG_LEVEL = logging.DEBUG - 1
 
-_EPOCH = datetime.datetime.utcfromtimestamp(0)
+# Hacks
+ASCENDING = 1
+DESCENDING = 2
 
-def Execute(query_string, *args, **keyword_args):
-  """Execute command to parse and run the query.
+class BadQueryError(Exception):
+    """BadQueryError"""
 
-  Calls the query parser code to build a proto-query which is an
-  unbound query. The proto-query is then bound into a real query and
-  executed.
-
-  Args:
-    query_string: properly formatted GQL query string.
-    args: rest of the positional arguments used to bind numeric references in
-          the query.
-    keyword_args: dictionary-based arguments (for named parameters).
-
-  Returns:
-    the result of running the query with *args.
-  """
-  app = keyword_args.pop('_app', None)
-  proto_query = GQL(query_string, _app=app)
-  return proto_query.Bind(args, keyword_args).Run()
+class BadArgumentError(Exception):
+    """BadArgumentError"""
 
 
 class GQL(object):
       query_string: properly formatted GQL query string.
 
     Raises:
-      datastore_errors.BadQueryError: if the query is not parsable.
+      BadQueryError: if the query is not parsable.
     """
     self._entity = ''
     self.__filters = {}
     self.__symbols = self.TOKENIZE_REGEX.findall(query_string)
     self.__next_symbol = 0
     if not self.__Select():
-      raise datastore_errors.BadQueryError(
-          'Unable to parse query')
+      raise BadQueryError('Unable to parse query')
     else:
       pass
 
       keyword_args: dictionary-based arguments (for named parameters).
 
     Raises:
-      datastore_errors.BadArgumentError: when arguments are left unbound
+      BadArgumentError: when arguments are left unbound
         (missing from the inputs arguments) or when arguments do not match the
         expected type.
 
     unused_args = input_args - used_args
     if unused_args:
       unused_values = [unused_arg + 1 for unused_arg in unused_args]
-      raise datastore_errors.BadArgumentError('Unused positional arguments %s' %
-                                              unused_values)
+      raise BadArgumentError('Unused positional arguments %s' % unused_values)
 
     if enumerated_queries:
       logging.debug('Multiple Queries Bound: %s' % enumerated_queries)
       BadQueryError and passes on an error message from the caller. Will raise
       BadQueryError on all calls.
     """
-    raise datastore_errors.BadQueryError(
-        'Type Cast Error: unable to cast %r with operation %s (%s)' %
+    raise BadQueryError('Type Cast Error: unable to cast %r with operation %s (%s)' %
         (values, operator.upper(), error_message))
 
   def __CastNop(self, values):
   def __CastKey(self, values):
     """Cast input values to Key() class using encoded string or tuple list."""
     if not len(values) % 2:
-      return datastore_types.Key.from_path(_app=self.__app, *values)
+      return db.Key.from_path(_app=self.__app, *values)
     elif len(values) == 1 and isinstance(values[0], str):
-      return datastore_types.Key(values[0])
+      return db.Key(values[0])
     else:
       self.__CastError('KEY', values,
                        'requires an even number of operands'
       if reference <= num_args:
         return args[reference - 1]
       else:
-        raise datastore_errors.BadArgumentError(
+        raise BadArgumentError(
             'Missing argument for bind, requires argument #%i, '
             'but only has %i args.' % (reference, num_args))
     elif isinstance(reference, str):
       if reference in keyword_args:
         return keyword_args[reference]
       else:
-        raise datastore_errors.BadArgumentError(
+        raise BadArgumentError(
             'Missing named arguments for bind, requires argument %s' %
             reference)
     else:
 
     if condition == '!=':
       if len(enumerated_queries) * 2 > self.MAX_ALLOWABLE_QUERIES:
-        raise datastore_errors.BadArgumentError(
+        raise BadArgumentError(
           'Cannot satisfy query -- too many IN/!= values.')
 
       num_iterations = CloneQueries(enumerated_queries, 2)
         enumerated_queries[2 * i + 1]['%s >' % identifier] = value
     elif condition.lower() == 'in':
       if not isinstance(value, list):
-        raise datastore_errors.BadArgumentError('List expected for "IN" filter')
+        raise BadArgumentError('List expected for "IN" filter')
 
       in_list_size = len(value)
       if len(enumerated_queries) * in_list_size > self.MAX_ALLOWABLE_QUERIES:
-        raise datastore_errors.BadArgumentError(
+        raise BadArgumentError(
           'Cannot satisfy query -- too many IN/!= values.')
 
       num_iterations = CloneQueries(enumerated_queries, in_list_size)
       BadQueryError on all calls to __Error()
     """
     if self.__next_symbol >= len(self.__symbols):
-      raise datastore_errors.BadQueryError(
+      raise BadQueryError(
           'Parse Error: %s at end of string' % error_message)
     else:
-      raise datastore_errors.BadQueryError(
+      raise BadQueryError(
           'Parse Error: %s at symbol %s' %
           (error_message, self.__symbols[self.__next_symbol]))
 
       assert condition.lower() == 'is'
 
     if condition.lower() != 'in' and operator == 'list':
-      sef.__Error('Only IN can process a list of values')
+      self.__Error('Only IN can process a list of values')
 
     self.__filters.setdefault(filter_rule, []).append((operator, parameters))
     return True
     identifier = self.__AcceptRegex(self.__identifier_regex)
     if identifier:
       if self.__Accept('DESC'):
-        self.__orderings.append((identifier, datastore.Query.DESCENDING))
+        self.__orderings.append((identifier, DESCENDING))
       elif self.__Accept('ASC'):
-        self.__orderings.append((identifier, datastore.Query.ASCENDING))
+        self.__orderings.append((identifier, ASCENDING))
       else:
-        self.__orderings.append((identifier, datastore.Query.ASCENDING))
+        self.__orderings.append((identifier, ASCENDING))
     else:
       self.__Error('Invalid ORDER BY Property')
 
         value2 = self.__GetValueForId(that, identifier, order)
 
         result = cmp(value1, value2)
-        if order == datastore.Query.DESCENDING:
+        if order == DESCENDING:
           result = -result
         if result:
           return result
       if self.__min_max_value_cache.has_key((entity_key, identifier)):
         value = self.__min_max_value_cache[(entity_key, identifier)]
       elif isinstance(value, list):
-        if sort_order == datastore.Query.DESCENDING:
+        if sort_order == DESCENDING:
           value = min(value)
         else:
           value = max(value)

File gae2django/gaeapi/appengine/runtime/__init__.py

+#
+# Copyright 2008 Andi Albrecht <albrecht.andi@gmail.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+class DeadlineExceededError(Exception):
+    """Not used."""

File gae2django/middleware.py

+#
+# Copyright 2008 Andi Albrecht <albrecht.andi@gmail.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import threading
+
+from django.conf import settings
+
+from gae2django.utils import CallableString
+
+_thread_locals = threading.local()
+
+
+# get_current_user hack found here:
+#   http://lukeplant.me.uk/blog.php?id=1107301634
+def get_current_user():
+    user = getattr(_thread_locals, 'user', None)
+    if user and user.is_anonymous():
+        return None
+    return user
+
+class ThreadLocals(object):
+    """Middleware that gets various objects from the
+    request object and saves them in thread local storage."""
+    def process_request(self, request):
+        _thread_locals.user = getattr(request, 'user', None)
+
+
+class FixRequestUserMiddleware(object):
+    def process_request(self, request):
+        if getattr(request, 'user', None) is None:
+            return
+        if not request.user.is_anonymous():
+            request.user.email = CallableString(request.user.email)
+            request.user.nickname = CallableString(request.user.username)
+            try:
+                profile = user.get_profile()
+                if hasattr(profile, 'nickname'):
+                    request.user.nickname = CallableString(profile.nickname)
+            except:
+                pass
+        else:
+            request.user.email = CallableString()
+            request.user.nickname = CallableString()

File gae2django/utils.py

+#
+# Copyright 2008 Andi Albrecht <albrecht.andi@gmail.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+class CallableString(str):
+    def __call__(self):
+        return self
+
+    def id(self):
+        try:
+            return int(self.split('_')[-1])
+        except:
+            return None
 # Django settings for django_gae2django project.
 
+# NOTE: Keep the settings.py in examples directories in sync with this one!
+
 DEBUG = True
 TEMPLATE_DEBUG = DEBUG
 
     'django.middleware.common.CommonMiddleware',
     'django.contrib.sessions.middleware.SessionMiddleware',
     'django.contrib.auth.middleware.AuthenticationMiddleware',
+    'gae2django.middleware.FixRequestUserMiddleware',
     'django.middleware.doc.XViewMiddleware',
 )