Last active
September 17, 2019 21:49
-
-
Save kingbuzzman/c3495ef0f600c1356179e0fff8db29aa to your computer and use it in GitHub Desktop.
Adds the ability to add prefixes to django model field names
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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() |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
How to use it: (preferably inside a docker container)
or just copy-and-paste.