Django admin inline conditioned on model field values - python

I'm trying to show one of three different child model inlines for a parent model, based on a selected field value within the parent model. I have tried every solution that pops up from various google searches but nothing seems to work.
First a little background, I'm new to python and django but so far I feel that I have been a quick study. I'm attempting to build a web application to house information linked to various spatial locations. The geometry type (geom_type) for each location may be different (i.e., points, linestring, and polygons are possible). To capture this information I plan to create a parent model (Location) to house the name and geom_type (and possibly other metadata). The spatial data related to each Location would then be housed in three separate child models; one for each geom_type. When entering data I would like to create a new location and select the geom_type, which would then pull up the correct inline.
Now for the details:
Models
from django.contrib.gis.db import models
class Geometry(models.Model):
TYPE = (
('Point', 'Point'),
('Linestring', 'Linestring'),
('Polygon', 'Polygon'),
)
geom_type = models.CharField('Geometry Type', choices = TYPE, max_length = 30)
class Meta:
verbose_name = 'Geometry'
verbose_name_plural = 'Geometries'
def __str__(self):
return self.geom_type
class Location(models.Model):
name = models.CharField('Location Name', max_length = 50)
geom_type = models.ForeignKey(Geometry, on_delete=models.CASCADE)
def __str__(self):
return self.name
class Point(models.Model):
name = models.OneToOneField(Location, on_delete=models.CASCADE)
geometry = models.PointField()
def __str__(self):
return self.name.name
class Linestring(models.Model):
name = models.OneToOneField(Location, on_delete=models.CASCADE)
geometry = models.LineStringField()
def __str__(self):
return self.name.name
class Polygon(models.Model):
name = models.OneToOneField(Location, on_delete=models.CASCADE)
geometry = models.PolygonField()
def __str__(self):
return self.name.name
Admin
from django.contrib.gis import admin
from leaflet.admin import LeafletGeoAdmin, LeafletGeoAdminMixin
from .models import Geometry, Location, Point, Linestring, Polygon
class GeometryAdmin(admin.ModelAdmin):
list_display = ('id', 'geom_type')
admin.site.register(Geometry, GeometryAdmin)
class PointInline(LeafletGeoAdminMixin, admin.StackedInline):
model = Point
class LinestringInline(LeafletGeoAdminMixin, admin.StackedInline):
model = Linestring
class PolygonInline(LeafletGeoAdminMixin, admin.StackedInline):
model = Polygon
class LocationAdmin(admin.ModelAdmin):
model = Location
list_display = ('id', 'name', 'geom_type')
inlines = [
PointInline,
LinestringInline,
PolygonInline
]
admin.site.register(Location, LocationAdmin)
All three inlines show up correctly with the code above as expected. However, when I try to incorporate the conditional logic with different variations of get_inlines or get_inline_instances it always just ends up displaying the inline associated with the final "else" statement.
My failed attempt
def get_inlines(self, request, obj: Location):
if obj.geom_type == 'Point':
return [PointInline]
elif obj.geom_type == 'Location':
return [LinestringInline]
elif obj.geom_type == 'Polygon':
return [PolygonInline]
else:
return []
I believe the problem occurs because conditional statements are not referencing the model field correctly. But I can't seem to stumble upon the correct way to achieve my expected outcome.

Use related_name in model like below:
next_question = models.ForeignKey(Question, on_delete=models.CASCADE, null = True, blank = True, related_name='next_question', limit_choices_to={'is_active': True})
and then fk_name Like the example below: Then try. Hope you can find a solution by yourself.
class Labels(admin.TabularInline):
model = Label
extra = 0
fk_name = "next_question"
Use admin.StackedInline for OneToOne and admin.TabularInline for ForeignKey.
class ProfileInline(admin.StackedInline):
model = Profile
can_delete = False
Create separate admin for 'Geometry' and 'Location' if you stacked.

Related

Saving centroid of a (multi)polygon as point geometry in a model

