Commits

Waldemar Kornewald  committed a96e962

reorganized package. added unit tests, renamed a few packages, and got rid of some of the default settings because they're a little bit too project-specific

  • Participants
  • Parent commits dcbecc9

Comments (0)

Files changed (20)

File aecmd.py

-# -*- coding: utf-8 -*-
-import logging, os, sys
-
-COMMON_DIR = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
-PROJECT_DIR = os.path.dirname(COMMON_DIR)
-ZIP_PACKAGES_DIRS = (os.path.join(PROJECT_DIR, 'zip-packages'),
-                     os.path.join(COMMON_DIR, 'zip-packages'))
-# Overrides for os.environ
-env_ext = {'DJANGO_SETTINGS_MODULE': 'settings'}
-
-def setup_env():
-    """Configures app engine environment for command-line apps."""
-    # Try to import the appengine code from the system path.
-    try:
-        from google.appengine.api import apiproxy_stub_map
-    except ImportError:
-        for k in [k for k in sys.modules if k.startswith('google')]:
-            del sys.modules[k]
-
-        # 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(COMMON_DIR, '.google_appengine'),
-                 '/usr/local/google_appengine',
-                 '/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine']
-        for path in os.environ.get('PATH', '').replace(';', ':').split(':'):
-            path = path.rstrip(os.sep)
-            if path.endswith('google_appengine'):
-                paths.append(path)
-        if os.name in ('nt', 'dos'):
-            prefix = '%(PROGRAMFILES)s' % os.environ
-            paths.append(prefix + r'\Google\google_appengine')
-        # Loop through all possible paths and look for the SDK dir.
-        SDK_PATH = None
-        for sdk_path in paths:
-            sdk_path = os.path.realpath(sdk_path)
-            if os.path.exists(sdk_path):
-                SDK_PATH = 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'
-                             'Visit http://code.google.com/p/app-engine-patch/'
-                             ' for installation instructions.\n')
-            sys.exit(1)
-        # Add the SDK and the libraries within it to the system path.
-        EXTRA_PATHS = [SDK_PATH]
-        lib = os.path.join(SDK_PATH, 'lib')
-        # Automatically add all packages in the SDK's lib folder:
-        for dir in os.listdir(lib):
-            path = os.path.join(lib, dir)
-            # Package can be under 'lib/<pkg>/<pkg>/' or 'lib/<pkg>/lib/<pkg>/'
-            detect = (os.path.join(path, dir), os.path.join(path, 'lib', dir))
-            for path in detect:
-                if os.path.isdir(path) and not dir == 'django':
-                    EXTRA_PATHS.append(os.path.dirname(path))
-                    break
-        sys.path = EXTRA_PATHS + sys.path
-        from google.appengine.api import apiproxy_stub_map
-
-    # Add this folder to sys.path
-    sys.path = [os.path.abspath(os.path.dirname(__file__))] + sys.path
-
-    setup_project()
-    setup_logging()
-
-def setup_threading():
-    # XXX: GAE's threading.local doesn't work correctly with subclassing
-    try:
-        from django.utils._threading_local import local
-        import threading
-        threading.local = local
-    except ImportError:
-        pass
-
-def setup_logging():
-    # Fix Python 2.6 logging module
-    logging.logMultiprocessing = 0
-
-    # Enable logging
-    from django.conf import settings
-    if settings.DEBUG:
-        logging.getLogger().setLevel(logging.DEBUG)
-    else:
-        logging.getLogger().setLevel(logging.INFO)
-
-def setup_project():
-    from .utils import on_production_server
-    if on_production_server:
-        # This fixes a pwd import bug for os.path.expanduser()
-        global env_ext
-        env_ext['HOME'] = PROJECT_DIR
-
-    os.environ.update(env_ext)
-
-    # Add the two parent folders and appenginepatcher's lib folder to sys.path.
-    # The current folder has to be added in main.py or setup_env(). This
-    # suggests a folder structure where you separate reusable code from project
-    # code:
-    # project -> common -> appenginepatch
-    # You can put a custom Django version into the "common" folder, for example.
-    EXTRA_PATHS = [
-        PROJECT_DIR,
-        COMMON_DIR,
-        os.path.dirname(PROJECT_DIR),
-    ]
-
-    EXTRA_PATHS.append(os.path.join(os.path.abspath(os.path.dirname(__file__)),
-        'lib'))
-
-    # We support zipped packages in the common and project folders.
-    # The files must be in the packages folder.
-    for packages_dir in ZIP_PACKAGES_DIRS:
-        if os.path.isdir(packages_dir):
-            for zip_package in os.listdir(packages_dir):
-                EXTRA_PATHS.append(os.path.join(packages_dir, zip_package))
-
-    # App Engine causes main.py to be reloaded if an exception gets raised
-    # on the first request of a main.py instance, so don't call setup_project()
-    # multiple times. We ensure this indirectly by checking if we've already
-    # modified sys.path.
-    if len(sys.path) < len(EXTRA_PATHS) or \
-            sys.path[:len(EXTRA_PATHS)] != EXTRA_PATHS:
-
-        sys.path = EXTRA_PATHS + sys.path
+import os, sys
+
+# We allow a two-level project structure where your root folder contains
+# project-specific apps and the "common" subfolder contains common apps.
+COMMON_DIR = os.path.dirname(os.path.dirname(__file__))
+PROJECT_DIR = os.path.dirname(COMMON_DIR)
+if os.path.basename(COMMON_DIR) == 'common-apps':
+    MAIN_DIRS = (PROJECT_DIR, COMMON_DIR)
+else:
+    PROJECT_DIR = COMMON_DIR
+    MAIN_DIRS = (PROJECT_DIR,)
+
+# Overrides for os.environ
+env_ext = {'DJANGO_SETTINGS_MODULE': 'settings'}
+
+def setup_env():
+    """Configures app engine environment for command-line apps."""
+    # Try to import the appengine code from the system path.
+    try:
+        from google.appengine.api import apiproxy_stub_map
+    except ImportError:
+        for k in [k for k in sys.modules if k.startswith('google')]:
+            del sys.modules[k]
+
+        # 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(PROJECT_DIR, '.google_appengine'),
+                 os.path.join(COMMON_DIR, '.google_appengine'),
+                 '/usr/local/google_appengine',
+                 '/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine']
+        for path in os.environ.get('PATH', '').replace(';', ':').split(':'):
+            path = path.rstrip(os.sep)
+            if path.endswith('google_appengine'):
+                paths.append(path)
+        if os.name in ('nt', 'dos'):
+            prefix = '%(PROGRAMFILES)s' % os.environ
+            paths.append(prefix + r'\Google\google_appengine')
+        # Loop through all possible paths and look for the SDK dir.
+        SDK_PATH = None
+        for sdk_path in paths:
+            sdk_path = os.path.realpath(sdk_path)
+            if os.path.exists(sdk_path):
+                SDK_PATH = 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'
+                             "Make sure it's accessible via your PATH environment.")
+            sys.exit(1)
+        # Add the SDK and the libraries within it to the system path.
+        EXTRA_PATHS = [SDK_PATH]
+        lib = os.path.join(SDK_PATH, 'lib')
+        # Automatically add all packages in the SDK's lib folder:
+        for dir in os.listdir(lib):
+            path = os.path.join(lib, dir)
+            # Package can be under 'lib/<pkg>/<pkg>/' or 'lib/<pkg>/lib/<pkg>/'
+            detect = (os.path.join(path, dir), os.path.join(path, 'lib', dir))
+            for path in detect:
+                if os.path.isdir(path) and not dir == 'django':
+                    EXTRA_PATHS.append(os.path.dirname(path))
+                    break
+        sys.path = EXTRA_PATHS + sys.path
+        from google.appengine.api import apiproxy_stub_map
+
+    setup_project()
+    setup_logging()
+
+def setup_threading():
+    # XXX: GAE's threading.local doesn't work correctly with subclassing
+    try:
+        from django.utils._threading_local import local
+        import threading
+        threading.local = local
+    except ImportError:
+        pass
+
+def setup_logging():
+    import logging
+
+    # Fix Python 2.6 logging module
+    logging.logMultiprocessing = 0
+
+    # Enable logging
+    from django.conf import settings
+    if settings.DEBUG:
+        logging.getLogger().setLevel(logging.DEBUG)
+    else:
+        logging.getLogger().setLevel(logging.INFO)
+
+def setup_project():
+    from .utils import on_production_server
+    if on_production_server:
+        # This fixes a pwd import bug for os.path.expanduser()
+        global env_ext
+        env_ext['HOME'] = PROJECT_DIR
+
+    os.environ.update(env_ext)
+
+    EXTRA_PATHS = list(MAIN_DIRS)
+    EXTRA_PATHS.append(os.path.dirname(PROJECT_DIR))
+    EXTRA_PATHS.append(os.path.join(os.path.dirname(__file__), 'lib'))
+
+    ZIP_PACKAGES_DIRS = tuple(os.path.join(dir, 'zip-packages')
+                              for dir in MAIN_DIRS)
+
+    # We support zipped packages in the common and project folders.
+    for packages_dir in ZIP_PACKAGES_DIRS:
+        if os.path.isdir(packages_dir):
+            for zip_package in os.listdir(packages_dir):
+                EXTRA_PATHS.append(os.path.join(packages_dir, zip_package))
+
+    # App Engine causes main.py to be reloaded if an exception gets raised
+    # on the first request of a main.py instance, so don't call setup_project()
+    # multiple times. We ensure this indirectly by checking if we've already
+    # modified sys.path.
+    if len(sys.path) < len(EXTRA_PATHS) or \
+            sys.path[:len(EXTRA_PATHS)] != EXTRA_PATHS:
+
+        sys.path = EXTRA_PATHS + sys.path
             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)
