Update the Queryset of a Django-Select2 AutoModelSelect2Field - python

I can't figure out how to update the queryset of a AutoModelSelect2Field dynamically. I'm getting really strange results. For example, sometimes the select2 box will return the correct, filtered results, and sometimes it will return NO results when I enter the same characters.
my code:
#views.py
form = MyForm()
#forms.py
class MyField(AutoModelSelect2Field):
search_fields = ['name__icontains']
max_results = 10
class MyForm(forms.Form):
my_field = MyField(
queryset=project.objects.none(),
required=True,
widget=AutoHeavySelect2Widget(
select2_options={
'width': '100%',
}
)
)
def __init__(self, *args, **kwargs):
qs = kwargs.pop('queryset')
self.base_fields['my_field'].queryset = qs
super(MyForm, self).__init__(*args, **kwargs)
#self.fields['my_field'].queryset = qs
#self.base_fields['my_field'].queryset = qs
A few of the things I've tried -
update from the view:
#views.py
form = MyForm()
form.base_fields['my_field'].queryset = new_qs
and:
form = MyForm()
form.fields['my_field'].queryset = new_qs
pass the qs to the form:
#views.py
form = MyForm(queryset=Project.objects.filter(project_type=pt))
# see above code for forms.py
I've also tried setting the initial qs to all objects:
class MyForm(forms.Form):
my_field = MyField(
queryset=project.objects,
...
But I get the same problem, 90% of the time I get the results of the initial queryset, rather than the filtered objects based on the new qs.

We were able to find a pretty straightforward way to get the dropdown options to filter by additional fields (ie. first select country and then have the state dropdown only showing states from the selected country)
It was inspired by a suggestion from here (where we also posted this solution):
https://github.com/applegrew/django-select2/issues/22
in forms.py:
class StateChoices(AutoModelSelect2Field):
queryset = State.objects
def get_results(self, request, term, page, context):
country = request.GET.get('country', '')
states = State.objects.filter(country=country, name__istartswith=term)
s2_results = [(s.id, s.name, {}) for s in states]
return ('nil', False, s2_results)
the form field:
# only include this when you would have a screen where the country
# is preset and would not change, but you want to use it as a search context
country = forms.ModelChoiceField(queryset=Country.objects.all(),
widget=forms.HiddenInput())
state = StateChoices(widget = AutoHeavySelect2Widget(
select2_options = {
'minimumInputLength': 1,
'ajax': {
'dataType': 'json',
'quietMillis': 100,
'data': JSFunctionInContext('s2_state_param_gen'),
'results': JSFunctionInContext('django_select2.process_results'),
}
}))
in our javascript:
function s2_state_param_gen(term, page) {
var proxFunc = $.proxy(django_select2.get_url_params, this);
results = proxFunc(term, page, 's2_condition_param_gen');
results.country = $('#id_country').val();
return results;
}

Related

Show soft warning from clean method of Django Formset for Model Form

I've got a model form and a formset that I'm using successfully without issue.
In my clean method of the formset I perform some logic - but one of the requirements that previously caused a ValidationError is now being changed and should just be a simple warning to the user in the template.
My model form and formset look like this:
class AlignmentForm(forms.ModelForm):
def __init__(self, *args, employee, **kwargs):
self.person = person
super().__init__(*args, **kwargs)
class Meta:
model = Alignment
fields = [
"alignment",
"start_date",
"end_date",
]
class AlignmentFormSet(forms.BaseModelFormSet):
def clean(self):
if any(self.errors):
return
# I've trimmed out additional logic and checks
# This is the area of concern
alignment_error = {}
for i, form in enumerate(self.forms):
if not end_date:
if alignment == "Wrong":
alignment_error = {
"alignment": alignment,
"added_message": "Corrected",
}
if alignment_error:
raise forms.ValidationError(
f"""
The alignment is
({alignment_error['alignment']}) and must
be ({alignment_error['added_message']}).
"""
)
return form
The current ValidationError needs to become just a message that is presented to the user in the template - but does not stop the form from validating and saving.
I've tried doing something like this:
if alignment_error:
messages.warning(
request,
f"""
The alignment is
({alignment_error['alignment']}) and must
be ({alignment_error['added_message']}).
""",
)
But that didn't work because I don't have access to request. Can I get access to it?
What is the best way to accomplish this? I still want to display the message, but just don't want it to prevent the form from saving.
EDIT TO ADD:
The views function looks like this: (the essential pieces)
def person(request, person_no):
person = get_user_model().objects.get(person_no=person_no)
formset = get_alignment_formset(person, request.POST)
if request.method == "POST":
# If add alignment is in form the util method will handle adding
# the extra form if add alignment not in post data actually validate
# and process the form
if "add_alignment" not in request.POST:
if formset.is_valid():
# If form is valid we want to get the person being updated
# current alignment
alignment_before = Alignment.get_active_primary(person)
for formset_form in formset:
if formset_form.has_changed():
alignment = formset_form.save(commit=False)
alignment.user = person
if not alignment.pk:
alignment.created_by = request.user
alignment.modified_by = request.user
alignment.save()
else:
alignment = formset_form.save(commit=False)
# do some logic with position control here
warnings, errors = update_foil_alignment_app(
request.user, person, alignment_before, alignment
)
if errors or warnings:
for error in errors:
messages.error(request, error)
for warning in warnings:
messages.warning(request, warning)
kwargs = {"person_no": person.person_no}
return redirect("app_name:person", **kwargs)
messages.success(
request,
f"""
You have successfully updated alignments for
{alignment.user.last_name}, {alignment.user.first_name}.
""",
)
kwargs = {"person_no": person.person_no}
return redirect("app_name:person", **kwargs)
The utils function looks like this:
def get_alignment_formset(person, post_data=None):
extra = 0
# If add alignment in post_data we add the extra form and clear out
# post data, we don't actually want to post only data
if post_data:
if "add_alignment" in post_data:
post_data = None
extra = 1
formset = modelformset_factory(
Alignment,
fields=("alignment", "start_date", "end_date"),
extra=extra,
form=AlignmentForm,
formset=AlignmentFormSet,
)
formset = formset(
post_data or None,
form_kwargs={"person": person},
queryset=Alignment.exclude_denied.filter(user=person).annotate(
num_end_date=Count("end_date")
).order_by(
"-end_date",
),
)
return formset
I believe I've been able to pass the request in appropriately at this point by doing the following:
Views.py:
def person(request, person_no):
person = get_user_model().objects.get(person_no=person_no)
formset = get_alignment_formset(person, request.POST, request)
Utils.py:
def get_alignment_formset(person, post_data=None, request=None):
extra = 0
# If add alignment in post_data we add the extra form and clear out
# post data, we don't actually want to post only data
if post_data:
if "add_alignment" in post_data:
post_data = None
extra = 1
formset = modelformset_factory(
Alignment,
fields=("alignment", "start_date", "end_date"),
extra=extra,
form=AlignmentForm,
formset=AlignmentFormSet,
)
formset = formset(
post_data or None,
form_kwargs={"person": person, "request": request},
queryset=Alignment.exclude_denied.filter(user=person).annotate(
num_end_date=Count("end_date")
).order_by(
"-end_date",
),
)
return formset
Forms.py:
class AlignmentForm(forms.ModelForm):
def __init__(self, *args, employee, request, **kwargs):
self.person = person
self.request = request
super().__init__(*args, **kwargs)
class AssignmentFormSet(forms.BaseModelFormSet):
def clean(self):
...
...
if alignment_error:
warning = "There was an issue"
messages.warning(form.request, warning)
It now shows up as a warning as expected.

Django ModelFormSet object changes type on post

I'm converting a page from using manually rendered post/get functions to one using class-based views. I am attempting to move towards using all of Django's built-in functionality. But I'm encountering a confusing error where the ModelFormSet object is not the right type when the form is posted.
I have this form:
class AssignmentDatesForm(forms.ModelForm):
class Meta:
model = AssignmentDates
fields = [
'assignment_name',
'start_date',
'due_date',
'end_date',
]
widgets = { 'start_date' : forms.DateInput(format = '%m/%d/%Y', attrs ={ 'class' : 'datepicker'}),
'due_date' : forms.DateInput(format = '%m/%d/%Y', attrs ={ 'class' : 'datepicker' }),
'end_date' : forms.DateInput(format = '%m/%d/%Y', attrs ={ 'class' : 'datepicker' }),}
def clean(self):
logging.warning("Clean function")
logging.warning("start_date: " + str(start_date))
logging.warning("due_date: " + str(due_date))
logging.warning("end_date: " + str(end_date))
# Actual validation logic here
And create the ModelFormSet using modelformset_factory:
AssignmentDatesFormSet = forms.modelformset_factory(
model = AssignmentDates,
form = AssignmentDatesForm,
extra = 0,
)
Here is the view, shortened for brevity:
class EditDueDates(CurrentUserMixin, ModelFormSetView):
model = AssignmentDates
page_title = 'Edit Due Dates'
success_message = "Dates updated successfully!"
add_button_title = "Add Button Title"
form = AssignmentDatesForm
formset = AssignmentDatesFormSet
template_name = "edit_due_dates.html"
sidebar_group = ['scaffold', 'edit_due_dates']
columns = [
('Assignment Name', 'assignment_name'),
('Start Date', 'start_date'),
('Due Date', 'due_date'),
('End Date', 'end_date'),
]
fields = [
'assignment_name',
'start_date',
'due_date',
'end_date',
]
show_context_menu = False
def get_context_data(self, **kwargs):
context = super(EditDueDates, self).get_context_data(**kwargs)
# Date retrieval/ DB storage logic here
form = AssignmentDatesFormSet(queryset = AssignmentDates.objects.filter(created_by = creds.get_OUNetID()))
logging.warning("get_context_data form type: " + str(type(form)))
context['form'] = form
return context
def formset_valid(self, formset):
logging.warning("formset_valid form type: " + str(type(formset)))
cleaned_data = formset.clean()
if formset.is_valid():
formset.save()
return super(EditDueDates, self).formset_valid(formset)
The ModelFormSet is displayed fine in the template, and the logging message I have above prints out the type of the form as:
<class 'scaffold.forms.AssignmentDatesFormSet'>
But my clean function is never executed. When it executes the formset_valid function I have in the view and print out the formset type, I get this:
<class 'django.forms.formsets.AssignmentDatesFormFormSet'>
So it never executes my clean function because the type isn't right. Executing formset.clean() again just executes the clean function of this weird type.
I haven't a clue why it's a different type when it's POSTed from the template. If anyone has any ideas please let me know! Thank you!
-Will

save() generating new entry instead updating Django

I am not understanding why, but an instance always get created copying the one I am trying to edit. Also, as I can see FormSet I am using does not have an "instance" parameter to be added to its constructor. Anyways, the problem is that an instance of both Offer and OfferItem gets generated when I am editing an object.
def manage_offer(request, number=None):
param_offer = Offer.objects.filter(id=number).first()
param_items = OfferItem.objects.filter(offer=param_offer).values()
if request.method == 'POST':
offer_form = OfferForm(request.POST, instance=param_offer)
item_formset = OfferItemFormSet(request.POST, initial=param_items)
if offer_form.is_valid() and item_formset.is_valid():
# User selected go back and correct something
if request.POST.get('back', False):
return render(request,
'offer_edit.html',
{
'forms': offer_form,
'formset': item_formset,
'offer_edit': True,
})
# Proceeds with either saving or submitting request
offer = offer_form.save(commit=False)
offer.tax = offer_form.cleaned_data['tax'].value
#Sotres items to be sent back to commit part
offer_items = []
#Gets the items from the form and stores them to conf. page
for item_in_formset in item_formset.forms:
item = item_in_formset.save(commit=False)
item.item_code = get_item_code(item_in_formset.cleaned_data['name'])
item.type = get_item_type(item_in_formset.cleaned_data['name'])
offer.update_total(item.calc_total())
# Adds items into list for invoice_ready page
offer_items.append(item)
# Request goes to confirmation page
if request.POST.get('proceed', False):
return render(request,
'offer_edit.html',
{
'offer_form': offer_form,
'item_formset': item_formset,
'offer_ready': True,
'offer': offer,
'items': offer_items,
})
# Passes confirmation page and saves offer
offer.save()
# Makes sure the value is correct by recalculating
offer.reset_total()
for obj_item in offer_items:
obj_item.offer = offer
offer.update_total(obj_item.calc_total())
#commits to DB
offer.save()
obj_item.save()
return render(request,
'offer_edit.html',
{
'success_add_offer': True,
'offer': offer,
},
)
# GET generates a blank or instanced page
else:
offer_form = OfferForm(initial=
{'company': Company.objects.filter(is_default=True).first(),
'tax': Tax.objects.filter(is_default=True).first()
}, instance=param_offer)
item_formset = OfferItemFormSet(initial=param_items)
return render(request, 'offer_edit.html',
{
'forms': offer_form,
'formset': item_formset,
'edit_offer': number,
})
Forms.py
class OfferItemForm(ModelForm):
class Meta:
model = OfferItem
# Some widgets and stuff ...
class RequiredFormSet(BaseFormSet):
def __init__(self, *args, **kwargs):
super(RequiredFormSet, self).__init__(*args, **kwargs)
for form in self.forms:
form.empty_permitted = False
OfferItemFormSet = formset_factory(OfferItemForm, formset=RequiredFormSet)
I found the problem: since I am using the same View function to either edit or add a new entry, on my template form I must make sure I am also identifying if I am editing or not, because the function takes a parameter in case I am editing. In that case I must change the URL in the Post form.
Just a silly mistake take took me a few hours to find out.

How to return table data as different format types via views in Django

I have below view which returns data of three different models Employees,Testimonial and Skills and it is perfectly working.
Views.py
#page_template("app/profession.html") # just add this decorator
def profile(request, template="app/profession.html",
extra_context=None):
context = {
'employ_V': Employees.objects.get(id = 45),
'testim_V':Testimonial.objects.get(id = 45),
'skills_V':Skills.objects.filter(id = 45),
}
if extra_context is not None:
context.update(extra_context)
return render_to_response(template, context,
context_instance=RequestContext(request))
If you see, there is only one record that will be returned for Employees and Testimonial models (Notice objects.get() in the query)
whereas Skills model has more than 1 record (it returns id,skill,order fields for more than 5 rows).
Is it possible to use same view to return Skills as Json format along with Employee and Testimonial to my template (plain text and JSON) ?
Or should I use separate view to return data in Json format for Skills model ?
What could be the best approach in this case.
In a separate view for Skills - I could achieve the results successfully in JSON format - for this I used customer serializer and JsonResponse.
You can use Django values from queryset, but if you need special transformation (like thumbnail for image or so on) add to your model as_dict method
class YourModel(models.Model):
# here your fields
def as_dict(self):
context = {
'pk': self.id,
'name': self.name,
# ...
}
return context
Then in your views
# import section
from django.utils.safestring import mark_safe
# ...
#page_template("app/profession.html") # just add this decorator
def profile(request, template="app/profession.html", extra_context=None):
employ_v_obj = Employees.objects.get(id=45)
testim_v_obj = Testimonial.objects.get(id=45)
skills_v_qs = Skills.objects.filter(id=45)
skills_v_json_list = [obj.as_dict() for obj in skills_v_qs]
context = {
'employ_V': employ_v_obj,
'testim_V': testim_v_obj,
}
context['skills_V_json'] = mark_safe(json.dumps(skills_v_json_list, ensure_ascii=False))
if extra_context is not None:
context.update(extra_context)
return render_to_response(template, context, context_instance=RequestContext(request))

BaseModelFormSet __init__() got an unexpected keyword argument

I am getting the above error when I have tried to convert my inline_formset to have at least the first row required. (please see here for the StackOverflow question)
My existing code is below:
#views.py
def application(request, job_id):
job = get_object_or_404(Job, pk=job_id)
#return 404 if job isn't yet published
if (job.pub_date>timezone.now() or job.close_date<timezone.now()):
return HttpResponseNotFound('<h1>Job not found</h1>')
#create all the inlineformsets (can_delete) set to false as will always be empty upon population
EducationInlineFormSet = inlineformset_factory(Applicant, Education, extra=1, can_delete=False)
QualificationInlineFormSet = inlineformset_factory(Applicant, Qualification, extra=1, can_delete=False)
EmploymentInlineFormSet = inlineformset_factory(Applicant, Employment, extra=1, can_delete=False)
if request.method == 'POST':
applicant = Applicant(job=job)
form = ApplicantForm(request.POST, instance=applicant)
bottom_form = ApplicantFormBottom(request.POST, instance=applicant)
education_formset = EducationInlineFormSet(request.POST, instance=applicant)
qualification_formset = QualificationInlineFormSet(request.POST, instance=applicant)
employment_formset = EmploymentInlineFormSet(request.POST, instance=applicant)
#check all of the forms and formsets are valid
if form.is_valid() and bottom_form.is_valid() and education_formset.is_valid() and qualification_formset.is_valid() and employment_formset.is_valid():
# save the model to database, directly from the form:
form.save()
bottom_form.save()
education_formset.save()
qualification_formset.save()
employment_formset.save()
return render(request, 'jobs/success.html')
else:
applicant = Applicant(job=job)
form = ApplicantForm(instance=applicant)
bottom_form = ApplicantFormBottom(instance=applicant)
education_formset = EducationInlineFormSet(instance=applicant)
qualification_formset = QualificationInlineFormSet(instance=applicant)
employment_formset = EmploymentInlineFormSet(instance=applicant)
c = {
'job' : job,
'form' : form ,
'bottom_form' : bottom_form,
'education_formset' : education_formset,
'qualification_formset' : qualification_formset,
'employment_formset' : employment_formset,
}
return render(request, 'jobs/application.html', c)
In order to customise the formset I defined the following:
class BaseFormSet(BaseModelFormSet):
def __init__(self, *args, **kwargs):
super(BaseFormSet, self).__init__(*args, **kwargs)
for form in self.forms:
form.empty_permitted = False
and pass use it as follows:
EducationInlineFormSet = inlineformset_factory(Applicant, Education, extra=1, can_delete=False, formset=BaseFormSet)
This returns the above error and having read around a lot I'm still none the wiser how I can keep passing an instance to the formset.
Any help would be greatly appreciated.
Regards,
Chris.
I had a similar issue - the problem was the customised formset.
Try subclassing from BaseInlineFormSet (not BaseModelFormSet).
Here is the relevant section of the docs.

Categories

Resources