Loose coupling of apps & model inheritance - django

I have a design question concerning Django. I am not quite sure how to apply the principle of loose coupling of apps to this specific problem:
I have an order-app that manages orders (in an online shop). Within this order-app I have two classes:
class Order(models.Model):
# some fields
def order_payment_complete(self):
# do something when payment complete, ie. ship products
pass
class Payment(models.Model):
order = models.ForeignKey(Order)
# some more fields
def save(self):
# determine if payment has been updated to status 'PAID'
if is_paid:
self.order.order_payment_complete()
super(Payment, self).save()
Now the actual problem: I have a more specialized app that kind of extends this order. So it adds some more fields to it, etc. Example:
class SpecializedOrder(Order):
# some more fields
def order_payment_complete(self):
# here we do some specific stuff
pass
Now of course the intended behaviour would be as follows: I create a SpecializedOrder, the payment for this order is placed and the order_payment_complete() method of the SpecializedOrder is called. However, since Payment is linked to Order, not SpecializedOrder, the order_payment_complete() method of the base Order is called.
I don't really know the best way to implement such a design. Maybe I am completely off - but I wanted to build this order-app so that I can use it for multiple purposes and wanted to keep it as generic as possible.
It would be great if someone could help me out here!
Thanks,
Nino

I think what you're looking for is the GenericForeignKey from the ContentTypes framework, which is shipped with Django in the contrib package. It handles recording the type and id of the subclass instance, and provides a seamless way to access the subclasses as a foreign key property on the model.
In your case, it would look something like this:
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic
class Payment(models.Model):
order_content_type = models.ForeignKey(ContentType)
order_object_id = models.PositiveIntegerField()
order = generic.GenericForeignKey('order_content_type', 'order_object_id')
You don't need to do anything special in order to use this foreign key... the generics handle setting and saving the order_content_type and order_object_id fields transparently:
s = SpecializedOrder()
p = Payment()
p.order = s
p.save()
Now, when your Payment save method runs:
if is_paid:
self.order.order_payment_complete() # self.order will be SpecializedOrder

The thing you want is called dynamic polymorphism and Django is really bad at it. (I can feel your pain)
The simplest solution I've seen so far is something like this:
1) Create a base class for all your models that need this kind of feature. Something like this: (code blatantly stolen from here)
class RelatedBase(models.Model):
childclassname = models.CharField(max_length=20, editable=False)
def save(self, *args, **kwargs):
if not self.childclassname:
self.childclassname = self.__class__.__name__.lower()
super(RelatedBase, self).save(*args, **kwargs)
#property
def rel_obj(self):
return getattr(self, self.childclassname)
class Meta:
abstract = True
2) Inherit your order from this class.
3) Whenever you need an Order object, use its rel_obj attribute, which will return you the underlying object.
This solution is far from being elegant, but I've yet to find a better one...

Related

Enforce or test on_delete behavior of all ForeignKey fields using a specific model

Let's say I have a proxy user model as
class UserWithProfile(User):
profile_description = models.TextField()
class Meta:
proxy = True
ordering = ('first_name', )
I want to make certain that all data which could in the future be associated with a UserWithProfile entry is deleted when this profile is deleted. In other words I want to guarantee the on_delete behavior of all existing and future ForeignKey fields referencing this model.
How would one implement either a test checking this, or raise an error when another on_delete behavior is implemented?
I know it would be possible to make a custom ForeignKey class, which is what I will be probably doing, ...
class UserWithProfileField(models.ForeignKey):
def __init__(self, *args, **kwargs):
kwargs.setdefault('to', UserWithProfile)
kwargs.setdefault('on_delete', models.CASCADE)
super().__init__(*args, **kwargs)
... however that couldn't stop future users from using the ForeignKey class with a different on_delete behavior.
Instead of setdefault, you can override the on_delete parameter:
class UserWithProfileField(models.ForeignKey):
def __init__(self, *args, **kwargs):
kwargs['to'] = UserWithProfile
kwargs['on_delete'] = models.CASCADE
super().__init__(*args, **kwargs)
regardless what the user will now use for to=… or on_delete=…, it will use UserWithProfile and CASCADE.
Strictly speaking one can of course still try to alter the attributes of the ForeignKey, but that is more complicated, especially since Django constructs a ForeignObjectRel object to store relation details.
Note that a proxy model [Django-doc] is not used to add exta fields to the model. THis is more to alter the order, etc. and define new/other methods.
I don't get the invariants you are starting out with:
It's irrelevant whether you want to delete references to User or UserWithProfile since these are the same table?
You cannot police what other tables and model authors do and in which way shape or form they point to this table. If they use any kind of ForeignKey that's fine, but they could also point to the table using an unconstrained (integer?) field.
Could you make a test that bootstraps the database and everything, iterates over all models (both in this app and others) and checks every ForeignKey that is there to see if it is pointing to this model and it is setup correctly? That should serve the intended goal I believe.

