summaryrefslogtreecommitdiffstats
path: root/appengine_django/__init__.py
diff options
context:
space:
mode:
Diffstat (limited to 'appengine_django/__init__.py')
-rw-r--r--appengine_django/__init__.py528
1 files changed, 528 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