Source

djangobook-cn / common / appenginepatch / appenginepatcher / patch.py

Full commit
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
# -*- coding: utf-8 -*-
# Unfortunately, we have to fix a few App Engine bugs here because otherwise
# not all of our features will work. Still, we should keep the number of bug
# fixes to a minimum and report everything to Google, please:
# http://code.google.com/p/googleappengine/issues/list

from google.appengine.ext import db
from google.appengine.ext.db import polymodel
import logging, new, os, re, sys

base_path = os.path.abspath(os.path.dirname(__file__))

get_verbose_name = lambda class_name: re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', class_name).lower().strip()

DEFAULT_NAMES = ('verbose_name', 'ordering', 'permissions', 'app_label',
                 'abstract', 'db_table', 'db_tablespace')

def patch_all():
    patch_python()
    patch_app_engine()
    patch_django()
    setup_logging()

def patch_python():
    # Remove modules that we want to override
    for module in ('memcache',):
        if module in sys.modules:
            del sys.modules[module]

    # For some reason the imp module can't be replaced via sys.path
    from appenginepatcher import have_appserver
    if have_appserver:
        from appenginepatcher import imp
        sys.modules['imp'] = imp

    if have_appserver:
        def unlink(_):
            raise NotImplementedError('App Engine does not support FS writes!')
        os.unlink = unlink

