wagtail / wagtail-localize

Translation plugin for Wagtail CMS
https://wagtail-localize.org/
Other
226 stars 86 forks source link

partners.models.<MODEL>.DoesNotExist: <MODEL> matching query does not exist. #583

Open hallpower opened 2 years ago

hallpower commented 2 years ago

As discussed with @zerolab in the Wagtail Slack support channel, I'm getting a ' DoesNotExist matching query does not exist' error while trying to create translations for some pages.

I have an existing wagtail project that has been upgraded to Wagtail 3.0, running wagtail-localize 1.2.

The issue is described in: https://wagtailcms.slack.com/archives/C81FGJR2S/p1654081344988459?thread_ts=1654035708.629839&cid=C81FGJR2S

There is a 3 min demo video of the process to generate the issue here: https://www.youtube.com/watch?v=ihxmNtvY5OA

Some code and configurations are available here:

zerolab commented 2 years ago

Hey @hallpower,

I... cannot reproduce this locally. See https://youtu.be/uWctc6HyOIY using a barebones Wagtail project (as in wagtail start projectname) with a HomePage and a Partners model simply inheriting from Page.

adding the traceback from the Slack thread here for reference:

Traceback ``` Traceback (most recent call last): File "/Users/markhallbauer/.pyenv/versions/3.9.10/lib/python3.9/site-packages/django/core/handlers/exception.py", line 55, in inner response = get_response(request) File "/Users/markhallbauer/.pyenv/versions/3.9.10/lib/python3.9/site-packages/django/core/handlers/base.py", line 197, in _get_response response = wrapped_callback(request, *callback_args, **callback_kwargs) File "/Users/markhallbauer/.pyenv/versions/3.9.10/lib/python3.9/site-packages/django/views/decorators/cache.py", line 62, in _wrapped_view_func response = view_func(request, *args, **kwargs) File "/Users/markhallbauer/.pyenv/versions/3.9.10/lib/python3.9/site-packages/wagtail/admin/urls/__init__.py", line 161, in wrapper return view_func(request, *args, **kwargs) File "/Users/markhallbauer/.pyenv/versions/3.9.10/lib/python3.9/site-packages/wagtail/admin/auth.py", line 182, in decorated_view response = view_func(request, *args, **kwargs) File "/Users/markhallbauer/.pyenv/versions/3.9.10/lib/python3.9/site-packages/django/views/generic/base.py", line 84, in view return self.dispatch(request, *args, **kwargs) File "/Users/markhallbauer/.pyenv/versions/3.9.10/lib/python3.9/site-packages/wagtail_localize/views/submit_translations.py", line 175, in dispatch return super().dispatch(request, *args, **kwargs) File "/Users/markhallbauer/.pyenv/versions/3.9.10/lib/python3.9/site-packages/django/views/generic/base.py", line 119, in dispatch return handler(request, *args, **kwargs) File "/Users/markhallbauer/.pyenv/versions/3.9.10/lib/python3.9/site-packages/wagtail_localize/views/submit_translations.py", line 119, in post return self.form_valid(form) File "/Users/markhallbauer/.pyenv/versions/3.9.10/lib/python3.9/contextlib.py", line 79, in inner return func(*args, **kwds) File "/Users/markhallbauer/.pyenv/versions/3.9.10/lib/python3.9/site-packages/wagtail_localize/views/submit_translations.py", line 151, in form_valid single_translated_object = self.object.get_translation( File "/Users/markhallbauer/.pyenv/versions/3.9.10/lib/python3.9/site-packages/wagtail/models/i18n.py", line 165, in get_translation return self.get_translations(inclusive=True).get(locale_id=pk(locale)) File "/Users/markhallbauer/.pyenv/versions/3.9.10/lib/python3.9/site-packages/django/db/models/query.py", line 496, in get raise self.model.DoesNotExist( Exception Type: DoesNotExist at /admin/localize/submit/page/14/ Exception Value: Partner matching query does not exist. ```

a few things to try: check in the django shell that the page gets created. e.g.

# replace ID with the page id you are trying to translate. e.g. 14 from /admin/localize/submit/page/14/ in the traceback
for translation in Partner.objects.get(pk=ID).get_translations(inclusive=True):
    print(translation, translation.pk, translation.locale)
hallpower commented 2 years ago

@zerolab, sorry for slow response; I was off on family vacation.

Thank you so much for supporting in troubleshooting this. I really appreciate it!

I figured it out! Of course it now makes perfect sense. I had a unique constraint on a few fields in my models. When I tried to create the translation, it failed to create the translation page due to the constraint violation. The issue can be reproduced by putting a unique=True, constraint in a charfield in a model. You can test it with this snippet model, which will throw the same error:

@register_snippet
class Amenity(TranslatableMixin, models.Model):
    """Type model for amenities."""

    title = models.CharField(
        max_length=100,
        blank=False,
        null=True,
        unique=True,
    )  
    panels = [
        MultiFieldPanel(
            [
                FieldPanel("title"),
            ],
            heading="Amenity",
        )
    ]

    def __str__(self):
        """String representation of this Class."""
        return (
            f"{self.title}"
        )

    class Meta:
        verbose_name = "Amenity"
        verbose_name_plural = "Amenities"
        ordering = ["title"]
        unique_together = ("translation_key", "locale")

The workaround would be to add a tuple in the class Meta: unique_together to ensure the constraint. However, this isn't handled very elegantly when it fails, causing an IntegrityError.

