Opened 20 years ago
Closed 14 years ago
#1751 closed defect (fixed)
unique_together does not work when any of the listed fields contains a FK
| Reported by: | Olive | Owned by: | Gary Wilson |
|---|---|---|---|
| Component: | contrib.admin | Version: | 1.3 |
| Severity: | normal | Keywords: | fk unique_together |
| Cc: | nslater@…, gary.wilson@…, cathy@…, larlet@… | Triage Stage: | Accepted |
| Has patch: | no | Needs documentation: | no |
| Needs tests: | no | Patch needs improvement: | no |
| Easy pickings: | no | UI/UX: | no |
Description
An IntegrityError is raised when saving the following kind of object:
class DocTitle(models.Model):
sequence = models.IntegerField(_('sequence'))
title_en = models.CharField(_('title'),maxlength=200)
def __repr__(self):
return self.title_en
class Meta:
verbose_name = _('documentation title')
verbose_name_plural = _('documentation titles')
ordering = ['sequence']
class Admin:
list_display = ('title_en',)
class DocChunk(models.Model):
application = models.ForeignKey(Application,edit_inline=True,num_in_admin=1)
title = models.ForeignKey(DocTitle)
text = models.TextField(_('text'),core=True)
def __repr__(self):
return self.title.title_en
class Meta:
verbose_name = _('Document chunk')
verbose_name_plural = _('Document chunks')
ordering = ['title']
unique_together = (("application","title"),)
Change History (33)
comment:1 by , 20 years ago
comment:2 by , 19 years ago
| Severity: | major → blocker |
|---|
In fact the problem occures if there is at least one ForeignKey field specified in unique_together list (no matter if it is an edit_inline or not)
comment:3 by , 19 years ago
| priority: | high → normal |
|---|---|
| Severity: | blocker → normal |
comment:5 by , 19 years ago
| Summary: | unique_together does not work when one of the fields has edit_inline set to True → unique_together does not work when any of the fields contains a FK or has edit_inline set to True |
|---|
comment:6 by , 19 years ago
| Summary: | unique_together does not work when any of the fields contains a FK or has edit_inline set to True → unique_together does not work when any of the listed fields contains a FK |
|---|
comment:7 by , 19 years ago
| Cc: | added |
|---|
I am using unique_together with ForeignKey fields in a model that looks like:
class Course(models.Model): """A course number and title combination.""" department = models.ForeignKey(Department) number = models.CharField('course number', maxlength=6) title = models.CharField('course title', maxlength=100) class Meta: # Don't allow duplicate courses. unique_together = (("department", "number", "title"),)
I have no problems saving Department or Course objects. I also have no problems when I add edit_inline=True in the department field as long as at least one other field has core=True. It's only if I fail to add a core=True to any field that I get the following IntegrityError:
Traceback (most recent call last): File "/usr/lib/python2.4/site-packages/django/core/handlers/base.py" in get_response 74. response = callback(request, *callback_args, **callback_kwargs) File "/usr/lib/python2.4/site-packages/django/contrib/admin/views/decorators.py" in _checklogin 55. return view_func(request, *args, **kwargs) File "/usr/lib/python2.4/site-packages/django/views/decorators/cache.py" in _wrapped_view_func 39. response = view_func(request, *args, **kwargs) File "/usr/lib/python2.4/site-packages/django/contrib/admin/views/main.py" in add_stage 254. new_object = manipulator.save(new_data) File "/usr/lib/python2.4/site-packages/django/db/models/manipulators.py" in save 198. new_rel_obj.save() File "/usr/lib/python2.4/site-packages/django/db/models/base.py" in save 203. ','.join(placeholders)), db_values) File "/usr/lib/python2.4/site-packages/django/db/backends/util.py" in execute 12. return self.cursor.execute(sql, params) IntegrityError at /admin/courses/department/add/ ERROR: duplicate key violates unique constraint "courses_course_department_id_key" INSERT INTO "courses_course" ("department_id","number","title") VALUES (6,'','')
I also have a model that uses unique_together using two fields that are both ForeignKeys and I don't experience any problem there with or without edit_inline=True.
follow-up: 18 comment:8 by , 19 years ago
Notice that "This validation needs to occur after html2python to be effective." but validator always run before html2python. so I change ForeignKey to use field.name instead of field.attname.
Index: db/models/manipulators.py
===================================================================
--- db/models/manipulators.py (revision 4643)
+++ db/models/manipulators.py (working copy)
@@ -283,7 +283,7 @@
# This is really not going to work for fields that have different
# form fields, e.g. DateTime.
# This validation needs to occur after html2python to be effective.
- field_val = all_data.get(f.attname, None)
+ field_val = all_data.get(f.name, None)
if field_val is None:
# This will be caught by another validator, assuming the field
# doesn't have blank=True.
comment:9 by , 19 years ago
New version: unique together fields could be null=True now.
Index: db/models/manipulators.py
===================================================================
--- db/models/manipulators.py (revision 4643)
+++ db/models/manipulators.py (working copy)
@@ -283,13 +283,13 @@
# This is really not going to work for fields that have different
# form fields, e.g. DateTime.
# This validation needs to occur after html2python to be effective.
- field_val = all_data.get(f.attname, None)
- if field_val is None:
+ field_val = f.get_manipulator_new_data(all_data)
+ if not f.null and field_val is None:
# This will be caught by another validator, assuming the field
# doesn't have blank=True.
return
if isinstance(f.rel, ManyToOneRel):
- kwargs['%s__pk' % f.name] = field_val
+ kwargs[f.get_validator_unique_lookup_type()] = field_val
else:
kwargs['%s__iexact' % f.name] = field_val
try:
comment:10 by , 19 years ago
| Cc: | added |
|---|
comment:12 by , 19 years ago
Maybe the problem is backend specific? For anyone seeing this problem, which database backend are you using?
My comments from above were on postgresql.
comment:15 by , 19 years ago
I had this same problem until I switched the order of my fields in the unique_together clause. In my example, using 'slug' first threw an integrity error, but having 'site' first validates correctly. (I'm working in 91-bugfixes with PostgreSQL)
slug = meta.SlugField(prepopulate_from=('name',))
site = meta.ForeignKey(Site)
class META:
unique_together=(('site', 'slug'),)
comment:16 by , 19 years ago
| Keywords: | fk unique_together added |
|---|---|
| Triage Stage: | Unreviewed → Accepted |
comment:18 by , 18 years ago
Replying to favo@exoweb.net:
I change ForeignKey to use field.name instead of field.attname.
This change was made in [4028].
comment:19 by , 18 years ago
i'm not sure what's the status of this ticket, but maybe a clean reproducible test-case will help.
this is with from-svn-django (r6022), postgresql8, "postgresql" driver.
from django.db.models import *
class A(Model):
name = CharField(maxlength=200)
class Admin: pass
class B(Model):
name = CharField(maxlength=200)
class Admin: pass
class C(Model):
name = CharField(maxlength=200)
a = ForeignKey(A)
b = ForeignKey(B)
class Meta:
unique_together = (("a","b"),)
class Admin: pass
in the admin:
- create an instance of A
- go to create an instance of C, and fill out the "name" field, and select an "a" object, but do not select a "b" object
- save the instance
you will get an exception saying:
ERROR: invalid input syntax for integer:
"" SELECT "main_c"."id","main_c"."name","main_c"."a_id","main_c"."b_id"
FROM "main_c" INNER JOIN "main_a" AS "main_c__a" ON "main_c"."a_id" = "main_c__a"."id"
WHERE ("main_c"."b_id" = '' AND "main_c__a"."id" ILIKE '1')
(isn't that ILIKE '1' quite nice for a numeric-key? :))
btw. this works fine in sqlite3, probably because it simply accepts this syntax...
comment:20 by , 18 years ago
the stacktrace:
Traceback (most recent call last):
File "/home/gabor/src/django/django/core/handlers/base.py" in get_response
77. response = callback(request, *callback_args, **callback_kwargs)
File "/home/gabor/src/django/django/contrib/admin/views/decorators.py" in _checklogin
55. return view_func(request, *args, **kwargs)
File "/home/gabor/src/django/django/views/decorators/cache.py" in _wrapped_view_func
39. response = view_func(request, *args, **kwargs)
File "/home/gabor/src/django/django/contrib/admin/views/main.py" in add_stage
257. errors = manipulator.get_validation_errors(new_data)
File "/home/gabor/src/django/django/oldforms/__init__.py" in get_validation_errors
61. errors.update(field.get_validation_errors(new_data))
File "/home/gabor/src/django/django/oldforms/__init__.py" in get_validation_errors
378. self.run_validator(new_data, validator)
File "/home/gabor/src/django/django/oldforms/__init__.py" in run_validator
368. validator(new_data.get(self.field_name, ''), new_data)
File "/home/gabor/src/django/django/utils/functional.py" in _curried
3. return _curried_func(*(args+moreargs), **dict(kwargs, **morekwargs))
File "/home/gabor/src/django/django/db/models/manipulators.py" in manipulator_validator_unique_together
303. old_obj = self.manager.get(**kwargs)
File "/home/gabor/src/django/django/db/models/manager.py" in get
69. return self.get_query_set().get(*args, **kwargs)
File "/home/gabor/src/django/django/db/models/query.py" in get
261. obj_list = list(clone)
File "/home/gabor/src/django/django/db/models/query.py" in __iter__
114. return iter(self._get_data())
File "/home/gabor/src/django/django/db/models/query.py" in _get_data
482. self._result_cache = list(self.iterator())
File "/home/gabor/src/django/django/db/models/query.py" in iterator
189. cursor.execute("SELECT " + (self._distinct and "DISTINCT " or "") + ",".join(select) + sql, params)
File "/home/gabor/src/django/django/db/backends/util.py" in execute
19. return self.cursor.execute(sql, params)
File "/home/gabor/src/django/django/db/backends/postgresql/base.py" in execute
47. return self.cursor.execute(smart_str(sql, self.charset), self.format_params(params))
ProgrammingError at /admin/main/c/add/
ERROR: invalid input syntax for integer: "" SELECT "main_c"."id","main_c"."name","main_c"."a_id","main_c"."b_id" FROM "main_c" INNER JOIN "main_a" AS "main_c__a" ON "main_c"."a_id" = "main_c__a"."id" WHERE ("main_c"."b_id" = '' AND "main_c__a"."id" ILIKE '2')
comment:21 by , 18 years ago
| Cc: | added |
|---|
comment:22 by , 18 years ago
I found that the above patches weren't effective for 0.96, and also did not handle BooleanField correctly with sqlite3, as the value is stored as True/False as opposed to 'on' -- so the generated <name>__iexact = 'on' would not match results correctly. I wrote the following patch for 0.96 that seems to resolve this issue:
Index: manipulators.py
===================================================================
--- manipulators.py
+++ manipulators.py
@@ -278,6 +278,7 @@
def manipulator_validator_unique_together(field_name_list, opts, self, field_data, all_data):
from django.db.models.fields.related import ManyToOneRel
+ from django.db.models.fields import BooleanField
from django.utils.text import get_text_list
field_list = [opts.get_field(field_name) for field_name in field_name_list]
if isinstance(field_list[0].rel, ManyToOneRel):
@@ -294,7 +295,15 @@
# doesn't have blank=True.
return
if isinstance(f.rel, ManyToOneRel):
- kwargs['%s__pk' % f.name] = field_val
+ if field_val is '':
+ kwargs['%s__isnull' % f.name] = True
+ else:
+ kwargs['%s__pk' % f.name] = field_val
+ elif isinstance(f, BooleanField):
+ if field_val in ('on', 'True', 'true', True):
+ kwargs['%s' % f.name] = True
+ else:
+ kwargs['%s' % f.name] = False
else:
kwargs['%s__iexact' % f.name] = field_val
try:
comment:23 by , 17 years ago
| milestone: | → 1.0 |
|---|
comment:24 by , 17 years ago
| Owner: | changed from to |
|---|---|
| Status: | new → assigned |
comment:25 by , 17 years ago
I've tried the above scenarios and the only way I am currently able to produce an IntegrityError with unique_together FKs is by adding an actual duplicate -- but this is the issue in #8209.
I'm going to look into #8209 and then see if this is resolved..
comment:26 by , 17 years ago
| Owner: | changed from to |
|---|---|
| Status: | assigned → new |
I cannot reproduce this one so I'll release it. Perhaps someone else can resolve?
comment:27 by , 17 years ago
| Owner: | changed from to |
|---|---|
| Status: | new → assigned |
comment:28 by , 17 years ago
I suspect that this is no longer a problem, especially since manipulators are no longer used and about to be removed. Can anyone verify that this still is a problem?
comment:29 by , 17 years ago
| Resolution: | → fixed |
|---|---|
| Status: | assigned → closed |
Agreed. The impending removal of manipulators (part of #7742) would seem to make this fixed through attrition.
comment:31 by , 14 years ago
| Easy pickings: | unset |
|---|---|
| Resolution: | fixed |
| Status: | closed → reopened |
| Triage Stage: | Accepted → Unreviewed |
| UI/UX: | unset |
| Version: | → 1.3 |
comment:32 by , 14 years ago
I am having the problem described in this ticket with following model
class Vehicle(models.Model):
class Meta:
verbose_name = _('Vehicle')
verbose_name_plural = _('Vehicles')
unique_together = (('vehicle_identifier', 'company'),)
objects = VehicleManager()
name = models.CharField(max_length=32, unique=True,
verbose_name=_('Vehicle name'), default="")
description = models.TextField(max_length=300,
verbose_name=_('Description'),
default="")
model = models.ForeignKey(CarModel, verbose_name=_('Car model'),
default="", null=True, blank=True)
car_plate = models.CharField(max_length=7, unique=True,
verbose_name=_('Car plate'), default="")
vehicle_identifier = models.CharField(max_length=32,
verbose_name=_('Vehicle Identifier'),
default="")
# purchase attributes
fecha_compra = models.DateTimeField(null=True, blank=True)
fecha_baja = models.DateTimeField(null=True, blank=True)
valor_compra = models.DecimalField(max_digits=9, decimal_places=2,
null=True, blank=True)
#Extras
has_automatic_transmission = models.BooleanField(verbose_name=_('Automatic transmission'))
has_mp3 = models.BooleanField(verbose_name=_('MP3'))
has_climate = models.BooleanField(verbose_name=_('Climate'))
pets_allowed = models.BooleanField(verbose_name=_('Pets allowed'))
has_gps = models.BooleanField(verbose_name=_('GPS'))
luggage_capacity_gt_200 = models.BooleanField(verbose_name=_('Luggage capacity greater than 200L'))
image = models.ImageField(verbose_name=_('Image'),
upload_to=get_image_path,
null=True, blank=True,)
# This fields will be updated with data received in trip data convadis messages
last_location_lat = models.DecimalField(verbose_name=_('Latitude'),
max_digits=17, decimal_places=15,
null=True, blank=True,
) # 2.15
last_location_long = models.DecimalField(verbose_name=_('Longitude'),
max_digits=17, decimal_places=15,
null=True, blank=True,
) # 2.15
# Relationships
station = models.ForeignKey(Station, verbose_name=_('Station'),
null=True, blank=True)
company = models.ForeignKey(Company, verbose_name=_('Company'),
null=True, blank=True)
# Automatic saved fields
created_by = models.ForeignKey(User, related_name='vehicle_created_by')
created_on = models.DateTimeField(auto_now_add=True)
edited_by = models.ForeignKey(User, related_name='vehicle_edited_by')
edited_on = models.DateTimeField(auto_now=True)
# TODO replace this with a better idea, this is temporally only for demo
def image_url(self):
import settings
return settings.MEDIA_URL + str(self.image)
def save(self, *args, **kwargs):
if self.last_location_lat == 0 and self.station:
self.last_location_lat = self.station.location_lat
if self.last_location_long == 0 and self.station:
self.last_location_long = self.station.location_long
super(Vehicle, self).save(*args, **kwargs)
def __unicode__(self):
return (self.car_plate + ' - ' +
unicode(self.name) + ' (' +
unicode(self.model)) + ')'
comment:33 by , 14 years ago
| Resolution: | → fixed |
|---|---|
| Status: | reopened → closed |
| Triage Stage: | Unreviewed → Accepted |
The problem this ticket identified definitely no longer exists in the Django codebase. Also, unique_together certainly does work with FK fields, I use it frequently. So whatever problem you are having with your model, it is something else. Please use user-support channels (#django on Freenode, the django-users mailing list), and only file a ticket if you are sure that the problem is in Django itself, and you can reduce it to a simplest-case reproduction.
I mean, when saving an "application" object, of course