Two model fields referring to each other - django rest - python

I want to change one of the existing field names in the Django model. But, for the backward-compatibleness, we'd like not to override the existing field with the new one, keep both of them for now. Is there any way to have multiple fields referring to the same database object? i.e
Code right now:
class NetworkPackage:
name = models.CharField(unique=True, blank=False, null=False)
inbound = models.CharField(unique=True, blank=False, null=False)
...
I want to implement:
class NetworkPackage:
name = models.CharField(max_length=32, unique=True, blank=False, null=False)
inbound = models.CharField(max_length=128, unique=True, blank=True)
mobile = models.CharField(max_length=128, unique=True, blank=True)
...
Basically, 'inbound' and 'mobile' should refer to the same field and the request could be sent either with 'inbound' field or 'mobile'.

It's a bad idea having two fields within the same model that hold the same info, especially if you need to enforce uniqueness because
You'll need to maintain parity for both fields, so that means if the request was setting inbound, then you'll also have to set mobile.
The database now has to index both inbound and mobile due to uniqueness.
What you can do is utilize python properties as properties are perfect solutions for cases where you have legacy attributes:
class NetworkPackage(models.Model):
name = models.CharField(unique=True, blank=False, null=False)
inbound = models.CharField(unique=True, blank=False, null=False)
...
#property
def mobile(self):
return self.inbound
#mobile.setter
def mobile(self, value):
self.inbound = value
Then in your serializer, you need to:
Add mobile as an additional field sourcing to inbound.
Override the required and allow_blank arguments on both fields since the serializer can allow either fields...
BUT, you'll then need to write a custom validation method to ensure at least 1 of the 2 fields are populated with a value.
Also prioritize the inbound value over the mobile value if both fields are populated.
class NetworkPackageSerializer(serializers.ModelSerializer):
inbound = serializers.CharField(required=False, allow_blank=True)
mobile = serializers.CharField(source="inbound", required=False, allow_blank=True)
class Meta:
model = NetworkPackage
fields = ("inbound", "mobile", ...)
def validate(self, data):
"""Validate `inbound` and/or `mobile`."""
if not data["inbound"] and not data["mobile"]:
raise serializers.ValidationError("missing value on inbound or mobile")
if data["inbound"]:
del data["mobile"]
else:
del data["inbound"]
return data

Not sure why do you make duplicate fields, but I have some suggestions for you.
1. Custom property
class NetworkPackage:
name = models.CharField(unique=True, blank=False, null=False)
inbound = models.CharField(unique=True, blank=False, null=False)
#poperty
def mobile(self):
return self.inbound
2. Serializer
class NetworkPackageSerializer(serializers.Serializer):
mobile = serializers.CharField(source='inbound')
class Meta:
model = NetworkPackage
fields = (
'id',
'inbound',
'mobile',
'name',
...
)

Related

Django OneToOneField allow online one reference

I am trying to create a one to one reference and want to make sure that that reference is not allowed to be used for another model or instance.
For example
Say I have an address model, Person Model and Company Model
Person has a OneToOneField field to Address
Company also has a OneToOneField field to Address
address=Address(data="some address")
company=Company(name="some company",address=address)
person=Person(name="my name",address=address)
Models:
class Address(models.Model):
data = models.CharField(max_length=255, blank=True, null=True)
class Company(models.Model):
name = models.CharField(max_length=255, blank=True, null=True)
address=models.OneToOneField(Address,on_delete=models.CASCADE)
class Person(models.Model):
name = models.CharField(max_length=255, blank=True, null=True)
address=models.OneToOneField(Address,on_delete=models.CASCADE)
I would like the system to throw an error on this since I am setting the same address to 2 different models.
Also this would delete both person and company if I delete address.
Usually you catch this with code and not make a stupid mistake like this.
But can system catch it since it is one to one ?
In the case of the deletion you could use on_delete=models.PROTECT. In the other case you could add unique=True so a person id = 1 will have a address id = 1, a person id = 2 can't have a address id = 1 anymore. But it would only solve for one model:
address=models.ForeignKey(Address, unique=True, on_delete=models.PROTECT)
A new approach would be create a model to reference the address of both company and person and be able to forbid the creation with the same address id:
class AddressExample(models.Model):
id_address = models.ForeignKey(Address, unique=True,on_delete=models.PROTECT)
id_person = models.ForeignKey(Person, blank=True, null=True, unique=True, on_delete=models.PROTECT)
id_company = models.ForeignKey(Person, blank=True, null=True, unique=True, on_delete=models.PROTECT)
Note that I used blank=True, null=True so you can create an instance only with a Person or a Company, without the need to create a instance with both. There is a Meta to use combination of primary keys too.
class AddressExample(models.Model):
id_address = models.ForeignKey(Address, unique=True,on_delete=models.PROTECT)
id_person = models.ForeignKey(Person, blank=True, null=True, unique=True, on_delete=models.PROTECT)
id_company = models.ForeignKey(Person, blank=True, null=True, unique=True, on_delete=models.PROTECT)
class Meta:
unique_togther = ('id_address', 'id_person', 'id_company')
# Not sure if it will throw a error here because `id_person` and `id_company` can be blank
# or null. But the use of `unique together` is for cases where you want to guarantee
# the combination of the primary keys will be unique.
Hope it helps.

