Issues with intermediary many to many field in Django - python

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.

Related

Two model fields referring to each other - django rest

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',
...
)

Django: Accessing extra field in ManytoMany (through=)

I am trying to access the purchaser field in my ManytoMany field. I used through= to add some extra fields. However, it seems I am only able to access the event object, not the extra fields. Can someone explain to me why?
for selected_order in Order.objects.all():
contact_exists = Contact.objects.filter(
email=selected_order.email,
event_related_fields=selected_order.event,
)
if contact_exists:
contact = contact_exists.first()
for x in contact.event_related_fields.all():
print(x.purchaser)
models.py
class Contact(TimeStampedModel):
consent = models.BooleanField(verbose_name=_("Consent"))
email = models.EmailField(verbose_name=_("Your email"))
first_name = models.CharField(
max_length=100, # TODO Length must be same as for billing model
verbose_name=_("First name"),
null=True,
blank=True,
)
last_name = models.CharField(
max_length=100, # TODO Length must be same as for billing model
verbose_name=_("Last name"),
null=True,
blank=True,
)
events = models.ManyToManyField(Event, related_name='contacts')
event_related_fields = models.ManyToManyField(
Event, related_name='event_related_fields', through='EventRelatedFields'
)
organizer = models.ForeignKey(
Organizer, on_delete=models.PROTECT, related_name='contacts'
) # PROTECT = don't allow to delete the organizer if contact exists
class Meta:
verbose_name = _("Contact")
verbose_name_plural = _("Contacts")
ordering = ('created',)
unique_together = ('email', 'organizer')
def __repr__(self):
return "{}: {}".format(self.__class__.__name__, self)
def __str__(self):
return self.email
class EventRelatedFields(TimeStampedModel):
event = models.ForeignKey(Event, on_delete=models.CASCADE)
contact = models.ForeignKey(Contact, on_delete=models.CASCADE)
lead = models.BooleanField(
verbose_name='Lead', default=False
) # Contact who 'Signed Up'
attendee = models.BooleanField(
verbose_name='Attendee', default=False
) # Contact assigned to ticket
purchaser = models.BooleanField(
verbose_name='Purchaser', default=False
) # Contact made the order
class Meta:
unique_together = [['event', 'contact']]
You are defining two many-to-many relationships for some reason, and you've called one of them event_related_fields, with the same related name. As a result you are confusing that with the through table. But since the through table is actually calledEventRelatedFields, you would access its related objects as eventrelatedfields_set.
You should only have one m2m, events:
events = models.ManyToManyField(Event, through='EventRelatedFields', related_name='contacts')
and your class EventRelatedFields should itself set related names:
event = models.ForeignKey(Event, related_name='event_related_fields', on_delete=models.CASCADE)
contact = models.ForeignKey(Contact, related_name='event_related_fields, on_delete=models.CASCADE)
Now you can do:
for x in contact.event_related_fields.all():
print(x.purchaser)

Django Models, on delete set pk

This is my models:
class Customer(models.Model):
nome = models.CharField(max_length=40)
indirizzo = models.CharField(max_length=40)
class Appo(models.Model):
appo = models.CharField(max_length=40)
customer = models.ForeignKey(Customer, on_delete=models.SET_NULL, null=True, blank=True, default=1,related_name='appocli')
now, if I delete a Customer I need SQL set in Appo models a specific PK for customer Foreign Key. For example 1.
Something like: on delete set 1
Please help
You can use SET_DEFAULT to set your foreign key to a default value
customer = models.ForeignKey(Customer, on_delete=models.SET_DEFAULT, default=1)

How to expose some specific fields of model_b based on a field of model_a?

I want to create a ModelForm which gonna show some specific field of ControlInstruction if device_type of Device is equals DC. Otherwise show all fields.
Suppose,
if device type == 'DC':
show these filed in form-> on_off_flag, speed_flag, direction_flag
else:
show all
How can I do that?
class Device(models.Model):
DEVICE_TYPES = (
('AC', 'AC MOTOR'),
('DC', 'DC MOTOR'),
)
user = models.ForeignKey(User, on_delete=models.CASCADE)
device_id = models.CharField(max_length=64, unique=True, blank=False)
device_name = models.CharField(max_length=100, blank=False)
device_model = models.CharField(max_length=10)
device_type = models.CharField(max_length=2, choices=DEVICE_TYPES, blank=False)
location = models.CharField(max_length=150)
def __str__(self):
return self.device_name
class ControlInstruction(models.Model):
DIRECTION_CHOICES = (
('FW', 'Forward'),
('BW', 'Backward'),
)
# OneToOneField is is similar to a ForeignKey with unique=True, but the “reverse”
# side of the relation will directly return a single object.
device = models.OneToOneField(Device, on_delete=models.CASCADE, primary_key=True)
on_off_flag = models.BooleanField(default=False)
voltage_flag = models.FloatField(max_length=20, default=0)
current_flag = models.FloatField(max_length=20, default=0)
speed_flag = models.IntegerField(default=0, validators=[MinValueValidator(0), MaxValueValidator(100)])
direction_flag = models.CharField(max_length=2, choices=DIRECTION_CHOICES, default='FW')
frequency_flag = models.IntegerField(default=0)
I would recommend creating two forms, one including only the fields for a DC device, and one form with all of the fields. Then in your view, choose which form to use based on the device_type.
class DeviceForm(forms.ModelForm):
class Meta:
model = Device
fields = "__all__"
def __init__(self,*args,**kwargs):
super().__init__(*args, **kwargs)
if self.instance.pk:
if self.instance.device_type != "DC":
del self.fields["on_off_flag"]
del self.fields["speed_flag"]
del self.fields["direction_flag"]
But I dont recommended since you will find that this approach is very limited

Django ORM: Models with 2 table referencing each other

I have 2 tables. User and Group. 1:Many relationship. Each user can only belong to a single group.
here's the model.py.
class Group(models.Model):
group_name = models.CharField(max_length=150, blank=True, null=True)
group_description = models.TextField(blank=True, null=True)
group_creator = models.ForeignKey(User, models.DO_NOTHING)
class User(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
...
group = models.ForeignKey(Group, models.DO_NOTHING)
The issue I have is that they are both referencing each other which is acceptable in MySQL and Oracle, but, I get an error when migrating:
group_creator = models.ForeignKey(User, models.DO_NOTHING)
NameError: name 'User' is not defined
Now when I reverse the order (so, User first than Group), I get
group = models.ForeignKey(Group, models.DO_NOTHING, blank=True, null=True)
NameError: name 'Group' is not defined
This is getting quite frustrating. I have a few work around (make it a many:many and keep creator on Group class), but before I start destroying my datamodel and move data move all the data around, I wonder if anyone has this issue before. How did you solve this? Do you really have to change your datamodel?
as Pourfar mentioned in a comment, you may avoid the NameError via the quoting the model object as string. also it is safe to set related_name for accessing this relation.
class Group(models.Model):
...
group_creator = models.ForeignKey('User', related_name='creator_set')
and then, with your constraint,
Each user can only belong to a single group.
in that case, OneToOneField is more appropriate.
class User(models.Model):
...
group = models.OneToOneField(Group)
then you can access the relations as follows:
# USER is a User object
GROUP_BELONGED = USER.group # access to 1-1 relation
GROUP_CREATED = USER.creator_set.all() # reverse access to foreignkey relation
# now GROUP_BELONGED is a Group object
CREATOR = GROUP_BELONGED.group_creator # access to foreignkey relation
Add related_name to your ForeignKey fields:
class Group(models.Model):
group_name = models.CharField(max_length=150, blank=True, null=True)
group_description = models.TextField(blank=True, null=True)
group_creator = models.ForeignKey('User',related_name='myUser')
class User(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
group = models.ForeignKey('Group', related_name='MyGroup')

Categories