I am working on an application that manage the locations for a web site. This application (locations) contains the model(simplified):
class Location(models.Model):
""" Model for storing locations, places and addresses """
address = models.CharField(_('address'), max_length=100, blank=True)
""" Location address """
latitude = models.FloatField(_('latitude'), blank = True)
""" Latitude info point """
longitude = models.FloatField(_('longitude'), blank = True)
""" Longitude info point """
So that other applications and models who want to store a location, actually contain a ForeignKey to this model.
In addition, the locations application, defines a widget that instead of displaying the typical select associated with ForeignKey fields, renders a google map supported by a javascript that saves the latitude, longitude, and address in a HiddenInput field(also simplified):
class LocationEditWidget(HiddenInput):
def __init__(self, attrs=None):
default_attrs = {}
if attrs:
default_attrs.update(attrs)
super(LocationEditWidget, self).__init__(default_attrs)
class Media:
js = (
'http://maps.google.com/maps/api/js?sensor=true',
'js/gmaps.js',
'js/location-select.js',
)
def render(self, name, value, attrs=None):
self.attrs['id'] = self.attrs.get('id', 'unique')
self.attrs['latitude'] = self.attrs.get('latitude', 'auto')
self.attrs['longitude'] = self.attrs.get('longitude', 'auto')
self.attrs['geocoding'] = self.attrs.get('geocoding', 'True')
self.attrs['geolocation'] = self.attrs.get('geolocation', 'True')
self.attrs['width'] = self.attrs.get('width', '400')
self.attrs['height'] = self.attrs.get('height', '400')
output = super(LocationEditWidget, self).render(name, '%s,%s,%s' % (value.latitude, value.longitude, value.address), attrs)
map_html = render_to_string('./select_location.html',
{
"id" : self.attrs['id'],
"geocoding_button" : self.attrs['geocoding'],
"geolocation_button": self.attrs['geolocation'],
"latitude" : self.attrs['latitude'],
"longitude" : self.attrs['longitude'],
"width" : self.attrs['width'],
"height" : self.attrs['height']
})
return mark_safe(output+map_html)
The idea is that any other model that need to work with locations, has a ForeignKey field associated with the Location model, for example:
class Event(models.Model):
"""
`Events` main model.
"""
title = models.CharField(_('title'), max_length=100, blank=True)
""" Title. """
place = models.ForeignKey(Location, verbose_name=_('place'), related_name="place", blank=True)
""" Location. """
meeting_point = models.ForeignKey(Location, verbose_name=_('place'), related_name="meeting_point", blank=True)
""" Meeting Point """
duration = models.PositiveIntegerField(_('duration days'), blank=True)
""" Duration, specified in days. """
price = models.DecimalField(_('price'), max_digits=7, decimal_places=2, blank=True)
""" Price. """
So when you send the form (apart from save conventionally all the other model fields), a new Location object will be created with the provided location information (latitude, longitude and address) and it will be stored in the database. Finally establishing the relationship with the ForeignKey field ("place" in the previous model) using this new Location object.
For now, the widget works correctly and renders the map instead of the typical select widget, besides filling the HiddenInput with latitude, longitude and direction.
In my events/forms.py:
class NewEventForm(forms.ModelForm):
def save(self):
value = self.data['place'].split(',');
location = Location(latitude=value[0], longitude=value[1], address=value[2:])
self.instance.place = location
new_event = super(NewEventForm, self).save(commit=False)
new_event.save()
return new_offer
class Meta:
model = Event
fields = ('title', 'duration', 'price', 'place')
widgets = {
'place': LocationEditWidget(),
}
and in my events/views.py:
#login_required
def event_new(request):
new_event = Event(price=0, owner=request.user)
if request.method == 'POST':
# Form submited
Event_form = NewEventForm(request.POST, instance=new_event)
if event_form.is_valid():
# Form succesfully validated
event_form.save()
return HttpResponseRedirect('/Event/%s/edit' % new_event.id)
else:
# Initial form
event_form = NewEventForm(instance=new_event)
return render_to_response( 'events/event_new.html',
{'event_form':event_form},
context_instance=RequestContext(request))
But I am new to Django and I have some problems to understand the relationships between widgets, forms, fields and form fields. The problem is that the form does not validate correctly and if I force validation (bypasing 'event_form.is_valid ()') I get an error when saving to the database. On the other hand does not seem quite right (Because of DRY) having to subclassing a ModelForm for each application/model that uses locations.
Do I need to create custom fields and custom form fields?
Any ideas as how to orient it?
Can I use a widget to a ForeingKey?
Thanks in advance and please forgive me for my poor English.
Related
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.
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.
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.
I built a django web app to manage medical datas.
I have many specialities using the same project. I'd like to display specific interfaces/forms/templates...depending of the user speciality.
I'm on django 1.11 python 3.6.
The app is running well. I have patients, and users.
Each user have only one speciality (cardiologist, surgeon...), defined by a specific class and linked to the user by a ForeignKey.
models.py
class Specialite(Auditable): #list of specialites, and link it to user !
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
spe = models.CharField( max_length = 200, null = False,)
#define which app the specialite is linked to...
data_app = models.CharField(max_length = 200, null = False, blank = False)
def __str__(self):
return self.spe
class Patient(Auditable):
# https://stackoverflow.com/questions/3052975/django-models-avoid-duplicates
class Meta:
unique_together = ["nom", "prenom", "dob"]
MALE = "MALE"
FEMALE = "FEMALE"
SEXE = (
(MALE, 'Homme'),
(FEMALE, 'Femme'),
)
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
nom = models.CharField( max_length=50,)
nom_naissance = models.CharField( max_length=50,)
prenom = models.CharField(max_length=150,)
sexe = models.CharField(max_length = 100,
choices = SEXE,
)
For now, i have one table by patient: first name, last name, dob, weight, height...Only general informations shared by all specialities.
I'd like to create specific onetoone tables depending of the speciality to display relevant informations for each speciality/practicioner.
Each field is based on a SNOMED CT classification for more consistency and data connection.
I though about many ways:
A huge model on top with a lot of fields, with abstract = true, and sub models using this model
A huge model with a lot of fields, and for each speciality specific template, form to update datas, but data might change as long as doctors might have a different analysis of the data...
Eventually, the one i think would be the more appropriate: an app for each speciality with all the logic of the fields, forms etc... inside the app.
So my choice is more into creating an app for each speciality, linked to the "speciality" model by a foreign key or text (like the name of the app:
data_app = models.CharField(max_length = 200, null = False, blank = False)
).
In each app, i have a model with just a class linked to a patient by a OneToOne relationship.
class Cardiology (Auditable):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
zkf_patient = models.OneToOneField(Patient, on_delete = models.PROTECT, null = True, blank = True)
hta = models.BooleanField()
tobacco = models.BooleanField()
.....
For now, i'm ok with creating a new entry for a patient with inline formset factory.
However, what i'd like to do is displaying the appropriate Template/CBV/Urls... depending of the user speciality to edit and display the related information.
For instance, if i'm a cardiologist: show on the main page, along with the "patient" model class details, specific information related to cardiology. But if i'm a surgeon, showing the same page, but with the specific informations for surgery...
I can do that now, i'm using the {% include '...html' %} in my template to insert what i want.
I'm thinking about creating a specific tag to dynamically display the related information...
But i've no clue about how to do for the edit page etc....Except creating a script with bunch of dicts to create the relationships, but it seems to me a nightmare.
What's your opinion, is there a more elegant way, more "logic". Based on names of each class view maybe...
Thanks for your help !
I eventually ended up with a custom tag...Works well so far :) !
#TAGS
from django import template
from django.template.loader import render_to_string #generate a string from a template
import datetime
from django.urls import reverse
from django.template.base import (
Node, Template, TemplateSyntaxError, TextNode, Variable, token_kwargs,
)
register = template.Library()
def get_activ_specialite(request, context):
app_name = request.resolver_match.app_name
if app_name == 'patient':
#get the specialite of the user
specialite = request.user.profile.specialite_active.spe
specialite = '_'.join(specialite.lower().split(' '))
return specialite
elif app_name == 'hospitalisation':
#get the specialite of the service where the patient is hospitalized !
specialite = context['hospitalisation'].specialite_referente.spe
specialite = '_'.join(specialite.lower().split(' '))
return specialite
return
#register.simple_tag(takes_context=True)
def routing_antecedents(context, destination):
if 'patient' in context:
patient = context['patient']
id = patient.id
if 'hospitalisation' in context:
hos = context['hospitalisation']
id = hos.id
request = context['request']
#1/get service référent du patient
app_name = get_activ_specialite(request, context)
#2/ redirect to urls...! Name consistency is mandatory !!!
url = str(app_name + ":" + destination)
url = reverse(str(url), args=(id,) )
# return url path !
return url
#register.simple_tag(takes_context=True)
def include_antecedents_spe(context, template_name):
request = context['request']
#1/get service référent du patient
app_name = get_activ_specialite(request, context)
template_name = template_name.replace("'","").replace('"', '')
template_name = str(str(app_name) + "/" + str(template_name))
html = render_to_string(template_name, context.flatten())
return html
I would like my data to be editable inline in the Django admin page. However, I only want some fields columns in each row to be editable. These columns will change for each row. Basically, I want a dropdown choice to be displayed if the value in a certain cell is null. If it is not null, then I don't want it to be editable and would like it to be readonly.
models.py:
class Size(models.Model):
size = models.CharField(max_length=20, primary_key=True)
class Book(models.Model):
title = models.CharField(max_length=100, primary_key=True)
size = models.ForeignKey(Size, null=False)
class Pamphlet(models.Model):
title = models.CharField(max_length=100, primary_key=True)
size = models.ForeignKey(Size, null=True)
book = models.ForeignKey(Book, null=True)
admin.py:
class PamphletAdmin(admin.ModelAdmin):
model = Pamphlet
list_editable = ('size','book')
list_display = ('title', 'size', 'book',)
def get_changelist_form(self, request, **kwargs):
return PamphletChangeListForm
forms.py:
class PamphletChangeListForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(PamphletChangeListForm, self).__init__(*args, **kwargs)
instance = kwargs.get('instance')
if instance:
self.fields['book'].queryset = Book.objects.filter(
size=instance.size
)
if instance.size is not None:
self.fields['size'].widget.attrs['readonly'] = 'readonly'
This setup is not working for me. The size shows as editable in the changelist form even when it is not null. Also - what have I failed to understand?
If your field uses an input element, such as a TextField, add the readonly attribute to the field's widget's attrs dict in the changelist form's __init__ method. Something like this:
class PamphletChangeListForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(PamphletChangeListForm, self).__init__(*args, **kwargs)
instance = kwargs.get('instance')
if instance:
self.fields['book'].queryset = Book.objects.filter(
size=instance.size
)
if instance.size is not None:
self.fields['size'].widget.attrs['readonly'] = 'readonly'
That won't protect you against a malicious user faking post data - for that you'd need to customize your admin further. But if you have malicious users on your admin you have bigger problems.
If your field uses a select element, you have to change it more - select attributes don't have readonly as a supported attribute. Instead, you'll want a hidden input with the unchanging value, and a text representation so the user can see what the setting is. Django does not include such a widget, but you can define your own:
class LabeledHiddenInput(forms.widgets.HiddenInput):
def render(self, name, value, attrs=None):
base_output = super(LabeledHiddenInput, self).render(name, value, attrs)
if value:
return base_output + unicode(value)
else:
return base_output
You might need more careful escaping or even some HTML formatting, this is just a quick example. Check the source code for the built in widgets if you need more examples.
Then you can use that widget instead of the default select:
if instance.size is not None:
self.fields['size'].widget = LabeledHiddenInput()