django admin for abstract base class? - django

Suppose I have car - suv/bus models.
I'd like to list all cars in the django admin (with name attribute).
When user clicks one of the car, it would go to the appropriate detail page of either suv or bus model.
How do I create a such admin list page?
class Car:
name = models.CharField()
class Meta:
abstract=True
class Suv(Car):
pass
class Bus(Car):
pass

Not sure this is the best approach, but sharing my solution here.
First, create a Database View
create view [app_label_model_name] as
select id, name, 1 as car_type
from suv
union
select id, name, 2 as car_type
from bus
order by something;
then create non-manged model
class PostBaseView(models.Model):
# this would be CarBaseView, I'm copying my actual code
id = models.IntegerField()
raw_html = models.TextField(null=True)
created_at = models.DateTimeField(primary_key=True)
post_type = models.IntegerField()
class Meta:
managed = False
then, from admin page, change the links based on subclass types.
class ChangeList(ChangeListDefault):
def url_for_result(self, result):
# pk = getattr(result, self.pk_attname)
id = result.id
app_label = result.get_app_label()
model_name = result.get_model_name()
return reverse('admin:%s_%s_change' % (app_label,
model_name),
args=(quote(id),))
class PostBaseViewAdmin(admin.ModelAdmin):
list_display = ['__str__', 'post_type_str']
class Meta:
model = PostBaseView
def get_changelist(self, request, **kwargs):
"""
Returns the ChangeList class for use on the changelist page.
"""
return ChangeList
admin.site.register(PostBaseView, PostBaseViewAdmin)
volla you have a admin that shows multiple subclasses at one list.

eugene's answer took me to the right direction. Thank you.
I'm not aware if there is a better way either, but in my case this approach solved the problem.
I already had a base class (Project) with some generic fields (SportProject and others), and specific classes that extends Project with their fields, and I wanted to have a single list of project, but wanted to have specific edit form for each project type.
In app/admin.py file, I did:
from django.contrib.admin.views.main import ChangeList as ChangeListDefault
from django.urls import reverse
class ProjectChangeList(ChangeListDefault):
def url_for_result(self, result):
app_label = result._meta.app_label.lower()
model_name = result.children._meta.object_name.lower()
return reverse('admin:%s_%s_change' % (app_label, model_name), args=(result.id,))
class ProjectDataAdmin(admin.ModelAdmin):
#...
def get_changelist(self, request, **kwargs):
"""
Returns the ChangeList class for use on the changelist page.
"""
return ProjectChangeList
In my model Project, I have a children property that returns the children instance (SportProject for instance). Not sure if there is another way to have this structure in Django.
I also had all classes registered in django-admin (Project and all children, as SportProject), to have django-admin pages for those classes. Thus, I'm using django-modeladmin-reorder to hide undesired links on django-admin main page.
Hope it helps someone.

You need to make your Car not an abstract model, as you have to have some base table for your vehicles.
class Car:
name = models.CharField()
class Meta:
abstract = False
class Suv(Car):
pass
class Bus(Car):
pass
class CarAdmin(admin.ModelAdmin):
model = Car
But here the easy things end. The admin doesn't have a built-in solution for your task. I suggest you overriding at least the CarAdmin.get_form() and CarAdmin.get_queryset() methods, though I'm not sure they are the only ones you have to customize.

I think this answer is applicable. ModelAdmin needs to know foreign key entries will be readonly, specified in a tuple called readonly_fields.
Using the problem that brought me here and there, I have (models.py):
class Choice(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE)
choice_text = models.CharField(max_length=200)
def __str__(self):
return self.choice_text
class Answer(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE, default = 1)
class Meta:
abstract = True
class Vote(Answer):
choice = models.ForeignKey(Choice, on_delete=models.CASCADE)
def answer(self):
return self.choice
def __str__(self):
return self.choice.choice_text
And (admin.py):
class VoteAdmin(admin.ModelAdmin):
#list_display = ('Answer.question.question_text', 'Answer.User.user_id', 'Choice.choice_text')
readony_fields = ('question', 'user')
list_display = ('question', 'user', 'choice')
fieldsets = [
('Question', {'fields': ['question']}),
('User', {'fields': ['user']}),
('Vote', {'fields' : ['choice']}),
]
Hope this proves useful to future searchers.

Related

Display information about linked models fields in the django admin

