#35488: Unexpected traceback: A JSONField being in a UniqueConstraint is
handled/documented poorly
-------------------------------------+-------------------------------------
               Reporter:  Hanne Moa  |          Owner:  nobody
                   Type:             |         Status:  new
  Uncategorized                      |
              Component:             |        Version:  5.0
  Uncategorized                      |       Keywords:  unique,
               Severity:  Normal     |  UniqueConstraint, JSONField, json
           Triage Stage:             |      Has patch:  0
  Unreviewed                         |
    Needs documentation:  0          |    Needs tests:  0
Patch needs improvement:  0          |  Easy pickings:  0
                  UI/UX:  0          |
-------------------------------------+-------------------------------------
 Given a User based on AbstractUser and a model:

 {{{
 class DestinationConfig(model.Model):
     class Meta:
         constraints = [models.UniqueConstraint(fields=["user",
 "settings"], name="unique_destination_per_user")]

     user = models.ForeignKey(User, on_delete=models.CASCADE)
     settings = models.JSONField()
 }}}

 Admin like so:

 {{{
 from django.contrib import admin
 from django.contrib.auth.admin import UserAdmin as BaseUserAdmin

 class DestinationConfigInline(admin.TabularInline):
     model = DestinationConfig
     ordering = ["media", "label"]
     extra = 1

     def get_formset(self, request, obj=None, **kwargs):
         self.parent_obj = obj
         return super(DestinationConfigInline, self).get_formset(request,
 obj, **kwargs)

     def get_queryset(self, request):
         qs = super(DestinationConfigInline, self).get_queryset(request)
         return qs.filter(user=self.parent_obj)

 class UserAdmin(BaseUserAdmin):
     inlines = [DestinationConfigInline]

 admin.site.register(User, UserAdmin)
 }}}

 Then attempting to save a change to a specific user leads to a traceback
 like this:

 {{{
 Environment:


 Request Method: POST
 Request URL: https://ACENSORED.DOMAIN/admin/APP_auth/user/65/change/

 Django Version: 5.0.6
 Python Version: 3.10.12
 Installed Applications:
 ['channels',
  'django.contrib.admin',
  'django.contrib.auth',
  'django.contrib.contenttypes',
  'django.contrib.sessions',
  'django.contrib.messages',
  'django.contrib.staticfiles',
  'corsheaders',
  'social_django',
  'rest_framework',
  'rest_framework.authtoken',
  'drf_spectacular',
  'django_filters',
  'phonenumber_field',
  'argus.auth',
  'argus.incident',
  'argus.ws',
  'argus.notificationprofile',
  'argus.dev']
 Installed Middleware:
 ['django.middleware.security.SecurityMiddleware',
  'corsheaders.middleware.CorsMiddleware',
  'whitenoise.middleware.WhiteNoiseMiddleware',
  'django.contrib.sessions.middleware.SessionMiddleware',
  'django.middleware.common.CommonMiddleware',
  'django.middleware.csrf.CsrfViewMiddleware',
  'django.contrib.auth.middleware.AuthenticationMiddleware',
  'django.contrib.messages.middleware.MessageMiddleware',
  'django.middleware.clickjacking.XFrameOptionsMiddleware',
  'social_django.middleware.SocialAuthExceptionMiddleware',
  'django.contrib.auth.middleware.RemoteUserMiddleware']

 Traceback (most recent call last):
   File "/usr/local/lib/python3.10/site-
 packages/django/core/handlers/exception.py", line 55, in inner
     response = get_response(request)
   File "/usr/local/lib/python3.10/site-
 packages/django/core/handlers/base.py", line 197, in _get_response
     response = wrapped_callback(request, *callback_args,
 **callback_kwargs)
   File "/usr/local/lib/python3.10/site-
 packages/django/contrib/admin/options.py", line 688, in wrapper
     return self.admin_site.admin_view(view)(*args, **kwargs)
   File "/usr/local/lib/python3.10/site-
 packages/django/utils/decorators.py", line 134, in _wrapper_view
     response = view_func(request, *args, **kwargs)
   File "/usr/local/lib/python3.10/site-
 packages/django/views/decorators/cache.py", line 62, in _wrapper_view_func
     response = view_func(request, *args, **kwargs)
   File "/usr/local/lib/python3.10/site-
 packages/django/contrib/admin/sites.py", line 242, in inner
     return view(request, *args, **kwargs)
   File "/usr/local/lib/python3.10/site-
 packages/django/contrib/admin/options.py", line 1889, in change_view
     return self.changeform_view(request, object_id, form_url,
 extra_context)
   File "/usr/local/lib/python3.10/site-
 packages/django/utils/decorators.py", line 46, in _wrapper
     return bound_method(*args, **kwargs)
   File "/usr/local/lib/python3.10/site-
 packages/django/utils/decorators.py", line 134, in _wrapper_view
     response = view_func(request, *args, **kwargs)
   File "/usr/local/lib/python3.10/site-
 packages/django/contrib/admin/options.py", line 1747, in changeform_view
     return self._changeform_view(request, object_id, form_url,
 extra_context)
   File "/usr/local/lib/python3.10/site-
 packages/django/contrib/admin/options.py", line 1797, in _changeform_view
     if all_valid(formsets) and form_validated:
   File "/usr/local/lib/python3.10/site-packages/django/forms/formsets.py",
 line 579, in all_valid
     return all([formset.is_valid() for formset in formsets])
   File "/usr/local/lib/python3.10/site-packages/django/forms/formsets.py",
 line 579, in <listcomp>
     return all([formset.is_valid() for formset in formsets])
   File "/usr/local/lib/python3.10/site-packages/django/forms/formsets.py",
 line 384, in is_valid
     self.errors
   File "/usr/local/lib/python3.10/site-packages/django/forms/formsets.py",
 line 366, in errors
     self.full_clean()
   File "/usr/local/lib/python3.10/site-packages/django/forms/formsets.py",
 line 456, in full_clean
     self.clean()
   File "/usr/local/lib/python3.10/site-packages/django/forms/models.py",
 line 789, in clean
     self.validate_unique()
   File "/usr/local/lib/python3.10/site-packages/django/forms/models.py",
 line 831, in validate_unique
     if row_data in seen_data:

 Exception Type: TypeError at /admin/APP_auth/user/65/change/
 Exception Value: unhashable type: 'dict'
 }}}

 * `row_data` was `('user', {'email_address':
 'CENSORED@ANOTHERCENSORED.DOMAIN', 'html': True})`
 * `seen_data` was `set()`.

 Is it a design decision that JSONFields cannot be unique/in
 UniqueConstraints?

 * If yes, can it be documented, preferably with a note as to what to do if
 you *do* want a unique JSONField?
 * If no, can validate_unique be changed to work in this instance?

 This has also been tested on:

 * Django Version: 4.2.11
 * Python Version: 3.10.14
-- 
Ticket URL: <https://code.djangoproject.com/ticket/35488>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to django-updates+unsubscr...@googlegroups.com.
To view this discussion on the web visit 
https://groups.google.com/d/msgid/django-updates/0107018fc3bd4365-6224dfb0-6885-4d88-8271-29fad94e57ad-000000%40eu-central-1.amazonses.com.

Reply via email to