djangodjango-cmsdjango-parler

Django parler TranslatableSlugMixin translates from English to another language, but once translated cannot be translated back returns 404


I'm using Django CMS with Django Parler and have run into a problem that is driving me mad, so if anybody could help, it would be much appreciated!

So I'm creating a simple blog app that has the slug as a translatable field. Here is the model simplified:

from parler.models import TranslatableModel, TranslatedFields

class Article(TranslatableModel):
    ...
    translations = TranslatedFields(
        ...
        slug = models.SlugField(_('slug'), max_length=255, blank=True, allow_unicode=True),
        meta = {'unique_together': (('language_code', 'slug'),)}
    )

    ...

    def get_absolute_url(self):
        return reverse('blog:article_detail', kwargs={'slug': self.slug})

Here are the urls:

from django.conf.urls import include, url

from .views import ArticleDetailView

urlpatterns = [
    ...
    url(r'^(?P<slug>\w[-\w]*)/$', ArticleDetailView.as_view(), name='article_detail'),
]

And finally here is the view:

from django.views.generic import DetailView
from parler.views import TranslatableSlugMixin

from .models import Article

class ArticleDetailView(TranslatableSlugMixin, DetailView):
    model = Article
    template_name = 'blog/_article.html'

I've created an article that is in English, French & German, with a different slug for each language, lets call those:

/en/blog/english-slug
/fr/blog/french-slug
/de/blog/german-slug

I can navigate to these all correctly, but in Django CMS you have the language menu at the top that on the English page shows the links as:

/en/blog/english-slug
/fr/blog/english-slug
/de/blog/english-slug

This is fine, as that's what the TranslatableSlugMixin in the view handles (see here http://django-parler.readthedocs.io/en/latest/api/parler.views.html).

So when I click one of the links (say the French one) the view correctly finds the correct article and redirects me to the correct url. So clicking:

/fr/blog/english-slug

Has taken me correctly to:

/fr/blog/french-slug

But here's where it's all going wrong. I now want to navigate back to the English page, which is showing as:

/en/blog/french-slug

But when I click the link it navigates to a 404. This is the same if I navigate to the German URL from the French one. However if I go from English to German straight away it works.

Sorry, I know this is confusing to explain but it seems the translation works one way from base/default to other language but doesn't work correctly when swapping between languages or back to the base/default.

Surely TranslatableSlugMixin is designed to allow this to happen?! So am I missing something here?

Any help would be much appreciated. Happy to provide more info if necessary.

Thanks


Solution

  • Ok so I've figured out how to make this work and it turns out it's a combination of things...

    1. Using the default Django CMS chooser was a mistake:

      {% language_chooser "menu/language_chooser.html" %}
      

      This leads to the URLs I described above:

      /en/blog/english-slug
      /fr/blog/english-slug
      /de/blog/english-slug
      

      Reading the Django Parler docs led me to using their language navigation menu:

      {% for lang_code, title in LANGUAGES %}
              {% get_language_info for lang_code as lang %}
              {% get_translated_url lang_code as tr_url %}
              {% if tr_url %}<li{% if lang_code == LANGUAGE_CODE %} class="is-selected"{% endif %}><a href="{{ tr_url }}" hreflang="{{ lang_code }}">{{ lang.name_local|capfirst }}</a></li>{% endif %}
      {% endfor %}
      

      This leads to the urls pointing to the correct location:

      /en/blog/english-slug
      /fr/blog/french-slug
      /de/blog/german-slug
      
    2. For the Django Parler navigation to work I needed to update the get_absolute_url() in the model to handle different languages. I did that as follows:

      from django.utils.translation import get_language
      
      from parler.models import TranslatableModel, TranslatedFields
      
      class Article(TranslatableModel):
      
          ...
      
          def get_absolute_url(self):
              language = get_language()
              if self.has_translation(language):
                  slug = self.safe_translation_getter('slug', language_code=language)
                  return reverse('blog:article_detail', kwargs={'slug': slug})
              # no translation so fallback to all article list
              return reverse('blog:article_list')
      

    Phew! That was a headache! Hopefully this helps somebody else in the future!

    P.S. During my research I came across this blog app that seems really great:

    https://github.com/nephila/djangocms-blog

    It helped me get to the bottom of this nightmare!

    UPDATE

    Looking at the get_absolute_url() in djangocms-blog (link above), they have a far better solution to the problem. Their implementation is:

    from django.utils.translation import get_language
    
    from parler.models import TranslatableModel, TranslatedFields
    
    class Article(TranslatableModel):
    
        ...
    
        def get_absolute_url(self, lang=None):
            if not lang or lang not in self.get_available_languages():
                lang = self.get_current_language()
            if not lang or lang not in self.get_available_languages():
                lang = get_language()
            with switch_language(self, lang):
                slug = self.safe_translation_getter('slug', language_code=lang, any_language=True)
                return reverse('blog:article_detail', kwargs={'slug': slug})
    

    Thanks nephila, this has saved me from a lot of cursing and frustration :)