Django Inlines in admin for multiple foreign keys - django

I have 3 models: Document, MetaData, MetaDataValue, and a "connector table" DocumentMetaDataValue. I want to add a document and all the associated metadata values on one admin page.
models.py:
# MetaData
class MetaData(models.Model):
metadata_id = models.AutoField(primary_key = True)
name = models.CharField('metadata name', max_length=200, unique=True)
description = models.TextField('description')
def __str__(self):
return self.name
# MetaData Value
class MetaDataValue(models.Model):
metadata_id = models.ForeignKey(MetaData, on_delete=models.CASCADE,)
value = models.CharField('value', max_length=200, unique=True)
def __str__(self):
return self.value
# Document
class Document(models.Model):
document_id = models.AutoField(primary_key=True)
metadata = models.ManyToManyField('MetaData', through='DocumentMetaDataValue', through_fields=('document', 'metadata'))
metadatavalue = models.ManyToManyField('MetaDataValue', through='DocumentMetaDataValue', through_fields=('document', 'metadataValue'))
class DocumentMetaDataValue(models.Model):
document = models.ForeignKey(Document, on_delete=models.CASCADE)
metadata = models.ForeignKey(MetaData, on_delete=models.CASCADE)
metadataValue = models.ForeignKey(MetaDataValue, on_delete=models.CASCADE)
admin.py:
class Fred(forms.ModelForm):
""" something newer
"""
class Meta:
model = DocumentMetaDataValue
fields = '__all__'
class DocumentMetaDataValueInline(admin.TabularInline):
model = DocumentMetaDataValue
form = Fred
class DocumentAdmin(admin.ModelAdmin):
form = DocumentForm
list_display = ('get_document_type', 'document_state', 'title', 'description', 'original_file_name', 'created', 'storage_file_name', 'get_thumb', )
ordering = ('created',)
readonly_field = ('document_state',)
filter_horizontal = ('metadata', 'metadatavalue', )
inlines = (DocumentMetaDataValueInline, )
# other code not related to this problem
admin.site.register(Document, DocumentAdmin)
There are 16+ metadata names, and each one has one or more metadata values. For example, the metadata name Person has 23 values ('Bob', 'Sam', 'Sally', etc), and the metadata name Decade has 12+ values (1890, 1900, 1910, 1920, etc). A Document can have one or more metadata names associated with it, and one or more metadata values for that metadata name can be associated with that Document. For example, a Document can be a photograph with two people in it. The metadata value for Decade could be 1910, and the metadata values for Person could be Sam and Sally.
On each line of the inline, I want to show the metadata name opposite a drop down with the associated metadata values. One should be able to pick more than one metadata value for a particular metadata name.
What happens in the code above is each line of the inline has two drop down lists - the first is all the metadata names and the second is all the metadata values.
I want to change the first drop down to be a single string for a metadata name, and the second drop down to still be a drop down, but only the values for that metadata name. I don't see how to make these changes.
Thanks!
Mark

Related

Django-import-export doesn't skip unchanged when skip_unchanged==True