I have two tables, one with multipolygon geometries, the other with a column for point geometries. I want the centroid of the selected polygon to be saved as the point geometry for the other table.
class matview_all_administrative_units(models.Model):
lau_id = models.IntegerField(primary_key=True)
ortsgemeinde = models.CharField(max_length=150)
verwaltungsgemeinde = models.CharField(max_length=150)
landkreis_bezirk = models.CharField(max_length=150)
bundesland_kanton = models.CharField(max_length=150)
staat = models.CharField(max_length=150)
geom = models.MultiPolygonField(srid=4326)
class Meta:
managed = False
db_table = 'administrative_hierarchy_full_geom'
class site(models.Model):
sid = models.AutoField(primary_key=True)
site_name = models.CharField(max_length=250)
site_notes = models.CharField(max_length=2500, blank=True, null=True)
municipality = models.ForeignKey('local_administrative_unit', on_delete=models.PROTECT)
geom = models.PointField(srid=4326)
def __str__(self):
return '{}, {} ({})'.format(self.sid, self.site_name, self.municipality)
To add a new site, an existing administrative unit must be associated with it and the center of it's polygon should be used as the location/geometry of the site. For now I made this:
class NewSiteView(CreateView):
model = models.site
form_class = forms.NewSiteForm
template_name = 'datamanager/newsite.html'
success_url = '/sites/'
calling this form:
from django.forms import ModelForm, HiddenInput
from django.contrib.gis.db.models.functions import Centroid
from . import models
class NewSiteForm(ModelForm):
class Meta:
model = models.site
fields = ['site_name', 'site_notes', 'municipality','geom']
widgets = {
'geom': HiddenInput(),
}
def clean(self):
super().clean()
self.cleaned_data['geom'] = Centroid(models.matview_all_administrative_units.objects.values('geom').filter(lau_id=self.cleaned_data['municipality'].lau_id))
however, this leads to this error:
So I am basically not calculating a point but a 'centroid object' - so far so good, the django documentation tells us that. Now I am stuck trying to get something out of this centroid thingy that I can shove into that geometry column. As far as I understand I take the right data and handle it over to the right function (otherwise the error should appear earlier in this code i think?), but the result is not useful for inserting it into a geometry column. So how do i get my point? (lol)
finally I found a solution. Added this to the CreateView:
def form_valid(self, form):
pol = models.local_administrative_unit.objects.values('geom').filter(lau_id=form.cleaned_data['municipality'].lau_id)[0]['geom']
cent_point = pol.centroid
form.instance.geom = cent_point.wkt
form.save()
return super().form_valid(form)
I takes the geometry from the polygon, calculates the centroid of it and inserts it's geometry as well-known text into the form, then saves the form.

Loading extra fields for update in Django dependent dropdown list

I implemented a dependent dropdown on my Django webapp. I used this tutorial Implementing dependent drop down. However, challenge comes when I want to update the form. To put this in perspective, let me recreate the code here.
Model.py
class VehicleMake(models.Model):
make = models.CharField(max_length=20)
manufacturer = models.CharField(max_length=20)
def __str__(self):
return self.make
class VehicleModel(models.Model):
make = models.ForeignKey(VehicleMake, on_delete=models.CASCADE)
model_name = models.CharField(max_length=20)
def __str__(self):
return self.model_name
class Vehicle(models.Model):
model = models.ForeignKey(VehicleModel, on_delete=models.CASCADE)
description = models.TextField()
Notice that unlike in the provided tutorial, I don't have both of the dependent fields on the vehicle model. That is to avoid repetition since if you know the vehicle model, you will definitely know the make from the VehicleModel table.
Here is the form:
forms.py
class VehicleDetails(forms.ModelForm):
make = forms.ModelChoiceField(queryset=VehicleMake.objects.all(),
empty_label="Select Vehicle Make")
class Meta:
model = Vehicle
fields = ['make', 'model', 'description'
]
def __init__(self, *args, **kwargs):
super(VehicleDetails, self).__init__(*args, **kwargs)
self.fields['model'].queryset = VehicleModel.objects.none()
if 'make' in self.data:
try:
make = int(self.data.get('make'))
self.fields['model'].queryset = VehicleModel.objects.filter(make=make).order_by('model_name')
except (ValueError, TypeError):
pass # invalid input from the client; ignore and fallback to empty VehicleModel queryset
elif self.instance.pk:
vehicle_model = VehicleModel.objects.get(self.instance.model)
self.fields['make'] = vehicle_model.make
self.fields['model'].queryset = self.instance.model_set.filter(make=vehicle_model.make).order_by('model_name')
So, my challenge is, when I want to update the form, I get an error from the last section of the form under the elif code. I want to get the value of make using the store value of model then use that to render the form of course with the select of options of model being those from the selected make, unless the user now makes changes to the make field.
This is what I have tried so far (especially under the elif section on the forms.py) but I keep getting the error: TypeError: 'VehicleModel' object is not iterable. What am I doing wrong?
I was able to solve this by changing this in the elif block:
vehicle_model = VehicleModel.objects.get(pk=self.instance.model.id)
self.fields['make'].queryset = self.instance.model.make.__class__.objects.all()
self.fields['model'].queryset = self.instance.model.__class__.objects.filter(make=vehicle_model.make).order_by('model_name')
Then all I had to do at the views.py to ensure that current value is loaded was to add a initial value while loading the form, i.e.
vehicle_form = VehicleDetails(instance=listing.vehicle, initial = {'make': listing.vehicle.model.make })
I hope it helps anyone in the same problem.

