Skip to content

Instantly share code, notes, and snippets.

@kingbuzzman
Last active September 17, 2019 21:49
Show Gist options
  • Select an option

  • Save kingbuzzman/c3495ef0f600c1356179e0fff8db29aa to your computer and use it in GitHub Desktop.

Select an option

Save kingbuzzman/c3495ef0f600c1356179e0fff8db29aa to your computer and use it in GitHub Desktop.
Adds the ability to add prefixes to django model field names
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Stolen from: https://mlvin.xyz/django-single-file-project.html
import inspect
import os
import sys
from types import ModuleType
import django
from django.conf import settings
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
# The current name of the file, which will be the name of our app
APP_LABEL, _ = os.path.splitext(os.path.basename(os.path.abspath(__file__)))
# Migrations folder need to be created, and django needs to be told where it is
APP_MIGRATION_MODULE = '%s_migrations' % APP_LABEL
APP_MIGRATION_PATH = os.path.join(BASE_DIR, APP_MIGRATION_MODULE)
# Create the folder and a __init__.py if they don't exist
if not os.path.exists(APP_MIGRATION_PATH):
os.makedirs(APP_MIGRATION_PATH)
open(os.path.join(APP_MIGRATION_PATH, '__init__.py'), 'w').close()
# Hack to trick Django into thinking this file is actually a package
sys.modules[APP_LABEL] = sys.modules[__name__]
sys.modules[APP_LABEL].__path__ = [os.path.abspath(__file__)]
settings.configure(
DEBUG=True,
ROOT_URLCONF='%s.urls' % APP_LABEL,
MIDDLEWARE=(),
INSTALLED_APPS=[APP_LABEL],
MIGRATION_MODULES={APP_LABEL: APP_MIGRATION_MODULE},
SITE_ID=1,
DATABASES={
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
},
LOGGING={
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'simple': {
'format': "%(levelname)s %(message)s",
},
},
'handlers': {
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'simple',
}
},
'loggers': {
'django.db.backends': {'handlers': ['console'], 'level': 'DEBUG', 'propagate': False},
'django.db.backends.schema': {'level': 'ERROR'}, # Causes sql logs to duplicate -- really annoying
}
},
STATIC_URL='/static/'
)
django.setup()
from django.apps import apps # noqa: E402 isort:skip
# Setup the AppConfig so we don't have to add the app_label to all our models
def get_containing_app_config(module):
if module == '__main__':
return apps.get_app_config(APP_LABEL)
return apps._get_containing_app_config(module)
apps._get_containing_app_config = apps.get_containing_app_config
apps.get_containing_app_config = get_containing_app_config
# Here be dragons
################################################################################
import collections # noqa: E402 isort:skip
import copy # noqa: E402 isort:skip
from django.conf import settings # noqa: E402 isort:skip
from django.db import models # noqa: E402 isort:skip
from django.db.models import Manager, Field # noqa: E402 isort:skip
from django.db.models.base import ModelBase # noqa: E402 isort:skip
from django.test import TestCase # noqa: E402 isort:skip
class ManagerAwareMetaClass(ModelBase):
"""
Joins all the Model mixins that have "objects" defined together to form a single Manager.
Usage:
class MyMixin(models.Models, metaclass=ManagerAwareMetaClass):
...
objects = CustomManager()
"""
def __new__(cls, name, bases, attrs, **kwargs):
# The user specified it's own manager, let's respect it
if 'objects' in attrs:
return super().__new__(cls, name, bases, attrs, **kwargs)
managers = []
querysets = []
# Loop over all the classes in the inheritance, and save the Manager / QuerySet if they're custom
# otherwise ignore them.
for subclass in bases:
if not (hasattr(subclass, '_meta') and hasattr(subclass._meta, 'managers_map')):
continue
if 'objects' not in subclass._meta.managers_map:
continue
manager = subclass._meta.managers_map['objects'].__class__
# If there is a custom QuerySet chances are high that the the Manager was created using the .as_manager()
# hence it's not really custom, so we can stop there.
if manager._queryset_class is not models.QuerySet:
querysets.append(manager._queryset_class)
continue
# If it's a custom Manager, save it
if manager is not models.Manager:
managers.append(manager)
# Remove duplicates and maintain the order
managers = list(collections.OrderedDict.fromkeys(managers).keys())
querysets = list(collections.OrderedDict.fromkeys(querysets).keys())
if len(querysets) == 1:
QuerySetKlass = querysets[0]
elif querysets:
# Join all the querysets together and make a single one.
QuerySetKlass = type('JoinedQuerySet', tuple(querysets), {'__module__': ''})
else:
QuerySetKlass = models.QuerySet
if managers:
if len(managers) == 1:
ManagerKlass = managers[0]
else:
# Join all the managers together and make a single manager
ManagerKlass = type('JoinedManager', tuple(managers), {'__module__': ''})
attrs['objects'] = ManagerKlass.from_queryset(QuerySetKlass)()
else:
attrs['objects'] = QuerySetKlass.as_manager()
return super().__new__(cls, name, bases, attrs, **kwargs)
class PrefixMetaClass(ManagerAwareMetaClass):
"""
Keeps track of the model's fields before they're completly tainted by django's ModelBase.
When the Model is first constructed, django runs contribute_to_class() on all the model's fields,
completely tainting them with very specific information. This metaclass constructor saves the original
fields before any of that happens so we can clone it later.
"""
def __new__(cls, name, bases, attrs, **kwargs):
original_fields = {}
for _name, field in attrs.items():
if not isinstance(field, Field):
continue
original_fields[_name] = copy.deepcopy(field)
new_obj = super().__new__(cls, name, bases, attrs, **kwargs)
if original_fields:
new_obj._meta._original_fields = original_fields
return new_obj
class PrefixModel(models.Model, metaclass=PrefixMetaClass):
"""
Allows for adding field prefixes to model mixins.
For example:
class PersonMixin(PrefixModel):
class Meta:
abstract = True
name = models.CharField(max_length=10)
age = models.IntField()
class Person(PersonMixin, PersonMixin.prefix('mother'), PersonMixin.prefix('father')):
# # The following fields will be given to the class
# name = models.CharField(max_length=10)
# age = models.IntField()
# mother_name = models.CharField(max_length=10)
# mother_age = models.IntField()
# father_name = models.CharField(max_length=10)
# father_age = models.IntField()
"""
@classmethod
def prefix(cls, prefix):
fields = {}
for name, field in cls._meta._original_fields.items():
if not isinstance(field, models.Field):
continue
field = copy.deepcopy(field)
# Fixes issues with comparing fields
if field.auto_created:
field.creation_counter = models.fields.Field.auto_creation_counter
models.fields.Field.auto_creation_counter -= 1
else:
field.creation_counter = models.fields.Field.creation_counter
models.fields.Field.creation_counter += 1
# Fixes issue with mixins with ForeignKey clashing with one another
if isinstance(field, models.ForeignKey):
related_name = field.remote_field.related_name or '%(app_label)s_%(model_name)ss'
field.remote_field.related_name = '{}_{}'.format(related_name, prefix)
fields[(prefix + '_' if prefix else '') + name] = field
class Meta:
abstract = True
attrs = {'Meta': Meta, '__module__': ''}
attrs.update(fields)
return type(prefix.capitalize() + cls.__name__, (models.Model,), attrs)
class Meta:
abstract = True
# Normal code here
################################################################################
urlpatterns = []
class ColorPreferenceManager(models.Manager):
"""Useless Manager to demonstrate managers work"""
def get_color(self, name):
return self.get_queryset().get(color=name)
class ColorPreferenceMixin(models.Model, metaclass=ManagerAwareMetaClass):
"""Useless Model to demonstrate managers work"""
color = models.CharField(max_length=30)
objects = ColorPreferenceManager()
class Meta:
abstract = True
class AddressType(models.Model):
name = models.CharField(max_length=10)
floor = models.CharField(max_length=10, blank=True, null=True)
class AddressQuerySet(models.QuerySet):
"""Useless QuerySet to demonstrate querysets work"""
def get_americans(self):
return self.filter(country='USA')
class AddressMixin(PrefixModel, metaclass=ManagerAwareMetaClass):
address = models.CharField(max_length=30)
address2 = models.CharField(max_length=30)
city = models.CharField(max_length=30)
province = models.CharField(max_length=30)
country = models.CharField(max_length=30)
type = models.ForeignKey(AddressType, null=True, blank=True, on_delete=models.SET_NULL)
objects = AddressQuerySet.as_manager()
class Meta:
abstract = True
class Role(models.Model):
name = models.CharField(max_length=30)
class PersonMixinQuerySet(models.QuerySet):
"""Useless QuerySet to demonstrate querysets work"""
def get_females(self):
return self.filter(gender='F')
def get_males(self):
return self.filter(gender='M')
class PersonMixin(PrefixModel, metaclass=ManagerAwareMetaClass):
gender = models.CharField(max_length=30)
role = models.ForeignKey(Role, on_delete=models.CASCADE)
objects = PersonMixinQuerySet.as_manager()
class Meta:
abstract = True
class Person(ColorPreferenceMixin, AddressMixin, AddressMixin.prefix('secondary'), PersonMixin):
name = models.CharField(max_length=30)
# address = models.CharField(max_length=30)
# ..
# country = models.CharField(max_length=30)
# secondary_address = models.CharField(max_length=30)
# ..
# secondary_country = models.CharField(max_length=30)
# gender = models.CharField(max_length=30)
# role = models.ForeignKey(Role, on_delete=models.CASCADE)
# secondary_gender = models.CharField(max_length=30)
# secondary_role = models.ForeignKey(Role, on_delete=models.CASCADE)
class PersonTestCase(TestCase):
def test_person_has_all_fields(self):
fields = [f.name for f in Person._meta.fields]
self.assertIn('address', fields)
self.assertIn('address2', fields)
self.assertIn('city', fields)
self.assertIn('province', fields)
self.assertIn('country', fields)
self.assertIn('secondary_address', fields)
self.assertIn('secondary_address2', fields)
self.assertIn('secondary_city', fields)
self.assertIn('secondary_province', fields)
self.assertIn('secondary_country', fields)
self.assertIn('role', fields)
self.assertIn('gender', fields)
def test_person_manager(self):
properties = dir(Person.objects)
self.assertIn('get_color', properties)
self.assertIn('get_americans', properties)
self.assertIn('get_males', properties)
self.assertIn('get_females', properties)
def test_person_manager_override(self):
class NewPerson(Person):
objects = Manager()
properties = dir(NewPerson.objects)
self.assertNotIn('get_color', properties)
self.assertNotIn('get_americans', properties)
self.assertNotIn('get_males', properties)
self.assertNotIn('get_females', properties)
def test_query(self):
house_type = AddressType.objects.create(name='house')
role = Role.objects.create(name='R')
building_type = AddressType.objects.create(name='building', floor='one')
Person.objects.create(name='john smith',
address='one west st',
address2='',
role=role,
gender='M',
city='Barcelona',
province='Catalunya',
country='Spain',
type=house_type,
secondary_address='two west st',
secondary_address2='',
secondary_city='Gerona',
secondary_province='Catalunya',
secondary_country='Spain',
secondary_type=building_type)
person = Person.objects.select_related('type', 'secondary_type').get()
self.assertEqual(house_type, person.type)
self.assertEqual(building_type, person.secondary_type)
# Your code above this line
# ##############################################################################
# Used so you can do 'from <name of file>.models import *'
models_module = ModuleType('%s.models' % (APP_LABEL))
tests_module = ModuleType('%s.tests' % (APP_LABEL))
urls_module = ModuleType('%s.urls' % (APP_LABEL))
urls_module.urlpatterns = urlpatterns
for variable_name, value in list(locals().items()):
# We are only interested in models
if inspect.isclass(value) and issubclass(value, models.Model):
setattr(models_module, variable_name, value)
# We are only interested in tests
if inspect.isclass(value) and issubclass(value, TestCase):
setattr(tests_module, variable_name, value)
# Setup the fake modules
sys.modules[models_module.__name__] = models_module
sys.modules[tests_module.__name__] = tests_module
sys.modules[urls_module.__name__] = urls_module
sys.modules[APP_LABEL].models = models_module
sys.modules[APP_LABEL].tests = tests_module
sys.modules[APP_LABEL].urls = urls_module
if __name__ == "__main__":
# Hack to fix tests
argv = [arg for arg in sys.argv if not arg.startswith('-')]
if len(argv) == 2 and argv[1] == 'test':
sys.argv.append(APP_LABEL)
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)
else:
from django.core.wsgi import get_wsgi_application
get_wsgi_application()
@kingbuzzman
Copy link
Copy Markdown
Author

kingbuzzman commented Apr 24, 2019

How to use it: (preferably inside a docker container)

curl https://gist.githubusercontent.com/kingbuzzman/c3495ef0f600c1356179e0fff8db29aa/raw/django_prefix_mixing.py > django_prefix_mixing.py
pip install django==2.1 
python django_prefix_mixing.py makemigrations
python django_prefix_mixing.py test

or just copy-and-paste.

docker run -it --rm python:3.7 bash -c '
curl https://gist.githubusercontent.com/kingbuzzman/c3495ef0f600c1356179e0fff8db29aa/raw/django_prefix_mixing.py > django_prefix_mixing.py
pip install django==2.1 
python django_prefix_mixing.py makemigrations
python django_prefix_mixing.py test
'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment