unique_together Validator for a model with an (editable=False) field

95 views
Skip to first unread message

Mark Soper

unread,
Apr 6, 2007, 12:12:01 AM4/6/07
to Django users
I'm trying to figure out the best way to validate a unique_together
constraint. The relevant parts of the model are here:

------------------------------------------
class Thesis(models.Model):
thesis_name = CharField('Thesis Name', maxlength=50,
core=True, validator_list=[???????])
thesis_owner = models.ForeignKey(User, editable=False)

def save(self):
if not self.id:
self.thesis_owner = threadlocals.get_current_user() ***
super(Thesis,self).save()

class Meta:
unique_together = (("thesis_name", "thesis_owner"),)

*** - using the threadlocals middleware from Luke Plant
http://lukeplant.me.uk/blog.php?id=1107301634
-----------------------------------------

I'm using the standard django admin interfaces. The thesis_owner
field is populated automatically with the user info, but not until
after user inputs have been validated. So the standard
unique_together validation doesn't catch duplicate entries. They
aren't caught until the database issues an IntegrityError, generating
a fatal error to the user. Instead I'd like to raise the standard red
error banner with a message like "A thesis already exists with name
XXXXX. Please choose a different name".

Possible solutions:

1. Add a custom field validator to validator_list -
Field validator will only check at the individual field level where
information about the model instance (i.e. User info) isn't in
context. Also, there doesn't seem to be a good way to find out
whether it is an "add" or a "change" operation, which need to be
handled differently. Any way to get this out-of-context information?

2. Custom uniqueness check in the model's save method -
This approach allows for access to user info and checking for "add" or
"change" (see above). However, the save happens after the view's
error checking has completed, so raising a ValidationError at this
stage results in a fatal browser error rather than the red error
banner. Is it possible to cause save to raise the red banner instead?

3. Supplement the user info into the POSTed data before it is passed
to the validation -
Theoretically the standard manipulator_validator_unique_together
function will catch the error. Can this be done through a custom
manager or manipulator or in some other way that doesn't require
changes to the admin views, models, etc.?

Thanks! Any insight is appreciated.

Mark

Malcolm Tredinnick

unread,
Apr 6, 2007, 12:22:24 AM4/6/07
to django...@googlegroups.com

I can't offer much encouragement here... the drawbacks to all the
solutions you list are very accurate. You've understood the problem. :-(

This is the motivation behind getting model-aware validation in place in
Django. We need it for exactly these types of cases.

>
> Possible solutions:
>
> 1. Add a custom field validator to validator_list -
> Field validator will only check at the individual field level where
> information about the model instance (i.e. User info) isn't in
> context. Also, there doesn't seem to be a good way to find out
> whether it is an "add" or a "change" operation, which need to be
> handled differently. Any way to get this out-of-context information?

If you field submission includes enough information to work out the
model, you can use the DB API to select the model. However, often that
information is in the URL and it kind of assumes all the other data is
valid. It's very hacky -- I'm using it in one application where there
was no other choice, but I hate it as a solution.

>
> 2. Custom uniqueness check in the model's save method -
> This approach allows for access to user info and checking for "add" or
> "change" (see above). However, the save happens after the view's
> error checking has completed, so raising a ValidationError at this
> stage results in a fatal browser error rather than the red error
> banner. Is it possible to cause save to raise the red banner instead?

