Wagtail ModelAdmin > How to use custom validation? - wagtail

I am using wagtail ModelAdmin for some of my non page models and want to add some custom validation.
This is some of the code.
class EditPlanningView(EditView):
def publish_url(self):
return self.url_helper.get_action_url('publish', self.pk_quoted)
def unpublish_url(self):
return self.url_helper.get_action_url('unpublish', self.pk_quoted)
def post(self, request, *args, **kwargs):
form = self.get_form()
if form.is_valid():
instance = form.save(commit=False)
if bool(request.POST.get('action-publish')):
try:
instance.publish(commit=True)
except PublishWithoutMeetingError as e:
form.add_error(
'planning_meeting',
e
)
return self.form_invalid(form)
When validation fails the invalid form is returned, but the error I added is not bound to the field. In stead a 'general error message' appears at the top.
Can someone help me out?
Cheers,
Robert

I think the error is in the following lines.
form.add_error(
'planning_meeting',
e
)
Actually can't say anything without knowing about PublishWithoutMeetingError, the type of e. Better to replace e with a string. And make sure the post method is not throwing any exceptions. Other than that, what you have done is correct. Read the following to also to check if you have missed any point.
Long Answer
There are two ways that you can achieve showing an error messages in forms.
Overriding the Form
Overriding the EditView
In both of these cases, you are going to use a method called add_error. That method takes 2 argument, field and error. From these two, error is the most important argument. The field simply state the field of the form that this error applies to. This can be None.
The error argument can be multiple types.
The error argument can be an instance of str. Then wagtail will assign the given error to the given field.
The error argument can be an instance of list of str. Then wagtail will assign the given list of errors to the given field.
The error argument can be an instance of dict with str keys and str or list of str values. In this case field should be None. The keys will be used as the fields for the errors given by values.
The error argument can be an instance of ValidationError exception. You can create a ValidationError using a str, list, or dict, which represent the above three cases.
Overriding the Form
In the form clean method need to be overridden in order to find errors.
from wagtail.admin.forms.models import WagtailAdminModelForm
class ExtraForm(WagtailAdminModelForm):
def clean(self):
cleaned_data = super().clean() # Get the already cleaned data. Same as self.cleaned_data in this case. But this way is better.
title = cleaned_data.get('title') # Get the cleaned title
if title is None: # Title is never None here, but still..
return cleaned_data
title = title.strip() # Do some formatting if needed
if title.startswith('A'): # Validation
self.add_error('title', 'Title cannot start with A') # Validation error
else:
cleaned_data['title'] = title # Use the formatted title
return cleaned_data
class MyModel(models.Model):
id = models.AutoField(primary_key=True)
title = models.CharField(max_length=500, default='', blank=False)
# Or any other fields you have
base_form_class = ExtraForm # Tell wagtail to use ExtraForm instead of the default one
Overriding the EditView
This way is same as the way that you have mentioned in the question. You need to override post method. You need to check if the form associated with the EditView is valid or invalid and return the appropriate form.
To check validity, is_valid method of the form is used by default. That method will clean the form and check if there are errors added to the form.
If form is valid, you need to return self.valid_form and self.invalid_form otherwise.
Unlike overriding the Form, you can access the request here.
class MyEditView(EditView):
def post(self, request, *args, **kwargs):
form = self.get_form() # Get the form associated with this edit view
if form.is_valid(): # Check if the form pass the default checks
my_field = request.POST.get('my_field') # You can access the request
title = form.cleaned_data.get('title') # You can access the form data
if title != my_field: # Validation
form.add_error('title', 'Title must match my_field') # Validation error
return self.form_invalid(form) # Return invalid form if there are validation errors
return self.form_valid(form) # Return the valid form if there are no validation errors
else:
return self.form_invalid(form) # Return invalid form if default check failed
class MyModelAdmin(ModelAdmin):
model = MyModel
menu_label = 'My Model'
list_display = ('id', 'title')
search_fields = (
'title',
)
edit_view_class = MyEditView # Tell wagtail to use MyEditView instead of the default one.

Related

Creating object with ManyToMany field via DRF ViewSet's perform_create

