exposing data with django form get request - python

I'm building a small form on which I'm displaying some data in the table, addition to that I have two dropdown, with which you can select current data or year for the data you want to see in the table.
My question is how can I populate dropdown with current month and year, using django form get request, I'm little confused how to process this in my view, note that I'm using CBV FormView.
I've tried something like this
form.py
from django import forms
import datetime
class StatisticsForm(forms.Form):
"""TODO: Simple form with two field, one for year
other for month, so user can list Statistics for
current month and year.
:returns: TODO"""
invoice_month = forms.CharField(label="month", max_length=225)
invoice_year = forms.CharField(label="year", max_length=225)
def get_initial(self):
initial = super(StatisticsForm, self).get_initial()
initial["invoice_month"] = datetime.date.today()
initial["invoice_year"] = datetime.date.today()
return initial
and in my view I'm displaying table and I need to do the rest to.
view.py
from django.views.generic.edit import FormView
from .models import Rate
from statistics.forms import StatisticsForm
from statistics.services import StatisticsCalculation
class StatisticsView(FormView):
"""
TODO: We need to handle
Total Invoice - no matter how old, basically all of them
Current month Total Invoice
"""
template_name = "statistics/invoice_statistics.html"
form_class = StatisticsForm
def get_context_data(self, **kwargs):
context = super(StatisticsView, self).get_context_data(**kwargs)
def_currency = Rate.EUR
context["can_view"] = self.request.user.is_superuser
context["currency"] = def_currency
context["supplier_statistic"] = StatisticsCalculation.statistic_calculation(default_currency)
return context

When the FormView creates the actual form object, it gets the arguments to pass to the form from get_form_kwargs():
def get_form_kwargs(self):
"""
Returns the keyword arguments for instantiating the form.
"""
kwargs = {
'initial': self.get_initial(),
'prefix': self.get_prefix(),
}
if self.request.method in ('POST', 'PUT'):
kwargs.update({
'data': self.request.POST,
'files': self.request.FILES,
})
return kwargs
Notice how it's calling the get_initial() on itself (the view) rather than the form. It can't call it on the form because it's not initialized yet. Move your method into the view and you're good to go.
As a sidenote, use django.utils.timezone.now() as opposed to stdlib datetime.date.today() since it respects your django timezone settings and you may occasionally see some off-by-one quirks otherwise.
Edit: You should also update your form to use ChoiceField, and set the defaults with timezone.now().month and timezone.now().year.
Happy coding.

Related

Django - unable to handle form submission with dynamic field

