Django Form with a one-to-many relationship - python

I have a form in Django called PersonForm this forms model has a one-to-many relationship with Car. When displaying the PersonForm just like in the Django Admin I would like to allow my users to select/deselect from a list of Cars etc. Is this possible? I'm looking for information on where to start.
This is what I have so far for the PersonForm:
class PersonForm(forms.ModelForm):
class Meta:
model = Person
fields = ('description',)
The Models:
class Person(models.Model):
description = models.CharField(max_length="150")
class Car(models.Model):
make = models.CharField(max_length="25")
owner = models.ForeignKey('Person', related_name="Car")
So in the person form I need to show a list of cars that person is the owner of an allow selecting/deselecting of them. I'm assuming I can do this in the form i.e. using something like the related name.

Sounds like you want an inline model form. This give you the ability to add/remove Car objects from a Person within the Person form.
That previous link was for inlinemodeladmin. This next link is for an inline form:
https://docs.djangoproject.com/en/dev/topics/forms/modelforms/#modelforms-factory

I didn't have any chance with inline formset, so i would suggest to override your save method of the model, i feel it's more DRY:
class PersonForm(forms.ModelForm):
# add a field to select a car
car = forms.ModelChoiceField(car.objects.all())
class Meta:
model = Person
fields = ('description', 'car')
def save(self, commit=True):
instance = super().save(commit)
# set Car reverse foreign key from the Person model
instance.car_set.add(self.cleaned_data['car']))
return instance

I know this is an old thread, but since I found myself almost exclusively pointed here by google when searching, I thought I would include the following for anyone else looking for an answer.
The answer, I think, is to use
https://docs.djangoproject.com/en/3.1/ref/forms/fields/#modelchoicefield
or
https://docs.djangoproject.com/en/3.1/ref/forms/fields/#modelmultiplechoicefield
There is a good article on how to use the modelmultiplechoicefield at :
https://medium.com/swlh/django-forms-for-many-to-many-fields-d977dec4b024
But it works for one to many fields as well. These allow us to generate a form with multiple choices as checkboxes or similar widgets based upon a related field in a model.

Related

How to assign Django object ownership without explicitly declaring an owner field on all models?

