How to make two manytomany fields in a model hold different values - django

I have Two simplified models as bellow:
class Courses:
name = models.CharField(_("Course name"), max_length=256)
class School:
main_courses = models.ManyToManyField(_("Main Courses"), to="course.Courses", related_name="maincourses", blank=True)
enhancement_courses = models.ManyToManyField(_("Enhancement Courses"), to="course.Courses" related_name="enhancementcourses", blank=True)
def clean(self) -> None:
#check if selected items in main_courses and enhancement_courses are not equal
status = [True for course in self.main_courses.all() if course in self.enhancement_courses.all()]
if any(status):
raise ValidationError("Chosen contents should not be equal")
return None
main_courses and enhancement_courses are going to hold a list of Courses. But I need to make sure their values wont be equal. For example if in school_1, main_courses are math, physics then enhancement_courses can't be these values. What is the simplest way in django to do this?
Update
I need to validate when user select items in the fields main_courses and enhancement_courses in School table, after saving the changes, model should verify the chosen items in those two fields are not equal. At the moment, there is a bug in clean() method that it keeps the values of first validation/save. For example it raises error if main_courses and enhancement_courseschosen items are equal, but after deselecting, again it raises error.

You can utilize ManyToMany.limit_choices_to to constrain only certain items for each field:
class CourseCategory(models.TextChoices):
MAIN = "main", "Main"
ENHANCHEMENT = "enhancement", "Enhancement"
class Course(models.Model):
name = models.CharField(_("Course name"), max_length=256)
category = models.CharField(_("Course Category"), max_length=32, choices=CourseCategory.choices)
class School(models.Model):
main_courses = models.ManyToManyField(_("Main Courses"), to="course.Courses", related_name="maincourses", blank=True, limit_choices_to=Q(category=CourseCategory.MAIN))
enhancement_courses = models.ManyToManyField(_("Enhancement Courses"), to="course.Courses" related_name="enhancementcourses", blank=True, limit_choices_to=Q(category=CourseCategory.ENHANCEMENT))

Related

Filter objects from a table based on ManyToMany field

I am working on a messaging app.
The models are as follows.
class ChatRoom(SafeDeleteModel):
_safedelete_policy = SOFT_DELETE_CASCADE
room_name = models.CharField(max_length=100, null=True, blank=True)
participants = models.ManyToManyField(User)
class Meta:
db_table = TABLE_PREFIX + "chat_room"
class Message(SafeDeleteModel):
_safedelete_policy = SOFT_DELETE_CASCADE
chat_room = models.ForeignKey(ChatRoom, on_delete=models.CASCADE)
message = models.TextField()
sent_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_on = models.DateTimeField(default=django.utils.timezone.now)
class Meta:
db_table = TABLE_PREFIX + "chat_message"
I have to filter ChatRoom object which contains a list of users.
Eg: On sending data like this to the api
{
"participants": [2,3]
}
It should return a ChatRoom object which contains both user '2' and user '3' as participants.
I had tried filtering it with
room = ChatRoom.objects.filter(participants__in=serializer.validated_data['participants'])
but it returns a list of room objects with user '1' and user '2'.
I had also tried using get() but it returns an error.
What am I doing wrong and how can i do it right?
Thanks for looking into my problem. Hope you have a solution.
You said you are using Query..
ChatRoom.objects.filter(participants__in=serializer.validated_data['participants'])
filter function returns a list of objects of the model matching the conditions.
get function only return a single object of model and if none or more than one item matches the condition then an exception is thrown. That is why get is only used for candidate keys or unique resultant conditions.
In your case, I suppose both participants can be part of many rooms, also there might be a possibility that there is no such room. if you want only one queryset to be returned you can use this.
room_list= ChatRoom.objects.filter(participants__in=serializer.validated_data['participants']
if len(room_list) >0:
room = room_list[0]
else:
room = None
This will return the first room in which all the given participants are present, you can replace 0 with any valid value or add something more in the query if you want it to return a specific room.

Django query complex query in one to one field model

I have two models of Student and Parent
Student models.py:
class StudentInfo(models.Model):
admissionNumber = models.BigIntegerField(primary_key=True,default=0)
firstName = models.CharField(max_length=20)
lastName = models.CharField(max_length=20)
fullName = models.CharField(max_length=50)
gender = models.CharField(max_length=20)
dob = models.DateField(null=True)
classSection = models.CharField(max_length=20)
Parent models.py
class ParentInfo(models.Model):
student = models.OneToOneField(StudentInfo,primary_key=True, on_delete=models.CASCADE)
fatherName = models.CharField(max_length=20)
motherName = models.CharField(max_length=20)
I have a form to search students through their fatherName.
So, what I want is to filter those students whose father's name contains 'some name'.
I tried this but it resultes in query set of ParentInfo:
parentInfo = ParentInfo.objects.all()
studentsInfo = parentInfo.filter(parent__fName = fName).select_related('student')
You should filter the opposite way, like:
StudentInfo.objects.filter(parentinfo__fatherName='name of father')
You here thus obtain a QuerySet of StudentInfos which contains zero, one, or more StudentInfos where there is a related ParentInfo object where the fatherName field is, in this case 'Name of father'.
Note: It might be better to implement a ForeignKey in the opposite order, such that multiple students can refer to the same ParentInfo object. Right now, a ParentInfo object can refer to exactly one StudentInfo. If there are students with the same parents (so siblings), then you introduce data duplication in the database.
# You can use contains attribute on the field of model and your query can be like this
student = models.ParentInfo.objects.values('student__firstName', 'student__lastName').filter(fatherName__contains='your value')
print(student[0]['student__firstName'])
print(student[0]['student__lastName'])

django query with prefetch_related

I am struggling to get at the data I need from a prefetch-related query.
I have a table of events (the calendar table), a table of members and an attendee table which links the two.
My models look like:
class Member(models.Model):
firstname = models.CharField(max_length=40)
lastname = models.CharField(max_length=50)
email = models.EmailField(blank=True, verbose_name ='e-mail')
phone = models.CharField(max_length=40)
membershipnum = models.CharField(max_length=40)
class Attendee(models.Model):
memberid = models.ForeignKey(Member, on_delete=models.SET(0), related_name="attendingmembers")
calendarid = models.ForeignKey(Calendar, on_delete=models.SET(0))
attended = models.BooleanField(default=0)
paid = models.BooleanField(default=0)
class Meta:
db_table = 'attendee'
For a particular event I want a list of attending members with the attended and paid fields from the attendee table.
In my view I have
attendees = Member.objects.filter(attendingmembers__calendarid_id=id).prefetch_related('attendingmembers')
I am getting the right members, but I don't know if this is the best way to do it? And I can't figure out how to get at the attendee fields.
If I do
for thisone in attendees:
print(thisone)
print(thisone.attendingmembers)
I get the expected return from the first print, but the second just gives me
myapp.Attendee.None
Any advice much appreciated.
You still need .all() to get the list of items of your relation:
for this one in attendees:
print(thisone.attendingmembers.all())
Btw, do you wish to get all attendingmembers or only the ones with the right calendar_id ?
attendees = Member.objects.filter(attendingmembers__calendar_id=id).prefetch_related('attendingmembers')
# return all Members having at least one attendingmember with calendar_id=id, and prefetch all of their attendingmembers
attendees = Member.objects.filter(attendingmembers__calendar_id=id).prefetch_related(Prefetch('attendingmembers', queryset=Attendee.objects.filter(calendar_id=id)))
# return all Members having at least one attendingmember with calendar_id=id, and prefetch their attendingmembers matching the filter
The documentation shows you how to use the to_attr argument in the Prefetch objects ;)

