Django - manage error with full_clean() validaton - python

I use modelformset_factory, and I use full_clean() to validate the form with unique_together=True. I wonder what is the best way to handle error in case the unique_together do not validate in order to return the error message in the template.
Please take a look to my view, and tell me if im correct the way I do it, or if there is a better approach.
model:
class Attribute(models.Model):
shapefile = models.ForeignKey(Shapefile)
name = models.CharField(max_length=255, db_index=True)
type = models.IntegerField()
width = models.IntegerField()
precision = models.IntegerField()
def __unicode__(self):
return self.name
def delete(self):
shapefile = self.shapefile
feature_selected = Feature.objectshstore.filter(shapefile=shapefile)
feature_selected.hremove('attribute_value', self.name)
super(Attribute, self).delete()
class Meta:
unique_together = (('name', 'shapefile'),)
form:
class AttributeForm(ModelForm):
def __init__(self, *args, **kwargs):
super(AttributeForm, self).__init__(*args, **kwargs)
instance = getattr(self, 'instance', None)
if instance and instance.pk:
self.fields['type'].widget.attrs['disabled'] = True
self.fields['type'].required = False
self.fields['width'].widget.attrs['readonly'] = True
self.fields['precision'].widget.attrs['readonly'] = True
def clean_type(self):
if self.instance and self.instance.pk:
return self.instance.type
else:
return self.cleaned_data['type']
type = forms.ChoiceField(choices=FIELD_TYPE)
class Meta:
model = Attribute
exclude = 'shapefile'
view:
def editFields(request, shapefile_id):
layer_selected = Shapefile.objects.get(pk=shapefile_id)
attributes_selected= Attribute.objects.filter(shapefile__pk=shapefile_id)
attributesFormset = modelformset_factory(Attribute, form=AttributeForm, extra=1, can_delete=True)
if request.POST:
formset = attributesFormset(request.POST, queryset=attributes_selected)
if formset.is_valid():
instances = formset.save(commit=False)
for instance in instances:
instance.shapefile = layer_selected
try:
instance.full_clean()
except ValidationError as e:
non_field_errors = e.message_dict[NON_FIELD_ERRORS]
print non_field_errors
formset = attributesFormset(queryset=attributes_selected)
return render_to_response("basqui/manage_layer_editFields.html", {'shapefile': layer_selected, 'formset':formset}, context_instance=RequestContext(request))
instance.save()
formset = attributesFormset(queryset=attributes_selected)
return render_to_response("basqui/manage_layer_editFields.html", {'shapefile': layer_selected, 'formset':formset}, context_instance=RequestContext(request))

The disadvantage of your approach is that you have moved the validation from the form to the view.
I had the same problem recently of validating a unique together constraint where one field is excluded from the model form. My solution was to override the model form's clean method, and query the database to check the unique together constraint. This duplicates the code that is called by full_clean, but I like it because it's explicit.
I briefly thought about overriding _get_validation_exclusions which would have been more DRY, but I decided not to rely on a private api.

Related

'collections.OrderedDict' object has no attribute 'pk' - django rest framework