These are my models:
class Partner(models.Model):
name = models.CharField(max_length=200, verbose_name="Organisation name")
class ResearchActivity(models.Model):
title = models.CharField(max_length=200)
partner = models.ManyToManyField(ActivityPartner, blank=True)
I'd like, in the Django administration forms, to have a field in my Partner edit form representing the ResearchActivity linked to that Partner.
Can this be achieved by adding a field to my Partner model (say, naming it linked_partner) and then edit my admin.py like so:
#admin.register(ActivityPartner)
class ActivityPartnerAdmin(admin.ModelAdmin):
search_fields = ['academic',]
autocomplete_fields = ['partnership_type', 'relationship_type', 'academic_links']
def get_changeform_initial_data(self, request):
return {'live_contract': ResearchActivity.objects.all().filter(linked_partner__id=request.ResearchActivity.partner.id)}
?
I have just come across in the display() decorator, new from Django 3.2. With it, I can simply do:
#admin.register(ActivityPartner)
class ActivityPartnerAdmin(admin.ModelAdmin):
search_fields = ['academic',]
autocomplete_fields = ['partnership_type', 'relationship_type', 'academic_links',]
readonly_fields = ('get_ra',)
#admin.display(description='Live contract(s)')
def get_ra(self, obj):
return list(ResearchActivity.objects.filter(partner=obj.id))
to achieve what I want.
If I also wanted to edit those ManyToMany relations, I can use the inlines option:
class LiveContractsInline(admin.TabularInline):
model = ResearchActivity.partner.through
#admin.register(ActivityPartner)
class ActivityPartnerAdmin(admin.ModelAdmin):
inlines = [
LiveContractsInline,
]

Mutual relationship between two Django models

I have the (simple, I suppose) need of having a situation like so: there are many profiles, and there are many ensembles, and each profile has to be able to be part of one or more ensembles. This is my code:
class Ensemble(models.Model):
ensembleName = models.CharField(max_length=200)
members = models.ManyToManyField('Profile', related_name='members')
def __str__(self):
return self.ensembleName
class Profile(models.Model):
ensemble = models.ForeignKey(Ensemble, on_delete=models.CASCADE, blank=True, null=True)
[...]
It all works well, but to an extent. From the Django administration I can, from the 'ensemble' page, select its members. I can also select, from the 'profile' page, which ensembles that profile belongs. The issue is: they are not synchronised: if I add a profile to an ensemble via the 'profile' page this is not reflected in the 'ensemble' page and the other way round, i.e. in the 'profiles details' page I don't see the ensemble to which I previously assigned that profile from the 'ensemble' page.
My form
class ProfileUpdateForm(forms.ModelForm):
class Meta:
model = Profile
fields = ('image', 'role', 'skills', 'gender', etc...)
class EnsemblesForm(forms.ModelForm):
class Meta:
model = Ensemble
fields = ('ensemble_name',)
def __init__(self, *args, **kwargs):
super(EnsemblesForm, self).__init__(*args, **kwargs)
self.fields['ensemble_name'].queryset = (obj for obj in Ensemble.objects.all()) #This doesn't output anything

how to create django form with multiplechoicefield and add those value to database

