Django forms with prefixes not updating on save() - django

I iterate through a list of Block objects, instantiate a ModelForm for each of them with a mapping dictionary that links a block_type to a ModelForm model, and then append the form to a list which I pass off to a template for display.
for block in blocks:
block_instance = block_map[block.block_type].objects.get(id=block.id)
new_form = block_forms[block.block_type]
new_form_instance = new_form(
request.user,
request.POST or None,
instance=block_instance,
prefix = block.id
)
form_zones.append(new_form_instance)
Later, while checking request.POST I validate each form
if request.POST.get("save_submit"):
for zone_form_check in story_zones:
for block_form_check in zone_form_check:
if block_form_check.is_valid():
print(block_form_check.cleaned_data.get("content"))
saved = block_form_check.save()
print(saved.content)
valid = True
if valid:
return redirect("Editorial:content", content_id=content_id)
cleaned_data.get("content") produces the updated data, but even after calling save() on the valid form, saved.content produces the object's old content attribute. In other words, a valid form is having save() called upon it, but it is not saving.
One of the forms in question (and currently my only one) is:
class Edit_Text_Block_Form(ModelForm):
content = forms.CharField(widget = forms.Textarea(
attrs = {
"class": "full_tinymce"
}),
label = "",
)
class Meta:
model = TextBlock
fields = []
def __init__(self, user, *args, **kwargs):
self.user = user
super(Edit_Text_Block_Form, self).__init__(*args, **kwargs)
The model in question is a TextBlock, which inherits from a Block objets. Both of those are below:
class Block(models.Model):
zone = models.ForeignKey(Zone)
order = models.IntegerField()
weight = models.IntegerField()
block_type = models.CharField(max_length=32, blank=True)
class Meta:
ordering = ['order']
def delete(self, *args, **kwargs):
# Calling custom delete methods of child blocks
child = block_map[self.block_type].objects.get(id=self.id)
if getattr(child, "custom_delete", None):
child.custom_delete()
# Overriding delete to check if there are any other blocks in the zone.
# If not, the zone itself is deleted
zones = Block.objects.filter(zone=self.zone).count()
if zones <= 1:
self.zone.delete()
# Children of Block Object
class TextBlock(Block):
content = models.TextField(blank=True)
Any ideas for why calling saved = block_form_check.save() isn't updating my model?
Thanks!

I think this is because you've effectively excluded all the model fields from the form by setting fields = [] in the form's Meta class. This means that Django no longer relates the manually-defined content field on the form with the one in the model.
Instead, set fields to ['content'], and it should work as expected.

TL;DR form name cannot start with a number as per html4 specs
Try prefix = "block_%s" % block.id

Related

How to increase the counter on my Foreign Key Model

I have the following doubt:
Let's assume that my Django project has the following models:
class State(models.Model):
initials = models.CharField('Initials', max_length=2, blank = False)
name = models.CharField('State', max_length=50, blank = False)
count = models.IntegerField('Foo Counter', default=0)
....
class Foo(models.Model):
name = models.CharField('Name', max_length=50, blank = False)
state = models.ForeignKey(State, verbose_name='State', related_name='state_fk', on_delete=models.DO_NOTHING),
....
So i have a form to add Foo instances to my db:
class FooForm(forms.ModelForm):
class Meta:
model = Foo
fields = '__all__'
this is the view.py file:
def home(request):
template_name = 'home.html'
form = FooForm(request.POST or None)
if form.is_valid():
salvar = form.save(commit=False)
salvar.save()
return redirect('FooApp:home')
else:
context = {
'form': form
}
return render(request, template_name, context)
I need that, every time the user registers a new 'Foo' the counter of the 'State' chosen by him is increased by 1, i search a lot here and in docs of Django but i could not find a way to do this.
Why do you need count defined as a model field if it's dependent on a database computation, and not something that will be entered from outside?
As mentioned before, you can add logic in the application to update count value from State to self.foo_set.count()
However, I think that it might worth looking into a different approach which would be defining a cached_property on State as it follows:
#cached_property
def count(self):
return self.foo_set.count()
In this way, you'll be able to access State.count wherever you want in the application and get the right value without worrying to keep it updated.
You may not need to keep track of count manually like that. For any instance of State you can always call:
state.foo_set.count()
That will always give you the current count.

Django Rest Framework: How to save a field as an object of a different model, then return the key to the original model