Parametrize filtering manager queryset in Django

I want to implement kind of row level security for my model in Django. I want to filter out data as low as it's possible based on some requirement.
Now, I know I could create specified managers for this model as docs says but it seems for me that it needs to be static. I also know that I can create just method that will return queryset as I want but I'll be not sufficient, I mean the possibility to just get all data is still there and the simplest mistake can lead to leak of them.
So I found this post but as author said - it's not safe nor pretty to mess around with global states. This post is nearly 10 years old so I hope that maybe someone has come up with a better generic solution.
Here is piece of example to visualise what I need:
models.py:
class A(models.Model):
...
class B(models.Model):
user = models.ForeignKey(User)
a = models.ForeignKey(A)
And I want to create global functionality for getting objects of A only if instance of B with user as logged in user exists.
I came up with solution to just override get_queryset() of A manager like so:
managers.py
class AManager(models.Manager):
def get_queryset(self):
return super().get_queryset(b__user=**and_here_i_need_a_user**)
but I can't find hot to parametrize it.
==== EDIT ====
Another idea is to simply not allow to get querysets of A explicitly but only via related field from B but I can't find any reference how to accomplish that. Has anyone done something like that?
So you're sort of on the right track. How about something like this...
class AQuerySet(models.QuerySet):
def filter_by_user(self, user, *args, **kwargs):
user_filter = Q(b__user=user)
return self.filter(user_filter, *args, **kwargs)
class AManager(models.Manager):
queryset_class = AQuerySet
def filter_by_user(self, user, *args, **kwargs):
return self.get_queryset().filter_by_user(user, *args, **kwargs)
class A(models.Model):
objects = AManager()
# ...
then you can use it like this:
A.objects.filter_by_user(get_current_user(), some_filter='some_value')

Django + Factory Boy: Use Trait to create other factory objects

Is it possible to use Traits (or anything else in Factory Boy) to trigger the creation of other factory objects? For example: In a User-Purchase-Product situation, I want to create a user and inform that this user has a product purchased with something simple like that:
UserFactory.create(with_purchased_product=True)
Because it feels like too much trouble to call UserFactory, ProductFactory and PurchaseFactory, then crate the relationship between them. There has to be a simpler way to do that.
Any help would be appreciated.
First, I'll be honest with you: I do not know if this is the best answer or if it follows the good practices of python.
Anyway, the solution that I found for this kind of scenario was to use post_generation.
import factory
class UserFactory(factory.DjangoModelFactory):
class Meta:
model = User
name = factory.Faker('name'))
#factory.post_generation
def with_purchased_products(self, create, extracted, **kwargs):
if extracted is not None:
PurchaseFactory.create(user=self, with_products=extracted)
class PurchaseFactory(factory.DjangoModelFactory):
class Meta:
model = Purchase
user = factory.SubFactory(UserFactory)
#factory.post_generation
def with_products(self, create, extracted, **kwargs):
if extracted is not None:
ProductFactory.create_batch(extracted, purchase=self)
class ProductFactory(factory.DjangoModelFactory):
class Meta:
model = Product
purchase = factory.SubFactory(PurchaseFactory)
To make this work you just need to:
UserFactory.create(with_purchased_products=10)
And this is just a article that is helping learn more about Django tests with fakes & factories. Maybe can help you too.

Subclassing AbstractUser in Django for two types of users