In my view, ideally Wagtail-Localize should recognize the unique constraint on a field and automatically interpret the unique constraint in conjunction with the locale. It would also be nice if it failed more elegantly.

enzedonline commented 2 years ago

@hallpower Hi Mark, I've come across this on my own site, and needed an additional pair in the unique_together with the unique field. In your case, probably

unique_together = ('translation_key', 'locale'), ('locale', 'parent_amenity') Then I added some extra code around limiting new instances to the default locale to deal with the integrity error issue.

https://enzedonline.com/en/tech-blog/dealing-with-unique-fields-on-a-multi-lingual-site/

The field on mine was an editable slug-field rather than an ID. Maybe some of the steps will be redundant since your field is auto-generated and is already unique.

zerolab commented 2 years ago

Thank you for pitching in, @enzedonline!

We could improve documentation as a starting point. The unique_together = ("translation_key", "locale") constraint comes from core via TranslatableMixin. I feel that TranslatableMixin should perhaps be wiser in its check - https://github.com/wagtail/wagtail/blob/5994cc43dfc5cc1ed891ab78eff3a3bcf56f6830/wagtail/models/i18n.py#L103-L127

enzedonline commented 2 years ago

@zerolab glad this was useful

I think the biggest part of this problem is that unique is in the standard validation (so an integrity error is picked up even if not handled) while unique_together isn't - it just fails silently while looking like it has succeeded in translating.

This means you have to write all the code to check integrity in a custom clean method each time. Gets messy.

RoyTea commented 1 year ago

raise self.model.DoesNotExist( artisan_information.models.Artisan.DoesNotExist: Artisan matching query does not exist.

RoyTea commented 1 year ago

How can I solve the problem of

raise self.model.DoesNotExist( artisan_information.models.Artisan.DoesNotExist: Artisan matching query does not exist.

RoyTea commented 1 year ago

artisan = Artisan.objects.get(user=user), user is defined in my models.py.

zerolab commented 1 year ago

@RoyTea please join the Wagtail Slack and ask there. The two lines you shared do not give enough context, other than Artisan.objects.get(user=user) can fail if you do not have and Artisan instance with the given user. But that is very much unrelated to wagtail-localize

zemogle commented 3 months ago

I had this exact problem but with a totally different cause. There was a ForeignKey field which had a poorly set ID (from porting the content in from a different CMS). Something that Django does badly is tell you which field is the issue.

I wrote a management command (based on modelcluster) to find the field in my model Activity which subclasses Wagtail Page.

import json

from django.conf import settings
from django.core.management.base import CommandError, BaseCommand
from django.core.exceptions import FieldDoesNotExist
from django.db.models.fields.related import ForeignObjectRel
from wagtail.models import Locale
from wagtail_localize.models import TranslatableObject, TranslationSource

from activities.models import Activity

import logging

logger = logging.getLogger(__name__)

class Command(BaseCommand):
    """
    Add all activities and other content into wagtail
    """

    help = 'Check through all activities and check for missing pdfs in translation files, and sync to DB if missing'

    def add_arguments(self, parser):
        parser.add_argument("-c", "--code", dest='code', type=str, help="Fix specific activity by code")
        parser.add_argument("-a", "--all", dest='all', action='store_true', help="Fix all activities")
        parser.add_argument("-d", "--diagnostic", dest='diagnostic', action='store_true', help="Show the issue without changing DB")

    def handle(self, *args, **options):
        en = Locale.objects.get(language_code='en')
        if options['code']:
            activities = Activity.objects.filter(code=options['code'], locale=en)
        elif options['all']:
            activities = Activity.objects.filter(locale=en)
        else:
            logger.error('Select either --code or --all to run this command.')
            return

        for a in activities:
            try:
                transobj = TranslatableObject.objects.get(translation_key=a.translation_key)
            except TranslatableObject.DoesNotExist:
                logger.error(f'No activity translation for {a.code}')
                continue
            try:
                ts = TranslationSource.objects.get(object=transobj)
            except:
                logger.error(f'Error with {a.code}')
                continue
            data = json.loads(ts.content_json)

            pk_field = Activity._meta.pk
            kwargs = {}

            # If model is a child via multitable inheritance, we need to set ptr_id fields all the way up
            # to the main PK field, as Django won't populate these for us automatically.
            while pk_field.remote_field and pk_field.remote_field.parent_link:
                kwargs[pk_field.attname] = data['pk']
                pk_field = pk_field.remote_field.model._meta.pk

            kwargs[pk_field.attname] = data['pk']
            for field_name, field_value in data.items():
                try:
                    field = Activity._meta.get_field(field_name)
                except FieldDoesNotExist:
                    continue

                # Filter out reverse relations
                if isinstance(field, ForeignObjectRel):
                    continue

                if field.remote_field and isinstance(field.remote_field, models.ManyToManyRel):
                    related_objects = field.remote_field.model._default_manager.filter(pk__in=field_value)
                    kwargs[field.attname] = list(related_objects)

                elif field.remote_field and isinstance(field.remote_field, models.ManyToOneRel):
                    if field_value is None:
                        kwargs[field.attname] = None
                    else:
                        try:
                            clean_value = field.remote_field.model._meta.get_field(field.remote_field.field_name).to_python(field_value)
                            print("All OK with {}".format(a.title))
                        except:
                            if options['diagnostic']:
                                print('Issue with {} in {}'.format(field_name, a.title))
                                continue
                            else:
                                ts.update_from_db()
                                print('Synced {} from DB'.format(a.title))
                                continue