I have a model and I want to write an update() method for it in order to update.
The below snippet is my model:
class Klass(models.Model):
title = models.CharField(max_length=50)
description = models.CharField(max_length=500)
university = models.CharField(max_length=50,blank=True, null=True)
teacher = models.ForeignKey(Profile, related_name='teacher', on_delete=models.CASCADE)
and the below snippet is corresponding Serializer:
class KlassSerializer(ModelSerializer):
teacher = ProfileSerializer()
url = HyperlinkedIdentityField(view_name='mainp-api:detail', lookup_field='pk')
klass_settings = KlassSettingsSerializer()
class Meta:
model = Klass
fields = ('url', 'id', 'title', 'description', 'university','teacher')
def update(self, instance, validated_data):
instance.title = validated_data.get('title', instance.title)
instance.description = validated_data.get('description', instance.description)
instance.university = validated_data.get('university', instance.university)
instance.save()
return instance
And for update, I use below snippet:
class KlassAPIView(APIView):
def put(self, request, pk=None):
if pk == None:
return Response({'message': 'You must specify class ID'}, status=HTTP_400_BAD_REQUEST)
klass = Klass.objects.get(pk=pk)
if request.user.profile.type != 't':
raise PermissionDenied(detail={'message': 'You aren't teacher of this class, so you can't edit information.'})
serializer = KlassSerializer(data=request.data, context={'request': request})
serializer.initial_data['teacher'] = request.user.profile.__dict__
if serializer.is_valid():
serializer.update(instance=klass, validated_data=serializer.data) # Retrieve teacher and store
return Response({'data': serializer.data}, status=HTTP_200_OK)
else:
return Response({'data': serializer.errors}, status=HTTP_400_BAD_REQUEST)
but when I send data with PUT method, it returns below error:
AttributeError at /api/class/49/
'collections.OrderedDict' object has no attribute 'pk'
and the error occurs in serializer.update(instance=klass, validated_data=serializer.data) line.
Just ran into the same error.
In my case the problem was I accessed serializer.data before doing serializer.save().
Google dropped me here, so maybe someone else will also find this helpful.
Source: https://github.com/encode/django-rest-framework/issues/2964
i don't know if this helps. I always add the id field in the serializer due to that similar issue:
id = serializers.ModelField(model_field=YourModel._meta.get_field('id'), required=False)
Make sure it's required=False because when you create a new record the id field is not present.
Well in my case, I was doing:
champions_list = []
for champion in champions_serializer.data:
c = {"id": champion.id}
champions_list.append(c)
And the correct way to do it is:
champions_list = []
for champion in champions_serializer.data:
c = {"id": champion["id"]}
champions_list.append(c)
And make sure that you return the id inside the serializer.
Many answers to this question note that serializer.save() must be called before using serializer.data.
In my case, I was definitely calling serializer.save(), however, I was overriding the save method on my serializer and did not set self.instance on the serializer in that method.
So if you are overriding save be sure to do:
class MySerializer(serializers.ModelSerializer):
def save(self, *args, **kwargs):
...
self.instance = instance
return self.instance

My form with a ModelMultipleChoiceField is not saving data.

In the admin panel, I can add Persons to my CompleteClass model. There is a M2M relationship between CompleteClass and Person. But, my form doesn't work as it should. The pub_date will update, and I can save the head_count, but not the ModelMultipleChoiceField (persons) -- it will not save.
models.py
class Person(models.Model):
name = models.CharField(max_length=255)
persona_description = models.CharField(max_length=255)
def __str__(self):
return self.name
class CompleteClass(models.Model):
persons = models.ManyToManyField(Person)
class_name = models.CharField(max_length=255)
class_head_count = models.IntegerField()
class_pub_date = models.DateField()
def __str__(self):
return '%s %s' % (self.class_name, self.class_head_count)
def save_complete_class(self):
self.class_pub_date = timezone.now()
self.save()
class Meta:
ordering = ('class_pub_date',)
Here is views.py:
def class_new(request):
if request.method == "POST":
form = CompleteClassForm(request.POST)
if form.is_valid():
complete_class = form.save(commit=False)
complete_class.class_pub_date = timezone.now()
complete_class.save()
form.save_m2m()
return redirect('class_detail', pk=complete_class.pk)
else:
form = CompleteClassForm()
return render(request, 'app/class_edit.html', {'form': form})
and forms.py
class CompleteClassForm(forms.ModelForm):
class Meta:
model = CompleteClass
fields = ('class_name', 'class_head_count',)
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super(CompleteClassForm, self).__init__(*args, **kwargs)
self.fields['class_persons']=forms.ModelMultipleChoiceField(queryset=Person.objects.all())
I've read through the documentation and used the save_m2m since i've set commit=false.
The POST data contains person data, but it's not being written to the database. I'm stumped. Please help!
Only fields named in the fields tuple are saved to the instance. You don't have your m2m field listed there.
You also define your modelchoicefield with a different name - class_persons instead of persons. In fact, there is no reason to define that field separately at all - you haven't changed any of the attributes from the defaults.
And once you've removed that definition, there ​is also no reason to override __init__, seeing as you never pass the user parameter nor do you use it anywhere in the form.

