【问题标题】:Readonly for existing items only in Django admin inline仅在 Django admin inline 中对现有项目只读
【发布时间】:2011-08-02 21:52:00
【问题描述】:

我在 Django 管理员中有一个表格内联模型。我需要其中 1 个字段在创建后不可更改,但将其设置为只读(通过 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 个只读字段)。

【问题讨论】:

  • 将您的解决方案添加为答案,以便问题显示为已回答:)

标签: django django-admin


【解决方案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

一点都不完美,但它对我来说是最好的选择,因为我必须提供在用户管理页面上添加订阅的功能,但是添加后,只能通过内部应用逻辑进行更改。

【讨论】:

  • 我做了类似的事情,但修改了新记录的内联查询集(示例中的 AddSubscriptionInline):def queryset(self, request): return super(AddSubscriptionInline, self).queryset(request).none() 并将 max_num = 0 添加到现有记录的内联(示例中的 SubscriptionInline) .
  • 我也使用了这个解决方案。它像魔术一样工作。我见过的最优雅的 hack 之一!
  • 我在 django admin 中见过的 Awsom hack。非常感谢!!
  • 我的关键是在AddSubscriptionInline 中使用extra = 1max_num = 1,因为它不包括表单下方的“添加另一个...”链接。
【解决方案2】:

你可以像这样只用一个内联来实现这一点:

class MyInline(admin.TabularInline):
    fields = [...]
    extra = 0

    def has_change_permission(self, request, obj):
        return False

【讨论】:

  • 非常感谢。这是这里涉及最少的答案,而且效果很好。
  • 很棒的答案,这就是我想要的。从 Django 3.x 开始,args 应该是 (self, request, obj)(假设是相关模型)并且只需要 has_change_permission
  • 感谢@MattSanders -- 更新了 Django 3.x 的答案
  • 有点老套,但这行得通!谢谢@Travis Lloyd
【解决方案3】:

我实际上遇到了另一个似乎非常有效的解决方案(我不能为此归功于link here)。

您可以在TabularInline 上定义get_readonly_fields 方法,并在有对象(编辑)与没有对象(创建)时适当地设置只读字段。

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 票在讨论这个here

【讨论】:

  • 不幸的是,这并不像这里描述的那样工作;传入的obj 不是“内联”模型的实例,而是父/相关模型的实例。老实说,我有点认为这是一个 Django 错误。
  • 是的,我意识到,在我尝试实现之后,这是一种不同的情况。在一种情况下,我能够解决问题,但在另一种情况下,我不能。我更新了帖子并添加了讨论此问题的 django 票证的链接。
【解决方案4】:

根据this post,此问题已报告为Ticket15602 中的错误。

解决方法是覆盖 forms.py 中内联模型的 clean 方法,并在更改现有内联时引发错误:

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_&lt;field&gt; 方法可以提供帮助。例如,如果字段是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 

【讨论】:

    【解决方案5】:

    此代码完全符合您的要求。

    实际上,我从我自己的问题中得到了这个答案,但具体到我的问题,我删除了一些与我的问题相关的行。归功于@YellowShark。 Check here my question.

    创建新内联后,您将无法编辑现有内联。

    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
    

    您只能添加新的内联并只读所有现有的内联。

    【讨论】:

      【解决方案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
      

      【讨论】:

      • 在我尝试这个的情况下,它在表单验证期间给了我一个 ValueError:Cannot assign None: "MyModel.field" does not allow null values.
      【解决方案7】:

      这是我以前用过的更好的只读小部件:
      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
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2013-12-16
        • 2011-03-08
        • 1970-01-01
        • 2012-11-28
        • 2019-02-17
        • 2021-05-12
        • 1970-01-01
        相关资源
        最近更新 更多