In my Django form, I have one select field that needs to be populated dynamically based on some information about the current user. I am able to get the field set up correctly and rendered in the form - however, I'm getting an error when submitting the form because it's getting hung up on some of the logic that I have in the form's __init__ method that only makes sense in the context of generating the form in the first place. I'm super-new to Django, and I'm not quite familiar with the design principles for this sort of situation.
In my app's admin.py, I have a method that's used for creating a custom view for a data export form - the relevant parts of it are set up like so...
# admin.py
from organizations.models import Organization
from .forms import ExportForm
class SomeModelAdmin(SimpleHistoryAdmin, SoftDeletionModelAdmin):
def export_view(self, request):
authorized_orgs_queryset = Organization.objects.viewable_for_user(request.user).all()
authorized_orgs = [{'id': org.id, 'name': org.name} for org in authorized_orgs_queryset]
context = dict(
self.admin_site.each_context(request),
form = ExportForm({'authorized_orgs': authorized_orgs}),
)
if request.method == 'POST':
form = ExportForm(request.POST)
if form.is_valid():
# do some stuff with the form.cleaned_data and return a .csv file as a response
return response
return TemplateResponse(request, 'export.html', context)
So the current user may be authorized to export data for multiple organizations, and in the form I'd like to present the user with a select element populated with these organizations.
The ExportForm has a number of "fixed" fields that are always the same, and just the one dynamic select element, which is populated by the authorized_orgs arg that I pass to it - it's defined as...
# forms.py
from django import forms
min_year = 1950
export_formats = [
'csv',
'xls',
'xlsx',
'ods',
'json',
]
class ExportForm(forms.Form):
current_year = datetime.datetime.now().year
export_format = forms.ChoiceField(required=True, label='Format', choices=export_format_choices)
apply_date_range = forms.BooleanField(required=False)
year_from = forms.IntegerField(required=False, disabled=True, min_value=min_year, max_value=current_year, initial=current_year)
year_through = forms.IntegerField(required=False, disabled=True, min_value=min_year, max_value=current_year, initial=current_year)
def __init__(self, *args, **kwargs):
super(ExportForm, self).__init__(*args, **kwargs)
authorized_orgs_choices = [(org['id'], org['name']) for org in args[0]['authorized_orgs']]
self.fields['authorized_org'] = forms.ChoiceField(required=False, label='Choose an authorized organization', choices=authorized_orgs_choices)
When I render the form, all is well. However, form submission is where things go awry. Submitting the form produces the error
File "/code/observations/forms.py", line 28, in __init__
authorized_orgs_choices = [(org['id'], org['name']) for org in args[0]['authorized_orgs']]
File "/usr/local/lib/python3.7/site-packages/django/utils/datastructures.py", line 78, in __getitem__
raise MultiValueDictKeyError(key)
django.utils.datastructures.MultiValueDictKeyError: 'authorized_orgs'
Now, I do understand why this is happening - the __init__ is getting the values from the submitted form as its args, which are different from what I've supplied when setting up the form in the first place.
What I don't know is how this sort of thing should typically be handled in Django... how do I make it so that this dynamic field is created correctly when defining the form to be rendered, and that the data is available to me in form.cleaned_data when it's submitted?
Thanks very much for any insight and help.
Aha - found an answer here - django forms post request raising an error on __init__ method
I needed to make sure to pass those values again when handling the form's POST - I also reworked it a bit to make use of kwargs -
# admin.py
if request.method == 'POST':
form = ExportForm(request.POST, authorized_orgs=authorized_orgs)
# forms.py
def __init__(self, *args, **kwargs):
super(ExportForm, self).__init__(*args)
authorized_orgs_choices = [(org['id'], org['name']) for org in kwargs['authorized_orgs']]
self.fields['authorized_org'] = forms.ChoiceField(required=False, label='Choose an authorized organization', choices=authorized_orgs_choices)

Add argument to all views without having to edit them all to include variable in Django

What I need
I'm developing a Pull Notification System to an existing Django Project. With there begin over 100+ views I'm looking to find a way to incorporate a argument(notification queryset) into all the views, as this is rendered to my base.html which I can do by passing it into a view's arguments dictionary.
Problem
I want to do this without editing all of the views as this would take a long time and would be required for all future views to include this variable.
What I've tried
Creating a template filter and pass in request.user as variable to return notification for that user. This works, however when the user selects a 'New' notification I want to send a signal back to the server to both redirect them to the notification link and change the status of viewed to 'True' (POST or AJAX). Which would required that specific view to know how to handle that particular request.
What I've considered
• Editing the very core 'View' in Django.
I've tried my best to explain the issue, but if further info is required feel free to ask.
Thanks!
models.py
class NotificationModel(models.Model):
NOTIFICATION_TYPES = {'Red flag':'Red flag'}
NOTIFICATION_TYPES = dict([(key, key) for key, value in NOTIFICATION_TYPES.items()]).items()
notification_type = models.CharField(max_length=50, choices=NOTIFICATION_TYPES, default='')
user = models.ForeignKey(User, on_delete=models.CASCADE)
created = models.DateTimeField(blank=True, null=True)
text = models.CharField(max_length=500, default='')
viewed = models.BooleanField(default=False)
views.py Example
class HomeView(TemplateView):
template_name = 'app/template.html'
def get(self, request, *args, **kwargs):
notifications = NotificationsModel.objects.filter(user=request.user)
args={'notifications':notifications}
return render(request, self.template_name, args)
def notification_handler(self, request)
if 'notification_select' in request.POST:
notification_id = request.POST['notification_id']
notification = NotificationModel.objects.get(id=notification_id)
notification.viewed = True
notification.save()
return redirect #some_url based on conditions
def post(self, request, *args, **kwargs):
notifications = self.notification_handler(request, *args, **kwargs)
if notifications:
return notifications
return self.get(self, request)
With there begin over 100+ views I'm looking to find a way to incorporate a argument(notification queryset) into all the views, as this is rendered to my base.html which I can do by passing it into a view's arguments dictionary.
You don't have to put it in the views (and actually, you shouldn't - views shouldn't have to deal with unrelated responsabilities) - you can just write a simple custom template tag to fetch and render whatever you need in your base template. That's the proper "django-esque" solution FWIW as it leaves your views totally decoupled from this feature.
Have you considered mixins ?
class NotificationMixin:
my_var = 'whatever'
class MyDjangoView(NotificationMixin,.....,View)
pass
Better, Using django builtins..
from django.views.generic import base
class NotificationMixin(base.ContextMixin):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['notification'] = 'whatever'
return context
and use this mixin with all your views AS THE FIRST CLASS INHERITED, See this.
For your case, Typing it instead of applying it to the base class is better, The base view class is not intended to be altered, It's meant to be extended, That's why we can solve the issue by this...
from django.generic.views import View as DjangoBaseView
from .mixins import NotificationMixin
class View(NotificationMixin, DjangoBaseView):
pass
and use any IDE to change all the imported Views to your View and not the django one.

