I have a MySQL database with four related tables: project, unit, unit_equipment, and equipment. A project can have many units; a unit can have many related equipment entries. A single unit can only belong to one project, but there is a many-to-many between equipment and unit (hence the unit_equipment bridge table in the DB). I'm using Django and trying to create a view (or a list?) that shows all 3 models on the same page, together. So it would list all projects, all units, and all equipment. Ideally, the display would be like this:
Project --------- Unit ------------- Equipment
Project 1 first_unit some_equipment1, some_equipment2
Project 1 second_unit more_equipment1, more_equipment2
Project 2 another_unit some_equipment1, more_equipment1
Project 2 and_another_unit some_equipment2, more_equipment2
but at this point I'd also be happy with just having a separate line for each piece of equipment, if comma-separating them is a pain.
Although it seems straightforward to create a form where I can add a new project and add related unit and equipment data (using the TabularInline class), I cannot for the life of me figure out how to bring this data together and just display it. I just want a "master list" of everything in the database, basically.
Here's the code I have so far:
models.py
class Project(models.Model):
name = models.CharField(max_length=255, blank=True, null=True)
class Meta:
managed = False
db_table = 'project'
def __str__(self):
return self.name
class Unit(models.Model):
project = models.ForeignKey(Project, models.DO_NOTHING, blank=True, null=True)
name = models.CharField(max_length=255, blank=True, null=True)
class Meta:
managed = False
db_table = 'unit'
def __str__(self):
return self.name
class UnitEquipment(models.Model):
unit = models.ForeignKey(Unit, models.DO_NOTHING, blank=True, null=True)
equipment = models.ForeignKey(Equipment, models.DO_NOTHING, blank=True, null=True)
class Meta:
managed = False
db_table = 'unit_equipment'
class Equipment(models.Model):
name = models.CharField(max_length=100, blank=True, null=True)
description = models.CharField(max_length=255, blank=True, null=True)
class Meta:
managed = False
db_table = 'equipment'
def __str__(self):
return self.name
views.py
def project_detail_view(request):
obj = Project.objects.all()
context = {'object': obj}
return render(request, "project/project_detail.html", context)
urls.py
urlpatterns = [
path('project/', project_detail_view),
path('', admin.site.urls),
]
admin.py
class UnitTabularInLine(admin.TabularInline):
model = Unit
extra = 0
class ProjectAdmin(admin.ModelAdmin):
inlines = [UnitTabularInLine]
class Meta:
model = Project
# a list of displayed columns name.
list_display = ['name']
# define search columns list, then a search box will be added at the top of list page.
search_fields = ['name']
# define filter columns list, then a filter widget will be shown at right side of list page.
list_filter = ['name']
# define model data list ordering.
ordering = ('name')
I think I need to somehow add more entries to the list_display in the admin file, but every time I try to add unit or equipment it throws an error. I've also tried adding more attributes to Project, but I can't seem to get the syntax right, and I'm never sure which model class I'm supposed to make it.
I've also looked at FormSets, but I cannot get my head around how to alter my current code to get it to work.
How do I get these models together into a unified view?
You don't need to edit the admin view to add your own view: which you may find you are able to do in this case to get your data displayed exactly as you want.
If you do want to show the related object values in the admin list, then you can use lookups and custom columns: however in this case your list would be based upon the Unit.
# You don't need an explicit UnitEquipment model here: you can
# use a simple ManyToManyField
class Unit(models.Model):
project = ...
name = ...
equipment = models.ManyToManyField(Equipment, related_name='units')
def equipment_list(admin, instance):
return ', '.join([x.name for x in instance.equimpent.all()])
class UnitAdmin(admin.ModelAdmin):
class Meta:
model = Unit
list_display = ['project__name', 'name', equipment_list]
def get_queryset(self, request):
return super().get_queryset(request)\
.select_related('project')\
.prefetch_related('equipment')
Note that you need to have the queryset override, otherwise there will be a bunch of extra queries as each unit also requires fetching the project and list of equipment for that unit.
There's also a further improvement you can make to your queries: you could aggregate the related equipment names using a Subquery annotation, and prevent the second query (that fetches all related equipment items for the units in the queryset). This would replace the prefetch_related()
Thanks to #Matthew Schinckel, I was able to find my way to the answer. Here's what my files look like now (only edited the Unit class in models.py):
models.py
class Unit(models.Model):
project = models.ForeignKey(Project, models.DO_NOTHING, blank=True, null=True)
name = models.CharField(max_length=255, blank=True, null=True)
equipment = models.ManyToManyField(Equipment, related_name='units')
class Meta:
managed = False
db_table = 'unit'
def __str__(self):
return self.name
def equipment_list(self):
return ', '.join([x.name for x in self.equipment.all()])
admin.py
class UnitAdmin(admin.ModelAdmin):
class Meta:
model = Unit
# a list of displayed columns name.
list_display = ('project', 'name', 'equipment_list')
# define search columns list, then a search box will be added at the top of list page.
search_fields = ['project']
# define filter columns list, then a filter widget will be shown at right side of list page.
list_filter = ['project', 'name']
# define model data list ordering.
ordering = ('project', 'name')
def get_queryset(self, request):
return super().get_queryset(request)\
.select_related('project')\
.prefetch_related('equipment')
So the changes I made were:
1. Make list_display a tuple instead of a list.
2. Throw def equipment_list(self) into the Unit class (so it's callable as an attribute of Unit) and pass (self) instead of (admin, instance) (I kept getting an error that was looking for the instance argument).
Related
I have the following models:
# Get or create a 'Not selected' category
def get_placeholder_categoy():
category, _ = ListingCategories.objects.get_or_create(category='Not selected')
return category
# Get default's category ID
def get_placeholder_category_id():
return get_placeholder_categoy().id
class ListingCategories(models.Model):
category = models.CharField(max_length=128, unique=True)
def __str__(self):
return f'{self.category}'
class Listing(models.Model):
title = models.CharField(max_length=256)
seller = models.ForeignKey(User, on_delete=models.CASCADE, related_name='listings')
description = models.TextField(max_length=5120, blank=True)
img_url = models.URLField(default='https://media.istockphoto.com/vectors/no-image-available-picture-coming-soon-missing-photo-image-vector-id1379257950?b=1&k=20&m=1379257950&s=170667a&w=0&h=RyBlzT5Jt2U87CNkopCku3Use3c_3bsKS3yj6InGx1I=')
category = models.ForeignKey(ListingCategories, on_delete=models.CASCADE, default=get_placeholder_category_id, related_name='listings')
creation_date = models.DateTimeField()
base_price = models.DecimalField(max_digits=10, decimal_places=2, validators=[
MinValueValidator(0.01),
MaxValueValidator(99999999.99)
])
With these, I have the following form:
class ListingForm(ModelForm):
class Meta:
model = Listing
exclude = ['seller', 'creation_date']
widgets = {
'title': TextInput(attrs=base_html_classes),
'description': Textarea(attrs=base_html_classes),
'img_url': URLInput(attrs=base_html_classes),
'category': Select(attrs=base_html_classes),
'base_price': NumberInput(attrs=base_html_classes)
}
One of the available categories I have is "Not selected", since I want to allow that if at some point a category were to be removed, items can be reassigned to that one, however, when rendering the form, I will do some validation on the view function to prevent it from being submitted if the "not selected" category is sent with the form.
Because of this, I want the HTML form on the template to assign the 'disabled' attribute to the option corresponding to that category, however, I have been searching for a couple of days now without finding anything that I was able to understand to the point where I could try it.
Ideally, another thing I'd like to achieve is to be able to modify the order of the rendered options on the form so that I can move to the top 'not selected' regardless of its primary key within the model.
I am aware I can just create a form instead of a model form, or just modify the template so I manually specify how to render the form itself, but I do feel like there is a simple fix to this either on the model or on the model form that I am just not finding yet.
Thanks in advance!
I would suggest you use (in model definition)
class Listing(models.Model):
..
category = model.ForeignKey(ListingCategories, on_delete=models.SET_NULL, null=True, related_name='listings')
..
and optionally in form definition
class ListingForm(ModelForm):
category = forms.ModelChoiceField(ListingCategories, empty_label='Not Selected')
..
While rendering model form, a required attribute will be automatically added, and in form validating, it is also required. It is only in database validation that the field can be left NULL
I'm making a classic single view application, mapping multiple
datasources.
Django-admin is paginated so there's no impact when I view my list, the problem is when I want to change/add it is .
Using the debug-toolbar my queries look fine, I don't think they take a long time.
I tried to use a suggestion here Django admin change form load quite slow and created a form, but this had no impact.
When is use exclude = ['e_vehicle','e_product'] it's no surprise that add/change screens load instantly.
Any thoughts please
model.py
class Product_Mapping(Trackable):
product_mapping_id = models.AutoField(primary_key=True)
s_product = models.OneToOneField(sProduct, on_delete=models.CASCADE)
e_fund_manager = models.ForeignKey(eManager, models.DO_NOTHING, blank=True, null=True)
e_product = models.ForeignKey(eProduct, models.DO_NOTHING, blank=True, null=True)
e_vehicle = models.ForeignKey(eVehicle, models.DO_NOTHING, blank=True, null=True)
eManager has around 3K
eProduct has around 17K (has fkey to eManager)
eVehicle has around 25K (has fkey to eProduct)
form.py
class MappingProductForm(forms.ModelForm):
s_product = forms.ChoiceField(required=False,
choices=sProduct.objects.values_list('final_publications_product_id', 'product_name'))
e_fund_manager = forms.ChoiceField(required=False,
choices=eManager.objects.values_list('e_fund_manager_id', 'manager_name'))
e_product = forms.ChoiceField(required=False,
choices=eProduct.objects.values_list('e_product_id', 'product_name'))
e_vehicle = forms.ChoiceField(required=False,
choices=eVehicle.objects.values_list('e_vehicle_id', 'formal_vehicle_name'))
class Meta:
model = Product_Mapping
fields = '__all__'
admin.py
#admin.register(Product_Mapping)
class ChampProductMappingAdmin(admin.ModelAdmin):
form = MappingProductForm
It can be seen that there are too many values in e_product and e_vehicle tables. You are using ChoiceField it means inside HTML dropdown there would be ~17K-25K options this would slow down rendering and sometimes hang on client side.
Solution
edit your forms.py
from django.contrib.admin.widgets import ForeignKeyRawIdWidget
class MappingProductForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(MappingProductForm, self).__init__(*args, **kwargs)
rel_model = self.Meta.model
rel_eprod = rel_model._meta.get_field('e_product').rel
rel_eprod = rel_model._meta.get_field('e_vehicle').rel
self.fields['e_product'].widget = ForeignKeyRawIdWidget(rel_eprod, admin_site=admin.site)
self.fields['e_vehicle'].widget = ForeignKeyRawIdWidget(rel_eprod, admin_site=admin.site)
s_product = forms.ChoiceField(required=False,
choices=sProduct.objects.values_list('final_publications_product_id', 'product_name'))
e_fund_manager = forms.ChoiceField(required=False,
choices=eManager.objects.values_list('e_fund_manager_id', 'manager_name'))
e_product = forms.ModelChoiceField(required=False,
choices=eProduct.objects.all())
e_vehicle = forms.ModelChoiceField(required=False,
choices=eVehicle.objects.all()
)
class Meta:
model = Product_Mapping
fields = '__all__'
This would keep raw id as widget. You can get something like below.
You can add other entries by hitting search button besides input.
This is better option when you have lots of choices in ModelChoiceField
I am currently trying to create a dynamic product model that will allow admins to create add their own "option sets" to products.
For example, Product A has flap valve with 400mm, 500mm and 600mm widths available.
To facilitate this I have created 3 models.
models.py
# A container that can hold multiple ProductOptions
class ProductOptionSet(models.Model):
title = models.CharField(max_length=20)
# A string containing the for the various options available.
class ProductOption(models.Model):
value = models.CharField(max_length=255)
option_set = models.ForeignKey(ProductOptionSet)
# The actual product type
class HeadwallProduct(Product):
dimension_a = models.IntegerField(null=True, blank=True)
dimension_b = models.IntegerField(null=True, blank=True)
# (...more variables...)
flap_valve = models.CharField(blank=True, max_length=255, null=True)
...and a form...
forms.py
class HeadwallVariationForm(forms.ModelForm):
flap_valve = forms.MultipleChoiceField(required=False, widget=forms.SelectMultiple)
def __init__(self, *args, **kwargs):
super(HeadwallVariationForm, self).__init__(*args, **kwargs)
self.fields['flap_valve'].choices = [(t.id, t.value) for t in ProductOption.objects.filter(option_set=1)]
def save(self, commit=True):
instance = super(HeadwallVariationForm, self).save(commit=commit)
return instance
class Meta:
fields = '__all__'
model = HeadwallProduct
This works fine for during the initial creation of a product. The list from the MultipleChoiceForm is populated with entries from the ProductOptionSet and the form can be saved.
However, when the admin adds a 700mm flap valve as an option to the ProductOptionSet of Product A things fall apart. Any new options will show up in the admin area of the existing product - and will even be persisted to the database when the product is saved - but they will not be shown as selected in the admin area.
If a Product B is created the new options work as intended, but you cannot add new options to an existing product.
Why does this happen and what can I do to fix it? Thanks.
Urgh... after about 4 hours I figured it out...
Changing:
class ProductOption(models.Model):
value = models.CharField(max_length=20)
option_set = models.ForeignKey(ProductOptionSet)
to
class ProductOption(models.Model):
option_value = models.CharField(max_length=20)
option_set = models.ForeignKey(ProductOptionSet)
Fixed my issue.
Hi there pretty new to django but considering the below models, with their relationships, how do I create a read only field for the object that is a reference to a field in another class? I've looked for a while on stackoverflow, but not sure what kind of model reference that would be.
The basic logic for this being:
I have this server rack that sites on a floor in a server room, and I'm associating it to a rack position, and row to manage power consumption and other goodies. Just for my end-user's reference I want a read only field to show them what row this rack lives in, and its derived from the rack position. I'd been fiddling around with creating a method to look it up, but can't seem to figure out the syntax or find something related on the django admin pages.
Any ideas would be super appreciated, I really could use the help as I've been staring through docs forever, and can't seem to find a relevant model reference for this.
class rack(models.Model):
class Meta:
verbose_name = "Rack"
verbose_name_plural = "Racks"
def __unicode__(self):
return str(self.position)
def row(self, obj):
return self.position.row
position = models.OneToOneField("rackposition")
row = row(position.row.row)
asstag = models.CharField("Asset Tag", max_length=200, unique=True)
rackunits = models.IntegerField("Rack Units")
class rackposition(models.Model):
class Meta:
verbose_name = "Rack Position"
verbose_name_plural = "Rack Positions"
def __unicode__(self):
return str(self.position)
position = models.CharField("Position", max_length=35, primary_key=True)
row = models.ForeignKey("row")
class row(models.Model):
class Meta:
verbose_name = "Row"
verbose_name_plural = "Rows"
def __unicode__(self):
return str(self.row) + "." + str(self.suite)
row = models.CharField("Row ID", max_length=200, unique=True)
suite = models.ForeignKey(suite, blank=False)
power_budget = models.IntegerField("Power Budget")
power_volt = models.IntegerField("Power Voltage")
dual_bus = models.BooleanField("Dual Bus", default=False)
You don't need a method. Assuming you have a rack instance called my_rack, you can get its row with my_rack.position.row.
Note, you should really follow PEP8 and use CamelCase for your class names.
If you want to see it as a readonly field in the admin, you will need to define a method either on the model or on the ModelAdmin class. For example:
class RackAdmin(admin.ModelAdmin):
model = Rack
readonly_fields = ('row',)
def row(self, obj):
return obj.position.row
I've been looking at the docs for search_fields in django admin in the attempt to allow searching of related fields.
So, here are some of my models.
# models.py
class Team(models.Model):
name = models.CharField(max_length=255)
class AgeGroup(models.Model):
group = models.CharField(max_length=255)
class Runner(models.Model):
"""
Model for the runner holding a course record.
"""
name = models.CharField(max_length=100)
agegroup = models.ForeignKey(AgeGroup)
team = models.ForeignKey(Team, blank=True, null=True)
class Result(models.Model):
"""
Model for the results of records.
"""
runner = models.ForeignKey(Runner)
year = models.IntegerField(_("Year"))
time = models.CharField(_("Time"), max_length=8)
class YearRecord(models.Model):
"""
Model for storing the course records of a year.
"""
result = models.ForeignKey(Result)
year = models.IntegerField()
What I'd like is for the YearRecord admin to be able to search for the team which a runner belongs to. However as soon as I attempt to add the Runner FK relationship to the search fields I get an error on searches; TypeError: Related Field got invalid lookup: icontains
So, here is the admin setup where I'd like to be able to search through the relationships. I'm sure this matches the docs, but am I misunderstanding something here? Can this be resolved & the result__runner be extended to the team field of the Runner model?
# admin.py
class YearRecordAdmin(admin.ModelAdmin):
model = YearRecord
list_display = ('result', 'get_agegroup', 'get_team', 'year')
search_fields = ['result__runner', 'year']
def get_team(self, obj):
return obj.result.runner.team
get_team.short_description = _("Team")
def get_agegroup(self, obj):
return obj.result.runner.agegroup
get_agegroup.short_description = _("Age group")
The documentation reads:
These fields should be some kind of text field, such as CharField or TextField.
so you should use 'result__runner__team__name'.