Django admin: make field editable in add but not edit - django

I've got a model similar to this:
class Product(models.Model):
third_party_id = models.CharField(max_length=64, blank=False, unique=True)
that uses the Django default primary key. I want users to be able to add products by setting the third_party_id on the add page, but I don't want that field editable in the edit page to avoid corrupting the third_party_id. In the Django docs, the same settings appear to be used for add and edit. Is this possible?

Do not set self.readonly_fields to avoid thread issues. Instead override get_readonly_fields method:
def get_readonly_fields(self, request, obj=None):
if obj: # obj is not None, so this is an edit
return ['third_party_id',] # Return a list or tuple of readonly fields' names
else: # This is an addition
return []

The above is helpful (shanyu's answer using get_readonly_fields), however it does not work properly if used in "StackedInline". The result is two copies of whatever field is marked readonly, and it is not editable in the "add" instance. See this bug: https://code.djangoproject.com/ticket/15602
Hope this saves someone some searching!

I am not sure if this is the best way, but you could define your own form for the admin. And custom validate your third_party_id, rejecting if it is already set:
Admin.py
class ProductAdminForm(forms.ModelForm):
class Meta:
model = Product
def clean_third_party_id(self):
cleaned_data = self.cleaned_data
third_party_id = cleaned_data['third_party_id']
id = cleaned_data['id']
obj = Product.objects.get(id=id)
if obj.third_party_id != third_party_id:
raise ValidationError("You cannot edit third_party_id, it must stay as %s" % obj.third_party_id)
return third_party_id
class ProductAdmin(admin.Admin):
form = [ProductAdminForm,]

Related

How to make non-editable fields appear when creating objects in Django admin?

I have a model with a non-editable field in my models file.
class Table(models.Model):
label = models.CharField(max_length=40, editable=False)
In my admin site, when updating existing Table objects, I can't edit the label. That is fine, this is exactly what I want with this constraint. However, when trying to create an object using the admin site, the field is still hidden, so I can only create Table objects using the shell.
How can I make this field appear only on creation, but on updates, it will be read-only? Thanks.
Try to use readonly_fields in admin.py file
class TableAdmin(admin.ModelAdmin):
readonly_fields = ('label',)
admin.site.register(Table, TableAdmin)
Approach 1
Make label field presented on creation but completely remove it while updating. We will be using ModelAdmin.get_exclude and ModelAdmin.get_fields hooks to accomplish this.
## models.py
class Table(models.Model):
label = models.CharField(max_length=40) # remove editable option
## admin.py
#admin.register(Table)
class TableAdmin(admin.ModelAdmin):
non_editable_fields = ['label']
def get_exclude(self, request, obj=None):
defaults = super().get_exclude(request, obj=obj) or ()
if obj: # if we are updating an object
defaults = (*defaults, *self.non_editable_fields)
return defaults or None
def get_fields(self, request, obj=None):
defaults = super().get_fields(request, obj=obj)
if obj: # if we are updating an object
defaults = tuple(f for f in defaults if f not in self.non_editable_fields)
return defaults
Approach 2
Make label field presented on both creation and update but make it read only while updating. django admin provides a hook for this functioanlity and it is called ModelAdmin.get_readonly_fields. You can find the documentation here.
So we can write the following code to create a field which can be presented/added when creating an object but can not be edited any further despite set value is being displayed(through admin site).
## models.py
class Table(models.Model):
label = models.CharField(max_length=40) # remove editable option
## admin.py
#admin.register(Table)
class TableAdmin(admin.ModelAdmin):
def get_readonly_fields(self, request, obj=None):
defaults = super().get_readonly_fields(request, obj=obj)
if obj: # if we are updating an object
defaults = tuple(defaults) + ('label', ) # make sure defaults is a tuple
return defaults
Bonus for Approach 2
Also if you have multiple fields on that table you can use fields property to set the ordering(read only fields which are not specifically ordered will be shown at the end of the field list). Down side for this ordering approach is that you have to remember to reflect model changes to fields property every time you make a change in your model.

Fill a form field with initial values from another model (which is a foreign key)

I have 2 models, Company and Post.
class Post(Meta):
company = models.ForeignKey(Company, related_name='company', on_delete=models.CASCADE)
company_description = models.TextField(max_length=800)
What I need:
1) When a user create a post, the company FK will be set thru code
2) Also at form initialization the field company_description, will be prepopulated from the Company model, field description(not the same name)
3) On save if the user doesn't modify anything to the field, the initial value will be save
1,2,3 only on creation.
I checked Django documentation regarding initialization
but their example, is more simple they just get some data/text, in my case I first need to set FK, than get description from the FK Model, so not just text.
The pk/slug of the company I can get from url or from request thru multiple calls request.user.acc.company
path('<int:pk>/post/add/', CompanyPostCreateView.as_view(), name='create')
and in the view:
company_pk = kwargs.get('pk')
and overwrite the form_valid, but here is the issue, form_valid is called on validation, and I want to show this info before validation on form initialization, and I don't know how.
There isn't anything particularly complicated about this. You can get the FK at the start of the view and use it in the initial dictionary:
company = Company.objects.get(pk=company_pk)
form = PostForm(intial={'company_description': company.description})
Edit With a CreateView, you could just override get_initial:
def get_initial(self):
company = Company.objects.get(pk=self.kwargs['pk'])
return {'company_description': company.description}
I found a solution by overwriting get_form_kwargs
def get_form_kwargs(self):
self.company = Company.objects.get(pk=self.kwargs['pk'])
kwargs = super().get_form_kwargs()
kwargs['initial']['company_description'] = self.company.description
return kwargs
I use self.company to pass later to a different method, not redo the query.

