Django multiple forms on one view - python

I have a Django template that has data from a few different model types combining to make it. A dashboard if you will. And each of those has an edit form.
Is it best to process all those forms in one view as they are posted back to the same place and differentiating between them by a unique field like below?
Or if having lots of different dedicated avenues is the way forward? Thanks for any guidance
class ProjectDetail(DetailView):
template_name = 'project/view.html'
def get_object(self):
try:
return Project.objects.filter(brief__slug=self.kwargs['slug']).filter(team=get_user_team(self.request)).first()
# add loop to allow multiple teams to work on the same brief (project)
except Exception as e:
project_error = '%s (%s)' % (e.message, type(e))
messages.error(self.request, 'OH NO! %s' % project_error)
return redirect('home')
def get_context_data(self, **kwargs):
project = self.get_object()
context = dict()
context['project'] = project
context['milestone_form'] = MilestoneForm(initial={'project': project})
context['view'] = self
return context
def post(self, request, *args, **kwargs):
# get the context for the page
context = self.get_context_data()
try:
# switch for each of the form types on the team profile page (shown if member)
if 'milestone_form_submit' in request.POST:
project=self.get_object()
# set date arbitrarily to half way to brief deadline
brief = Brief.objects.get(project=project)
last_milestone = self.milestones().last()
milestone_del_date = last_milestone.del_date + timedelta(days=7)
new_milestone = Milestone(
project=project,
title_text=request.POST.get('title_text'),
del_date=milestone_del_date,
)
try:
new_milestone.save()
messages.success(self.request, "Excellent! New delivery popped on the bottom of the list")
except Exception as e:
# pass the erroring form back in the context if not
form = MilestoneForm(request.POST)
context['milestone_form'] = form
messages.error(self.request, "OH NO! Deadline didn't save. Be a sport and check what you entered")
elif 'milestone-edit-date-form-submit' in request.POST:
# get object from db
milestone = Milestone.objects.get(pk=request.POST['id'])
# update del_date field sent
milestone.del_date = request.POST['del_date']
# save back to db
milestone.save()
messages.success(self.request, "Updated that delivery right there!")
elif ...
except Exception as e:
messages.error(self.request, "OH NO! Deadline didn't save. Be a sport and check what you entered")
return render(request, self.template_name, context)

You can use mixins in order to solve your problem.
Example from the gist https://gist.github.com/michelts/1029336
class MultipleFormsMixin(FormMixin):
"""
A mixin that provides a way to show and handle several forms in a
request.
"""
form_classes = {} # set the form classes as a mapping
def get_form_classes(self):
return self.form_classes
def get_forms(self, form_classes):
return dict([(key, klass(**self.get_form_kwargs())) \
for key, klass in form_classes.items()])
def forms_valid(self, forms):
return super(MultipleFormsMixin, self).form_valid(forms)
def forms_invalid(self, forms):
return self.render_to_response(self.get_context_data(forms=forms))
As you can see, when you inherit from this class, you can handle multiple forms simultaneously. Look at the gist's code and adapt it to your problem.
Look at this answer

Related

Validating Entered Data with custom constraints in Django Forms in __init__