Adding additional attributes to a Django field

Consider this Family model in Django:
class Family(models.Model):
EMPLOYEE = 'Employee'
PARTNER = 'Partner'
BIRTH_PARENT_CHOICES = (
(EMPLOYEE, EMPLOYEE),
(PARTNER, PARTNER),
)
employee_user = models.OneToOneField(User, blank=True, null=True, related_name='employee_family')
partner_user = models.OneToOneField(User, blank=True, null=True, related_name='partner_family')
employee_first_name = models.CharField(max_length=255, blank=True)
employee_last_name = models.CharField(max_length=255, blank=True)
employee_email = models.CharField(max_length=255, blank=True)
employee_phone = models.CharField(max_length=255, blank=True)
partner_first_name = models.CharField(max_length=255, blank=True)
partner_last_name = models.CharField(max_length=255, blank=True)
partner_email = models.CharField(max_length=255, blank=True)
partner_phone = models.CharField(max_length=255, blank=True)
point_of_contact = models.CharField(max_length=255, choices=BIRTH_PARENT_CHOICES)
A Family consists of an employee and a partner, both of which have various attributes (user, first name, last name, email, phone). There is also a point_of_contact field which is either 'Employee' or 'Partner'.
What I'd like to be able to do is to, on an instance family of Family, do something like
family.point_of_contact.phone_number
which would resolve to family.employee_phone_number if family.point_of_contact == Family.EMPLOYEE and family.partner_phone_number otherwise, and similarly for first_name, last_name, etc.
As far as I can tell from https://docs.djangoproject.com/en/2.0/ref/models/fields/, however, it isn't possible to define additional attributes on Django fields. Is there some other way I could do this?
No, in order to do that, you would need to create a separate model Contact and join to it from Family using a OneToOneField if there can only be one contact per family, or using a ForeignKey in your Contact model if there can be more than one contact per family.
Django doesn't provide a way to do this, but you can do it with some simple Python:
from types import SimpleNamespace
class Family(SimpleNamespace):
EMPLOYEE = 'employee'
PARTNER = 'partner'
#property
def contact(self):
return SimpleNamespace(**{
attr: getattr(self, '%s_%s' % (self.point_of_contact, attr))
for attr in 'first_name last_name'.split()
})
family = Family(
employee_first_name='Kurt',
employee_last_name='Peek',
partner_first_name='Jane',
partner_last_name='Doe',
point_of_contact=Family.EMPLOYEE,
)
print(family.contact.first_name)
print(family.contact.last_name)
Here SimpleNamespace is used in two ways:
As a superclass of Family to make this example easy to test - skip that and stick to models.Model.
In the contact property, keep that.

No exception raised when saving an empty field with null=False - Django