Sorry for the weird title.
I have 2 models:
class TranslationWord(models.Model):
translation = models.TextField(unique=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class TranslationByUser(models.Model):
synset = models.ForeignKey(Synset)
user = models.ForeignKey(User)
translation = models.ForeignKey(TranslationWord)
The first one is supposed to basically just save words. The second is supposed to get a user's input. If the word exists in the first class, the foreign key value is simply stored. If it doesn't exist, I want to first create an instance of the TranslationWord, and then add the foreign key to the second class.
I'm doing all this with the Django Rest Framework, so I'm pretty stumped.
Currently, I've got these 2 models, 2 serializers (both just instances of ModelSerializer), and a view to save it (ListCreateAPIView).
How should I go about doing this?
These are basically the steps for creating a successfully validated object in a ModelViewSet create method (it's defined in CreateModelMixin):
if serializer.is_valid():
self.pre_save(serializer.object)
self.object = serializer.save(force_insert=True)
self.post_save(self.object, created=True)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED,
headers=headers)
It means you can override pre_save for your action in your ViewSet for TranslationByUser, setting attributes as side-effects in the object:
def pre_save(self, obj):
#get your_translation_word from obj or self.kwargs
your_translation_word = get_translation_word()
translation = TranslationWord(translation=your_translation_word)
translation.save()
setattr(obj, 'translation', translation)
#if you also want to support Update, call super method
super(TranslationByUserViewSet, self).pre_save(obj)
Another thing you can try is to define TranslationWordSerializer as a nested field in TranslationByUserSerializer. This topic is explained in the docs. Not sure if DRF handles everything about the creation though. I've only tested this behaviour with Multi-table Inheritance (and it works).
Anyway, for anyone who is curious, I created a write-only field in the Serializer, and used it to create the instance in the restore_object method.
class MySerializer(serializers.ModelSerializer):
user = UserSerializer(required=False)
translation = TranslationLemmaSerializer(required=False)
translation_text = serializers.WritableField(required=False, write_only=True)
class Meta:
model = TranslationByUser
fields = ('id','user','synset', 'translation', 'translation_text',)
read_only_fields = ('id', 'created_at', 'updated_at',)
def restore_object(self, attrs, instance=None):
print attrs
if instance is not None:
instance.synset = attrs.get('synset', instance.synset)
return instance
translation_text = attrs.get('translation_text')
del attrs['translation_text'] #delete non-model attribute before actually creating it
translationHIT = TranslationByUser(**attrs) #create model here
translation_id = None
try:
translation_instance = TranslationWord.objects.get(translation=translation_text) #check if translationWord is already present
except:
translation_instance = TranslationWord(translation=translation_text)
translation_instance.save() #otherwise, create it
TranslationByUser.translation = translation_instance
print attrs
return TranslationByUser
def get_validation_exclusions(self,instance=None):
exclusions = super(MySerializer, self).get_validation_exclusions()
return exclusions + ['user', 'translation']

Django custom update model form - display selected field of related model rather than foreign key id.

My question is: is there a way to create custom model form that will use a specified field from a related model rather than the related model's id?
To clarify, if I have the following two models:
class ModelOne(models.Model):
id = models.AutoField(primary_key = True)
name = models.CharField(unique = True, blank = False, null = False)
class ModelTwo(models.Model):
id = models.AutoField(primary_key = True)
parent = models.ForeignKey(ModelOne, blank = False, null = False)
attribute_1 = models.CharField(blank = False, null = False)
attribute_2 = models.IntegerField(blank = False, null = False)
Now if I create an UpdateView on ModelTwo using a ModelForm then the parent field will be pre-filled with the corresponding id from ModelOne. However I want it to display the name attribute of ModelOne and then on form submission parse the unique name (of ModelOne) to the corresponding instance of ModelOne. The reason I want to do it this way, is that I believe it is far more intuitive from a users perspective to deal with the name of ModelOne (when updating a ModelTwo instance) rather than its "id".
Any suggestions of how I can do this?
Firstly, try defining the unicode method on ModelOne. It might not apply to the solution, but it's worth having - it will drive the text values in a form Select widget...
def __unicode__(self):
'''Warning: be careful not to use related objects here,
could cause unwanted DB hits when debugging/logging
'''
return self.name
If that's not sufficient, something like this might work (it is adapted from a form I have that updates the user's name attached to a profile)...
class M2Form(forms.ModelForm):
m1_name = forms.CharField()
class Meta:
model = ModelTwo
def save(self, *args, **kw):
# Update your name field here, something like
if self.cleaned_data.get('m1_name'):
self.instance.parent = ModelOne.objects.get(name=self.cleaned_data.get('m1_name'))
return super(M2Form, self).save(*args, **kw)
This is untested, and you'll likely need to adapt this to validate that the name exists and make sure the original parent field doesn't appear on the form. With any luck, the first answer covers what I think your question is.
Using Rog's answer as a starting point and delving through some of Django's internals I eventually came to a working solution. Given my level of Django knowledge, I imagine there is a better way of doing this; so if you have another method please add it.
So based on the above two models, I created the following form class:
class CustomForm(forms.ModelForm):
parent = models.CharField(label='Name')
class Meta:
model = ModelTwo
exclude = ['parent']
def __init__(self,*args,**kwargs):
# The line of code below is the one that I was looking for. It pre-populates
# the "parent" field of the form with the "name" attribute of the related
# ModelOne instance.
kwargs['initial']['parent'] = kwargs['instance'].parent.name
super(CustomForm,self).__init__(*args,**kwargs)
# The next line is for convenience and orders the form fields in our desired
# order. I got this tip from:
# http://stackoverflow.com/questions/913589/django-forms-inheritance-and-order-of-form-fields
self.fields.keyOrder = ['parent','attribute_1','attribute_2']
def save(self, *args, **kwargs):
if self.cleaned_data.get('parent'):
# This section of code is important because we need to convert back from the
# unique 'name' attribute of ModelOne to the corresponding instance so that
# ModelTwo can be saved. Thanks goes to Rog for this section of code.
self.instance.parent = ModelOne.objects.get(name=self.cleaned_data.get('parent'))
return super(CustomForm, self).save(*args, **kwargs)

