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.
Related
I have been trying to figure out the best way (or correct) way to set up models for our PIM/PriceModel app in Django.
Example models (stripped):
class ProductItem(models.Model):
"""
An actual item/product, what it all revolves around.
"""
part_number = models.CharField(unique=True, max_length=50, help_text='')
internal_part_number = models.CharField(primary_key=True, max_length=50, help_text='') # prefilled by partnumber
type = models.ForeignKey('Type', null=True, on_delete=models.SET_NULL)
attributes = JSONField() # Or another dynamic field
# and more ....
class Type(models.Model):
"""
Product type i.e. camera-dome, camera-bullet, pir, etc.
"""
pass
class Segment(models.Model):
"""
A segment of the company like Industry, Retail, Guarding etc.
"""
pass
class ProductCategory(models.Model):
"""
Supposedly a child category of TopCategory.
"""
pass
class InstallPrice(models.Model):
"""
Product item installation prices based on Type, Product Category and Segment.
"""
install_price = models.DecimalField(max_digits=8, decimal_places=2, help_text='')
type = models.ForeignKey('Type', null=True, on_delete=models.SET_NULL)
product_category = models.ForeignKey('ProductCategory', null=True, on_delete=models.SET_NULL)
segment = models.ForeignKey('Segment', null=True, on_delete=models.SET_NULL)
Take a look at "attributes = HStoreField(db_index=True)" in the ProductItem model.
The main thing i need to store in a product item is attributes like how many inputs/outputs/connection-options does it have. I need to store this for testing products against each other further down the line in the price-model app. This to make sure you have the right amount of products with matching attributes like inputs or outputs. I also need the User/Admin to be able to add this attributes dynamically so the app becomes self sustainable and not requires a migration id there is a new attribute I dont yet know about.
As I could not figure out a reasonable model configuration I ended up looking at postgres specific fields. This is not a must!
ideally when selecting type in the admin section i would like a "preset" of attributes to be presented based on the type.
Attributes could be:
inputs # number
outputs # number
channels # number
analysis # Boolean
Is this achievable? Any suggestions are welcome as I have limited Data Base experience. I need help figuring out the models.
I'm trying to access a field of a foreign key within a Tabular Inline in the Django Admin.
Despite my best efforts I can't seem to get it working. My current code is:
class RankingInline(admin.TabularInline):
model = BestBuy.products.through
fields = ('product', 'account_type', 'rank')
readonly_fields = ('product', 'rank')
ordering = ('rank',)
extra = 0
def account_type(self, obj):
return obj.products.account_type
Which results in:
'RankingInline.fields' refers to field 'account_type' that is missing from the form.
I have also tried using the model__field method, which I used as:
fields = ('product', 'product__account_type', 'rank')
Which results in:
'RankingInline.fields' refers to field 'product__account_type' that is missing from the form.
The models are defined as so:
class Product(BaseModel):
account_type = models.CharField(choices=ACCOUNT_TYPE_OPTIONS, verbose_name='Account Type', max_length=1, default='P')
class Ranking(models.Model):
product = models.ForeignKey(Product)
bestbuy = models.ForeignKey(BestBuy)
rank = models.IntegerField(null=True, blank = True)
class BestBuy(BaseModel):
products = models.ManyToManyField(Product, through='Ranking')
class BaseModel(models.Model):
title = models.CharField(max_length = TODO_LENGTH)
slug = models.CharField(max_length = TODO_LENGTH, help_text = """The slug is a url encoded version of your title and is used to create the web address""")
created_date = models.DateTimeField(auto_now_add = True)
last_updated = models.DateTimeField(auto_now = True)
What am I doing wrong?
I think what you are looking for is nested inlines since you want to expand "Product" as inline within RankingInline. At present Django does not have such feature built in. This question is relevant: Nested inlines in the Django admin?
You can also look at "Working with many-to-many intermediary models" section in Django DOC. That might be useful.
Actually Django will show you a small green '+' button besides the inline product field entry which you can use to create a new product to assign to your current entry for BestBuy. This might be an alternative for you to use.
You simply need to add the method-field to readonly_fields:
readonly_fields = ('product', 'rank', 'account_type')
Your new field account_type should be defined in ModelAdmin (i.e. RankingAdmin) not in TabularInline (i. e. RankingInline). It should be only accessed from TabularInline.
I have models like this:
class Vendor(models.Model):
title = models.CharField()
class Product(models.Model):
...
vendor = models.ForeignKey(Vendor, null=True, blank=True)
stock = models.ManyToManyField(Supplier, through='Stock')
class Stock(models.Model):
in_stock = models.BooleanField(default=True)
supplier = models.ForeignKey('catalog.Supplier', related_name='supplier_stock')
product = models.ForeignKey('catalog.Product', related_name='product_stock')
priority = models.IntegerField(default=0)
I designed models like this, because one Product can be supplied by different suppliers, and I need to know, what supplier exactly has this Product in stock.
So, in my view I want to get all results in values, to reduce number of queries and some specific logic. Also it duplicates me Product row with different Stock, by in python I group them up.
In my view I use:
Product.objects.all().values(
'id', 'title', 'vendor_code', 'vendor__title', 'price',
'product_stock__in_stock', 'stock__title', 'stock__id', 'stock__priority')
Because of INNER JOIN and null=True for Vendor related model, it returns me not all records for Product model. It just returns values where Vendor reference is set.
If I use 'vendor' instead of 'vendor__title' it returns me more results, than previous one, because in vendor field I can get {...'vendor': *id goes here*...} or {...'vendor': None...}, but I need the vendor__title value there. So any suggestions, how to achieve this?
Thanks in advance
Changed from vendor__title to product_stock__product__vendor__title helped me to fix my problem.
I imported my (PHP) old site's database tables into Django. By default it created a bunch of primary key fields within the model (since most of them were called things like news_id instead of id).
I just renamed all the primary keys to id and removed the fields from the model. The problem then came specifically with my News model. New stuff that I add doesn't appear in the admin. When I remove the following line from my ModelAdmin, they show up:
list_display = ['headline_text', 'news_category', 'date_posted', 'is_sticky']
Specifically, it's the news_category field that causes problems. If I remove it from that list then I see my new objects. Now, when I edit those items directly (hacking the URL with the item ID) they have a valid category, likewise in the database. Here's the model definitions:
class NewsCategory(models.Model):
def __unicode__(self):
return self.cat_name
#news_category_id = models.IntegerField(primary_key=True, editable=False)
cat_name = models.CharField('Category name', max_length=75)
cat_link = models.SlugField('Category name URL slug', max_length=75, blank=True, help_text='Used in URLs, eg spb.com/news/this-is-the-url-slug/ - generated automatically by default')
class Meta:
db_table = u'news_categories'
ordering = ["cat_name"]
verbose_name_plural = "News categories"
class News(models.Model):
def __unicode__(self):
return self.headline_text
#news_id = models.IntegerField(primary_key=True, editable=False)
news_category = models.ForeignKey('NewsCategory')
writer = models.ForeignKey(Writer) # todo - automate
headline_text = models.CharField(max_length=75)
headline_link = models.SlugField('Headline URL slug', max_length=75, blank=True, help_text='Used in URLs, eg spb.com/news/this-is-the-url-slug/ - generated automatically by default')
body = models.TextField()
extra = models.TextField(blank=True)
date_posted = models.DateTimeField(auto_now_add=True)
is_sticky = models.BooleanField('Is this story featured on the homepage?', blank=True)
tags = TaggableManager(blank=True)
class Meta:
db_table = u'news'
verbose_name_plural = "News"
You can see where I've commented out the autogenerated primary key fields.
It seems like somehow Django thinks my new items don't have news_category_ids, but they definitely do. I tried editing an existing piece of news and changing the category and it worked as normal. If I run a search for one of the new items, it doesn't show up, but the bottom of the search says "1 News found", so something is going on.
Any tips gratefully received.
EDIT: here's my ModelAdmin too:
class NewsCategoryAdmin(admin.ModelAdmin):
prepopulated_fields = {"cat_link": ("cat_name",)}
list_display = ['cat_name', '_cat_count']
def _cat_count(self, obj):
return obj.news_set.count()
_cat_count.short_description = "Number of news stories"
class NewsImageInline(admin.TabularInline):
model = NewsImage
extra = 1
class NewsAdmin(admin.ModelAdmin):
prepopulated_fields = {"headline_link": ("headline_text",)}
list_display = ['headline_text', 'news_category', 'date_posted', 'is_sticky'] #breaking line
list_filter = ['news_category', 'date_posted', 'is_sticky']
search_fields = ['headline_text']
inlines = [NewsImageInline]
The answer you are looking for I think would lie in the SQL schema that you altered and not in the django models.
It could probably have something to do with null or blank values in the news_category_id, or news that belongs to a category that doesn't exist in the news_category. Things I'd check:
You have renamed the primary key on the News category from news_category_id to id. Does the foreign key on the News also map to news_category_id and not anything else?
Are all the values captured in the news.news_category also present in news_category.id
Also, as an aside, I don't see any reason why you need to rename the primary keys to id from something that they already are. Just marking them primary_key=True works just fine. Django provides you a convenient alias pk to access a model's integer primary key, irrespective of what the name of the field actually is.
I am building a football predictions app whilst learning django and have the following models:
class Team(models.Model):
Name = models.CharField(max_length=30)
class Fixture(models.Model):
HomeTeam = models.ForeignKey(Team, related_name='HomeTeamRef')
AwayTeam = models.ForeignKey(Team, related_name='AwayTeamRef')
HomeTeamScore = models.IntegerField(null=True, blank=True)
AwayTeamScore = models.IntegerField(null=True, blank=True)
Date = models.DateField()
class Player(models.Model):
User = models.ForeignKey(User)
DefaultHomeScore = models.IntegerField()
DefaultAwayScore = models.IntegerField()
class Prediction(models.Model):
Fixture = models.ForeignKey(Fixture)
HomeTeamScore = models.IntegerField()
AwayTeamScore = models.IntegerField()
Date = models.DateTimeField()
Player = models.ForeignKey(Player)
I have many fixture objects populated and have been using model formsets based on the Prediction model to render a view which allows the user to enter scores.
The problem is that they must choose which fixture the prediction relates to. I would like to pre-populate this so they get a list of fixtures and just enter the hometeamscore and awayteamscore. This involves pre-poulating the Prediction.Fixture field and Prediction.Player field but I am unsure on how to go about this?
Any help is much appreciated.
Edit: The problems seems to be how to pass multiple instances of Fixture into a Prediction model formset, I have seen examples of passing one but would like to do this all in one go.
I also would like the user to be able to make one Prediction per Fixture.
I think this is what you are looking for:
https://docs.djangoproject.com/en/1.7/topics/forms/formsets/#using-initial-data-with-a-formset
Your code would look something like this:
initial_data = []
for fixture in fixtures:
initial_data.append({'Player':player,'Fixture':fixture})
formset = MyPredictionFormset(initial=initial_data)
P.S. Not to be pedantic, but your field names are not PEP 8 compliant. They should be lowercase with underscores, but it's your call. (http://www.python.org/dev/peps/pep-0008/)