Readonly для существующих элементов только в Django admin inline
У меня есть встроенная в таблицу модель в администраторе Django. Мне нужно, чтобы одно из полей не изменялось после его создания, но устанавливало его как readonly (через readonly_fields), который отлично работает, но превращает поле в метку при нажатии "Добавить другой элемент" вместо раскрывающегося списка.
Есть ли способ сохранить поле только для чтения, но все же разрешить создание новых элементов с правильным полем ввода?
Спасибо!
Томас
Изменить: удалось определить его с помощью пользовательского виджета
class ReadOnlySelectWidget(forms.Select):
def render(self, name, value, attrs=None):
if value:
final_attrs = self.build_attrs(attrs, name=name)
output = u'<input value="%s" type="hidden" %s />' % (value, flatatt(final_attrs))
return mark_safe(output + str(self.choices.queryset.get(id=value)))
else:
return super(ReadOnlySelectWidget, self).render(name, value, attrs)
Он просто превращает его в скрытое, если есть значение, не будет работать в каждой ситуации (только на самом деле работает только с 1 полем для чтения).
Ответы
Ответ 1
Имея ту же проблему, я наткнулся на это исправление:
Создайте два встроенных объекта, один без прав на изменение, а другой со всеми полями только для чтения. Включите оба в модель администратора.
class SubscriptionInline(admin.TabularInline):
model = Subscription
extra = 0
readonly_fields = ['subscription', 'usedPtsStr', 'isActive', 'activationDate', 'purchaseDate']
def has_add_permission(self, request):
return False
class AddSupscriptionInline(admin.TabularInline):
model = Subscription
extra = 0
fields = ['subscription', 'usedPoints', 'isActive', 'activationDate', 'purchaseDate']
def has_change_permission(self, request, obj=None):
return False
# For Django Version > 2.1 there is a "view permission" that needs to be disabled too (https://docs.djangoproject.com/en/2.2/releases/2.1/#what-s-new-in-django-2-1)
def has_view_permission(self, request, obj=None):
return False
Включите их в ту же модель администратора:
class UserAdmin(admin.ModelAdmin):
inlines = [ AddSupscriptionInline, SubscriptionInline]
Чтобы добавить новую подписку, я использую AddSubscriptionInline
в админке. После сохранения новая подписка исчезает из этой строки, но теперь она появляется в SubscriptionInline
, как только для чтения.
Для SubscriptionInline
важно упомянуть extra = 0
, чтобы он не отображал нежелательные подписки только для чтения. Также лучше скрыть опцию добавления для SubscriptionInline
, чтобы разрешить добавление только через AddSubscriptionInline
, установив has_add_permission
чтобы он всегда возвращал False
.
Совсем не идеально, но это лучший вариант для меня, так как я должен предоставить возможность добавлять подписки на странице администратора пользователя, но после добавления она должна изменяться только через внутреннюю логику приложения.
Ответ 2
Согласно этому сообщению, эта проблема была зарегистрирована как ошибка в Ticket15602.
Обходной путь - переопределить метод clean
встроенной модели в forms.py и вызвать ошибку при изменении существующего встроенного:
class NoteForm(forms.ModelForm):
def clean(self):
if self.has_changed() and self.initial:
raise ValidationError(
'You cannot change this inline',
code='Forbidden'
)
return super().clean()
class Meta(object):
model = Note
fields='__all__'
Вышесказанное дает решение на уровне модели.
Чтобы вызвать ошибку при изменении определенного поля, может помочь метод clean_<field>
. Например, если поле представляет собой ForeignKey
именем category
:
class MyModelForm(forms.Form):
pass # Several lines of code here for the needs of the Model Form
# The following form will be called from the admin inline class only
class MyModelInlineForm(MyModelForm):
def clean_category(self):
category = self.cleaned_data.get('category', None)
initial_value = getattr(
self.fields.get('category', None),
'initial',
None
)
if all(
(
self.has_changed(),
category.id != initial_value,
)
):
raise forms.ValidationError(
_('You cannot change this'),
code='Forbidden'
)
return category
class Meta:
# Copy here the Meta class of the parent model
Ответ 3
Я действительно наткнулся на другое решение, которое, кажется, работает очень хорошо (я не могу взять кредит на себя, но ссылка здесь).
Вы можете определить метод get_readonly_fields
на вашем TabularInline
и установить поля только для чтения соответствующим образом, когда есть объект (редактирование), против, когда его нет (создание).
def get_readonly_fields(self, request, obj=None):
if obj is not None: # You may have to check some other attrs as well
# Editing an object
return ('field_name', )
else:
# Creating a new object
return ()
Это приводит к тому, что целевое поле становится доступным только для чтения при редактировании выходящего экземпляра, в то же время позволяя его редактировать при создании нового экземпляра.
Как указано ниже в комментарии, это не совсем работает, как задумано, потому что переданный obj
является на самом деле родителем... Там есть старый билет на django, который обсуждает это здесь.
Ответ 4
Этот код отлично работает в соответствии с вашими требованиями.
На самом деле я получил этот ответ от своего собственного вопроса, но относящегося к моей проблеме, и я удалил несколько строк, связанных с моей проблемой. И кредит идет на @YellowShark. Проверьте здесь мой вопрос.
После того, как вы создали новый встроенный, вы не сможете редактировать существующий встроенный.
class XYZ_Inline(admin.TabularInline):
model = YourModel
class RequestAdmin(admin.ModelAdmin):
inlines = [XYZ_Inline, ]
# If you wanted to manipulate the inline forms, to make one of the fields read-only:
def get_inline_formsets(self, request, formsets, inline_instances, obj=None):
inline_admin_formsets = []
for inline, formset in zip(inline_instances, formsets):
fieldsets = list(inline.get_fieldsets(request, obj))
readonly = list(inline.get_readonly_fields(request, obj))
prepopulated = dict(inline.get_prepopulated_fields(request, obj))
inline_admin_formset = helpers.InlineAdminFormSet(
inline, formset, fieldsets, prepopulated, readonly,
model_admin=self,
)
if isinstance(inline, XYZ_Inline):
for form in inline_admin_formset.forms:
#Here we change the fields read only.
form.fields['some_fields'].widget.attrs['readonly'] = True
inline_admin_formsets.append(inline_admin_formset)
return inline_admin_formsets
Вы можете добавлять только новые inline и читать только все существующие inline.
Ответ 5
Вот лучший вид только для чтения, который я использовал раньше:
https://bitbucket.org/stephrdev/django-readonlywidget/
from django_readonlywidget.widgets import ReadOnlyWidget
class TestAdmin(admin.ModelAdmin):
def formfield_for_dbfield(self, db_field, **kwargs):
field = super(TestAdmin, self).formfield_for_dbfield(db_field, **kwargs)
if field:
field.widget = ReadOnlyWidget(db_field=db_field)
return field
Ответ 6
Это возможно с патчем обезьяны.
В следующем примере поле "note" будет считано только для существующих объектов AdminNote. В отличие от преобразования полей, которые должны быть скрыты, как предложено в других ответах, это фактически удалит поля из рабочего процесса отправки/проверки (что более безопасно и использует существующие средства визуализации полей).
#
# app/models.py
#
class Order(models.Model):
pass
class AdminNote(models.Model):
order = models.ForeignKey(Order)
time = models.DateTimeField(auto_now_add=True)
note = models.TextField()
#
# app/admin.py
#
import monkey_patches.admin_fieldset
...
class AdminNoteForm(forms.ModelForm):
class Meta:
model = AdminNote
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in self.get_readonly_fields():
del self.fields[field]
def get_readonly_fields(self):
if self.instance.pk:
return ['note']
return []
class AdminNoteInline(admin.TabularInline):
model = AdminNote
form = AdminNoteForm
extra = 1
fields = 'note', 'time'
readonly_fields = 'time',
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
inlines = AdminNoteInline,
#
# monkey_patches/admin_fieldset.py
#
import django.contrib.admin.helpers
class Fieldline(django.contrib.admin.helpers.Fieldline):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if hasattr(self.form, 'get_readonly_fields'):
self.readonly_fields = list(self.readonly_fields) + list(self.form.get_readonly_fields())
django.contrib.admin.helpers.Fieldline = Fieldline