field choices() as queryset?

I need to make a form, which have 1 select and 1 text input. Select must be taken from database.
model looks like this:
class Province(models.Model):
name = models.CharField(max_length=30)
slug = models.SlugField(max_length=30)
def __unicode__(self):
return self.name
It's rows to this are added only by admin, but all users can see it in forms.
I want to make a ModelForm from that. I made something like this:
class ProvinceForm(ModelForm):
class Meta:
CHOICES = Province.objects.all()
model = Province
fields = ('name',)
widgets = {
'name': Select(choices=CHOICES),
}
but it doesn't work. The select tag is not displayed in html. What did I wrong?
UPDATE:
This solution works as I wanto it to work:
class ProvinceForm(ModelForm):
def __init__(self, *args, **kwargs):
super(ProvinceForm, self).__init__(*args, **kwargs)
user_provinces = UserProvince.objects.select_related().filter(user__exact=self.instance.id).values_list('province')
self.fields['name'].queryset = Province.objects.exclude(id__in=user_provinces).only('id', 'name')
name = forms.ModelChoiceField(queryset=None, empty_label=None)
class Meta:
model = Province
fields = ('name',)
Read Maersu's answer for the method that just "works".
If you want to customize, know that choices takes a list of tuples, ie (('val','display_val'), (...), ...)
Choices doc:
An iterable (e.g., a list or tuple) of
2-tuples to use as choices for this
field.
from django.forms.widgets import Select
class ProvinceForm(ModelForm):
class Meta:
CHOICES = Province.objects.all()
model = Province
fields = ('name',)
widgets = {
'name': Select(choices=( (x.id, x.name) for x in CHOICES )),
}
ModelForm covers all your needs (Also check the Conversion List)
Model:
class UserProvince(models.Model):
user = models.ForeignKey(User)
province = models.ForeignKey(Province)
Form:
class ProvinceForm(ModelForm):
class Meta:
model = UserProvince
fields = ('province',)
View:
if request.POST:
form = ProvinceForm(request.POST)
if form.is_valid():
obj = form.save(commit=True)
obj.user = request.user
obj.save()
else:
form = ProvinceForm()
If you need to use a query for your choices then you'll need to overwrite the __init__ method of your form.
Your first guess would probably be to save it as a variable before your list of fields but you shouldn't do that since you want your queries to be updated every time the form is accessed. You see, once you run the server the choices are generated and won't change until your next server restart. This means your query will be executed only once and forever hold your peace.
# Don't do this
class MyForm(forms.Form):
# Making the query
MYQUERY = User.objects.values_list('id', 'last_name')
myfield = forms.ChoiceField(choices=(*MYQUERY,))
class Meta:
fields = ('myfield',)
The solution here is to make use of the __init__ method which is called on every form load. This way the result of your query will always be updated.
# Do this instead
class MyForm(forms.Form):
class Meta:
fields = ('myfield',)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Make the query here
MYQUERY = User.objects.values_list('id', 'last_name')
self.fields['myfield'] = forms.ChoiceField(choices=(*MYQUERY,))
Querying your database can be heavy if you have a lot of users so in the future I suggest some caching might be useful.
the two solutions given by maersu and Yuji 'Tomita' Tomita perfectly works, but there are cases when one cannot use ModelForm (django3 link), ie the form needs sources from several models / is a subclass of a ModelForm class and one want to add an extra field with choices from another model, etc.
ChoiceField is to my point of view a more generic way to answer the need.
The example below provides two choice fields from two models and a blank choice for each :
class MixedForm(forms.Form):
speaker = forms.ChoiceField(choices=([['','-'*10]]+[[x.id, x.__str__()] for x in Speakers.objects.all()]))
event = forms.ChoiceField(choices=( [['','-'*10]]+[[x.id, x.__str__()] for x in Events.objects.all()]))
If one does not need a blank field, or one does not need to use a function for the choice label but the model fields or a property it can be a bit more elegant, as eugene suggested :
class MixedForm(forms.Form):
speaker = forms.ChoiceField(choices=((x.id, x.__str__()) for x in Speakers.objects.all()))
event = forms.ChoiceField(choices=(Events.objects.values_list('id', 'name')))
using values_list() and a blank field :
event = forms.ChoiceField(choices=([['','-------------']] + list(Events.objects.values_list('id', 'name'))))
as a subclass of a ModelForm, using the one of the robos85 question :
class MixedForm(ProvinceForm):
speaker = ...

