Django Model Forms with Multi-Level Foreign Keys - django

I need to create a model form in Django and have the following arbitrary scenario.
Real Estates
============
ID
...some extra fields...
CityID
Cities
======
ID
Name
Region
======
ID
Name
Country
=======
ID
Name
What I would like to do is to let user choose the Country first, then Region and lastly the City. (Populate the child category with javascript after user selects the parent category.) However, I don't want to add the 'Region' and 'Country' fields to the 'Real Estate' table. The order of the fields are also important, that is, 1) Country, 2) Region and 3) City.
Can you suggest any approach to this? Thanks!

I'm assuming you are trying to show the user a limited set of options for "Region" after he selected a country and a limited set of options for "City" after he selected the region so as to provide a way to sensibly select a city for the real estate instead of having to pick something from a long list of random cities?
You could specify additional fields on the RealEstate ModelForm that aren't actual fields on the RealEstate model and provide logic in the form's .__init__() and if needed also in .clean() and .save() methods to take care of the additional fields. Perhaps something like this:
class Country(models.Model):
name = models.CharField(...)
class Region(models.Model):
name = models.CharField(...)
country = models.ForeignKey(Country, on_delete=models.CASCADE)
class City(models.Model):
name = models.CharField(...)
region = models.ForeignKey(Region, on_delete=models.CASCADE)
class RealEstate(models.Model):
name = models.CharField(...)
city = models.ForeignKey(City, on_delete=models.CASCADE)
class RealEstateForm(forms.ModelForm):
country = forms.ChoiceField(required=False)
region = forms.ChoiceField(required=False)
class Meta:
model = RealEstate
fields = ['name', 'city', 'country', 'region']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['country'].choices = Country.objects.all().values('id', 'name)
self.fields['region'].choices = Region.objects.all().values('id', 'name', 'country__id')
self.fields['city'].choices = City.objects.all().values('id', 'name', 'region__id', 'region__country__id')
Providing choices that include the referenced region ID and country ID for each city will allow you to filter these in the front end if you manage to use these choices as the user is selecting country and region without having to make a separate backend request to get the filtered list. Of course, you would end up with a larger response being sent this way in one go - if your data set size is very large (how many countries, how many regions, how many cities) this may not be wanted.

Related

Dynamic Drop List for Many to Many relation

I have 2 tables "Client" and "Location".
class Client(models.Model):
name = models.CharField(max_length=50)
class Location(models.Model):
name = models.CharField(max_length=50)
A Client can be in many Locations and a Location can have many Clients.
I created a third table to hold this relationship:
class Client_Location(models.Model):
client = models.ForeignKey(Client, on_delete=models.CASCADE)
location = models.ForeignKey(Location, on_delete=models.CASCADE)
I created a form to test whether i can make the dropdownlist dynamic, so if i were to pick a client, any location linked to that client would only appear.
class ClientLocationForm(forms.ModelForm):
class Meta:
model = Client_Location
fields = '__all__'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['location'].queryset = Location.objects.none()
So far i was only able to make the location field blank. Not sure where to go next as examples i've seen aren't exactly like mine.
With minimal changes to your code, you may add the existing Client_Location model as through model for a new explicit ManyToMany relationship. To do so, add the following field to your Client model:
locations = models.ManyToManyField('Location', through='Client_Location', related_name='clients')
If you need fully dynamic updates, you need to write a view which provides a list of locations for a specified client (client.locations.all()) and then show them on your page.

query chain of models in Django

I have three models for countries, provinces and cities. one of URLs includes the city name where a post should go. I could get the name of the city through the URL . In view function, I could find the city name and then I could find the country which the city is belong to. Now, I want to list all cities under this country. My question is how can I do this where there is no direct relation between city and country. The city is related to the province and the province related to the country. The list I want should includes all cities inside the country regardless of the province. How can I do this? is there any possible solution rather than make direct relation between city and country model?
Note: I could get the country ID through the variable post_city. The country ID for selected city was 3. Therefor, I want all of cities that are under the country that has an ID of 3.
Example: through the URL: I got post_city variable as Los Angeles. Hence, I want all cities in the USA regardless of state of California. Another example, If the post_city variable was London, then, I want all cities in Britain regardless of the province.
the models I have are as follow:
class Country(models.Model):
country_name = models.CharField(max_length=64, unique=True)
def __str__(self):
return "%s" % (self.country_name)
class Province(models.Model):
country_id = models.ForeignKey(Country, on_delete=models.CASCADE)
province_name = models.CharField(max_length=64)
def __str__(self):
return "%s" % (self.province_name)
class City(models.Model):
province_name = models.ForeignKey(Province, on_delete=models.CASCADE)
city_name = models.CharField(max_length=64)
def Country(self):
return self.province_name.country_id.country_name
the view function as follow:
def list_page(request, post_city):
p_c = City.objects.filter(city_name__iexact=post_city).get()
p_p = p_c.province_name
p_country = p_p.country_id
pp=City.objects.all()
print(pp)
context = {
'post_city' : post_city,
'all_p_cities': all_p_cities,
}
return render(request, 'path/to/list_page.html', context )
You can chain multiple related model lookups by using several __ parts in the lookup argument name.
cities_in_narnia = City.objects.filter(province_name__country_id__country_name='Narnia')
Read more in the Django docs: Lookups that span relationships
As a side note. There are some naming conventions you can use that will make your code more readable, especially when seeking help from the online community.
class City(models.Model):
# foreign key fields should be the snake_case or lower case name of the related model class
province = models.ForeignKey(Province, on_delete=models.CASCADE)
# there's no need to prefix field or attribute names with the class name
# City.city_name is superfluous. City.name is perfectly clear.
name = models.CharField(max_length=64)
# methods and attributes should also be snake_case, only use CapitalCase for class names.
#property
def country_name(self):
return self.province.country.name
If you follow these naming conventions, the filter lookup will also be readable and concise.
cities_in_narnia = City.objects.filter(province__country__name='Narnia')

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.

limit_choices_to in DjangoAdmin

One of my models contains a ForeignKey-field to a model that has multiple thousand instances.
When I display a record, all of these are loaded into a dropdown, which I a) don't need and b) is slow as frack, especially when displaying multiple records on one page.
Page size shoots up to multiples of 3.5mb because of the size of the dropdown.
I thought about using "limit_choices_to" to contain that, but
country = models.IntegerField(blank=True, null=True)
location = models.ForeignKey(Geonames, limit_choices_to = {'cowcode': country}, related_name='events')
does not work.
Is there even a way to do that?
Update:
What do I want to display?
I want to show all places (Geonames) that are in the country of the EventRecord that the code above is taken from. I want to show only these places, not the whole list of all possible places.
Why don't I need all places?
a) Page load times: 3.5 minutes for a page load is a tad too long
b) See above: An Event takes place in a certain country, so I don't need to show locations that are not in that country
What you want is to make limit_choices_to aware to your instance, which is not possible.
What you should do is set the queryset property of location field in your admin form, something similar to this:
class EventRecordAdminForm(forms.ModelForm):
class Meta:
model = EventRecord
def __init__(self, *args, **kwargs):
super(EventRecordAdminForm, self).__init__(*args, **kwargs)
self.fields['location'].queryset = Geonames.objects.filter(cowcode=self.instance.country)
and of course use that form for your admin:
class EventRecordAdmin(admin.ModelAdmin):
form = EventRecordAdminForm
See here for docs
HTH!
if you are using admin interface you can use raw_id_fields in ModelAdmin:
class BookAdmin(admin.ModelAdmin):
list_display = ('title', 'publisher', 'publication_date')
list_filter = ('publication_date',)
date_hierarchy = 'publication_date'
ordering = ('-publication_date',)
filter_horizontal = ('authors',)
raw_id_fields = ('publisher',)
from Django Book:
sometimes you don’t want to incur the overhead of having to select all the related objects to display in the drop-down. For example, if our book database grows to include thousands of publishers, the “Add book” form could take a while to load, because it would have to load every publisher for display in the box.
The way to fix this is to use an option called raw_id_fields. Set this to a tuple of ForeignKey field names, and those fields will be displayed in the admin with a simple text input box () instead of a select.
Not sure why that is not working for you. But I think a better solution would be to use django-smart-selects. That way you can have the user choose country first. Then the Geoname dropdown is only populated when the user first chooses country.
From the docs:
If you have the following model:
class Location(models.Model)
continent = models.ForeignKey(Continent)
country = models.ForeignKey(Country)
area = models.ForeignKey(Area)
city = models.CharField(max_length=50)
street = models.CharField(max_length=100)
And you want that if you select a continent only the countries are available that are located on this continent and the same for areas you can do the following:
from smart_selects.db_fields import ChainedForeignKey
class Location(models.Model)
continent = models.ForeignKey(Continent)
country = ChainedForeignKey(
Country,
chained_field="continent",
chained_model_field="continent",
show_all=False,
auto_choose=True
)
area = ChainedForeignKey(Area, chained_field="country", chained_model_field="country")
city = models.CharField(max_length=50)
street = models.CharField(max_length=100)
This example asumes that the Country Model has a continent = ForeignKey(Continent) field and that the Area model has country = ForeignKey(Country) field.