In my project, the user is entering data in a settings page of the application and it should update the database with the user's settings preference. I read this answer by Alasdair and how using the __init__() will allow to access the user's details. I was wondering if it's possible to return data from __init__() so I can validate the entered data before calling the save() function in the view? This is what I tried (and did not work). I am open to going about this in a better approach so I appreciate all your suggestions!
**EDIT: ** I am deciding on moving the validation of the data entered by the user in the forms file because when I wrote it in my views, I ended up getting 5 if statements (nested). IMO, and given the situation of this application, moving it to the forms seems to be a cleaner approach. I also considered using the clean_['field'] but I need the side variable from the __init__() function to do that and I am not sure how to extract that value.
Forms.py
class t_max_form(forms.ModelForm):
side = None
def __init__(self, *args, **kwargs):
side = kwargs.pop('side', None)
super(t_max_form, self).__init__(*args,**kwargs)
if side == "ms":
self.fields['hs_max_sessions'].widget.attrs.update({'class':'form-control', 'readonly':True})
self.fields['ms_max_sessions'].widget.attrs.update({'class':'form-control mb-3'})
elif side == "hs":
self.fields['hs_max_sessions'].widget.attrs.update({'class':'form-control'})
self.fields['ms_max_sessions'].widget.attrs.update({'class':'form-control', 'readonly':True})
else:
self.fields['hs_max_sessions'].widget.attrs.update({'class':'form-control'})
self.fields['ms_max_sessions'].widget.attrs.update({'class':'form-control'})
#Trying to validate the data here
valid_session_count = settings.objects.first()
if(side == "ms"):
input_sessions = self.fields['ms_max_sessions'].widget.attrs['readonly']
if(input_sessions > valid_session_count.max_sessions):
self.add_error("ms_max_sessions", "You have entered more than the limit set by the TC. Try again")
elif(side == "hs"):
input_sessions = self.cleaned_data['hs_max_sessions']
if(input_sessions > valid_session_count.max_sessions):
self.add_error("hs_max_sessions", "You have entered more than the limit set by the TC. Try again")
else:
input_sessions = self.cleaned_data['ms_max_sessions'] + self.cleaned_data['hs_max_sessions']
if(input_sessions > valid_session_count.max_sessions):
self.add_error("hs_max_sessions", "You have entered more than the limit set by the TC. Try again")
return input_sessions
And this is what I was trying in my views
views.py
def t_time_slots(request):
name = request.user.username
t = t_info.objects.get(student__user__username = name)
timeSlots = tutor_time_slot.objects.filter(tutor = tutor).order_by('time')
side = decide_side(request.user)
if request.method == 'POST':
form = t_time_slot_form(request.POST)
if form.is_valid():
if check_unique_time_slot(request.user, form.cleaned_data['day'], form.cleaned_data['time']):
timeSlots = form.save(commit=False)
timeSlots.t = t
timeSlots.save()
messages.success(request, "Added Successfully")
return redirect('TTimeSlot')
else:
print("Overlap")
messages.error(request, "The time slot overlaps with a Current one. Please Change the time and try again.")
return redirect('TTimeSlot')
else:
form = t_time_slot_form()
context = {'timeSlots': timeSlots, 'form': form, 'side':side}
return render(request, 't/t_time_slot.html', context)
Set self.side in the __init__ method, then you can access it in the clean() or clean_<field> methods.
I would avoid putting validation code in the __init__ method.
class MyForm(forms.Form):
my_field = forms.CharField()
def __init__(self, *args, **kwargs):
self.side = kwargs.pop('side', None)
super(t_max_form, self).__init__(*args,**kwargs)
...
def clean_my_field(self):
my_field = self.cleaned_data['my_field']
# Use self.side to validate data
if my_field = self.side:
raise forms.ValidationError("Invalid")
return my_field

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.

How to export django admin changelist as csv

I want to export my changelist (fields in list_display) as csv. I used the code from https://books.agiliq.com/projects/django-admin-cookbook/en/latest/export.html But it creates csv for the model fields. But in my case, I want to export changelist as csv, not the model fields.
Also, note that most of the fields in changelist(list_display) are calculated fields in Admin.
This is my code
class ExportCsvMixin:
def export_as_csv(self, request, queryset):
meta = queryset.model._meta
field_names = [field.name for field in meta.fields]
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename={}.csv'.format(meta)
writer = csv.writer(response)
writer.writerow(field_names)
for obj in queryset:
row = writer.writerow([getattr(obj, field) for field in field_names])
return response
export_as_csv.short_description = "Export Selected"
class MyAdmin(admin.ModelAdmin, ExportCsvMixin):
list_display = ('field1',
'field2'
)
list_filter = ('field2')
actions = ["export_as_csv"]
def field1(self, obj):
<return logic here>
def field2(self, obj):
<return logic here>
NOTE:
field1 and field2 are calculated fields, and not model fields.
My model is a proxy model. But I don't think it would any difference in this case.
I want the csv to contain data for field1 and field2only, as they are in my changelist.
May be the trick would be to somehow point the queryset to that of changelist. but how do that ? Or if someone can suggest some other solution or even api to achieve this goal?
I had the same issue and I managed to hack it this way.
I looked into the ModelAdmin base class and I found the function responsible for handling actions methods, it's called response_action, I looked into it and changed the queryset it return what I need.
Let say you have a query set that return 'field1' and 'field2'.
Here is how I edited the function to return a custom queryset:
def response_action(self, request, queryset):
"""
Handle an admin action. This is called if a request is POSTed to the
changelist; it returns an HttpResponse if the action was handled, and
None otherwise.
"""
# There can be multiple action forms on the page (at the top
# and bottom of the change list, for example). Get the action
# whose button was pushed.
try:
action_index = int(request.POST.get('index', 0))
except ValueError:
action_index = 0
# Construct the action form.
data = request.POST.copy()
data.pop(admin.helpers.ACTION_CHECKBOX_NAME, None)
data.pop("index", None)
# Use the action whose button was pushed
try:
data.update({'action': data.getlist('action')[action_index]})
except IndexError:
# If we didn't get an action from the chosen form that's invalid
# POST data, so by deleting action it'll fail the validation check
# below. So no need to do anything here
pass
action_form = self.action_form(data, auto_id=None)
action_form.fields['action'].choices = self.get_action_choices(request)
# If the form's valid we can handle the action.
if action_form.is_valid():
action = action_form.cleaned_data['action']
select_across = action_form.cleaned_data['select_across']
func = self.get_actions(request)[action][0]
# Get the list of selected PKs. If nothing's selected, we can't
# perform an action on it, so bail. Except we want to perform
# the action explicitly on all objects.
selected = request.POST.getlist(admin.helpers.ACTION_CHECKBOX_NAME)
if not selected and not select_across:
# Reminder that something needs to be selected or nothing will
# happen
msg = _("Items must be selected in order to perform "
"actions on them. No items have been changed.")
self.message_user(request, msg, messages.WARNING)
return None
if not select_across:
##### change this line with your queryset that return field one and two
queryset = 'your_queryset_with_field'
response = func(self, request, queryset)
# Actions may return an HttpResponse-like object, which will be
# used as the response from the POST. If not, we'll be a good
# little HTTP citizen and redirect back to the changelist page.
if isinstance(response, HttpResponseBase):
return response
else:
return HttpResponseRedirect(request.get_full_path())
else:
msg = _("No action selected.")
self.message_user(request, msg, messages.WARNING)
return None
Check in this function the part I command with #####
redefine that function in the class that inherits from the model admin class
The function returns a custom queryset , now you can edit your export_as_csv_function according to that.
def export_as_csv(self, request, queryset):
field_names = ["field_one", "field_two"]
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename={}.csv'.format(
'working_hours')
writer = csv.writer(response)
writer.writerow(field_names)
for obj in queryset:
writer.writerow([obj.get(field) for field in field_names])
return response
That it and you can go and download your CSV with the customs field.
I hope it's not too late and this helps other folks with the same issue.