Free-form input for ForeignKey Field on a Django ModelForm

I have two models related by a foreign key:
# models.py
class TestSource(models.Model):
name = models.CharField(max_length=100)
class TestModel(models.Model):
name = models.CharField(max_length=100)
attribution = models.ForeignKey(TestSource, null=True)
By default, a django ModelForm will present this as a <select> with <option>s; however I would prefer that this function as a free form input, <input type="text"/>, and behind the scenes get or create the necessary TestSource object and then relate it to the TestModel object.
I have tried to define a custom ModelForm and Field to accomplish this:
# forms.py
class TestField(forms.TextInput):
def to_python(self, value):
return TestSource.objects.get_or_create(name=value)
class TestForm(ModelForm):
class Meta:
model=TestModel
widgets = {
'attribution' : TestField(attrs={'maxlength':'100'}),
}
Unfortunately, I am getting: invalid literal for int() with base 10: 'test3' when attempting to check is_valid on the submitted form. Where am I going wrong? Is their and easier way to accomplish this?
Something like this should work:
class TestForm(ModelForm):
attribution = forms.CharField(max_length=100)
def save(self, commit=True):
attribution_name = self.cleaned_data['attribution']
attribution = TestSource.objects.get_or_create(name=attribution_name)[0] # returns (instance, <created?-boolean>)
self.instance.attribution = attribution
return super(TestForm, self).save(commit)
class Meta:
model=TestModel
exclude = ('attribution')
There are a few problems here.
Firstly, you have defined a field, not a widget, so you can't use it in the widgets dictionary. You'll need to override the field declaration at the top level of the form.
Secondly get_or_create returns two values: the object retrieved or created, and a boolean to show whether or not it was created. You really just want to return the first of those values from your to_python method.
I'm not sure if either of those caused your actual error though. You need to post the actual traceback for us to be sure.
TestForm.attribution expects int value - key to TestSource model.
Maybe this version of the model will be more convenient for you:
class TestSource(models.Model):
name = models.CharField(max_length=100, primary_key=True)
Taken from:
How to make a modelform editable foreign key field in a django template?
class CompanyForm(forms.ModelForm):
s_address = forms.CharField(label='Address', max_length=500, required=False)
def __init__(self, *args, **kwargs):
super(CompanyForm, self).__init__(*args, **kwargs)
try:
self.fields['s_address'].initial = self.instance.address.address1
except ObjectDoesNotExist:
self.fields['s_address'].initial = 'looks like no instance was passed in'
def save(self, commit=True):
model = super(CompanyForm, self).save(commit=False)
saddr = self.cleaned_data['s_address']
if saddr:
if model.address:
model.address.address1 = saddr
model.address.save()
else:
model.address = Address.objects.create(address1=saddr)
# or you can try to look for appropriate address in Address table first
# try:
# model.address = Address.objects.get(address1=saddr)
# except Address.DoesNotExist:
# model.address = Address.objects.create(address1=saddr)
if commit:
model.save()
return model
class Meta:
exclude = ('address',) # exclude form own address field
This version sets the initial data of the s_address field as the FK from self, during init , that way, if you pass an instance to the form it will load the FK in your char-field - I added a try and except to avoid an ObjectDoesNotExist error so that it worked with or without data being passed to the form.
Although, I would love to know if there is a simpler built in Django override.