ForeignKey field will not appear in Django admin site

A foreign key on a model is not appearing in the Django admin site. This is irrespective of whether the field is explicitly specified in a ModelAdmin instance (fields = ('title', 'field-that-does-not-show-up')) or not.
I realize there are many variables that could be causing this behavior.
class AdvertiserAdmin(admin.ModelAdmin):
search_fields = ['company_name', 'website']
list_display = ['company_name', 'website', 'user']
class AdBaseAdmin(admin.ModelAdmin):
list_display = ['title', 'url', 'advertiser', 'since', 'updated', 'enabled']
list_filter = ['updated', 'enabled', 'since', 'updated', 'zone']
search_fields = ['title', 'url']
The problem is the advertiser foreign key is not showing up in the admin for AdBase
class Advertiser(models.Model):
""" A Model for our Advertiser
"""
company_name = models.CharField(max_length=255)
website = models.URLField(verify_exists=True)
user = models.ForeignKey(User)
def __unicode__(self):
return "%s" % self.company_name
def get_website_url(self):
return "%s" % self.website
class AdBase(models.Model):
"""
This is our base model, from which all ads will inherit.
The manager methods for this model will determine which ads to
display return etc.
"""
title = models.CharField(max_length=255)
url = models.URLField(verify_exists=True)
enabled = models.BooleanField(default=False)
since = models.DateTimeField(default=datetime.now)
expires_on=models.DateTimeField(_('Expires on'), blank=True, null=True)
updated = models.DateTimeField(editable=False)
# Relations
advertiser = models.ForeignKey(Advertiser)
category = models.ForeignKey(AdCategory)
zone = models.ForeignKey(AdZone)
# Our Custom Manager
objects = AdManager()
def __unicode__(self):
return "%s" % self.title
#models.permalink
def get_absolute_url(self):
return ('adzone_ad_view', [self.id])
def save(self, *args, **kwargs):
self.updated = datetime.now()
super(AdBase, self).save(*args, **kwargs)
def impressions(self, start=None, end=None):
if start is not None:
start_q=models.Q(impression_date__gte=start)
else:
start_q=models.Q()
if end is not None:
end_q=models.Q(impression_date__lte=end)
else:
end_q=models.Q()
return self.adimpression_set.filter(start_q & end_q).count()
def clicks(self, start=None, end=None):
if start is not None:
start_q=models.Q(click_date__gte=start)
else:
start_q=models.Q()
if end is not None:
end_q=models.Q(click_date__lte=end)
else:
end_q=models.Q()
return self.adclick_set.filter(start_q & end_q).count()
class BannerAd(AdBase):
""" A standard banner Ad """
content = models.ImageField(upload_to="adzone/bannerads/")
The mystery deepens. I just tried to create a ModelForm object for both AdBase and BannerAd, and both generated fields for the advertiser. Some crazy admin things going on here...
I believe I've just run into exactly the same problem, but was able to debug it thanks to the help of persistent co-workers. :)
In short, if you look in the raw HTML source you'll find the field was always there - it's just that:
Django tries to be clever and put the form field inside a div with CSS class="form-row $FIELD_NAME",
The field's name was "advertiser", so the CSS class was "form-row advertiser",
...Adblock Plus.
Adblock Plus will hide anything with the CSS class "advertiser", along with a hell of a lot of other CSS classes.
I consider this a bug in Django.
maybe it is an encode error. I had the same problem, but when i added # -- coding: UTF-8 -- in the models.py, all fine.
Another very dumb cause of the same problem:
If there is only one instance of the related model, then the filter simply won't show. There is a has_output() method in RelatedFieldListFilter class that returns False in this case.
It's a strange problem for sure. On the AdBase model if you change
advertiser = models.ForeignKey(Advertiser)
to
adver = models.ForeignKey(Advertiser)
then I believe it'll show up.
Powellc, do you have the models registered with their respective ModelAdmin class?
admin.site.register(Advertiser, AdvertiserAdmin) after the ModelAdmin definitions.
You are talking about the list_display option, right?
Is the unicode-method for your related model set?
If the field is a ForeignKey, Django
will display the unicode() of the
related object
Also check this thread for some hints: Can "list_display" in a Django ModelAdmin display attributes of ForeignKey fields?
Try disabling your ad blocker. No, this is not a joke. I just ran into this exact problem.
We just ran into this problem.
It seems that if you call you field advertiser the in the admin the gets given an 'advertiser' class.
Then is then hidden by standard ad blocking plugins. If you view source your field will be there.

Django admin: How to display a field that is marked as editable=False' in the model?

