Django 2.2 : how to add a hidden input and populate it? - django

How do I add an arbitrary hidden input to a specific model's view and populate it with a value? I would like to get it later from request.GET.get('my_input_name') on submit (e.g. "Save") . It's not a field that exist in the model, it's just a random value. Also: I am not trying to make some existing field hidden with widgets. If possible, without introducing a custom template.
My view is a model-based class in admin.py ( I have no forms.py ) with custom get_form and formfield_for_foreignkey methods.
I looked at several solutions related to Dynamic fields, they are not working in Django 2.2:
Dynamical Form fields in __init__in Django admin
Dynamic fields in Django Admin
Add dynamic field to django admin model form
I don't really care about it being a dynamic field, or widget or whatever, as long as I can have it saved and retrieved from the request on GET / POST submit. It's also somewhat strange that I don't find any discussions (or documentation) about dynamic fields for Django 2.2 or higher.
My code
model.py
class MyModel(SuperModel):
...model fields definitions...
admin.py
from .models import MyModel
class MyModelAdmin(SuperAdmin):
def get_form(self, request, obj=None, **kwargs):
...some code...
def formfield_for_foreignkey(self, db_field, request, **kwargs):
...some code...
admin.site.register(MyModel, MyModelAdmin)
Thanks.
UPDATE
Never figured out how to implement what I wanted, so solved it with an ugly but working "flag-based" workaround using request.session. It does not answer the question so I do not post it as an answer.
I store in the session the value upon arrival to the entity edit form from the list (in get_form), use it in subsequent POST submits (formfield_for_foreignkey) and erase it when it's not needed anymore (get_queryset). This way I have one out of two states defined, i.e. everything is filtered down to the "parent id" passed from the "list" view to "edit" view, or not, and this state persists through multiple actions in "edit" view. Relevant code:
admin.py
class MyModelAdmin(SuperAdmin)
def get_form(self, request, obj=None, **kwargs):
......
if "parent_model_value_inline_request" in request.GET:
request.session['parent_model_value'] = request.GET['parent_model_value_inline_request']
......
def formfield_for_foreignkey(self, db_field, request, **kwargs):
......
if db_field.name == "parent_model_dropdown_name":
# Make sure we are dealing with a GET method
if request.method == 'GET' or request.method == 'POST': # GET - arrived from list, POST - submitting "save" or "save and and add another"
parent_model_value = None
# if the GET method contains the variable we are interested in that was passed by the action button
# "add entity":
if "parent_model_value_inline_request" in request.GET: # coming from parent with "add child" button
parent_model_value = request.GET.get('parent_model_value_inline_request')
elif 'parent_model_value' in request.session:
if request.session['parent_model_value'] != None:
parent_model_value = request.session['parent_model_value']
if parent_model_value:
...do conditional stuff...
def get_queryset(self, request):
.........
request.session['parent_model_value'] = None
........
UPDATE Jan 24 2020.
No luck so far (tried a custom init gain in a different way), so I implemented it by writing needed info in session and erasing it later.

Related

Django: how to re-create my function-based view as a (Generic Editing) Class Based View

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/

Django conditional field display on form

