How to add conditional fields to ModelAdmin in Django? - python

I have model classes:
class Product(models.Model):
category = models.ForeignKey(Category)
name = models.CharField(max_length=90)
...
class Category(models.Model):
name = models.CharField(max_length=90)
description = models.CharField(max_length=2000)
properies = models.ManyToManyField(Property)
...
#property type, ex: 'weight', 'length'
class Property(models.Model):
...
#value for every product
class PropertyValue(models.Model):
product = models.ForeignKey(Product)
property = models.ForeignKey(Property)
...
and I need custom product/add/ page, having PropertyValue forms set depends on chosen category.
I've made a method getting PropertyValue list by category_id in ModelAdmin class, but how can I call it in runtime when chosen category changes? Is it possible in django?

What do you mean when you said in runtime. If those categories change, the new records will apear every time you load the add pages.
Did you do yor form class? Kind of:
class PropertyValueForm(forms.Form):
product = forms.ModelChoiceField(queryset=Product.objects.all())
property = forms.ModelChoiceField(queryset=Property.objects.all())
Or:
def getProduct():
# DO YOUR STUFF
return product_list
class PropertyValueForm(forms.Form):
product = forms.ChoiceField(choices=get_my_choices())

Related

Not able to access related model data using foreign key in Django

models.py
class products(models.Model):
name = models.CharField(max_length=100)
sku = models.CharField(max_length=50)
vendor = models.CharField(max_length=50)
brand = models.CharField(max_length=50)
price = models.FloatField()
product_status = models.BooleanField()
quantity = models.IntegerField()
def __str__(self):
return self.name
# categories
class categories(models.Model):
category_name = models.CharField(max_length=50)
parent_id = models.IntegerField()
# product categories
class product_categories(models.Model):
product = models.ForeignKey(products, on_delete=models.CASCADE)
category = models.ForeignKey(categories, on_delete=models.CASCADE)
def __str__(self):
return self.category
I can access 'category' table data(inside django shell) using
data = products.objects.all()
data.values('product_categories__category__category_name')
output: <QuerySet [{'product_categories__category__category_name': 'xxxx'}}]>
If I put this(inside django shell)
data.product_categories.category
output: 'QuerySet' object has no attribute 'product_categories'
How do I get a queryset(can be passed to html) which includes data from "categories" table along with the data of "products" table
There are a couple of issues happening here. First, data is a queryset, which is kind of like a list of objects, even though here there's just one object in the list. What you want is to get an attribute off of the item in the list, so you need something like a data.first() to get to that object before you start dotting into its attributes.
Secondly, the way Django handles reverse FK relationships requires that you refer to the FK by the standard name of, in your case, product_categories_set, OR you set your own related_name attribute on the FK. Something like:
# product categories
class product_categories(models.Model):
product = models.ForeignKey(products, on_delete=models.CASCADE, related_name='product_categories')
category = models.ForeignKey(categories, on_delete=models.CASCADE, related_name='product_categories')
def __str__(self):
return self.category
so that you can refer to your product_categories model from both the product and categories using just data.product_categories.
Thirdly, when accessing a reverse FK relationship, just like in point (1) above, you will get a related manager, from which you can get a queryset of items. Thus, to get the category name, you need to indicate which item you want the category name for. Assuming it's just the first item for everything, it would look something like:
data = products.objects.all()
product_category = data.product_categories.all()
category_name = product_category.category.category_name
Of course once you have more data, you'll not always want to just pick the first item, so you'll need to add filtering logic into the query to make sure you get the item you're looking for.
ETA, I do agree with the comment by Jorge above - a MTM would make this a bit simpler and would, in essence, create your product_categories table for you.

How can I display all fields from a Many-to-Many Model in Django Admin

I have the following Models:
class ModelA(models.Model):
some_field_A = models.CharField()
some_other_field_A = models.CharField()
class ModelB(models.Model):
some_field_B = models.CharField()
many_to_many_relation = models.ManyToManyField(ModelA)
In admin.py I am using filter_horizontal to edit the ManyToManyField:
class ModelB(admin.ModelAdmin):
model = ModelB
filter_horizontal = ('many_to_many_relation',)
but it shows only some_field_A and I want it to show both fields from ModelA, because the entries in ModelA are unique depending on both fields and as you can see from the picture there are multiple entries with the same value (i.e. some_field_A = EUV) but they have different values for some_other_field_A:
It displays the result of the __str__(…) method you defined in your ModelA, so if you return the value of some_field in the __str__(…) method, then it will return only the data of some_field.
You thus can alter this method and return both fields:
class ModelA(models.Model):
some_field_A = models.CharField()
some_other_field_A = models.CharField()
def __str__(self):
return f'{self.some_field_A} {self.some_other_field_A}'
I'm not sure if this exactly the solution you are looking for but you could override the __str__ method of ModelA to return the information in a single line.
So for example:
class ModelA(models.Model):
first_field = models.CharField(max_length=16)
second_field = models.CharField(max_length=16)
def __str__(self):
return f"{self.first_field} ({self.second_field'})"
Your admin view should then show each object as "foo (bar)"

Validation of a Django model using a foreign key set after saving the form

