How do you create a dynamic form field in Django? - python

I want to create a dynamic form field that always filters a model and shows the filtered elements in a list in the form. Here is the code I have so far:
class OrganisorClassStatusUpdateForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(OrganisorClassStatusUpdateForm, self).__init__(*args, **kwargs)
self.fields['off_day_student'] = forms.ModelChoiceField(
queryset=Class.objects.filter(teacher=self.object.teacher).filter(is_new_set=True).filter(time=self.object.time).order_by('-date')[:10]
)
The problem I have with this code is that the self.object refers to the form itself, not the model I am trying to update. This is important since I need to filter the queryset using some of the information in the model I am trying to update. Please ask me any question if you need anything. Thanks a lot.

Related

How to use variables in ModelChoiceField queryset in Django

I am trying to use a specific list of data in my form with Django. I am using ModelChoiceField to retrieve from the model the data I need to display in the form (to let the users select from a scolldown menu).
My query is complicate because need two filters based on variables passed by views
I've tried to use the sessions but in form is not possible to import the session (based to my knowledge).
form.py
def __init__(self, pass_variables, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['initiative'] = forms.ModelChoiceField(queryset=raid_User_Initiative.objects.filter(company=pass_variables[1], username=pass_variables[0]).values_list('initiative', flat=True))
view.py
pass_variables = ((str(request.user), companyname))
f = Issue_form(pass_variables)
If I don't pass the variable the process works. The problem is with the code above as the form don't provide any error but it doesn't pass the if f.is_valid():
Thanks
I solved myself! Anyway if anyone interested the solution is using the sessions:
form.py
Before I declare the queryset as:
initiative = forms.ModelChoiceField(queryset=raid_User_Initiative.objects.all(), to_field_name="initiative")
And it is very important to use the , to_field_name="initiative"
After I amend the queryset as:
def __init__(self, user, company, *args, **kwargs):
super(raid_Issue_form, self).__init__(*args, **kwargs)
self.fields['initiative'].queryset = raid_User_Initiative.objects.filter(company=company, username=user).values_list('initiative', flat=True)
view.py
f = raid_Issue_form(request.user, request.session['company'])
Hope this help!

Django FilePathField filter used entries

I have the following Model where the FilePathField should be unique:
class Gallery(models.Model):
template = models.FilePathField(path=".../templates/galleries/", unique=True)
In the admin, I would like the dropdown list to only show me those entries that have not been used, yet, in order to make the selection among available answers more easy.
After all, any already used option in the resulting dropdown list will give me an error anyway and does not need to be shown to me in the admin. Unfortunately I am having problems wrapping my head around this.
Can anyone tell me where I could insert something similar to the following:
used = [gallery.template for gallery in Gallery.objects.all()]
return [file for file in files if file not in used]
...or might I have overseen an option somewhere in Django that could already give me the desired result? Any help would be appreciated.
So, after a lot of digging, I managed to come up with a solution myself. Ill post it here as an answer if anyone seeks a similar solution:
Extend a ModelAdmin for your Model and implement a new get_form() method that takes the choices of your named FilePathField and filter this list to your liking.
Ill give an example for the Gallery Model above:
class GalleryAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
used = [gallery.template for gallery in Gallery.objects.all()]
form = super(GalleryAdmin, self).get_form(request, obj, **kwargs)
form.base_fields['template'].choices = [choice for choice in form.base_fields['template'].choices if choice[0] not in used]
return form
EDIT: I noticed this prevents you from changing an entry, as the option originally set will now be removed. I managed to get this to work with this small tweak:
class GalleryAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if obj: # we are changing an entry
used = [gallery.template for gallery in Gallery.objects.all() if gallery.template != obj.template]
else: # we are adding a new entry
used = [gallery.template for gallery in Gallery.objects.all()]
form = super(GalleryAdmin, self).get_form(request, obj, **kwargs)
form.base_fields['template'].choices = [choice for choice in form.base_fields['template'].choices if choice[0] not in used]
return form
Hope this may help anyone in the future!

How can I get session in forms?

I have a multistep form with checkboxes, after the user submit the first step form I save the objects he checked on his session, at the second step form I would like to filter the objects with the session datas.
To accomplish this I need to get the session on the new ModelForm for the second step, unfortunaltely request is not defined in forms.
How can I access my sessions ?
class IconSubChoiceForm(forms.ModelForm):
session_icons = request.session.get('icons')
query = Q(tags__contains=session_icons[0]) | Q(tags__contains=session_icons[1]) | Q(tags__contains=session_icons[2])
icons = CustomSubChoiceField(queryset=CanvaIcon.objects.filter(query), widget=forms.CheckboxSelectMultiple)
class Meta:
model = CanvaIcon
fields = ['icons']
Any suggestion ?
As you have found, you can't access request inside the form definition.
You can override the __init__ method to take extra parameters, and set the queryset for your field. In the example below, I've used session_icons as the argument, instead of request.
class IconSubChoiceForm(forms.ModelForm):
icons = CustomSubChoiceField(queryset=CanvaIcon.objects.none(), widget=forms.CheckboxSelectMultiple)
def __init__(self, *args, **kwargs):
session_icons = kwargs.pop('session_icons')
super(IconSubChoiceForm, self).__init__(*args, **kwargs)
self.fields['icons'].queryset = CanvaIcon.objects.filter(...)
Then in your view, instantiate your form with session_icons.
form = IconSubChoiceForm(data=request.POST, session_icons=request.session.get('icons'))

Primary keys as choices in MultipleChoiceField

The thing is quite obvious to my mind, still I can get it working.
Previously I tried to get the filtered model instances from MultipleModelChoiceField by overriding the __init__ method and it worked as expected. Now I need to get only pk from those instances and I decided to do it in MultipleChoiceField. I try to do it the following way but do not succeed:
class AnswerForm(forms.Form):
answers = forms.MultipleChoiceField(
choices = [answer.pk for answer in Answer.objects.all()],
widget = forms.CheckboxSelectMultiple,
)
def __init__(self, *args, **kwargs):
q_pk = kwargs.pop('q_pk')
super(AnswerForm, self).__init__(*args, **kwargs)
self.fields['answers'].choices = [answer.pk for answer in Answer.objects.filter(question__pk=q_pk)]
In a nutshell: don't do this, stick with ModelMultipleChoiceField.
It obviously won't work because choices expects a list of tuples. Taking that in account, [answer.pk for answer in Answer.objects.filter(question__pk=q_pk)] can be rewritten like Answer.objects.filter(question__pk=q_pk).values_list('pk', 'someotherfield'), which brings you back to what ModelMultipleChoiceField does.
Many thanks to Ivan for his pointing me at using ModelChoiceField.
It is my inattention, since I only now figured out that I need some other model fields (except pk) to be passed to the form as well.
In that case the best way, that I found to get the model primary key as a value of a chosen input(s) is to get the entire models from form first and then iterate them to get the desired field value as follows:
forms.py
class AnswerForm(forms.Form):
answer = forms.ModelMultipleChoiceField(
queryset = Answer.objects.all(),
widget = forms.CheckboxSelectMultiple,
)
def __init__(self, *args, **kwargs):
q_pk = kwargs.pop('q_pk', None)
super(AnswerForm, self).__init__(*args, **kwargs)
self.fields['answer'].queryset = Answer.objects.filter(question__pk=q_pk)
views.py
checked = [answer.pk for answer in form.cleaned_data['answer']]

Django MultiSelectField: Make some choices readonly when editing

I'm using Django MultiSelectField to store a project's category/categories.
My requirement is as follows: A project can have one or more categories. When creating a project initially, the user should be able to select any of the categories listed in the select field. If a user selects 'Not Applicable', then the other categories become disabled.
When editing a project, user should not be able to unselect the originally selected categorie(s), but he can add/select other categories in addition to the original ones. The originally selected categories should be readonly when editing.
My model looks like this:
from multiselectfield import MultiSelectField
(...)
class Project(models.Model):
CAT_0 = 0
CAT_1 = 1
CAT_2 = 2
CAT_3 = 3
PROJECT_CATEGORIES = (
(CAT_0, _('Not Applicable')),
(CAT_1, _('My Category 1')),
(CAT_2, _('My Category 2')),
(CAT_3, _('My Category 3')),
)
(...)
project_categories = MultiSelectField(choices=PROJECT_CATEGORIES, max_choices=3, default=CAT_0)
And my forms.py
class ProjectForm(ModelForm):
class Meta:
model = Project
(...)
def __init__(self, *args, **kwargs):
super(ProjectForm, self).__init__(*args, **kwargs)
(...)
if self.instance.pk:
for choice in self.fields['project_categories'].choices:
if str(choice[0]) in self.instance.project_categories:
#TODO: Make this choice to be readonly i.e. user should not be able to uncheck it
else:
#TODO: Leave the choice as it is i.e. user can select/check it
# This is what I had earlier, but it makes all the choices to be readonly. Not what I want
self.fields['strategic_objectives'].widget.attrs['readonly'] = True
self.fields['strategic_objectives'].widget.attrs['disabled'] = True
How do I make sure that the original categories are shown as readonly?
Is this the way to go about it or JS will be a better option?
Is kinda useless doing that in backend before rendering, because an user can modifies your html code and enable it again.
So, disable them using JS and then in your Backend after user submit the form, you need to check it.
No metters what, have front-end validations is up to you, but you always need to have them in your backend as well.
I had the same problem as you and solved it with the help of this blogpost.
Basically it is necessary to modify the widget which is used to display the choices to be able to mark some choices as disabled.
So I created a widgets.py with the following content:
from django.forms import CheckboxSelectMultiple
class CheckboxSelectMultipleWithDisabled(CheckboxSelectMultiple):
def __init__(self, *args, **kwargs):
self.disabled = kwargs.pop("disabled", set())
super().__init__(*args, **kwargs)
def create_option(self, *args, **kwargs):
options_dict = super().create_option(*args, **kwargs)
if options_dict["value"] in self.disabled:
options_dict["attrs"]["disabled"] = True
return options_dict
Then in the constructor of the form class (ProjectForm) I exchanged the widget of the respective field (project_categories) and used the added disabled keyword argument introduced above:
def __init__(self, *args, **kwargs):
super(ProjectForm, self).__init__(*args, **kwargs)
# fill a local 'disabled' set/list/tuple/... variable using your logic
self.fields['project_categories'].widget = CheckboxSelectMultipleWithDisabled(
choices=self.fields['project_categories'].choices,
disabled=disabled
)
Also do not forget to import the new widget to use it: from .widgets import CheckboxSelectMultipleWithDisabled.
What levi posted is still true, though: you can remove the disabled tag in the html code in your browser and be able to modify the choice again.
So without any extra validation you have to trust your users.
A similar solution is presented in the answer to this question.

Categories