I am trying to make a simple form, that conditionally shows the website input field based on the value of another database field (that is not on the form) status. For the sake of this process the status field is not editable by the user, just by the admin. Both fields are in the same table: profile.
After working at this for a while I copped-out and just did the conditional hiding and showing on the template. But, I realise this is the unsophisticated method, and would like to improve it.
What I tried so far in forms.py:
class WebsiteForm(forms.ModelForm):
class Meta:
model = Profile
fields = (
'e-mail',
'website',
)
if Profile.status == 'personal' :
exclude = ('website',)
This method in forms.py works effectively, in that I can conditionally show and hide the field if I use test comparitors in the if statement like:
if 1 == 1:
or
if 1 != 1:
But, I cannot get an effective test using the field Profile.status, the value in the field seems to be unavailable at the point the if test in forms.py is performed.
If I use print(Profile.status) I get the following output in the terminal: user__profile__status__isnull, so I think this means that I am at least testing the correct field in the database. Although I am also noting that this output only shows at initialisation of runserver, not when the form page is accessed.
One final point, the user is authenticated and editing their own record.
Any help very much appreciated.
After a lot of trial and even more error, and some wide-ranging searching, I found the answer via the documentation at https://ccbv.co.uk/.
Essentially the path I decided to take was to use a different form for the respective fields that I wanted to use (I'm sure there are other solutions out there that add or subtract fields from the views). This involved changing the form_class with get_form_class:
# views.py
class telephone_view(UpdateView):
template_name = 'account/telephone.html'
#no need to define "form_class" here
#form_class = TelephoneForm
success_url = '/accounts/telephone/'
def get_form_class(self):
if self.request.user.profile.status == 'managed':
messages.success(self.request, _('you got the managed form'))
return TelephoneFormExtended
else:
messages.success(self.request, _('you got the other form'))
return TelephoneFormLight
def get_object(self, queryset=None):
return Profile.get_or_create_for_user(self.request.user)
def form_valid(self, form):
messages.success(self.request, _('Your telephone setting was updated'))
return super(telephone_view, self).form_valid(form)
#method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super(telephone_view, self).dispatch(*args, **kwargs)
After working it out for myself I also found this answer which does the same thing:
Updateview with dynamic form_class

Admin override form fields attributes

I'm building a profile driven admin for an app I'm building. Users have some permissions on parts of a hierarchy tree (displayed as a select in the admin form), and I'ld like to display only this part of the tree in the select. I'ld like to change the queryset attribute of this select field.
The form has no knowledge about the request (user), So I can't et it in the __init__ of it.
I've tryed to set form.base_fields in ModelAdmin.get_form(), but I've side effects with this method: some users can see trees of other users, and have an error message, due to permission. The only way to avoid those errors is to reload the project (at web server level), which is not an option...
I've also tryed to override the ModelAdmin.get_fields() method, but it does not seems to be called.
Have someone an idea on how to do this ?
... I'll provide some code ...
Admin:
https://gist.github.com/frague59/f90ba63bb2548fb27e32576329159543
Forms:
https://gist.github.com/frague59/aa5236eb11982bd810f81342da8bc05d
It sounds like you might be looking for the formfield_for_foreignkey method. The example in the docs shows how you restrict the queryset for a field based on the user:
class MyModelAdmin(admin.ModelAdmin):
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "car":
kwargs["queryset"] = Car.objects.filter(owner=request.user)
return super(MyModelAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
I've found a way to overrrides thos fields : using deepcopy in the get_form() method to copy the form class, and then update the fields with base_field:
class MyAdmin(ModelAdmin):
form = MyForm # class !
def get_form(self, ...):
self.form = deepcopy(self.form)
form = super(MyAdmin, self).get_form(...)
if 'foo' in form.base_fields:
form.base_fields['foo'].queryset = my_reduced_queryset
form.base_fields['foo'].widget = my_pretty_widget
...
return form
Thanks for your help !

Accessing variables in Django forms

The problem I've run into here is how to display some additional text in a form,
specifically the values of the previous step. The point is to show the user a message such as "you have chosen {{ value_from_step_0 }} as your starter, now choose a main course."
I can get that value from step 0 as follows:
class MenuWizard(SessionWizardView):
def get_form_kwargs(self, step=None):
kwargs = {}
if step == '1':
starter = self.get_cleaned_data_for_step('0')['starter']
kwargs.update({'starter': starter})
class MainCourseForm(forms.Form):
def __init__(self, *args, **kwargs):
starter = kwargs.pop('starter')
print "{0} chosen".format(starter) # This works
super(DomainForm, self).__init__(*args, **kwargs)
# What next?
If I don't pop the 'starter' value then the super call fails. But, how may I get that value into the relevant template? Presumably I need to do something further within init or override something else, but what?
First, note that 1.7 is unsupported, and the form wizards were removed from Django in 1.8 and are now in the third-party formtools library.
This isn't really a job for the form, but the view. SessionWizardView is a standard class-based view, and like most class-based views it allows you to override get_context_data to add elements to the template contents. See the documentation, which has an example of using self.steps.current to check the current step name.

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