def patch_app_engine():
    # This allows for using Paginator on a Query object. We limit the number
    # of results to 301, so there won't be any timeouts (301, so you can say
    # "more than 300 results").
    def __len__(self):
        return self.count(301)
    db.Query.__len__ = __len__

    # Add "model" property to Query (needed by generic views)
    class ModelProperty(object):
        def __get__(self, query, unused):
            try:
                return query._Query__model_class
            except:
                return query._model_class
    db.Query.model = ModelProperty()

    # Add a few Model methods that are needed for serialization and ModelForm
    def _get_pk_val(self):
        if self.has_key():
            return unicode(self.key())
        else:
            return None
    db.Model._get_pk_val = _get_pk_val
    def __eq__(self, other):
        if not isinstance(other, self.__class__):
            return False
        return self._get_pk_val() == other._get_pk_val()
    db.Model.__eq__ = __eq__
    def __ne__(self, other):
        return not self.__eq__(other)
    db.Model.__ne__ = __ne__
    def pk(self):
        return self._get_pk_val()
    db.Model.id = db.Model.pk = property(pk)

    # Make Property more Django-like (needed for serialization and ModelForm)
    db.Property.serialize = True
    db.Property.editable = True
    db.Property.help_text = ''
    def _get_verbose_name(self):
        if not getattr(self, '_verbose_name', None):
            self._verbose_name = self.name.replace('_', ' ')
        return self._verbose_name
    def _set_verbose_name(self, verbose_name):
        self._verbose_name = verbose_name
    db.Property.verbose_name = property(_get_verbose_name, _set_verbose_name)

    def attname(self):
        return self.name
    db.Property.attname = property(attname)

    class Rel(object):
        def __init__(self, property):
            self.field_name = 'key'
            self.property = property
            self.to = property.reference_class
            self.multiple = True
            self.parent_link = False
            self.related_name = getattr(property, 'collection_name', None)

    class RelProperty(object):
        def __get__(self, property, cls):
            if property is None:
                return self
            if not hasattr(property, 'reference_class'):
                return None
            if not hasattr(property, '_rel_cache'):
                property._rel_cache = Rel(property)
            return property._rel_cache
    db.Property.rel = RelProperty()

    def formfield(self, **kwargs):
        return self.get_form_field(**kwargs)
    db.Property.formfield = formfield

    # Add repr to make debugging a little bit easier
    from django.utils.datastructures import SortedDict
    def __repr__(self):
        d = SortedDict()
        if self.has_key() and self.key().name():
            d['key_name'] = self.key().name()
        for field in self._meta.fields:
            try:
                d[field.name] = getattr(self, field.name)
            except:
                d[field.name] = field.get_value_for_datastore(self)
        return u'%s(**%s)' % (self.__class__.__name__, repr(d))
    db.Model.__repr__ = __repr__

    # Add default __str__ and __unicode__ methods
    def __str__(self):
        return unicode(self).encode('utf-8')
    db.Model.__str__ = __str__
    def __unicode__(self):
        return unicode(repr(self))
    db.Model.__unicode__ = __unicode__

    # Replace save() method with one that calls put(), so a monkey-patched
    # put() will also work if someone uses save()
    def save(self):
        self.put()
    db.Model.save = save

    # Add _meta to Model, so porting code becomes easier (generic views,
    # xheaders, and serialization depend on it).
    from django.conf import settings
    from django.utils.encoding import force_unicode, smart_str
    from django.utils.translation import string_concat, get_language, \
        activate, deactivate_all
    class _meta(object):
        many_to_many = ()
        class pk:
            name = 'key'
            attname = 'pk'

        def __init__(self, model, bases):
            try:
                self.app_label = model.__module__.split('.')[-2]
            except IndexError:
                raise ValueError('Django expects models (here: %s.%s) to be defined in their own apps!' % (model.__module__, model.__name__))
            self.parents = [b for b in bases if issubclass(b, db.Model)]
            self.object_name = model.__name__
            self.module_name = self.object_name.lower()
            self.verbose_name = get_verbose_name(self.object_name)
            self.ordering = ()
            self.abstract = model is db.Model
            self.model = model
            self.unique_together = ()
            self.installed = model.__module__.rsplit('.', 1)[0] in \
                             settings.INSTALLED_APPS
            self.permissions = []

            meta = model.__dict__.get('Meta')
            if meta:
                meta_attrs = meta.__dict__.copy()
                for name in meta.__dict__:
                    # Ignore any private attributes that Django doesn't care about.
                    # NOTE: We can't modify a dictionary's contents while looping
                    # over it, so we loop over the *original* dictionary instead.
                    if name.startswith('_'):
                        del meta_attrs[name]
                for attr_name in DEFAULT_NAMES:
                    if attr_name in meta_attrs:
                        setattr(self, attr_name, meta_attrs.pop(attr_name))
                    elif hasattr(meta, attr_name):
                        setattr(self, attr_name, getattr(meta, attr_name))

                # verbose_name_plural is a special case because it uses a 's'
                # by default.
                setattr(self, 'verbose_name_plural', meta_attrs.pop('verbose_name_plural', string_concat(self.verbose_name, 's')))

                # Any leftover attributes must be invalid.
                if meta_attrs != {}:
                    raise TypeError, "'class Meta' got invalid attribute(s): %s" % ','.join(meta_attrs.keys())
            else:
                self.verbose_name_plural = self.verbose_name + 's'

            if not self.abstract:
                self.permissions.extend([
                    ('add_%s' % self.object_name.lower(),
                        string_concat('Can add ', self.verbose_name)),
                    ('change_%s' % self.object_name.lower(),
                        string_concat('Can change ', self.verbose_name)),
                    ('delete_%s' % self.object_name.lower(),
                        string_concat('Can delete ', self.verbose_name)),
                ])

        def __repr__(self):
            return '<Options for %s>' % self.object_name

        def __str__(self):
            return "%s.%s" % (smart_str(self.app_label), smart_str(self.module_name))

        def _set_db_table(self, db_table):
            self._db_table = db_table
        
        def _get_db_table(self):
            if getattr(settings, 'DJANGO_STYLE_MODEL_KIND', True):
                if hasattr(self, '_db_table'):
                    return self._db_table
                return '%s_%s' % (self.app_label, self.module_name)
            return self.object_name

        db_table = property(_get_db_table, _set_db_table)

        def _set_db_tablespace(self, db_tablespace):
            self._db_tablespace = db_tablespace
        
        def _get_db_tablespace(self):
            if hasattr(self, '_db_tablespace'):
                return self._db_tablespace
            return settings.DEFAULT_TABLESPACE

        db_tablespace = property(_get_db_tablespace, _set_db_tablespace)

        @property
        def verbose_name_raw(self):
            """
            There are a few places where the untranslated verbose name is needed
            (so that we get the same value regardless of currently active
            locale).
            """
            lang = get_language()
            deactivate_all()
            raw = force_unicode(self.verbose_name)
            activate(lang)
            return raw

        @property
        def local_fields(self):
            return tuple(sorted([p for p in self.model.properties().values()
                                 if not isinstance(p, db.ListProperty)],
                                key=lambda prop: prop.creation_counter))

        @property
        def local_many_to_many(self):
            return tuple(sorted([p for p in self.model.properties().values()
                                 if isinstance(p, db.ListProperty) and
                                     not p.name == '_class'],
                                key=lambda prop: prop.creation_counter))

        @property
        def fields(self):
            return self.local_fields + self.local_many_to_many

        def get_field(self, name, many_to_many=True):
            """
            Returns the requested field by name. Raises FieldDoesNotExist on error.
            """
            from django.db.models.fields import FieldDoesNotExist
            for f in self.fields:
                if f.name == name:
                    return f
            raise FieldDoesNotExist, '%s has no field named %r' % (self.object_name, name)

        def get_all_related_objects(self, local_only=False):
            try:
                self._related_objects_cache
            except AttributeError:
                self._fill_related_objects_cache()
            if local_only:
                return [k for k, v in self._related_objects_cache.items() if not v]
            return self._related_objects_cache.keys()

        def get_all_related_objects_with_model(self):
            """
            Returns a list of (related-object, model) pairs. Similar to
            get_fields_with_model().
            """
            try:
                self._related_objects_cache
            except AttributeError:
                self._fill_related_objects_cache()
            return self._related_objects_cache.items()

        def _fill_related_objects_cache(self):
            from django.db.models.loading import get_models
            from django.db.models.related import RelatedObject
            cache = SortedDict()
            parent_list = self.get_parent_list()
            for parent in self.parents:
                for obj, model in parent._meta.get_all_related_objects_with_model():
                    if (obj.field.creation_counter < 0 or obj.field.rel.parent_link) and obj.model not in parent_list:
                        continue
                    if not model:
                        cache[obj] = parent
                    else:
                        cache[obj] = model
            for klass in get_models():
                for f in klass._meta.local_fields:
                    if f.rel and not isinstance(f.rel.to, str) and self == f.rel.to._meta:
                        cache[RelatedObject(f.rel.to, klass, f)] = None
            self._related_objects_cache = cache

        def get_all_related_many_to_many_objects(self, local_only=False):
            try:
                cache = self._related_many_to_many_cache
            except AttributeError:
                cache = self._fill_related_many_to_many_cache()
            if local_only:
                return [k for k, v in cache.items() if not v]
            return cache.keys()

        def get_all_related_m2m_objects_with_model(self):
            """
            Returns a list of (related-m2m-object, model) pairs. Similar to
            get_fields_with_model().
            """
            try:
                cache = self._related_many_to_many_cache
            except AttributeError:
                cache = self._fill_related_many_to_many_cache()
            return cache.items()

        def _fill_related_many_to_many_cache(self):
            from django.db.models.loading import get_models, app_cache_ready
            from django.db.models.related import RelatedObject
            cache = SortedDict()
            parent_list = self.get_parent_list()
            for parent in self.parents:
                for obj, model in parent._meta.get_all_related_m2m_objects_with_model():
                    if obj.field.creation_counter < 0 and obj.model not in parent_list:
                        continue
                    if not model:
                        cache[obj] = parent
                    else:
                        cache[obj] = model
            for klass in get_models():
                for f in klass._meta.local_many_to_many:
                    if f.rel and not isinstance(f.rel.to, str) and self == f.rel.to._meta:
                        cache[RelatedObject(f.rel.to, klass, f)] = None
            if app_cache_ready():
                self._related_many_to_many_cache = cache
            return cache

        def get_add_permission(self):
            return 'add_%s' % self.object_name.lower()

        def get_change_permission(self):
            return 'change_%s' % self.object_name.lower()

        def get_delete_permission(self):
            return 'delete_%s' % self.object_name.lower()

        def get_ordered_objects(self):
            return []

        def get_parent_list(self):
            """
            Returns a list of all the ancestor of this model as a list. Useful for
            determining if something is an ancestor, regardless of lineage.
            """
            result = set()
            for parent in self.parents:
                result.add(parent)
                result.update(parent._meta.get_parent_list())
            return result

    # Required to support reference properties to db.Model
    db.Model._meta = _meta(db.Model, ())
    
    def _initialize_model(cls, bases):
        cls._meta = _meta(cls, bases)
        cls._default_manager = cls
        if not cls._meta.abstract:
            from django.db.models.loading import register_models
            register_models(cls._meta.app_label, cls)

    # Register models with Django
    from django.db.models import signals
    old_propertied_class_init = db.PropertiedClass.__init__
    def __init__(cls, name, bases, attrs, map_kind=True):
        """Creates a combined appengine and Django model.

        The resulting model will be known to both the appengine libraries and
        Django.
        """
        _initialize_model(cls, bases)
        old_propertied_class_init(cls, name, bases, attrs,
            not cls._meta.abstract)
        signals.class_prepared.send(sender=cls)
    db.PropertiedClass.__init__ = __init__

    old_poly_init = polymodel.PolymorphicClass.__init__
    def __init__(cls, name, bases, attrs):
        if polymodel.PolyModel not in bases:
            _initialize_model(cls, bases)
        old_poly_init(cls, name, bases, attrs)
        if polymodel.PolyModel not in bases:
            signals.class_prepared.send(sender=cls)
    polymodel.PolymorphicClass.__init__ = __init__

    @classmethod
    def kind(cls):
        return cls._meta.db_table
    db.Model.kind = kind

    # Add model signals
    old_model_init = db.Model.__init__
    def __init__(self, *args, **kwargs):
        signals.pre_init.send(sender=self.__class__, args=args, kwargs=kwargs)
        old_model_init(self, *args, **kwargs)
        signals.post_init.send(sender=self.__class__, instance=self)
    db.Model.__init__ = __init__

    old_put = db.Model.put
    def put(self, *args, **kwargs):
        raw = False
        signals.pre_save.send(sender=self.__class__, instance=self, raw=raw)
        created = not self.is_saved()
        result = old_put(self, *args, **kwargs)
        signals.post_save.send(sender=self.__class__, instance=self,
            created=created, raw=raw)
        return result
    db.Model.put = put

    old_delete = db.Model.delete
    def delete(self, *args, **kwargs):
        signals.pre_delete.send(sender=self.__class__, instance=self)
        result = old_delete(self, *args, **kwargs)
        signals.post_delete.send(sender=self.__class__, instance=self)
        return result
    db.Model.delete = delete

    # This has to come last because we load Django here
    from django.db.models.fields import BLANK_CHOICE_DASH
    def get_choices(self, include_blank=True, blank_choice=BLANK_CHOICE_DASH):
        first_choice = include_blank and blank_choice or []
        if self.choices:
            return first_choice + list(self.choices)
        if self.rel:
            return first_choice + [(obj.pk, unicode(obj))
                                   for obj in self.rel.to.all().fetch(301)]
        return first_choice
    db.Property.get_choices = get_choices

    fix_app_engine_bugs()

