diff --git a/hvad/admin.py b/hvad/admin.py index 57a3f7dc..b6d243ea 100644 --- a/hvad/admin.py +++ b/hvad/admin.py @@ -2,13 +2,12 @@ Part of hvad public API. """ import functools -import warnings + import django -from django.contrib.admin.options import ModelAdmin, csrf_protect_m, InlineModelAdmin -from django.contrib.admin.utils import flatten_fieldsets, unquote, get_deleted_objects +from django.contrib.admin.options import InlineModelAdmin, ModelAdmin, csrf_protect_m +from django.contrib.admin.utils import flatten_fieldsets, get_deleted_objects, unquote from django.contrib.contenttypes.models import ContentType from django.core.exceptions import FieldDoesNotExist, PermissionDenied, ValidationError -from django.urls import reverse from django.db import router, transaction from django.forms.models import model_to_dict from django.forms.utils import ErrorList @@ -16,14 +15,17 @@ from django.shortcuts import render from django.template import TemplateDoesNotExist from django.template.loader import select_template -from django.utils.encoding import iri_to_uri, force_text +from django.urls import reverse +from django.utils.encoding import force_text, iri_to_uri from django.utils.functional import curry -from django.utils.translation import ugettext_lazy as _, get_language, get_language_info +from django.utils.safestring import mark_safe +from django.utils.translation import get_language, get_language_info, ugettext_lazy as _ from urllib.parse import urlencode, urlparse + from hvad.forms import TranslatableModelForm, translatable_inlineformset_factory, translatable_modelform_factory +from hvad.manager import TranslationQueryset from hvad.settings import hvad_settings from hvad.utils import load_translation -from hvad.manager import TranslationQueryset __all__ = ( 'TranslatableAdmin', @@ -33,7 +35,8 @@ 'InlineModelForm', ) -#=============================================================================== + +# =============================================================================== class InlineModelForm(TranslatableModelForm): def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, @@ -56,15 +59,17 @@ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, object_data["id"] = trans.master.id object_data.update(initial or {}) super(TranslatableModelForm, self).__init__(data, files, auto_id, - prefix, object_data, - error_class, label_suffix, - empty_permitted, instance, **kwargs) + prefix, object_data, + error_class, label_suffix, + empty_permitted, instance, **kwargs) -#=============================================================================== + +# =============================================================================== class TranslatableModelAdminMixin(object): query_language_key = 'language' + @mark_safe def all_translations(self, obj): """ Get an HTML-formatted list of all translations, with links to admin pages """ if obj is None or not obj.pk: @@ -78,6 +83,7 @@ def all_translations(self, obj): entry = u'%s' % entry languages.append(entry) return u', '.join(languages) + all_translations.allow_tags = True all_translations.short_description = _(u'all translations') @@ -103,20 +109,20 @@ def get_language_tabs(self, obj, request, available_languages): def _language(self, request): return request.GET.get(self.query_language_key, get_language()) -#=============================================================================== + +# =============================================================================== class TranslatableAdmin(ModelAdmin, TranslatableModelAdminMixin): form = TranslatableModelForm - + change_form_template = 'admin/hvad/change_form.html' - + deletion_not_allowed_template = 'admin/hvad/deletion_not_allowed.html' - + def __init__(self, *args, **kwargs): super(TranslatableAdmin, self).__init__(*args, **kwargs) self.reverse = functools.partial(reverse, current_app=self.admin_site.name) - def get_url(self, obj, lang=None, get={}): ct = ContentType.objects.get_for_model(self.model) info = ct.app_label, ct.model @@ -125,17 +131,16 @@ def get_url(self, obj, lang=None, get={}): url = '%s?%s' % (self.reverse('admin:%s_%s_change' % info, args=(obj.pk,)), urlencode(get)) return url - def get_urls(self): from django.conf.urls import url urlpatterns = super(TranslatableAdmin, self).get_urls() info = self.model._meta.app_label, self.model._meta.model_name return [ - url(r'^(.+)/delete-translation/(.+)/$', - self.admin_site.admin_view(self.delete_translation), - name='%s_%s_delete_translation' % info), - ] + urlpatterns - + url(r'^(.+)/delete-translation/(.+)/$', + self.admin_site.admin_view(self.delete_translation), + name='%s_%s_delete_translation' % info), + ] + urlpatterns + def get_form(self, request, obj=None, change=False, **kwargs): """ Returns a Form class for use in the admin add view. This is used by @@ -146,9 +151,9 @@ def get_form(self, request, obj=None, change=False, **kwargs): else: fields = flatten_fieldsets(self.get_fieldsets(request, obj)) exclude = ( - tuple(self.exclude or ()) + - tuple(kwargs.pop("exclude", ())) + - tuple(self.get_readonly_fields(request, obj) or ()) + tuple(self.exclude or ()) + + tuple(kwargs.pop("exclude", ())) + + tuple(self.get_readonly_fields(request, obj) or ()) ) old_formfield_callback = curry(self.formfield_for_dbfield, request=request) defaults = { @@ -186,10 +191,10 @@ def render_change_form(self, request, context, add=False, change=False, form_url = form_url._replace(query=query.urlencode()).geturl() return super(TranslatableAdmin, self).render_change_form(request, - context, - add, change, - form_url, obj) - + context, + add, change, + form_url, obj) + def response_change(self, request, obj): response = super(TranslatableAdmin, self).response_change(request, obj) if 'Location' in response: @@ -198,9 +203,9 @@ def response_change(self, request, obj): if response['Location'] in (uri, "../add/", self.reverse('admin:%s_%s_add' % (app_label, model_name))): if self.query_language_key in request.GET: response['Location'] = '%s?%s=%s' % (response['Location'], - self.query_language_key, request.GET[self.query_language_key]) + self.query_language_key, request.GET[self.query_language_key]) return response - + @csrf_protect_m @transaction.atomic def delete_translation(self, request, object_id, language_code): @@ -208,17 +213,17 @@ def delete_translation(self, request, object_id, language_code): opts = self.model._meta app_label = opts.app_label translations_model = opts.translations_model - + try: obj = translations_model.objects.select_related('master').get( - master__pk=unquote(object_id), - language_code=language_code) + master__pk=unquote(object_id), + language_code=language_code) except translations_model.DoesNotExist: raise Http404 if not self.has_delete_permission(request, obj): raise PermissionDenied - + if len(obj.master.translations.all_languages()) <= 1: return self.deletion_not_allowed(request, obj, language_code) @@ -226,7 +231,7 @@ def delete_translation(self, request, object_id, language_code): # Populate deleted_objects, a data structure of all related objects that # will also be deleted. - + protected = False if django.VERSION >= (2, 1): deleted_objects, model_count, perms_needed, protected = get_deleted_objects( @@ -234,10 +239,10 @@ def delete_translation(self, request, object_id, language_code): else: deleted_objects, model_count, perms_needed, protected = get_deleted_objects( [obj], translations_model._meta, request.user, self.admin_site, using) - + lang = get_language_info(language_code)['name_local'] - if request.POST: # The user has already confirmed the deletion. + if request.POST: # The user has already confirmed the deletion. if perms_needed: raise PermissionDenied obj_display = u'%s translation of %s' % (force_text(lang), force_text(obj.master)) @@ -245,11 +250,11 @@ def delete_translation(self, request, object_id, language_code): self.delete_model_translation(request, obj) self.message_user(request, - _(u'The %(name)s "%(obj)s" was deleted successfully.') % { - 'name': force_text(opts.verbose_name), - 'obj': force_text(obj_display) - } - ) + _(u'The %(name)s "%(obj)s" was deleted successfully.') % { + 'name': force_text(opts.verbose_name), + 'obj': force_text(obj_display) + } + ) if not self.has_change_permission(request, None): return HttpResponseRedirect(self.reverse('admin:index')) @@ -279,7 +284,7 @@ def delete_translation(self, request, object_id, language_code): "app_label": app_label, }, ) - + def deletion_not_allowed(self, request, obj, language_code): opts = self.model._meta return render( @@ -294,10 +299,10 @@ def deletion_not_allowed(self, request, obj, language_code): 'object_name': force_text(opts.verbose_name), }, ) - + def delete_model_translation(self, request, obj): obj.delete() - + def get_object(self, request, object_id, from_field=None): queryset = self.get_queryset(request) assert isinstance(queryset, TranslationQueryset) @@ -349,10 +354,11 @@ def get_change_form_base_template(self): ] try: return select_template(search_templates) - except TemplateDoesNotExist: #pragma: no cover + except TemplateDoesNotExist: # pragma: no cover return None -#=============================================================================== + +# =============================================================================== class TranslatableInlineModelAdmin(InlineModelAdmin, TranslatableModelAdminMixin): form = InlineModelForm @@ -368,9 +374,9 @@ def get_formset(self, request, obj=None, **kwargs): else: fields = flatten_fieldsets(self.get_fieldsets(request, obj)) exclude = ( - tuple(self.exclude or ()) + - tuple(kwargs.pop("exclude", ())) + - self.get_readonly_fields(request, obj) + tuple(self.exclude or ()) + + tuple(kwargs.pop("exclude", ())) + + self.get_readonly_fields(request, obj) ) defaults = { @@ -395,10 +401,10 @@ def get_urls(self): info = self.model._meta.app_label, self.model._meta.model_name return [ - url(r'^(.+)/delete-translation/(.+)/$', - self.admin_site.admin_view(self.delete_translation), - name='%s_%s_delete_translation' % info), - ] + urlpatterns + url(r'^(.+)/delete-translation/(.+)/$', + self.admin_site.admin_view(self.delete_translation), + name='%s_%s_delete_translation' % info), + ] + urlpatterns def get_form(self, request, obj=None, **kwargs): """ @@ -410,9 +416,9 @@ def get_form(self, request, obj=None, **kwargs): else: fields = flatten_fieldsets(self.get_fieldsets(request, obj)) exclude = ( - tuple(self.exclude or ()) + - tuple(kwargs.pop("exclude", ())) + - self.get_readonly_fields(request, obj) + tuple(self.exclude or ()) + + tuple(kwargs.pop("exclude", ())) + + self.get_readonly_fields(request, obj) ) old_formfield_callback = curry(self.formfield_for_dbfield, request=request) defaults = { @@ -431,21 +437,23 @@ def response_change(self, request, obj): if redirect['Location'] in (uri, "../add/"): if self.query_language_key in request.GET: redirect['Location'] = '%s?%s=%s' % (redirect['Location'], - self.query_language_key, request.GET[self.query_language_key]) + self.query_language_key, request.GET[self.query_language_key]) return redirect def get_queryset(self, request): - qs = self.model._default_manager.all()#.language(language) + qs = self.model._default_manager.all() # .language(language) # TODO: this should be handled by some parameter to the ChangeList. - ordering = getattr(self, 'ordering', None) or () # otherwise we might try to *None, which is bad ;) + ordering = getattr(self, 'ordering', None) or () # otherwise we might try to *None, which is bad ;) if ordering: qs = qs.order_by(*ordering) return qs -#=============================================================================== + +# =============================================================================== class TranslatableStackedInline(TranslatableInlineModelAdmin): template = 'admin/hvad/edit_inline/stacked.html' + class TranslatableTabularInline(TranslatableInlineModelAdmin): template = 'admin/hvad/edit_inline/tabular.html' diff --git a/hvad/fields.py b/hvad/fields.py index 65556cbc..150300a4 100644 --- a/hvad/fields.py +++ b/hvad/fields.py @@ -143,7 +143,7 @@ def get_path_info(self, filtered_relation=None): path = super(SingleTranslationObject, self).get_path_info(filtered_relation) return [path[0]._replace(direct=False)] - def contribute_to_class(self, cls, name, virtual_only=False): + def contribute_to_class(self, cls, name, private_only=False): """ Prevent the field from appearing into the class, we only want it in queries """ super(SingleTranslationObject, self).contribute_to_class(cls, name, False) delattr(cls, self.name)