How to refactor similar looking functions across 2 different class based views in Django 1.10?

Am using codecoverage and it complains that I have 2 functions in 2 different class based views that are too similar.
Attached is the codecoverage error
Below are the code that were highlighted:
class PalletContentPickup(APIView):
"""
Picking up a pallet content to transfer to exiting pallet content
"""
def put(self, request, pk):
count = request.data['count']
pallet_id = request.data['pallet_id']
from_pallet_content = QuickFind.get_pallet_content_or_404(pallet_content_id=pk)
to_pallet = QuickFind.get_pallet_or_404(pallet_id=pallet_id)
Transfer.validate_if_can_pickup_pallet_content(from_pallet_content, to_pallet, request.user)
to_pallet_content = QuickFind.get_or_create_pallet_content(pallet=to_pallet, product=from_pallet_content.product)
ExitFormHelper.create_exit_form_line_item_on_pallet_content_if_no_exit_form_line(to_pallet_content, request.user)
Transfer.previous_pallet_content_to_new_pallet_content(from_pallet_content, to_pallet_content, count)
serializer = PalletSerializer(from_pallet_content.pallet)
return Response({"data": serializer.data}, status=status.HTTP_202_ACCEPTED)
class PalletContentPutback(APIView):
"""
Put back pallet content to an approved pallet
"""
def put(self, request, pk):
count = request.data['count']
pallet_id = request.data['pallet_id']
from_pallet_content = QuickFind.get_pallet_content_or_404(pallet_content_id=pk)
to_pallet = QuickFind.get_pallet_or_404(pallet_id=pallet_id)
Transfer.validate_if_can_putback_pallet_content(from_pallet_content, to_pallet, request.user)
to_pallet_content = QuickFind.get_or_create_pallet_content(pallet=to_pallet, product=from_pallet_content.product)
ExitFormHelper.create_exit_form_line_item_on_pallet_content_if_no_exit_form_line(to_pallet_content, request.user)
Transfer.previous_pallet_content_to_new_pallet_content(from_pallet_content, to_pallet_content, count)
serializer = PalletSerializer(from_pallet_content.pallet)
return Response({"data": serializer.data}, status=status.HTTP_202_ACCEPTED)
I read about strategy pattern in Python
Not sure if I should apply strategy pattern here and if so, how? Because the example in the url still does not help me realise exactly how to perform strategy pattern here.
As i see it there is only one line of difference between the two classes. so you can keep it really simple by
class PalletContent(object):
"""
Put back pallet content to an approved pallet
"""
def do_action(self, request, pk, action):
count = request.data['count']
pallet_id = request.data['pallet_id']
from_pallet_content = QuickFind.get_pallet_content_or_404(pallet_content_id=pk)
to_pallet = QuickFind.get_pallet_or_404(pallet_id=pallet_id)
if action == 'putback':
Transfer.validate_if_can_putback_pallet_content(from_pallet_content, to_pallet, request.user)
else:
Transfer.validate_if_can_pickup_pallet_content(from_pallet_content, to_pallet, request.user)
to_pallet_content = QuickFind.get_or_create_pallet_content(pallet=to_pallet, product=from_pallet_content.product)
ExitFormHelper.create_exit_form_line_item_on_pallet_content_if_no_exit_form_line(to_pallet_content, request.user)
Transfer.previous_pallet_content_to_new_pallet_content(from_pallet_content, to_pallet_content, count)
serializer = PalletSerializer(from_pallet_content.pallet)
return Response({"data": serializer.data}, status=status.HTTP_202_ACCEPTED)
class PalletContentPickup(APIView, PalletContent):
def put(self,request,pk):
self.do_action(request,pk,'pickup')
class PalletContentPutback(APIView, PalletContent):
def put(self,request,pk):
self.do_action(request,pk,'putback')
You are saving only a few lines of code but it may be worth it when it comes to maintenance. At the same time your validate methods do not seem to return anything. Are they raising exceptions?

