summaryrefslogtreecommitdiffstats
path: root/appengine_django
diff options
context:
space:
mode:
Diffstat (limited to 'appengine_django')
-rw-r--r--appengine_django/__init__.py528
-rw-r--r--appengine_django/__init__.pycbin0 -> 19408 bytes
-rw-r--r--appengine_django/auth/__init__.py25
-rw-r--r--appengine_django/auth/__init__.pycbin0 -> 478 bytes
-rw-r--r--appengine_django/auth/decorators.py31
-rw-r--r--appengine_django/auth/decorators.pycbin0 -> 1008 bytes
-rw-r--r--appengine_django/auth/middleware.py36
-rw-r--r--appengine_django/auth/middleware.pycbin0 -> 1428 bytes
-rw-r--r--appengine_django/auth/models.py172
-rw-r--r--appengine_django/auth/models.pycbin0 -> 7999 bytes
-rw-r--r--appengine_django/auth/templatetags.py62
-rw-r--r--appengine_django/auth/templatetags.pycbin0 -> 2124 bytes
-rw-r--r--appengine_django/auth/tests.py58
-rw-r--r--appengine_django/conf/app_template/__init__.py0
-rw-r--r--appengine_django/conf/app_template/models.py4
-rw-r--r--appengine_django/conf/app_template/views.py1
-rwxr-xr-xappengine_django/db/__init__.py20
-rw-r--r--appengine_django/db/__init__.pycbin0 -> 183 bytes
-rwxr-xr-xappengine_django/db/base.py150
-rw-r--r--appengine_django/db/base.pycbin0 -> 6210 bytes
-rwxr-xr-xappengine_django/db/creation.py37
-rw-r--r--appengine_django/db/creation.pycbin0 -> 1288 bytes
-rw-r--r--appengine_django/mail.py95
-rw-r--r--appengine_django/mail.pycbin0 -> 3351 bytes
-rwxr-xr-xappengine_django/management/__init__.py0
-rw-r--r--appengine_django/management/__init__.pycbin0 -> 158 bytes
-rwxr-xr-xappengine_django/management/commands/__init__.py0
-rw-r--r--appengine_django/management/commands/__init__.pycbin0 -> 167 bytes
-rwxr-xr-xappengine_django/management/commands/console.py49
-rwxr-xr-xappengine_django/management/commands/flush.py36
-rwxr-xr-xappengine_django/management/commands/reset.py32
-rwxr-xr-xappengine_django/management/commands/rollback.py52
-rwxr-xr-xappengine_django/management/commands/runserver.py77
-rw-r--r--appengine_django/management/commands/runserver.pycbin0 -> 2710 bytes
-rw-r--r--appengine_django/management/commands/startapp.py43
-rw-r--r--appengine_django/management/commands/startapp.pycbin0 -> 1690 bytes
-rwxr-xr-xappengine_django/management/commands/testserver.py71
-rwxr-xr-xappengine_django/management/commands/update.py51
-rwxr-xr-xappengine_django/management/commands/vacuum_indexes.py52
-rwxr-xr-xappengine_django/models.py182
-rw-r--r--appengine_django/models.pycbin0 -> 8148 bytes
-rw-r--r--appengine_django/replacement_imp.py26
-rw-r--r--appengine_django/replacement_imp.pycbin0 -> 662 bytes
-rwxr-xr-xappengine_django/serializer/__init__.py0
-rw-r--r--appengine_django/serializer/__init__.pycbin0 -> 158 bytes
-rwxr-xr-xappengine_django/serializer/python.py130
-rw-r--r--appengine_django/serializer/python.pycbin0 -> 4241 bytes
-rwxr-xr-xappengine_django/serializer/xml.py147
-rw-r--r--appengine_django/sessions/__init__.py0
-rw-r--r--appengine_django/sessions/__init__.pycbin0 -> 156 bytes
-rw-r--r--appengine_django/sessions/backends/__init__.py0
-rw-r--r--appengine_django/sessions/backends/__init__.pycbin0 -> 165 bytes
-rw-r--r--appengine_django/sessions/backends/db.py82
-rw-r--r--appengine_django/sessions/backends/db.pycbin0 -> 2820 bytes
-rw-r--r--appengine_django/sessions/models.py22
-rw-r--r--appengine_django/sessions/models.pycbin0 -> 622 bytes
-rw-r--r--appengine_django/tests/__init__.py56
-rwxr-xr-xappengine_django/tests/commands_test.py183
-rwxr-xr-xappengine_django/tests/core_test.py37
-rwxr-xr-xappengine_django/tests/db_test.py62
-rw-r--r--appengine_django/tests/memcache_test.py43
-rwxr-xr-xappengine_django/tests/model_test.py110
-rwxr-xr-xappengine_django/tests/serialization_test.py310
63 files changed, 3072 insertions, 0 deletions
diff --git a/appengine_django/__init__.py b/appengine_django/__init__.py
new file mode 100644
index 0000000..629695d
--- /dev/null
+++ b/appengine_django/__init__.py
@@ -0,0 +1,528 @@
+#!/usr/bin/python2.5
+#
+# Copyright 2008 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.
+
+"""Support for integrating a Django project with the appengine infrastructure.
+
+This requires Django 1.0beta1 or greater.
+
+This module enables you to use the Django manage.py utility and *some* of it's
+subcommands. View the help of manage.py for exact details.
+
+Additionally this module takes care of initialising the datastore (and a test
+datastore) so that the Django test infrastructure can be used for your
+appengine project.
+
+To use this module add the following two lines to your main.py and manage.py
+scripts at the end of your imports:
+ from appengine_django import InstallAppengineHelperForDjango
+ InstallAppengineHelperForDjango()
+
+If you would like to use a version of Django other than that provided by the
+system all you need to do is include it in a directory just above this helper,
+eg:
+ appengine_django/__init__.py - This file
+ django/... - your private copy of Django.
+"""
+
+import logging
+import os
+import re
+import sys
+import unittest
+import zipfile
+
+
+DIR_PATH = os.path.abspath(os.path.dirname(__file__))
+PARENT_DIR = os.path.dirname(DIR_PATH)
+if PARENT_DIR.endswith(".zip"):
+ # Check for appengine_django itself being in a zipfile.
+ PARENT_DIR = os.path.dirname(PARENT_DIR)
+
+# Add this project to the start of sys path to enable direct imports.
+sys.path = [PARENT_DIR,] + sys.path
+
+# Try to import the appengine code from the system path.
+try:
+ from google.appengine.api import apiproxy_stub_map
+except ImportError, e:
+ # Not on the system path. Build a list of alternative paths where it may be.
+ # First look within the project for a local copy, then look for where the Mac
+ # OS SDK installs it.
+ paths = [os.path.join(PARENT_DIR, '.google_appengine'),
+ os.path.join(PARENT_DIR, 'google_appengine'),
+ '/usr/local/google_appengine']
+ # Then if on windows, look for where the Windows SDK installed it.
+ for path in os.environ.get('PATH', '').split(';'):
+ path = path.rstrip('\\')
+ if path.endswith('google_appengine'):
+ paths.append(path)
+ try:
+ from win32com.shell import shell
+ from win32com.shell import shellcon
+ id_list = shell.SHGetSpecialFolderLocation(
+ 0, shellcon.CSIDL_PROGRAM_FILES)
+ program_files = shell.SHGetPathFromIDList(id_list)
+ paths.append(os.path.join(program_files, 'Google',
+ 'google_appengine'))
+ except ImportError, e:
+ # Not windows.
+ pass
+ # Loop through all possible paths and look for the SDK dir.
+ SDK_PATH = None
+ for sdk_path in paths:
+ if os.path.exists(sdk_path):
+ SDK_PATH = os.path.realpath(sdk_path)
+ break
+ if SDK_PATH is None:
+ # The SDK could not be found in any known location.
+ sys.stderr.write("The Google App Engine SDK could not be found!\n")
+ sys.stderr.write("See README for installation instructions.\n")
+ sys.exit(1)
+ if SDK_PATH == os.path.join(PARENT_DIR, 'google_appengine'):
+ logging.warn('Loading the SDK from the \'google_appengine\' subdirectory '
+ 'is now deprecated!')
+ logging.warn('Please move the SDK to a subdirectory named '
+ '\'.google_appengine\' instead.')
+ logging.warn('See README for further details.')
+ # Add the SDK and the libraries within it to the system path.
+ EXTRA_PATHS = [
+ SDK_PATH,
+ os.path.join(SDK_PATH, 'lib', 'antlr3'),
+ os.path.join(SDK_PATH, 'lib', 'django'),
+ os.path.join(SDK_PATH, 'lib', 'webob'),
+ os.path.join(SDK_PATH, 'lib', 'yaml', 'lib'),
+ ]
+ # Add SDK paths at the start of sys.path, but after the local directory which
+ # was added to the start of sys.path on line 50 above. The local directory
+ # must come first to allow the local imports to override the SDK and
+ # site-packages directories.
+ sys.path = sys.path[0:1] + EXTRA_PATHS + sys.path[1:]
+ from google.appengine.api import apiproxy_stub_map
+
+# Look for a zipped copy of Django.
+have_django_zip = False
+django_zip_path = os.path.join(PARENT_DIR, 'django.zip')
+if os.path.exists(django_zip_path):
+ have_django_zip = True
+ sys.path.insert(1, django_zip_path)
+
+# Remove the standard version of Django if a local copy has been provided.
+if have_django_zip or os.path.exists(os.path.join(PARENT_DIR, 'django')):
+ for k in [k for k in sys.modules if k.startswith('django')]:
+ del sys.modules[k]
+
+# Must set this env var *before* importing any more of Django.
+os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'
+
+from django import VERSION
+from django.conf import settings
+
+from google.appengine.api import yaml_errors
+
+# Flags made available this module
+appid = None
+have_appserver = False
+
+# Hide everything other than the flags above and the install function.
+__all__ = ("appid", "have_appserver", "have_django_zip",
+ "django_zip_path", "InstallAppengineHelperForDjango")
+
+
+INCOMPATIBLE_COMMANDS = ["adminindex", "createcachetable", "dbshell",
+ "inspectdb", "runfcgi", "syncdb", "validate"]
+
+
+def LoadAppengineEnvironment():
+ """Loads the appengine environment.
+
+ Returns:
+ This function has no return value, but it sets the following parameters on
+ this package:
+ - appid: The name of the application.
+ - have_appserver: Boolean parameter which is True if the code is being run
+ from within the appserver environment.
+ """
+ global appid, have_appserver
+
+ # Detect if we are running under an appserver.
+ have_appserver = False
+ stub = apiproxy_stub_map.apiproxy.GetStub("datastore_v3")
+ if stub:
+ have_appserver = True
+
+ # Load the application identifier.
+ if have_appserver:
+ appid = os.environ.get("APPLICATION_ID", "unknown")
+ else:
+ # Running as manage.py script, read from config file.
+ try:
+ from google.appengine.tools import dev_appserver
+ appconfig, unused_matcher = dev_appserver.LoadAppConfig(PARENT_DIR, {})
+ appid = appconfig.application
+ except (ImportError, yaml_errors.EventListenerYAMLError), e:
+ logging.warn("Could not read the Application ID from app.yaml. "
+ "This may break things in unusual ways!")
+ # Something went wrong.
+ appid = "unknown"
+
+ logging.debug("Loading application '%s' %s an appserver" %
+ (appid, have_appserver and "with" or "without"))
+
+
+def InstallAppengineDatabaseBackend():
+ """Installs the appengine database backend into Django.
+
+ The appengine database lives in the db/ subdirectory of this package, but is
+ known as "appengine" to Django. This function installs the module where
+ Django expects to find its database backends.
+ """
+ from appengine_django import db
+ sys.modules['django.db.backends.appengine'] = db
+ logging.debug("Installed appengine database backend")
+
+
+def InstallGoogleMemcache():
+ """Installs the Google memcache into Django.
+
+ By default django tries to import standard memcache module.
+ Because appengine memcache is API compatible with Python memcache module,
+ we can trick Django to think it is installed and to use it.
+
+ Now you can use CACHE_BACKEND = 'memcached://' in settings.py. IP address
+ and port number are not required.
+ """
+ from google.appengine.api import memcache
+ sys.modules['memcache'] = memcache
+ logging.debug("Installed App Engine memcache backend")
+
+
+def InstallDjangoModuleReplacements():
+ """Replaces internal Django modules with App Engine compatible versions."""
+
+ # Replace the session module with a partial replacement overlay using
+ # __path__ so that portions not replaced will fall through to the original
+ # implementation.
+ try:
+ from django.contrib import sessions
+ orig_path = sessions.__path__[0]
+ sessions.__path__.insert(0, os.path.join(DIR_PATH, 'sessions'))
+ from django.contrib.sessions import backends
+ backends.__path__.append(os.path.join(orig_path, 'backends'))
+ except ImportError:
+ logging.debug("No Django session support available")
+
+ # Replace incompatible dispatchers.
+ import django.core.signals
+ import django.db
+ import django.dispatch.dispatcher
+
+ # Rollback occurs automatically on Google App Engine. Disable the Django
+ # rollback handler.
+ CheckedException = KeyError
+ def _disconnectSignal():
+ django.core.signals.got_request_exception.disconnect(
+ django.db._rollback_on_exception)
+
+ try:
+ _disconnectSignal()
+ except CheckedException, e:
+ logging.debug("Django rollback handler appears to be already disabled.")
+
+def PatchDjangoSerializationModules():
+ """Monkey patches the Django serialization modules.
+
+ The standard Django serialization modules to not correctly handle the
+ datastore models provided by this package. This method installs replacements
+ for selected modules and methods to give Django the capability to correctly
+ serialize and deserialize datastore models.
+ """
+ # These can't be imported until InstallAppengineDatabaseBackend has run.
+ from django.core.serializers import python
+ from appengine_django.serializer.python import Deserializer
+ if not hasattr(settings, "SERIALIZATION_MODULES"):
+ settings.SERIALIZATION_MODULES = {}
+ base_module = "appengine_django"
+ settings.SERIALIZATION_MODULES["xml"] = "%s.serializer.xml" % base_module
+ python.Deserializer = Deserializer
+ PatchDeserializedObjectClass()
+ DisableModelValidation()
+ logging.debug("Installed appengine json and python serialization modules")
+
+
+def PatchDeserializedObjectClass():
+ """Patches the DeserializedObject class.
+
+ The default implementation calls save directly on the django Model base
+ class to avoid pre-save handlers. The model class provided by this package
+ is not derived from the Django Model class and therefore must be called
+ directly.
+
+ Additionally we need to clear the internal _parent attribute as it may
+ contain a FakeParent class that is used to deserialize instances without
+ needing to load the parent instance itself. See the PythonDeserializer for
+ more details.
+ """
+ # This can't be imported until InstallAppengineDatabaseBackend has run.
+ from django.core.serializers import base
+ class NewDeserializedObject(base.DeserializedObject):
+ def save(self, save_m2m=True):
+ self.object.save()
+ self.object._parent = None
+ base.DeserializedObject = NewDeserializedObject
+ logging.debug("Replacement DeserializedObject class installed")
+
+def DisableModelValidation():
+ """Disables Django's model validation routines.
+
+ The model validation is primarily concerned with validating foreign key
+ references. There is no equivalent checking code for datastore References at
+ this time.
+
+ Validation needs to be disabled or serialization/deserialization will fail.
+ """
+ from django.core.management import validation
+ validation.get_validation_errors = lambda x, y=0: 0
+ logging.debug("Django SQL model validation disabled")
+
+def CleanupDjangoSettings():
+ """Removes incompatible entries from the django settings module."""
+
+ # Ensure this module is installed as an application.
+ apps = getattr(settings, "INSTALLED_APPS", ())
+ found = False
+ for app in apps:
+ if app.endswith("appengine_django"):
+ found = True
+ break
+ if not found:
+ logging.warn("appengine_django module is not listed as an application!")
+ apps += ("appengine_django",)
+ setattr(settings, "INSTALLED_APPS", apps)
+ logging.info("Added 'appengine_django' as an application")
+
+ # Ensure the database backend is appropriately configured.
+ dbe = getattr(settings, "DATABASE_ENGINE", "")
+ if dbe != "appengine":
+ settings.DATABASE_ENGINE = "appengine"
+ logging.warn("DATABASE_ENGINE is not configured as 'appengine'. "
+ "Value overriden!")
+ for var in ["NAME", "USER", "PASSWORD", "HOST", "PORT"]:
+ val = getattr(settings, "DATABASE_%s" % var, "")
+ if val:
+ setattr(settings, "DATABASE_%s" % var, "")
+ logging.warn("DATABASE_%s should be blank. Value overriden!")
+
+ # Remove incompatible middleware modules.
+ mw_mods = list(getattr(settings, "MIDDLEWARE_CLASSES", ()))
+ disallowed_middleware_mods = (
+ 'django.middleware.doc.XViewMiddleware',)
+ for modname in mw_mods[:]:
+ if modname in disallowed_middleware_mods:
+ # Currently only the CommonMiddleware has been ported. As other base
+ # modules are converted, remove from the disallowed_middleware_mods
+ # tuple.
+ mw_mods.remove(modname)
+ logging.warn("Middleware module '%s' is not compatible. Removed!" %
+ modname)
+ setattr(settings, "MIDDLEWARE_CLASSES", tuple(mw_mods))
+
+ # Remove incompatible application modules
+ app_mods = list(getattr(settings, "INSTALLED_APPS", ()))
+ disallowed_apps = (
+ 'django.contrib.contenttypes',
+ 'django.contrib.sites',)
+ for app in app_mods[:]:
+ if app in disallowed_apps:
+ app_mods.remove(app)
+ logging.warn("Application module '%s' is not compatible. Removed!" % app)
+ setattr(settings, "INSTALLED_APPS", tuple(app_mods))
+
+ # Remove incompatible session backends.
+ session_backend = getattr(settings, "SESSION_ENGINE", "")
+ if session_backend.endswith("file"):
+ logging.warn("File session backend is not compatible. Overriden "
+ "to use db backend!")
+ setattr(settings, "SESSION_ENGINE", "django.contrib.sessions.backends.db")
+
+
+def ModifyAvailableCommands():
+ """Removes incompatible commands and installs replacements where possible."""
+ if have_appserver:
+ # Commands are not used when running from an appserver.
+ return
+ from django.core import management
+ project_directory = os.path.join(__path__[0], "../")
+ if have_django_zip:
+ FindCommandsInZipfile.orig = management.find_commands
+ management.find_commands = FindCommandsInZipfile
+ management.get_commands()
+ # Replace startapp command which is set by previous call to get_commands().
+ from appengine_django.management.commands.startapp import ProjectCommand
+ management._commands['startapp'] = ProjectCommand(project_directory)
+ RemoveCommands(management._commands)
+ logging.debug("Removed incompatible Django manage.py commands")
+
+
+def FindCommandsInZipfile(management_dir):
+ """
+ Given a path to a management directory, returns a list of all the command
+ names that are available.
+
+ This implementation also works when Django is loaded from a zip.
+
+ Returns an empty list if no commands are defined.
+ """
+ zip_marker = ".zip%s" % os.sep
+ if zip_marker not in management_dir:
+ return FindCommandsInZipfile.orig(management_dir)
+
+ # Django is sourced from a zipfile, ask zip module for a list of files.
+ filename, path = management_dir.split(zip_marker)
+ zipinfo = zipfile.ZipFile("%s.zip" % filename)
+
+ # Add commands directory to management path.
+ path = os.path.join(path, "commands")
+
+ # The zipfile module returns paths in the format of the operating system
+ # that created the zipfile! This may not match the path to the zipfile
+ # itself. Convert operating system specific characters to a standard
+ # character (#) to compare paths to work around this.
+ path_normalise = re.compile(r"[/\\]")
+ path = path_normalise.sub("#", path)
+ def _IsCmd(t):
+ """Returns true if t matches the criteria for a command module."""
+ filename = os.path.basename(t)
+ t = path_normalise.sub("#", t)
+ if not t.startswith(path):
+ return False
+ if filename.startswith("_") or not t.endswith(".py"):
+ return False
+ return True
+
+ return [os.path.basename(f)[:-3] for f in zipinfo.namelist() if _IsCmd(f)]
+
+
+def RemoveCommands(command_dict):
+ """Removes incompatible commands from the specified command dictionary."""
+ for cmd in command_dict.keys():
+ if cmd.startswith("sql"):
+ del command_dict[cmd]
+ elif cmd in INCOMPATIBLE_COMMANDS:
+ del command_dict[cmd]
+
+
+def InstallReplacementImpModule():
+ """Install a replacement for the imp module removed by the appserver.
+
+ This is only to find mangement modules provided by applications.
+ """
+ if not have_appserver:
+ return
+ modname = 'appengine_django.replacement_imp'
+ imp_mod = __import__(modname, {}, [], [''])
+ sys.modules['imp'] = imp_mod
+ logging.debug("Installed replacement imp module")
+
+
+def InstallAppengineHelperForDjango():
+ """Installs and Patches all of the classes/methods required for integration.
+
+ If the variable DEBUG_APPENGINE_DJANGO is set in the environment verbose
+ logging of the actions taken will be enabled.
+ """
+ if VERSION < (1, 0, None):
+ logging.error("Django 1.0 or greater is required!")
+ sys.exit(1)
+
+ if os.getenv("DEBUG_APPENGINE_DJANGO"):
+ logging.getLogger().setLevel(logging.DEBUG)
+ else:
+ logging.getLogger().setLevel(logging.INFO)
+ logging.debug("Loading the Google App Engine Helper for Django...")
+
+ # Force Django to reload its settings.
+ settings._target = None
+
+ LoadAppengineEnvironment()
+ InstallReplacementImpModule()
+ InstallAppengineDatabaseBackend()
+ InstallModelForm()
+ InstallGoogleMemcache()
+ InstallDjangoModuleReplacements()
+ PatchDjangoSerializationModules()
+ CleanupDjangoSettings()
+ ModifyAvailableCommands()
+ InstallGoogleSMTPConnection()
+ InstallAuthentication()
+
+ logging.debug("Successfully loaded the Google App Engine Helper for Django.")
+
+
+def InstallGoogleSMTPConnection():
+ from appengine_django import mail as gmail
+ from django.core import mail
+ logging.debug("Installing Google Email Adapter for Django")
+ mail.SMTPConnection = gmail.GoogleSMTPConnection
+ mail.mail_admins = gmail.mail_admins
+ mail.mail_managers = gmail.mail_managers
+
+
+def InstallAuthentication():
+ if "django.contrib.auth" not in settings.INSTALLED_APPS:
+ return
+ try:
+ from appengine_django.auth import models as helper_models
+ from django.contrib.auth import models
+ models.User = helper_models.User
+ models.Group = helper_models.Group
+ models.Permission = helper_models.Permission
+ models.Message = helper_models.Message
+ from django.contrib.auth import middleware as django_middleware
+ from appengine_django.auth.middleware import AuthenticationMiddleware
+ django_middleware.AuthenticationMiddleware = AuthenticationMiddleware
+ from django.contrib.auth import decorators as django_decorators
+ from appengine_django.auth.decorators import login_required
+ django_decorators.login_required = login_required
+ from django.contrib import auth as django_auth
+ from django.contrib.auth import tests as django_tests
+ django_auth.suite = unittest.TestSuite
+ django_tests.suite = unittest.TestSuite
+ logging.debug("Installing authentication framework")
+ except ImportError:
+ logging.debug("No Django authentication support available")
+
+
+def InstallModelForm():
+ """Replace Django ModelForm with the AppEngine ModelForm."""
+ # This MUST happen as early as possible, but after any auth model patching.
+ from google.appengine.ext.db import djangoforms as aeforms
+ try:
+ # pre 1.0
+ from django import newforms as forms
+ except ImportError:
+ from django import forms
+
+ forms.ModelForm = aeforms.ModelForm
+
+ # Extend ModelForm with support for EmailProperty
+ # TODO: This should be submitted to the main App Engine SDK.
+ from google.appengine.ext.db import EmailProperty
+ def get_form_field(self, **kwargs):
+ """Return a Django form field appropriate for an email property."""
+ defaults = {'form_class': forms.EmailField}
+ defaults.update(kwargs)
+ return super(EmailProperty, self).get_form_field(**defaults)
+ EmailProperty.get_form_field = get_form_field
diff --git a/appengine_django/__init__.pyc b/appengine_django/__init__.pyc
new file mode 100644
index 0000000..12557cb
--- /dev/null
+++ b/appengine_django/__init__.pyc
Binary files differ
diff --git a/appengine_django/auth/__init__.py b/appengine_django/auth/__init__.py
new file mode 100644
index 0000000..d2db207
--- /dev/null
+++ b/appengine_django/auth/__init__.py
@@ -0,0 +1,25 @@
+# Copyright 2008 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.
+
+"""
+Authentication module that mimics the behavior of Django's authentication
+implementation.
+
+Limitations:
+ - all user permissions methods are not available (requires contenttypes)
+"""
+
+from django.template import add_to_builtins
+
+add_to_builtins('appengine_django.auth.templatetags')
diff --git a/appengine_django/auth/__init__.pyc b/appengine_django/auth/__init__.pyc
new file mode 100644
index 0000000..4d1edc3
--- /dev/null
+++ b/appengine_django/auth/__init__.pyc
Binary files differ
diff --git a/appengine_django/auth/decorators.py b/appengine_django/auth/decorators.py
new file mode 100644
index 0000000..d897c24
--- /dev/null
+++ b/appengine_django/auth/decorators.py
@@ -0,0 +1,31 @@
+# Copyright 2008 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.
+
+"""Decorators for the authentication framework."""
+
+from django.http import HttpResponseRedirect
+
+from google.appengine.api import users
+
+
+def login_required(function):
+ """Implementation of Django's login_required decorator.
+
+ The login redirect URL is always set to request.path
+ """
+ def login_required_wrapper(request, *args, **kw):
+ if request.user.is_authenticated():
+ return function(request, *args, **kw)
+ return HttpResponseRedirect(users.create_login_url(request.path))
+ return login_required_wrapper
diff --git a/appengine_django/auth/decorators.pyc b/appengine_django/auth/decorators.pyc
new file mode 100644
index 0000000..477c819
--- /dev/null
+++ b/appengine_django/auth/decorators.pyc
Binary files differ
diff --git a/appengine_django/auth/middleware.py b/appengine_django/auth/middleware.py
new file mode 100644
index 0000000..a727e47
--- /dev/null
+++ b/appengine_django/auth/middleware.py
@@ -0,0 +1,36 @@
+# Copyright 2008 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.
+
+from django.contrib.auth.models import AnonymousUser
+
+from google.appengine.api import users
+
+from appengine_django.auth.models import User
+
+
+class LazyUser(object):
+ def __get__(self, request, obj_type=None):
+ if not hasattr(request, '_cached_user'):
+ user = users.get_current_user()
+ if user:
+ request._cached_user = User.get_djangouser_for_user(user)
+ else:
+ request._cached_user = AnonymousUser()
+ return request._cached_user
+
+
+class AuthenticationMiddleware(object):
+ def process_request(self, request):
+ request.__class__.user = LazyUser()
+ return None
diff --git a/appengine_django/auth/middleware.pyc b/appengine_django/auth/middleware.pyc
new file mode 100644
index 0000000..ff36de8
--- /dev/null
+++ b/appengine_django/auth/middleware.pyc
Binary files differ
diff --git a/appengine_django/auth/models.py b/appengine_django/auth/models.py
new file mode 100644
index 0000000..d93e240
--- /dev/null
+++ b/appengine_django/auth/models.py
@@ -0,0 +1,172 @@
+# Copyright 2008 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.
+
+"""
+App Engine compatible models for the Django authentication framework.
+"""
+
+from django.core import mail
+from django.core.exceptions import ImproperlyConfigured
+from django.db import models
+from django.utils.encoding import smart_str
+import urllib
+
+from django.db.models.manager import EmptyManager
+
+from google.appengine.api import users
+from google.appengine.ext import db
+
+from appengine_django.models import BaseModel
+
+
+class User(BaseModel):
+ """A model with the same attributes and methods as a Django user model.
+
+ The model has two additions. The first addition is a 'user' attribute which
+ references a App Engine user. The second is the 'get_djangouser_for_user'
+ classmethod that should be used to retrieve a DjangoUser instance from a App
+ Engine user object.
+ """
+ user = db.UserProperty(required=True)
+ username = db.StringProperty(required=True)
+ first_name = db.StringProperty()
+ last_name = db.StringProperty()
+ email = db.EmailProperty()
+ password = db.StringProperty()
+ is_staff = db.BooleanProperty(default=False, required=True)
+ is_active = db.BooleanProperty(default=True, required=True)
+ is_superuser = db.BooleanProperty(default=False, required=True)
+ last_login = db.DateTimeProperty(auto_now_add=True, required=True)
+ date_joined = db.DateTimeProperty(auto_now_add=True, required=True)
+ groups = EmptyManager()
+ user_permissions = EmptyManager()
+
+ def __unicode__(self):
+ return self.username
+
+ def __str__(self):
+ return unicode(self).encode('utf-8')
+
+ @classmethod
+ def get_djangouser_for_user(cls, user):
+ query = cls.all().filter("user =", user)
+ if query.count() == 0:
+ django_user = cls(user=user, email=user.email(), username=user.nickname())
+ django_user.save()
+ else:
+ django_user = query.get()
+ return django_user
+
+ def set_password(self, raw_password):
+ raise NotImplementedError
+
+ def check_password(self, raw_password):
+ raise NotImplementedError
+
+ def set_unusable_password(self):
+ raise NotImplementedError
+
+ def has_usable_password(self):
+ raise NotImplementedError
+
+ def get_group_permissions(self):
+ return self.user_permissions
+
+ def get_all_permissions(self):
+ return self.user_permissions
+
+ def has_perm(self, perm):
+ return False
+
+ def has_perms(self, perm_list):
+ return False
+
+ def has_module_perms(self, module):
+ return False
+
+ def get_and_delete_messages(self):
+ """Gets and deletes messages for this user"""
+ msgs = []
+ for msg in self.message_set:
+ msgs.append(msg)
+ msg.delete()
+ return msgs
+
+ def is_anonymous(self):
+ """Always return False"""
+ return False
+
+ def is_authenticated(self):
+ """Always return True"""
+ return True
+
+ def get_absolute_url(self):
+ return "/users/%s/" % urllib.quote(smart_str(self.username))
+
+ def get_full_name(self):
+ full_name = u'%s %s' % (self.first_name, self.last_name)
+ return full_name.strip()
+
+ def email_user(self, subject, message, from_email):
+ """Sends an email to this user.
+
+ According to the App Engine email API the from_email must be the
+ email address of a registered administrator for the application.
+ """
+ mail.send_mail(subject,
+ message,
+ from_email,
+ [self.email])
+
+ def get_profile(self):
+ """
+ Returns site-specific profile for this user. Raises
+ SiteProfileNotAvailable if this site does not allow profiles.
+
+ When using the App Engine authentication framework, users are created
+ automatically.
+ """
+ from django.contrib.auth.models import SiteProfileNotAvailable
+ if not hasattr(self, '_profile_cache'):
+ from django.conf import settings
+ if not hasattr(settings, "AUTH_PROFILE_MODULE"):
+ raise SiteProfileNotAvailable
+ try:
+ app_label, model_name = settings.AUTH_PROFILE_MODULE.split('.')
+ model = models.get_model(app_label, model_name)
+ self._profile_cache = model.all().filter("user =", self).get()
+ if not self._profile_cache:
+ raise model.DoesNotExist
+ except (ImportError, ImproperlyConfigured):
+ raise SiteProfileNotAvailable
+ return self._profile_cache
+
+
+class Group(BaseModel):
+ """Group model not fully implemented yet."""
+ # TODO: Implement this model, requires contenttypes
+ name = db.StringProperty()
+ permissions = EmptyManager()
+
+
+class Message(BaseModel):
+ """User message model"""
+ user = db.ReferenceProperty(User)
+ message = db.TextProperty()
+
+
+class Permission(BaseModel):
+ """Permission model not fully implemented yet."""
+ # TODO: Implement this model, requires contenttypes
+ name = db.StringProperty()
diff --git a/appengine_django/auth/models.pyc b/appengine_django/auth/models.pyc
new file mode 100644
index 0000000..0a73b9c
--- /dev/null
+++ b/appengine_django/auth/models.pyc
Binary files differ
diff --git a/appengine_django/auth/templatetags.py b/appengine_django/auth/templatetags.py
new file mode 100644
index 0000000..8237890
--- /dev/null
+++ b/appengine_django/auth/templatetags.py
@@ -0,0 +1,62 @@
+# Copyright 2008 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.
+
+"""
+Template tags for the auth module. These are inserted into Django as "built-in"
+tags so you do not need to use the load statement in your template to get
+access to them.
+"""
+
+from django.template import Library
+from django.template import Node
+
+from google.appengine.api import users
+
+
+class AuthLoginUrlsNode(Node):
+ """Template node that creates an App Engine login or logout URL.
+
+ If create_login_url is True the App Engine's login URL is rendered into
+ the template, otherwise the logout URL.
+ """
+ def __init__(self, create_login_url, redirect):
+ self.redirect = redirect
+ self.create_login_url = create_login_url
+
+ def render(self, context):
+ if self.create_login_url:
+ return users.create_login_url(self.redirect)
+ else:
+ return users.create_logout_url(self.redirect)
+
+
+def auth_login_urls(parser, token):
+ """Template tag registered as 'auth_login_url' and 'auth_logout_url'
+ when the module is imported.
+
+ Both tags take an optional argument that specifies the redirect URL and
+ defaults to '/'.
+ """
+ bits = list(token.split_contents())
+ if len(bits) == 2:
+ redirect = bits[1]
+ else:
+ redirect = "/"
+ login = bits[0] == "auth_login_url"
+ return AuthLoginUrlsNode(login, redirect)
+
+
+register = Library()
+register.tag("auth_login_url", auth_login_urls)
+register.tag("auth_logout_url", auth_login_urls)
diff --git a/appengine_django/auth/templatetags.pyc b/appengine_django/auth/templatetags.pyc
new file mode 100644
index 0000000..7121f59
--- /dev/null
+++ b/appengine_django/auth/templatetags.pyc
Binary files differ
diff --git a/appengine_django/auth/tests.py b/appengine_django/auth/tests.py
new file mode 100644
index 0000000..20aecfa
--- /dev/null
+++ b/appengine_django/auth/tests.py
@@ -0,0 +1,58 @@
+# Copyright 2008 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.
+
+BASIC_TESTS = """
+>>> from google.appengine.api import users
+>>> from models import User, AnonymousUser
+>>> appengine_user = users.User("test@example.com")
+>>> django_user = User.get_djangouser_for_user(appengine_user)
+>>> django_user.email == appengine_user.email()
+True
+>>> django_user.username == appengine_user.nickname()
+True
+>>> django_user.user == appengine_user
+True
+
+>>> django_user.username = 'test2'
+>>> key = django_user.save()
+>>> django_user.username == 'test2'
+True
+
+>>> django_user2 = User.get_djangouser_for_user(appengine_user)
+>>> django_user2 == django_user
+True
+
+>>> django_user.is_authenticated()
+True
+>>> django_user.is_staff
+False
+>>> django_user.is_active
+True
+
+>>> a = AnonymousUser()
+>>> a.is_authenticated()
+False
+>>> a.is_staff
+False
+>>> a.is_active
+False
+>>> a.groups.all()
+[]
+>>> a.user_permissions.all()
+[]
+
+
+"""
+
+__test__ = {'BASIC_TESTS': BASIC_TESTS}
diff --git a/appengine_django/conf/app_template/__init__.py b/appengine_django/conf/app_template/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/appengine_django/conf/app_template/__init__.py
diff --git a/appengine_django/conf/app_template/models.py b/appengine_django/conf/app_template/models.py
new file mode 100644
index 0000000..4d7c5d0
--- /dev/null
+++ b/appengine_django/conf/app_template/models.py
@@ -0,0 +1,4 @@
+from appengine_django.models import BaseModel
+from google.appengine.ext import db
+
+# Create your models here.
diff --git a/appengine_django/conf/app_template/views.py b/appengine_django/conf/app_template/views.py
new file mode 100644
index 0000000..60f00ef
--- /dev/null
+++ b/appengine_django/conf/app_template/views.py
@@ -0,0 +1 @@
+# Create your views here.
diff --git a/appengine_django/db/__init__.py b/appengine_django/db/__init__.py
new file mode 100755
index 0000000..619bc78
--- /dev/null
+++ b/appengine_django/db/__init__.py
@@ -0,0 +1,20 @@
+# Copyright 2008 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.
+
+# Explicitly set the name of this package to "appengine".
+#
+# The rationale for this is so that Django can refer to the database as
+# "appengine" even though at a filesystem level it appears as the "db" package
+# within the appengine_django package.
+__name__ = "appengine"
diff --git a/appengine_django/db/__init__.pyc b/appengine_django/db/__init__.pyc
new file mode 100644
index 0000000..879eeaa
--- /dev/null
+++ b/appengine_django/db/__init__.pyc
Binary files differ
diff --git a/appengine_django/db/base.py b/appengine_django/db/base.py
new file mode 100755
index 0000000..8a90182
--- /dev/null
+++ b/appengine_django/db/base.py
@@ -0,0 +1,150 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 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.
+
+"""This module looks after initialising the appengine api stubs."""
+
+import logging
+import os
+
+from appengine_django import appid
+from appengine_django import have_appserver
+from appengine_django.db.creation import DatabaseCreation
+
+
+from django.db.backends import BaseDatabaseWrapper
+from django.db.backends import BaseDatabaseFeatures
+from django.db.backends import BaseDatabaseOperations
+
+
+def get_datastore_paths():
+ """Returns a tuple with the path to the datastore and history file.
+
+ The datastore is stored in the same location as dev_appserver uses by
+ default, but the name is altered to be unique to this project so multiple
+ Django projects can be developed on the same machine in parallel.
+
+ Returns:
+ (datastore_path, history_path)
+ """
+ from google.appengine.tools import dev_appserver_main
+ datastore_path = dev_appserver_main.DEFAULT_ARGS['datastore_path']
+ history_path = dev_appserver_main.DEFAULT_ARGS['history_path']
+ datastore_path = datastore_path.replace("dev_appserver", "django_%s" % appid)
+ history_path = history_path.replace("dev_appserver", "django_%s" % appid)
+ return datastore_path, history_path
+
+
+def get_test_datastore_paths(inmemory=True):
+ """Returns a tuple with the path to the test datastore and history file.
+
+ If inmemory is true, (None, None) is returned to request an in-memory
+ datastore. If inmemory is false the path returned will be similar to the path
+ returned by get_datastore_paths but with a different name.
+
+ Returns:
+ (datastore_path, history_path)
+ """
+ if inmemory:
+ return None, None
+ datastore_path, history_path = get_datastore_paths()
+ datastore_path = datastore_path.replace("datastore", "testdatastore")
+ history_path = history_path.replace("datastore", "testdatastore")
+ return datastore_path, history_path
+
+
+def destroy_datastore(datastore_path, history_path):
+ """Destroys the appengine datastore at the specified paths."""
+ for path in [datastore_path, history_path]:
+ if not path: continue
+ try:
+ os.remove(path)
+ except OSError, e:
+ if e.errno != 2:
+ logging.error("Failed to clear datastore: %s" % e)
+
+
+class DatabaseError(Exception):
+ """Stub class for database errors. Required by Django"""
+ pass
+
+
+class IntegrityError(Exception):
+ """Stub class for database integrity errors. Required by Django"""
+ pass
+
+
+class DatabaseFeatures(BaseDatabaseFeatures):
+ """Stub class to provide the feaures member expected by Django"""
+ pass
+
+
+class DatabaseOperations(BaseDatabaseOperations):
+ """Stub class to provide the options member expected by Django"""
+ pass
+
+
+class DatabaseWrapper(BaseDatabaseWrapper):
+ """App Engine database definition for Django.
+
+ This "database" backend does not support any of the standard backend
+ operations. The only task that it performs is to setup the api stubs required
+ by the appengine libraries if they have not already been initialised by an
+ appserver.
+ """
+
+ def __init__(self, *args, **kwargs):
+ super(DatabaseWrapper, self).__init__(*args, **kwargs)
+ self.features = DatabaseFeatures()
+ self.ops = DatabaseOperations()
+ self.creation = DatabaseCreation(self)
+ self.use_test_datastore = kwargs.get("use_test_datastore", False)
+ self.test_datastore_inmemory = kwargs.get("test_datastore_inmemory", True)
+ if have_appserver:
+ return
+ self._setup_stubs()
+
+ def _get_paths(self):
+ if self.use_test_datastore:
+ return get_test_datastore_paths(self.test_datastore_inmemory)
+ else:
+ return get_datastore_paths()
+
+ def _setup_stubs(self):
+ # If this code is being run without an appserver (eg. via a django
+ # commandline flag) then setup a default stub environment.
+ from google.appengine.tools import dev_appserver_main
+ args = dev_appserver_main.DEFAULT_ARGS.copy()
+ args['datastore_path'], args['history_path'] = self._get_paths()
+ from google.appengine.tools import dev_appserver
+ dev_appserver.SetupStubs(appid, **args)
+ if self.use_test_datastore:
+ logging.debug("Configured API stubs for the test datastore")
+ else:
+ logging.debug("Configured API stubs for the development datastore")
+
+ def flush(self):
+ """Helper function to remove the current datastore and re-open the stubs"""
+ destroy_datastore(*self._get_paths())
+ self._setup_stubs()
+
+ def close(self):
+ pass
+
+ def _commit(self):
+ pass
+
+ def cursor(self, *args):
+ pass
diff --git a/appengine_django/db/base.pyc b/appengine_django/db/base.pyc
new file mode 100644
index 0000000..8699add
--- /dev/null
+++ b/appengine_django/db/base.pyc
Binary files differ
diff --git a/appengine_django/db/creation.py b/appengine_django/db/creation.py
new file mode 100755
index 0000000..74e3048
--- /dev/null
+++ b/appengine_django/db/creation.py
@@ -0,0 +1,37 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 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.
+
+
+import logging
+
+from django.db.backends.creation import BaseDatabaseCreation
+
+
+class DatabaseCreation(BaseDatabaseCreation):
+
+ def create_test_db(self, *args, **kw):
+ """Destroys the test datastore. A new store will be recreated on demand"""
+ self.destroy_test_db()
+ self.connection.use_test_datastore = True
+ self.connection.flush()
+
+
+ def destroy_test_db(self, *args, **kw):
+ """Destroys the test datastore files."""
+ from appengine_django.db.base import destroy_datastore
+ from appengine_django.db.base import get_test_datastore_paths
+ destroy_datastore(*get_test_datastore_paths())
+ logging.debug("Destroyed test datastore")
diff --git a/appengine_django/db/creation.pyc b/appengine_django/db/creation.pyc
new file mode 100644
index 0000000..2055fb6
--- /dev/null
+++ b/appengine_django/db/creation.pyc
Binary files differ
diff --git a/appengine_django/mail.py b/appengine_django/mail.py
new file mode 100644
index 0000000..bf3e2dd
--- /dev/null
+++ b/appengine_django/mail.py
@@ -0,0 +1,95 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 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.
+
+"""
+This module replaces the Django mail implementation with a version that sends
+email via the mail API provided by Google App Engine.
+
+Multipart / HTML email is not yet supported.
+"""
+
+import logging
+
+from django.core import mail
+from django.core.mail import SMTPConnection
+from django.conf import settings
+
+from google.appengine.api import mail as gmail
+
+
+class GoogleSMTPConnection(SMTPConnection):
+ def __init__(self, host=None, port=None, username=None, password=None,
+ use_tls=None, fail_silently=False):
+ self.use_tls = (use_tls is not None) and use_tls or settings.EMAIL_USE_TLS
+ self.fail_silently = fail_silently
+ self.connection = None
+
+ def open(self):
+ self.connection = True
+
+ def close(self):
+ pass
+
+ def _send(self, email_message):
+ """A helper method that does the actual sending."""
+ if not email_message.to:
+ return False
+ try:
+ if (isinstance(email_message,gmail.EmailMessage)):
+ e = message
+ elif (isinstance(email_message,mail.EmailMessage)):
+ e = gmail.EmailMessage(sender=email_message.from_email,
+ to=email_message.to,
+ subject=email_message.subject,
+ body=email_message.body)
+ if email_message.extra_headers.get('Reply-To', None):
+ e.reply_to = email_message.extra_headers['Reply-To']
+ if email_message.bcc:
+ e.bcc = list(email_message.bcc)
+ #TODO - add support for html messages and attachments...
+ e.send()
+ except:
+ if not self.fail_silently:
+ raise
+ return False
+ return True
+
+
+def mail_admins(subject, message, fail_silently=False):
+ """Sends a message to the admins, as defined by the ADMINS setting."""
+ _mail_group(settings.ADMINS, subject, message, fail_silently)
+
+
+def mail_managers(subject, message, fail_silently=False):
+ """Sends a message to the managers, as defined by the MANAGERS setting."""
+ _mail_group(settings.MANAGERS, subject, message, fail_silently)
+
+
+def _mail_group(group, subject, message, fail_silently=False):
+ """Sends a message to an administrative group."""
+ if group:
+ mail.send_mail(settings.EMAIL_SUBJECT_PREFIX + subject, message,
+ settings.SERVER_EMAIL, [a[1] for a in group],
+ fail_silently)
+ return
+ # If the group had no recipients defined, default to the App Engine admins.
+ try:
+ gmail.send_mail_to_admins(settings.SERVER_EMAIL,
+ settings.EMAIL_SUBJECT_PREFIX + subject,
+ message)
+ except:
+ if not fail_silently:
+ raise
diff --git a/appengine_django/mail.pyc b/appengine_django/mail.pyc
new file mode 100644
index 0000000..71ab53f
--- /dev/null
+++ b/appengine_django/mail.pyc
Binary files differ
diff --git a/appengine_django/management/__init__.py b/appengine_django/management/__init__.py
new file mode 100755
index 0000000..e69de29
--- /dev/null
+++ b/appengine_django/management/__init__.py
diff --git a/appengine_django/management/__init__.pyc b/appengine_django/management/__init__.pyc
new file mode 100644
index 0000000..e80538d
--- /dev/null
+++ b/appengine_django/management/__init__.pyc
Binary files differ
diff --git a/appengine_django/management/commands/__init__.py b/appengine_django/management/commands/__init__.py
new file mode 100755
index 0000000..e69de29
--- /dev/null
+++ b/appengine_django/management/commands/__init__.py
diff --git a/appengine_django/management/commands/__init__.pyc b/appengine_django/management/commands/__init__.pyc
new file mode 100644
index 0000000..48a948a
--- /dev/null
+++ b/appengine_django/management/commands/__init__.pyc
Binary files differ
diff --git a/appengine_django/management/commands/console.py b/appengine_django/management/commands/console.py
new file mode 100755
index 0000000..2c40697
--- /dev/null
+++ b/appengine_django/management/commands/console.py
@@ -0,0 +1,49 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 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.
+import code
+import getpass
+import os
+import sys
+
+from django.conf import settings
+from django.core.management.base import BaseCommand
+
+from google.appengine.ext.remote_api import remote_api_stub
+
+
+def auth_func():
+ return raw_input('Username:'), getpass.getpass('Password:')
+
+class Command(BaseCommand):
+ """ Start up an interactive console backed by your app using remote_api """
+
+ help = 'Start up an interactive console backed by your app using remote_api.'
+
+ def run_from_argv(self, argv):
+ app_id = argv[2]
+ if len(argv) > 3:
+ host = argv[3]
+ else:
+ host = '%s.appspot.com' % app_id
+
+ remote_api_stub.ConfigureRemoteDatastore(app_id,
+ '/remote_api',
+ auth_func,
+ host)
+
+ code.interact('App Engine interactive console for %s' % (app_id,),
+ None,
+ locals())
diff --git a/appengine_django/management/commands/flush.py b/appengine_django/management/commands/flush.py
new file mode 100755
index 0000000..c5f3f8c
--- /dev/null
+++ b/appengine_django/management/commands/flush.py
@@ -0,0 +1,36 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 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.
+
+import logging
+import os
+import sys
+
+from django.core.management.base import BaseCommand
+
+
+class Command(BaseCommand):
+ """Overrides the default Django flush command.
+ """
+ help = 'Clears the current datastore and loads the initial fixture data.'
+
+ def run_from_argv(self, argv):
+ from django.db import connection
+ connection.flush()
+ from django.core.management import call_command
+ call_command('loaddata', 'initial_data')
+
+ def handle(self, *args, **kwargs):
+ self.run_from_argv(None)
diff --git a/appengine_django/management/commands/reset.py b/appengine_django/management/commands/reset.py
new file mode 100755
index 0000000..126f386
--- /dev/null
+++ b/appengine_django/management/commands/reset.py
@@ -0,0 +1,32 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 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.
+
+
+import logging
+import os
+import sys
+
+from django.core.management.base import BaseCommand
+
+
+class Command(BaseCommand):
+ """Overrides the default Django reset command.
+ """
+ help = 'Clears the current datastore.'
+
+ def run_from_argv(self, argv):
+ from django.db import connection
+ connection.flush()
diff --git a/appengine_django/management/commands/rollback.py b/appengine_django/management/commands/rollback.py
new file mode 100755
index 0000000..6ce9e4e
--- /dev/null
+++ b/appengine_django/management/commands/rollback.py
@@ -0,0 +1,52 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 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.
+
+
+import sys
+import logging
+
+from django.core.management.base import BaseCommand
+
+
+def run_appcfg():
+ # import this so that we run through the checks at the beginning
+ # and report the appropriate errors
+ import appcfg
+
+ # We don't really want to use that one though, it just executes this one
+ from google.appengine.tools import appcfg
+
+ # Reset the logging level to WARN as appcfg will spew tons of logs on INFO
+ logging.getLogger().setLevel(logging.WARN)
+
+ # Note: if we decide to change the name of this command to something other
+ # than 'rollback' we will have to munge the args to replace whatever
+ # we called it with 'rollback'
+ new_args = sys.argv[:]
+ new_args.append('.')
+ appcfg.main(new_args)
+
+
+class Command(BaseCommand):
+ """Calls the appcfg.py's rollback command for the current project.
+
+ Any additional arguments are passed directly to appcfg.py.
+ """
+ help = 'Calls appcfg.py rollback for the current project.'
+ args = '[any appcfg.py options]'
+
+ def run_from_argv(self, argv):
+ run_appcfg()
diff --git a/appengine_django/management/commands/runserver.py b/appengine_django/management/commands/runserver.py
new file mode 100755
index 0000000..7b91f65
--- /dev/null
+++ b/appengine_django/management/commands/runserver.py
@@ -0,0 +1,77 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 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.
+
+
+import os
+import sys
+
+from appengine_django.db.base import get_datastore_paths
+
+from django.core.management.base import BaseCommand
+
+
+def start_dev_appserver():
+ """Starts the appengine dev_appserver program for the Django project.
+
+ The appserver is run with default parameters. If you need to pass any special
+ parameters to the dev_appserver you will have to invoke it manually.
+ """
+ from google.appengine.tools import dev_appserver_main
+ progname = sys.argv[0]
+ args = []
+ # hack __main__ so --help in dev_appserver_main works OK.
+ sys.modules['__main__'] = dev_appserver_main
+ # Set bind ip/port if specified.
+ if len(sys.argv) > 2:
+ addrport = sys.argv[2]
+ try:
+ addr, port = addrport.split(":")
+ except ValueError:
+ addr, port = None, addrport
+ if not port.isdigit():
+ print "Error: '%s' is not a valid port number." % port
+ sys.exit(1)
+ else:
+ addr, port = None, "8000"
+ if addr:
+ args.extend(["--address", addr])
+ if port:
+ args.extend(["--port", port])
+ # Add email settings
+ from django.conf import settings
+ args.extend(['--smtp_host', settings.EMAIL_HOST,
+ '--smtp_port', str(settings.EMAIL_PORT),
+ '--smtp_user', settings.EMAIL_HOST_USER,
+ '--smtp_password', settings.EMAIL_HOST_PASSWORD])
+ # Pass the application specific datastore location to the server.
+ p = get_datastore_paths()
+ args.extend(["--datastore_path", p[0], "--history_path", p[1]])
+ # Append the current working directory to the arguments.
+ dev_appserver_main.main([progname] + args + [os.getcwdu()])
+
+
+class Command(BaseCommand):
+ """Overrides the default Django runserver command.
+
+ Instead of starting the default Django development server this command
+ fires up a copy of the full fledged appengine dev_appserver that emulates
+ the live environment your application will be deployed to.
+ """
+ help = 'Runs a copy of the appengine development server.'
+ args = '[optional port number, or ipaddr:port]'
+
+ def run_from_argv(self, argv):
+ start_dev_appserver()
diff --git a/appengine_django/management/commands/runserver.pyc b/appengine_django/management/commands/runserver.pyc
new file mode 100644
index 0000000..38abf92
--- /dev/null
+++ b/appengine_django/management/commands/runserver.pyc
Binary files differ
diff --git a/appengine_django/management/commands/startapp.py b/appengine_django/management/commands/startapp.py
new file mode 100644
index 0000000..2648cbd
--- /dev/null
+++ b/appengine_django/management/commands/startapp.py
@@ -0,0 +1,43 @@
+# Copyright 2008 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.
+
+
+import os
+
+import django
+from django.core.management.commands import startapp
+
+import appengine_django
+
+
+class Command(startapp.Command):
+ def handle_label(self, *args, **kwds):
+ """Temporary adjust django.__path__ to load app templates from the
+ helpers directory.
+ """
+ old_path = django.__path__
+ django.__path__ = appengine_django.__path__
+ startapp.Command.handle_label(self, *args, **kwds)
+ django.__path__ = old_path
+
+
+class ProjectCommand(Command):
+ def __init__(self, project_directory):
+ super(ProjectCommand, self).__init__()
+ self.project_directory = project_directory
+
+ def handle_label(self, app_name, **options):
+ super(ProjectCommand, self).handle_label(app_name, self.project_directory,
+ **options)
+
diff --git a/appengine_django/management/commands/startapp.pyc b/appengine_django/management/commands/startapp.pyc
new file mode 100644
index 0000000..844eb68
--- /dev/null
+++ b/appengine_django/management/commands/startapp.pyc
Binary files differ
diff --git a/appengine_django/management/commands/testserver.py b/appengine_django/management/commands/testserver.py
new file mode 100755
index 0000000..bd2c6d1
--- /dev/null
+++ b/appengine_django/management/commands/testserver.py
@@ -0,0 +1,71 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 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.
+
+
+import os
+import sys
+
+from appengine_django.db.base import destroy_datastore
+from appengine_django.db.base import get_test_datastore_paths
+
+from django.core.management.base import BaseCommand
+
+
+class Command(BaseCommand):
+ """Overrides the default Django testserver command.
+
+ Instead of starting the default Django development server this command fires
+ up a copy of the full fledged appengine dev_appserver.
+
+ The appserver is always initialised with a blank datastore with the specified
+ fixtures loaded into it.
+ """
+ help = 'Runs the development server with data from the given fixtures.'
+
+ def run_from_argv(self, argv):
+ fixtures = argv[2:]
+
+ # Ensure an on-disk test datastore is used.
+ from django.db import connection
+ connection.use_test_datastore = True
+ connection.test_datastore_inmemory = False
+
+ # Flush any existing test datastore.
+ connection.flush()
+
+ # Load the fixtures.
+ from django.core.management import call_command
+ call_command('loaddata', 'initial_data')
+ if fixtures:
+ call_command('loaddata', *fixtures)
+
+ # Build new arguments for dev_appserver.
+ datastore_path, history_path = get_test_datastore_paths(False)
+ new_args = argv[0:1]
+ new_args.extend(['--datastore_path', datastore_path])
+ new_args.extend(['--history_path', history_path])
+ new_args.extend([os.getcwdu()])
+
+ # Add email settings
+ from django.conf import settings
+ new_args.extend(['--smtp_host', settings.EMAIL_HOST,
+ '--smtp_port', str(settings.EMAIL_PORT),
+ '--smtp_user', settings.EMAIL_HOST_USER,
+ '--smtp_password', settings.EMAIL_HOST_PASSWORD])
+
+ # Start the test dev_appserver.
+ from google.appengine.tools import dev_appserver_main
+ dev_appserver_main.main(new_args)
diff --git a/appengine_django/management/commands/update.py b/appengine_django/management/commands/update.py
new file mode 100755
index 0000000..e489d5d
--- /dev/null
+++ b/appengine_django/management/commands/update.py
@@ -0,0 +1,51 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 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.
+
+
+import sys
+import logging
+
+from django.core.management.base import BaseCommand
+
+
+def run_appcfg():
+ # import this so that we run through the checks at the beginning
+ # and report the appropriate errors
+ import appcfg
+
+ # We don't really want to use that one though, it just executes this one
+ from google.appengine.tools import appcfg
+
+ # Reset the logging level to WARN as appcfg will spew tons of logs on INFO
+ logging.getLogger().setLevel(logging.WARN)
+
+ # Note: if we decide to change the name of this command to something other
+ # than 'update' we will have to munge the args to replace whatever
+ # we called it with 'update'
+ new_args = sys.argv[:]
+ new_args.append('.')
+ appcfg.main(new_args)
+
+class Command(BaseCommand):
+ """Calls the appcfg.py's update command for the current project.
+
+ Any additional arguments are passed directly to appcfg.py.
+ """
+ help = 'Calls appcfg.py update for the current project.'
+ args = '[any appcfg.py options]'
+
+ def run_from_argv(self, argv):
+ run_appcfg()
diff --git a/appengine_django/management/commands/vacuum_indexes.py b/appengine_django/management/commands/vacuum_indexes.py
new file mode 100755
index 0000000..ab276b4
--- /dev/null
+++ b/appengine_django/management/commands/vacuum_indexes.py
@@ -0,0 +1,52 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 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.
+
+
+import sys
+import logging
+
+from django.core.management.base import BaseCommand
+
+
+def run_appcfg():
+ # import this so that we run through the checks at the beginning
+ # and report the appropriate errors
+ import appcfg
+
+ # We don't really want to use that one though, it just executes this one
+ from google.appengine.tools import appcfg
+
+ # Reset the logging level to WARN as appcfg will spew tons of logs on INFO
+ logging.getLogger().setLevel(logging.WARN)
+
+ # Note: if we decide to change the name of this command to something other
+ # than 'vacuum_indexes' we will have to munge the args to replace whatever
+ # we called it with 'vacuum_indexes'
+ new_args = sys.argv[:]
+ new_args.append('.')
+ appcfg.main(new_args)
+
+
+class Command(BaseCommand):
+ """Calls the appcfg.py's vacuum_indexes command for the current project.
+
+ Any additional arguments are passed directly to appcfg.py.
+ """
+ help = 'Calls appcfg.py vacuum_indexes for the current project.'
+ args = '[any appcfg.py options]'
+
+ def run_from_argv(self, argv):
+ run_appcfg()
diff --git a/appengine_django/models.py b/appengine_django/models.py
new file mode 100755
index 0000000..0b9f6dc
--- /dev/null
+++ b/appengine_django/models.py
@@ -0,0 +1,182 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 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.
+
+import sys
+import types
+
+from google.appengine.ext import db
+
+from django import VERSION
+from django.core.exceptions import ObjectDoesNotExist
+from django.db.models.fields import Field
+from django.db.models.options import Options
+from django.db.models.loading import register_models, get_model
+
+
+class ModelManager(object):
+ """Replacement for the default Django model manager."""
+
+ def __init__(self, owner):
+ self.owner = owner
+
+ def __getattr__(self, name):
+ """Pass all attribute requests through to the real model"""
+ return getattr(self.owner, name)
+
+
+class ModelOptions(object):
+ """Replacement for the default Django options class.
+
+ This class sits at ._meta of each model. The primary information supplied by
+ this class that needs to be stubbed out is the list of fields on the model.
+ """
+
+ def __init__(self, cls):
+ self.object_name = cls.__name__
+ self.module_name = self.object_name.lower()
+ model_module = sys.modules[cls.__module__]
+ self.app_label = model_module.__name__.split('.')[-2]
+ self.abstract = False
+
+ class pk:
+ """Stub the primary key to always be 'key_name'"""
+ name = "key_name"
+
+ def __str__(self):
+ return "%s.%s" % (self.app_label, self.module_name)
+
+ @property
+ def many_to_many(self):
+ """The datastore does not support many to many relationships."""
+ return []
+
+
+class Relation(object):
+ def __init__(self, to):
+ self.field_name = "key_name"
+
+
+def PropertyWrapper(prop):
+ """Wrapper for db.Property to make it look like a Django model Property"""
+ if isinstance(prop, db.Reference):
+ prop.rel = Relation(prop.reference_class)
+ else:
+ prop.rel = None
+ prop.serialize = True
+ return prop
+
+
+class PropertiedClassWithDjango(db.PropertiedClass):
+ """Metaclass for the combined Django + App Engine model class.
+
+ This metaclass inherits from db.PropertiedClass in the appengine library.
+ This metaclass has two additional purposes:
+ 1) Register each model class created with Django (the parent class will take
+ care of registering it with the appengine libraries).
+ 2) Add the (minimum number) of attributes and methods to make Django believe
+ the class is a normal Django model.
+
+ The resulting classes are still not generally useful as Django classes and
+ are intended to be used by Django only in limited situations such as loading
+ and dumping fixtures.
+ """
+
+ def __new__(cls, name, bases, attrs):
+ """Creates a combined appengine and Django model.
+
+ The resulting model will be known to both the appengine libraries and
+ Django.
+ """
+ if name == 'BaseModel':
+ # This metaclass only acts on subclasses of BaseModel.
+ return super(PropertiedClassWithDjango, cls).__new__(cls, name,
+ bases, attrs)
+
+ new_class = super(PropertiedClassWithDjango, cls).__new__(cls, name,
+ bases, attrs)
+
+ new_class._meta = ModelOptions(new_class)
+ new_class.objects = ModelManager(new_class)
+ new_class._default_manager = new_class.objects
+ new_class.DoesNotExist = types.ClassType('DoesNotExist',
+ (ObjectDoesNotExist,), {})
+
+ m = get_model(new_class._meta.app_label, name, False)
+ if m:
+ return m
+
+ register_models(new_class._meta.app_label, new_class)
+ return get_model(new_class._meta.app_label, name, False)
+
+ def __init__(cls, name, bases, attrs):
+ """Initialises the list of Django properties.
+
+ This method takes care of wrapping the properties created by the superclass
+ so that they look like Django properties and installing them into the
+ ._meta object of the class so that Django can find them at the appropriate
+ time.
+ """
+ super(PropertiedClassWithDjango, cls).__init__(name, bases, attrs)
+ if name == 'BaseModel':
+ # This metaclass only acts on subclasses of BaseModel.
+ return
+
+ fields = [PropertyWrapper(p) for p in cls._properties.values()]
+ cls._meta.local_fields = fields
+
+
+class BaseModel(db.Model):
+ """Combined appengine and Django model.
+
+ All models used in the application should derive from this class.
+ """
+ __metaclass__ = PropertiedClassWithDjango
+
+ def __eq__(self, other):
+ if not isinstance(other, self.__class__):
+ return False
+ return self._get_pk_val() == other._get_pk_val()
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def _get_pk_val(self):
+ """Return the string representation of the model's key"""
+ return unicode(self.key())
+
+ def __repr__(self):
+ """Create a string that can be used to construct an equivalent object.
+
+ e.g. eval(repr(obj)) == obj
+ """
+ # First, creates a dictionary of property names and values. Note that
+ # property values, not property objects, has to be passed in to constructor.
+ def _MakeReprTuple(prop_name):
+ prop = getattr(self.__class__, prop_name)
+ return (prop_name, prop.get_value_for_datastore(self))
+
+ d = dict([_MakeReprTuple(prop_name) for prop_name in self.properties()])
+ return "%s(**%s)" % (self.__class__.__name__, repr(d))
+
+
+class RegistrationTestModel(BaseModel):
+ """Used to check registration with Django is working correctly.
+
+ Django 0.96 only recognises models defined within an applications models
+ module when get_models() is called so this definition must be here rather
+ than within the associated test (tests/model_test.py).
+ """
+ pass
diff --git a/appengine_django/models.pyc b/appengine_django/models.pyc
new file mode 100644
index 0000000..ed6c6ec
--- /dev/null
+++ b/appengine_django/models.pyc
Binary files differ
diff --git a/appengine_django/replacement_imp.py b/appengine_django/replacement_imp.py
new file mode 100644
index 0000000..330aaf0
--- /dev/null
+++ b/appengine_django/replacement_imp.py
@@ -0,0 +1,26 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 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.
+
+"""This file acts as a very minimal replacement for the 'imp' module.
+
+It contains only what Django expects to use and does not actually implement the
+same functionality as the real 'imp' module.
+"""
+
+
+def find_module(name, path=None):
+ """Django needs imp.find_module, but it works fine if nothing is found."""
+ raise ImportError
diff --git a/appengine_django/replacement_imp.pyc b/appengine_django/replacement_imp.pyc
new file mode 100644
index 0000000..c8abe0b
--- /dev/null
+++ b/appengine_django/replacement_imp.pyc
Binary files differ
diff --git a/appengine_django/serializer/__init__.py b/appengine_django/serializer/__init__.py
new file mode 100755
index 0000000..e69de29
--- /dev/null
+++ b/appengine_django/serializer/__init__.py
diff --git a/appengine_django/serializer/__init__.pyc b/appengine_django/serializer/__init__.pyc
new file mode 100644
index 0000000..8dbe892
--- /dev/null
+++ b/appengine_django/serializer/__init__.pyc
Binary files differ
diff --git a/appengine_django/serializer/python.py b/appengine_django/serializer/python.py
new file mode 100755
index 0000000..bce16e7
--- /dev/null
+++ b/appengine_django/serializer/python.py
@@ -0,0 +1,130 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 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.
+
+"""
+A Python "serializer", based on the default Django python serializer.
+
+The only customisation is in the deserialization process which needs to take
+special care to resolve the name and parent attributes of the key for each
+entity and also recreate the keys for any references appropriately.
+"""
+
+
+from django.conf import settings
+from django.core.serializers import base
+from django.core.serializers import python
+from django.db import models
+
+from google.appengine.api import datastore_types
+from google.appengine.ext import db
+
+from django.utils.encoding import smart_unicode
+
+Serializer = python.Serializer
+
+
+class FakeParent(object):
+ """Fake parent 'model' like object.
+
+ This class exists to allow a parent object to be provided to a new model
+ without having to load the parent instance itself.
+ """
+
+ def __init__(self, parent_key):
+ self._entity = parent_key
+
+
+def Deserializer(object_list, **options):
+ """Deserialize simple Python objects back into Model instances.
+
+ It's expected that you pass the Python objects themselves (instead of a
+ stream or a string) to the constructor
+ """
+ models.get_apps()
+ for d in object_list:
+ # Look up the model and starting build a dict of data for it.
+ Model = python._get_model(d["model"])
+ data = {}
+ key = resolve_key(Model._meta.module_name, d["pk"])
+ if key.name():
+ data["key_name"] = key.name()
+ parent = None
+ if key.parent():
+ parent = FakeParent(key.parent())
+ m2m_data = {}
+
+ # Handle each field
+ for (field_name, field_value) in d["fields"].iteritems():
+ if isinstance(field_value, str):
+ field_value = smart_unicode(
+ field_value, options.get("encoding",
+ settings.DEFAULT_CHARSET),
+ strings_only=True)
+ field = Model.properties()[field_name]
+
+ if isinstance(field, db.Reference):
+ # Resolve foreign key references.
+ data[field.name] = resolve_key(Model._meta.module_name, field_value)
+ if not data[field.name].name():
+ raise base.DeserializationError(u"Cannot load Reference with "
+ "unnamed key: '%s'" % field_value)
+ else:
+ data[field.name] = field.validate(field_value)
+ # Create the new model instance with all it's data, but no parent.
+ object = Model(**data)
+ # Now add the parent into the hidden attribute, bypassing the type checks
+ # in the Model's __init__ routine.
+ object._parent = parent
+ # When the deserialized object is saved our replacement DeserializedObject
+ # class will set object._parent to force the real parent model to be loaded
+ # the first time it is referenced.
+ yield base.DeserializedObject(object, m2m_data)
+
+
+def resolve_key(model, key_data):
+ """Creates a Key instance from a some data.
+
+ Args:
+ model: The name of the model this key is being resolved for. Only used in
+ the fourth case below (a plain key_name string).
+ key_data: The data to create a key instance from. May be in four formats:
+ * The str() output of a key instance. Eg. A base64 encoded string.
+ * The repr() output of a key instance. Eg. A string for eval().
+ * A list of arguments to pass to db.Key.from_path.
+ * A single string value, being the key_name of the instance. When this
+ format is used the resulting key has no parent, and is for the model
+ named in the model parameter.
+
+ Returns:
+ An instance of db.Key. If the data cannot be used to create a Key instance
+ an error will be raised.
+ """
+ if isinstance(key_data, list):
+ # The key_data is a from_path sequence.
+ return db.Key.from_path(*key_data)
+ elif isinstance(key_data, basestring):
+ if key_data.find("from_path") != -1:
+ # key_data is encoded in repr(key) format
+ return eval(key_data)
+ else:
+ try:
+ # key_data encoded a str(key) format
+ return db.Key(key_data)
+ except datastore_types.datastore_errors.BadKeyError, e:
+ # Final try, assume it's a plain key name for the model.
+ return db.Key.from_path(model, key_data)
+ else:
+ raise base.DeserializationError(u"Invalid key data: '%s'" % key_data)
diff --git a/appengine_django/serializer/python.pyc b/appengine_django/serializer/python.pyc
new file mode 100644
index 0000000..d9a3507
--- /dev/null
+++ b/appengine_django/serializer/python.pyc
Binary files differ
diff --git a/appengine_django/serializer/xml.py b/appengine_django/serializer/xml.py
new file mode 100755
index 0000000..f67588a
--- /dev/null
+++ b/appengine_django/serializer/xml.py
@@ -0,0 +1,147 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 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.
+
+
+"""
+Replaces the default Django XML serializer with one that uses the built in
+ToXml method for each entity.
+"""
+
+import re
+
+from django.conf import settings
+from django.core.serializers import base
+from django.core.serializers import xml_serializer
+from django.db import models
+
+from google.appengine.api import datastore_types
+from google.appengine.ext import db
+
+from python import FakeParent
+
+getInnerText = xml_serializer.getInnerText
+
+
+class Serializer(xml_serializer.Serializer):
+ """A Django Serializer class to convert datastore models to XML.
+
+ This class relies on the ToXml method of the entity behind each model to do
+ the hard work.
+ """
+
+ def __init__(self, *args, **kwargs):
+ super(Serializer, self).__init__(*args, **kwargs)
+ self._objects = []
+
+ def handle_field(self, obj, field):
+ """Fields are not handled individually."""
+ pass
+
+ def handle_fk_field(self, obj, field):
+ """Fields are not handled individually."""
+ pass
+
+ def start_object(self, obj):
+ """Nothing needs to be done to start an object."""
+ pass
+
+ def end_object(self, obj):
+ """Serialize the object to XML and add to the list of objects to output.
+
+ The output of ToXml is manipulated to replace the datastore model name in
+ the "kind" tag with the Django model name (which includes the Django
+ application name) to make importing easier.
+ """
+ xml = obj._entity.ToXml()
+ xml = xml.replace(u"""kind="%s" """ % obj._entity.kind(),
+ u"""kind="%s" """ % unicode(obj._meta))
+ self._objects.append(xml)
+
+ def getvalue(self):
+ """Wrap the serialized objects with XML headers and return."""
+ str = u"""<?xml version="1.0" encoding="utf-8"?>\n"""
+ str += u"""<django-objects version="1.0">\n"""
+ str += u"".join(self._objects)
+ str += u"""</django-objects>"""
+ return str
+
+
+class Deserializer(xml_serializer.Deserializer):
+ """A Django Deserializer class to convert XML to Django objects.
+
+ This is a fairly manualy and simplistic XML parser, it supports just enough
+ functionality to read the keys and fields for an entity from the XML file and
+ construct a model object.
+ """
+
+ def next(self):
+ """Replacement next method to look for 'entity'.
+
+ The default next implementation exepects 'object' nodes which is not
+ what the entity's ToXml output provides.
+ """
+ for event, node in self.event_stream:
+ if event == "START_ELEMENT" and node.nodeName == "entity":
+ self.event_stream.expandNode(node)
+ return self._handle_object(node)
+ raise StopIteration
+
+ def _handle_object(self, node):
+ """Convert an <entity> node to a DeserializedObject"""
+ Model = self._get_model_from_node(node, "kind")
+ data = {}
+ key = db.Key(node.getAttribute("key"))
+ if key.name():
+ data["key_name"] = key.name()
+ parent = None
+ if key.parent():
+ parent = FakeParent(key.parent())
+ m2m_data = {}
+
+ # Deseralize each field.
+ for field_node in node.getElementsByTagName("property"):
+ # If the field is missing the name attribute, bail (are you
+ # sensing a pattern here?)
+ field_name = field_node.getAttribute("name")
+ if not field_name:
+ raise base.DeserializationError("<field> node is missing the 'name' "
+ "attribute")
+ field = Model.properties()[field_name]
+ field_value = getInnerText(field_node).strip()
+
+ if isinstance(field, db.Reference):
+ m = re.match("tag:.*\[(.*)\]", field_value)
+ if not m:
+ raise base.DeserializationError(u"Invalid reference value: '%s'" %
+ field_value)
+ key = m.group(1)
+ key_obj = db.Key(key)
+ if not key_obj.name():
+ raise base.DeserializationError(u"Cannot load Reference with "
+ "unnamed key: '%s'" % field_value)
+ data[field.name] = key_obj
+ else:
+ data[field.name] = field.validate(field_value)
+
+ # Create the new model instance with all it's data, but no parent.
+ object = Model(**data)
+ # Now add the parent into the hidden attribute, bypassing the type checks
+ # in the Model's __init__ routine.
+ object._parent = parent
+ # When the deserialized object is saved our replacement DeserializedObject
+ # class will set object._parent to force the real parent model to be loaded
+ # the first time it is referenced.
+ return base.DeserializedObject(object, m2m_data)
diff --git a/appengine_django/sessions/__init__.py b/appengine_django/sessions/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/appengine_django/sessions/__init__.py
diff --git a/appengine_django/sessions/__init__.pyc b/appengine_django/sessions/__init__.pyc
new file mode 100644
index 0000000..fd13c38
--- /dev/null
+++ b/appengine_django/sessions/__init__.pyc
Binary files differ
diff --git a/appengine_django/sessions/backends/__init__.py b/appengine_django/sessions/backends/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/appengine_django/sessions/backends/__init__.py
diff --git a/appengine_django/sessions/backends/__init__.pyc b/appengine_django/sessions/backends/__init__.pyc
new file mode 100644
index 0000000..4242888
--- /dev/null
+++ b/appengine_django/sessions/backends/__init__.pyc
Binary files differ
diff --git a/appengine_django/sessions/backends/db.py b/appengine_django/sessions/backends/db.py
new file mode 100644
index 0000000..e2e3aa4
--- /dev/null
+++ b/appengine_django/sessions/backends/db.py
@@ -0,0 +1,82 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 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.
+
+from datetime import datetime
+
+from django.contrib.sessions.backends import base
+from django.core.exceptions import SuspiciousOperation
+
+from appengine_django.sessions.models import Session
+
+
+class SessionStore(base.SessionBase):
+ """A key-based session store for Google App Engine."""
+
+ def load(self):
+ session = self._get_session(self.session_key)
+ if session:
+ try:
+ return self.decode(session.session_data)
+ except SuspiciousOperation:
+ # Create a new session_key for extra security.
+ pass
+ self.session_key = self._get_new_session_key()
+ self._session_cache = {}
+ self.save()
+ # Ensure the user is notified via a new cookie.
+ self.modified = True
+ return {}
+
+ def save(self, must_create=False):
+ if must_create and self.exists(self.session_key):
+ raise base.CreateError
+ session = Session(
+ key_name='k:' + self.session_key,
+ session_data = self.encode(self._session),
+ expire_date = self.get_expiry_date())
+ session.put()
+
+ def exists(self, session_key):
+ return Session.get_by_key_name('k:' + session_key) is not None
+
+ def delete(self, session_key=None):
+ if session_key is None:
+ session_key = self._session_key
+ session = self._get_session(session_key=session_key)
+ if session:
+ session.delete()
+
+ def _get_session(self, session_key):
+ session = Session.get_by_key_name('k:' + session_key)
+ if session:
+ if session.expire_date > datetime.now():
+ return session
+ session.delete()
+ return None
+
+ def create(self):
+ while True:
+ self.session_key = self._get_new_session_key()
+ try:
+ # Save immediately to ensure we have a unique entry in the
+ # database.
+ self.save(must_create=True)
+ except base.CreateError:
+ # Key wasn't unique. Try again.
+ continue
+ self.modified = True
+ self._session_cache = {}
+ return
diff --git a/appengine_django/sessions/backends/db.pyc b/appengine_django/sessions/backends/db.pyc
new file mode 100644
index 0000000..934b2f7
--- /dev/null
+++ b/appengine_django/sessions/backends/db.pyc
Binary files differ
diff --git a/appengine_django/sessions/models.py b/appengine_django/sessions/models.py
new file mode 100644
index 0000000..1681644
--- /dev/null
+++ b/appengine_django/sessions/models.py
@@ -0,0 +1,22 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 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.
+
+from google.appengine.ext import db
+
+class Session(db.Model):
+ """Django compatible App Engine Datastore session model."""
+ session_data = db.BlobProperty()
+ expire_date = db.DateTimeProperty()
diff --git a/appengine_django/sessions/models.pyc b/appengine_django/sessions/models.pyc
new file mode 100644
index 0000000..8db907d
--- /dev/null
+++ b/appengine_django/sessions/models.pyc
Binary files differ
diff --git a/appengine_django/tests/__init__.py b/appengine_django/tests/__init__.py
new file mode 100644
index 0000000..b511f58
--- /dev/null
+++ b/appengine_django/tests/__init__.py
@@ -0,0 +1,56 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 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.
+
+"""Loads all the _test.py files into the top level of the package.
+
+This file is a hack around the fact that Django expects the tests "module" to
+be a single tests.py file and cannot handle a tests package inside an
+application.
+
+All _test.py files inside this package are imported and any classes derived
+from unittest.TestCase are then referenced from this file itself so that they
+appear at the top level of the tests "module" that Django will import.
+"""
+
+
+import os
+import re
+import types
+import unittest
+
+TEST_RE = r"^.*_test.py$"
+
+# Search through every file inside this package.
+test_names = []
+test_dir = os.path.dirname( __file__)
+for filename in os.listdir(test_dir):
+ if not re.match(TEST_RE, filename):
+ continue
+ # Import the test file and find all TestClass clases inside it.
+ test_module = __import__('appengine_django.tests.%s' %
+ filename[:-3], {}, {},
+ filename[:-3])
+ for name in dir(test_module):
+ item = getattr(test_module, name)
+ if not (isinstance(item, (type, types.ClassType)) and
+ issubclass(item, unittest.TestCase)):
+ continue
+ # Found a test, bring into the module namespace.
+ exec "%s = item" % name
+ test_names.append(name)
+
+# Hide everything other than the test cases from other modules.
+__all__ = test_names
diff --git a/appengine_django/tests/commands_test.py b/appengine_django/tests/commands_test.py
new file mode 100755
index 0000000..a02ddbf
--- /dev/null
+++ b/appengine_django/tests/commands_test.py
@@ -0,0 +1,183 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 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.
+
+"""
+Tests that the manage.py commands execute correctly.
+
+These tests only verify that the commands execute and exit with a success code.
+They are intended to catch import exceptions and similar problems, it is left
+up to tests in other modules to verify that the functionality of each command
+works correctly.
+"""
+
+
+import os
+import re
+import signal
+import subprocess
+import tempfile
+import time
+import unittest
+
+from django.db.models import get_models
+
+from google.appengine.ext import db
+from appengine_django.models import BaseModel
+from appengine_django.models import ModelManager
+from appengine_django.models import ModelOptions
+from appengine_django.models import RegistrationTestModel
+
+
+class CommandsTest(unittest.TestCase):
+ """Unit tests for the manage.py commands."""
+
+ # How many seconds to wait for a command to exit.
+ COMMAND_TIMEOUT = 10
+
+ def runCommand(self, command, args=None, int_after=None, input=None):
+ """Helper to run the specified command in a child process.
+
+ Args:
+ command: The name of the command to run.
+ args: List of command arguments to run the command with.
+ int_after: If set to a positive integer, SIGINT will be sent to the
+ running child process after this many seconds to cause an exit. This
+ should be less than the COMMAND_TIMEOUT value (10 seconds).
+ input: A string to write to stdin when the command starts. stdin is
+ closed after the string is written.
+
+ Returns:
+ rc: The integer return code of the process.
+ output: A string containing the childs output.
+ """
+ if not args:
+ args = []
+ start = time.time()
+ int_sent = False
+ fd = subprocess.PIPE
+
+ child = subprocess.Popen(["./manage.py", command] + args, stdin=fd,
+ stdout=fd, stderr=fd, cwd=os.getcwdu())
+ if input:
+ child.stdin.write(input)
+ child.stdin.close()
+
+ while 1:
+ rc = child.poll()
+ if rc is not None:
+ # Child has exited.
+ break
+ elapsed = time.time() - start
+ if int_after and int_after > 0 and elapsed > int_after and not int_sent:
+ # Sent SIGINT as requested, give child time to exit cleanly.
+ os.kill(child.pid, signal.SIGINT)
+ start = time.time()
+ int_sent = True
+ continue
+ if elapsed < self.COMMAND_TIMEOUT:
+ continue
+ # Command is over time, kill and exit loop.
+ os.kill(child.pid, signal.SIGKILL)
+ time.sleep(2) # Give time for the signal to be received.
+ break
+
+ # Return status and output.
+ return rc, child.stdout.read(), child.stderr.read()
+
+ def assertCommandSucceeds(self, command, *args, **kwargs):
+ """Asserts that the specified command successfully completes.
+
+ Args:
+ command: The name of the command to run.
+ All other arguments are passed directly through to the runCommand
+ routine.
+
+ Raises:
+ This function does not return anything but will raise assertion errors if
+ the command does not exit successfully.
+ """
+ rc, stdout, stderr = self.runCommand(command, *args, **kwargs)
+ fd, tempname = tempfile.mkstemp()
+ os.write(fd, stdout)
+ os.close(fd)
+ self.assertEquals(0, rc,
+ "%s did not return successfully (rc: %d): Output in %s" %
+ (command, rc, tempname))
+ os.unlink(tempname)
+
+ def getCommands(self):
+ """Returns a list of valid commands for manage.py.
+
+ Args:
+ None
+
+ Returns:
+ A list of valid commands for manage.py as read from manage.py's help
+ output.
+ """
+ rc, stdout, stderr = self.runCommand("help")
+ parts = re.split("Available subcommands:", stderr)
+ if len(parts) < 2:
+ return []
+
+ return [t.strip() for t in parts[-1].split("\n") if t.strip()]
+
+ def testDiffSettings(self):
+ """Tests the diffsettings command."""
+ self.assertCommandSucceeds("diffsettings")
+
+ def testDumpData(self):
+ """Tests the dumpdata command."""
+ self.assertCommandSucceeds("dumpdata")
+
+ def testFlush(self):
+ """Tests the flush command."""
+ self.assertCommandSucceeds("flush")
+
+ def testLoadData(self):
+ """Tests the loaddata command."""
+ self.assertCommandSucceeds("loaddata")
+
+ def testLoadData(self):
+ """Tests the loaddata command."""
+ self.assertCommandSucceeds("loaddata")
+
+ def testReset(self):
+ """Tests the reste command."""
+ self.assertCommandSucceeds("reset", ["appengine_django"])
+
+ def testRunserver(self):
+ """Tests the runserver command."""
+ self.assertCommandSucceeds("runserver", int_after=2.0)
+
+ def testShell(self):
+ """Tests the shell command."""
+ self.assertCommandSucceeds("shell", input="exit")
+
+ def testUpdate(self):
+ """Tests that the update command exists.
+
+ Cannot test that it works without mocking out parts of dev_appserver so for
+ now we just assume that if it is present it will work.
+ """
+ cmd_list = self.getCommands()
+ self.assert_("update" in cmd_list)
+
+ def testZipCommandListFiltersCorrectly(self):
+ """When running under a zipfile test that only valid commands are found."""
+ cmd_list = self.getCommands()
+ self.assert_("__init__" not in cmd_list)
+ self.assert_("base" not in cmd_list)
diff --git a/appengine_django/tests/core_test.py b/appengine_django/tests/core_test.py
new file mode 100755
index 0000000..7454177
--- /dev/null
+++ b/appengine_django/tests/core_test.py
@@ -0,0 +1,37 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 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.
+
+"""Tests that the core module functionality is present and functioning."""
+
+
+import unittest
+
+from appengine_django import appid
+from appengine_django import have_appserver
+
+
+class AppengineDjangoTest(unittest.TestCase):
+ """Tests that the helper module has been correctly installed."""
+
+ def testAppidProvided(self):
+ """Tests that application ID and configuration has been loaded."""
+ self.assert_(appid is not None)
+
+ def testAppserverDetection(self):
+ """Tests that the appserver detection flag is present and correct."""
+ # It seems highly unlikely that these tests would ever be run from within
+ # an appserver.
+ self.assertEqual(have_appserver, False)
diff --git a/appengine_django/tests/db_test.py b/appengine_django/tests/db_test.py
new file mode 100755
index 0000000..452e8f9
--- /dev/null
+++ b/appengine_django/tests/db_test.py
@@ -0,0 +1,62 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 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.
+
+"""Tests that the db module correctly initialises the API stubs."""
+
+
+import unittest
+
+from django.db import connection
+from django.db.backends.appengine.base import DatabaseWrapper
+
+from appengine_django import appid
+from appengine_django.db import base
+
+
+class DatastoreTest(unittest.TestCase):
+ """Tests that the datastore stubs have been correctly setup."""
+
+ def testDjangoDBConnection(self):
+ """Tests that the Django DB connection is using our replacement."""
+ self.assert_(isinstance(connection, DatabaseWrapper))
+
+ def testDjangoDBConnectionStubs(self):
+ """Tests that members required by Django are stubbed."""
+ self.assert_(hasattr(connection, "features"))
+ self.assert_(hasattr(connection, "ops"))
+
+ def testDjangoDBErrorClasses(self):
+ """Tests that the error classes required by Django are stubbed."""
+ self.assert_(hasattr(base, "DatabaseError"))
+ self.assert_(hasattr(base, "IntegrityError"))
+
+ def testDatastorePath(self):
+ """Tests that the datastore path contains the app name."""
+ d_path, h_path = base.get_datastore_paths()
+ self.assertNotEqual(-1, d_path.find("django_%s" % appid))
+ self.assertNotEqual(-1, h_path.find("django_%s" % appid))
+
+ def testTestInMemoryDatastorePath(self):
+ """Tests that the test datastore is using the in-memory datastore."""
+ td_path, th_path = base.get_test_datastore_paths()
+ self.assert_(td_path is None)
+ self.assert_(th_path is None)
+
+ def testTestFilesystemDatastorePath(self):
+ """Tests that the test datastore is on the filesystem when requested."""
+ td_path, th_path = base.get_test_datastore_paths(False)
+ self.assertNotEqual(-1, td_path.find("testdatastore"))
+ self.assertNotEqual(-1, th_path.find("testdatastore"))
diff --git a/appengine_django/tests/memcache_test.py b/appengine_django/tests/memcache_test.py
new file mode 100644
index 0000000..4e5f02e
--- /dev/null
+++ b/appengine_django/tests/memcache_test.py
@@ -0,0 +1,43 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 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.
+
+"""Ensures the App Engine memcache API works as Django's memcache backend."""
+
+import unittest
+
+from django.core.cache import get_cache
+from appengine_django import appid
+from appengine_django import have_appserver
+
+
+class AppengineMemcacheTest(unittest.TestCase):
+ """Tests that the memcache backend works."""
+
+ def setUp(self):
+ """Get the memcache cache module so it is available to tests."""
+ self._cache = get_cache("memcached://")
+
+ def testSimpleSetGet(self):
+ """Tests that a simple set/get operation through the cache works."""
+ self._cache.set("test_key", "test_value")
+ self.assertEqual(self._cache.get("test_key"), "test_value")
+
+ def testDelete(self):
+ """Tests that delete removes values from the cache."""
+ self._cache.set("test_key", "test_value")
+ self.assertEqual(self._cache.has_key("test_key"), True)
+ self._cache.delete("test_key")
+ self.assertEqual(self._cache.has_key("test_key"), False)
diff --git a/appengine_django/tests/model_test.py b/appengine_django/tests/model_test.py
new file mode 100755
index 0000000..8611d8b
--- /dev/null
+++ b/appengine_django/tests/model_test.py
@@ -0,0 +1,110 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 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.
+
+"""Tests that the combined appengine and Django models function correctly."""
+
+
+import unittest
+
+from django import VERSION
+from django.db.models import get_models
+from django import forms
+
+from google.appengine.ext.db import djangoforms
+from google.appengine.ext import db
+
+from appengine_django.models import BaseModel
+from appengine_django.models import ModelManager
+from appengine_django.models import ModelOptions
+from appengine_django.models import RegistrationTestModel
+
+
+class TestModelWithProperties(BaseModel):
+ """Test model class for checking property -> Django field setup."""
+ property1 = db.StringProperty()
+ property2 = db.IntegerProperty()
+ property3 = db.Reference()
+
+
+class ModelTest(unittest.TestCase):
+ """Unit tests for the combined model class."""
+
+ def testModelRegisteredWithDjango(self):
+ """Tests that a combined model class has been registered with Django."""
+ self.assert_(RegistrationTestModel in get_models())
+
+ def testDatastoreModelProperties(self):
+ """Tests that a combined model class still has datastore properties."""
+ self.assertEqual(3, len(TestModelWithProperties.properties()))
+
+ def testDjangoModelClass(self):
+ """Tests the parts of a model required by Django are correctly stubbed."""
+ # Django requires model options to be found at ._meta.
+ self.assert_(isinstance(RegistrationTestModel._meta, ModelOptions))
+ # Django requires a manager at .objects
+ self.assert_(isinstance(RegistrationTestModel.objects, ModelManager))
+ # Django requires ._default_manager.
+ self.assert_(hasattr(RegistrationTestModel, "_default_manager"))
+
+ def testDjangoModelFields(self):
+ """Tests that a combined model class has (faked) Django fields."""
+ fields = TestModelWithProperties._meta.local_fields
+ self.assertEqual(3, len(fields))
+ # Check each fake field has the minimal properties that Django needs.
+ for field in fields:
+ # The Django serialization code looks for rel to determine if the field
+ # is a relationship/reference to another model.
+ self.assert_(hasattr(field, "rel"))
+ # serialize is required to tell Django to serialize the field.
+ self.assertEqual(True, field.serialize)
+ if field.name == "property3":
+ # Extra checks for the Reference field.
+ # rel.field_name is used during serialization to find the field in the
+ # other model that this field is related to. This should always be
+ # 'key_name' for appengine models.
+ self.assertEqual("key_name", field.rel.field_name)
+
+ def testDjangoModelOptionsStub(self):
+ """Tests that the options stub has the required properties by Django."""
+ # Django requires object_name and app_label for serialization output.
+ self.assertEqual("RegistrationTestModel",
+ RegistrationTestModel._meta.object_name)
+ self.assertEqual("appengine_django", RegistrationTestModel._meta.app_label)
+ # The pk.name member is required during serialization for dealing with
+ # related fields.
+ self.assertEqual("key_name", RegistrationTestModel._meta.pk.name)
+ # The many_to_many method is called by Django in the serialization code to
+ # find m2m relationships. m2m is not supported by the datastore.
+ self.assertEqual([], RegistrationTestModel._meta.many_to_many)
+
+ def testDjangoModelManagerStub(self):
+ """Tests that the manager stub acts as Django would expect."""
+ # The serialization code calls model.objects.all() to retrieve all objects
+ # to serialize.
+ self.assertEqual([], list(RegistrationTestModel.objects.all()))
+
+ def testDjangoModelPK(self):
+ """Tests that each model instance has a 'primary key' generated."""
+ obj = RegistrationTestModel(key_name="test")
+ obj.put()
+ pk = obj._get_pk_val()
+ self.assert_(pk)
+ new_obj = RegistrationTestModel.get(pk)
+ self.assertEqual(obj.key(), new_obj.key())
+
+ def testModelFormPatched(self):
+ """Tests that the Django ModelForm is being successfully patched."""
+ self.assertEqual(djangoforms.ModelForm, forms.ModelForm)
diff --git a/appengine_django/tests/serialization_test.py b/appengine_django/tests/serialization_test.py
new file mode 100755
index 0000000..39b92ea
--- /dev/null
+++ b/appengine_django/tests/serialization_test.py
@@ -0,0 +1,310 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 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.
+
+"""Tests that the serialization modules are functioning correctly.
+
+In particular, these tests verify that the modifications made to the standard
+Django serialization modules function correctly and that the combined datastore
+and Django models can be dumped and loaded to all of the provided formats.
+"""
+
+
+import os
+import re
+import unittest
+from StringIO import StringIO
+
+from django.core import serializers
+
+from google.appengine.ext import db
+from appengine_django.models import BaseModel
+
+
+class ModelA(BaseModel):
+ description = db.StringProperty()
+
+
+class ModelB(BaseModel):
+ description = db.StringProperty()
+ friend = db.Reference(ModelA)
+
+
+class TestAllFormats(type):
+
+ def __new__(cls, name, bases, attrs):
+ """Extends base test functions to be called for every serialisation format.
+
+ Looks for functions matching 'run.*Test', where the wildcard in the middle
+ matches the desired test name and ensures that a test case is setup to call
+ that function once for every defined serialisation format. The test case
+ that is created will be called 'test<format><name>'. Eg, for the function
+ 'runKeyedObjectTest' functions like 'testJsonKeyedObject' will be created.
+ """
+ test_formats = serializers.get_serializer_formats()
+ test_formats.remove("python") # Python serializer is only used indirectly.
+
+ for func_name in attrs.keys():
+ m = re.match("^run(.*)Test$", func_name)
+ if not m:
+ continue
+ for format in test_formats:
+ test_name = "test%s%s" % (format.title(), m.group(1))
+ test_func = eval("lambda self: getattr(self, \"%s\")(\"%s\")" %
+ (func_name, format))
+ attrs[test_name] = test_func
+
+ return super(TestAllFormats, cls).__new__(cls, name, bases, attrs)
+
+
+class SerializationTest(unittest.TestCase):
+ """Unit tests for the serialization/deserialization functionality.
+
+ Tests that every loaded serialization format can successfully dump and then
+ reload objects without the objects changing.
+ """
+ __metaclass__ = TestAllFormats
+
+ def compareObjects(self, orig, new, format="unknown"):
+ """Compares two objects to ensure they are identical.
+
+ Args:
+ orig: The original object, must be an instance of db.Model.
+ new: The new object, must be an instance of db.Model.
+ format: The serialization format being tested, used to make error output
+ more helpful.
+
+ Raises:
+ The function has no return value, but will raise assertion errors if the
+ objects do not match correctly.
+ """
+ if orig.key().name():
+ # Only compare object keys when the key is named. Key IDs are not static
+ # and will change between dump/load. If you want stable Keys they need to
+ # be named!
+ self.assertEqual(orig.key(), new.key(),
+ "keys not equal after %s serialization: %s != %s" %
+ (format, repr(orig.key()), repr(new.key())))
+
+ for key in orig.properties().keys():
+ oval = getattr(orig, key)
+ nval = getattr(new, key)
+ if isinstance(orig.properties()[key], db.Reference):
+ # Need to compare object keys not the objects themselves.
+ oval = oval.key()
+ nval = nval.key()
+ self.assertEqual(oval, nval, "%s attribute differs after %s "
+ "serialization: %s != %s" % (key, format, oval, nval))
+
+ def doSerialisationTest(self, format, obj, rel_attr=None, obj_ref=None):
+ """Runs a serialization test on an object for the specified format.
+
+ Args:
+ format: The name of the Django serialization class to use.
+ obj: The object to {,de}serialize, must be an instance of db.Model.
+ rel_attr: Name of the attribute of obj references another model.
+ obj_ref: The expected object reference, must be an instance of db.Model.
+
+ Raises:
+ The function has no return value but raises assertion errors if the
+ object cannot be successfully serialized and then deserialized back to an
+ identical object. If rel_attr and obj_ref are specified the deserialized
+ object must also retain the references from the original object.
+ """
+ serialised = serializers.serialize(format, [obj])
+ # Try and get the object back from the serialized string.
+ result = list(serializers.deserialize(format, StringIO(serialised)))
+ self.assertEqual(1, len(result),
+ "%s serialization should create 1 object" % format)
+ result[0].save() # Must save back into the database to get a Key.
+ self.compareObjects(obj, result[0].object, format)
+ if rel_attr and obj_ref:
+ rel = getattr(result[0].object, rel_attr)
+ if callable(rel):
+ rel = rel()
+ self.compareObjects(rel, obj_ref, format)
+
+ def doLookupDeserialisationReferenceTest(self, lookup_dict, format):
+ """Tests the Key reference is loaded OK for a format.
+
+ Args:
+ lookup_dict: A dictionary indexed by format containing serialized strings
+ of the objects to load.
+ format: The format to extract from the dict and deserialize.
+
+ Raises:
+ This function has no return value but raises assertion errors if the
+ string cannot be deserialized correctly or the resulting object does not
+ reference the object correctly.
+ """
+ if format not in lookup_dict:
+ # Check not valid for this format.
+ return
+ obj = ModelA(description="test object", key_name="test")
+ obj.put()
+ s = lookup_dict[format]
+ result = list(serializers.deserialize(format, StringIO(s)))
+ self.assertEqual(1, len(result), "expected 1 object from %s" % format)
+ result[0].save()
+ self.compareObjects(obj, result[0].object.friend, format)
+
+ def doModelKeyDeserialisationReferenceTest(self, lookup_dict, format):
+ """Tests a model with a key can be loaded OK for a format.
+
+ Args:
+ lookup_dict: A dictionary indexed by format containing serialized strings
+ of the objects to load.
+ format: The format to extract from the dict and deserialize.
+
+ Returns:
+ This function has no return value but raises assertion errors if the
+ string cannot be deserialized correctly or the resulting object is not an
+ instance of ModelA with a key named 'test'.
+ """
+ if format not in lookup_dict:
+ # Check not valid for this format.
+ return
+ s = lookup_dict[format]
+ result = list(serializers.deserialize(format, StringIO(s)))
+ self.assertEqual(1, len(result), "expected 1 object from %s" % format)
+ result[0].save()
+ self.assert_(isinstance(result[0].object, ModelA))
+ self.assertEqual("test", result[0].object.key().name())
+
+ # Lookup dicts for the above (doLookupDeserialisationReferenceTest) function.
+ SERIALIZED_WITH_KEY_AS_LIST = {
+ "json": """[{"pk": "agR0ZXN0chMLEgZNb2RlbEIiB21vZGVsYmkM", """
+ """"model": "tests.modelb", "fields": {"description": "test", """
+ """"friend": ["ModelA", "test"] }}]""",
+ "yaml": """- fields: {description: !!python/unicode 'test', friend: """
+ """ [ModelA, test]}\n model: tests.modelb\n pk: """
+ """ agR0ZXN0chMLEgZNb2RlbEEiB21vZGVsYWkM\n"""
+ }
+ SERIALIZED_WITH_KEY_REPR = {
+ "json": """[{"pk": "agR0ZXN0chMLEgZNb2RlbEIiB21vZGVsYmkM", """
+ """"model": "tests.modelb", "fields": {"description": "test", """
+ """"friend": "datastore_types.Key.from_path("""
+ """'ModelA', 'test')" }}]""",
+ "yaml": """- fields: {description: !!python/unicode 'test', friend: """
+ """\'datastore_types.Key.from_path("ModelA", "test")\'}\n """
+ """model: tests.modelb\n pk: """
+ """ agR0ZXN0chMLEgZNb2RlbEEiB21vZGVsYWkM\n"""
+ }
+
+ # Lookup dict for the doModelKeyDeserialisationReferenceTest function.
+ MK_SERIALIZED_WITH_LIST = {
+ "json": """[{"pk": ["ModelA", "test"], "model": "tests.modela", """
+ """"fields": {}}]""",
+ "yaml": """-\n fields: {description: null}\n model: tests.modela\n """
+ """pk: [ModelA, test]\n"""
+ }
+ MK_SERIALIZED_WITH_KEY_REPR = {
+ "json": """[{"pk": "datastore_types.Key.from_path('ModelA', 'test')", """
+ """"model": "tests.modela", "fields": {}}]""",
+ "yaml": """-\n fields: {description: null}\n model: tests.modela\n """
+ """pk: \'datastore_types.Key.from_path("ModelA", "test")\'\n"""
+ }
+ MK_SERIALIZED_WITH_KEY_AS_TEXT = {
+ "json": """[{"pk": "test", "model": "tests.modela", "fields": {}}]""",
+ "yaml": """-\n fields: {description: null}\n model: tests.modela\n """
+ """pk: test\n"""
+ }
+
+ # Lookup dict for the function.
+ SERIALIZED_WITH_NON_EXISTANT_PARENT = {
+ "json": """[{"pk": "ahhnb29nbGUtYXBwLWVuZ2luZS1kamFuZ29yIgsSBk1vZG"""
+ """VsQiIGcGFyZW50DAsSBk1vZGVsQSIEdGVzdAw", """
+ """"model": "tests.modela", "fields": """
+ """{"description": null}}]""",
+ "yaml": """- fields: {description: null}\n """
+ """model: tests.modela\n """
+ """pk: ahhnb29nbGUtYXBwLWVuZ2luZS1kamFuZ29yIgsSBk1"""
+ """vZGVsQiIGcGFyZW50DAsSBk1vZGVsQSIEdGVzdAw\n""",
+ "xml": """<?xml version="1.0" encoding="utf-8"?>\n"""
+ """<django-objects version="1.0">\n"""
+ """<entity kind="tests.modela" key="ahhnb29nbGUtYXBwL"""
+ """WVuZ2luZS1kamFuZ29yIgsSBk1vZGVsQiIGcGFyZW50DA"""
+ """sSBk1vZGVsQSIEdGVzdAw">\n """
+ """<key>tag:google-app-engine-django.gmail.com,"""
+ """2008-05-13:ModelA[ahhnb29nbGUtYXBwLWVuZ2luZS1kam"""
+ """FuZ29yIgsSBk1vZGVsQiIGcGFyZW50DAsSBk1vZGVsQSIEdGVzdAw"""
+ """]</key>\n <property name="description" """
+ """type="null"></property>\n</entity>\n</django-objects>"""
+ }
+
+ # The following functions are all expanded by the metaclass to be run once
+ # for every registered Django serialization module.
+
+ def runKeyedObjectTest(self, format):
+ """Test serialization of a basic object with a named key."""
+ obj = ModelA(description="test object", key_name="test")
+ obj.put()
+ self.doSerialisationTest(format, obj)
+
+ def runObjectWithIdTest(self, format):
+ """Test serialization of a basic object with a numeric ID key."""
+ obj = ModelA(description="test object")
+ obj.put()
+ self.doSerialisationTest(format, obj)
+
+ def runObjectWithReferenceTest(self, format):
+ """Test serialization of an object that references another object."""
+ obj = ModelA(description="test object", key_name="test")
+ obj.put()
+ obj2 = ModelB(description="friend object", friend=obj)
+ obj2.put()
+ self.doSerialisationTest(format, obj2, "friend", obj)
+
+ def runObjectWithParentTest(self, format):
+ """Test serialization of an object that has a parent object reference."""
+ obj = ModelA(description="parent object", key_name="parent")
+ obj.put()
+ obj2 = ModelA(description="child object", key_name="child", parent=obj)
+ obj2.put()
+ self.doSerialisationTest(format, obj2, "parent", obj)
+
+ def runObjectWithNonExistantParentTest(self, format):
+ """Test deserialization of an object referencing a non-existant parent."""
+ self.doModelKeyDeserialisationReferenceTest(
+ self.SERIALIZED_WITH_NON_EXISTANT_PARENT, format)
+
+ def runCreateKeyReferenceFromListTest(self, format):
+ """Tests that a reference specified as a list in json/yaml can be loaded OK."""
+ self.doLookupDeserialisationReferenceTest(self.SERIALIZED_WITH_KEY_AS_LIST,
+ format)
+
+ def runCreateKeyReferenceFromReprTest(self, format):
+ """Tests that a reference specified as repr(Key) in can loaded OK."""
+ self.doLookupDeserialisationReferenceTest(self.SERIALIZED_WITH_KEY_REPR,
+ format)
+
+ def runCreateModelKeyFromListTest(self, format):
+ """Tests that a model key specified as a list can be loaded OK."""
+ self.doModelKeyDeserialisationReferenceTest(self.MK_SERIALIZED_WITH_LIST,
+ format)
+
+ def runCreateModelKeyFromReprTest(self, format):
+ """Tests that a model key specified as a repr(Key) can be loaded OK."""
+ self.doModelKeyDeserialisationReferenceTest(
+ self.MK_SERIALIZED_WITH_KEY_REPR, format)
+
+ def runCreateModelKeyFromTextTest(self, format):
+ """Tests that a reference specified as a plain key_name loads OK."""
+ self.doModelKeyDeserialisationReferenceTest(
+ self.MK_SERIALIZED_WITH_KEY_AS_TEXT, format)
+
+
+if __name__ == '__main__':
+ unittest.main()