I'm building an app using Django, and I want to import data from an Excel file using django-import-export.
When importing data I want to skip unchanged rows, for this, I'm using skip_unchanged = True in the resource class (like below) but I get unexpected behavior. In my model, I have an attribute updated_at which is a DateTimeField with auto_now=True attribute, it takes a new value each time I upload the Excel file even if the values of rows have not changed in the file.
Below are portions of my code.
models.py
class HREmployee(models.Model):
code = models.IntegerField()
name_en = models.CharField(max_length=55)
status = models.CharField(max_length=75)
termination_date = models.DateField(null=True)
hiring_date = models.DateField()
birth_date = models.DateField()
# other fields to be imported from the file ...
# fields that I want to use for some purposes (not imported from the file)
comment = models.TextField()
updated_at = models.DateTimeField(auto_now=True)
resources.py
class HREmployeeResource(ModelResource):
code = Field(attribute='code', column_name='Employee Code')
name_en = Field(attribute='name_en', column_name='Employee Name - English')
status = Field(attribute='status', column_name='Employee Status')
termination_date = Field(attribute='termination_date', column_name='Termination Date')
hiring_date = Field(attribute='hiring_date', column_name='Hiring Date')
birth_date = Field(attribute='birth_date', column_name='Birth Date')
# other fields to be imported ...
class Meta:
model = HREmployee
import_id_fields = ('code', )
skip_unchanged = True
How can I fix this unexpected behavior?
Edit
After few tries, I've found that columns with date values are causing this problem.
In the Excel file, I have three columns that have date values like in the picture below, when I comment the corresponding attributes in the resource class and do the import, I get the expected behavior (if no changes in the file the import_type equals skip and no changes are made in the DB).
I've edited the code of the model and resource classes (please check above).
This should be easy to fix, simply use the fields parameter to define only the fields you wish to import (docs):
class Meta:
...
fields = ('code', 'name',)
If skip_unchanged is True, then only these fields will be compared for changes, and the instance will be updated if any one of them has changed, otherwise it will be skipped.
The field name has to be the model attribute name, not the name of the column in the import.
Sorry for the revival of that post, but I was stucked on the same issue, and I found only this post exaclty related to, so i post my answer.
In my model I've defined dateField and not DateTimeField.
But it import as DateTimeField, so the comparison failed.
To compare carrots with carrots, I defined a field class to convert values if needed :
import datetime
class DateField(Field):
def get_value(self, obj):
val=super().get_value(obj)
if isinstance(val, datetime.datetime):
return val.date()
return val
and then in my resource
class HREmployeeResource(ModelResource):
hiring_date = DateField(attribute='hiring_date', column_name='Hiring Date')
birth_date = DateField(attribute='birth_date', column_name='Birth Date')
# ....

Tablename with ID "Some_ID" doesn't exist. Perhaps it was deleted? Django Sqlite

I have database with many tables; three of those which are interlinked via primary and foreign keys are "Vendor_Details" , "Channel_Details" and "MSO_Details"
models.py
class Vendor_Details(models.Model):
class Meta:
verbose_name_plural = "Vendor_Details"
vendor_name = models.CharField(primary_key = True,max_length=50)
mso_id = models.CharField(unique = True, max_length=20)
class Channel_Details(models.Model):
class Meta:
verbose_name_plural = "Channel_Details"
channel_id = models.CharField(primary_key = True, max_length=20)
vendor_name = models.ForeignKey(Vendor_Details, on_delete=models.CASCADE)
channel_logo = models.CharField(max_length=50)
channel_name = models.CharField(max_length=50)
class MSO_Details(models.Model):
class Meta:
verbose_name_plural = "MSO_Details"
mso_id = models.ForeignKey(Vendor_Details, on_delete=models.CASCADE)
channel_id = models.ForeignKey(Channel_Details, on_delete=models.CASCADE)
channel_number = models.PositiveIntegerField()
So,
Channel_Details is linked to Vendor_Details with vendor_name and
MSO_Details is linked with Vendor_Details and Channel_Details with mso_id and channel_id respectively.
Now, I am inside Django's Administrator's MSO_Details table and trying to click on edit icon of CHANNEL ID column i get a new window opens with message Channel_ details with ID "CH6" doesn't exist. Perhaps it was deleted? May be this is because channel_id is primary key of reference table and DB will not allow the changes? But then the message should had been something different. How can i handle this situation? I clicked on edit for CH_006 and message shows CH6. I am confused whats going on here, what is django's DB refering to here?
Note : I can very well add new CHANNEL_DETAILS after click add button.
I had this kind problem for the last two days and the problem was
1. If on adding details to a new form initially, you do not add the right field required.(I was including both text and integers to a field that was only CharField)
2.The other solution when the error came again was to delete migrations and the database itself and create a new database again(Using the same database name).
In my case, I had an existing SQLite database that I've been migration over to Django. All my entities had a UUID column as their primary key.
I had set the primary key column as django.models.UUIDField thinking that Django would support it, but it doesn't.
So I converted it to a text field with UUID as default value, it started working again.
class Model(models.Model):
# id = models.UUIDField(primary_key=True)
id = models.TextField(primary_key=True, default=uuid.uuid4)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
abstract = True

limit one record to be default in django model

