Django Models: Reference the same field twice in a fk model - django

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.

Related

Retrieving all database objects and its related objects 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.

Django: How to filter foreign key in admin detail view

core/models.py
from django.db import models
from django.db.models.signals import post_save
class Person(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
middle_name = models.CharField(max_length=30)
class Company(models.Model):
name = models.CharField(max_length=100)
class Entity(models.Model):
is_person = models.BooleanField(default=True)
person = models.ForeignKey(Person, on_delete=models.PROTECT, null=True)
company = models.ForeignKey(Company, on_delete=models.PROTECT, null=True)
name = models.CharField(max_length=30)
def __str__(self):
return self.name
#property
def title(self):
return self.name
class Meta:
verbose_name_plural = 'entities'
def post_save_person_receiver(sender, instance, created, *args, **kwargs):
if created:
entity, is_created = Entity.objects.get_or_create(is_person=True, person=instance, company=None, name=instance.last_name) # noqa
post_save.connect(post_save_person_receiver, sender=Person)
def post_save_company_receiver(sender, instance, created, *args, **kwargs):
if created:
entity, is_created = Entity.objects.get_or_create(is_person=False, person=None, company=instance, name=instance.short_name) # noqa
post_save.connect(post_save_company_receiver, sender=Company)
class Group(models.Model):
name = models.CharField(max_length=20)
is_individual = models.BooleanField(default=True)
members = models.ManyToManyField(Entity, through='Membership')
class Membership(models.Model):
group = models.ForeignKey(Group, on_delete=models.PROTECT, null=False)
entity = models.ForeignKey(Entity, on_delete=models.PROTECT, null=False)
class Meta:
unique_together = ("entity", "group")
For every Company and Person created, an Entity is automatically created where Entity.is_person=True if it's a Person. An Entity can then become a member of a Group such as 'Employee', 'Supplier' and 'Customer' through a ManyToMany relationship in the Membership Model.
How do I filter Membership.entity in Admin View (for add and update) that when the Group selected is an 'is_individual=True', such as 'Employee', Entity Field only shows 'is_person=True' Persons in the Entity combobox?
Admin View
I would consider writing your own view for this. In my opinion, Djangos admin is not a good site to base a frontend on. I use it only for making quick changes where validation doesn't need to be done as much because the users of admin should know what they're doing. When you start adding more responsive stuff, that's when I consider using another view and starting from scratch (or a framework) but a different view none-the-less.
There are several different names for what it sounds like you want: dependent dropdowns, chained fields, etc. Django itself doesn't have anything out of the box for this.
Your 2 options are: (1) do it yourself or (2) use a 3rd-party package.
As far as doing it yourself goes, you're going to need to do some work in JS for making it work on the frontend, and you're probably going to need some kind of flexible view that outputs JSON data, and you're going to need some kind of custom select field to handle this.
For using a 3rd-party package, I don't see anything particularly recent that does this. Here's one that's a few years old: https://github.com/runekaagaard/django-admin-flexselect
Hope this helps some!

override delete_selected to check for records that exist in other tables in django 1.10

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.

Django drop downs in model

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)

M2M using through and form with multiple checkboxes

I'd like to create a form allowing me to assign services to supplier from these models. There is no M2M relationship defined since I use a DB used by others program, so it seems not possible to change it. I might be wrong with that too.
class Service(models.Model):
name = models.CharField(max_length=30L, blank=True)
class ServiceUser(models.Model):
service = models.ForeignKey(Service, null=False, blank=False)
contact = models.ForeignKey(Contact, null=False, blank=False)
class SupplierPrice(models.Model):
service_user = models.ForeignKey('ServiceUser')
price_type = models.IntegerField(choices=PRICE_TYPES)
price = models.DecimalField(max_digits=10, decimal_places=4)
I've created this form:
class SupplierServiceForm(ModelForm):
class Meta:
services = ModelMultipleChoiceField(queryset=Service.objects.all())
model = ServiceUser
widgets = {
'service': CheckboxSelectMultiple(),
'contact': HiddenInput(),
}
Here is the view I started to work on without any success:
class SupplierServiceUpdateView(FormActionMixin, TemplateView):
def get_context_data(self, **kwargs):
supplier = Contact.objects.get(pk=self.kwargs.get('pk'))
service_user = ServiceUser.objects.filter(contact=supplier)
form = SupplierServiceForm(instance=service_user)
return {'form': form}
I have the feeling that something is wrong in the way I'm trying to do it. I have a correct form displayed but it is not instantiated with the contact and checkboxes aren't checked even if a supplier has already some entries in service_user.
You are defining services inside your Meta class. Put it outside, right after the beginning of SupplierServiceForm. At the very least it should show up then.
Edit:
I misunderstood your objective. It seems you want to show a multiple select for a field that can only have 1 value. Your service field will not be able to store the multiple services.
So, by definition, your ServiceUser can have only one Service.
If you don't want to modify the database because of other apps using it, you can create another field with a many to many relationship to Service. That could cause conflicts with other parts of your apps using the old field, but without modifying the relationship i don't see another way.
The solution to my problem was indeed to redefine my models in oder to integrate the m2m relationship that was missing, using the through argument. Then I had to adapt a form with a special init method to have all selected services displayed in checkboxes, and a special save() method to save the form using m2m relationship.
class Supplier(Contact):
services = models.ManyToManyField('Service', through='SupplierPrice')
class Service(models.Model):
name = models.CharField(max_length=30L, blank=True)
class ServiceUser(models.Model):
service = models.ForeignKey(Service, null=False, blank=False)
supplier = models.ForeignKey(Supplier, null=False, blank=False)
price = models.Decimal(max_digits=10, decimal_places=2, default=0)
And the form, adapted from the very famous post about toppings and pizza stuff.
class SupplierServiceForm(ModelForm):
class Meta:
model = Supplier
fields = ('services',)
widgets = {
'services': CheckboxSelectMultiple(),
'contact_ptr_id': HiddenInput(),
}
services = ModelMultipleChoiceField(queryset=Service.objects.all(), required=False)
def __init__(self, *args, **kwargs):
# Here kwargs should contain an instance of Supplier
if 'instance' in kwargs:
# We get the 'initial' keyword argument or initialize it
# as a dict if it didn't exist.
initial = kwargs.setdefault('initial', {})
# The widget for a ModelMultipleChoiceField expects
# a list of primary key for the selected data (checked boxes).
initial['services'] = [s.pk for s in kwargs['instance'].services.all()]
ModelForm.__init__(self, *args, **kwargs)
def save(self, commit=True):
supplier = ModelForm.save(self, False)
# Prepare a 'save_m2m' method for the form,
def save_m2m():
new_services = self.cleaned_data['services']
old_services = supplier.services.all()
for service in old_services:
if service not in new_services:
service.delete()
for service in new_services:
if service not in old_services:
SupplierPrice.objects.create(supplier=supplier, service=service)
self.save_m2m = save_m2m
# Do we need to save all changes now?
if commit:
self.save_m2m()
return supplier
This changed my first models and will make a mess in my old DB but at least it works.