I'm developing a school database system in Django 1.5, and was planning on having a number of different user types (Student, Staff, Parent) which subclass AbstractUser (actually, another abstract subclass of AbstractUser). I was just attempting to add an externally developed app to my system, which uses User in a ForeignKey for some of its models, however, this fails as my user type is not a 'User' instance. I can't set the apps models to use AbstractUser as one can't use abstract classes for Foreign Keys. I was then considering adding to my settings.py AUTH_USER_MODEL = 'myapp.MyUser' and using settings.AUTH_USER_MODEL in place of User for the ForeignKey in the app. However, I have 3 different user types, so can't do this either.
An earlier prototype used Django 1.4, which did not support custom User models, hence had a reference to a User instead, but this required an extra join for every query, which was leading to quite complex queries. Is this the only way I can go forward with this, or is there another solution?
I have successfully used the following solution:
1. Create SchoolUser class in models.py - this will be your AUTH_USER_MODEL class
TYPES = (('Student', 'Student'), ('Staff', 'Staff'), ('Parent', 'Parent'), )
class SchoolUser(AbstractUser):
type = models.CharField(max_length=10, choices=TYPES, default='Student')
2. Create users.py file and put whole users logic there. Have one abstract class that all others inherit from and which will implement the factory method:
class UserManager(object):
def __init__(self, user):
self.user = user
#classmethod
def factory(cls, user):
"""
Dynamically creates user object
"""
if cls.__name__.startswith(user.type): # Children class naming convention is important
return cls(user)
for sub_cls in cls.__subclasses__():
result = sub_cls.factory(user)
if result is not None:
return result
Sample children classes (also go to users.py file):
class StudentUser(UserManager):
def do_something(self):
pass
class StaffUser(UserManager):
def do_something(self):
pass
class ParentUser(UserManager):
def do_something(self):
pass
Views is where the magic happens ;)
def my_view(request):
school_user = UserManager.factory(request.user)
if school_user.do_something: # each class can have different behaviour
This way you don't need to know, which type of user it is, just implement your logic.
I hope this is clear enough, if not let me know!

Adding to the "constructor" of a django model

I want to do an extra initalization whenever instances of a specific django model are created. I know that overriding __init__ can lead to trouble. What other alternatives should I consider?
Update. Additional details: The intent is to initialize a state-machine that the instances of that model represent. This state-machine is provided by an imported library, and it's inner state is persisted by my django-model. The idea is that whenever the model is loaded, the state machine would be automatically initialized with the model's data.
Overriding __init__ might work, but it's bad idea and it's not the Django way.
The proper way of doing it in Django is using signals.
The ones that are of interest to you in this case are pre_init and post_init.
django.db.models.signals.pre_init
Whenever you instantiate a Django
model, this signal is sent at the beginning of the model’s __init__()
method.
django.db.models.signals.post_init
Like pre_init, but this one is sent
when the __init__(): method finishes
So your code should be something like
from django.db import models
from django.db.models.signals import post_init
class MyModel(models.Model):
# normal model definition...
def extraInitForMyModel(**kwargs):
instance = kwargs.get('instance')
do_whatever_you_need_with(instance)
post_init.connect(extraInitForMyModel, MyModel)
You can as well connect signals to Django's predefined models.
While I agree that there often is a better approach than overriding the __init__ for what you want to do, it is possible and there might be cases where it could be useful.
Here is an example on how to correctly override the __init__ method of a model without interfering with Django's internal logic:
from django.db import models
class Book(models.Model):
title = models.CharField(max_length=100)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# add your own logic
The two suggested methods in the docs rely on the instance being created in an arbitrary way:
Add a classmethod on the model class:
from django.db import models
class Book(models.Model):
title = models.CharField(max_length=100)
#classmethod
def create(cls, title):
book = cls(title=title)
# do something with the book
return book
book = Book.create("Pride and Prejudice")
Add a method on a custom manager:
class BookManager(models.Manager):
def create_book(self, title):
book = self.create(title=title)
# do something with the book
return book
class Book(models.Model):
title = models.CharField(max_length=100)
objects = BookManager()
book = Book.objects.create_book("Pride and Prejudice")
If that is your case, I would go that way. If not, I would stick to #vartec's answer.