Send current user when model is saved in Django

I have a problem after a bigger upgrade from Django 1.7 to 1.10 and to Django Rest Framework 3.5.4. The problem is when I try to access the endopint /claims it returns a 500 Error with the text: IntegrityError at /claims (1048, "Column 'user_id' cannot be null")
In urls.py that route is added like this:
url('^claims', v.ClaimList.as_view(), name='claim_list')
And the relevant part from the views.py file is:
class ClaimSerializer(serializers.ModelSerializer):
company_name = s.ReadOnlyField(source="ad.company.name")
company_address = s.ReadOnlyField(source="ad.company.address")
ad_thumbnail = ThumbnailField(source="ad.picture", size="200x200", read_only=True)
class ClaimSerializerDeep(ClaimSerializer):
class Meta:
exclude = ('user',)
model = m.Claim
depth = 2
class ClaimSerializerFlat(ClaimSerializer):
class Meta:
exclude = ('user',)
model = m.Claim
#permission_classes((IsAuthenticated,))
class ClaimList(Limitable, generics.ListCreateAPIView):
model = m.Claim
def get_queryset(self):
tab = self.request.GET.get("tab", "active")
q = m.Claim.objects.filter(user=self.request.user.pk)
return self.limit(q)
def pre_save(self, obj):
obj.user = self.request.user
def get_serializer_class(self):
if self.request.method == "POST":
return ClaimSerializerFlat
else:
return ClaimSerializerDeep
And the claim model is:
class Claim(models.Model):
ad = models.ForeignKey("Ad")
user = models.ForeignKey(settings.AUTH_USER_MODEL)
created = models.DateTimeField(auto_now_add=True)
redeemed = models.BooleanField(default=False)
def save(self, *args, **kwargs):
increase_claimed = False
super(Claim, self).save(*args, **kwargs) # here is where it crashes
Do you have any ideas what could cause the problem?
user = models.ForeignKey(settings.AUTH_USER_MODEL)
Your model requires that a user gets set, but obviously it isn't (anymore).
The question is why?
Looking at your code this seens to be the relevant line:
def pre_save(self, obj):
obj.user = self.request.user
Assuming you also upgraded DRF it is quite plausible that this doesn't work anymore: http://www.django-rest-framework.org/topics/3.0-announcement/#changes-to-prepost-save-hooks
So you have to adapt your code and use perform_create as explained in the docs.

Validation error for one to one relationship in django modelfrom

