django custom form - removing fields based on user

When logged on as a superuser, I want the edit form to have access to ‘span’ field.

What am I doing wrong here?

class ArticleEditForm(forms.ModelForm):
    
    class Meta:
        model = Article
        fields =    ('title','category','intro','content','image',)
        
         
    def __init__(self, *args, **kwargs):
            
            user = kwargs.pop('user', None)

            if  user.is_superuser:
               self.Meta.fields['span'] = forms.CharField()
           
            super().__init__(*args, **kwargs)

I realise this is because you can’t change the tuple. How do I do this?

I managed previously to do this on the model, before I installed ckeditor, and now since I use the custom form, I can’t get it working.

I’ve also tried to change self.Meta in the constructor, but that didn’t seem to do anything.

self.Meta.fields =    ('title','category','intro','content','image', 'span',)

my very basic knowledge of python inner classes, and Django’s class hierarchy is letting me down.

my view code

class Article_Update(LoginRequiredMixin,UserPassesTestMixin,UpdateView):
    template_name = "articles/article_update.html"
    form_class = ArticleEditForm
    model = Article
    #fields =    ('title','category','intro','content','image',)
    def test_func(self):
        article = self.get_object()
        if self.request.user == article.author or self.request.is_superuser :
           
           #self.fields =  ('title','category','intro','content','image','span') 
           return True
        return False


    # Sending user object to the form, to verify which fields to display/remove (depending on group)
    def get_form_kwargs(self):
        kwargs = super().get_form_kwargs()
        kwargs.update({'user': self.request.user})
        return kwargs

It might help to consider what is what. I’ll try to explain things as I understand them, but anyone is very welcome to correct me where I’m wrong.

Meta class

When you define a class (e. g. Meta) inside of a class’ (e. g. ArticleEditForm) definition, the inner class is bound to the outer class itself. It’s similar to class attributes in the sense that upon generating instances, there is no new “copy” of the inner class being formed. So for example, if you do this…

f1 = MyForm()
f2 = MyForm()

… then f1.Meta and f2.Meta both refer to the same thing/class. This becomes clear if you do the following:

f1.Meta is f2.Meta # True
f1.Meta is MyForm.Meta

the point of the Meta class is to describe “meta characteristics” of the outer class (Form) itself and, by extension, all instances of the outer class (forms). The Meta class cannot be used for changing the behavior of individual instances. So for example:

f1.Meta.fields = ['bar', 'baz']

since f1.Meta refers to the same class as e. g. f2.Meta or MyForm.Meta, doing something like this affects the behavior/meta characteristics of MyForm itself and all current or future instances (forms).

Meta.fields

The Meta.fields attribute isn’t itself a collection of fields. It’s just meta data, saying which of the model’s (Article) fields that Form instances’ field collections (dictionaries, with field names as keys and Field instances as values) should be based on. It’s like part of a blueprint, a specification, for how individual forms’ collections of fields should be constructed. So trying to add an actual field (e. g. a forms.CharField instance) to this specification doesn’t make sense. This also follows from what Meta actually is, as described above. Speaking of which, based on what I’ve written so far you might want to ponder why it makes sense to force Meta.fields to be immutable (i. e. a tuple).

form_instance.fields

What you actually want is to include an additional field (a forms.CharField instance) for certain ArticleEditForm instances. If we look in Django’s code base, where the BaseForm class is defined, specifically its __init__ method, we can read this (around line 82):

        # The base_fields class attribute is the *class-wide* definition of
        # fields. Because a particular *instance* of the class might want to
        # alter self.fields, we create self.fields here by copying base_fields.
        # Instances should always modify self.fields; they should not modify
        # self.base_fields.
        self.fields = copy.deepcopy(self.base_fields)

This is more or less the answer to your question. You should be looking at modifying ArticleEditForm instances by modifying their .fields attribute.

Hopefully you can now see why a possible solution, or at least something that’s closer to what you want, might be:

class ArticleEditForm(forms.ModelForm):
    """
    [descriptive docstring...]
    """
    class Meta:
        model = Article
        fields =    ('title','category','intro','content','image',)


    def __init__(self, *args, **kwargs):
        user = kwargs.pop('user', None)
        super().__init__(*args, **kwargs)
        
        if  user.is_superuser:
            self.fields['span'] = forms.CharField()

An alternative, possibly slightly more efficient (someone more knowledgeable please chime in on this) solution might be to create a ArticleSuperEditForm class that inherits from ArticleEditForm and has a modified Meta class, and then you put logic for deciding which kind of form to use in your views. I don’t know if this difference is large enough to be meaningful to the point where you would consider going with this less concise and less “fat model”-based solution, but I’m guessing no.

If you want to properly learn about OOP in Python, you might like this tutorial series by coreyms. I’ve only watched a couple of videos of it (having learned OOP from various other sources), but I’ve done the same author’s series on Django and it was very good.

Hi there, thanks for the reply. I did finally get this working.

class Article_Update(LoginRequiredMixin,UserPassesTestMixin,UpdateView):

    template_name = "articles/article_update.html"
   # form_class = ArticleEditForm
    model = Article
   # fields =    ('title','category','intro','content','image',)
    def test_func(self):
        article = self.get_object()
         
        if self.request.user == article.author or self.request.user.is_superuser :
           return True
        return False
    
    def dispatch(self, request, *args, **kwargs):
        # Check permissions for the request.user here
        article = self.get_object()
        self.fields = ['title','category','intro','content','image',]
        if request.user.is_superuser :
            self.fields += ['span']   #denoting number of columns taken up
        
        return super().dispatch(request, *args, **kwargs)

I have no idea if this is the ‘gold-standard’ way to do it, but it works. I basically saw something on Stack exchange which led me to the solution.

As you can see, I took off the form class, because I realised I didn’t need to create my own. face palm. But what you’ve given me will help when I do.

I really do need get a better understanding of inner classes.