I'm currently trying to figure out per user object permissions for our Django website API.
I have several models with sensitive information, that I need to be able to filter on a user basis.
For a simplified example of one of the models:
Restaurant, main customer of the website.
User, each user gets assigned a restaurant when the user account is
created. As such, a restaurant can have many users and they all
should only be able to access that restaurant's information.
Oven, which belong to a specific restaurant. A restaurant can have
many ovens.
Recipe, which belong to an oven. An oven can have many different
recipes.
Recipe Results, which belong to a recipe. There can be many different
Recipe Results belonging to the same Recipe (different ingredients
tried, etc).
There are at least 12+ different models. All models from a particular restaurant have to be hidden from other restaurants, we don't want them to be able to look at other restaurant recipes after all!
Not all models have a user = models.ForeignKey(User)
Without having to go into each one of my models and declaring owner = models.ForeignKey(User), is there a way to filter them in my API List Views and Detail Views?
Currently my List API View looks like this (simplified example):
class RecipeResultsListAPIView(ListAPIView):
queryset = RecipeResults.objects.all()
queryset = queryset.prefetch_related('oven')
serializer_class = RecipeResultsListSerializer
filter_backends = (DjangoFilterBackend,)
filter_fields = ('id', 'time', 'oven', 'recipe_name', 'recipe_description')
pagination_class = ExpertPageNumberPagination
def list(self, request):
user = User.objects.get(username=request.user)
restaurant = Restaurant.objects.get(user=user)
ovens = Oven.objects.filter(restaurant=restaurant)
queryset = RecipeResults.objects.filter(oven__in=ovens)
serializer = RecipeResultsListSerializer(queryset, many=True, context={'request':request})
return Response(serializer.data)
And the model for that looks like this:
class RecipeResults(models.Model):
time = models.DateTimeField()
oven = models.ForeignKey(Oven, on_delete=models.CASCADE)
recipe_name = models.CharField(max_length=20)
recipe_description = models.CharField(max_length=50)
def __str__(self):
return str(self.time) + ': ' + self.recipe_name + ' = ' + self.recipe_description
def __key(self):
return self.oven, self.time, self.recipe_name
def __eq__(self, y):
return isinstance(y, self.__class__) and self.__key() == y.__key()
def __hash__(self):
return hash(self.__key())
class Meta:
unique_together=(('time','recipe_name', 'oven-'),)
Specifically looking at the modified list method, currently this works properly to filter API call results to display only those Recipe Results that belong to the user that is logged in.
What I'm trying to figure out is if there's an easier way to do this, as for each model I would have to trace back ownership to the specific restaurant which would get confusing fast as I have 12+ different models.
What I'm not sure is if declaring "owner = models.ForeignKey(User)" on each of those models is the way to go. It feels like it would create many extra steps when retrieving the data.
I have also tried
class IsOwnerOrAdmin(BasePermission):
"""
Custom permission to only allow owners of an object to see and edit it.
Admin users however have access to all.
"""
def has_object_permission(self, request, view, obj):
# Permissions are only allowed to the owner of the snippet
if request.user.is_staff:
return True
return obj.user == request.user
But this didn't seem to filter properly, and besides, not all of the models have a user field assigned to them.
Please keep in mind I'm a junior developer and I'm learning a lot as I go. I'm only working on the API side of the company. The website and schema is already a work in progress and other systems depend on it, and so I'm trying not to modify the schema or models too much (I would like to avoid this if possible, but will do it if it's the only way). I was also brought in just to work on the API at first. The company understands I'm a junior developer and I'm extremely grateful to have been given the opportunity to grow while learning this project, but this one issue seems to be giving me a lot more trouble than actually building the rest of the API for the website.
I would greatly appreciate any help I can get with this!
I think you might benefit from model inheritance in this case.
You can define a base model for your owner-affected objects.
An example can look like:
class OwnedModel(models.Model):
owner = models.ForeignKey(User)
class Meta:
abstract = True
Then you can simply add this as the base for your other models:
class SomeModel(OwnedModel):
"""
This class already has the owner field
"""
A big downside of this approach is that you will still need a migration that will alter every involved table.
If you aren't allowed to do that, you might be able to do it with a loose, non relational approach, for example with django's permission model. You can assign automatically generated permission strings, eg: myapp.mymodel.pkey:
A final alternative is this third party source app that handles things for you: django-guardian

Django form: ManyToMany inline creation

I would like to be able to let users create locations "on-the-fly" when they create a report. Using the following models if possible.
models:
class Report(models.Model):
...
location = forms.ManyToManyField(Location)
class Location(models.Model):
name = forms.CharField(max_length=255)
...
The behavior I am searching for is close to the one given by the django formsets.
Indeed I am currently able to somewhat do that using instead a foreignkey relationship and relying on formsets. Using this technic,django-extra-views and django-dynamic-formset, I am even able to let users choose how many locations they want to add.
A strip down version of the actual setup :
models:
class Report(models.Model):
...
class Location(models.Model):
name = forms.CharField(max_length=255)
report = forms.ForeignKey('Report')
forms:
class LocationForm(forms.ModelForm):
class Meta:
model = Location
fields = '__all__'
class LocationFormSet(extra_views.InlineFormSet):
model = TestLocation
form_class = TestLocationForm
views:
class ReportCreateView(extra_views.CreateWithInlinesView):
model = Report
inlines = [LocationFormSet, ]
Unfortunately using this setup, a location can not belong to more than one report which bring a lot of duplicated location. That's why I think the ManyToMany relationship would be better suited. I stumbled upon some answers related to this question that talked about intermediary model but I wasn't able to go anywhere on my own to make it work.
Is their a way to tweak my working setup to make it work with a manytomany relationship? Or does it exists a completely different approach to let user create "on-the-fly" the locations during the report creation?
Thank's!
For create "on-the-fly" I had changed ManyToMany by ForeingKey.
class Report(models.Model):
...
location = forms.ManyToManyField(Location) # REMOVE
class Location(models.Model):
report = models.ForeignKey(Report) # ADD
name = forms.CharField(max_length=255)
...
And then, InlineForms will work.

Using Django ModelForm with non-persistent model.Field

Is there a way to add a field to a Django model class such that:
It doesn't get persisted to the database (i.e. no column in the DB)
It does get rendered by a ModelForm
The widget for that field can be customised
I believe 3. can be done with a custom widget, and 2. will happen if the field inherits from models.Field. However, I haven't found a way to achieve 1. without breaking 2. and 3. I was hoping for a persist=False or db_column=None type of solution.
Scenario:
I'm using this to quickly produce data capture forms by only adding a class to the model, but in order to insert headers for sub sections I still having to edit the template. Was hoping to do the following:
models.py
from django.db import models
class Applicant(models.Model):
sectionA = models.SectionField(help_text="Personal details")
title = models.CharField(max_length=100)
name = models.CharField(max_length=100)
sectionB = models.SectionField(help_text="Banking details")
account = models.CharField(max_length=100)
pin = models.CharField(max_length=100)
In the above example, sectionA and sectionB are instances of a custom model.Field that doesn't actually get persisted but cause a heading to be rendered by the ModelForm and a custom widget
Finally:
I realise this probably violates separation of View and Model.
Other questions have been asked about non-persisting fields but their solutions don't render in a ModelForm
Sort of, Just don't make them a model field, theres no need for them to be.
sectionA = "Personal details"
sectionB = "Banking details"
You can access them via form.instance where you need them, you could even make them a form field instead of a string as I've shown here.

Django 1.7: how to make ManyToManyField required?

I have a Django Model with ManyToManyField in it. I need to require user to select at least one M2M value in this field.
I tried to set blank=False to M2M field but it didn't help.
class Skill(models.Model):
name = models.CharField(max_length=200)
class PersonSkills(models.Model):
person = models.ForeignKey('Person')
skill = models.ForeignKey('Skill')
class Person(models.Model):
name = models.CharField(max_length=200)
skills = models.ManyToManyField('Skill', through='PersonSkills')
p = Person(name='Bob')
p.save()
# success, but I expect that this should throw ValidationError, because I didn't select at least one Skill for this person
I can solve this situation with custom Form definition or with override save() method for Person model.
Is it possible to prevent create Person without at least one Skill selected, with set ManyToManyField options? Or I need to create custom logic to handle this situation? Thanks.
I use Django 1.7 and Python 3.4
Update 1. How to create ModelForm to control M2M? Because in cleaned_data I have only fields that I pass for Person form, and haven't data that I pass as M2M fields. I try to create object in Admin Site and control that Skills selected. I enter Skill's via inline.
# admin.py
class PersonSkillsInline(admin.TabularInline):
model = Person.skills.through
extra = 2
class PersonAdmin(admin.ModelAdmin):
inlines = [PersonSkillsInline]
admin.site.register(Person, PersonAdmin)
On a database level... no, that's not possible. Any enforcement of this will have to come from your application logic.
The reason is that every m2m relation has a record with a foreign key to both sides of the m2m relation. SQL cannot enforce the existence of the referencing side of a relationship, only of the referenced side of a relationship.
Furthermore, you can't enforce it in your model either, because the Person has to be created and saved before you can assign any many-to-many relations.
Your only options are to enforce it in the form or the view.
In an InlineModelAdmin this can easily be done by specifying min_num (1.7+):
class PersonSkillsInline(admin.TabularInline):
model = Person.skills.through
min_num = 1
extra = 2

Django: saving a ModelForm with custom many-to-many models

I have Publications and Authors. Since the ordering of Authors matters (the professor doesn't want to be listed after the intern that contributed some trivial data), I defined a custom many-to-many model:
class Authorship(models.Model):
author = models.ForeignKey("Author")
publication = models.ForeignKey("Publication")
ordering = models.IntegerField(default=0)
class Author(models.Model):
name = models.CharField(max_length=100)
class Publication(models.Model):
title = models.CharField(max_length=100)
authors = models.ManyToManyField(Author, through=Authorship)
I've got aModelForm for publications and use it in a view. Problem is, when I call form.save(), the authors are obviously added with the default ordering of 0. I've written a OrderedModelMultipleChoiceField with a clean method that returns the objects to be saved in the correct order, but I didn't find the hook where the m2m data is actually saved, so that I could add/edit/remove the Authorship instances myself.
Any ideas?
If you are using a custom M2M table using the through parameter, I believe you must do the saves manually in order to save the additional fields. So in your view you would add:
...
publication = form.save()
#assuming that these records are in order! They may not be
order_idx = 0
for author in request.POST.getlist('authors'):
authorship = Authorship(author=author, publication=publication, ordering=order_idx)
authorship.save()
order_idx += 1
You may also be able to place this in your ModelForm's save function.
I'm not sure if there's a hook for this, but you could save it manually with something like:
form = PublicationForm(...)
pub = form.save(commit=False)
pub.save()
form.save_m2m()
So you can handle any custom actions in between as required. See the examples in the docs for the save method.

Categories

Resources