django-filters using ModelChoiceFilter to get value of ForeignKey

I'm trying to use ModelChoiceFilter to filter a database of letters based on the author. Author is a ForeignKey, and I can't seem to get it to display the "name" value of the ForeignKey.
Here is what I have:
models.py (limited to relevant bits)
class Person(models.Model):
name = models.CharField(max_length=250, verbose_name='Full Name')
...
def __str__(self):
return self.name
class Letter(models.Model):
author = models.ForeignKey(Person, related_name='author', on_delete=models.PROTECT, verbose_name='Letter Author')
recipient = models.ForeignKey(Person, related_name='recipient', on_delete=models.PROTECT, verbose_name='Recipient')
...
title = models.CharField(max_length=250, verbose_name='Title of Letter')
def __str__(self):
return self.title
letter_filters.py
class LetterFilter(django_filters.FilterSet):
...
author = django_filters.ModelChoiceFilter(queryset=Letter.objects.order_by('author__name'))
class Meta:
model = Letter
fields = ['author', 'recipient']
I can see that this kind of works. It is indeed limiting and ordering it properly, but instead of the author name being presented in the select box, it's presenting "title" from the letter (but I can tell from the title, in the proper order).
What I thought should work is this:
fields = ['author__name', 'recipient']
But that too continues to list "title" from Letter instead of "name" from Person.
I know it has what I need, because if I do:
author = django_filters.ModelChoiceFilter(queryset=Letter.objects.order_by('author__name').values('author__name'))
I get exactly what I want! But, it's presented as {'author__name':'Jane Doe'} with fields author or author_name. I just can't seem to get the right syntax.
Finally, I know I can do:
author = django_filters.ModelChoiceFilter(queryset=Person.objects.order_by('name'))
Which returns all Persons, properly ordered. However there are many more persons in the database than just authors. This is the same result as just allowing the default fields['author'... without setting the author= in the class (though unordered).
Well the queryset you specify deals with Letters, so as a result the Letters are in that cases added in the ModelChoiceFiler, which is not ideal at all.
You can however generate a list of Persons that has written at least one letter like:
django_filters.ModelChoiceFilter(
queryset=Person.objects.filter(letter_set__isnull=False).order_by('name').distinct()
)
So here we filter on the fact that the letter_set is not empty, and since this will result in a JOIN where a Person can occur multiple times, we add .distinct() to it.
I find this modeling however very weird (in your three examples). It basically means that you only can assign Persons that already wrote a Letter. What if a person that has never written a Letter wants to write a Letter?
Usually in case there are different such roles, you can for example add a BooleanField:
class Person(models.Model):
name = models.CharField(max_length=250, verbose_name='Full Name')
is_author = models.BooleanField(verbose_name='Is the person an author')
# ...
Then we can filter on Persons that are Authors:
django_filters.ModelChoiceFilter(
queryset=Person.objects.filter(is_author=True).order_by('name')
)

Django (Model)Form Field: Manytomany with key value pair

I have a situation where I need to do something similar to rendering a formset within a formset. But I'd rather focus on the problem before jumping to a solution.
In English first:
I'm creating a shipment from a warehouse.
Each shipment can contain multiple lines (unique combinations of product_type and package_type) with an item_count
However for each line there could be multiple "Packages" - a package_type of a product_type that has an item_count. Think of this as a batch.
The customer is only interested in seeing one line for each product_type/package_type
But we need to pull out the stock and correctly attribute the particular units from each batch to allow stock control, recall control etc to function. Therefore the dispatch staff IS interested in exactly which Packages are shipped.
Add to this the sales staff enter a SalesOrder that only specifies the product_type/package_type. They aren't interested in the Packages either. (Think putting in a forward order for next month - who knows what will be in stock then?).
Now the models (simplified for clarity):
class Package(models.Model):
create_date = models.DateField()
quantity = models.FloatField()
package_type = models.ForeignKey(PackageType, on_delete=models.PROTECT)
product_type = models.ForeignKey(ProductType, on_delete=models.PROTECT)
class CheckOut(models.Model):
package = models.ForeignKey(Package, on_delete=models.PROTECT)
create_date = models.DateField()
quantity = models.FloatField()
class Shipment(models.Model):
sales_order = models.ForeignKey(SalesOrder, null=True, blank=True)
ship_date = models.DateField(default=date.today,
verbose_name='Ship Date')
class ShipmentLine(models.Model):
shipment = models.ForeignKey(Shipment, null=True, blank=True)
sales_order_line = models.ForeignKey(SalesOrderLine, null=True, blank=True)
quantity = models.FloatField(verbose_name='Quantity Shipped')
checkout = models.ManytoManyField(CheckOut)
I currently have it working well with the constraint of a 1:M relationship of CheckOut:ShipmentLine. However when changing this to a M:M, things get knarly form-wise.
In the 1:M version the Shipment form (plus formset for the ShipmentLines) looks like this:
class CreateShipmentForm(forms.ModelForm):
class Meta:
model = om.Shipment
contact = forms.ModelChoiceField(
queryset=om.Contact.objects.filter(is_customer=True, active=True),
label='Customer')
customer_ref = forms.CharField(required=False, label='Customer Reference')
sales_order = forms.ModelChoiceField(queryset=om.SalesOrder.objects.all(),
required=False, widget=forms.HiddenInput())
number = forms.CharField(label='Shipment Number', required=False,
widget=forms.TextInput(attrs={'readonly': 'readonly'}))
class CreateShipmentLineForm(forms.ModelForm):
class Meta:
model = om.ShipmentLine
widgets = {
'checkout': forms.HiddenInput()
}
fields = ('package', 'quantity', 'id',
'sales_order_line', 'checkout')
id = forms.IntegerField(widget=forms.HiddenInput())
sales_order_line = forms.ModelChoiceField(
widget=forms.HiddenInput(), required=False,
queryset=om.SalesOrderLine.objects.all())
package = forms.ModelChoiceField(required=True, queryset=None) # queryset populated in __init__, removed for brevity
So for the 1:M, I could select a package, set the quantity and done.
For M:M, I will need to select product_type, package_type, and then 1 or more packages, AND for each package a quantity. (I'll be using JS in the form to filter these)
In my mind's eye I have a few possibilities:
create a (child) formset for the Packages and quantities and include in each line of the (parent) formset
create some sort of multi-field, multi-value matrix custom form field and use that
construct a modal dialog where the M:M stuff happens and somehow save the result to the form where validation, saving happens.
I hope I have explained it correctly and clearly enough. It's the most complex application of Django forms I've encountered and I'm not sure what the limitations/pros/cons of each of my options is.
Has anyone encountered this situation and have a solution? Or any words to the wise?
My thanks in advance,
Nathan
I have a similar situation, I am doing something like your second and third options:
I have overridden __init__() and, after calling super, I have a loop that adds a value selector for every field (of course you could use a single custom element here)
Then override save() and after calling super I process the extra field adding all the values.