Django Crispy Form class not creating an object

My Issue
I'm writing a Django application where I want users to input addresses via a form. I want users to be able to add additional address fields if they want to enter more than the default number of addresses (3). I have been struggling to figure out how to accomplish this.
First Attempt
The first way I tried was simply using JavaScript to add fields on the click of a button, but when I do this the data from the added field is not submitted. I get the data from the three fields hard-coded into the form class, but the additional fields are not submitted, I assume because they are not part of the form class that I created.
Current Attempt
Since I couldn't use data from fields added using JavaScript, I'm now trying to create a dynamically-sized form in Python. My general thinking is to pass the number of location fields to the class and then use that number to create the form. I know my looping mechanism for creating the form itself works because I tested it with hard-coded numbers. However, I did that without overriding the __init__ function. To pass a number to the form, I need to override this function. When I do this, it seems that no form object is created and I have no idea why. Here is a sample of my code:
forms.py
from django import forms
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit, Layout, Row, Column, ButtonHolder, Button
class EnterLocationsForm(forms.Form):
def __init__(self, *args, **kwargs):
num_locations = kwargs.pop('num_locations')
super().__init__(*args, **kwargs)
for i in range(1, num_locations + 1):
name = 'location{}'.format(i)
exec("{} = {}".format(
name,
"forms.CharField(widget=forms.TextInput(
attrs={'placeholder': 'Address {}'.format(i)}))")
)
helper = FormHelper()
helper.form_class = 'form-horizontal'
helper.form_method = 'POST'
helper.form_show_labels = False
helper.layout = Layout()
for i in range(1, num_locations + 1):
helper.layout.append(Row(
Column('location{}'.format(i), css_class='form-group'),
css_class='form-row'
),)
helper.layout.append(ButtonHolder(
Button('submit', 'Go', css_class='btn btn-primary'),
Button('add', '+', css_class="btn btn-success"),
css_id="button-row",
))
views.py
from .forms import EnterLocationsForm
location_count = 3
def index(request):
if request.method == 'POST':
form = EnterLocationsForm(request.POST, num_locations=location_count)
...
else:
form = EnterLocationsForm(num_locations=location_count)
# if I write print(form) here, nothing is printed
return render(request, 'index.html', {'form': form})
Any tips on why this isn't creating a form object would be much appreciated! If there's a way to make my first approach work that would be great too.

Include the pk or models fields when creating the object in POST

