Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 71 additions & 63 deletions hvad/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,30 @@
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
from django.http import Http404, HttpResponseRedirect, QueryDict
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',
Expand All @@ -33,7 +35,8 @@
'InlineModelForm',
)

#===============================================================================

# ===============================================================================

class InlineModelForm(TranslatableModelForm):
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
Expand All @@ -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:
Expand All @@ -78,6 +83,7 @@ def all_translations(self, obj):
entry = u'<strong>%s</strong>' % entry
languages.append(entry)
return u', '.join(languages)

all_translations.allow_tags = True
all_translations.short_description = _(u'all translations')

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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 = {
Expand Down Expand Up @@ -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:
Expand All @@ -198,58 +203,58 @@ 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):
"The 'delete translation' admin view for this model."
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)

using = router.db_for_write(translations_model)

# 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(
[obj], request, self.admin_site)
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))
self.log_deletion(request, obj, obj_display)
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'))
Expand Down Expand Up @@ -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(
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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 = {
Expand All @@ -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):
"""
Expand All @@ -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 = {
Expand All @@ -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'
2 changes: 1 addition & 1 deletion hvad/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down