Passing a Django Form Class variable to the nested class Meta, how?

G'day there. I've currently got a bit of an annoyance more than anything. The code below works fully, as expected. Basically it's a ModelForm that dynamically bases it's model on a string received in the url, or based on the class of an instance if that's provided.
My question is whether it's possible to abstract this out into another module, forms.py, by passing the model_name variable. I can pass model_name to the form Class no problems, but I can't figure out how to pass it to Meta after that. Is there any easy way to do this? If not this'll do, but it would make my view code heaps neater.
#user_passes_test(lambda u: u.is_staff, login_url="%slogin/" % NINJA_ADMIN_URL_PREFIX)
def content_form(request, model_name=None, edit=False, call_name=''):
if edit:
content = Content.objects.get(call_name=call_name)
model_name = content.fields.__class__.__name__
class ContentForm(forms.ModelForm):
parent = ModelTextField(queryset=Content.objects.all(), widget=JQueryAutocomplete(
source_url='%sjson/call_names.json' % NINJA_ADMIN_URL_PREFIX, jquery_opts = {'minLength': 2},
override_label='item.fields.call_name', override_value='item.fields.call_name'),
required=False)
class Meta():
model = get_ninja_type(model_name)
widgets = {
'ninja_type': forms.widgets.HiddenInput(),
}
def clean_parent(self):
call_name = self.cleaned_data['parent']
if call_name:
try:
parent = Content.objects.get(call_name=call_name)
except Content.DoesNotExist:
raise forms.ValidationError("The call name '%s' doesn't exist. Choose another parent." % call_name)
return parent
else:
return None
if request.method == 'POST':
if edit:
form = ContentForm(request.POST, instance=content.fields)
else:
form = ContentForm(request.POST)
if form.is_valid():
content = form.save()
messages.success(request, "Your new content has been saved.")
return HttpResponseRedirect('%scontent/' % NINJA_ADMIN_URL_PREFIX)
else:
if edit:
form = ContentForm(instance=content.fields)
else:
form = ContentForm(initial={'ninja_type': model_name.lower(),
'author': request.user})
if edit:
page_title = 'Edit %s' % model_name
else:
page_title = 'Create New %s' % model_name
return render(request, 'ninja/admin/content_form.html', {
'form': form,
'ninja_type': model_name,
'page_title': page_title,
'edit': edit,
'meta_field_names': NINJA_META_FIELD_NAMES,
})
The answer was pretty obvious when I thought about it. A function returning a class works fine. This is what's left at the start of views.py. I'm thinking this is likely the best way to do this, or at least the most concise.
#user_passes_test(lambda u: u.is_staff, login_url="%slogin/" % NINJA_ADMIN_URL_PREFIX)
def content_form(request, model_name=None, edit=False, call_name=''):
if edit:
content = Content.objects.get(call_name=call_name)
model_name = content.fields.__class__.__name__
ContentForm = get_content_form(model_name)
if request.method == 'POST':
if edit:
form = ContentForm(request.POST, instance=content.fields)
else:
This is the forms.py entry.
def get_content_form(model_name):
class DynamicContentForm(forms.ModelForm):
parent = ModelTextField(queryset=Content.objects.all(), widget=JQueryAutocomplete(
source_url='%sjson/call_names.json' % NINJA_ADMIN_URL_PREFIX, jquery_opts = {'minLength': 2},
override_label='item.fields.call_name', override_value='item.fields.call_name'),
required=False)
class Meta():
model = get_ninja_type(model_name)
widgets = {
'ninja_type': forms.widgets.HiddenInput(),
}
def clean_parent(self):
call_name = self.cleaned_data['parent']
if call_name:
try:
parent = Content.objects.get(call_name=call_name)
except Content.DoesNotExist:
raise forms.ValidationError("The call name '%s' doesn't exist. Choose another parent." % call_name)
return parent
else:
return None
return DynamicContentForm

Categories

Resources