I'm trying to do validate a model based on a value that I set after save(commit=False). How can I achieve this?
I have multiple forms (Item, Listing, Price) that I combine in one view and then create one instance after the other while saving.
edit: I'll try to explain a bit further. Every listing lists an item. Every item has a category. Every category has a minimum price. Listings have prices(can be multiple to retain a history). When the instance of Price is saved, I want to validate that the amount is greater than or equal to the minimum set for the category of the item referenced by the listing.
models.py
...
class Category(models.Model):
...
class Item(models.Model):
category = models.ForeignKey(Category, ...)
class Listing(models.Model):
for_item = models.ForeignKey(Item, ...)
class Price(models.Model):
for_listing = models.ForeignKey(Listing, ...)
amount = MoneyField(...)
class MinPriceForCategory(models.Model):
category = models.ForeignKey(Category, ...)
amount = MoneyField(...)
views.py
...
def add_item(self):
...
# create form instances from request.POST etc
...
item = form_item.save()
listing = form_listing.save(commit=False)
listing.for_item = item
listing.save()
# validation happens in the first call to save()
price = form_price.save(commit=False)
price.for_listing = listing
# I need the validation to happen here so the instance of Listing is available
price.save()
```
I just found another way to do what I wanted, although I'm not sure if that's the right way. I now instantiate the form for the Model that depends on Listing and pass the instance of Listing to the the constructor:
# views.py
...
form_listing = ListingForm(request.POST)
listing = form_listing.save()
form_price = PriceForm(request.POST, listing=listing)
if form_price.is_valid():
price = form_price.save()
# forms.py
class PriceForm(forms.ModelForm):
...
def __init__(self, *args, **kwargs):
self.listing = kwargs.pop('listing', None)
super(PriceForm, self).__init__(*args, **kwargs)

django-tables 2 M2M field not shown

I am trying to show a M2M field in a django-table2 as seen in Django-tables2: How to use accessor to bring in foreign columns? and Accessing related models with django-tables2
Using: foreigncolumn = tables.Column(accessor='foreignmodel.foreigncolumnname'), I only see a '--'...
# The models:
class Organism(models.Model):
species_name = models.CharField(max_length=200)
strain_name = models.CharField(max_length=200)
eukaryotic = models.BooleanField(default=True)
lipids = models.ManyToManyField('Lipid',blank=True)
class Lipid(models.Model):
lm_id = models.CharField(max_length=100)
common_name = models.CharField(max_length=100,blank=True)
category = models.CharField(max_length=100,blank=True)
#The tables
class OrganismTable(tables.Table):
name = tables.LinkColumn('catalog:organism-detail', text=lambda record: record.species_name, args=[A('pk')])
lp = tables.Column(accessor='Lipid.common_name')
class Meta:
model = Organism
sequence = ['name','lp']
exclude = ['id','species_name']
Any idea what I'm doing wrong?
This does not work so easily for ManyToManyFields because of the simple way Accessor works. You could display the repr of the related QuerySet via 'lipids.all' but that does not seem sufficient here. You can, however, add a property (or method) to your Organism model and use it in the accessor. This way, you can display any custom information related to the instance:
class Organism(models.Model):
# ...
#property
def lipid_names(self):
return ', '.join(l.common_name for l in self.lipids.all()) # or similar
class OrganismTable(tables.Table):
# ...
lp = tables.Column(accessor='lipid_names')
I would recommend then to add a prefetch_related('lipids') to the Organism QuerySet that you pass to the table for better performance.

Return Django queryset of items that appear in a single category when items can appear in multiple categories

I'm trying to return a queryset of all items in a Category where items can occur in multiple categories. The relevant model declarations are below along with one of many attempts that did not work. Is there a way to do this using Django's built-in intermediate table functionality without having to explicitly declare a model for the intermediary table?
class Category(models.Model):
parent = models.ForeignKey('self',null=True,blank=True)
name = models.CharField(max_length=150,null=True)
description = models.CharField(max_length=255,null=True,blank=True)
def items(self):
curr_category = Category.objects.filter(pk=self.id)
items_in_category = curr_category.item__categories_set.all().values('item_id')
return Item.objects.filter(pk__in=items_in_category)
def __unicode__(self):
return self.name
class Item(models.Model):
name = models.TextField(null=True,blank=True)
price = models.DecimalField(max_digits=10, decimal_places=2,null=True,blank=True)
categories = models.ManyToManyField(Category,null=True)
One way you could do this is with a custom models.Manager on your Item model. This is ideal, IMHO, because this logic doesn't really belong in your Category model. What if you want categories for things besides Item's? Then you'd have to implement more retrieval methods on Category, bloating it.
class Category(models.Model):
parent = models.ForeignKey('self',null=True,blank=True)
name = models.CharField(max_length=150,null=True)
description = models.CharField(max_length=255,null=True,blank=True)
def __unicode__(self):
return self.name
class ItemManager(models.Manager):
def get_for_category(self, category):
return self.filter(categories=category)
class Item(models.Model):
name = models.TextField(null=True,blank=True)
price = models.DecimalField(max_digits=10, decimal_places=2,null=True,blank=True)
categories = models.ManyToManyField(Category,null=True)
objects = ItemManager()
Then call this using:
items = Item.objects.get_for_category(category_instance)
If you really want to do it in a Category method, then why not:
class Category(models.Model):
def items(self):
# probably need to import Item model here in order to avoid
# circular import reference
from myapp.models import Item
return Item.objects.filter(categories__id=self.id)

Categories