iiuni / projektzapisy

System Zapisów na zajęcia w Instytucie Informatyki Uniwersytetu Wrocławskiego
https://zapisy.ii.uni.wroc.pl
32 stars 10 forks source link

IntegrityError: null value in column "employee_id" violates not-null constraint DETAIL: Failing row contains (2003, null, null, null). #1322

Open rollbar[bot] opened 2 years ago

rollbar[bot] commented 2 years ago

View details in Rollbar: https://rollbar.com/iiuni/projektzapisy/items/483/

Traceback (most recent call last):
  File "/home/zapisy/deploy/releases/20220610165135/venv/lib/python3.8/site-packages/django/core/handlers/base.py", line 181, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/home/zapisy/deploy/releases/20220610165135/venv/lib/python3.8/site-packages/newrelic/hooks/framework_django.py", line 554, in wrapper
    return wrapped(*args, **kwargs)
  File "/home/zapisy/deploy/releases/20220610165135/venv/lib/python3.8/site-packages/django/contrib/auth/decorators.py", line 21, in _wrapped_view
    return view_func(request, *args, **kwargs)
  File "/home/zapisy/deploy/releases/20220610165135/zapisy/apps/offer/preferences/views.py", line 16, in main
    formset.save()
  File "/home/zapisy/deploy/releases/20220610165135/venv/lib/python3.8/site-packages/django/forms/models.py", line 673, in save
    return self.save_existing_objects(commit) + self.save_new_objects(commit)
  File "/home/zapisy/deploy/releases/20220610165135/venv/lib/python3.8/site-packages/django/forms/models.py", line 811, in save_new_objects
    self.new_objects.append(self.save_new(form, commit=commit))
  File "/home/zapisy/deploy/releases/20220610165135/venv/lib/python3.8/site-packages/django/forms/models.py", line 650, in save_new
    return form.save(commit=commit)
  File "/home/zapisy/deploy/releases/20220610165135/venv/lib/python3.8/site-packages/django/forms/models.py", line 460, in save
    self.instance.save()
  File "/home/zapisy/deploy/releases/20220610165135/venv/lib/python3.8/site-packages/django/db/models/base.py", line 753, in save
    self.save_base(using=using, force_insert=force_insert,
  File "/home/zapisy/deploy/releases/20220610165135/venv/lib/python3.8/site-packages/django/db/models/base.py", line 790, in save_base
    updated = self._save_table(
  File "/home/zapisy/deploy/releases/20220610165135/venv/lib/python3.8/site-packages/django/db/models/base.py", line 895, in _save_table
    results = self._do_insert(cls._base_manager, using, fields, returning_fields, raw)
  File "/home/zapisy/deploy/releases/20220610165135/venv/lib/python3.8/site-packages/django/db/models/base.py", line 933, in _do_insert
    return manager._insert(
  File "/home/zapisy/deploy/releases/20220610165135/venv/lib/python3.8/site-packages/django/db/models/manager.py", line 85, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/home/zapisy/deploy/releases/20220610165135/venv/lib/python3.8/site-packages/django/db/models/query.py", line 1254, in _insert
    return query.get_compiler(using=using).execute_sql(returning_fields)
  File "/home/zapisy/deploy/releases/20220610165135/venv/lib/python3.8/site-packages/django/db/models/sql/compiler.py", line 1397, in execute_sql
    cursor.execute(sql, params)
  File "/home/zapisy/deploy/releases/20220610165135/venv/lib/python3.8/site-packages/django/db/backends/utils.py", line 66, in execute
    return self._execute_with_wrappers(sql, params, many=False, executor=self._execute)
  File "/home/zapisy/deploy/releases/20220610165135/venv/lib/python3.8/site-packages/django/db/backends/utils.py", line 75, in _execute_with_wrappers
    return executor(sql, params, many, context)
  File "/home/zapisy/deploy/releases/20220610165135/venv/lib/python3.8/site-packages/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
  File "/home/zapisy/deploy/releases/20220610165135/venv/lib/python3.8/site-packages/django/db/utils.py", line 90, in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
  File "/home/zapisy/deploy/releases/20220610165135/venv/lib/python3.8/site-packages/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
  File "/home/zapisy/deploy/releases/20220610165135/venv/lib/python3.8/site-packages/newrelic/hooks/database_psycopg2.py", line 64, in execute
    return super(CursorWrapper, self).execute(sql, parameters, *args,
  File "/home/zapisy/deploy/releases/20220610165135/venv/lib/python3.8/site-packages/newrelic/hooks/database_dbapi2.py", line 38, in execute
    return self.__wrapped__.execute(sql, parameters,
IntegrityError: null value in column "employee_id" violates not-null constraint
DETAIL:  Failing row contains (2003, null, null, null).
Traceback (most recent call last):
  File "/home/zapisy/deploy/releases/20220610165135/venv/lib/python3.8/site-packages/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
  File "/home/zapisy/deploy/releases/20220610165135/venv/lib/python3.8/site-packages/newrelic/hooks/database_psycopg2.py", line 64, in execute
    return super(CursorWrapper, self).execute(sql, parameters, *args,
  File "/home/zapisy/deploy/releases/20220610165135/venv/lib/python3.8/site-packages/newrelic/hooks/database_dbapi2.py", line 38, in execute
    return self.__wrapped__.execute(sql, parameters,
NotNullViolation: null value in column "employee_id" violates not-null constraint
DETAIL:  Failing row contains (2003, null, null, null).
lgpawel commented 1 year ago

To prawdopodobnie będzie wymagać pewnego (ale chyba nie jakiegoś bardzo bolesnego) wgryzienia się w niuanse Django.

Hipoteza do zweryfikowania na początek (a przynajmniej miejsce, od którego można zacząć) jest taka, że domyślna walidacja formularza nie chroni nas przed próbą wstawienia do bazy "nielegalnych" danych dlatego, że tylko pole answer modelu Preference pojawia się w definicji klasy PreferenceForm; można więc spróbować dodać tam więcej pól (ale oczywiście nie chcemy, by w formularzu pojawiły się nowe odpowiadające im kontrolki) bądź przeciążyć metodę is_valid lub jakąś powiązaną.

mikiSpoko200 commented 1 year ago

Zasugerowana hipoteza była prawdziwa. Formularz PreferenceForm w definicji klasy używał jedynie pola answer i tym samym tylko ono podlegało domyślnej walidacji. Aplikacja preferences opiera się na dynamicznie generowanej klasie tworzonej za pomocą modelformset_factory. Klasa ta dziedziczy z BaseModelFormSet, która z kolei dziedziczy z BaseFormSet.

Podczas generowania klasy za pomocą modelformset_factory przekazany musi być model dla, którego tworzony będzie formset oraz można przekazać parametr form, który będzie wzorcem według którego będą tworzone poszczególne formularze. W naszym przypadku odpowiednio Preference oraz PreferenceForm).

Następnie tworząc instancję tak wygenerowanej klasy można przekazać parametr queryset, który określa wartości jakimi zostaną wypełnione poszczególne formularze (1 formularz per rekord (?)). W naszym przypadku jest to Preference.objects.filter(employee=employee).order_by('question__proposal') (patrz prepare_formset w preference/forms.py). To wiąże te formularze z tymi danymi (bound), więc wszystkie operacje na tych danych będą powodować modyfikację istniejących danych a nie tworznie nowych.

Dodatkowo formset wyróżnia dwa rodzaje formualrzy initial oraz extra. initial to lista formularzy, do których dane zostały wczytane z systemu (to są te bound formularze) a extra - przeciwnie - to lista formularzy, które zostały wygenerowane jako puste i mają służyć tworzeniu nowych instacji modelu.

Ilość tych formularzy może być dynamicznie zarządzana przez JS na frontendzie dlatego wartości tych zmiennych wyrażone są w DOM'ie poprzez: <input type="hidden" name="form-TOTAL_FORMS" value="22" id="id_form-TOTAL_FORMS"> <input type="hidden" name="form-INITIAL_FORMS" value="22" id="id_form-INITIAL_FORMS">

Manipulacja wartością form-INITIAL_FORMS poprzez zmniejszenie jej o n spowoduje, że django uzna n ostatnich formularzy jako extra a tym samym spóbuje stworzyć dla nich nowe instancje. W tym celu na pewnym etapie wywoła save na klasie formularza używanego przez formset. W naszym przypadku jest to PreferenceForm, a ten formularz obsługuje jedynie pole answer i tylko te pole waliduje, więc podczas próby zapisania brakujące wartości są null'owane co powoduje błąd.

Alternatywne podejście polegające na dodaniu pozostałych pól modelu i ukryciu ich w formularzu poprawnie odrzuci podczas walidacji i zgłosi w tym przypadku błąd łamania ograniczenia unique_together = ('employee', 'question') z preferences/models.py. Dzieje się tak dlatego, że tym razem dostępne są jakieś dane do utworzenia nowej instancji, ale ponieważ są to dane wcześniej odczytane z bazy to ich ponowna próba dodania łamie wspomniane ograniczenie.

Z tego co rozumiem, to nie ma opcji żeby wymusić statyczną ilość wpisów - najlepsze co można zrobić to przeciążyć funkcję walidującą żeby porównywała ilość zwracanych formularzy z oczekiwaną ilością wyliczaną z długości quertset'u z pytaniami.

mikiSpoko200 commented 1 year ago

Zamknąłem issue przypadkiem - reopen