+            dev_appserver.SetupStubs(appid, **args)
         # If we're supposed to set up the remote_api, do that now.
         if not self.use_test_datastore: # Never when testing
             if self.use_remote:

File email/__init__.py

+from django.core.mail.backends.base import BaseEmailBackend
+from django.core.mail import EmailMultiAlternatives
+from django.core.exceptions import ImproperlyConfigured
+
+from google.appengine.api import mail as aeemail
+
+def _send_deferred(message):
+    message.send()
+
+class EmailBackend(BaseEmailBackend):
+    can_defer = False
+
+    def send_messages(self, email_messages):
+        num_sent = 0
+        for message in email_messages:
+            if self._send(message):
+                num_sent += 1
+        return num_sent
+
+    def _copy_message(self, message):
+        """Create and return App Engine EmailMessage class from message."""
+        gmsg = aeemail.EmailMessage(sender=message.from_email,
+                                    to=message.to,
+                                    subject=message.subject,
+                                    body=message.body)
+        if message.extra_headers.get('Reply-To', None):
+            gmsg.reply_to = message.extra_headers['Reply-To']
+        if message.bcc:
+            gmsg.bcc = list(message.bcc)
+        if message.attachments:
+            gmsg.attachments = [(a[0], a[1]) for a in message.attachments]
+        if isinstance(message, EmailMultiAlternatives):  # look for HTML
+            for content, mimetype in message.alternatives:
+                if mimetype == 'text/html':
+                    gmsg.html = content
+                    break
+        return gmsg
+
+    def _send(self, message):
+        try:
+            message = self._copy_message(message)
+        except (ValueError, aeemail.InvalidEmailError), err:
+            import logging
+            logging.warn(err)
+            if not self.fail_silently:
+                raise
+            return False
+        if self.can_defer:
+            self._defer_message(message)
+            return True
+        try:
+            message.send()
+        except aeemail.Error:
+            if not self.fail_silently:
+                raise
+            return False
+        return True
+
+    def _defer_message(self, message):
+        from google.appengine.ext import deferred
+        deferred.defer(_send_deferred, message)

File email/async.py

+from . import EmailBackend
+
+class EmailBackend(EmailBackend):
+    can_defer = True
-# -*- coding: utf-8 -*-
 import os, sys
 
-# Add parent folder to sys.path, so we can import aecmd.
+# Add parent folder to sys.path, so we can import boot.
 # App Engine causes main.py to be reloaded if an exception gets raised
 # on the first request of a main.py instance, so don't add parent_dir multiple
 # times.
 parent_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
 if parent_dir not in sys.path:
-    sys.path = [parent_dir] + sys.path
+    sys.path.insert(0, parent_dir)
 
 # Remove the standard version of Django
 for k in [k for k in sys.modules if k.startswith('django')]:
     del sys.modules[k]
 
-from djangoappengine import aecmd
-aecmd.setup_threading()
-aecmd.setup_project()
-aecmd.setup_logging()
+from djangoappengine import boot
+boot.setup_threading()
+boot.setup_project()
+boot.setup_logging()
 
 import django.core.handlers.wsgi
 from google.appengine.ext.webapp import util
         sys.path = path_backup[:]
     except:
         path_backup = sys.path[:]
-    os.environ.update(aecmd.env_ext)
-    aecmd.setup_logging()
+    os.environ.update(boot.env_ext)
+    boot.setup_logging()
 
     # Create a Django application for WSGI.
     application = django.core.handlers.wsgi.WSGIHandler()

File management/commands/runserver.py

 
 
 def start_dev_appserver(argv):
-  """Starts the appengine dev_appserver program for the Django project.
+  """Starts the App Engine 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.
     """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
+    fires up a copy of the full fledged App Engine dev_appserver that emulates
     the live environment your application will be deployed to.
     """
-    help = 'Runs a copy of the appengine development server.'
+    help = 'Runs a copy of the App Engine development server.'
     args = '[optional port number, or ipaddr:port]'
 
     def run_from_argv(self, argv):

