I know this issue has been asked more than once, but as Django is evolving with new version, I'll ask the question again :
I am using the model User (Django User, not in my models.py) and create another model with a Foreign key to User.
models.py :
class Plan(models.Model):
user = models.ForeignKey(User)
I can simply display every Plan in my user by doing this in admin.py :
class PlanInline(admin.TabularInline):
model = Plan
extra = 0
class MyUserAdmin(UserAdmin):
ordering = ('-date_joined', 'username')
inlines = [PlanInline,]
admin.site.unregister(User)
admin.site.register(User, MyUserAdmin)
But things are about to get more tricky. I want to add a model that has a foreign key pointing to Plan :
class Order(models.Model):
plan = models.ForeignKey('Plan')
And I want to be able to see all Orders for each Plan. As of today, it is impossible to have nested inlines in Django Admin (without editing the HTML, which I want to avoid) :
User
-> Plan 1
-> Order 1
-> Order 2
-> Plan 2
-> Order 3
So my idea is to display in the User Admin only A LINK for each plan, to the page to edit Plans, and put Orders as inline :
class OrderInline(admin.TabularInline):
model = Order
extra = 0
class PlanAdmin(admin.ModelAdmin):
inlines = [OrderInline,]
admin.site.register(Plan, PlanAdmin)
The question is, how do I display a link to a Plan in my User Admin?
class MyUserAdmin(UserAdmin):
ordering = ('-date_joined', 'username')
??? LINK ????
I saw some solutions on this topic : Django InlineModelAdmin: Show partially an inline model and link to the complete model, but they are a bit "dirty' as they make us write HTML and absolute path into the code.
Then I saw this ticket on Djangoproject : https://code.djangoproject.com/ticket/13163. It seems exactly what I'm looking for, and the ticket is "fixed". So I tried adding like in the fix show_change_link = True :
class PlanInline(admin.TabularInline):
model = Plan
extra = 0
show_change_link = True
class MyUserAdmin(UserAdmin):
ordering = ('-date_joined', 'username')
show_change_link = True
inlines = [UserProfileInline, PlanInline]
But it doesn't work (and I have no log or error).
Is there any way to do this in a clean way?
Update for django 1.8
show_change_link = True
https://github.com/django/django/pull/2957/files
I suggest adding a custom PlanInline method that returns the link and see if it helps. Something along these lines:
from django.utils.safestring import mark_safe
from django.core.urlresolvers import reverse
class PlanInline(TabularInline):
model = Plan
readonly_fields = ('change_link',)
...other options here...
def change_link(self, obj):
return mark_safe('Full edit' % \
reverse('admin:myapp_plan_change',
args=(obj.id,)))
Basically all we do here is create the custom method that returns a link to the change page (this specific implementation is not tested, sorry if there is any parse error but you get the idea) and then add it to the readonly_fields as described here: https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.readonly_fields
A couple of notes for the change_link method: You need to replace 'myapp' in the view name with your actual application name. The mark_safe method just marks the text as safe for the template engine to render it as html.
Related
I've been looking on the documentation and stackoverflow/forums for a way to ignore the inline children of a model when I save it in django admin. I've been searching for a few days and I can't seem to find an answer.
I have a normal tabularinline object:
class UserOrdersAdmin(admin.TabularInline):
model = Order
classes = ['collapse']
And a normal User admin registration:
class UserAdmin(BaseUserAdmin):
inlines = (UserOrdersAdmin, UserSettingsAdmin)
admin.site.unregister(User)
admin.site.register(User, UserAdmin)
What I simply want is when I press save within the User "change view", it will ignore the inline "UserOrderAdmin" which is inline to UserAdmin.
From your response on my comment I am getting the idea you want to show some extra information in the admin, that is not editable. This can be achieved using readonly_fields in the Inline, for completeness you should also set the max_num to 0, because otherwise you can add empty inlines.
You could enter all fields manually or use something like given in this answer: https://stackoverflow.com/a/42877484/2354734
The end result would look something like this.
class UserOrdersAdmin(admin.TabularInline):
model = Order
classes = ['collapse']
max_num = 0
def get_readonly_fields(self, request, obj=None):
return list(set(
[field.name for field in self.opts.local_fields] +
[field.name for field in self.opts.local_many_to_many]
))
For completeness of the answer also a link to the documentation
Try this:
class UserOrdersAdmin(admin.TabularInline):
model = Order
classes = ['collapse']
extra = 0
The story begins with 2 models (User and ClientAccount) which are linked with an extra M2M model.
It is possible to create ClientAccount during editing User. The page will show a popup which allow you create a new ClientAccount. But the problem is: can I disable the foreign key fields of ClientAccount which is linking to User? This is quite confusing.
Code here:
class User(models.Model):
client_accounts = models.ManyToManyField('ClientAccount', related_name='+', through='UserClientAccountM2M', through_fields=('user', 'client_account'))
class ClientAccount(models.Model):
users = models.ManyToManyField('User', related_name='+', through='UserClientAccountM2M', through_fields=('client_account', 'user'))
class UserClientAccountM2M(models.Model):
user = models.ForeignKey(User, db_column='user_id')
client_account = models.ForeignKey(ClientAccount, db_column='client_id')
class UserAdmin(TimeLimitedAdmin):
class ClientAccountInline(admin.TabularInline):
model = ClientAccount.users.through
inlines = [
ClientAccountInline,
]
class ClientAccountAdmin(TimeLimitedAdmin):
class UserInline(admin.TabularInline):
model = ClientAccount.users.through
inlines = [
UserInline,
]
admin.site.register(User, UserAdmin)
I realise this is an old question, but I came across it as the most relevant question whilst looking to solve a similar problem (hide a FK inline only in the add popup). I'm sure you have left this problem well behind by now, but maybe others will find it useful.
I added the below to the ModelAdmin class that the popup is for. I check the request for the GET params only present when the popup dialog is launched, and if they are there, I loop through the inlines and remove the one I don't want.
def get_inline_instances(self, request, obj=None):
inline_instances = super().get_inline_instances(request, obj=None)
if '_to_field' in request.GET and '_popup' in request.GET:
# Popup dialog is open.
unwanted_inline = None
for inline in inline_instances:
inline_model_name = inline.opts.model.__name__
if inline_model_name == 'UnwantedModelName':
unwanted_inline = inline
if unwanted_inline:
inline_instances.remove(unwanted_inline)
return inline_instances
In your case, I would add the above to ClientAccountAdmin, removing the UserInline.
If you simply want to hide the m2m fields in the ClientAccount you can remove the line in the Admin.py
because there you explicitely say it should show the connection to the user in a tabularInline:
class ClientAccountAdmin(TimeLimitedAdmin):
class UserInline(admin.TabularInline):
model = ClientAccount.users.through
#inlines = [UserInline,]
There you say explicitely you want to have the M2M field from ClientAccount to User in an Inline, which you do not want. Get rid of it and the fields will disappear
EDIT:
The problem is that the "add..." link will always refer to the .../ClientAccount/add/?_popup=1 page which uses the default admin view for this model.
I need to implement the following:
The user shall be presented with a form that will have a drop down choice menu consisting of property names. There are two types of properties: general properties, i.e. properties common for all users and custom properties, i.e. properties that each user has defined prior to that. The models would look something like that:
class GeneralPropertyName(models.Model):
name = models.CharField(max_length=20)
class CustomPropertyName(models.Model):
user = models.ForeignKey(User)
name = models.CharField(max_length=20)
The drop down menu should have all general properties and only those custom properties that pertain to the user.
First question: how to define such a model?
I need to: 1. somehow unify both properties, 2. take only those items from CustomPropertyName that pertain to the user
class SpecData(models.Model):
user = models.ForeignKey(User)
selection_title = models.CharField(max_length=20)
property = ForeignKey(GeneralPropertyName) ??UNIFY??? ForeignKey(CustomPropertyName)
Second, is there anything special that needs to be done with ModelForm?
class SpecDataForm(ModelForm):
class Meta:
model = SpecData
And the 3rd question is what needs to be done in the view? I will need to use inline formsets since I will have a few dynamic forms like that.
def index(request):
user = User.objects.get(username=request.user.username)
specdataFormSet = inlineformset_factory(User, SpecData, form=SpecDataForm, extra=30)
...
specdata_formset = specdataFormSet(instance=user, prefix='specdata_set')
...
Thanks.
EDIT: Adjusted juliocesar's suggestion to include formsets. Somehow I am getting the following error message: Cannot resolve keyword 'property' into field. Choices are: id, name, selection_title, user
def index(request):
user = User.objects.get(username=request.user.username)
user_specdata_form = UserSpecDataForm(user=user)
SpecdataFormSet = inlineformset_factory(User, SpecData, form=user_specdata_form, extra=30)
You can use a GenericForeignKey to handle it, but you still need more to solve your further questions about forms and view.
I have made an example of how you solve your problem (logged user can select from General properties and his Custom properties, non-logged user only can select General properties). I used model inheritance for the properties (In your sample code it seems that a CustomPropertyName is a PropertyName with other fields). I think inheritance is an easier and a more basic concept than ContentTypes and it fits to your needs.
NOTE: I remove some code like imports to simplify the code.
1) models.py file:
class PropertyName(models.Model):
name = models.CharField(max_length=20)
def __unicode__(self):
return self.name
class CustomPropertyName(PropertyName): # <-- Inheritance!!
user = models.ForeignKey(User)
def __unicode__(self):
return self.name
class SpecData(models.Model):
user = models.ForeignKey(User)
selection_title = models.CharField(max_length=20)
property = models.ForeignKey(PropertyName)
NOTES: The field SpecData.property points to PropertyName since all properties are saved in the PropertyName's database table.
2) forms.py file:
from django import forms
from django.db.models import Q
from models import SpecData, PropertyName
def UserSpecDataForm(user=None):
UserPropertiesQueryset = PropertyName.objects.filter(Q(custompropertyname__user=None) | Q(custompropertyname__user__id=user.id))
class SpecDataForm(forms.ModelForm):
property = forms.ModelChoiceField(queryset=UserPropertiesQueryset)
class Meta:
model = SpecData
exclude = ('user',)
return SpecDataForm
NOTES: The trick here is to generate the form SpecDataForm dynamically, by filtering properties according the user specified in the parameter.
3) views.py file:
from forms import UserSpecDataForm
def index(request):
if request.POST:
form = UserSpecDataForm(request.user)(request.POST) # instance=user
if form.is_valid():
spec_data = form.save(commit=False)
spec_data.user = request.user
spec_data.save()
else:
form = UserSpecDataForm(request.user)()
return render_to_response('properties.html', {'form': form}, context_instance=RequestContext(request))
NOTES: Nothing special here, just a call to form.UserSpecDataForm(request.user) that returns the form class and then instantiate. Also setted the logged-in user to the object returned on save since It was excluded in the form to not show in front-end.
Following this basic example you can do the same with formsets if you need it.
UPDATE:
Formset can be used by adding following code to the view:
user_specdata_form = UserSpecDataForm(user=request.user)
SpecdataFormSet = inlineformset_factory(User, SpecData, form=user_specdata_form, extra=30)
The complete project sample can be downloaded from http://ge.tt/904Wg7O1/v/0
Hope this helps
1a) have you looked into django's ContentType framework this will allow you to have generic foreign keys and you can put restrictions on what types of models are acceptable to store in.
1b) I think that the validation for accepting what type of foreign key is acceptable shouldn't be in your model but should be part of your form validation before saving.
2) If you do use a model form you're going to have to define your own custom widget for the propery field. This means you're probably going to have to write you're own render function to render the html from the field. You should also define your own validation function on the form to make sure that only the appropriate data is acceptable to save.
3) I don't think you'll have to do anything you aren't already doing in the views
Use GenericForeignKey:
class SpecData(models.Model):
user = models.ForeignKey(User)
selection_title = models.CharField(max_length=20)
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
property = GenericForeignKey('content_type', 'object_id')
You can use this to combine the two fields(type & id) into a single choice field.
One way is that you have only one model, make user nullable:
class PropertyName(models.Model):
user = models.ForeignKey(User, null=True, blank=True)
name = models.CharField(max_length=20)
class SpecData(models.Model):
user = models.ForeignKey(User)
selection_title = models.CharField(max_length=20)
property = ForeignKey(PropertyName)
So, if user is not set, it is a general property. If it is set, it is related to this user.
However, please note that if you need unique property names, that NULL != NULL.
Of course, the suggested GenericForeignKey solution is better for some cases.
Also, you can easily make the normal (non-model) form with that you describe and separate form logic from model logic.
I need a nested django admin inline,
which I can include the date field inlines in an other inline like below.
I have the models below:
class Person(models.Model):
name = models.CharField(max_length=200)
id_no = models.IntegerField()
class Certificate(models.Model):
cerfificate_no = models.CharField(max_length=200)
certificate_date = models.DateField(max_length=100)
person = models.ForeignKey(Person)
training = models.CharField(max_length=200)
class Training_Date(models.Model):
date = models.DateField()
certificate = models.ForeignKey(Certificate)
And, the admin below:
class CertificateInline(admin.StackedInline):
model = Certificate
class PersonAdmin(admin.ModelAdmin):
inlines = [CertificateInline,]
admin.site.register(Person,PersonAdmin)
But, I need to include the Training_Date model as inline which is part of Certificate admin inline.
Any idea?
There has been some movement in https://code.djangoproject.com/ticket/9025 recently, but I wouldn't hold my breath.
One common way around this is to link to an admin between first and second (or second and third) level by having both a ModelAdmin and an Inline for the same model:
Give Certificate a ModelAdmin with TrainingDate as an inline. Set show_change_link = True for CertificateInline so you can click on an inline to go to its ModelAdmin change form.
admin.py:
# Certificate change form has training dates as inline
class TrainingDateInline(admin.StackedInline):
model = TrainingDate
class CertificateAdmin(admin.ModelAdmin):
inlines = [TrainingDateInline,]
admin.site.register(Certificate ,CertificateAdmin)
# Person has Certificates inline but rather
# than nesting inlines (not possible), shows a link to
# its own ModelAdmin's change form, for accessing TrainingDates:
class CertificateLinkInline(admin.TabularInline):
model = Certificate
# Whichever fields you want: (I usually use only a couple
# needed to identify the entry)
fields = ('cerfificate_no', 'certificate_date')
# Django 1.8 introduced this, no need to make your own link
show_change_link = True
class PersonAdmin(admin.ModelAdmin):
inlines = [CertificateLinkInline,]
admin.site.register(Person, PersonAdmin)
More universal solution
from django.utils.safestring import mark_safe
from django.urls import reverse
class EditLinkToInlineObject(object):
def edit_link(self, instance):
url = reverse('admin:%s_%s_change' % (
instance._meta.app_label, instance._meta.model_name), args=[instance.pk] )
if instance.pk:
return mark_safe(u'edit'.format(u=url))
else:
return ''
class MyModelInline(EditLinkToInlineObject, admin.TabularInline):
model = MyModel
readonly_fields = ('edit_link', )
class MySecondModelAdmin(admin.ModelAdmin):
inlines = (MyModelInline, )
admin.site.register(MyModel)
admin.site.register(MySecondModel, MySecondModelAdmin)
AFAIK, you can't have a second level of inlines in the default Django admin.
The Django admin is just a normal Django application, so nothing prevents you from implementing a second level of nested forms, but IMHO it would be a kind of convoluted design to implement. Perhaps that is why there is no provision for it.
pip install django-nested-inline
This package should do what you need.
Nested inlines are provided at:
https://github.com/BertrandBordage/django-super-inlines/
pip install django-super-inlines
A more up to date solution (february 2021) is to use the show_change_link config variable: https://docs.djangoproject.com/en/3.1/ref/contrib/admin/#django.contrib.admin.InlineModelAdmin.show_change_link
This does exactly the same as the EditLinkToInlineObject proposed in solutions above, but is less code and is probably well tested by Django Developers
You would just have to define show_change_link=True in each one of your inlines
UPDATE (January 25th, 2022):
Here's the updated link in the docs (Django 4.0): https://docs.djangoproject.com/en/4.0/ref/contrib/admin/#django.contrib.admin.InlineModelAdmin.show_change_link
Use django-nested-admin which is the best package to do nested inlines.
First, install "django-nested-admin":
pip install django-nested-admin
Then, add "nested_admin" to "INSTALLED_APPS" in "settings.py":
# "settings.py"
INSTALLED_APPS = (
# ...
"nested_admin", # Here
)
Then, add "path('_nested_ad..." to "urlpatterns" in "urls.py":
# "urls.py"
from django.urls import include, path
urlpatterns = [
# ...
path('_nested_admin/', include('nested_admin.urls')), # Here
]
Finally, extend "NestedTabularInline" with "Training_DateInline()" and "CertificateInline()" classes and extend "NestedModelAdmin" with "PersonAdmin()" class in "admin.py" as shown below:
# "admin.py"
from .models import Training_Date, Certificate, Person
from nested_admin import NestedTabularInline, NestedModelAdmin
class Training_DateInline(NestedTabularInline):
model = Training_Date
class CertificateInline(NestedTabularInline):
model = Certificate
inlines = [Training_DateInline]
#admin.register(Person)
class PersonAdmin(NestedModelAdmin):
inlines = [CertificateInline]
I used the solution provided by #bigzbig (thank you).
I also wanted to go back to the first list page once changes had been saved so added:
class MyModelInline(EditLinkToInlineObject, admin.TabularInline):
model = MyModel
readonly_fields = ('edit_link', )
def response_post_save_change(self, request, obj):
my_second_model_id = MyModel.objects.get(pk=obj.pk).my_second_model_id
return redirect("/admin/mysite/mysecondmodel/%s/change/" % (my_second_model_id))
So I've set up my django site with the following admin.py:
import models
from django.contrib import admin
admin.site.register(models.Comment)
which uses this models.py:
from django.db import models
class Comment(models.Model):
text = models.CharField(max_length=400)
name = models.CharField(max_length=100)
date = models.DateTimeField(auto_now = True)
article = models.CharField(max_length=100)
However, when I go to the admin page, it shows the following:
Which generally is not very helpful. Clicking on each link gives me a page with that object's data, but I would like to be able to see the information for each object in this view. I've been looking at the ModelAdmin class at:
https://docs.djangoproject.com/en/dev/ref/contrib/admin/
but have not managed to wrap my head around it. Is it a separate model class that needs to be kept in sync with my "actual" model? Is it just an interface through which my Admin site accesses the actual model? Does it do what I want (allowing useful data to be shown in the admin interface) or does it do something else?
I'm thinking that the Django Admin page should be able to replace PHPMyAdmin for doing simple tasks, like browsing the DB and manually modifying individual objects. Is that the case?
The admin turns your object into a string so just put a def __str__ or def __unicode__
(As #Mandax has reminded me the docs recommend to define __unicode__ only.)
def __unicode__(self);
return u"%s (%s): %s" % (self.article, self.date, self.name)
Just as it says in the documentation, your model's ModelAdmin describes how the admin section will represent your model. It does need to be somewhat in sync with the actual model, it doesn't make sense for you to display fields that aren't present on your model, etc. You seem interested in the changelist view, which has numerous customization options (all described in the documentation, and in the tutorial). A simple start might be:
from django.contrib import admin
class CommentAdmin(admin.ModelAdmin):
# define which columns displayed in changelist
list_display = ('text', 'name', 'date', 'article')
# add filtering by date
list_filter = ('date',)
# add search field
search_fields = ['text', 'article']
admin.site.register(Comment, CommentAdmin)
There are a lot of options for customization, as always refer to the docs! Finally, you could certainly use it in lieu of PHPMyAdmin, it's very easy to setup admin for browsing, modifying object, etc, how much use you get out of it is up to you.