Saving should never raise validation errors for exactly the reason you
mention. About the only thing saving might raise is database connection
errors (we can't help it if somebody pulls out the network cable) or
possibly IntegrityError: another thread or process beat you to the punch
and we can't control that either.

> 3. Supplement the user info into the POSTed data before it is passed
> to the validation -
> Theoretically the standard manipulator_validator_unique_together
> function will catch the error. Can this be done through a custom
> manager or manipulator or in some other way that doesn't require
> changes to the admin views, models, etc.?

Might be possible. Never tried it.

Regards,
Malcolm


Mark Soper

unread,
Apr 6, 2007, 12:54:49 PM4/6/07
to Django users

Thanks, Malcolm! It's encouraging to hear that others are also
working on this. A follow-up question:

I'm inclined to try approach #1, by adding custom subclasses of Add-
and ChangeManipulator to the model (in this case called Thesis). If I
can figure out way to get the dispatch mechanism to associate these
new custom manipulators with the model class (it doesn't happen by
simply including these custom classes in the model definition), then I
should have (the stock ChangeValidator already get it in __init___) or
be able to pass (AddValidator would need such an __init__) access to
model instance info in the manipulator context, allowing me to write
model-aware validators here without changing any django code.

I haven't tried this yet. I'll keep you posted on how it goes. If
you have any insight on pros and cons of this approach, please let
know.

Mark


> 1. Add a custom field validator to validator_list -
> Field validator will only check at the individual field level where
> information about the model instance (i.e. User info) isn't in
> context. Also, there doesn't seem to be a good way to find out
> whether it is an "add" or a "change" operation, which need to be
> handled differently. Any way to get this out-of-context information?

If you field submission includes enough information to work out the
model, you can use the DB API to select the model. However, often that
information is in the URL and it kind of assumes all the other data is
valid. It's very hacky -- I'm using it in one application where there
was no other choice, but I hate it as a solution.

On Apr 6, 12:22 am, Malcolm Tredinnick <malc...@pointy-stick.com>
wrote:


> On Thu, 2007-04-05 at 21:12 -0700, Mark Soper wrote:
> > I'm trying to figure out the best way to validate aunique_together
> > constraint. The relevant parts of the model are here:
>
> > ------------------------------------------
> > class Thesis(models.Model):
> > thesis_name = CharField('Thesis Name', maxlength=50,
> > core=True, validator_list=[???????])
> > thesis_owner = models.ForeignKey(User, editable=False)
>
> > def save(self):
> > if not self.id:
> > self.thesis_owner = threadlocals.get_current_user() ***
> > super(Thesis,self).save()
>
> > class Meta:

> > unique_together= (("thesis_name", "thesis_owner"),)


>
> > *** - using the threadlocals middleware from Luke Plant
> >http://lukeplant.me.uk/blog.php?id=1107301634
> > -----------------------------------------
>
> > I'm using the standard django admin interfaces. The thesis_owner
> > field is populated automatically with the user info, but not until
> > after user inputs have been validated. So the standard

> >unique_togethervalidation doesn't catch duplicate entries. They

Mark Soper

unread,
Apr 10, 2007, 12:54:56 AM4/10/07
to Django users
I created custom AddManipulator and ChangeManipulator classes in my
model to do model-level validation. This did require a minor change
to django/db/models/manipulators.py. The details of this change and
the definition of the model are appended here. I have not done
extensive testing yet, but this seems to work.

Mark
-----------------------------------------------------------------------------
class MymodelManager(models.Manager):
def get_query_set(self):
current_user=threadlocals.get_current_user()
if current_user.is_superuser:
return super(MymodelManager, self).get_query_set()
else:
return super(MymodelManager,
self).get_query_set().filter(mymodel_owner=threadlocals.get_current_user())

class Mymodel(models.Model):
mymodel_name = models.CharField('Mymodel Name', maxlength=50,
core=True)
mymodel_owner = models.ForeignKey(User, editable=False)

objects = MymodelManager()

def __str__(self):
return self.mymodel_name

def save(self):
if self.id:
pass
else:
self.mymodel_owner = threadlocals.get_current_user()
super(Mymodel,self).save()

def get_absolute_url(self):
return "/home/mymodelapp/mymodel/%s/" % self.id

class Meta:
unique_together = (("mymodel_name", "mymodel_owner"),)

class Admin:
fields = (
(None, {
'fields': (('mymodel_name',),)}
),
)
list_display = ('mymodel_name',)
search_fields = ['mymodel_name']
manager = MymodelManager()

class AddManipulator(manipulators.AutomaticAddManipulator):

def contribute_to_class(cls, other_cls, name):
setattr(other_cls, name,
manipulators.ManipulatorDescriptor(name, cls))
contribute_to_class = classmethod(contribute_to_class)

def get_validation_errors(self, new_data):
"Returns dictionary mapping field_names to error-
message lists"
errors = {}
self.prepare(new_data)
for field in self.fields:

errors.update(field.get_validation_errors(new_data))
val_name = 'validate_%s' % field.field_name
if hasattr(self, val_name):
val = getattr(self, val_name)
try:
field.run_validator(new_data, val)
except (validators.ValidationError,
validators.CriticalValidationError), e:
errors.setdefault(field.field_name,
[]).extend(e.messages)
for any_mymodel in Mymodel.objects.all():
if (any_mymodel.mymodel_name ==
new_data["mymodel_name"]):
errors.setdefault('mymodel_name',
[]).append(_('A mymodel named %(name)s already exists' %
{'name':new_data["mymodel_name"]}))
return errors

class
ChangeManipulator(manipulators.AutomaticChangeManipulator):

def contribute_to_class(cls, other_cls, name):
setattr(other_cls, name,
manipulators.ManipulatorDescriptor(name, cls))
contribute_to_class = classmethod(contribute_to_class)

def get_validation_errors(self, new_data):
"Returns dictionary mapping field_names to error-
message lists"
errors = {}
self.prepare(new_data)
for field in self.fields:

errors.update(field.get_validation_errors(new_data))
val_name = 'validate_%s' % field.field_name
if hasattr(self, val_name):
val = getattr(self, val_name)
try:
field.run_validator(new_data, val)
except (validators.ValidationError,
validators.CriticalValidationError), e:
errors.setdefault(field.field_name,
[]).extend(e.messages)
# Above this line is code taken directly from django/
db/models/manipulators.py: per-field validation
# Below this line is custom code to do per-model
validation
for any_mymodel in
Mymodel.objects.exclude(id=self.original_object.id):
if (any_mymodel.mymodel_name ==
new_data["mymodel_name"]):
errors.setdefault('mymodel_name',
[]).append(_('A mymodel named %(name)s already exists' %
{'name':new_data["mymodel_name"]}))
return errors

"""
The custom Add and Change Manipulators require that the
add_manipulator function in django/db/models/manipulators.py be
changed to:

def add_manipulators(sender):
if (sender.__name__ in ["Mymodel",]):
return
cls = sender
cls.add_to_class('AddManipulator', AutomaticAddManipulator)
cls.add_to_class('ChangeManipulator', AutomaticChangeManipulator)
"""
-----------------------------------------------------------------------------

Reply all
Reply to author
Forward
0 new messages