I have a simple model:
class Item(models.Model):
user = ForeignKey(User, related_name="user_items")
users = ManyToManyField(User, related_name="users_items")
I want it so that when a user creates an Item via ViewSet, that user is automatically assigned to the user and users fields.
I typically do this for ForeignKey's via the ViewSet's perform_create:
class ItemViewSet(viewsets.ModelViewSet):
...
def perform_create(self, serializer):
if self.request.user.is_authenticated:
serializer.save(user=self.request.user)
else:
serializer.save()
When I try to do it for the ManyToManyField too, I get this error:
{'users': [ErrorDetail(string='This list may not be empty.', code='empty')]}
I've tried the following in perform_create():
# 1
serializer.save(user=self.request.user, users=self.request.user)
# 2
serializer.save(user=self.request.user, users=[self.request.user])
# 2
serializer.save(user=self.request.user, users=[self.request.user.id])
How can I update a ManyToManyField via a ViewSet's perform_create?
Edit:
I guess the following works:
obj = serializer.save(user=self.request.user)
obj.users.add(self.request.user)
Is there no way to have the M2M field when the object is initially created though?
when you want set a list to m2m field one of the things you can do is this:
item_obj.users.set(user_list)
probably you need first get your item_obj.
for this you can get your object id from item_id = serializer.data.get('id') , and then item_obj = Item.objects.get(id = item_id)

How to make two fields unique of same model in django

I'm using DispensingUnit class name and having two fields 'keypad1_sr_no_hw' and 'keypad2_sr_no_hw'.
How can we compare the uniqueness of these two fields meaning two fields never having same combination ofvalues?
Used this unique_together, but its not working.
class Meta:
unique_together = (("keypad1_sr_no_hw", "keypad2_sr_no_hw",))
class DispensingUnit(models.Model):
keypad1_sr_no_hw = models.CharField(U'Keypad 1', max_length=20 ,)
keypad2_sr_no_hw = models.CharField(U'Keypad 2', max_length=20,)
value in keypad1_sr_no_hw is KP2019310001 and in keypad2_sr_no_hw KP2019310001 and still get saved.
I expect that when both the values are same it will show error and values do not get stored.
Have you tried overriding the save method, do something along the line
def save(self, *args, **kwargs):
if self.keypad1_sr_no_hw == self.keypad2_sr_no_hw:
raise ValidationError("keypad1_sr_no_hw and keypad2_sr_no_hw have same values")
return super(DispensingUnit, self).save(*args, **kwargs)
I know It might not be what you are looking for, but it will do the job for you, I am not sure that any built-in django property exists that can compare multiple fields for Uniqueness.

django How do I get the user id in the data base with a foreign key

I'd like to get the user id in the db automatically when the user fill a form, but I don't know what I have to do. I'm working on that for all day long, I tried to do it by an hidden input but I'm not it's the solution.
This is my code:
class ImputationForm(forms.Form):
user = forms.ModelChoiceField(queryset=User.objects.all(), widget=forms.HiddenInput())
morning = forms.BooleanField( required=False)
afternoon = forms.BooleanField(required=False)
workpackage = forms.ModelChoiceField(
queryset=Workpackage.objects.all()
)
class ImputationFormModel(ModelForm):
class Meta:
model = Imputation
def imputation(request):
if request.method == 'POST': # If the form has been submitted...
form = ImputationForm(request.POST) # A form bound to the POST data
f = ImputationFormModel(request.POST)
if form.is_valid(): # All validation rules pass
user = request.user.id
morning = form.cleaned_data['morning']
afternoon = form.cleaned_data['afternoon']
workpackage = form.cleaned_data['workpackage']
f.save()
return render_to_response( 'no/form.html', {'form' : form, }, context_instance=RequestContext(request))
else:
form = ImputationForm()
return render_to_response( 'no/form.html', {'form' : form, }, context_instance=RequestContext(request))
Great question. Let's first start by identifying the purpose of various lines of code of your application and see if we can make sense of it. I see that you've got two forms right now which seems to reference the same kinds of data, which is a bit redundant. We'll want to avoid redundancy where possible, so let's start by perhaps cleaning up the code.
We can tell by the code you've shared that you're trying to:
Allow a user to submit a form with data regarding "morning", "afternoon", and "workpackage".
We're not checking whether or not you're editing an instance, so we can assume that upon successful form submission, a new Imputation record is created in the database with the data passed into it.
You're asking for a hidden input of a user value on the client side, which binds the submitted user ID to the form for adding a relationship with the user to the newly saved Imputation record.
Generally, you never want to allow hidden variables regarding sensitive or personal data that can be associated with a user on the client side. It's considered a huge security risk. A user may edit the hidden field using Firebug (or other DOM editing tools) and bind the user ID to a different user than which is intended.
With that being said, let's have a gander at the code now:
class ImputationForm(ModelForm):
class Meta:
model = Imputation
fields = [
'morning',
'afternoon',
'workpackage'
]
# assumed fields of morning, afternoon, workpackage
def imputation(request):
if request.method == 'POST': # If the form has been submitted...
form = ImputationForm(request.POST) # A form bound to the POST data
if form.is_valid(): # All validation rules pass
form.user = request.user
form.save()
return render_to_response( 'no/form.html', {'form' : form, }, context_instance=RequestContext(request))
else:
form = ImputationForm()
return render_to_response( 'no/form.html', {'form' : form, }, context_instance=RequestContext(request))
With this code, we're no longer asking the user what his ID is on the form, and we're no longer even asking for it in the form altogether. We're allowing Django to determine what the current user ID is and pass it into the model right before saving the form. You'll want to make sure that the user field is set to null=True, blank=True on the model to avoid an exception from being raised.
I hope this helps answer your question.
I found it I'm really a newbie in django sorry for disturbing you and thank you very much for your help I would have not change my way of thinking on this issue without you!!!!! here is what I changed:
def imputation(request):
if request.method == 'POST': # If the form has been submitted...
form = ImputationForm(request.POST) # A form bound to the POST data
if form.is_valid(): # All validation rules pass
#print("%s" % form.user)
print("%s" % form)
new = form.save(commit=False) #here
new.user = request.user #here
new.save() #here
Thanks again ;)

