Modifying a form's fields after user authentication - django

I have a view that inherits from UpdateView and UserPassesTestMixin. Two types of user can access it - either a superuser or a normal one. I determine which one it is in test_func() and store the result in self.superuser. Now I want to modify the displayed form depending on the level of the user (namely disable almost all fields in the form if the current user is not a superuser, otherwise leave them all enabled). Which view method should I override to modify the view's form (in this case disable the necessary fields)? Or is this idea wrong and I should approach this differently?

You can pass the data you need to recognize the user to the form and in your form, you can get the data in the __init__ then you can change the self.fields in there to reflect the type of user.
Check this example that removes the ReadOnly to the field superuser_field if the form is instantiated for a superuser.
class MyForm(forms.Form):
def __init__(self, is_superuser=False, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
if is_superuser:
self.fields['superuser_field'].disabled = False
In your code you can, if I understood correctly:
def yourview(...):
# ...
myform = MyForm(is_superuser=self.superuser)
# ....

Related

Django inline model formset and inline model form handle initial differently

I'm using a StackedInline to populate a OneToOneField, but I want to make it optional, so I set min_num=0 and extra = 0.
I also want to include default values when the user presses the "➕ Add another" button.
class MyForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
kwargs['initial'] = {'hello': 'world'}
class MyInline(admin.StackedInline):
form = MyForm
extra = 0
min_num=0
This works. When someone presses the "➕ Add another" button, the hello field is populated with world.
I also want to and do some custom validation, so it looks like I have to use BaseInlineFormSet. I moved the initial stuff to MyFormSet.__init__
class MyFormSet(BaseInlineFormSet):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not self.forms:
return
self.forms[0].fields['hello'].initial = 'world'
def clean(self):
# My custom validation
class MyInline(admin.StackedInline):
formset = MyFormSet
extra = 0
min_num=0
But the initial values are no longer populated when the user presses the "➕ Add another" button, only when the form is initially displayed with extra = 1.
Is there another thing I need to do in MyFormSet to preserve the MyForm behavior?
The problem
You can't try and set the initial property in the __init__ of your formset, since self.forms[0] doesn't even exist at that point. The correct place to do this is (as in your first) example, in the init method of your MyForm.
The solution
What you can do is use MyFormSet to implement your custom validation using clean, and use MyForm to add the logic about default values, then:
from django.forms import formset_factory
class MyInline(admin.StackedInline):
form = Myform
formset = MyFormSet
extra = 0
min_num=0

Get request in form field validation

I have a form, where validation depends on logged user. For some users are certain values valid, for other users they are invalid. What is valid and what is invalid is dynamic - I can't create new form for each user group.
What's more I need this same validation in more forms, so I created custom form field. To this custom form field I need to pass user instance somehow to check if the value is valid or not.
How to do this?
I am doing it like that:
class EventForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
# doing stuff with the user…
super(EventForm, self).__init__(*args, **kwargs)
In your view class/method you have to instantiate the form like this:
form = EventForm(user=request.user, data=request.POST)
You don't need an extra field for this.
You can access the current user by passing it explicitly or by fetching it from the request in your form's init() method.
Then you can use the retrieved value when cleaning your form.
If you need this functionality in several forms I'd create either a base class that the specialized forms inherit from or create a mixin that adds the desired functionality.

Django admin change to_python output based on request

I'm wondering how to change the behavior of a form field based on data in the request... especially as it relates to the Django Admin. For example, I'd like to decrypt a field in the admin based on request data (such as POST or session variables).
My thoughts are to start looking at overriding the change_view method in django/contrib/admin/options.py, since that has access to the request. However, I'm not sure how to affect how the field value displays the field depending on some value in the request. If the request has the correct value, the field value would be displayed; otherwise, the field value would return something like "NA".
My thought is that if I could somehow get that request value into the to_python() method, I could directly impact how the field is displayed. Should I try passing the request value into the form init and then somehow into the field init? Any suggestions how I might approach this?
Thanks for reading.
In models.py
class MyModel(models.Model):
hidden_data = models.CharField()
In admin.py
class MyModelAdmin(models.ModelAdmin):
class Meta:
model = MyModel
def change_view(self, request, object_id, extra_context=None):
.... # Perhaps this is where I'd do a lot of overriding?
....
return self.render_change_form(request, context, change=True, obj=obj)
I haven't tested this, but you could just overwrite the render_change_form method of the ModelAdmin to sneak in your code to change the field value between when the change_view is processed and the actual template rendered
class MyModelAdmin(admin.ModelAdmin):
...
def render_change_form(self, request, context, **kwargs):
# Here we have access to the request, the object being displayed and the context which contains the form
form = content['adminform'].form
field = form.fields['field_name']
...
if 'obj' in kwargs:
# Existing obj is being saved
else:
# New object is being created (an empty form)
return super(MyModelAdmin).render_change_form(request, context, **kwargs)

trying to get request.user, and then a query, in a form that overrides ModelChoiceField and is subclassed

I need to pass an instance variable (self.rank) to be used by a class variable (provider) (see the commented out line below).
Commented out, the code below works. But I'm pretty sure I shouldn't be trying to pass an instance variable up to a class variable anyway. So I'm dumbfounded as to how to accomplish my goal, which is to dynamically filter down my data in the ModelChoiceField.
As you can see, I already overrided ModelChoiceField so I could beautify the usernames. And I also subclassed my basic SwapForm because I have several other forms I'm using (not shown here).
Another way of saying what I need ... I want the value of request.user in my Form so I can then determine the rank of that user and then filter out my Users by rank to build a smaller ModelChoiceField (that looks good too). Note that in my views.py, I call the form using:
form = NewSwapForm(request.user)
or
form = NewSwapForm(request.user, request.POST)
In forms.py:
from myapp.swaps.models import Swaps
from django.contrib.auth.models import User
class UserModelChoiceField(forms.ModelChoiceField):
""" Override the ModelChoiceField to display friendlier name """
def label_from_instance(self, obj):
return "%s" % (obj.get_full_name())
class SwapForm(forms.ModelForm):
""" Basic form from Swaps model. See inherited models below. """
class Meta:
model = Swaps
class NewSwapForm(SwapForm):
# Using a custom argument 'user'
def __init__(self, user, *args, **kwargs):
super(NewSwapForm, self).__init__(*args, **kwargs)
self.rank = User.objects.get(id=user.id).firefighter_rank_set.get().rank
provider = UserModelChoiceField(User.objects.all().
order_by('last_name').
filter(firefighter__hirestatus='active')
### .filter(firefighter_rank__rank=self.rank) ###
)
class Meta(SwapForm.Meta):
model = Swaps
fields = ['provider', 'date_swapped', 'swap_shift']
Thanks!
You can't do it that way, because self doesn't exist at that point - and even if you could, that would be executed at define time, so the rank would be static for all instantiations of the form.
Instead, do it in __init__:
provider = UserModelChoiceField(User.objects.none())
def __init__(self, user, *args, **kwargs):
super(NewSwapForm, self).__init__(*args, **kwargs)
rank = User.objects.get(id=user.id).firefighter_rank_set.get().rank # ??
self.fields['provider'].queryset = User.objects.order_by('last_name').filter(
firefighter__hirestatus='active', firefighter_rank__rank=rank)
I've put a question mark next to the rank line, because rank_set.get() isn't valid... not sure what you meant there.

How to limit fields in django-admin depending on user?

I suppose similar problem would have been discussed here, but I couldn't find it.
Let's suppose I have an Editor and a Supervisor. I want the Editor to be able to add new content (eg. a news post) but before publication it has to be acknowledged by Supervisor.
When Editor lists all items, I want to set some fields on the models (like an 'ack' field) as read-only (so he could know what had been ack'ed and what's still waiting approval) but the Supervisor should be able to change everything (list_editable would be perfect)
What are the possible solutions to this problem?
I think there is a more easy way to do that:
Guest we have the same problem of Blog-Post
blog/models.py:
Class Blog(models.Model):
...
#fields like autor, title, stuff..
...
class Post(models.Model):
...
#fields like blog, title, stuff..
...
approved = models.BooleanField(default=False)
approved_by = models.ForeignKey(User)
class Meta:
permissions = (
("can_approve_post", "Can approve post"),
)
And the magic is in the admin:
blog/admin.py:
...
from django.views.decorators.csrf import csrf_protect
...
def has_approval_permission(request, obj=None):
if request.user.has_perm('blog.can_approve_post'):
return True
return False
Class PostAdmin(admin.ModelAdmin):
#csrf_protect
def changelist_view(self, request, extra_context=None):
if not has_approval_permission(request):
self.list_display = [...] # list of fields to show if user can't approve the post
self.editable = [...]
else:
self.list_display = [...] # list of fields to show if user can approve the post
return super(PostAdmin, self).changelist_view(request, extra_context)
def get_form(self, request, obj=None, **kwargs):
if not has_approval_permission(request, obj):
self.fields = [...] # same thing
else:
self.fields = ['approved']
return super(PostAdmin, self).get_form(request, obj, **kwargs)
In this way you can use the api of custom permission in django, and you can override the methods for save the model or get the queryset if you have to. In the methid has_approval_permission you can define the logic of when the user can or can't to do something.
Starting Django 1.7, you can now use the get_fields hook which makes it so much simpler to implement conditional fields.
class MyModelAdmin(admin.ModelAdmin):
...
def get_fields(self, request, obj=None):
fields = super(MyModelAdmin, self).get_fields(request, obj)
if request.user.is_superuser:
fields += ('approve',)
return fields
I have a system kind of like this on a project that I'm just finishing up. There will be a lot of work to put this together, but here are some of the components that I had to make my system work:
You need a way to define an Editor and a Supervisor. The three ways this could be done are 1.) by having an M2M field that defines the Supervisor [and assuming that everyone else with permission to read/write is an Editor], 2.) make 2 new User models that inherit from User [probably more work than necessary] or 3.) use the django.auth ability to have a UserProfile class. Method #1 is probably the most reasonable.
Once you can identify what type the user is, you need a way to generically enforce the authorization you're looking for. I think the best route here is probably a generic admin model.
Lastly you'll need some type of "parent" model that will hold the permissions for whatever needs to be moderated. For example, if you had a Blog model and BlogPost model (assuming multiple blogs within the same site), then Blog is the parent model (it can hold the permissions of who approves what). However, if you have a single blog and there is no parent model for BlogPost, we'll need some place to store the permissions. I've found the ContentType works out well here.
Here's some ideas in code (untested and more conceptual than actual).
Make a new app called 'moderated' which will hold our generic stuff.
moderated.models.py
class ModeratedModelParent(models.Model):
"""Class to govern rules for a given model"""
content_type = models.OneToOneField(ContentType)
can_approve = models.ManyToManyField(User)
class ModeratedModel(models.Model):
"""Class to implement a model that is moderated by a supervisor"""
is_approved = models.BooleanField(default=False)
def get_parent_instance(self):
"""
If the model already has a parent, override to return the parent's type
For example, for a BlogPost model it could return self.parent_blog
"""
# Get self's ContentType then return ModeratedModelParent for that type
self_content_type = ContentType.objects.get_for_model(self)
try:
return ModeratedModelParent.objects.get(content_type=self_content_type)
except:
# Create it if it doesn't already exist...
return ModeratedModelParent.objects.create(content_type=self_content_type).save()
class Meta:
abstract = True
So now we should have a generic, re-usable bit of code that we can identify the permission for a given model (which we'll identify the model by it's Content Type).
Next, we can implement our policies in the admin, again through a generic model:
moderated.admin.py
class ModeratedModelAdmin(admin.ModelAdmin):
# Save our request object for later
def __call__(self, request, url):
self.request = request
return super(ModeratedModelAdmin, self).__call__(request, url)
# Adjust our 'is_approved' widget based on the parent permissions
def formfield_for_dbfield(self, db_field, **kwargs):
if db_field.name == 'is_approved':
if not self.request.user in self.get_parent_instance().can_approve.all():
kwargs['widget'] = forms.CheckboxInput(attrs={ 'disabled':'disabled' })
# Enforce our "unapproved" policy on saves
def save_model(self, *args, **kwargs):
if not self.request.user in self.get_parent_instance().can_approve.all():
self.is_approved = False
return super(ModeratedModelAdmin, self).save_model(*args, **kwargs)
Once these are setup and working, we can re-use them across many models as I've found once you add structured permissions for something like this, you easily want it for many other things.
Say for instance you have a news model, you would simply need to make it inherit off of the model we just made and you're good.
# in your app's models.py
class NewsItem(ModeratedModel):
title = models.CharField(max_length=200)
text = models.TextField()
# in your app's admin.py
class NewsItemAdmin(ModeratedModelAdmin):
pass
admin.site.register(NewsItem, NewsItemAdmin)
I'm sure I made some code errors and mistakes in there, but hopefully this can give you some ideas to act as a launching pad for whatever you decide to implement.
The last thing you have to do, which I'll leave up to you, is to implement filtering for the is_approved items. (ie. you don't want un-approved items being listed on the news section, right?)
The problem using the approach outlined by #diegueus9 is that the ModelAdmin acts liked a singleton and is not instanced for each request. This means that each request is modifying the same ModelAdmin object that is being accessed by other requests, which isn't ideal. Below is the proposed solutions by #diegueus9:
# For example, get_form() modifies the single PostAdmin's fields on each request
...
class PostAdmin(ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if not has_approval_permission(request, obj):
self.fields = [...] # list of fields to show if user can't approve the post
else:
self.fields = ['approved', ...] # add 'approved' to the list of fields if the user can approve the post
...
An alternative approach would be to pass fields as a keyword arg to the parent's get_form() method like so:
...
from django.contrib.admin.util import flatten_fieldsets
class PostAdmin(ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if has_approval_permission(request, obj):
fields = ['approved']
if self.declared_fieldsets:
fields += flatten_fieldsets(self.declared_fieldsets)
# Update the keyword args as needed to allow the parent to build
# and return the ModelForm instance you require for the user given their perms
kwargs.update({'fields': fields})
return super(PostAdmin, self).get_form(request, obj=None, **kwargs)
...
This way, you are not modifying the PostAdmin singleton on every request; you are simply passing the appropriate keyword args needed to build and return the ModelForm from the parent.
It is probably worth looking at the get_form() method on the base ModelAdmin for more info: https://code.djangoproject.com/browser/django/trunk/django/contrib/admin/options.py#L431