pax_global_header00006660000000000000000000000064137217626550014530gustar00rootroot0000000000000052 comment=57cb22f7b14c17415ebdb3fbb92aa69f437fe3a9 django-auto-one-to-one-3.3.1/000077500000000000000000000000001372176265500157225ustar00rootroot00000000000000django-auto-one-to-one-3.3.1/.gitignore000066400000000000000000000000221372176265500177040ustar00rootroot00000000000000*.pyc *.egg-info/ django-auto-one-to-one-3.3.1/.travis.yml000066400000000000000000000007021372176265500200320ustar00rootroot00000000000000language: python python: - "2.7" - "3.5" - "3.6" - "3.7" env: matrix: - DJANGO="Django<2" - DJANGO="Django<2.1" - DJANGO="Django<2.2" - DJANGO="Django<3" matrix: exclude: - python: "2.7" env: DJANGO="Django<2.1" - python: "2.7" env: DJANGO="Django<2.2" - python: "2.7" env: DJANGO="Django<3" install: - pip install "$DJANGO" flake8 before_script: - flake8 . || true script: - ./runtests.py django-auto-one-to-one-3.3.1/COPYING000066400000000000000000000027201372176265500167560ustar00rootroot00000000000000Copyright © 2014-2018 Chris Lamb All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * The name of the contributors may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. django-auto-one-to-one-3.3.1/README.md000066400000000000000000000005631372176265500172050ustar00rootroot00000000000000# django-auto-one-to-one Automatically create child model instances when a parent class is created. See `django_auto_one_to_one/models.py` for further documentation. The project home page is at: https://chris-lamb.co.uk/projects/django-auto-one-to-one ... whilst the source and issue tracker is available at: https://github.com/lamby/django-auto-one-to-one django-auto-one-to-one-3.3.1/django_auto_one_to_one/000077500000000000000000000000001372176265500224205ustar00rootroot00000000000000django-auto-one-to-one-3.3.1/django_auto_one_to_one/__init__.py000066400000000000000000000000631372176265500245300ustar00rootroot00000000000000from .models import AutoOneToOneModel, PerUserData django-auto-one-to-one-3.3.1/django_auto_one_to_one/models.py000066400000000000000000000111611372176265500242550ustar00rootroot00000000000000import six from django.db import models from django.db.models.signals import post_save, pre_delete from django.contrib.auth import get_user_model from django.apps import apps def AutoOneToOneModel(parent, related_name=None, attr=None, on_delete=models.CASCADE, auto=True): """ Automatically create child model instances when a parent class is created. For example, given the following model definition:: from django.db import models from django_auto_one_to_one import AutoOneToOneModel class Parent(models.Model): field_a = models.IntegerField(default=1) class Child(AutoOneToOneModel(Parent)): field_b = models.IntegerField(default=2) ... creating a ``Parent`` instance automatically creates a related ``Child`` instance:: >>> p = Parent.objects.create() >>> p.child >>> p.child.field_b 2 You must ensure that child models can be created without arguments via ``default=`` rather than overriding ``save``. Related names ============= You can specify the related name on the parent via ``related_name``. The default is to use Django's default ``related_name`` logic, often resulting in clunky English or a clunky cross-app API: class UserData(AutoOneToOneModel('auth.User'), related_name='profile'): field_d = models.IntegerField(default=4) >>> u = User.objects.create_user(...) >>> u.profile.field_d 4 Custom field names ================== You can also specify the attribute or field name that is added to the child class. The default is to use a name based on the parent class name:: class Child3(AutoOneToOneModel(Parent): field_e = models.IntegerField(default=5) >>> p = Parent.objects.create() >>> p.child.parent == p True However, you can specify a custom attribute via ``attr``: class Child2(AutoOneToOneModel(Parent, attr='custom_parent_name'): field_f = models.IntegerField(default=6) >>> = Parent.objects.create() >>> p.child.custom_parent_name == p True Convenience classes =================== As a convenience method, ``PerUserData`` will create instances hanging off the default Django ``User`` class. class Profile(PerUserData('profile')): nickname = models.CharField(max_length=40) """ # Automatically calculate attribute on child class if not attr: attr = parent._meta.verbose_name_raw.replace(' ', '_') # The current implementation is a class factory that returns an abstract # parent model. It's a little convoluted but I'm not sure everything is # setup enough in a class decorator. class Base(models.base.ModelBase): def __new__(mcs, name, bases, attrs): model = super(Base, mcs).__new__(mcs, name, bases, attrs) if model._meta.abstract: return model # Avoid virtual models (for, for instance, deferred fields) if model._meta.concrete_model is not model: return model if not auto: return model # Don't set up signals for models that aren't installed as the # database tables won't exist. try: apps.get_app_config(model._meta.app_label) except LookupError: return model # Setup the signals that will automatically create and destroy # instances. # # We use weak=False or our (inline) receivers will be garbage # collected. def on_create_cb(sender, instance, created, *args, **kwargs): if created: model.objects.create(**{attr: instance}) def on_delete_cb(sender, instance, *args, **kwargs): model.objects.filter(pk=instance).delete() post_save.connect(on_create_cb, sender=parent, weak=False) pre_delete.connect(on_delete_cb, sender=parent, weak=False) return model @six.python_2_unicode_compatible class Parent(six.with_metaclass(Base, models.Model)): locals()[attr] = models.OneToOneField( parent, on_delete=on_delete, primary_key=True, related_name=related_name, ) class Meta: abstract = True def __str__(self): return "{}={}".format(attr, getattr(self, attr)) return Parent def PerUserData(*args, **kwargs): return AutoOneToOneModel(get_user_model(), *args, **kwargs) django-auto-one-to-one-3.3.1/runtests.py000077500000000000000000000006121372176265500201650ustar00rootroot00000000000000#!/usr/bin/env python import os import sys import django from django.conf import settings from django.test.utils import get_runner if __name__ == '__main__': os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.test_settings' django.setup() TestRunner = get_runner(settings) test_runner = TestRunner() failures = test_runner.run_tests(sys.argv[1:]) sys.exit(bool(failures)) django-auto-one-to-one-3.3.1/setup.py000077500000000000000000000007201372176265500174360ustar00rootroot00000000000000#!/usr/bin/env python3 from setuptools import setup, find_packages setup( name='django-auto-one-to-one', version='3.3.1', packages=find_packages(exclude=('tests',)), url='https://chris-lamb.co.uk/projects/django-auto-one-to-one', author="Chris Lamb", author_email="chris@chris-lamb.co.uk", description="Automatically create and destroy child model instances", install_requires=( 'Django>=1.11', 'six', ), ) django-auto-one-to-one-3.3.1/tests/000077500000000000000000000000001372176265500170645ustar00rootroot00000000000000django-auto-one-to-one-3.3.1/tests/__init__.py000066400000000000000000000000001372176265500211630ustar00rootroot00000000000000django-auto-one-to-one-3.3.1/tests/models.py000066400000000000000000000006051372176265500207220ustar00rootroot00000000000000from django.db import models from django_auto_one_to_one import AutoOneToOneModel, PerUserData class Hat(models.Model): name = models.CharField(max_length=20, unique=True) def __str__(self): # type: () -> str return self.name class Brim(AutoOneToOneModel(Hat)): pass class Profile(PerUserData('profile')): nickname = models.CharField(max_length=20) django-auto-one-to-one-3.3.1/tests/test_settings.py000066400000000000000000000012211372176265500223310ustar00rootroot00000000000000SECRET_KEY = 'fake-key' INSTALLED_APPS = [ 'django.contrib.auth', 'django.contrib.contenttypes', 'tests', ] DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': ':memory:', }, } # Tell Django to ignore migrations and create these apps' tables from the models # instead. This is needed in Django >= 2.2, where something changed such that if # the tables for our models (which don't have migrations) reference the tables # for contrib models (which do have migrations) then the creation of our tables # as part of the test setup fails. MIGRATION_MODULES = {x: None for x in ('auth', 'contenttypes')} django-auto-one-to-one-3.3.1/tests/tests.py000066400000000000000000000025131372176265500206010ustar00rootroot00000000000000from django.contrib.auth.models import User from django.test import TestCase from .models import Brim, Hat, Profile class SmokeTests(TestCase): def test_model_creation(self): # type: () -> None trilby = Hat(name="Trilby") self.assertFalse(Brim.objects.exists()) trilby.save() trilby_brim = Brim.objects.get() self.assertEqual(trilby_brim, trilby.brim) self.assertEqual(trilby_brim.hat, trilby) def test_str(self): # type: () -> None trilby = Hat.objects.create(name="Trilby") self.assertIn("Trilby", str(trilby.brim)) def test_two_models(self): # type: () -> None trilby = Hat.objects.create(name="Trilby") fedora = Hat.objects.create(name="Fedora") self.assertNotEqual(fedora.brim, trilby.brim) self.assertEqual(2, Brim.objects.count()) def test_model_deletion(self): # type: () -> None trilby = Hat.objects.create(name="Trilby") self.assertTrue(Brim.objects.exists()) trilby.delete() self.assertFalse(Brim.objects.exists()) def test_per_user_data(self): # type: () -> None user = User.objects.create() profile = Profile.objects.get() self.assertEqual(profile, user.profile) self.assertEqual(profile.user, user)