Unique in ModelForm, how to get back to the form with error message?

I have found some posts about more or less the same situation like this one or this other one, but I was not able to adapt any of these to my situation.
What I would like is to return to my form with a warning if the user try to create another model with the same name of one already stored by himself in the db.
I would like to use Django built in facilities as described here, but I need some advices to change my code: could you help me, please?
My code follows:
models.py
class ShapeFile(models.Model):
name = models.CharField(max_length=100)
srid = models.ForeignKey(SpatialRefSys)
model_definition = models.OneToOneField(ModelDefinition)
user = models.ForeignKey(User)
color_table = models.ManyToManyField(ColorTable)
file = models.FileField(upload_to=get_upload_path)
class Meta:
unique_together = ('name', 'user')
forms.py
class UploadForm(ModelForm):
class Meta:
model = ShapeFile
fields = ('name','srid','file','color_table')
widgets = {'srid': TextInput()}
views.py
#login_required
def index(request):
if request.method == 'POST':
form = UploadForm(request.POST, request.FILES)
if form.is_valid():
req = request.POST
sridVal = form.cleaned_data['srid'].pk
shpVal = form.cleaned_data['name']
# The final table name is something like 'mutant_celeryPy2_123_salzburg_lc'
end_table_name = request.user.username + "_" + shpVal + '_lc'
# Creates a table, otherwise return the retrieved one
model_def, created = ModelDefinition.objects.get_or_create(
app_label='celeryPy2',
object_name=end_table_name,
defaults=dict(
bases=(BaseDefinition(base=GeoModel),),
fields=(GeometryFieldDefinition(name='the_geom', srid=sridVal),
SmallIntegerFieldDefinition(name='cat'),)
)
)
obj = form.save(commit=False)
obj.user = request.user
obj.model_definition = model_def
obj.save()
messages.success(request, 'Shapefile upload succesful!')
return HttpResponse('Stored!')
else:
print "Upload shapefile form is invalid!!!"
else:
form = UploadForm()
return render_to_response('celeryPy2/index.html',
{'form': form,},
context_instance=RequestContext(request))
If I login as user1 and fill the form, let's suppose with name 'myshape', when I submit it I get the "Stored!" message: everything fine, model user1_myshape_lc is created.
If I re-login with the same user1 and try to store other data with field name set at 'myshape' as before I correctly get an exception:
Exception Type: IntegrityError at /celeryPy2/main
Exception Value: duplicate key value violates unique constraint "celeryPy2_shapefile_model_definition_id_key"
DETAIL: Key (model_definition_id)=(154) already exists.
How to get back my form with a warning instead to get the Django exception error message?
Thank you.
Model forms validation should take care of this and raise the validation error. But you have excluded the user field from form, which is part of unique_together constraint, hence its not getting verified. Ref validate_unique
You can try changing the view code to add user field in posted data and then instantiate the form.
Or better way would be add hidden user field in the form, so that its available in request.POST and the validation works as required.
Thanks Rohan, I found the hidden field solution the best: now I correctly get back my form with the error message: Shape file with this Name and User already exists.
Here's my modified code to get things work:
forms.py
class UploadForm(ModelForm):
class Meta:
model = ShapeFile
fields = ('name','user','srid','file','color_table')
widgets = {'srid': TextInput(), 'user': HiddenInput()}
views.py
#login_required
def index(request):
if request.method == 'POST':
form = UploadForm(request.POST, request.FILES)
if form.is_valid():
req = request.POST
sridVal = form.cleaned_data['srid'].pk
shpVal = form.cleaned_data['name']
# The final table name is something like 'mutant_celeryPy2_123_salzburg_lc'
end_table_name = request.user.username + "_" + shpVal + '_lc'
# Creates a table, otherwise return the retrieved one
model_def, created = ModelDefinition.objects.get_or_create(
app_label='celeryPy2',
object_name=end_table_name,
defaults=dict(
bases=(BaseDefinition(base=GeoModel),),
fields=(GeometryFieldDefinition(name='the_geom', srid=sridVal),
SmallIntegerFieldDefinition(name='cat'),)
)
)
obj = form.save(commit=False)
obj.model_definition = model_def
obj.save()
messages.success(request, 'Shapefile upload succesful!')
return HttpResponse('Stored!')
else:
print "Upload shapefile form is invalid!!!"
else:
form = UploadForm(initial={'user': request.user})
return render_to_response('celeryPy2/index.html',
{'form': form,},
context_instance=RequestContext(request))

