Django - Use TextInput form to look up a ForeignKey - python

I'm currently trying to build an application in Django where users should be able to create new entries for a model. Said model contains a ForeignKey relation to a different model. However, the classic dropdown form field is not practical, because there could be hundreds of entries to choose from. Instead, I'd like to allow users to simply type in the name of the entry (all names are unique), which then gets transformed to the corresponding ID when pressing the Submit button.
I feel like this should be pretty straightforward to do, but since I'm new to Django I'm having a hard time to figure out how to do it. Currently the problem is, that the form field is declared invalid, due to the ForeignKey not existing (since the user typed in the name and not the ID of the database entry).
Code example:
forms.py:
class DogForm(ModelForm):
class Meta:
model = Dog
fields = "__all__"
widgets = {" shelter": widgets.TextInput(attrs={"id":" shelter", "placeholder":" Dog Shelter"}),
"name": widgets.TextInput({"id":"name", "placeholder":"Name"}),
def clean(self):
shelter= self.data.get("shelter")
self.cleaned_data["shelter"] = Shelter.objects.get(name=shelter)
return self.cleaned_data
views.py:
class DogCreate(CreateView):
model = Dog
form_class = DogForm
def form_valid(self, form):
if form.is_valid():
instance = form.save(commit=False)
instance.save()
return super(DogCreate, self).form_valid(form)
As you can see, my idea was to overwrite the clean method, by adding the correct Shelter to self.cleaned_data (based on the name the user put into the TextInputField). But this does not seem to work, because when pressing submit, the form is still declared invalid.

Related

m2m relations are not displayed or saved in django

class A(Model):
to_b = ManyToManyField('B', blank=True, through='AtoB')
class B(Model):
to_a = ManyToManyField('A', blank=True, through='AtoB')
class AtoB(Model):
a = ForeignKey('A', on_delete=CASCADE)
b = ForeignKey('B', on_delete=CASCADE)
usr = ForeignKey(settings.USER, on_delete=CASCADE)
# some other fields
Im making a django application.
This is roughly equivalent to what i have in my models.py
I need m2m relation between A and B to go through another model because i need to store additional data there.
Now there is a problem - when i try to save instance of model A in my custom view, relations with B instances are not saved no matter whether i pick them or not.
And when i go to http://127.0.0.1:8000/admin and try to create instance of A from there, i dont even see proper field (should be <select multiple> i guess) for selecting relations with B.
Can someone please explain me why relations are not saved, and not even displayed in /admin?
Here is code roughly equivalent to what i have in views.py:
class Create(CreateView):
model = None # A or B
template_name = 'something.html'
def form_valid(self, form):
self.object = form.save(commit=False)
form.save()
return HttpResponseRedirect(self.get_success_url())
in urls.py i specify additional arguments like this:
views.Create.as_view(model=models.A, fields=['to_b'])
It is not going to work. If you are using your own ManytoMany intermediary table, you have to manually manage and save the objects yourself. Using Django's builtin functions won't work.
Save Object A, then save Object B and then save the relation in your AtoB table (which is also an object).
a_to_b = AtoB.objects.create(a=object_a, b=object_b, user=self.request.user)
print(a_to_b)
[...] Note that if you are using an intermediate
model for a many-to-many relationship, some of the related manager’s
methods are disabled, so some of these examples won’t work with such
models.
https://docs.djangoproject.com/en/1.10/topics/db/examples/many_to_many/
Your error is explained here: https://docs.djangoproject.com/en/1.10/topics/db/models/#intermediary-manytomany

Django. Get id of ForeignKey object in CreateView

I study the CBV in django.
I want a user can upload images to a certain card of an apartment.
So The model was created:
class Photo(models.Model):
photo_path = models.ImageField(verbose_name='Фотография')
photo_user = models.ForeignKey(User, verbose_name='Агент, добавивший фото')
photo_flat = models.ForeignKey(Flat, verbose_name='Квартира')
photo_description = models.CharField(verbose_name='Описание', max_length=200, null=True, blank=True)
The initial data of the form are photo_user and the photo_flat.
When I try to save one or multiple pictures through a form on the page I've got AttributeError.
'NoneType' object has no attribute 'pk'
My ModelForm looks like this:
class PhotoUploadModelForm(forms.ModelForm):
class Meta:
model = Photo
fields = ['photo_path','photo_user','photo_flat','photo_description'
My CreateView:
class PhotoUploadView(CreateView):
form_class = PhotoUploadModelForm
def get_initial(self):
return {'photo_user': self.request.user,
'photo_flat': self.object.pk
}
def get_success_url(self):
return reverse('ha:flatdetail')
and my urls.py
url(r'^dev/flat/(?P<flat_id>[0-9]+)/$', views_dev.flat_ajax, name='flatdetail'),
url(r'^dev/photo-update/$', views_dev.PhotoUploadView.as_view(), name='image-update')
I understand the error but has not enough skills to handle it. am I correct that self.object.pk would be the id of the picture but not the apartment?
Can I get the id of the apartment?
You're misinterpreting the error, but that is partly because what you're doing doesn't make a lot of sense.
self.object can only refer to the object that this view is concerned with, which in this case is the Photo; but of course that photo doesn't exist yet when you do get_initial, because it hasn't been created. It seems that you want to use the ID of a specific Flat object; in which case you would need to pass that ID to the view, ie via the URL. You do this already in your flat_ajax view; you should do exactly the same thing here, and then you will be able to reference the ID via self.kwargs['flat_id'].
Note that get_initial isn't really the best place to be doing this, anyway. The User and the Flat object are not values that you want to pass to the form so that they populate fields initially but let the user change them; they are values you want to automatically associate with the object on save. So, this should really be done in form_valid, as shown in the documentation:
def form_valid(self, form):
form.instance.photo_user = self.request.user
form.instance.photo_flat_id = self.kwargs['flat_id']
return super(PhotoUploadView, self).form_valid(form)
If you do it this way, you should remove photo_user and photo_flat from the fields listed in the form.

Django: combine two ForeignKeys into one field

I need to implement the following:
The user shall be presented with a form that will have a drop down choice menu consisting of property names. There are two types of properties: general properties, i.e. properties common for all users and custom properties, i.e. properties that each user has defined prior to that. The models would look something like that:
class GeneralPropertyName(models.Model):
name = models.CharField(max_length=20)
class CustomPropertyName(models.Model):
user = models.ForeignKey(User)
name = models.CharField(max_length=20)
The drop down menu should have all general properties and only those custom properties that pertain to the user.
First question: how to define such a model?
I need to: 1. somehow unify both properties, 2. take only those items from CustomPropertyName that pertain to the user
class SpecData(models.Model):
user = models.ForeignKey(User)
selection_title = models.CharField(max_length=20)
property = ForeignKey(GeneralPropertyName) ??UNIFY??? ForeignKey(CustomPropertyName)
Second, is there anything special that needs to be done with ModelForm?
class SpecDataForm(ModelForm):
class Meta:
model = SpecData
And the 3rd question is what needs to be done in the view? I will need to use inline formsets since I will have a few dynamic forms like that.
def index(request):
user = User.objects.get(username=request.user.username)
specdataFormSet = inlineformset_factory(User, SpecData, form=SpecDataForm, extra=30)
...
specdata_formset = specdataFormSet(instance=user, prefix='specdata_set')
...
Thanks.
EDIT: Adjusted juliocesar's suggestion to include formsets. Somehow I am getting the following error message: Cannot resolve keyword 'property' into field. Choices are: id, name, selection_title, user
def index(request):
user = User.objects.get(username=request.user.username)
user_specdata_form = UserSpecDataForm(user=user)
SpecdataFormSet = inlineformset_factory(User, SpecData, form=user_specdata_form, extra=30)
You can use a GenericForeignKey to handle it, but you still need more to solve your further questions about forms and view.
I have made an example of how you solve your problem (logged user can select from General properties and his Custom properties, non-logged user only can select General properties). I used model inheritance for the properties (In your sample code it seems that a CustomPropertyName is a PropertyName with other fields). I think inheritance is an easier and a more basic concept than ContentTypes and it fits to your needs.
NOTE: I remove some code like imports to simplify the code.
1) models.py file:
class PropertyName(models.Model):
name = models.CharField(max_length=20)
def __unicode__(self):
return self.name
class CustomPropertyName(PropertyName): # <-- Inheritance!!
user = models.ForeignKey(User)
def __unicode__(self):
return self.name
class SpecData(models.Model):
user = models.ForeignKey(User)
selection_title = models.CharField(max_length=20)
property = models.ForeignKey(PropertyName)
NOTES: The field SpecData.property points to PropertyName since all properties are saved in the PropertyName's database table.
2) forms.py file:
from django import forms
from django.db.models import Q
from models import SpecData, PropertyName
def UserSpecDataForm(user=None):
UserPropertiesQueryset = PropertyName.objects.filter(Q(custompropertyname__user=None) | Q(custompropertyname__user__id=user.id))
class SpecDataForm(forms.ModelForm):
property = forms.ModelChoiceField(queryset=UserPropertiesQueryset)
class Meta:
model = SpecData
exclude = ('user',)
return SpecDataForm
NOTES: The trick here is to generate the form SpecDataForm dynamically, by filtering properties according the user specified in the parameter.
3) views.py file:
from forms import UserSpecDataForm
def index(request):
if request.POST:
form = UserSpecDataForm(request.user)(request.POST) # instance=user
if form.is_valid():
spec_data = form.save(commit=False)
spec_data.user = request.user
spec_data.save()
else:
form = UserSpecDataForm(request.user)()
return render_to_response('properties.html', {'form': form}, context_instance=RequestContext(request))
NOTES: Nothing special here, just a call to form.UserSpecDataForm(request.user) that returns the form class and then instantiate. Also setted the logged-in user to the object returned on save since It was excluded in the form to not show in front-end.
Following this basic example you can do the same with formsets if you need it.
UPDATE:
Formset can be used by adding following code to the view:
user_specdata_form = UserSpecDataForm(user=request.user)
SpecdataFormSet = inlineformset_factory(User, SpecData, form=user_specdata_form, extra=30)
The complete project sample can be downloaded from http://ge.tt/904Wg7O1/v/0
Hope this helps
1a) have you looked into django's ContentType framework this will allow you to have generic foreign keys and you can put restrictions on what types of models are acceptable to store in.
1b) I think that the validation for accepting what type of foreign key is acceptable shouldn't be in your model but should be part of your form validation before saving.
2) If you do use a model form you're going to have to define your own custom widget for the propery field. This means you're probably going to have to write you're own render function to render the html from the field. You should also define your own validation function on the form to make sure that only the appropriate data is acceptable to save.
3) I don't think you'll have to do anything you aren't already doing in the views
Use GenericForeignKey:
class SpecData(models.Model):
user = models.ForeignKey(User)
selection_title = models.CharField(max_length=20)
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
property = GenericForeignKey('content_type', 'object_id')
You can use this to combine the two fields(type & id) into a single choice field.
One way is that you have only one model, make user nullable:
class PropertyName(models.Model):
user = models.ForeignKey(User, null=True, blank=True)
name = models.CharField(max_length=20)
class SpecData(models.Model):
user = models.ForeignKey(User)
selection_title = models.CharField(max_length=20)
property = ForeignKey(PropertyName)
So, if user is not set, it is a general property. If it is set, it is related to this user.
However, please note that if you need unique property names, that NULL != NULL.
Of course, the suggested GenericForeignKey solution is better for some cases.
Also, you can easily make the normal (non-model) form with that you describe and separate form logic from model logic.

