I have a modified admin form, where I added a field that shall modify the values of the current model's parent object. Now, depending on the user, I need to
alter the queryset of that extra field
set another field as readonly (or better, even hide it completely)
Basically my code below works as I'd expect it. A superuser gets the whole queryset and the other field is not readonly. All other users get a limited queryset and the other field is readonly. However, once I open that site in a different browser and as a non-superuser, even the superuser get the same result as the non-superusers. Seems like django somehow caches the result? If I put some print statements inside the conditional branches though, they get printed correctly. So the method still gets called each time and seems to still perform these steps. Only with a wrong outcome.
Is that a caching problem? Am I doing something entirely wrong? Can it be a bug in the django test server?
def get_form(self, request, obj=None, **kwargs):
form = super(MultishopProductAdmin, self).get_form(request, obj, **kwargs)
if obj is not None:
form.declared_fields['categories'].initial = obj.product.category.all()
if not request.user.is_superuser:
user_site = request.user.get_profile().site
form.declared_fields['categories'].queryset = Category.objects.filter(site__id=user_site.id)
self.readonly_fields = ('virtual_sites', )
if obj is not None:
form.declared_fields['categories'].initial = obj.product.category.filter(site__id=user_site.id)
return form
Yes you are doing it wrong. In Django 1.2+ you can use get_readonly_fields.
From this answer:
The ModelAdmin is only instantiated once for all requests that it receives. So when you define the readonly fields like that, you're setting it across the board permanently.
Regarding altering the queryset. From the documentation:
class MyModelAdmin(admin.ModelAdmin):
def queryset(self, request):
qs = super(MyModelAdmin, self).queryset(request)
if request.user.is_superuser:
return qs
return qs.filter(author=request.user)
To expand on dan-klasson's anwer: Do not ever set instance attributes in any ModelAdmin method (like self.readonly_fields) to prevent issues like the one you described. Only use Django ModelAdmin's options (which are class-level) or methods to manipulate any behavior. Regarding readonly fields, you can use the ModelAdmin.get_readonly_fields method which has this signature: get_readonly_fields(self, request, obj=None).
So I couldn't find a really clever way to do what I wanted with django admin's custom methods. What I ended up doing now is implementing the admin's change_view, setting up my own form manually and performing all my custom initializations from there.
I then provided a custom template by setting change_form_template, which is simply extending admin/change_form.html but rendering my own form instead of the default one. I also set extra_context['adminform'] = None so the default admin form gets removed.
That way I can now customize my form the way I need it to be but still use all the other admin conveniences. So far it seems to work very nicely. Not the very most elegant solution either I think, but the best I could think of.
Related
Using Django, I have a page with a form that is rendered using CreateView. The success_url is the same url as the original form. When the form is submitted and is valid, I want to insert some data into the context.
I'd like to know the most flexible and clear way to add this context data, because the example below (which works) feels sub-optimal for such a (presumably) common task. I'm looking for a more general solution that using SuccessMessageMixin.
class DemoView(CreateView):
template_name = "request-demo.html"
form_class = DemoForm
success_url = reverse_lazy("base:demo")
def form_valid(self, form):
create_and_send_confirmation_email(form)
return self.render_to_response(self.get_context_data(success_message=True))
Most generically you will define the succcess_url to be the DetailView of the object you have just created. In fact, if you do not define a success_url at all, the default get_success_url will return
url = self.object.get_absolute_url()
which is commonly the same thing.
If you want to do something special for a newly created object you might
Redirect to a different URL, such as a subclass of your object DetailView with a custom get_context_data method and/or template name. Or possibly, somewhere completely differerent.
Interrogate the newly created object (in the template and/or get_context_data method) to establish in some application-specific way that it is indeed newly created.
I have a function-based view that is currently working successfully. However, I want to learn how to create the equivalent Class Based View version of this function, using the generic UpdateView class -- though I imagine the solution, whatever it is, will be the exact same for CreateView, as well.
I know how to create and use Class Based Views generally, but there is one line of my function-based view that I have not been able to work into the corresponding UpdateView -- as usual with the Generic Editing Class Based Views, it's not immediately clear which method I need to override to insert the desired functionality.
The specific task that I can't port-over to the CBV, so to speak, is a line that overrides the queryset that will be used for the display of a specific field, one that is defined as ForeignKey to another model in my database.
First, the working function-based view, with highlight at the specific bit of code I can't get working in the CVB version:
#login_required
def update_details(request, pk):
"""update details of an existing record"""
umd_object = UserMovieDetail.objects.select_related('movie').get(pk=pk)
movie = umd_object.movie
if umd_object.user != request.user:
raise Http404
if request.method != 'POST':
form = UserMovieDetailForm(instance=umd_object)
# this is the single line of code I can't get working in UpdateView version:
form.fields['user_guess'].queryset = User.objects.filter(related_game_rounds=movie.game_round)
else:
form = UserMovieDetailForm(instance=umd_object, data=request.POST)
if form.is_valid():
form.save()
return redirect(movie)
context = {'form': form, 'object': umd_object }
return render(request, 'movies/update_details.html', context)
I can recreate every part of this function-based view in UpdateView successfully except for this line (copied from above for clarity):
form.fields['user_guess'].queryset = User.objects.filter(related_game_rounds=movie.game_round)
What this line does: the default Form-field for a ForeignKey is ModelChoiceField, and it by default displays all objects of the related Model. My code above overrides that behavior, and says: I only want the form to display this filtered set of objects. It works fine, as is, so long as I'm using this function-based view.
Side-Note: I am aware that this result can be achieved by modifying the ModelForm itself in my forms.py file. The purpose of this question is to better understand how to work with the built-in Generic Class Based Views, enabling them to recreate the functionality I can already achieve with function-based views. So please, refrain from answering my question with "why don't you just do this in the form itself instead" -- I am already aware of this option, and it's not what I'm attempting to solve, specifically.
Now for the UpdateView (and again, I think it would be the same for CreateView). To start off, it would look essentially like this:
class UpdateDetailsView(LoginRequiredMixin, UpdateView):
model = UserMovieDetail
template_name = 'movies/update_details.html'
form_class = UserMovieDetailForm
login_url = 'login' # used by LoginRequiredMixin
# what method do I override here, to include that specific line of code, which needs
# to occur in the GET portion of the view?
def get_success_url(self):
return reverse('movies:movie', kwargs={'pk': self.object.movie.pk, 'slug': self.object.movie.slug })
The above is a working re-creation of my function-based view, replicating all the behavior except that one important line that filters the results of a specific field's ModelChoiceField display in the Form.
How do I get that line of code to function inside this UpdateView? I've reviewed the methods built-in to UpdateView on the classy class-based views website, and then attempted (by pure guess-work) to over-ride the get_form_class method, but I it didn't work, and I was basically shooting in the dark to begin with.
Note that since the functionality I want to re-create is about the display of items in ModelChoiceField of the form, the desired behavior applies to the GET portion of the view, rather than the POST. So I need to be able to override the form fields before the form is rendered for the first time, just like I did in my function based view. Where and how can I do this in UpdateView?
First, a note not related to form - from raise Http404 in functional view I understand that you want to allow user to access only his own movies. For that in class based view you can override get_queryset method:
class UpdateDetailsView(LoginRequiredMixin, UpdateView):
def get_queryset(self):
return UserMovieDetail.objects \
.filter(user=request.user) \
.select_related('movie')
Now let's move to customizing form.
Option 1 - .get_form()
You can override get_form method of the UpdateView:
class UpdateDetailsView(LoginRequiredMixin, UpdateView):
form_class = UserMovieDetailForm
def get_form(self, form_class=None):
form = super().get_form(form_class)
# add your customizations here
round = self.object.movie.game_round
form.fields['user_guess'].queryset = \
User.objects.filter(related_game_rounds=round)
return form
Option 2 - moving customizations to form class and .get_form_kwargs()
You might prefer to move customization logic from view to form. For that you can override form's __init__ method. If customization logic requires extra information (for example, queryset depends on current user), then you can also override get_form_kwargs method to pass extra parameters to the form:
# views.py
class UpdateDetailsView(LoginRequiredMixin, UpdateView):
form_class = UserMovieDetailForm
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs.update({'current_user': self.request.user})
return kwargs
# forms.py
class UserMovieDetailForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.current_user = kwargs.pop('current_user')
super().__init__(*args, **kwargs)
# add your customizations here
self.fields['user_guess'].queryset = ...
P.S. In general, great resource for understanding django class based views is https://ccbv.co.uk/
I have a Post model with a boolean field, is_draft. If the user is logged and is member of the staff, then I want to displays all posts; otherwise, only the posts for which is_draft is false.
My first thought was to use a custom model manager and use request.user as following:
class PostManager(models.Manager):
def get_queryset(self):
if request.user and request.user.is_staff:
return super().get_queryset()
else:
return super().get_queryset().filter(is_draft=False)
but request is not available in the model.
I saw some people use a middleware to access the user in the model. It seems to me the safest way to address my need (ie it's done once, in the custom manager; people modifying the views.py don't have to bother, no risk of forgetting it) but this solution seems controversial.
I have of course the possibility to add some logic in the view, but I feel this is not DRY (as I query Post many times) nor safe (risk somebody forgets it at some point).
According to you, what would be the cleanest and safest way to address this need?
PS: I've found a similar question for which a solution was proposed for Class-based views, but my views are function-based.
Thanks a lot!
You can filter with a custom function in the manager.
class PostManager(models.Manager):
def get_posts(self, user):
if user.is_staff:
return super().get_queryset()
else:
return super().get_queryset().filter(is_draft=False)
And in your View, get the queryset using:
qs = Post.objects.get_posts(request.user)
My simple form looks like this:
class PropertyFilterForm(forms.Form):
property_type = forms.ModelChoiceField(queryset=Property.objects.values_list('type', flat=True).order_by().distinct())
property_type returns a flat list of String values to the drop-down list in my template.
Now, when i choose one of the values and hit "Submit" - i get the following error:
Select a valid choice. That choice is not one of the available
choices.
My view at the moment looks like this:
def index(request):
if request.method == 'POST':
form = PropertyFilterForm(request.POST)
if form.is_valid():
selected_type = form.cleaned_data['property_type']
properties = Property.objects.filter(type=selected_type)
else:
form = PropertyFilterForm()
properties = Property.objects.all()
return render(request, 'index.html', context=locals())
I read and re-read this question many times. It seems to be the same thing, but still I wasn't able to figure out the exact solution.
What I understood so far, is that I need to either explicitly specify a queryset for my form each time I call it in the view, or (better) specify the queryset in the init method of my form.
Could please someone elaborate on whether we need to specify a queryset the way i described above?
If yes, why? Haven't we already specified it in the form definition?
I would be really grateful for any code snippets
You want the user to select a string, not a property instance, so I think it would be a better fit to use a ChoiceField instead of a ModelChoiceField.
class PropertyFilterForm(forms.Form):
property_type = forms.ChoiceField(choices=[])
def __init__(self, *args, **kwargs):
super(PropertyFilterForm, self).__init__(*args, **kwargs)
self.fields['property_type'].choices = Property.objects.values_list('type', 'type').order_by('type').distinct()
The disadvantage of using a ChoiceField is we need to generate the choices in the __init__ method of the form. We have lost the nice functionality of the ModelChoiceField, where the queryset is evaluated every time the form is created.
It's not clear to me why Daniel recommended sticking with ModelChoiceField rather than ChoiceField. If you were to use ModelChoiceField, I think you'd have to subclass it and override label_from_instance. As far as I know, using values() is not going to work.
To specify the initial value, you can either hardcode it in the form definition,
class PropertyFilterForm(forms.Form):
property_type = forms.ChoiceField(choices=[], initial='initial_type')
or set it in the __init__ method,
self.fields['property_type'].initial = 'initial_type'
or provide it when instantiating the form:
form = PropertyFilterForm(request.POST, initial={'property_type': 'initial_type'})
Is there a way to make a model read-only in the django admin? but I mean the whole model.
So, no adding, no deleting, no changing, just see the objects and the fields, everything as read-only?
ModelAdmin provides the hook get_readonly_fields() - the following is untested, my idea being to determine all fields the way ModelAdmin does it, without running into a recursion with the readonly fields themselves:
from django.contrib.admin.util import flatten_fieldsets
class ReadOnlyAdmin(ModelAdmin):
def get_readonly_fields(self, request, obj=None):
if self.declared_fieldsets:
fields = flatten_fieldsets(self.declared_fieldsets)
else:
form = self.get_formset(request, obj).form
fields = form.base_fields.keys()
return fields
then subclass/mixin this admin whereever it should be a read-only admin.
For add/delete, and to make their buttons disappear, you'll probably also want to add
def has_add_permission(self, request):
# Nobody is allowed to add
return False
def has_delete_permission(self, request, obj=None):
# Nobody is allowed to delete
return False
P.S.: In ModelAdmin, if has_change_permission (lookup or your override) returns False, you don't get to the change view of an object - and the link to it won't even be shown. It would actually be cool if it did, and the default get_readonly_fields() checked the change permission and set all fields to readonly in that case, like above. That way non-changers could at least browse the data... given that the current admin structure assumes view=edit, as jathanism points out, this would probably require the introduction of a "view" permission on top of add/change/delete...
EDIT: regarding setting all fields readonly, also untested but looking promising:
readonly_fields = MyModel._meta.get_all_field_names()
EDIT: Here's another one
if self.declared_fieldsets:
return flatten_fieldsets(self.declared_fieldsets)
else:
return list(set(
[field.name for field in self.opts.local_fields] +
[field.name for field in self.opts.local_many_to_many]
))
As "view permissions" will not make it into Django 1.11, unfortunately, here's a solution that makes your ModelAdmin read-only by making both saving model changes and adding model history log entries a no-op.
def false(*args, **kwargs):
"""A simple no-op function to make our changes below readable."""
return False
class MyModelReadOnlyAdmin(admin.ModelAdmin):
list_display = [
# list your admin listview entries here (as usual)
]
readonly_fields = [
# list your read-only fields here (as usual)
]
actions = None
has_add_permission = false
has_delete_permission = false
log_change = false
message_user = false
save_model = false
(NOTE: Don't mistake the false no-op helper with the False builtin. If you don't sympathize with the helper function outside the class move it into the class, call it no_op or something else, or override the affected attributes by usual defs. Less DRY, but if you don't care...)
This will:
remove the actions drop-down box (with "delete") in the list view
disallow adding new model entries
disallow deleting existing model entries
avoid creating log entries in the model history
avoid displaying "was changed successfully" messages after saving
avoid saving changeform changes to the database
It will not:
remove or replace the two buttons "Save and continue editing" and "SAVE" (which would be nice to improve the user experience)
Note that get_all_field_names (as mentioned in the accepted answer) was removed in Django 1.10.
Tested with Django 1.10.5.
The selected answer doesn't work for Django 1.11, and I've found a much simpler way to do it so I thought I'd share:
class MyModelAdmin(admin.ModelAdmin):
def get_readonly_fields(self, request, obj=None):
return [f.name for f in obj._meta.fields]
def has_delete_permission(self, request, obj=None):
return False
def has_add_permission(self, request):
return False
You may customize your ModelAdmin classes with the readonly_fields attribute. See this answer for more.
I had a similar scenario where:
User should be able to create the model objects
User should be able to view listing of model objects
User SHOULD'NT be able to edit an object once it's been created
1. Overriding the Change View
Because it's possible to override the change_view() in a ModelAdmin, we can exploit that to prevent the editing of model instances once they have been created. Here's an example I've used:
def change_view(self, request, object_id, form_url='', extra_context=None):
messages.error(request, 'Sorry, but editing is NOT ALLOWED')
return redirect(request.META['HTTP_REFERER'])
2. Conditionally Change Edit Permissions
I also realized that the docs interpret the result of ModelAdmin.has_change_permission() in different ways:
Should return True if editing obj is permitted, False otherwise. If
obj is None, should return True or False to indicate whether editing
of objects of this type is permitted in general (e.g., False will be
interpreted as meaning that the current user is not permitted to edit
any object of this type).
Meaning I could check whether obj is None, in which case I return True, otherwise I return False, and this in effect allows users to view the change-list, but not be able to edit nor view the change_form after the model instance is saved.
def has_change_permission(self, request, obj = None, **kwargs):
if obj is None:
return True
else:
return False
Though am thinking this might also override any MODEL_can_change permissions allowing unwanted eyes from viewing the change-list?
According to my test on Django 1.8 we can not use following as noted on answer #3 but it works on Django 1.4:
## self.get_formset(request, obj) ##
answer 3 needs fix. Generally, alternative codes for this issue about below section
## form = self.get_formset(request, obj).form ##
## fields = form.base_fields.keys() ##
Can be something like:
#~ (A) or
[f.name for f in self.model._meta.fields]
#~ (B) or
MyModel._meta.get_all_field_names()
#~ (C)
list(set([field.name for field in self.opts.local_fields] +
[field.name for field in self.opts.local_many_to_many]
))