Use a db.StringProperty() as unique identifier in Google App Engine

I just have a hunch about this. But if feels like I'm doing it the wrong way. What I want to do is to have a db.StringProperty() as a unique identifier. I have a simple db.Model, with property name and file. If I add another entry with the same "name" as one already in the db.Model I want to update this.
As of know I look it up with:
template = Templates.all().filter('name = ', name)
Check if it's one entry already:
if template.count() > 0:
Then add it or update it. But from what I've read .count() is every expensive in CPU usage.
Is there away to set the "name" property to be unique and the datastore will automatic update it or another better way to do this?
..fredrik
You can't make a property unique in the App Engine datastore. What you can do instead is to specify a key name for your model, which is guaranteed to be unique - see the docs for details.
I was having the same problem and came up with the following answer as the simplest one :
class Car(db.Model):
name = db.StringProperty(required=True)
def __init__(self,*args, **kwargs):
super(Car, self).__init__(*args, **kwargs)
loadingAnExistingCar = ("key" in kwargs.keys() or "key_name" in kwargs.keys())
if not loadingAnExistingCar:
self.__makeSureTheCarsNameIsUnique(kwargs['name'])
def __makeSureTheCarsNameIsUnique(self, name):
existingCarWithTheSameName = Car.GetByName(name)
if existingCarWithTheSameName:
raise UniqueConstraintValidationException("Car should be unique by name")
#staticmethod
def GetByName(name):
return Car.all().filter("name", name).get()
It's important to not that I first check if we are loading an existing entity first.
For the complete solution : http://nicholaslemay.blogspot.com/2010/07/app-engine-unique-constraint.html
You can just try to get your entity and edit it, and if not found create a new one:
template = Templates.gql('WHERE name = :1', name)
if template is None:
template = Templates()
# do your thing to set the entity's properties
template.put()
That way it will insert a new entry when it wasn't found, and if it was found it will update the existing entry with the changes you made (see documentation here).
An alternative solution is to create a model to store the unique values, and store it transationally using a combination of Model.property_name.value as key. Only if that value is created you save your actual model. This solution is described (with code) here:
http://squeeville.com/2009/01/30/add-a-unique-constraint-to-google-app-engine/
I agree with Nick. But, if you do ever want to check for model/entity existence based on a property, the get() method is handy:
template = Templates.all().filter('name = ', name).get()
if template is None:
# doesn't exist
else:
# exists
I wrote some code to do this. The idea for it is to be pretty easy to use. So you can do this:
if register_property_value('User', 'username', 'sexy_bbw_vixen'):
return 'Successfully registered sexy_bbw_vixen as your username!'
else:
return 'The username sexy_bbw_vixen is already in use.'
This is the code. There are a lot of comments, but its actually only a few lines:
# This entity type is a registry. It doesn't hold any data, but
# each entity is keyed to an Entity_type-Property_name-Property-value
# this allows for a transaction to 'register' a property value. It returns
# 'False' if the property value is already in use, and thus cannot be used
# again. Or 'True' if the property value was not in use and was successfully
# 'registered'
class M_Property_Value_Register(db.Expando):
pass
# This is the transaction. It returns 'False' if the value is already
# in use, or 'True' if the property value was successfully registered.
def _register_property_value_txn(in_key_name):
entity = M_Property_Value_Register.get_by_key_name(in_key_name)
if entity is not None:
return False
entity = M_Property_Value_Register(key_name=in_key_name)
entity.put()
return True
# This is the function that is called by your code, it constructs a key value
# from your Model-Property-Property-value trio and then runs a transaction
# that attempts to register the new property value. It returns 'True' if the
# value was successfully registered. Or 'False' if the value was already in use.
def register_property_value(model_name, property_name, property_value):
key_name = model_name + '_' + property_name + '_' + property_value
return db.run_in_transaction(_register_property_value_txn, key_name )

Resources