Django link to existing item or create if unique in multi-model form

I have a form which uses multiple models, which have relationships. If the data for one of the forms matches an existing entry, I want to have the foreign key map to the existing entry rather than create a duplicate entry. However, is_valid() fails for the already existing entry and I'm not sure if it is safe to use the form data before it has been cleaned to do a database lookup for existence first. Perhaps I need to change the clean function to ignore the uniqueness requirement and then handle it in the view?
Here's an example, a user enters their name, city and state in a web form. If that city is already known to the database, then the foreign key for the person should just point to the existing entry. If it is a new city, it should be added to the database. So it is always a CREATE for Person, but it may or may not be a CREATE for Hometown.
models.py
class Person(models.Model):
name = models.CharField()
hometown = models.ForeignKey('Hometown')
class Hometown(models.Model):
cityName = models.CharField()
stateName = models.CharField()
mascot = models.CharField()
#If same city and state, it's the same place
class Meta:
unique_together = ("cityName", "stateName")
forms.py
class PersonForm(ModelForm):
class Meta:
model = Person
exclude = ('hometown')
class HometownForm(ModelForm):
class Meta:
model = Hometown
views.py
def newPerson(request):
if request.method == 'POST':
person = PersonForm(request.POST)
hometown = HometownForm(request.POST)
if (person.is_valid() and hometown.is_valid():
p = person.save(commit=False)
h = Hometown.objects.get_or_create(**hometown.cleaned_data)
p.hometown = h
p.save()
This code doesn't work, because hometown.is_valid() will be False if that city/state pair is already in the database. Should I override the clean() function to allow ignore the uniqueness requirement (enforcing it in the view by using get_or_create) or is that an indication that my design is fundamentally the wrong way of addressing this problem?
A similar question on stackoverflow contained the information I needed. Free-form input for ForeignKey Field on a Django ModelForm
As applied to my original question, I removed the HometownForm and instead added the relevant fields to PersonForm, then made the save() function of PersonForm handle the get_or_create() behavior for the Hometown.

ModelForm with a reverse ManytoMany field

I'm having trouble getting ModelMultipleChoiceField to display the initial values of a model instance. I haven't been able to find any documentation about the field, and the examples I've been reading are too confusing. Django: ModelMultipleChoiceField doesn't select initial choices seems to be similar, but the solution that was given there is not dynamic to the model instance.
Here is my case (each database user is connected to one or more projects):
models.py
from django.contrib.auth.models import User
class Project(Model):
users = ManyToManyField(User, related_name='projects', blank=True)
forms.py
from django.contrib.admin.widgets import FilteredSelectMultiple
class AssignProjectForm(ModelForm):
class Meta:
model = User
fields = ('projects',)
projects = ModelMultipleChoiceField(
queryset=Project.objects.all(),
required=False,
widget=FilteredSelectMultiple('projects', False),
)
views.py
def assign(request):
if request.method == 'POST':
form = AssignProjectForm(request.POST, instance=request.user)
if form.is_valid():
form.save()
return HttpResponseRedirect('/index/')
else:
form = AssignProjectForm(instance=request.user)
return render_to_response('assign.html', {'form': form})
The form that it returns is not selecting the instance's linked projects (it looks like: Django multi-select widget?). In addition, it doesn't update the user with any selections made when the form is saved.
Edit: Managed to solve this using the approach here: http://code-blasphemies.blogspot.com/2009/04/dynamically-created-modelmultiplechoice.html
Here's a solution that is better than the older ones, which really don't work.
You have to both load the existing related values from the database when creating the form, and save them back when saving the form. I use the set() method on the related name (manager) which does all the work for you: taking away existing relations that are not selected anymore, and adding new ones which have become selected. So you don't have to do any looping or checking.
class AssignProjectForm(ModelForm):
def __init__(self, *args, **kwargs):
super(AssignProjectForm, self).__init__(*args, **kwargs)
# Here we fetch your currently related projects into the field,
# so that they will display in the form.
self.fields['projects'].initial = self.instance.projects.all(
).values_list('id', flat=True)
def save(self, *args, **kwargs):
instance = super(AssignProjectForm, self).save(*args, **kwargs)
# Here we save the modified project selection back into the database
instance.projects.set(self.cleaned_data['projects'])
return instance
Aside from simplicity, using the set() method has another advantage that comes into play if you use Django signals (eg. post_save etc) on your m2m relation: If you add and remove entries one at a time in a loop, you'll get signals for each object. But if you do it in one operation using set(), you'll get just one signal with a list of objects. If the code in your signal handler does significant work, this is a big deal.
ModelForm's don't automatically work for reverse relationships.
Nothing is happening on save() because a ModelForm only knows what to do with its own fields - projects is not a field on the User model, it's just a field on your form.
You'll have to tell your form how to save itself with this new field of yours.
def save(self, *args, **kwargs):
for project in self.cleaned_data.get('projects'):
project.users.add(self.instance)
return super(AssignProjectForm, self).save(*args, **kwargs)

Categories

Resources