Django model reference and manipulation

I have the following models in Django that have a structure as follows:
class Office_Accounts(models.Model):
accountid = models.EmailField(max_length=200, unique=True)
validtill = models.DateField(default=datetime.now)
limit = models.CharField(max_length=2)
class Device(models.Model):
device_type = models.ForeignKey(DeviceType,to_field='device_type')
serial_number = models.CharField(max_length=200,unique=True)
in_use_by = models.ForeignKey(User,to_field='username')
brand = models.CharField(max_length=200,default="-", null=False)
model = models.CharField(max_length=200,default="-", null=False)
type_number = models.CharField(max_length=200,blank=True,null=True, default = None)
mac_address = models.CharField(max_length=200,blank=True,null=True, default = None)
invoice = models.FileField(upload_to='Device_Invoice', null=True, blank = True)
msofficeaccount = models.ForeignKey(Office_Accounts, to_field="accountid")
class Meta:
verbose_name_plural = "Devices"
def full_name(self):
return self.device_type + self.serial_number + self.brand
I will display both of the models in admin.py.
Now, I want to display the count of each accountid present in the field "msofficeaccount" (present in Device Models) in my admin page of Office_Accounts model. For an example if xyz#abc.com appears in 10 rows of msofficeaccount field then, the count should be displayed as 10 in Office_Accounts admin page. Can anyone please guide me how should I approach this problem to solve it?
You could add a method to your admin class that returns the count of related devices for each office_account, but that would be very inefficient. Instead you can override get_queryset to annotate the count from a database aggregation function:
from django.db.models import Count
class Office_AccountsAdmin(admin.ModelAdmin):
list_display = (..., 'device_count')
...
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.annotate(device_count=Count('device'))
(On a minor note, Python style is always to use CamelCase for class names, and Django style is to use singular model names, so your model should really be called OfficeAccount.)

Django Access to Foreign Key data to set a field default value

I have two models with their respective forms. One has a Foreign Key link to the other and from, here I would like to set some fields default data.
class Lexicon(models.Model):
[...]
case_sensitive = models.BooleanField(default=True)
invariant = models.NullBooleanField(default=False)
diacritics = models.BooleanField(default=True)
[...]
class Meta:
verbose_name = "lexicon"
ordering = ["filename"]
def __str__(self):
return self.filename
class Lexeme(models.Model):
lexicon = models.ForeignKey(Lexicon, on_delete=models.CASCADE)
case_sensitive = models.BooleanField(default=True)
diacritics = models.BooleanField(default=True)
[...]
class Meta:
verbose_name = "lexeme"
I would like the Lexeme model fields "case_sensitive" and "diacritics" to default from Lexicon. I suppose the forms may be a better place to do this.
Any idea ?
As I understand, you only need to populate data from Lexicon to Lexeme model fields. You can override get_form_kwargs in your FormView as follows
def get_form_kwargs(self):
lex_obj = Lexeme.objects.get(pk=self.kwargs['pk'])
kwargs = super().get_form_kwargs()
kwargs['initial']['case_sensitive'] = lex_obj.lexicon.case_sensitive
kwargs['initial']['diacritics'] = lex_obj.lexicon.diacritics
return kwargs
Is that what you want? I have not tested but, I have used similar thing on my project. Let me know if works or not.
I finally found the way to go. It was just basic initial setting of field, no need to touch to forms.py, models.py nor the html template.
I passed data to my form like this:
lexeme_form = LexemeForm(initial={'case_sensitive': lexicon.case_sensitive, 'diacritics': lexicon.diacritics})
use Ajax at template to change the initial value of "case_sensitive" and "diacritics" when Lexicon changed, and abstract model can be used to reduce repeat lines :
class BaseLex(models.Model):
case_sensitive = models.BooleanField(default=True)
diacritics = models.BooleanField(default=True)
class Meta:
abstract = True
class Lexicon(BaseLex):
# without `case_sensitive` and `diacritics' fields
...
class Lexeme(BaseLex):
# without `case_sensitive` and `diacritics' fields
lexicon = models.ForeignKey(Lexicon, on_delete=models.CASCADE)
...

