diff options
| author | bryan newbold <bnewbold@snark.mit.edu> | 2009-06-14 19:51:50 -0400 | 
|---|---|---|
| committer | bryan newbold <bnewbold@snark.mit.edu> | 2009-06-14 19:51:50 -0400 | 
| commit | ae078728c8bc1c7d4412c4cb19b6802e9b7e8f74 (patch) | |
| tree | 978ab726bbad7575ffb66d5e2ceee6b69a1096f1 /appengine_django | |
| parent | 0c17a5bb3d6b637f0197418f792045509b7e2d7b (diff) | |
| download | equator-ae078728c8bc1c7d4412c4cb19b6802e9b7e8f74.tar.gz equator-ae078728c8bc1c7d4412c4cb19b6802e9b7e8f74.zip  | |
initial appengine setup; BROKEN
Diffstat (limited to 'appengine_django')
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 Binary files differnew file mode 100644 index 0000000..12557cb --- /dev/null +++ b/appengine_django/__init__.pyc 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 Binary files differnew file mode 100644 index 0000000..4d1edc3 --- /dev/null +++ b/appengine_django/auth/__init__.pyc 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 Binary files differnew file mode 100644 index 0000000..477c819 --- /dev/null +++ b/appengine_django/auth/decorators.pyc 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 Binary files differnew file mode 100644 index 0000000..ff36de8 --- /dev/null +++ b/appengine_django/auth/middleware.pyc 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 Binary files differnew file mode 100644 index 0000000..0a73b9c --- /dev/null +++ b/appengine_django/auth/models.pyc 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 Binary files differnew file mode 100644 index 0000000..7121f59 --- /dev/null +++ b/appengine_django/auth/templatetags.pyc 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 Binary files differnew file mode 100644 index 0000000..879eeaa --- /dev/null +++ b/appengine_django/db/__init__.pyc 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 Binary files differnew file mode 100644 index 0000000..8699add --- /dev/null +++ b/appengine_django/db/base.pyc 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 Binary files differnew file mode 100644 index 0000000..2055fb6 --- /dev/null +++ b/appengine_django/db/creation.pyc 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 Binary files differnew file mode 100644 index 0000000..71ab53f --- /dev/null +++ b/appengine_django/mail.pyc 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 Binary files differnew file mode 100644 index 0000000..e80538d --- /dev/null +++ b/appengine_django/management/__init__.pyc 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 Binary files differnew file mode 100644 index 0000000..48a948a --- /dev/null +++ b/appengine_django/management/commands/__init__.pyc 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 Binary files differnew file mode 100644 index 0000000..38abf92 --- /dev/null +++ b/appengine_django/management/commands/runserver.pyc 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 Binary files differnew file mode 100644 index 0000000..844eb68 --- /dev/null +++ b/appengine_django/management/commands/startapp.pyc 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 Binary files differnew file mode 100644 index 0000000..ed6c6ec --- /dev/null +++ b/appengine_django/models.pyc 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 Binary files differnew file mode 100644 index 0000000..c8abe0b --- /dev/null +++ b/appengine_django/replacement_imp.pyc 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 Binary files differnew file mode 100644 index 0000000..8dbe892 --- /dev/null +++ b/appengine_django/serializer/__init__.pyc 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 Binary files differnew file mode 100644 index 0000000..d9a3507 --- /dev/null +++ b/appengine_django/serializer/python.pyc 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 Binary files differnew file mode 100644 index 0000000..fd13c38 --- /dev/null +++ b/appengine_django/sessions/__init__.pyc 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 Binary files differnew file mode 100644 index 0000000..4242888 --- /dev/null +++ b/appengine_django/sessions/backends/__init__.pyc 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 Binary files differnew file mode 100644 index 0000000..934b2f7 --- /dev/null +++ b/appengine_django/sessions/backends/db.pyc 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 Binary files differnew file mode 100644 index 0000000..8db907d --- /dev/null +++ b/appengine_django/sessions/models.pyc 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()  | 
