Django IntegrityError: Ключ отсутствует в таблице (но запрос ключа работает) - PullRequest
0 голосов
/ 16 июня 2020

У меня странная проблема с тестами Django. Проблема возникает только тогда, когда я запускал тестовую команду python3 manage.py test.

Справочная информация: у меня есть модель подписки с настраиваемым методом сохранения, который отправляет информационные письма, создает счета, уроки и многое другое. После того, как подписка сохраняется в базе данных с помощью команды super().save(using=using, *args, **kwargs), вызывается метод self.create_lessons() get, и этот метод вызывает IntegrityError при создании уроков, говоря, что подписки не существует в таблице.

Чтобы проверить, действительно ли подписка отсутствует в таблице подписок, я добавил запрос s = Subscription.objects.get(id=self.id) и распечатал его в консоли print(s). Запрос и команда печати работают нормально, но я все равно получаю ошибку целостности.

Что действительно сбивает с толку, по крайней мере для меня, так это то, что ошибка возникает только тогда, когда я запускаю команду python3 manage.py test. Когда я запускаю веб-сервер и создаю подписки с помощью панели администратора или оболочки, все работает так, как должно.

Еще более странно то, что ошибка исчезает на время (2-3 успешных выполнения тестовой команды), когда я вношу в код изменения, которые на самом деле не имеют значения. Например, комментарий или запрос к базе данных.

Я добавил трассировку, пользовательский метод сохранения модели подписки и метод create_lessons ниже. Просто скажите мне, если вам нужен дополнительный код или информация. Я надеюсь, что вы, ребята, можете помочь мне в этом вопросе, и мы очень ценим любой намек или совет! Я долго борюсь с этой проблемой ...

Изменить:

Я нашел обходной путь, чтобы избежать этой проблемы. Когда я добавляю keep-database и параметр debug-mode к тестовой команде и запускаю python3 manage.py test -k --failfast -v 3 --debug-mode, мои тесты работают всегда.

Отслеживание:

Traceback (most recent call last):
sut_1  |   File "/usr/local/lib/python3.8/dist-packages/django/db/backends/utils.py", line 84, in _execute
sut_1  |     return self.cursor.execute(sql, params)
sut_1  | psycopg2.errors.ForeignKeyViolation: insert or update on table "main_lesson" violates foreign key constraint "main_lesson_subscription_id_41a5ca44_fk_main_subscription_id"
sut_1  | DETAIL:  Key (subscription_id)=(5) is not present in table "main_subscription".
sut_1  |
sut_1  |
sut_1  | The above exception was the direct cause of the following exception:
sut_1  |
sut_1  | Traceback (most recent call last):
sut_1  |   File "/code/main/tests_commands.py", line 105, in setUp
sut_1  |     sub1 = Subscription.objects.create(first_lessons_date=date,
sut_1  |   File "/usr/local/lib/python3.8/dist-packages/django/db/models/manager.py", line 82, in manager_method
sut_1  |     return getattr(self.get_queryset(), name)(*args, **kwargs)
sut_1  |   File "/usr/local/lib/python3.8/dist-packages/django/db/models/query.py", line 422, in create
sut_1  |     obj.save(force_insert=True, using=self.db)
sut_1  |   File "/code/main/models.py", line 431, in save
sut_1  |     self.create_lessons()
sut_1  |   File "/code/main/models.py", line 211, in create_lessons
sut_1  |     l.save(using='primary')
sut_1  |   File "/usr/local/lib/python3.8/dist-packages/django/db/models/base.py", line 740, in save
sut_1  |     self.save_base(using=using, force_insert=force_insert,
sut_1  |   File "/usr/local/lib/python3.8/dist-packages/django/db/models/base.py", line 777, in save_base
sut_1  |     updated = self._save_table(
sut_1  |   File "/usr/local/lib/python3.8/dist-packages/django/db/models/base.py", line 870, in _save_table
sut_1  |     result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
sut_1  |   File "/usr/local/lib/python3.8/dist-packages/django/db/models/base.py", line 907, in _do_insert
sut_1  |     return manager._insert([self], fields=fields, return_id=update_pk,
sut_1  |   File "/usr/local/lib/python3.8/dist-packages/django/db/models/manager.py", line 82, in manager_method
sut_1  |     return getattr(self.get_queryset(), name)(*args, **kwargs)
sut_1  |   File "/usr/local/lib/python3.8/dist-packages/django/db/models/query.py", line 1186, in _insert
sut_1  |     return query.get_compiler(using=using).execute_sql(return_id)
sut_1  |   File "/usr/local/lib/python3.8/dist-packages/django/db/models/sql/compiler.py", line 1375, in execute_sql
sut_1  |     cursor.execute(sql, params)
sut_1  |   File "/usr/local/lib/python3.8/dist-packages/django/db/backends/utils.py", line 99, in execute
sut_1  |     return super().execute(sql, params)
sut_1  |   File "/usr/local/lib/python3.8/dist-packages/django/db/backends/utils.py", line 67, in execute
sut_1  |     return self._execute_with_wrappers(sql, params, many=False, executor=self._execute)
sut_1  |   File "/usr/local/lib/python3.8/dist-packages/django/db/backends/utils.py", line 76, in _execute_with_wrappers
sut_1  |     return executor(sql, params, many, context)
sut_1  |   File "/usr/local/lib/python3.8/dist-packages/django/db/backends/utils.py", line 84, in _execute
sut_1  |     return self.cursor.execute(sql, params)
sut_1  |   File "/usr/local/lib/python3.8/dist-packages/django/db/utils.py", line 89, in __exit__
sut_1  |     raise dj_exc_value.with_traceback(traceback) from exc_value
sut_1  |   File "/usr/local/lib/python3.8/dist-packages/django/db/backends/utils.py", line 84, in _execute
sut_1  |     return self.cursor.execute(sql, params)
sut_1  | django.db.utils.IntegrityError: insert or update on table "main_lesson" violates foreign key constraint "main_lesson_subscription_id_41a5ca44_fk_main_subscription_id"
sut_1  | DETAIL:  Key (subscription_id)=(5) is not present in table "main_subscription".

Пользовательский метод сохранения модели подписки:

#Custom save function which includes creating the lessons of the 
#subscription
def save(self, using='primary', *args, **kwargs):
    #Marks the bill as paid if the bill already was paid in advance
    if self.billing_mode == '2':
        self.bill_paid = '1'
    #If lessons_outstanding isn't defined and isn't zero, the value 
    #will be equal to the amount of lessons.
    if not self.lessons_outstanding:
        if self.lessons_outstanding != 0:
            self.lessons_outstanding = self.contract.lessons_count

    #Sends info email
    if self.bexio_id == None and self.info_mail != '1':
        #Sends email to student
        subject = 'Neues Abo wurde erstellt'

        #If there is a bill, the text with a bill will be sent.
        #Elsewise there's only the text without the reference
        #to the bill. 
        first_lessons_date = self.first_lessons_date + timedelta(minutes=60)
        if self.billing_mode == '2':
            body_text = """Hallo {} {}

            {} {} hat soeben ein Abo mit Startdatum {} für Sie erstellt.

            Wir wünschen viel Spass im Unterricht und stehen bei Fragen gerne zur Verfügung.

            """.format(self.contract.student.first_name, 
            self.contract.student.last_name,
            self.contract.teacher.first_name,
            self.contract.teacher.last_name,
            first_lessons_date.strftime("%d.%m.%Y"))

            body_html = """<p>Hallo {} {}</p><p></p><p>{} {} hat soeben
            ein Abo mit Startdatum {} für Sie erstellt.
            <p></p>
            <p>Wir wünschen viel Spass im Unterricht und stehen bei 
            Fragen gerne zur Verfügung.</p>

            """.format(self.contract.student.first_name, 
            self.contract.student.last_name,
            self.contract.teacher.first_name,
            self.contract.teacher.last_name,
            first_lessons_date.strftime("%d.%m.%Y"))
        else:
            body_text = """Hallo {} {}

            {} {} hat soeben ein Abo mit Startdatum {} für Sie erstellt. Die Rechnung erhalten Sie in einem separaten Mail. Falls Sie die Zahlungsweise «Papierrechnung» bei uns hinterlegt haben, erhalten Sie die Rechnung per Post.

            Wir wünschen viel Spass im Unterricht und stehen bei Fragen gerne zur Verfügung.

            """.format(self.contract.student.first_name, 
            self.contract.student.last_name,
            self.contract.teacher.first_name,
            self.contract.teacher.last_name,
            first_lessons_date.strftime("%d.%m.%Y"))

            body_html = """<p>Hallo {} {}</p><p></p><p>{} {} hat soeben
            ein Abo mit Startdatum {} für Sie erstellt. Die Rechnung
            erhalten Sie in einem separaten Mail. Falls Sie die
            Zahlungsweise «Papierrechnung» bei uns hinterlegt haben, 
            erhalten Sie die Rechnung per Post.</p>
            <p></p>
            <p>Wir wünschen viel Spass im Unterricht und stehen bei 
            Fragen gerne zur Verfügung.</p>

            """.format(self.contract.student.first_name, 
            self.contract.student.last_name,
            self.contract.teacher.first_name,
            self.contract.teacher.last_name,
            first_lessons_date.strftime("%d.%m.%Y"))

        #If there is no student address an escalation will
        #be created, because the info email can't be send
        if not self.contract.student.address:
            rel = ('Student (' + str(self.contract.student.pipedrive_id) + 
                    '): ' + self.contract.student.first_name + ' ' + 
                    self.contract.student.last_name)
            create_escalation(type='E1', category='S', object=rel)

        else:
            email_to = [self.contract.student.address.email]
            send_mail(subject, body_text, body_html, email_to)

        #send email to teacher
        subject = 'Abo erstellt für {} {}'.format(self.contract.student.last_name,
                                                  self.contract.student.first_name)
        #If there is a bill, the text with a bill will be sent.
        #Elsewise there's only the text without
        if self.billing_mode == '2':
            body_text = """Hallo {}

            Es wurde folgendes Abo erstellt. Die Rechnung wurde bereits mit einem Gutschein beglichen:

            Schüler: {} {}
            Startdatum {}

            Für weitere Informationen logge dich mit deinem Login unter backend.school78.ch ein. Dort kannst du auch die erteilten Lektionen eintragen.

            Bei Fragen wende dich an dein School78 Team.
            """.format(self.contract.teacher.first_name,
            self.contract.student.last_name,
            self.contract.student.first_name, 
            first_lessons_date.strftime("%d.%m.%Y"))

            body_html = """<p>Hallo {}</p>
            <p></p><p>Es wurde folgendes Abo erstellt. Die Rechnung wurde
            bereits mit einem Gutschein beglichen:</p>
            <p></p><p>Schüler: {} {}<br />Startdatum: {}</p>
            <p></p><p>Für weitere Informationen logge dich mit deinem Login
            unter <a href='https://backend.school78.ch'>backend.school78.ch</a>
            ein. Dort kannst du auch die erteilten Lektionen eintragen.</p><p></p><p>Bei Fragen 
            stehen wir dir gerne zur Verfügung.</P>""".format(self.contract.teacher.first_name,
            self.contract.student.last_name,
            self.contract.student.first_name, 
            first_lessons_date.strftime("%d.%m.%Y"))
        else:
            body_text = """Hallo {}

            Die Rechnung für das folgende Abo wurde per Email versendet – bei Papierrechnung erfolgt der Versand in den nächsten drei Arbeitstagen:

            Schüler: {} {}
            Startdatum {}

            Für weitere Informationen logge dich mit deinem Login unter backend.school78.ch ein. Dort kannst du auch die erteilten Lektionen eintragen und sehen, ob das Abo bereits bezahlt ist oder nicht.

            Bei Fragen wende dich an dein School78 Team.
            """.format(self.contract.teacher.first_name,
            self.contract.student.last_name,
            self.contract.student.first_name, 
            first_lessons_date.strftime("%d.%m.%Y"))

            body_html = """<p>Hallo {}</p>
            <p></p><p>Die Rechnung für das folgende Abo wurde per Email
            versendet – bei Papierrechnung erfolgt der Versand in den
            nächsten drei Arbeitstagen:</p>
            <p></p><p>Schüler: {} {}<br />Startdatum: {}</p>
            <p></p><p>Für weitere Informationen logge dich mit deinem Login
            unter <a href='https://backend.school78.ch'>backend.school78.ch</a>
            ein. Dort kannst du auch die erteilten Lektionen eintragen und sehen,
            ob das Abo bereits bezahlt ist oder nicht.</p><p></p><p>Bei Fragen 
            stehen wir dir gerne zur Verfügung.</P>""".format(self.contract.teacher.first_name,
            self.contract.student.last_name,
            self.contract.student.first_name, 
            first_lessons_date.strftime("%d.%m.%Y"))

        #If there is no teacher address an escalation will
        #be created, because the info email can't be send
        if not self.contract.teacher.address:
            rel = ('Teacher (' + str(self.contract.teacher.pipedrive_id) + 
                    '): ' + self.contract.teacher.first_name + ' ' + 
                    self.contract.teacher.last_name)
            create_escalation(type='E1', category='T', object=rel)

        else:
            email_to = [self.contract.teacher.address.email]
            send_mail(subject, body_text, body_html, email_to, teacher=True)
            self.info_mail = '1'

    #Saves the model        
    super().save(using=using, *args, **kwargs)

    s = Subscription.objects.get(id=self.id)
    print(s)

    #Creates lessons if they don't exist
    if self.lessons.count() == 0:
        self.create_lessons()

    #Creates bill if there is none 
    if self.bexio_id == None and self.bill_paid != '1':
        #If bill type is paper, an escalation will be created and the
        #office will send a bill manually
        if self.contract.bill_type == 'P':
            #Creates bill with customcommand
            args = [None, self.pk]
            opts = {}
            call_command('create_bill', *args, **opts)

        #If bill type is email, the bill will be created directly in
        #Bexio and will be sent...
        elif self.contract.bill_type == 'E': 
            #Creates bill with customcommand
            args = [None, self.pk]
            opts = {}
            call_command('create_bill', *args, **opts)

Create_lessons () - метод:

#Creates the lessons related to the new subscription
    def create_lessons(self):
        s = Subscription.objects.get(id=self.id)
        print(s)
        for i in range(0, self.contract.lessons_count):
            if i == 0:
                l = Lesson(lessons_date=self.first_lessons_date, status='T', subscription=self)
                l.save(using='primary')
            else:
                l = Lesson(status='N', subscription=self)
                l.save(using='primary')
...