I have my Order model which has a FK to Item model. I customized the Order create method to create the item which is passed in from the POST request. I want to customize it to allow the POST request to pass the Item pk instead of the name. so if the pk exists, I just use it and no need to create a new item.
However, when I look at the validated_data values in the create(), the id field doesn't exist.
class Item(models.Model):
name = models.CharField(max_length=255,blank=False)
class Order(models.Model):
user = models.OneToOneField(User,blank=True)
item = models.OneToOneField(Item,blank=True)
I want the POST body to be
{
"name":"iPhone"
}
or
{
"id":15
}
I tried to implement this kind of behavior myself, and didn't find a satisfying way in terms of quality. I think it's error prone to create a new object without having the user confirming it, because the user could type in "iphone" or "iPhon" instead of "iPhone" for example, and it could create a duplicate item for the iphone.
Instead, I recommend to have two form fields:
a select field for the item name,
a text input to create an item that's not in the list.
Then, it's easy to handle in the form class.
Or, with autocompletion:
user types in an Item name in an autocompletion field,
if it doesn't find anything, the autocomplete box proposes to create the "iPhon" Item,
then the user can realize they have a typo,
or click to create the "iPhone" item in which case it's easy to trigger an ajax request on a dedicated view which would respond with the new pk, to set in the form field
the form view can behave normally, no need to add confusing code.
This is how the example "creating choices on the fly" is demonstrated in django-autocomplete-light.
You may need to use Django Forms.
from django import forms
from django.contrib.auth.decorators import login_required
from django.core.exceptions import ValidationError
from .models import Order, Item
from django.shortcuts import render
class OrderForm(forms.Form):
item_name = forms.CharField(required=False)
item = forms.ModelChoiceField(queryset=Item.objects.none(), required=False)
def __init__(self, *args, **kwargs):
super(OrderForm, self).__init__(*args, **kwargs)
self.fields['item'].queryset = Item.objects.all()
def clean_item(self):
item_name = self.cleaned_data['item_name']
item = self.cleaned_data['item']
if item:
return item
if not item_name:
raise ValidationError('New item name or existing one is required.')
return Item.objects.create(name=item_name)
#login_required
def save(request):
if request.method == 'GET':
return render(request, 'template.html', {'form': OrderForm()})
user = request.user
form = OrderForm(request.POST)
if form.is_valid():
order = Order.objects.create(user=user, item=form.cleaned_data['item'])
return render(request, 'template.html', {'form': OrderForm(), 'order': order, 'success': True})
else:
return render(request, 'template.html', {'form': OrderForm(), 'error': True})

Django - Passing parameters to inline formset

I am using inlineformset_factory to create fields for a many to many relationship between Clients and Sessions, with an intermediary Attendance model.
I have the following in my views file:
AttendanceFormset = inlineformset_factory(
Session,
Attendance,
formset=BaseAttendanceFormSet,
exclude=('user'),
extra=1,
max_num=10,
)
session = Session(user=request.user)
formset = AttendanceFormset(request.POST, instance=session)
And, as I needed to override one of the form fields, I added the following to the formset base class:
class BaseAttendanceFormSet(BaseFormSet):
def add_fields(self, form, index):
super(BaseAttendanceFormSet, self).add_fields(form, index)
form.fields['client'] = forms.ModelChoiceField(
queryset=Client.objects.filter(user=2))
Now, the form works correctly, but I need to pass a value into the formset so that I can filter the clients displayed based the current user rather than just using the id 2.
Can anyone help?
Any advice appreciated.
Thanks.
EDIT
For anyone reading, this is what worked for me:
def get_field_qs(field, **kwargs):
if field.name == 'client':
return forms.ModelChoiceField(queryset=Client.objects.filter(user=request.user))
return field.formfield(**kwargs)
How about utilizing the inlineformset_factory's formfield_callback param instead of providing a formset ? Provide a callable which in turns returns the field which should be used in the form.
Form fields callback gets as 1st parameter the field, and **kwargs for optional params (e.g: widget).
For example (using request.user for the filter, replace with another if needed:
def my_view(request):
#some setup code here
def get_field_qs(field, **kwargs):
formfield = field.formfield(**kwargs)
if field.name == 'client':
formfield.queryset = formfield.queryset.filter(user=request.user)
return formfield
AttendanceFormset = inlineformset_factory(
...
formfield_callback=get_field_qs
...
)
formset = AttendanceFormset(request.POST, instance=session)
To better understand it, see the usage of formfield_callback in Django's FormSet code.

Categories