I'm currently providing choices in a dropdown in a model like this:
class FoodType(models.Model):
type = models.CharField(max_length=30, unique=True)
def __unicode__(self):
return self.type
class Food(models.Model):
name = models.CharField(max_length=30, unique=True)
type = models.ForeignKey(FoodType)
def __unicode__(self):
return self.name
I did it like this rather than hardcoded choices because I want to provide an option to add/delete/change FoodTypes via the admin once the app is deployed. But then I realised once that if a FoodType is deleted that a Food is dependent on, the Food is also deleted, which I don't want. I want to be able to keep all Food records unless I explicitly want to delete one.
Is there a better way to do this that still allows the user to modify FoodTypes via the admin?
Thanks :)
You can set the on_delete parameter of the ForeignKey field to a value different than CASCADE (the default value).
Ex:
type = models.ForeignKey(FoodType, null=True, on_delete=models.SET_NULL)
Related
This is a question about how to add a field to a many-to-many relationship in Django.
I have a model LandingPage and a model Product. (Code below). In my project, LandingPages can have many Products listed on them and those same Products can appear on multiple different LandingPages.
Product is connected to LandingPage via a ManyToManyField.
My Goal:
I am trying to figure out how to add a field so that I can set the order (1 through 10) for Products on their associated LandingPages. Reminder, Product instances can appear on multiple LandingPages, so each instance will need to have a different order attribute.
Ideally, I'd like to expose this functionality via the built-in Django admin. Right now it shows the relationships table, but not the order field as it does not yet exist. (Screenshots/mockups below).
My Code:
models.py
class LandingPage(models.Model):
"""Stores a single LandingPage and metadata.
"""
name = models.CharField(max_length=200, help_text="The name is only used internally. It is not visible to the public.")
slug = models.SlugField(default="", editable=False, max_length=150, null=False, verbose_name="Slug", help_text="This is not editable.")
# Additional fields that I do not believe are relevant
class Product(models.Model):
"""Stores a single Product and metadata.
"""
name = models.CharField(max_length=200, help_text="Used internally. Not visible to the public.")
active = models.BooleanField(default=False, verbose_name="Product is Live on Landing Pages", help_text="Determines whether the product should be visible on the assocaited landing page or not.")
landing_page = models.ManyToManyField(
LandingPage,
verbose_name="Landing Page",
help_text="The landing page or pages that this product is assocaited with.",
)
# Additional fields that I do not believe are relevant
admin.py
# Inline configuration used by LandingPageAdmin
class ProductInline(admin.TabularInline):
"""Creates Inline table format for displaying Product data."""
model = Product.landing_page.through
extra = 0
class LandingPageAdmin(admin.ModelAdmin):
"""Specifies LandingPage data in Admin."""
readonly_fields=('slug',)
inlines = [ProductInline]
save_as = True
# Inline configuration used by Product Admin
class LandingPageInline(admin.TabularInline):
"""Creates Inline table format for displaying LandingPage data."""
model = LandingPage.product_set.through
extra = 0
class ProductAdmin(admin.ModelAdmin):
"""Specifies Product data in Admin."""
inlines = [LandingPageInline]
save_as = True
Mockups (for clarity):
Current State
Desired State
(I added the desired functionality in red for clarity. The order integers should be editable so that the order can be re-arranged.)
My Question
How can I accomplish this goal of adding an editable order field to this pre-existing relationship?
Should I manually add an order field to the product-landingpage join table that was automatically created by Django? If I do that, is there a way to have the Django admin show that added field?
Or should I go about it a totally different way?
Thank you in advance!
I found the answer to this.
The solution is create an intermediary model and connect it using "through". Example below:
class Person(models.Model):
name = models.CharField(max_length=128)
def __str__(self):
return self.name
class Group(models.Model):
name = models.CharField(max_length=128)
members = models.ManyToManyField(Person, through='Membership')
def __str__(self):
return self.name
class Membership(models.Model):
person = models.ForeignKey(Person, on_delete=models.CASCADE)
group = models.ForeignKey(Group, on_delete=models.CASCADE)
date_joined = models.DateField()
invite_reason = models.CharField(max_length=64)
Official docs are here: https://docs.djangoproject.com/en/3.1/topics/db/models/#intermediary-manytomany
Others in my situation may find it useful to read this question/answer as it does a good job of explaining various solutions: Define an order for ManyToManyField with Django
I am currently learning Django, and I am finding it a bit difficult wrapping my head around the ManyToMany fields. I am using an intermediate model to manage my relationships.
I have three models; Ticket, User, and TicketUserRelation.
I want to be able to query the ticket model, and retrieve both its corresponding user objects and the ticket object. How would I go about doing this?
In Laravel I would do something along the lines of
Ticket::where('id', '1')->with('contributors')
But I can't really figure out how to do this in Django
The models:
class User(models.Model):
name = models.CharField(max_length=50)
def __str__(self):
return self.name
class Ticket(models.Model):
contributors = models.ManyToManyField(User, through=TicketUserRelation, related_name='tickets')
name = models.CharField(max_length=50)
created_at = models.DateField()
def __str__(self):
return self.name
class TicketUserRelation(models.Model):
id = models.AutoField(primary_key=True, db_column='relation_id')
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
EDIT: I am using an intermediate model so that I can easily add things like join date later.
You don't need the TicketUserRelation model when using Django ORM. You could simply use a ForeignKey in the Ticket model, or use the ManyToManyField already defined, if one ticket can be assigned to multiple users.
class Ticket(models.Model):
# For one user, use ForeignKey
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='tickets')
# For multiple users, use ManyToManyField
contributors = models.ManyToManyField(User, related_name='tickets')
name = models.CharField(max_length=50)
created_at = models.DateField()
def __str__(self):
return self.name
You can then get all tickets for a user u with:
u.tickets.all()
Figured it out myself, using prefetch_related. I was having trouble understanding how prefetch_related works. For those that are confused too, from my understanding it works like this:
Ticket.objects.all().prefetch_related('contributors')
This returns a queryset, something along the lines of this
<QuerySet [<Ticket: Testing ticket one>, <Ticket: Testing ticket two>, <Ticket: Testing ticket three'>, <Ticket: Testing ticket four>]>
When you then access the elements in the queryset, you can then call .contributors on the object, like so:
# Get the queryset
tickets_with_contribs = Ticket.objects.all().prefetch_related('contributors')
# Print the contributors of the first ticket returned
print(tickets_with_contribs[0].contributors)
# Print the contributors of each ticket
for ticket in tickets_with_contribs:
print(ticket.contributors)
Looking back at it this should have been pretty self explanatory, but oh well.
I have a django model called company with a manytomany field where company members are added.
I have another field, called 'company_contact' where I want to be able to choose from one of the company_members as if it was a ForeingKey to company_members.
Is there an easy way of doing this without customized forms, ajax request, django-autocomplete-light, etc?
I intend to fill this model using django admin.
Thanks
class Dm_Company(models.Model):
company_name = models.CharField(max_length=80, blank=True, verbose_name="Razon Social")
company_members = models.ManyToManyField(conf_settings.AUTH_USER_MODEL, verbose_name="Miembros")
#company_contact = models.ForeignKey(conf_settings.AUTH_USER_MODEL, related_name="company_members", on_delete=models.CASCADE)
company_phone = models.CharField(max_length=80, blank=True, verbose_name="Telefono compania")
company_email = models.CharField(max_length=80, blank=True, verbose_name="Email compania")
The one way that I can think of would be to use a ManyToMany with a through model.
class Dm_Company(models.Model):
company_name = models.CharField(max_length=80, blank=True, verbose_name="Razon Social")
company_members = models.ManyToManyField(conf_settings.AUTH_USER_MODEL, through='CompanyMembership')
...
class CompanyMembership(models.Model):
company = models.ForeignKey(Dm_Company)
user = models.ForeignKey(conf_settings.AUTH_USER_MODEL)
is_contact = models.BooleanField(default=False)
The difficulty with this model is that you need to write logic to prevent more than one CompanyMember from being set as is_contact. However, it does structure your data model such that there's no way for the company_contact to reference a user in a different company.
There is no way to filter the company_contact queryset in the way you describe. An alternative is to add the following to your model:
def clean_fields(self, exclude=None):
super().clean_fields(exclude=exclude)
if not self.company_members.exists(id=self.company_contact_id):
raise ValidationError('contact is not member')
That will prevent a contact being selected that is not a member
I am using the default admin dashboard of django. Basically, i want to override the delete_selected method on a model by model basis so I can check for records before allowing the deletion to take place.
My models.py is:
class Kind(models.Model):
name = models.CharField(max_length=50, unique=True)
addedby = models.ForeignKey(User,related_name='admin_kind')
createdon = models.DateTimeField(auto_now_add=True)
updatedon = models.DateTimeField(auto_now=True)
def __str__(self):
return self.name
class Meta:
ordering = ('name',)
class Item(models.Model):
name = models.CharField(max_length=50)
kind = models.ForeignKey(Kind,related_name="item_kind")
createdon = models.DateTimeField(auto_now_add=True)
updatedon = models.DateTimeField(auto_now=True)
def __str__(self):
return ' , '.join([self.kind.name, self.name])
class Meta:
ordering = ('kind', 'name',)
unique_together = (('name','kind'))
Now what I want is before a kind could be deleted, I want to check if there is a related record in items. If there is, do not delete it.
But i am stuck at how to go about overriding delete_selected method in admin.py.
def delete_selected(self, request, obj):
'''
Delete Kind only if there are no items under it.
'''
for o in obj.all():
featuredItems = Item.objects.filter(kind=o).count()
if featuredItems == 0:
o.delete()
However, django shows the warning and when i click yes, it deletes the kind even though there are records for it. I want to absolutely block the deletion.
Actually you are trying to write a lot of code for something that can be done merely by adding an attribute to your model field
PROTECT
Prevent deletion of the referenced object by raising ProtectedError, a subclass of django.db.IntegrityError.
class Item(models.Model):
name=models.CharField(max_length=50)
kind=models.ForeignKey(Kind,related_name="item_kind", on_delete=models.PROTECT)
What you are trying to achieve is made even more difficult by the fact that django displays a confirmation page on delete.
Function code is correct, but you need to explicitly tell django to use your own function to delete that model's objects. You can do that by declaring a list in your admin.py,
actions = ['delete_selected']
Where "delete_selected" is your function name.
So here's the issue i'm having with this Django Model.
I have a foriegn key to class SecretSantaGroup in class assignees called group.
I want to reference this fk in creator and assignee.
pretty much the data I want is like this:
creator = self.group.members
assignee = self.group.members
But I'm having issues on going about it and could use some help.
I want to be able to reference all the users in that specific group, just having trouble going about it.
class SecretSantaGroups(models.Model):
groupName = models.TextField()
members = models.ManyToManyField(User)
def __str__(self):
return self.groupName
class Meta:
verbose_name_plural = 'Secret Santa Groups'
class assignees(models.Model):
group = models.ForeignKey(SecretSantaGroups)
#person that gives gifts
creator = models.ForeignKey(self.group.members, null=True)
#person who receives gift
assignee = models.ForeignKey(self.group.members, null=True)
EDIT
---I used terrible wording, the assignees class is supposed to be who gets who in the group. 1 person gets another in each secret santa group. so gifter and giftee
class assignees(models.Model):
group = models.ForeignKey(SecretSantaGroups)
#person that gives gifts
giver = models.???(self.group.members, null=True)
#person who receives gift
giftee = models.???(self.group.members, null=True)
Unless I'm mistaken, it seems like what you're trying to do is define an association with all possible users for the SecretSantaGroup and then define which of those users is "assigned" or whatever you want to call it.
I also don't know if you want to edit these within Django admin, or part of a different view, but how I would define the model is as such:
class SecretSantaGroup(models.Model):
name = models.CharField(max_length=255)
creator = models.ForeignKey(User, related_name='creator')
members = models.ManyToMany(User, related_name='members')
assigned = models.ManyToMany(User, related_name='assigned', blank=True, null=True)
def __unicode__(self):
return self.name
If you want to limit the choices of "assigned" in Django admin, you'll need to do this in two steps. First, you would need to assign which members, then you'd need to assign the QuerySet of "assigned" to the objects in members so the choices are limited, and then you can assign which ones you want.
This can be done via a custom form. I have NOT tested this code:
class SecretSantaGroupForm(forms.ModelForm):
class Meta:
model = SecretSantaGroup
def __init__(self, *args, **kwargs):
super(SecretSantaGroupForm, self).__init__(*args, **kwargs)
self.fields['assigned'].queryset = self.instance.members.all()
Then you can assign the custom admin form on your model admin.
If you're doing this on the public side, you'll still need the same type of override, and you'll still have to do this in two steps, as best as I can tell. Hope that helps you out.