wagtail-modeltranslation/wagtail_modeltranslation/patch_wagtailadmin.py
Alexandre Silva 80e5feae4c Merge pull request #77 from sopelj/master
[FIX] Compatibility Fixes for Python3
2017-03-06 11:58:09 +00:00

549 lines
20 KiB
Python

# -*- coding: utf-8 -*-
import copy
import logging
import operator
from six import iteritems
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
from django.db import transaction
from django.db.models import Q
from django.http import Http404
from django.utils.translation import ugettext as _
from wagtail.wagtailadmin.edit_handlers import FieldPanel, \
MultiFieldPanel, FieldRowPanel
from wagtail.wagtailadmin.edit_handlers import StreamFieldPanel
from wagtail.wagtailcore.models import Page, Site
from wagtail.wagtailcore.url_routing import RouteResult
from wagtail.wagtailimages.edit_handlers import ImageChooserPanel
from wagtail.wagtailsearch.index import SearchField
from wagtail.wagtailsnippets.views.snippets import get_snippet_edit_handler, \
SNIPPET_EDIT_HANDLERS
from wagtail_modeltranslation.translator import translator, NotRegistered
from .utils import build_localized_fieldname
try:
from wagtail.wagtailadmin.views.pages import get_page_edit_handler, \
PAGE_EDIT_HANDLERS
except ImportError:
pass
try:
from functools import reduce
except ImportError:
pass
logger = logging.getLogger('wagtail.core')
class WagtailTranslator(object):
_patched_models = []
def __init__(self, model):
# Check if this class was already patched
if model in WagtailTranslator._patched_models:
return
WagtailTranslator._base_model = model
WagtailTranslator._required_fields = {}
# CONSTRUCT TEMPORARY EDIT HANDLER
if issubclass(model, Page):
if hasattr(model, 'get_edit_handler'):
edit_handler_class = model.get_edit_handler()
else:
edit_handler_class = get_page_edit_handler(model)
else:
edit_handler_class = get_snippet_edit_handler(model)
WagtailTranslator._base_model_form = edit_handler_class.get_form_class(model)
defined_tabs = WagtailTranslator._fetch_defined_tabs(model)
for tab_name, tab in defined_tabs:
patched_tab = []
for panel in tab:
trtab = WagtailTranslator._patch_panel(model, panel)
if trtab:
for x in trtab:
patched_tab.append(x)
setattr(model, tab_name, patched_tab)
# DELETE TEMPORARY EDIT HANDLER IN ORDER TO LET WAGTAIL RECONSTRUCT
# NEW EDIT HANDLER BASED ON NEW TRANSLATION PANELS
if issubclass(model, Page):
if hasattr(model, 'get_edit_handler'):
model.get_edit_handler.cache_clear()
edit_handler_class = model.get_edit_handler()
else:
if model in PAGE_EDIT_HANDLERS:
del PAGE_EDIT_HANDLERS[model]
edit_handler_class = get_page_edit_handler(model)
else:
if model in SNIPPET_EDIT_HANDLERS:
del SNIPPET_EDIT_HANDLERS[model]
edit_handler_class = get_snippet_edit_handler(model)
# Set the required of the translated fields that were required on the original field
form = edit_handler_class.get_form_class(model)
for fname, f in form.base_fields.items():
if fname in WagtailTranslator._required_fields[model]:
f.required = True
# Do the same to the formsets
for related_name, formset in iteritems(form.formsets):
if (formset.model in WagtailTranslator._required_fields and
WagtailTranslator._required_fields[formset.model]):
for fname, f in formset.form.base_fields.items():
if fname in WagtailTranslator._required_fields[formset.model]:
f.required = True
# Overide page methods
if issubclass(model, Page):
model.move = _new_move
model.set_url_path = _new_set_url_path
model.route = _new_route
model.get_site_root_paths = _new_get_site_root_paths
model.relative_url = _new_relative_url
model.url = _new_url
_patch_clean(model)
_patch_elasticsearch_fields(model)
WagtailTranslator._patched_models.append(model)
@staticmethod
def _fetch_defined_tabs(defined_class):
"""
Fetch tabs defined by user in models.py
"""
tabs = ()
# If user has defined panels dict on models.py
if hasattr(defined_class, 'panels'):
# TEST !!!
tabs += (('panels', copy.deepcopy(defined_class.panels)),)
# Check for common tabs
else:
if hasattr(defined_class, 'content_panels'):
tabs += (('content_panels', copy.deepcopy(defined_class.content_panels)),)
if hasattr(defined_class, 'promote_panels'):
tabs += (('promote_panels', copy.deepcopy(defined_class.promote_panels)),)
if hasattr(defined_class, 'settings_panels'):
tabs += (('settings_panels', copy.deepcopy(defined_class.settings_panels)),)
return tabs
@staticmethod
def _patch_panel(model, panel):
"""
Generic panel patching function
"""
WagtailTranslator._current_model = model
WagtailTranslator._translation_options = translator.get_options_for_model(model)
if model not in WagtailTranslator._required_fields:
WagtailTranslator._required_fields[model] = []
if panel.__class__.__name__ == 'FieldPanel':
trpanels = WagtailTranslator._patch_fieldpanel(panel)
elif panel.__class__.__name__ == 'ImageChooserPanel':
trpanels = WagtailTranslator._patch_imagechooser(panel)
elif panel.__class__.__name__ == 'MultiFieldPanel':
trpanels = [WagtailTranslator._patch_multifieldpanel(panel)]
elif panel.__class__.__name__ == 'InlinePanel':
WagtailTranslator._patch_inlinepanel(model, panel)
trpanels = [panel]
elif panel.__class__.__name__ == 'StreamFieldPanel':
trpanels = WagtailTranslator._patch_streamfieldpanel(panel)
elif panel.__class__.__name__ == 'FieldRowPanel':
trpanels = [WagtailTranslator._patch_fieldrowpanel(panel)]
else:
trpanels = [panel]
return trpanels
@classmethod
def _is_orig_required(cls, field_name):
"""
check if original field is required
"""
if cls._base_model == cls._current_model:
for fname, f in cls._base_model_form.base_fields.items():
if fname == field_name:
return f.required
else:
for related_name, formset in iteritems(cls._base_model_form.formsets):
if formset.model == cls._current_model:
for fname, f in formset.form.base_fields.items():
if fname == field_name:
return f.required
break
return False
# FieldPanel
####################################
@classmethod
def _patch_fieldpanel(cls, fieldpanel):
"""
Patch FieldPanels and return one per available language
"""
tr_fields = cls._translation_options.fields
translated_fieldpanels = []
if fieldpanel.field_name in tr_fields:
for lang in settings.LANGUAGES:
classes = fieldpanel.classname
if cls._is_orig_required(fieldpanel.field_name) and (lang[0] == settings.LANGUAGE_CODE):
if (build_localized_fieldname(fieldpanel.field_name, lang[0]) not in
cls._required_fields[cls._current_model]):
cls._required_fields[cls._current_model].append(
build_localized_fieldname(fieldpanel.field_name, lang[0]))
translated_field_name = build_localized_fieldname(fieldpanel.field_name, lang[0])
translated_fieldpanels.append(
FieldPanel(translated_field_name, classname=classes, widget=fieldpanel.widget)
)
else:
return [fieldpanel]
return translated_fieldpanels
# ImageChooserPanel
####################################
@classmethod
def _patch_imagechooser(cls, imagechooser):
"""
Patch ImageChooserPanels and return one per available language
"""
tr_fields = cls._translation_options.fields
translated_imagechoosers = []
if imagechooser.field_name in tr_fields:
for lang in settings.LANGUAGES:
if cls._is_orig_required(imagechooser.field_name) and (lang[0] == settings.LANGUAGE_CODE):
if (build_localized_fieldname(imagechooser.field_name, lang[0]) not in
cls._required_fields[cls._current_model]):
cls._required_fields[cls._current_model].append(
build_localized_fieldname(imagechooser.field_name, lang[0])
)
translated_field_name = build_localized_fieldname(imagechooser.field_name, lang[0])
translated_imagechoosers.append(ImageChooserPanel(translated_field_name))
else:
return [imagechooser]
return translated_imagechoosers
# StreamFieldPanel
####################################
@classmethod
def _patch_streamfieldpanel(cls, fieldpanel):
"""
Patch StreamFieldPanels and return one per available language
"""
tr_fields = cls._translation_options.fields
translated_fieldpanels = []
if fieldpanel.field_name in tr_fields:
for lang in settings.LANGUAGES:
if cls._is_orig_required(fieldpanel.field_name) and (lang[0] == settings.LANGUAGE_CODE):
if (build_localized_fieldname(fieldpanel.field_name, lang[0]) not in
cls._required_fields[cls._current_model]):
cls._required_fields[cls._current_model].append(
build_localized_fieldname(fieldpanel.field_name, lang[0])
)
translated_field_name = build_localized_fieldname(fieldpanel.field_name, lang[0])
translated_fieldpanels.append(StreamFieldPanel(translated_field_name))
else:
return [fieldpanel]
return translated_fieldpanels
@classmethod
def _patch_multifieldpanel(cls, mfpanel):
"""
Patch MultiFieldPanel
"""
patched_fields = []
for panel in mfpanel.children:
if panel.__class__.__name__ == 'FieldPanel':
for item in cls._patch_fieldpanel(panel):
patched_fields.append(item)
elif panel.__class__.__name__ == 'ImageChooserPanel':
for item in cls._patch_imagechooser(panel):
patched_fields.append(item)
elif panel.__class__.__name__ == 'FieldRowPanel':
patched_fields.append(cls._patch_fieldrowpanel(panel))
else:
patched_fields.append(panel)
return MultiFieldPanel(patched_fields, classname=mfpanel.classname, heading=mfpanel.heading)
@classmethod
def _patch_fieldrowpanel(cls, frpanel):
"""
Patch FieldRowPanel
"""
patched_fields = []
for panel in frpanel.children:
if panel.__class__.__name__ == 'FieldPanel':
for item in cls._patch_fieldpanel(panel):
patched_fields.append(item)
else:
patched_fields.append(panel)
return FieldRowPanel(
patched_fields,
classname=frpanel.classname)
@classmethod
def _patch_inlinepanel(cls, model, panel):
relation = getattr(model, panel.relation_name)
related_fieldname = 'related'
try:
inline_panels = getattr(getattr(relation, related_fieldname).related_model, 'panels', [])
except AttributeError:
related_fieldname = 'rel'
inline_panels = getattr(getattr(relation, related_fieldname).related_model, 'panels', [])
try:
related_model = getattr(getattr(model, panel.relation_name), related_fieldname).related_model
WagtailTranslator._translation_options = translator.get_options_for_model(related_model)
except NotRegistered:
return None
translated_inline = []
for inline_panel in inline_panels:
for item in cls._patch_panel(related_model, inline_panel):
translated_inline.append(item)
related_model.panels = translated_inline
# Overridden Page methods adapted to the translated fields
@transaction.atomic # only commit when all descendants are properly updated
def _new_move(self, target, pos=None):
"""
Extension to the treebeard 'move' method to ensure that url_path is updated too.
"""
old_url_path = Page.objects.get(id=self.id).url_path
super(Page, self).move(target, pos=pos)
# treebeard's move method doesn't actually update the in-memory instance, so we need to work
# with a freshly loaded one now
# added .specific to use the most specific class so that url_paths are updated to all languages
new_self = Page.objects.get(id=self.id).specific
new_url_path = new_self.set_url_path(new_self.get_parent())
new_self.save()
new_self._update_descendant_url_paths(old_url_path, new_url_path)
# Log
logger.info("Page moved: \"%s\" id=%d path=%s", self.title, self.id, new_url_path)
def _new_set_url_path(self, parent):
"""
This method override populates url_path for each specified language.
This way we can get different urls for each language, defined
by page slug.
"""
for lang in settings.LANGUAGES:
if parent:
parent = parent.specific
tr_slug = getattr(self, 'slug_' + lang[0]) if hasattr(
self, 'slug_' + lang[0]) else getattr(self, 'slug')
if not tr_slug:
tr_slug = getattr(self, 'slug_' + settings.LANGUAGE_CODE) if \
hasattr(self, 'slug_' + settings.LANGUAGE_CODE) else \
getattr(self, 'slug')
if hasattr(parent, 'url_path_' + lang[0]) and getattr(parent, 'url_path_' + lang[0]) is not None:
parent_url_path = getattr(parent, 'url_path_' + lang[0])
else:
parent_url_path = getattr(parent, 'url_path')
if hasattr(self, 'url_path_' + lang[0]):
setattr(self, 'url_path_' + lang[0], parent_url_path + tr_slug + '/')
else:
setattr(self, 'url_path', parent_url_path + tr_slug + '/')
else:
# a page without a parent is the tree root,
# which always has a url_path of '/'
if hasattr(self, 'url_path_' + lang[0]):
setattr(self, 'url_path_' + lang[0], '/')
else:
setattr(self, 'url_path', '/')
# update url_path for children pages
for child in self.get_children():
child.set_url_path(self.specific)
return self.url_path
def _new_route(self, request, path_components):
"""
Rewrite route method in order to handle languages fallbacks
"""
if path_components:
# request is for a child of this page
child_slug = path_components[0]
remaining_components = path_components[1:]
# try:
# q = Q()
# for lang in settings.LANGUAGES:
# tr_field_name = 'slug_%s' % (lang[0])
# condition = {tr_field_name: child_slug}
# q |= Q(**condition)
# subpage = self.get_children().get(q)
# except Page.DoesNotExist:
# raise Http404
# return subpage.specific.route(request, remaining_components)
subpages = self.get_children()
for page in subpages:
if page.specific.slug == child_slug:
return page.specific.route(request, remaining_components)
raise Http404
else:
# request is for this very page
if self.live:
return RouteResult(self)
else:
raise Http404
@staticmethod
def _new_get_site_root_paths():
"""
Return a list of (root_path, root_url) tuples, most specific path first -
used to translate url_paths into actual URLs with hostnames
Same method as Site.get_site_root_paths() but without cache
TODO: remake this method with cache and think of his integration in
Site.get_site_root_paths()
"""
result = [
(site.id, site.root_page.specific.url_path, site.root_url)
for site in Site.objects.select_related('root_page').order_by('-root_page__url_path')
]
return result
def _new_relative_url(self, current_site):
"""
Return the 'most appropriate' URL for this page taking into account the site we're currently on;
a local URL if the site matches, or a fully qualified one otherwise.
Return None if the page is not routable.
Override for using custom get_site_root_paths() instead of
Site.get_site_root_paths()
"""
for (id, root_path, root_url) in self.get_site_root_paths():
if self.url_path.startswith(root_path):
return ('' if current_site.id == id else root_url) + reverse('wagtail_serve',
args=(self.url_path[len(root_path):],))
@property
def _new_url(self):
"""
Return the 'most appropriate' URL for referring to this page from the pages we serve,
within the Wagtail backend and actual website templates;
this is the local URL (starting with '/') if we're only running a single site
(i.e. we know that whatever the current page is being served from, this link will be on the
same domain), and the full URL (with domain) if not.
Return None if the page is not routable.
Override for using custom get_site_root_paths() instead of
Site.get_site_root_paths()
"""
root_paths = self.get_site_root_paths()
for (id, root_path, root_url) in root_paths:
if self.url_path.startswith(root_path):
return ('' if len(root_paths) == 1 else root_url) + reverse(
'wagtail_serve', args=(self.url_path[len(root_path):],))
def _validate_slugs(page):
"""
Determine whether the given slug is available for use on a child page of
parent_page.
"""
parent_page = page.get_parent()
if parent_page is None:
# the root page's slug can be whatever it likes...
return {}
allowed_sibblings = parent_page.specific.allowed_subpage_models()
siblings = parent_page.get_children().exclude(pk=page.pk)
errors = {}
for lang in settings.LANGUAGES:
current_slug = 'slug_' + lang[0]
query_list = []
for model in allowed_sibblings:
slug = getattr(page, current_slug, '') or ''
if len(slug) and model is not Page:
if model in WagtailTranslator._patched_models:
field_name = '{0}__{1}'.format(model._meta.model_name, current_slug)
else:
field_name = '{0}__slug'.format(model._meta.model_name)
kwargs = {field_name: slug}
query_list.append(Q(**kwargs))
if query_list and siblings.filter(reduce(operator.or_, query_list)).exists():
errors[current_slug] = _(u'Slug already in use')
return errors
def _patch_clean(model):
old_clean = model.clean
# Call the original clean method to avoid losing validations
def clean(self):
old_clean(self)
errors = _validate_slugs(self)
if errors:
raise ValidationError(errors)
model.clean = clean
def _patch_elasticsearch_fields(model):
for field in model.search_fields:
# Check if the field is a SearchField and if it is one of the fields registered for translation
if field.__class__ is SearchField and field.field_name in WagtailTranslator._translation_options.fields:
# If it is we create a clone of the original SearchField to keep all the defined options
# and replace its name by the translated one
for lang in settings.LANGUAGES:
translated_field = copy.deepcopy(field)
translated_field.field_name = build_localized_fieldname(field.field_name, lang[0])
model.search_fields = model.search_fields + (translated_field,)