Populate Django ManyToManyField Options Based on Other Field in Admin

I am having problems filtering options for a ManyToManyField on the Django Admin Add screen based on input to another field on the same form. I am new to Django and have been unable to use any of the generic fixes described elsewhere because they are all slightly different than my situation. Here is my situation:
I have three models in my project: Class, Student, and AttendanceRecord. In the Django Admin, when adding an attendance record, I would like to change the options for the field Absent_Students based on the selection made for the field Associated_Class. So, for example, if Associated_Class "CS 450" is selected, the options for Absent_Students should change to only students whose class_list includes CS 450.
Here are my models:
from __future__ import unicode_literals
from django.db import models
from django.contrib.auth.models import User
from django.utils.encoding import python_2_unicode_compatible
import random, string
# Create your models here.
#This is the model for a student
#python_2_unicode_compatible
class Student(models.Model):
pass
Student_First_Name = models.CharField(max_length=200)
Student_Last_Name = models.CharField(max_length=200)
Student_ID_Number = models.CharField(max_length=200)
Student_Class = models.ForeignKey('Class', null=True)
def __str__(self):
return self.Student_Last_Name + ',' + self.Student_First_Name
# This is the model for a class
#python_2_unicode_compatible
class Class(models.Model):
class Meta:
verbose_name_plural = "Classes"
Class_Name = models.CharField(max_length=200)
Student_List = models.ManyToManyField('Student', related_name='class_list')
Professor = models.ForeignKey(User,null=True)
AddCode = models.IntegerField
pass
def __str__(self):
return self.Class_Name
def getName(self):
return self.Class_Name
def getProfessor(self):
return self.Professor.id
def getProf(self):
return self.Professor
def getStudents(self):
return self.Student_List
#This is the model for attendance records
class AttendanceRecord(models.Model):
class Meta:
verbose_name = "Attendance Record"
Associated_Class = models.ForeignKey(Class, on_delete=models.CASCADE, related_name='Attendance_Records')
Date = models.DateField()
Absent_Students = models.ManyToManyField('Student', blank=True)
Present_Students = models.ManyToManyField('Student', related_name='a')
def get_associated_class_id(self):
return self.Associated_Class
def __str__(self):
return self.Associated_Class.__str__() + ' on date ' + self.Date.__str__(self)
I have tried doing this by editing the AttendanceRecordAdminForm class and AttendanceRecordAdmin class. My problem is that when setting the self.fields['Absent_Students].queryset I do not know how to access the currently selected Associated_Class on the form. I keep getting an error that "AttendanceRecord has no Associated_Class". Here are those classes just discussed in their entirety:
class AttendanceRecordAdminForm(forms.ModelForm):
class Meta:
model = AttendanceRecord
fields = '__all__'
def __init__(self, *args, **kwargs):
super(AttendanceRecordAdminForm, self).__init__(*args, **kwargs)
instance = kwargs.get('instance', None)
self.fields['Absent_Students'].queryset = Student.objects.filter(class_list__id=self.instance.get_associated_class_id())
self.fields['Present_Students'].queryset = Student.objects.filter(class_list__id=1)
class AttendanceRecordAdmin(admin.ModelAdmin):
form = AttendanceRecordAdminForm
filter_horizontal = ('Absent_Students', 'Present_Students',)
Basically, I am looking for a way to access the currently entered Associated_Class on the admin form so I can properly filter the queryset.
After hours more of online searching I finally found what I needed. A chained ManyToMany from the smart_select app makes this very easy. This link: How to use django-smart-select describes the install process and also links to the documentation for using it once it is installed. Hopefully this helps some others as well.

Categories

Resources