I have 2 models which are related to each other using One to One Relation. How can i display the validation error if object of one model is already related to another.
Suppose i have 2 models
Class Place(models.Model):
field1 = CharField()
field2 = CharField()
Class Restaurant(models.Model):
related_field = OneToOneField(Place)
field3 = CharField()
In my case For one place one Restaurant. If user submits new Restaurant for the same place, then a validation should raise
If Restaurant object already attached to Place object, so for second Restaurant for same Place object i want to display validation
In admin we usually see this message "Restaurent with this Place already exists." But how can i achieve the same this using modelform with generic views
class RestaurantCreateView(CreateView):
form_class = RestaurantForm
template_name = 'restaurant_create.html'
def get_success_url(self):
return reverse_lazy('restuarants', args=[self.object.id])
def get_place(self, **kwargs):
id = self.kwargs['pk']
place = Place.objects.get(id=id)
return place
def get_context_data(self, **kwargs):
context = super(RestaurantCreateView, self).get_context_data(**kwargs)
place = self.get_place()
context['place'] = place
return context
def form_valid(self, form):
self.object = form.save(commit=False)
place = self.get_place()
self.object.place = place
self.object.save()
return HttpResponseRedirect(self.get_success_url())
How can perform One To One relation validation in class based generic views?
Then you need to pass the Place object into your Restaurant form. The form validation has no access to these things defined in the view unless you explicitly pass it that data.
class MyForm(forms.ModelForm):
class Meta:
model = Restaurant
def __init__(self, *args, **kwargs):
self.place = kwargs.pop('place', None)
super(MyForm, self).__init__(*args, **kwargs
def clean_related_field(self):
place = self.place
try:
place.restaurant
except Restaurant.DoesNotExist:
return place
raise forms.ValidationError("Restaurant already exists for that book")
class MyView(...):
form_class = MyForm
def get_form_kwargs(self):
kwargs = super(MyView, self).get_form_kwargs()
kwargs['place'] = Place.objects.get(id=self.kwargs['pk'])
return kwargs
{% if form.related_field.errors %}.........{% endif %}
Another approach is to fake validation in form_valid() and set the errors dict, and return the response. This method may be preferable since it's much more contained. Also especially if you are not using the form field anyways, you might as well add an error flag to the context.
class MyView():
def form_valid(self, form):
if Restaurant.objects.filter(related_field=self.get_place()):
ctx = self.get_context_data(form=form)
ctx['massive_error'] = True
return self.render_to_response(ctx)

How can i make a custom widget so that form.is_valid() returns true, instead of manytomany relation <select> input?

models.py:
class Tag(models.Model):
name = models.CharField(max_length=100)
description = models.CharField(max_length=500, null=True, blank=True)
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now_add=True)
class Post(models.Model):
user = models.ForeignKey(User)
tag = models.ManyToManyField(Tag)
title = models.CharField(max_length=100)
content = models.TextField()
created = models.DateTimeField(default=datetime.datetime.now)
modified = models.DateTimeField(default=datetime.datetime.now)
def __unicode__(self):
return '%s,%s' % (self.title,self.content)
class PostModelForm(forms.ModelForm):
class Meta:
model = Post
class PostModelFormNormalUser(forms.ModelForm):
class Meta:
model = Post
widgets = { 'tag' : TextInput() }
exclude = ('user', 'created', 'modified')
def __init__(self, *args, **kwargs):
super(PostModelFormNormalUser, self).__init__(*args, **kwargs)
self.fields['tag'].help_text = None
views.py:
if request.method == 'POST':
form = PostModelFormNormalUser(request.POST)
print form
print form.errors
tagstring = form.data['tag']
splitedtag = tagstring.split()
if form.is_valid():
temp = form.save(commit=False)
temp.user_id = user.id
temp.save()
l = len(splitedtag)
for i in range(l):
obj = Tag(name=splitedtag[i])
obj.save()
post.tag_set.add(obj)
post = Post.objects.get(id=temp.id)
return HttpResponseRedirect('/viewpost/' + str(post.id))
else:
form = PostModelFormNormalUser()
context = {'form':form}
return render_to_response('addpost.html', context, context_instance=RequestContext(request))
Here form.is_valid() is always false because it gets the tag as string from form. But it expects list as form.data['tag'] input. Can anyone tell me how can i fix it?
How can i write a custom widget to solve this?
I don't think you need a custom widget (you still want a TextInput), you want a custom Field. To do this, you should subclass django.forms.Field. Unfortunately the documentation is scant on this topic:
If the built-in Field classes don’t meet your needs, you can easily create custom Field classes. To do this, just create a subclass of django.forms.Field. Its only requirements are that it implement a clean() method and that its init() method accept the core arguments mentioned above (required, label, initial, widget, help_text).
I found this blog post that covers both custom widgets and fields in more depth. The author disagrees with the documentation I quoted above - it's worth reading over.
For your specific situation, you would do something like this (untested):
class MyTagField(forms.Field):
default_error_messages = {
'some_error': _(u'This is a message re: the somr_error!'),
}
def to_python(self, value):
# put code here to coerce 'value' (raw data from your TextInput)
# into the form your code will want (a list of Tag objects, perhaps)
def validate(self, value):
if <not valid for some reason>:
raise ValidationError(self.error_messages['some_error'])
Then in your ModelForm:
class PostModelFormNormalUser(forms.ModelForm):
tag = MyTagField()
class Meta:
model = Post
exclude = ('user', 'created', 'modified')
def __init__(self, *args, **kwargs):
super(PostModelFormNormalUser, self).__init__(*args, **kwargs)
self.fields['tag'].help_text = None

Categories