What is the best way to limit only one record to be default in django
I have a model where i have a flag for default
class BOMVersion(models.Model):
name = models.CharField(max_length=200,null=True, blank=True)
material = models.ForeignKey(Material)
is_default = models.BooleanField(default=False)
I want to have only one value to be default for the same material but this material can have a lot of non default ones.
It was odd that this question was not addressed more often. If I have a default record, I want to record that in the class as a member variable. And to determine if an instance is the default, I want to compare the class default member variable with the instance id. Unfortunately I could not figure out how to access class variables and instance variables nicely in the same class function in Python (may be someone can comment), but this does not require to hit the database for a default or store a bunch of records pointing to a default. Just one member variable in the class.
After I wrote this, I realized every time the application is restarted, default is reset to None, so you will have to store this in a database. I have updated my answer accordingly. However, checking that the member variable is not null, and only hitting the database if it is would reduce hits here. The model I used was:
class RecordOfInterest(models.Model):
"""
Record Records of interest here. Stores ID, and identifying character
"""
# asume maximum 64 character limit for the model.
model_name = models.CharField(max_length=64, unique=True)
record_no = models.IntegerField()
human_ident = models.CharField(max_length=64, help_text='How is this of interest')
# Id it as default, deposit, ... Don't bother indexing, as there will only be a few
def __unicode__(self):
return u'Model %s record %d for %s' % (self.model_name, self.record_no, self.human_ident)
class Meta:
unique_together = ('model_name', 'human_ident')
class Product(models.Model):
"""
Allow one product record to be the default using "Product.default = prod_instance"
check default with "Product.is_default(prod_instance)"
"""
default = None # set this to id of the default record
cart_heading = models.CharField(max_length=64, blank=True)
country = CountryField()
pricing = models.ForeignKey(
'Price', blank=True, null=True, related_name='visas', on_delete=models.SET_NULL)
#classmethod
def is_default(cls, obj):
if cls.default_no == None:
try:
cls.default_no = RecordOfInterest.objects.get(model_name=cls.__name__, human_ident='default')
except RecordOfInterest.DoesNotExist:
default_no = None
return cls.default_no == obj.id
#classmethod
def set_default(cls, obj):
try:
default_rec = RecordOfInterest.objects.get(model_name=cls.__name__, human_ident='default')
except RecordOfInterest.DoesNotExist:
RecordOfInterest.objects.create(model_name=cls.__name__, record_no=obj.id, human_ident='default')
else:
if default_rec.record_no != obj.id:
default_rec.record_no = obj.id
default_rec.save()
cls.default_no = obj.id
return
Saving the ID in settings.py if it is static.
Save it into a separate "default" table with one record (or use the most recent) if it's dynamic.
Save the default in another table like this:
class BOMVersion(models.Model):
name = models.CharField(max_length=200,null=True, blank=True)
material = models.ForeignKey(Material)
class BOMVersionDefault(model.Models)
time_set= models.Datetime(auto_created=True)
default_material = models.ForiegnKey(Material)
To query:
default = BOMVerDefault.objects.latest(time_set).get().default_material
If you have several material types that each need a default then default_material would be a field in a material-type table.
Getting one record to be default in a table is most basic requirement we developer come face to face, after spending couple of hours over it, i think a neat and clean solution in django would be update all records to default false if current form instance has default value to be "true" and then save the record.
class FeeLevelRate(TimeStampedModel):
"""
Stores the all the fee rates depend on feelevel
"""
feelevel = models.ForeignKey(FeeLevel, on_delete= models.PROTECT)
firstconsultfee = models.DecimalField(_('First Consultation Charges'),max_digits=10,decimal_places=2,blank=True)
medcharges = models.DecimalField(_('Medicines Charges per Day'),max_digits=10,decimal_places=2,blank=True)
startdate = models.DateField(_("Start Date "), default=datetime.date.today)
default_level = models.BooleanField(_('Is Default Level?'),default=False)
class Meta:
constraints = [
models.UniqueConstraint(fields=["feelevel","startdate"], name='unique_level_per_date'),
]
def __str__(self):
return "%s applicable from (%s)" % ( self.feelevel, self.startdate.strftime("%d/%m/%Y"))
class FeeLevelRateCreate(CreateView):
model = FeeLevelRate
fields = ['feelevel', 'firstconsultfee', 'medcharges', 'startdate', 'default_level']
context_object_name = 'categories'
success_url = reverse_lazy('patadd:feerate_list')
def form_valid(self, form):
# Update all the default_level with false.
#UserAddress.objects.filter(sendcard=True).update(sendcard=False)
if form.instance.default_level:
FeeLevelRate.objects.filter(default_level=True).update(default_level=False)
return super().form_valid(form)

Access foreign key fields from Admin Tabular Inline

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.

Changed Django's primary key field, now items don't appear in the admin

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.