File management/commands/testserver.py

     """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.
+    up a copy of the full fledged App Engine dev_appserver.
 
     The appserver is always initialised with a blank datastore with the specified
     fixtures loaded into it.

File settings_base.py

+from djangoappengine.utils import on_production_server, have_appserver
+
+DEBUG = not on_production_server
+TEMPLATE_DEBUG = DEBUG
+
+ROOT_URLCONF = 'urls'
+
+DATABASES = {
+    'default': {
+        'ENGINE': 'djangoappengine.db',
+        'NAME': '',
+        'USER': '',
+        'PASSWORD': '',
+        'HOST': '',
+        'PORT': '',
+        'SUPPORTS_TRANSACTIONS': False,
+    },
+}
+
+EMAIL_BACKEND = 'djangoappengine.email'
+
+FILE_UPLOAD_MAX_MEMORY_SIZE = 1024 * 1024
+FILE_UPLOAD_HANDLERS = (
+    'django.core.files.uploadhandler.MemoryFileUploadHandler',
+)
+
+CACHE_BACKEND = 'memcached://?timeout=0'
+SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'
+
+if not on_production_server:
+    INTERNAL_IPS = ('127.0.0.1',)

File settings_post.py

-# -*- coding: utf-8 -*-
-from settings import *
-import sys
-
-if '%d' in MEDIA_URL:
-    MEDIA_URL = MEDIA_URL % MEDIA_VERSION
-if '%s' in ADMIN_MEDIA_PREFIX:
-    ADMIN_MEDIA_PREFIX = ADMIN_MEDIA_PREFIX % MEDIA_URL
-
-TEMPLATE_DEBUG = DEBUG
-MANAGERS = ADMINS
-
-# You can override Django's or some apps' locales with these folders:
-if os.path.exists(os.path.join(COMMON_DIR, 'locale_overrides_common')):
-    INSTALLED_APPS += ('locale_overrides_common',)
-if os.path.exists(os.path.join(PROJECT_DIR, 'locale_overrides')):
-    INSTALLED_APPS += ('locale_overrides',)
-
-## Add admin interface media files if necessary
-#if 'django.contrib.admin' in INSTALLED_APPS:
-#    INSTALLED_APPS += ('django_export.admin_media',)
-#
-## Always add Django templates (exported from zip)
-#INSTALLED_APPS += (
-#    'django_export.django_templates',
-#)
-
-if have_appserver or on_production_server:
-    check_app_imports = None
-else:
-    def check_app_imports(app):
-        before = sys.modules.keys()
-        __import__(app, {}, {}, [''])
-        after = sys.modules.keys()
-        added = [key[len(app)+1:] for key in after if key not in before and
-                 key.startswith(app + '.') and key[len(app)+1:]]
-        if added:
-            import logging
-            logging.warn('The app "%(app)s" contains imports in '
-                         'its __init__.py (at least %(added)s). This can cause '
-                         'strange bugs due to recursive imports! You should '
-                         'either do the import lazily (within functions) or '
-                         'ignore the app settings/urlsauto with '
-                         'IGNORE_APP_SETTINGS and IGNORE_APP_URLSAUTO in '
-                         'your settings.py.'
-                         % {'app': app, 'added': ', '.join(added)})
-
-# Import app-specific settings
-_globals = globals()
-class _Module(object):
-    def __setattr__(self, key, value):
-        _globals[key] = value
-    def __getattribute__(self, key):
-        return _globals[key]
-    def __hasattr__(self, key):
-        return key in _globals
-settings = _Module()
-
-for app in INSTALLED_APPS:
-    # This is an optimization. Django's apps don't have special settings.
-    # Also, allow for ignoring some apps' settings.
-    if app.startswith('django.') or app.endswith('.*') or \
-            app == 'djangoappengine' or app in IGNORE_APP_SETTINGS:
-        continue
-    try:
-        # First we check if __init__.py doesn't import anything
-        if check_app_imports:
-            check_app_imports(app)
-        __import__(app + '.settings', {}, {}, [''])
-    except ImportError:
-        pass
-
-try:
-    from settings_overrides import *
-except ImportError:
-    pass

File settings_pre.py

-# -*- coding: utf-8 -*-
-from djangoappengine.utils import on_production_server, have_appserver
-import os
-DEBUG = not on_production_server
-
-# The MEDIA_VERSION will get integrated via %d
-MEDIA_URL = '/media/'
-# The MEDIA_URL will get integrated via %s
-ADMIN_MEDIA_PREFIX = '%sadmin_media/'
-
-ADMINS = ()
-
-DATABASES = {
-    'default': {
-        'ENGINE': 'djangoappengine.db',
-        'NAME': '',
-        'USER': '',
-        'PASSWORD': '',
-        'HOST': '',
-        'PORT': '',
-        'SUPPORTS_TRANSACTIONS': False,
-    },
-}
-
-# If you set this to False, Django will make some optimizations so as not
-# to load the internationalization machinery.
-USE_I18N = True
-
-EMAIL_HOST = 'localhost'
-EMAIL_PORT = 25
-EMAIL_HOST_USER = 'user'
-EMAIL_HOST_PASSWORD = 'password'
-EMAIL_USE_TLS = True
-DEFAULT_FROM_EMAIL = 'user@localhost'
-SERVER_EMAIL = 'user@localhost'
-
-ROOT_URLCONF = 'urls'
-
-TEMPLATE_LOADERS = (
-    'django.template.loaders.filesystem.load_template_source',
-    'django.template.loaders.app_directories.load_template_source',
-)
-
-COMMON_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
-PROJECT_DIR = os.path.dirname(COMMON_DIR)
-MAIN_DIRS = (PROJECT_DIR, COMMON_DIR)
-
-TEMPLATE_DIRS = tuple([os.path.join(dir, 'templates') for dir in MAIN_DIRS])
-
-LOCALE_PATHS = (
-    os.path.join(PROJECT_DIR, 'media', 'locale'),
-) + tuple([os.path.join(dir, 'locale') for dir in TEMPLATE_DIRS])
-
-FILE_UPLOAD_HANDLERS = (
-    'django.core.files.uploadhandler.MemoryFileUploadHandler',
-)
-
-CACHE_BACKEND = 'memcached://?timeout=0'
-SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'
-
-if not on_production_server:
-    INTERNAL_IPS = ('127.0.0.1',)
-
-IGNORE_APP_SETTINGS = ()

File tests/__init__.py

+from .field_db_conversion import FieldDBConversionTest
+from .field_options import FieldOptionsTest
+from .filter import FilterTest
+from .order import OrderTest
+from .transaction import TxTest
+from .not_return_sets import NonReturnSetsTest

File tests/field_db_conversion.py

+from .testmodels import FieldsWithoutOptionsModel
+from django.test import TestCase
+from google.appengine.api.datastore import Get
+from google.appengine.ext.db import Key
+from google.appengine.api.datastore_types import Text, Category, Email, Link, \
+    PhoneNumber, PostalAddress, Text, Blob, ByteString, GeoPt, IM, Key, \
+    Rating, BlobKey
+from google.appengine.api import users
+import datetime
+
+class FieldDBConversionTest(TestCase):
+    def test_db_conversion(self):
+        actual_datetime = datetime.datetime.now()
+        entity = FieldsWithoutOptionsModel(
+            datetime=actual_datetime, date=actual_datetime.date(),
+            time=actual_datetime.time(), floating_point=5.97, boolean=True,
+            null_boolean=False, text='Hallo', email='hallo@hallo.com',
+            ip_address='194.167.1.1', slug='you slugy slut :)',
+            url='http://www.scholardocs.com', long_text=1000*'A', xml=2000*'B',
+            integer=-400, small_integer=-4, positiv_integer=400,
+            positiv_small_integer=4)
+        entity.save()
+
+        # get the gae entity (not the model instance) and test if the fields
+        # have been converted right to the corresponding gae database types
+        gae_entity = Get(Key.from_path(FieldsWithoutOptionsModel._meta.db_table,
+            entity.pk))
+
+        for name, gae_db_type in [('long_text', Text), ('xml', Text),
+                ('text', unicode), ('ip_address', unicode), ('slug', unicode),
+                ('email', unicode), ('url', unicode), ('time', datetime.datetime),
+                ('datetime', datetime.datetime), ('date', datetime.datetime),
+                ('floating_point', float), ('boolean', bool),
+                ('null_boolean', bool), ('integer', (int, long)),
+                ('small_integer', (int, long)), ('positiv_integer', (int, long)),
+                ('positiv_small_integer', (int, long))] :
+            self.assertTrue(type(gae_entity[
+                FieldsWithoutOptionsModel._meta.get_field_by_name(
+                    name)[0].column]) in (isinstance(gae_db_type, (list, tuple)) and \
+                        gae_db_type or (gae_db_type, )))
+
+        # get the model instance and check if the fields convert back to the
+        # right types
+        entity = FieldsWithoutOptionsModel.objects.get()
+        for name, expected_type in [('long_text', unicode), ('xml', unicode),
+                ('text', unicode), ('ip_address', unicode), ('slug', unicode),
+                ('email', unicode), ('url', unicode), ('datetime', datetime.datetime),
+                ('date', datetime.date), ('time', datetime.time),
+                ('floating_point', float), ('boolean', bool),
+                ('null_boolean', bool), ('integer', (int, long)),
+                ('small_integer', (int, long)), ('positiv_integer', (int, long)),
+                ('positiv_small_integer', (int, long))]:
+            self.assertTrue(type(getattr(entity, name)) in (isinstance(
+                expected_type, (list, tuple)) and expected_type or (expected_type, )))
+
+
+# TODO: Add field conversions for ForeignKeys?

File tests/field_options.py

+from django.test import TestCase
+from django.db.models.fields import NOT_PROVIDED
+from .testmodels import FieldsWithOptionsModel
+from google.appengine.api.datastore import Get
+from google.appengine.ext.db import Key
+from google.appengine.api.datastore_types import Text, Category, Email, Link, \
+    PhoneNumber, PostalAddress, Text, Blob, ByteString, GeoPt, IM, Key, \
+    Rating, BlobKey
+from google.appengine.api import users
+import datetime
+
+class FieldOptionsTest(TestCase):
+    def test_options(self):
+        entity = FieldsWithOptionsModel()
+        # try to save the entity with non-nullable field time set to None, should
+        # raise an exception
+        self.assertRaises(ValueError, entity.save)
+
+        time = datetime.datetime.now().time()
+        entity.time = time
+        try:
+            entity.save()
+        except:
+            self.fail()
+
+        # check if primary_key=True is set correct for the saved entity
+        self.assertEquals(entity.pk, u'app-engine@scholardocs.com')
+        gae_entity = Get(Key.from_path(FieldsWithOptionsModel._meta.db_table,
+            entity.pk))
+        self.assertTrue(gae_entity is not None)
+        self.assertEquals(gae_entity.key().name(), u'app-engine@scholardocs.com')
+        
+        # check if default values are set correct on the db level, primary_key field
+        # is not stored at the db level
+        for field in FieldsWithOptionsModel._meta.local_fields:
+            if field.default and field.default != NOT_PROVIDED and not \
+                    field.primary_key:
+                self.assertEquals(gae_entity[field.column], field.default)
+            elif field.column == 'time':
+                self.assertEquals(gae_entity[field.column], datetime.datetime(
+                    1970, 1, 1, time.hour, time.minute, time.second, time.microsecond))
+            elif field.null:
+                self.assertEquals(gae_entity[field.column], None)
+
+        # check if default values are set correct on the model instance level
+        entity = FieldsWithOptionsModel.objects.get()
+        for field in FieldsWithOptionsModel._meta.local_fields:
+            if field.default and field.default != NOT_PROVIDED:
+                self.assertEquals(getattr(entity, field.column), field.default)
+            elif field.column == 'time':
+                self.assertEquals(getattr(entity, field.column), time)
+            elif field.null:
+                self.assertEquals(getattr(entity, field.column), None)
+
+        # check if nullable field with default values can be set to None
+        entity.slug = None
+        entity.positiv_small_integer = None
+        try:
+            entity.save()
+        except:
+            self.fail()
+
+        # check if slug and positiv_small_integer will be retrieved with values
+        # set to None (on db level and model instance level)
+        gae_entity = Get(Key.from_path(FieldsWithOptionsModel._meta.db_table,
+            entity.pk))
+        self.assertEquals(gae_entity[FieldsWithOptionsModel._meta.get_field_by_name(
+            'slug')[0].column], None)
+        self.assertEquals(gae_entity[FieldsWithOptionsModel._meta.get_field_by_name(
+            'positiv_small_integer')[0].column], None)
+
+        # on the model instance level
+        entity = FieldsWithOptionsModel.objects.get()
+        self.assertEquals(getattr(entity, FieldsWithOptionsModel._meta.get_field_by_name(
+            'slug')[0].column), None)
+        self.assertEquals(getattr(entity, FieldsWithOptionsModel._meta.get_field_by_name(
+            'positiv_small_integer')[0].column), None)
+
+        # TODO: check db_column option
+        # TODO: change the primary key and check if a new instance with the
+        # changed primary key will be saved (not in this test class)
+        

File tests/filter.py

+from .testmodels import FieldsWithOptionsModel, OrderedModel
+import datetime
+from django.test import TestCase
+from django.db.models import Q
+from google.appengine.api.datastore_errors import BadArgumentError, BadFilterError
+
+class FilterTest(TestCase):
+    floats = [5.3, 2.6, 9.1, 1.58]
+    emails = ['app-engine@scholardocs.com', 'sharingan@uchias.com',
+        'rinnengan@sage.de', 'rasengan@naruto.com']
+
+    def setUp(self):
+        for index, (float, email) in enumerate(zip(FilterTest.floats,
+                FilterTest.emails)):
+            self.last_save_time = datetime.datetime.now().time()
+            ordered_instance = OrderedModel(priority=index, pk=index + 1)
+            ordered_instance.save()
+            model = FieldsWithOptionsModel(floating_point=float,
+                                           integer=int(float), email=email,
+                                           time=self.last_save_time,
+                                           foreign_key=ordered_instance)
+            model.save()
+
+    def test_gt(self):
+        # test gt on float
+        self.assertEquals([entity.floating_point for entity in \
+                          FieldsWithOptionsModel.objects.filter(
+                          floating_point__gt=3.1).order_by('floating_point')],
+                          [5.3, 9.1])
+
+        # test gt on integer
+        self.assertEquals([entity.integer for entity in \
+                          FieldsWithOptionsModel.objects.filter(
+                          integer__gt=3).order_by('integer')],
+                          [5, 9])
+
+        # test filter on primary_key field
+        self.assertEquals([entity.email for entity in \
+                          FieldsWithOptionsModel.objects.filter(email__gt='as').
+                          order_by('email')], ['rasengan@naruto.com',
+                          'rinnengan@sage.de', 'sharingan@uchias.com', ])
+
+        # test ForeignKeys with id
+        self.assertEquals(sorted([entity.email for entity in \
+                            FieldsWithOptionsModel.objects.filter(
+                            foreign_key__gt=2)]),
+                            ['rasengan@naruto.com', 'rinnengan@sage.de', ])
+
+        # and with instance
+        ordered_instance = OrderedModel.objects.get(priority=1)
+        self.assertEquals(sorted([entity.email for entity in \
+                            FieldsWithOptionsModel.objects.filter(
+                            foreign_key__gt=ordered_instance)]),
+                            ['rasengan@naruto.com', 'rinnengan@sage.de', ])
+
+
+    def test_lt(self):
+        # test lt on float
+        self.assertEquals([entity.floating_point for entity in \
+                          FieldsWithOptionsModel.objects.filter(
+                          floating_point__lt=3.1).order_by('floating_point')],
+                          [1.58, 2.6])
+
+        # test lt on integer
+        self.assertEquals([entity.integer for entity in \
+                          FieldsWithOptionsModel.objects.filter(
+                          integer__lt=3).order_by('integer')],
+                          [1, 2])
+
+        # test filter on primary_key field
+        self.assertEquals([entity.email for entity in \
+                          FieldsWithOptionsModel.objects.filter(email__lt='as').
+                          order_by('email')], ['app-engine@scholardocs.com', ])
+
+         # filter on datetime
+        self.assertEquals([entity.email for entity in \
+                            FieldsWithOptionsModel.objects.filter(
+                            time__lt=self.last_save_time).order_by('time')],
+                            ['app-engine@scholardocs.com', 'sharingan@uchias.com',
+                            'rinnengan@sage.de',])
+
+        # test ForeignKeys with id
+        self.assertEquals(sorted([entity.email for entity in \
+                            FieldsWithOptionsModel.objects.filter(
+                            foreign_key__lt=3)]),
+                            ['app-engine@scholardocs.com', 'sharingan@uchias.com'])
+
+        # and with instance
+        ordered_instance = OrderedModel.objects.get(priority=2)
+        self.assertEquals(sorted([entity.email for entity in \
+                            FieldsWithOptionsModel.objects.filter(
+                            foreign_key__lt=ordered_instance)]),
+                            ['app-engine@scholardocs.com', 'sharingan@uchias.com'])
+
+
+    def test_gte(self):
+        # test gte on float
+        self.assertEquals([entity.floating_point for entity in \
+                          FieldsWithOptionsModel.objects.filter(
+                          floating_point__gte=2.6).order_by('floating_point')],
+                          [2.6, 5.3, 9.1])
+
+        # test gte on integer
+        self.assertEquals([entity.integer for entity in \
+                          FieldsWithOptionsModel.objects.filter(
+                          integer__gte=2).order_by('integer')],
+                          [2, 5, 9])
+
+        # test filter on primary_key field
+        self.assertEquals([entity.email for entity in \
+                          FieldsWithOptionsModel.objects.filter(
+                          email__gte='rinnengan@sage.de').order_by('email')],
+                          ['rinnengan@sage.de', 'sharingan@uchias.com', ])
+
+    def test_lte(self):
+        # test lte on float
+        self.assertEquals([entity.floating_point for entity in \
+                          FieldsWithOptionsModel.objects.filter(
+                          floating_point__lte=5.3).order_by('floating_point')],
+                          [1.58, 2.6, 5.3])
+
+        # test lte on integer
+        self.assertEquals([entity.integer for entity in \
+                          FieldsWithOptionsModel.objects.filter(
+                          integer__lte=5).order_by('integer')],
+                          [1, 2, 5])
+
+        # test filter on primary_key field
+        self.assertEquals([entity.email for entity in \
+                          FieldsWithOptionsModel.objects.filter(
+                          email__lte='rinnengan@sage.de').order_by('email')],
+                          ['app-engine@scholardocs.com', 'rasengan@naruto.com',
+                          'rinnengan@sage.de'])
+
+    def test_equals(self):
+        # test equality filter on primary_key field
+        self.assertEquals([entity.email for entity in \
+                          FieldsWithOptionsModel.objects.filter(
+                          email='rinnengan@sage.de').order_by('email')],
+                          ['rinnengan@sage.de'])
+
+        # test using exact
+        self.assertEquals(FieldsWithOptionsModel.objects.filter(
+                          email__exact='rinnengan@sage.de')[0].email,
+                          'rinnengan@sage.de')
+
+        self.assertEquals(FieldsWithOptionsModel.objects.filter(
+                           pk='app-engine@scholardocs.com')[0].email,
+                          'app-engine@scholardocs.com')
+
+    def test_is_null(self):
+        self.assertEquals(FieldsWithOptionsModel.objects.filter(
+            floating_point__isnull=True).count(), 0)
+
+        FieldsWithOptionsModel(integer=5.4, email='shinra.tensai@sixpaths.com',
+            time=datetime.datetime.now().time()).save()
+
+        self.assertEquals(FieldsWithOptionsModel.objects.filter(
+            floating_point__isnull=True).count(), 1)
+
+
+    def test_exclude(self):
+        self.assertEquals([entity.email for entity in \
+                            FieldsWithOptionsModel.objects.all().exclude(
+                            floating_point__lt=9.1).order_by('floating_point')],
+                            ['rinnengan@sage.de', ])
+
+        # test exclude with foreignKey
+        ordered_instance = OrderedModel.objects.get(priority=1)
+        self.assertEquals(sorted([entity.email for entity in \
+                            FieldsWithOptionsModel.objects.all().exclude(
+                            foreign_key__gt=ordered_instance)]),
+                            ['app-engine@scholardocs.com', 'sharingan@uchias.com',])
+
+
+    def test_chained_filter(self):
+        # additionally tests count :)
+        self.assertEquals(FieldsWithOptionsModel.objects.filter(
+                          floating_point__lt=5.3).filter(floating_point__gt=2.6).
+                          count(), 0)
+
+        # test across multiple columns. On app engine only one filter is allowed
+        # to be an inequality filter
+        self.assertEquals([(entity.floating_point, entity.integer) for entity in \
+                          FieldsWithOptionsModel.objects.filter(
+                          floating_point__lte=5.3).filter(integer=2).order_by(
+                          'floating_point')], [(2.6, 2), ])
+
+        # test multiple filters including the primary_key field
+        self.assertEquals([entity.email for entity in \
+                          FieldsWithOptionsModel.objects.filter(
+                          email__gte='rinnengan@sage.de').filter(integer=2).order_by(
+                          'email')], ['sharingan@uchias.com', ])
+
+        # test in filter on primary key with another arbitrary filter
+        self.assertEquals([entity.email for entity in \
+                          FieldsWithOptionsModel.objects.filter(
+                          email__in=['rinnengan@sage.de',
+                          'sharingan@uchias.com']).filter(integer__gt=2).order_by(
+                          'integer')], ['rinnengan@sage.de', ])
+
+        # Test exceptions
+
+        # test multiple filters exception when filtered and not ordered against
+        # the first filter
+        self.assertRaises(BadArgumentError, FieldsWithOptionsModel.objects.filter(
+                email__gte='rinnengan@sage.de').filter(floating_point=5.3).order_by(
+                'floating_point').count)
+
+        # test exception if filtered across multiple columns with inequality filter
+        self.assertRaises(BadFilterError, FieldsWithOptionsModel.objects.filter(
+                          floating_point__lte=5.3).filter(integer__gte=2).order_by(
+                          'floating_point').get)
+
+        # test exception if filtered across multiple columns with inequality filter
+        # with exclude
+        self.assertRaises(BadFilterError, FieldsWithOptionsModel.objects.filter(
+                            email__lte='rinnengan@sage.de').exclude(
+                            floating_point__lt=9.1).order_by('email').get)
+
+        self.assertRaises(BadArgumentError, FieldsWithOptionsModel.objects.all().exclude(
+                            floating_point__lt=9.1).order_by('email').count)
+
+        # test exception on inequality filter.
+        # TODO: support them for appengine via <>
+        self.assertRaises(TypeError, FieldsWithOptionsModel.objects.exclude(
+                            floating_point=9.1).order_by('floating_point').get)
+
+        # TODO: Maybe check all possible exceptions
+
+    def test_slicing(self):
+        # test slicing on filter with primary_key
+        self.assertEquals([entity.email for entity in \
+                          FieldsWithOptionsModel.objects.filter(
+                          email__lte='rinnengan@sage.de').order_by('email')[:2]],
+                          ['app-engine@scholardocs.com', 'rasengan@naruto.com', ])
+
+        self.assertEquals([entity.email for entity in \
+                          FieldsWithOptionsModel.objects.filter(
+                          email__lte='rinnengan@sage.de').order_by('email')[1:2]],
+                          ['rasengan@naruto.com', ])
+
+        # test on non pk field
+        self.assertEquals([entity.integer for entity in \
+                          FieldsWithOptionsModel.objects.all().order_by(
+                          'integer')[:2]], [1, 2, ])
+
+        self.assertEquals([entity.email for entity in \
+                          FieldsWithOptionsModel.objects.all().order_by(
+                            'email')[::2]],
+                          ['app-engine@scholardocs.com', 'rinnengan@sage.de',])
+
+    def test_Q_objects(self):
+        self.assertEquals([entity.email for entity in \
+                          FieldsWithOptionsModel.objects.filter(
+                            Q(email__lte='rinnengan@sage.de')).order_by('email')][:2],
+                          ['app-engine@scholardocs.com', 'rasengan@naruto.com', ])
+
+        self.assertEquals([entity.integer for entity in \
+                          FieldsWithOptionsModel.objects.exclude(Q(integer__lt=5) |
+                            Q(integer__gte=9)).order_by('integer')],
+                            [5, ])
+
+        self.assertRaises(TypeError, FieldsWithOptionsModel.objects.filter(
+            Q(floating_point=9.1), Q(integer=9) | Q(integer=2)))
+
+    def test_pk_in(self):
+        # test pk__in with field name email
+        self.assertEquals([entity.email for entity in
+                            FieldsWithOptionsModel.objects.filter(
+                            email__in=['app-engine@scholardocs.com',
+                            'rasengan@naruto.com'])], ['app-engine@scholardocs.com',
+                            'rasengan@naruto.com'])

File tests/not_return_sets.py

+from .testmodels import FieldsWithOptionsModel, OrderedModel
+import datetime
+from django.test import TestCase
+from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
+
+
+class NonReturnSetsTest(TestCase):
+    floats = [5.3, 2.6, 9.1, 1.58, 2.4]
+    emails = ['app-engine@scholardocs.com', 'sharingan@uchias.com',
+        'rinnengan@sage.de', 'rasengan@naruto.com', 'itachi@uchia.com']
+
+    def setUp(self):
+        for index, (float, email) in enumerate(zip(NonReturnSetsTest.floats,
+                NonReturnSetsTest.emails)):
+            self.last_save_time = datetime.datetime.now().time()
+            ordered_instance = OrderedModel(priority=index, pk=index + 1)
+            ordered_instance.save()
+            model = FieldsWithOptionsModel(floating_point=float,
+                                           integer=int(float), email=email,
+                                           time=self.last_save_time,
+                                           foreign_key=ordered_instance)
+            model.save()
+
+    def test_get(self):
+        self.assertEquals(FieldsWithOptionsModel.objects.get(
+                                                email='itachi@uchia.com')
+                                                .email, 'itachi@uchia.com')
+
+        # test exception when matching multiple entities
+        self.assertRaises(MultipleObjectsReturned, FieldsWithOptionsModel.objects
+                            .get, integer=2)
+
+        # test exception when entity does not exist
+        self.assertRaises(ObjectDoesNotExist, FieldsWithOptionsModel.objects
+                            .get, floating_point=5.2)
+
+        # TODO: test create when djangos model.save_base is refactored
+        # TODO: test get_or_create when refactored
+
+    def test_count(self):
+        self.assertEquals(FieldsWithOptionsModel.objects.filter(
+            integer=2).count(), 2)
+
+    def test_in_bulk(self):
+        self.assertEquals([key in ['sharingan@uchias.com', 'itachi@uchia.com']
+                        for key in FieldsWithOptionsModel.objects.in_bulk(
+                                ['sharingan@uchias.com', 'itachi@uchia.com']).keys()],
+                                [True, ]*2)
+
+    def test_latest(self):
+        self.assertEquals('itachi@uchia.com', FieldsWithOptionsModel.objects
+            .latest('time').email)
+
+    def test_exists(self):
+        self.assertEquals(True, FieldsWithOptionsModel.objects.exists())
+
+    def test_deletion(self):
+        # TODO: ForeignKeys will not be deleted! This has to be done via
+        # background tasks
+        self.assertEquals(FieldsWithOptionsModel.objects.count(), 5)
+
+        FieldsWithOptionsModel.objects.get(email='itachi@uchia.com').delete()
+        self.assertEquals(FieldsWithOptionsModel.objects.count(), 4)
+
+        FieldsWithOptionsModel.objects.filter(email__in=['sharingan@uchias.com',
+            'itachi@uchia.com', 'rasengan@naruto.com', ]).delete()
+        self.assertEquals(FieldsWithOptionsModel.objects.count(), 2)
+
+    def test_foreignKey_fetch(self):
+        # test fetching the ForeignKey
+        ordered_instance = OrderedModel.objects.get(priority=2)
+        self.assertEquals(FieldsWithOptionsModel.objects.get(integer=9).foreign_key,
+                            ordered_instance)
+
+    def test_foreignKey_backward(self):
+        entity = OrderedModel.objects.all()[0]
+        self.assertEquals(entity.keys.count(), 1)
+        # TODO: add should save the added instance transactional via for example
+        # force_insert
+        new_foreignKey = FieldsWithOptionsModel(floating_point=5.6, integer=3,
+            email='temp@temp.com', time=datetime.datetime.now())
+        entity.keys.add(new_foreignKey)
+        self.assertEquals(entity.keys.count(), 2)
+        # TODO: add test for create
+        entity.keys.remove(new_foreignKey)
+        self.assertEquals(entity.keys.count(), 1)
+        entity.keys.clear()
+        self.assertTrue(not entity.keys.exists())
+        entity.keys = [new_foreignKey, new_foreignKey]
+        self.assertEquals(entity.keys.count(), 1)

File tests/order.py

+from .testmodels import OrderedModel
+from django.test import TestCase
+
+class OrderTest(TestCase):
+    def create_ordered_model_items(self):
+        pks = []
+        priorities = [5, 2, 9, 1]
+        for pk, priority in enumerate(priorities):
+            pk += 1
+            model = OrderedModel(pk=pk, priority=priority)
+            model.save()
+            pks.append(model.pk)
+        return pks, priorities
+
+    def test_default_order(self):
+        pks, priorities = self.create_ordered_model_items()
+        self.assertEquals([item.priority
+                           for item in OrderedModel.objects.all()],
+                          sorted(priorities, reverse=True))
+
+    def test_override_default_order(self):
+        pks, priorities = self.create_ordered_model_items()
+        self.assertEquals([item.priority
+                           for item in OrderedModel.objects.all().order_by('priority')],
+                          sorted(priorities))
+
+    def test_remove_default_order(self):
+        pks, priorities = self.create_ordered_model_items()
+        self.assertEquals([item.pk
+                           for item in OrderedModel.objects.all().order_by()],
+                          sorted(pks))
+
+    def test_order_with_pk_filter(self):
+        pks, priorities = self.create_ordered_model_items()
+        self.assertEquals([item.priority
+                           for item in OrderedModel.objects.filter(pk__in=pks)],
+                          sorted(priorities, reverse=True))
+
+        # test with id__in
+        self.assertEquals([item.priority
+                           for item in OrderedModel.objects.filter(id__in=pks)],
+                          sorted(priorities, reverse=True))
+
+        # test reverse
+        self.assertEquals([item.priority
+                           for item in OrderedModel.objects.filter(
+                           pk__in=pks).reverse()], sorted(priorities,
+                           reverse=False))
+
+    def test_remove_default_order_with_pk_filter(self):
+        pks, priorities = self.create_ordered_model_items()
+        self.assertEquals([item.priority
+                           for item in OrderedModel.objects.filter(pk__in=pks).order_by()],
+                          priorities)
+
+    # TODO: test multiple orders
+

File tests/testmodels.py

+from django.db import models
+
+class FieldsWithoutOptionsModel(models.Model):
+    datetime = models.DateTimeField()
+    date = models.DateField()
+    time = models.TimeField()
+    floating_point = models.FloatField()
+    boolean = models.BooleanField()
+    null_boolean = models.NullBooleanField()
+    text = models.CharField(max_length=3)
+    email = models.EmailField()
+#    comma_seperated_integer = models.CommaSeparatedIntegerField()
+    ip_address = models.IPAddressField()
+    slug = models.SlugField()
+    url = models.URLField()
+#    file = models.FileField()
+#    file_path = models.FilePathField()
+    long_text = models.TextField()
+    xml = models.XMLField()
+    integer = models.IntegerField()
+    small_integer = models.SmallIntegerField()
+    positiv_integer = models.PositiveIntegerField()
+    positiv_small_integer = models.PositiveSmallIntegerField()
+#    foreign_key = models.ForeignKey('FieldsWithOptionsModel')
+#    foreign_key = models.ForeignKey('OrderedModel')
+#    one_to_one = models.OneToOneField()
+#    decimal = models.DecimalField() # can be None
+#    image = models.ImageField()
+
+class FieldsWithOptionsModel(models.Model):
+    # any type of unique (unique_data, ...) is not supported on GAE, instead you
+    # can use primary_key=True for some special cases. But be carefull: changing
+    # the  primary_key of an entity will not result in an updated entity,
+    # instead a new entity will be putted into the datastore. The old one will
+    # not be deleted and all references pointing to the old entitiy will not
+    # point to the new one either
+    datetime = models.DateTimeField(auto_now=True, db_column="birthday")
+    date = models.DateField(auto_now_add=True)
+    time = models.TimeField()
+    floating_point = models.FloatField(null=True)
+    boolean = models.BooleanField() # default is False
+    null_boolean = models.NullBooleanField(default=True)
+    text = models.CharField(default='Hallo', max_length=10)
+    email = models.EmailField(default='app-engine@scholardocs.com', primary_key=True)
+#    comma_seperated_integer = models.CommaSeparatedIntegerField()
+    ip_address = models.IPAddressField(default="192.168.0.2")
+    slug = models.SlugField(default="GAGAA", null=True)
+    url = models.URLField(default='http://www.scholardocs.com')
+#    file = FileField()
+#    file_path = FilePathField()
+    long_text = models.TextField(default=1000*'A')
+    xml = models.XMLField(default=2000*'B')
+    integer = models.IntegerField(default=100)
+    small_integer = models.SmallIntegerField(default=-5)
+    positiv_integer = models.PositiveIntegerField(default=80)
+    positiv_small_integer = models.PositiveSmallIntegerField(default=3, null=True)
+    foreign_key = models.ForeignKey('OrderedModel', null=True, related_name='keys')
+#    one_to_one = OneToOneField()
+#    decimal = DecimalField()
+#    image = ImageField()
+
+class OrderedModel(models.Model):
+    priority = models.IntegerField()
+
+    class Meta:
+        ordering = ('-priority',)

File tests/transaction.py

+from .testmodels import FieldsWithOptionsModel
+from datetime import datetime
+from django.db.transaction import commit_locked
+from django.test import TestCase
+
+class TxTest(TestCase):
+    def test_tx(self):
+        item = FieldsWithOptionsModel(time=datetime.now().time())
+        item.save()
+        self.run_tx(item.pk)
+        item = FieldsWithOptionsModel.objects.get(pk=item.pk)
+        self.assertEquals(item.text, 'Wooooo!')
+        self.assertRaises(Exception, self.run_nested_tx, [item.pk])
+
+    @commit_locked
+    def run_tx(self, pk):
+        item = FieldsWithOptionsModel.objects.get(pk=pk)
+        item.text = 'Wooooo!'
+        item.save()
+
+    @commit_locked
+    def run_nested_tx(self, pk):
+        self.run_tx(pk)
 else:
     try:
         from google.appengine.tools import dev_appserver
-        from aecmd import PROJECT_DIR
+        from .boot import PROJECT_DIR
         appconfig, unused = dev_appserver.LoadAppConfig(PROJECT_DIR, {})
         appid = appconfig.application
-    except ImportError:
-        appid = None
+    except ImportError, e:
+        raise Exception('Could not get appid. Is your app.yaml file missing? '
+                        'Error was: %s' % e)
 
 on_production_server = have_appserver and \
     not os.environ.get('SERVER_SOFTWARE', '').lower().startswith('devel')