Even though a field is marked as 'editable=False' in the model, I would like the admin page to display it. Currently it hides the field altogether.. How can this be achieved ?
Use Readonly Fields. Like so (for django >= 1.2):
class MyModelAdmin(admin.ModelAdmin):
readonly_fields=('first',)
Update
This solution is useful if you want to keep the field editable in Admin but non-editable everywhere else. If you want to keep the field non-editable throughout then #Till Backhaus' answer is the better option.
Original Answer
One way to do this would be to use a custom ModelForm in admin. This form can override the required field to make it editable. Thereby you retain editable=False everywhere else but Admin. For e.g. (tested with Django 1.2.3)
# models.py
class FooModel(models.Model):
first = models.CharField(max_length = 255, editable = False)
second = models.CharField(max_length = 255)
def __unicode__(self):
return "{0} {1}".format(self.first, self.second)
# admin.py
class CustomFooForm(forms.ModelForm):
first = forms.CharField()
class Meta:
model = FooModel
fields = ('second',)
class FooAdmin(admin.ModelAdmin):
form = CustomFooForm
admin.site.register(FooModel, FooAdmin)
Add the fields you want to display on your admin page.
Then add the fields you want to be read-only.
Your read-only fields must be in fields as well.
class MyModelAdmin(admin.ModelAdmin):
fields = ['title', 'author', 'published_date', 'updated_date', 'created_date']
readonly_fields = ('updated_date', 'created_date')
You could also set the readonly fields as editable=False in the model (django doc reference for editable here). And then in the Admin overriding the get_readonly_fields method.
# models.py
class MyModel(models.Model):
first = models.CharField(max_length=255, editable=False)
# admin.py
class MyModelAdmin(admin.ModelAdmin):
def get_readonly_fields(self, request, obj=None):
return [f.name for f in obj._meta.fields if not f.editable]
With the above solution I was able to display hidden fields for several objects but got an exception when trying to add a new object.
So I enhanced it like follows:
class HiddenFieldsAdmin(admin.ModelAdmin):
def get_readonly_fields(self, request, obj=None):
try:
return [f.name for f in obj._meta.fields if not f.editable]
except:
# if a new object is to be created the try clause will fail due to missing _meta.fields
return ""
And in the corresponding admin.py file I just had to import the new class and add it whenever registering a new model class
from django.contrib import admin
from .models import Example, HiddenFieldsAdmin
admin.site.register(Example, HiddenFieldsAdmin)
Now I can use it on every class with non-editable fields and so far I saw no unwanted side effects.
You can try this
#admin.register(AgentLinks)
class AgentLinksAdmin(admin.ModelAdmin):
readonly_fields = ('link', )

can't override default admin model form django

I need to add extra validation to my DateField in Admin to make sure the date given is in the future. I have no experience in such a thing, so here's what I've done.
1) I've created custom form field and added validation to it:
class PastDateField(forms.DateField):
def clean(self, value):
"""Validates if only date is in the past
"""
if not value:
raise forms.ValidationError('Plase enter the date')
if value > datetime.now():
raise forms.ValidationError('The date should be in the past, not in future')
return value
2) Then I've added custom model form:
class CustomNewsItemAdminForm(forms.ModelForm):
title = forms.CharField(max_length=100)
body = forms.CharField(widget=forms.Textarea)
date = PastDateField()
region = forms.ModelChoiceField(Region.objects)
3) And here's how I've registered admin:
class NewsItemAdmin(admin.ModelAdmin):
form = CustomNewsItemAdminForm
def queryset(self, request):
return NewsItem.objects.all()
admin.site.register(NewsItem, NewsItemAdmin)
The result of this is that my admin form
1) Shows field I haven't specified in custom admin form
2) Lacks JavaScript calendar for the datetime field
It's pretty obvious to me that I'm doing something wrong, but I've found no examples relevant to my needs as I am a noob. What is the better way to add custom validation to datetime field without messing things up?
EDIT: Thanks a lot to Brian Luft and Daniel Roseman for correct answers! To make this post helpful for someone facing the same problem here is the resulting code:
class CustomNewsItemAdminForm(forms.ModelForm):
class Meta:
model = NewsItem
def clean_date(self):
"""Validates if only date is in the past
"""
date = self.cleaned_data["date"]
if date is None:
raise forms.ValidationError('Plase enter the date')
if date > datetime.now().date():
raise forms.ValidationError('The date should be in the past, not in future')
return self.cleaned_data["date"]
class NewsItemAdmin(admin.ModelAdmin):
form = CustomNewsItemAdminForm
def queryset(self, request):
return NewsItem.objects.all()
admin.site.register(NewsItem, NewsItemAdmin)
Firstly, declaring fields explicitly on a ModelForm - whether in or out of the admin - does not mean that the other fields will not be displayed. You need to define the fields or exclude tuples in the form's inner Meta class. If the other fields are all the default, you can simply declare the one you are overriding.
Secondly, if you want your custom field to use the javascript, you'll need to use the right widget, which is django.contrib.admin.widgets.AdminDateWidget. However, there is a much easier way to do this, which is not define a custom field at all, but instead define a clean_date method on the form itself.