def fix_app_engine_bugs():
    # Fix handling of verbose_name. Google resolves lazy translation objects
    # immedately which of course breaks translation support.
    # http://code.google.com/p/googleappengine/issues/detail?id=583
    from django import forms
    from django.utils.text import capfirst
    # This import is needed, so the djangoforms patch can do its work, first
    from google.appengine.ext.db import djangoforms
    def get_form_field(self, form_class=forms.CharField, **kwargs):
        defaults = {'required': self.required}
        defaults['label'] = capfirst(self.verbose_name)
        if self.choices:
            choices = []
            if not self.required or (self.default is None and
                                     'initial' not in kwargs):
                choices.append(('', '---------'))
            for choice in self.choices:
                choices.append((unicode(choice), unicode(choice)))
            defaults['widget'] = forms.Select(choices=choices)
        if self.default is not None:
            defaults['initial'] = self.default
        defaults.update(kwargs)
        return form_class(**defaults)
    db.Property.get_form_field = get_form_field

    # Extend ModelForm with support for EmailProperty
    # http://code.google.com/p/googleappengine/issues/detail?id=880
    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(db.EmailProperty, self).get_form_field(**defaults)
    db.EmailProperty.get_form_field = get_form_field

    # Fix DateTimeProperty, so it returns a property even for auto_now and
    # auto_now_add.
    # http://code.google.com/p/googleappengine/issues/detail?id=994
    def get_form_field(self, **kwargs):
        defaults = {'form_class': forms.DateTimeField}
        defaults.update(kwargs)
        return super(db.DateTimeProperty, self).get_form_field(**defaults)
    db.DateTimeProperty.get_form_field = get_form_field
    def get_form_field(self, **kwargs):
        defaults = {'form_class': forms.DateField}
        defaults.update(kwargs)
        return super(db.DateProperty, self).get_form_field(**defaults)
    db.DateProperty.get_form_field = get_form_field
    def get_form_field(self, **kwargs):
        defaults = {'form_class': forms.TimeField}
        defaults.update(kwargs)
        return super(db.TimeProperty, self).get_form_field(**defaults)
    db.TimeProperty.get_form_field = get_form_field

    # Fix default value of UserProperty (Google resolves the user too early)
    # http://code.google.com/p/googleappengine/issues/detail?id=879
    from django.utils.functional import lazy
    from google.appengine.api import users
    def get_form_field(self, **kwargs):
        defaults = {'initial': lazy(users.GetCurrentUser, users.User)()}
        defaults.update(kwargs)
        return super(db.UserProperty, self).get_form_field(**defaults)
    db.UserProperty.get_form_field = get_form_field

    # Fix file uploads via BlobProperty
    def get_form_field(self, **kwargs):
        defaults = {'form_class': forms.FileField}
        defaults.update(kwargs)
        return super(db.BlobProperty, self).get_form_field(**defaults)
    db.BlobProperty.get_form_field = get_form_field
    def get_value_for_form(self, instance):
        return getattr(instance, self.name)
    db.BlobProperty.get_value_for_form = get_value_for_form
    from django.core.files.uploadedfile import UploadedFile
    def make_value_from_form(self, value):
        if isinstance(value, UploadedFile):
            return db.Blob(value.read())
        return super(db.BlobProperty, self).make_value_from_form(value)
    db.BlobProperty.make_value_from_form = make_value_from_form

    # Optimize ReferenceProperty, so it returns the key directly
    # http://code.google.com/p/googleappengine/issues/detail?id=993
    def get_value_for_form(self, instance):
        return self.get_value_for_datastore(instance)
    db.ReferenceProperty.get_value_for_form = get_value_for_form
    # Use our ModelChoiceField instead of Google's
    def get_form_field(self, **kwargs):
        defaults = {'form_class': forms.ModelChoiceField,
                    'queryset': self.reference_class.all()}
        defaults.update(kwargs)
        return super(db.ReferenceProperty, self).get_form_field(**defaults)
    db.ReferenceProperty.get_form_field = get_form_field

def patch_django():
    # Most patches are part of the django-app-engine project:
    # http://www.bitbucket.org/wkornewald/django-app-engine/

    # Activate ragendja's GLOBALTAGS support (automatically done on import)
    from ragendja import template

def setup_logging():
    from django.conf import settings
    if settings.DEBUG:
        logging.getLogger().setLevel(logging.DEBUG)
    else:
        logging.getLogger().setLevel(logging.INFO)