what I want to achieve is user will submit 3 inputs in the form 1) name 2) dropdown to select technician, 3) multiselect dropdown to select multiple products. Once the user submit the details
it will generate one lead in database with value like name,foreignkey of selected technician and id of selected products in different table. I don't know how to achieve this below I have mentioned my approch to achieve what I want. Please let me know if the models need any changes and how I can write a view for the same.
models.py
class product(models.Model):
name = models.CharField(max_length=20)
class technician(models.Model):
name = models.CharField(max_length=20)
class lead(models.Model):
name = models.CharField(max_length=20)
technician = models.ForeignKey(technician,on_delete=models.SET_NULL,null=True) #only single selection
products = models.ManyToManyField(product) #user can select multiple product in dropdown
form.py
class leadForm(form.ModelForm):
products = forms.MultipleChoiceField(queryset=Product.objects.all())
technician = forms.CharField(max_length=30,choices=[(i.id,i.name) for i in Technician.objects.all().values('id','name')
class Meta:
model = lead
fields = ('name','technician')
You should use a ModelMultipleChoiceField [Django-doc] here. The But in fact you do not need to implement the models yourself. You can simply let the Django logic do the work for you.
In order to give a textual representation at the HTML end, you can override the __str__ functions of the models:
class Product(models.Model):
name = models.CharField(max_length=20)
def __str__(self):
return self.name
class Technician(models.Model):
name = models.CharField(max_length=20)
def __str__(self):
return self.name
class Lead(models.Model):
name = models.CharField(max_length=20)
technician = models.ForeignKey(Technician, on_delete=models.SET_NULL, null=True)
products = models.ManyToManyField(Product)
Then we can simply define our form with:
class LeadForm(form.ModelForm):
class Meta:
model = Lead
fields = '__all__'
Note: usually classes are written in PamelCase and thus start with an Uppercase.
You can here use a class-based CreateView [Django-doc] for example:
from django.views.generic.edit import CreateView
from app.models import Lead
from app.forms import LeafForm
class LeadCreateView(CreateView):
model = Lead
form_class = LeadForm
template_name = 'create_lead.html'

How can make the admin for a ForeignKey('self') ban referring to itself?

I have a model with a forgein key to itself. For example:
class Folder(models.Model):
name = models.CharField()
parent_folder = models.ForeignKey('self', null=True, blank=True, default=None, on_delete=models.CASCADE)
For my purposes, I never want parent_folder to refer to itself, but the default admin interface for this model does allow the user to choose its own instance. How can I stop that from happening?
Edit: If you're trying to do a hierarchical tree layout, like I was, another thing you need to watch out for is circular parent relationships. (For example, A's parent is B, B's parent is C, and C's parent is A.) Avoiding that is not part of this question, but I thought I would mention it as a tip.
I would personally do it at the model level, so if you reuse the model in another form, you would get an error as well:
class Folder(models.Model):
name = models.CharField()
parent_folder = models.ForeignKey('self', null=True, blank=True, default=None, on_delete=models.CASCADE)
def clean(self):
if self.parent_folder == self:
raise ValidationError("A folder can't be its own parent")
If you use this model in a form, use a queryset so the model itself doesn't appear:
class FolderForm(forms.ModelForm):
class Meta:
model = Folder
fields = ('name','parent_folder')
def __init__(self, *args, **kwargs):
super(FolderForm, self).__init__(*args, **kwargs)
if hasattr(self, 'instance') and hasattr(self.instance, 'id'):
self.fields['parent_folder'].queryset = Folder.objects.exclude(id=self.instance.id)
To make sure the user does not select the same instance when filling in the foreign key field, implement a clean_FIELDNAME method in the admin form that rejects that bad value.
In this example, the model is Folder and the foreign key is parent_folder:
from django import forms
from django.contrib import admin
from .models import Folder
class FolderAdminForm(forms.ModelForm):
def clean_parent_folder(self):
if self.cleaned_data["parent_folder"] is None:
return None
if self.cleaned_data["parent_folder"].id == self.instance.id:
raise forms.ValidationError("Invalid parent folder, cannot be itself", code="invalid_parent_folder")
return self.cleaned_data["parent_folder"]
class FolderAdmin(admin.ModelAdmin):
form = FolderAdminForm
admin.site.register(Folder, FolderAdmin)
Edit: Combine my answer with raphv's answer for maximum effectiveness.

How to order the results of a ForeignKey relationship in a Django form?

I have this models in Django
class Country(models.Model):
name = models.CharField(max_length=80)
class Person(models.Model):
first_name = models.CharField(max_length=100, db_index=True)
last_name = models.CharField(max_length=100, db_index=True)
country = models.ForeignKey(Country)
and this ModelForm
class PersonForm(forms.ModelForm):
class Meta:
model = Person
when I use this form in a template, everything works fine, but the country list in the <select> appears disordered. How can I order it?
You can use the ordering property:
class Country(models.Model):
name = models.CharField(max_length=80)
class Meta:
ordering = ["name"]
If you set the ordering to the Country class, it shall display them as you want.
If you can't or don't want to use the ordering attribute in class Meta of model, you also can do this:
You need make a Form object, something like:
from django import forms
class PersonForm(forms.ModelForm):
country = forms.ModelChoiceField(queryset=Country.objects.all().order_by('name'))
class Meta:
model = Person
field types for formsmodels
There's 2 good answers here, but I wanted to retain help_text, blank, and other settings from the model without having to repeat them and also not change the default ordering on the model. Here's what I did:
class PersonForm(forms.ModelForm):
class Meta:
model = Person
def __init__(self, *args, **kwargs):
super(PersonForm, self).__init__(*args, **kwargs)
self.fields['country'].queryset = self.fields['country'].queryset.order_by('name')
Essentially I just updated the queryset on the automatically added field to order the way I wanted.
try adding this into class Meta, inside class Person:
ordering = ['country']
http://docs.djangoproject.com/en/dev/ref/models/options/#ordering
In view.py
First: you create the form
form = YourForm(request.POST)
Later your set the query:
form.fields['country '].queryset = YourDBTable.objects.all().order_by('Your_Attr')