I have the following model:
class User(models.Model):
email = models.EmailField(max_length=254, null=False, unique=True)
referral_code = models.CharField(max_length=10, null=False, unique=True)
And used the Django shell to save a user instance with referral_code undefined:
u = User(email="test#example.com")
u.save()
This did not raise an exception. My understanding was that null=False would require referral_code to be set - Is this not the case? How would I achieve that behaviour?
update
I notice that the field is set to u.referral_code='', so given the uniqueness constraint, this has raised an exception when I tried to repeat the process with a new instance. I would rather the exception was thrown because I did not set the field...
The value of your referral_code is not null, it is '' (blank string). This is the default of CharField.
Updated along with question:
You can raise an error before the data is stored in the database by overriding save on the model
class User(models.Model):
email = models.EmailField(max_length=254, null=False, unique=True)
referral_code = models.CharField(max_length=10, null=False, unique=True)
def save(self, *args, **kwargs):
assert self.email, "The 'email' field must be populated."
super().save(*args, **kwargs)
It should be noted that this is not to be preferred over form validation where possible.
Update now that newer features of Django are available:
Django's Constraints allow you to add custom database constraints:
class User(models.Model):
email = models.EmailField(max_length=254, null=False, unique=True)
referral_code = models.CharField(max_length=10, null=False, unique=True)
class Meta:
constraints = [
models.CheckConstraint(
check=(~models.Q(referral_code='')),
name='referral_code_populated',
)
]

Django: Parent Model with multiple child model types