django many to many admin shows all and not associated items

I have an object structure that looks like so:
Customer -- one to many -- Locations
Locations -- many to many -- Departments
Departments -- one to many -- Objects
here is my models.py (my admin.py is standard):
class Customer(models.Model):
customerName = models.CharField(max_length=64)
class Department(models.Model):
departmentName = models.CharField(max_length=64)
class Location(models.Model):
customer = models.ForeignKey(Customer)
departments = models.ManyToManyField(Department)
class Object(models.Model):
location = models.ForeignKey(Location)
department = models.ForeignKey(Department)
The problem is that when I want to set the department for objects I get every department in the django admin drop down. I even get the departments that are associate with locations of different customers.
Also, when I am setting the department of an object, I get the same list of all available departments, even those associated with different customers.
How can I have the drop down only show me the departments that a customer supports?
A quick one line solution to filter down a many to many relationship is put this line in your admin object:
filter_horizontal = ('departments',)
You can provide your own form with filtered queryset
class DepartmentAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(DepartmentAdminForm, self).__init__(*args, **kwargs)
self.fields['customers'].queryset = Customer.objects.filter(...)
class Meta:
model = Department
class DepartmentAdmin(admin.ModelAdmin):
form = DepartmentAdminForm
I believe the answer is to use the formfield_for_manytomany
https://docs.djangoproject.com/en/1.4/ref/contrib/admin/#django.contrib.admin.ModelAdmin.formfield_for_foreignkey