summaryrefslogtreecommitdiffstats
path: root/appengine_django/models.py
diff options
context:
space:
mode:
Diffstat (limited to 'appengine_django/models.py')
-rwxr-xr-xappengine_django/models.py182
1 files changed, 182 insertions, 0 deletions
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