I've created a set of Django model for a CMS to show a series of Products.
Each page contains a series of rows, so I have a generic
class ProductRow(models.Model):
slug = models.SlugField(max_length=100, null=False, blank=False, unique=True, primary_key=True)
name = models.CharField(max_length=200,null=False,blank=False,unique=True)
active = models.BooleanField(default=True, null=False, blank=False)
then I have a series of children of this model, for different types of row:
class ProductBanner(ProductRow):
wide_image = models.ImageField(upload_to='product_images/banners/', max_length=100, null=False, blank=False)
top_heading_text = models.CharField(max_length=100, null=False, blank=False)
main_heading_text = models.CharField(max_length=200, null=False, blank=False)
...
class ProductMagazineRow(ProductRow):
title = models.CharField(max_length=50, null=False, blank=False)
show_descriptions = models.BooleanField(null=False, blank=False, default=False)
panel_1_product = models.ForeignKey(Product, related_name='+', null=False, blank=False)
panel_2_product = models.ForeignKey(Product, related_name='+', null=False, blank=False)
panel_3_product = models.ForeignKey(Product, related_name='+', null=False, blank=False)
...
class ProductTextGridRow(ProductRow):
title = models.CharField(max_length=50, null=False, blank=False)
col1_title = models.CharField(max_length=50, null=False, blank=False)
col1_product_1 = models.ForeignKey(Product, related_name='+', null=False, blank=False)
col1_product_2 = models.ForeignKey(Product, related_name='+', null=False, blank=False)
col1_product_3 = models.ForeignKey(Product, related_name='+', null=False, blank=False)
...
and so on.
Then in my ProductPage I have a series of ProductRows:
class ProductPage(models.Model):
slug = models.SlugField(max_length=100, null=False, blank=False, unique=True, primary_key=True)
name = models.CharField(max_length=200, null=False, blank=False, unique=True)
title = models.CharField(max_length=80, null=False, blank=False)
description = models.CharField(max_length=80, null=False, blank=False)
row_1 = models.ForeignKey(ProductRow, related_name='+', null=False, blank=False)
row_2 = models.ForeignKey(ProductRow, related_name='+', null=True, blank=True)
row_3 = models.ForeignKey(ProductRow, related_name='+', null=True, blank=True)
row_4 = models.ForeignKey(ProductRow, related_name='+', null=True, blank=True)
row_5 = models.ForeignKey(ProductRow, related_name='+', null=True, blank=True)
The problem I have got, is that I want to allow those 5 rows in the ProductPage to be any of the different child types of ProductRow. However when I iterate over them such as
in views.py:
product_page_rows = [product_page.row_1,product_page.row_2,product_page.row_3,product_page.row_4,product_page.row_5]
and then in the template:
{% for row in product_page_rows %}
<pre>{{ row.XXXX }}</pre>
{% endfor %}
I cannot reference any child field as XXXX.
I tried adding a "type()" method to both the parent and children, to try and distinguish which class each row is:
class ProductRow(models.Model):
...
#classmethod
def type(cls):
return "generic"
and
class ProductTextGridRow(TourRow):
...
#classmethod
def type(cls):
return "text-grid"
but if i change XXXX for .type() in the template then it shows "generic" for every item in the list (I had defined a variety of row types in the data), so I guess everything is coming back as a ProductRow rather than the appropriate child type. I can find no way to get the children to be accessible as the correct child type rather than the parent type, or to determine which child type they actually are (I tried catching AttributeError as well, that didn't help).
Can someone advise how I can properly handle a list of varied model types all of which contain a common parent, and be able to access the fields of the appropriate child model type?
This is generally (read "always") a bad design to have something like this:
class MyModel(models.Model):
...
row_1 = models.ForeignKey(...)
row_2 = models.ForeignKey(...)
row_3 = models.ForeignKey(...)
row_4 = models.ForeignKey(...)
row_5 = models.ForeignKey(...)
It is not scalable. If ever you want to allow 6 rows or 4 rows instead of 5, one day (who knows?), you will have to add/delete a new row and alter your database scheme (and handle existing objects that had 5 rows). And it's not DRY, your amount of code depends on the number of rows you handle and it involves a lot of copy-pasting.
This become clear that it is a bad design if you wonder how you would do it if you had to handle 100 rows instead of 5.
You have to use a ManyToManyField() and some custom logic to ensure there is at least one row, and at most five rows.
class ProductPage(models.Model):
...
rows = models.ManyToManyField(ProductRow)
If you want your rows to be ordered, you can use an explicit intermediate model like this:
class ProductPageRow(models.Model):
class Meta:
order_with_respect_to = 'page'
row = models.ForeignKey(ProductRow)
page = models.ForeignKey(ProductPage)
class ProductPage(models.Model):
...
rows = model.ManyToManyField(ProductRow, through=ProductPageRow)
I want to allow only N rows (let's say 5), you could implement your own order_with_respect_to logic:
from django.core.validators import MaxValueValidator
class ProductPageRow(models.Model):
class Meta:
unique_together = ('row', 'page', 'ordering')
MAX_ROWS = 5
row = models.ForeignKey(ProductRow)
page = models.ForeignKey(ProductPage)
ordering = models.PositiveSmallIntegerField(
validators=[
MaxValueValidator(MAX_ROWS - 1),
],
)
The tuple ('row', 'page', 'ordering') uniqueness being enforced, and ordering being limited to five values (from 0 to 4), there can't be more than 5 occurrences of the couple ('row', 'page').
However, unless you have a very good reason to make 100% sure that there is no way to add more than N rows in the database by any mean (including direct SQL query input on your DBMS console), there is no need to "lock" it a this level.
It is very likely that all "untrusted" user will only be able to update your database through HTML form inputs. And you can use formsets to force both a minimum and a maximum number of rows when filling a form.
Note: This also applies to your other models. Any bunch of fields named
foobar_N, where N is an incrementing integer, betrays a very bad
database design.
Yet, this does not fix your issue.
The easiest (read "the first that comes to mind") way to get your child model instance back from the parent model instance is to loop over each possible child model until you get an instance that matches.
class ProductRow(models.Model):
...
def get_actual_instance(self):
if type(self) != ProductRow:
# If it's not a ProductRow, its a child
return self
attr_name = '{}_ptr'.format(ProductRow._meta.model_name)
for possible_class in self.__subclasses__():
field_name = possible_class._meta.get_field(attr_name).related_query_name()
try:
return getattr(self, field_name)
except possible_class.DoesNotExist:
pass
# If no child found, it was a ProductRow
return self
But it involves to hit the database for each try. And it is still not very DRY. The most efficient way to get it is to add a field that will tell you the type of the child:
from django.contrib.contenttypes.models import ContentType
class ProductRow(models.Model):
...
actual_type = models.ForeignKey(ContentType, editable=False)
def save(self, *args, **kwargs):
if self._state.adding:
self.actual_type = ContentType.objects.get_for_model(type(self))
super().save(*args, **kwargs)
def get_actual_instance(self):
my_info = (self._meta.app_label, self._meta.model_name)
actual_info = (self.actual_type.app_label, self.actual_type.model)
if type(self) != ProductRow or my_info == actual_info:
# If this is already the actual instance
return self
# Otherwise
attr_name = '{}_ptr_id'.format(ProductRow._meta.model_name)
return self.actual_type.get_object_for_this_type(**{
attr_name: self.pk,
})
Your type() method doesn't work because you're using multi-table inheritance: each of ProductRow's children is a separate model connected to ProductRow using an automatically generated OneToOneField.
If you ensure that each ProductRow instance has only one type of child (out of the three possible ones), there is a simple way to find out whether that child is a ProductBanner, a ProductMagazineRow or a ProductTextGridRow, and then to use the appropriate fields:
class ProductRow(models.Model):
...
def get_type(self):
try:
self.productbanner
return 'product-banner'
except ProductBanner.DoesNotExist:
pass
try:
self.productmagazinerow
return 'product-magazine'
except ProductMagazineRow.DoesNotExist:
pass
try:
self.producttextgridrow
return 'product-text-grid'
except ProductTextGridRow.DoesNotExist:
pass
return 'generic'
However, if you don't enforce otherwise, then one instance of ProductRow can be linked to more than one of ProductBanner, ProductMagazineRow and ProductTextGridRow at the same time. You'll have to work with the specific instances instead:
class ProductRow(models.Model):
...
def get_productbanner(self):
try:
return self.productbanner
except ProductBanner.DoesNotExist:
return None
def get_productmagazinerow(self):
try:
return self.productmagazinerow
except ProductMagazineRow.DoesNotExist:
return None
def get_producttextgridrow(self)
try:
return self.producttextgridrow
except ProductTextGridRow.DoesNotExist:
return None
Combine this with with Antonio Pinsard's answer to improve your database design.

Issues with intermediary many to many field in Django

I am inserting data in models which are related with other models via Many-to-Many relations. I was trying to use add() to add values in many-to-many filed but that doesn't work and raise this error Cannot set values on a ManyToManyField which specifies an intermediary model.
I read the docs and found out that we cannot use set(), add(), create() for intermediary model.
Here are my models :
class Venue(MPTTModel):
organisation = models.ForeignKey(Organisation, verbose_name=_('organisation'))
name = models.CharField(_('name'), max_length=100)
description = models.TextField(_('description'), null=True, blank=True)
address = models.ForeignKey(Address, verbose_name=_('address'))
org_users = models.ManyToManyField(PortalUser, through='OrgMapping', verbose_name=_('org users'))
modified_at = models.DateTimeField(_('modified at'), auto_now=True)
class OrgMapping(models.Model):
host_group = models.ForeignKey(OrgHostGroups, verbose_name=_('host group'))
org_user = models.ForeignKey(PortalUser, verbose_name=_('org user'))
venue = models.ForeignKey(Venue, verbose_name=_('venue'))
org_roles = models.ManyToManyField(OrgRole, verbose_name=_('org roles'))
org_permissions = models.ManyToManyField(OrgPermission, verbose_name=_('org permissions'), blank=True)
created_at = models.DateTimeField(_('created at'), auto_now_add=True)
modified_at = models.DateTimeField(_('modified at'), auto_now=True)
is_deleted = models.BooleanField(_('is deleted'), default=False)
def create_org_venue(org, name, desc, address, org_users):
"""
org_users must be a list or portal users pk's
"""
parent = get_or_none(Venue, organisation__name='TeraMatrix')
venue = Venue(organisation=org,
name=name,
description='Head Office',
address=address)
venue.save()
# save org_users to above venue
for user in org_users:
# venue.org_users.add(user)
venue.org_users = get_or_none(PortalUser,pk=user)
venue.save()
return venue
Now If I'll try to craete venue using create_org_venue it will raise error. Any idea how I can do this
Edit :-
class OrgHostGroups(MPTTModel):
organisation = models.ForeignKey(Organisation, verbose_name=_('organisation'))
name = models.CharField(_('name'), max_length=100)
description = models.TextField(_('description'), null=True, blank=True)
org_users = models.ManyToManyField(PortalUser, through='OrgMapping', verbose_name=_('org users'))
venues = models.ManyToManyField(Venue, through='OrgMapping', verbose_name=_('venues'))
As you can see Venue model mapped through orgmapping and which further need orghostgroup and it needs venues and mapping again. So we can say that a circular relationship.
It's not possible to use add() when using an intermediary model, because that wouldn't let you specify values for the extra fields on the intermediary model.
Instead, just create an instance of the intermediary model.
OrgMapping.objects.create(
venue=venue,
org_user=user,
# set all the other required OrgMapping fields here
)
See the docs on extra fields on many-to-many relationships for more info.

Categories