I am creating an expense submission system, which is multi user. For the purpose of this question, there are two models: Claim and Journey. A user creates a claim and each claim can have multiple journeys.
In the Journey CreateView, the following code:
Autofills the claim dropdown for which a journey is being logged, based on the claim pk, passed in the URL.
Based on the logged in user, only shows claims for that user (in the dropdown)
Sets the HTML attribute's of the <input>; type=date renders the date selector, and min='2018-09-10' specifies a disallowed date range:
In the following view, I am calculating the min and max dates, which output correctly in the sanity check:
class JourneyCreateView(CreateView):
model = Journey
form_class = JourneyForm
def get_initial(self):
try:
# Calculate date limit for the date picker
min = Claim.objects.get(id=self.kwargs['claim']).week_commencing
max = min + timedelta(days=7)
# Obtain the claim ID from the URL
claim = self.kwargs['claim']
# Sanity check
print (claim, min, max)
return {'claim': claim, 'min':min, 'max':max}
except Exception as ex:
print (ex)
return {}
def get_form_kwargs(self, *args, **kwargs):
# Only show claims owned by the logged in user in the claim dropdown
kwargs = super().get_form_kwargs(*args, **kwargs)
kwargs['alloweduser'] = self.request.user.id
return kwargs
And the Form:
class JourneyForm(forms.ModelForm):
# set html attribs which will apply to the form.
date = forms.CharField(widget=forms.TextInput(attrs={'type':'date',
'min':'2018-09-10'
}))
class Meta:
model = Journey
fields = ['date', 'distance','claim']
def __init__(self,alloweduser,*args,**kwargs):
# Make sure only to display the user's own claims.
super (JourneyForm,self ).__init__(*args,**kwargs)
self.fields['claim'].queryset = Claim.objects.filter(tech_id=alloweduser)
In this code claim is also returned by get_initial() and correctly pre-populates the claim dropdown with the current claim:
return {'claim': claim, 'min':min, 'max':max}
However, where I am confused is how I access the min and max variables, returned by get_initial() in the third line of the form, to replace the manual test string.
I have solved this myself. My solution is:
In the CreateView, put the logic in get_from_kwargs instead of the get_initial overide :
d = Claim.objects.get(id=self.kwargs['claim']).week_commencing
kwargs['min'], kwargs['max'] = d, d+timedelta(days=7)
The min and max then become arguments when overiding __init__ in the ModelForm:
def __init__(self,alloweduser,min,max,*args,**kwargs):
# Make sure only to display the user's own claims.
super (JourneyForm,self ).__init__(*args,**kwargs)
self.fields['claim'].queryset = Claim.objects.filter(tech_id=alloweduser)
self.fields['date'] = forms.CharField(widget=forms.TextInput(attrs={'type':'date',
'min':min, 'max':max
}))
Related
I am trying to refactor my code to inherit FormView instead of View. The view I'm working with receives values in the GET request. I retrieve the values in the get_context_data method and pass them through different functions to end up with a set of variables that I can pass in the context.
In short:
For the sake of example, the set of variables includes variables FOO and BAR. I need to initialise my form by passing variable FOO in the kwargs and additionally set my form field's initial value to BAR. I understand I should use the get_initial() and get_form_kwargs() methods to do this. I am just struggling with how to get FOO and BAR from the get_context_data method.
I tried adding FOO and BAR to the context dictionary:
context = super().get_context_data(**kwargs)
context["FOO"] = foo
context["BAR"] = bar
return context
And then calling it from the other methods:
def get_initial(self):
""" Get initial value for the form field """
initial = super(NameOfView, self).get_initial()
context = self.get_context_data()
initial_value = context["BAR"]
initial.update({'name': inital_value})
return initial
and the same for get_form_kwargs. But I get a RecursionError:
maximum recursion depth exceeded while calling a Python object
Any help understanding how I can acheive this will be appreciated
UPDATE: My Actual code is a bit more like this:*
class ConfirmTripView(FormView):
"""
Provides the user a set of choice options based on their search input in
the products.TripsView
"""
model = Booking
template_name = "bookings/trips_available.html"
form_class = DateChoiceForm
def __init__(self):
self.searched_date = None
self.passengers = None
self.destination_id = None
self.gte_dates = None
self.lt_dates = None
def convert_to_int(self, type_tuple):
""" Converts tuple value to integer """
type_int = int(''.join(type_tuple))
return type_int
def get_available_trips(self, destination, passengers):
""" Find trips with enough seats for searched no. of passengers """
available_trips = Trip.objects.filter(
destination=destination
).filter(seats_available__gte=passengers)
return available_trips
def get_trips_matched_or_post_date(self, date, destination, passengers):
"""
Returns trips that either match or are post- searched_date
Refine to trips with dates closest to searched_date
limit to 3 results
"""
available_trips = self.get_available_trips(destination, passengers)
gte_dates = available_trips.filter(date__gte=date)[:3]
return gte_dates
def get_trips_preceding_date(self, date, destination, passengers):
"""
Returns trips that are pre- searched_date
Refines to trips with dates closest to searched_date
limits to 3 results
"""
available_trips = self.get_available_trips(destination, passengers)
lt_dates = available_trips.filter(date__lt=date).order_by("-date")[:3]
return lt_dates
def make_timezone_naive(self, obj):
""" Turns date attribute to a time-zone naive date object """
date_attr = obj.date
date_string = date_attr.strftime("%Y-%m-%d")
datetime_naive = datetime.strptime(date_string, "%Y-%m-%d")
return datetime_naive
def get_trips_queryset(self, gte_dates, lt_dates):
""" Creates the queryset that will be used by the ModelChoiceField
in the DateChoiceForm """
# Merge both queries
trips = lt_dates | gte_dates
trips = trips.order_by('date')
return trips
def get_initial(self, **kwargs):
""" Takes values from get request and formulates variables
to be used in the form """
# Values from GET request
self.searched_date = self.request.GET.get('request_date')
self.passengers = self.request.GET.get('passengers')
self.destination_id = self.convert_to_int(
self.request.GET.get("destination")
)
# Return querysets for dates before/beyond searched_date respectively:
self.gte_dates = self.get_trips_matched_or_post_date(
self.searched_date,
self.destination_id,
self.passengers)
self.lt_dates = self.get_trips_preceding_date(
self.searched_date,
self.destination_id,
self.passengers)
naive_searched_date = datetime.strptime(self.searched_date, "%Y-%m-%d")
# Find the trip closest to the searched_date (for form initial value)
if self.gte_dates:
gte_date = self.gte_dates[0]
naive_gte_date = self.make_timezone_naive(gte_date)
if self.lt_dates:
lt_date = self.lt_dates[0]
naive_lt_date = self.make_timezone_naive(lt_date)
if (
naive_gte_date - naive_searched_date
> naive_searched_date - naive_lt_date
):
default_selected = lt_date
else:
default_selected = gte_date
else:
default_selected = gte_date
elif self.lt_dates:
lt_date = self.lt_dates[0]
default_selected = lt_date
else:
messages.error(
self.request,
"Sorry, there are no dates currently available for the"
"selected destination.",
)
# Get initial valuees for the form
initial = super(ConfirmTripView, self).get_initial()
initial.update({'trip': default_selected})
return initial
def get_form_kwargs(self, **kwargs):
""" Provides keyword arguemnt """
kwargs = super(ConfirmTripView, self).get_form_kwargs()
trips = self.get_trips_queryset(self.gte_dates, self.lt_dates)
kwargs.update({'trips': trips})
return kwargs
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
destination = Product.objects.filter(id=self.destination_id)
context["passengers"] = self.passengers
context["destination_obj"] = destination
return context
def form_valid(self, form):
"""
Takes the POST data from the DateChoiceForm and creates an
Intitial Booking in the database
"""
booking = form.save(commit=False)
booking.status = "RESERVED"
booking.save()
trip = form.cleaned_data['trip']
destination = trip.destination
booking_line_item = BookingLineItem(
booking=booking,
product=destination,
quantity=self.request.GET.get("passengers")
)
booking_line_item.save()
return redirect('create_passengers', booking.pk)
First of all, bookmark this.
Second, get_initial() and get_context_data() solve 2 different problems:
get_initial is to pass initial values to a form.
get_context_data is to pass variables to a template
As you can see in above site, the form is injected into the template variables through get_context_data() and that's where your recursion problem comes from:
- get()
|- get_context_data() <----------------------------------\
|- get_form() |
|- get_form_kwargs() |
|- get_initial() --> you call get_context_data here ---/
Now, how your GET parameters and form should be working together is unclear from your question, but if you need some values from GET for initial form values, then get them inside get_initial().
UPDATE:
I'd not have a method get_queryset() with a signature like this, reason is that several views dealing with models also have a get_queryset() method with a different signature. get_trips() in this context makes a lot of sense
You've already extracted a few functionalities, which is good, but the "finding closest to searched date" can be extracted as well and it's result stored on self.
It's possible to use __range lookups which probably makes your logic easier. Change the semantics to "find trips between 30 days before and 30 days after search date". This is not the same but a good enough approach in practical terms.
If you're still stuck, let us know on what specifically.
I have two models:
class User(models.Model):
name = models.CharField(max_length=32)
class Referral(models.Model):
referring_user = models.ForeignKey(User, related_name="referrals")
referred_user = models.ForeignKey(User, related_name="referrers")
percentage = models.PositiveIntegerField()
The idea is that every user has n referrers, and should have at least one. Each referrer has a percentage value which should add up to 100% when added to the other referrers.
So User "Alice" might have referrers "Bob" (50%) and "Cynthia" (50%), and User "Donald" might have one referrer: "Erin" (100%).
The problem I have is with validation. Is there a way (preferably one that plays nice with the Django admin using admin.TabularInline) that I can have validation reject the saving of a User if the sum of Refferrals != 100%?
Ideally I want this to happen at the form/admin level and not by overriding User.save(), but at this point I don't know where to start. Most of Django's validation code appears to be atomic, and validation across multiple rows is not something I've done in Django before.
After Jerry Meng suggested I look into the data property and not cleaned_data, I started poking around admin.ModelAdmin to see how I might access that method. I found get_form which appears to return a form class, so I overrode that method to capture the returning class, subclass it, and override .clean() in there.
Once inside, I looped over self.data, using a regex to find the relevant fields and then literally did the math.
import re
from django import forms
from django.contrib import admin
class UserAdmin(admin.ModelAdmin):
# ...
def get_form(self, request, obj=None, **kwargs):
parent = admin.ModelAdmin.get_form(self, request, obj=None, **kwargs)
class PercentageSummingForm(parent):
def clean(self):
cleaned_data = parent.clean(self)
total_percentage = 0
regex = re.compile(r"^referrers-(\d+)-percentage$")
for k, v in self.data.items():
match = re.match(regex, k)
if match:
try:
total_percentage += int(v)
except ValueError:
raise forms.ValidationError(
"Percentage values must be integers"
)
if not total_percentage == 100:
raise forms.ValidationError(
"Percentage values must add up to 100"
)
return cleaned_data
return PercentageSummingForm
As per the Django docs, clean() is the official function to implement for your purposes. You could imagine a function that looks like this:
from django.core.exceptions import ValidationError
def clean(self):
total_percentage = 0
for referrer in self.referrers.all():
total_percentage += referrer.percentage
if total_percentage !== 100:
raise ValidationError("Referrer percentage does not equal 100")
I'm using Django 1.4 with Python 2.7 on Ubuntu 12.10.
I have a form where I need to populate a few drop-downs dynamically (using jQuery) but need 2 of them to be required and the 3rd to be optional.
I'm using Tastypie to help with the API to get the options. Basically the first drop-down is populated with industry level codes for schools. Once a code is selected a category drop-down is populated for all categories for that code. Once the category is chosen a subcategory drop-down is populated for all subcategories for that combination of code and category.
I'm able to require the code drop-down (it's not dynamically populated). However, I'm having a tough time getting the category drop-down to be required. There are basically 2 routes I can take - front-end validation or back-end validation. I'm trying to go with back-end validation so I can easily create further validation if needed.
Here is the form:
class SchoolProductForm(forms.ModelForm):
cip_category = forms.ChoiceField(required=True,
choices=(('', '----------'),))
def __init__(self, *args, **kwargs):
super(SchoolProductForm, self).__init__(*args, **kwargs)
self.fields['short_description'].widget = TA_WIDGET
self.fields['salary_info'].widget = TA_WIDGET
self.fields['job_opportunities'].widget = TA_WIDGET
self.fields['related_careers'].widget = TA_WIDGET
self.fields['meta_keywords'].widget = TI_WIDGET
self.fields['meta_description'].widget = TI_WIDGET
self.fields['cip'].queryset = models.CIP.objects.filter(
parent_id__isnull=True)
class Meta:
model = models.SchoolProduct
exclude = ('campus',)
I've tried to override the clean method. I've tried to create a field specific clean method. Neither seem to work.
Variations of the following:
def clean(self):
super(SchoolProductForm, self).clean()
if cip_category in self._errors:
del self._errors['cip_category']
if self.cleaned_data['cip_category'] == '----------':
self._errors['cip_category'] = 'This field is required.'
return self.cleaned_data
This gives an error that there is no cip_category in cleaned_data, which makes sense because it didn't validate.
I've tried variations with the field specific clean:
def clean_cip_category(self):
data = self.cleaned_data['cip_category']
self.fields['cip_category'].choices = data
return data
But get a validation error on the page stating my choice is not one of the available choices.
I've tried to create a dynamic field type (several variations):
class DynamicChoiceField(forms.ChoiceField):
def valid_value(self, value):
return True
class SchoolProductForm(forms.ModelForm):
cip_category = DynamicChoiceField(required=True,
choices=(('', '----------'),))
But it accepts ---------- as a valid option (which I don't want) and causes an error since the ORM tries to match a value of ---------- in the database (which it won't find).
Any ideas?
I was able to solve this with a little overriding of a method in ChoiceField.
I added the field to the form and handled the pre-population with the self.initial:
class SchoolProductForm(forms.ModelForm):
cip_category = common_forms.DynamicChoiceField(
required=True, choices=(('', '----------'),))
def __init__(self, *args, **kwargs):
super(SchoolProductForm, self).__init__(*args, **kwargs)
self.fields['short_description'].widget = TA_WIDGET
self.fields['salary_info'].widget = TA_WIDGET
self.fields['job_opportunities'].widget = TA_WIDGET
self.fields['related_careers'].widget = TA_WIDGET
self.fields['meta_keywords'].widget = TI_WIDGET
self.fields['meta_description'].widget = TI_WIDGET
self.fields['cip'].queryset = models.CIP.objects.filter(
parent_id__isnull=True)
# Get the top parent and pre-populate
if 'cip' in self.initial:
self.initial['cip'] = models.CIP.objects.get(
pk=self.initial['cip']).top_parent()
class Meta:
model = models.SchoolProduct
exclude = ('campus',)
Where DynamicChoiceField is:
class DynamicChoiceField(forms.ChoiceField):
def valid_value(self, value):
return True
Then, in the view I added handling in the form_valid override:
def form_valid(self, form):
self.object = form.save(commit=False)
# Handle the CIP code
self.object.cip_id = self.request.POST.get('cip_subcategory')
if self.object.cip_id == '':
self.object.cip_id = self.request.POST.get('cip_category')
self.object.save()
First, I did look at this question, but its over a year old. Surely now there is a good way in Django 1.1.1 to carry filter selection forward after a user clicks the save button in the Admin.
In a table with thousands of records, filtering is essential. And if a user makes several filter choices that effort shouldn't have to be repeated.
The answer is still the same: out of the box, Django doesn't support this behavior. There are a couple of tickets in the issue tracker with patches: #3777, #6903. The middleware class in this comment works without modifying Django code.
This feature has been added to Django as part of the 1.6 release and is enabled now by default. It is described in the release notes:
ModelAdmin now preserves filters on the list view after creating,
editing or deleting an object. It’s possible to restore the previous
behavior of clearing filters by setting the preserve_filters attribute
to False.
another way is to use this snippet http://djangosnippets.org/snippets/2531/
Class Modeladmin_perso(admin.ModelAdmin):
def add_view(self, request, *args, **kwargs):
result = super(Modeladmin_perso, self).add_view(request, *args, **kwargs )
# Look at the referer for a query string '^.*\?.*$'
ref = request.META.get('HTTP_REFERER', '')
if ref.find('?') != -1:
# We've got a query string, set the session value
request.session['filtered'] = ref
if request.POST.has_key('_save'):
"""
We only kick into action if we've saved and if
there is a session key of 'filtered', then we
delete the key.
"""
try:
if request.session['filtered'] is not None:
result['Location'] = request.session['filtered']
request.session['filtered'] = None
except:
pass
return result
"""
Used to redirect users back to their filtered list of locations if there were any
"""
def change_view(self, request, object_id, extra_context={}):
"""
save the referer of the page to return to the filtered
change_list after saving the page
"""
result = super(Modeladmin_perso, self).change_view(request, object_id, extra_context )
# Look at the referer for a query string '^.*\?.*$'
ref = request.META.get('HTTP_REFERER', '')
if ref.find('?') != -1:
# We've got a query string, set the session value
request.session['filtered'] = ref
if request.POST.has_key('_save'):
"""
We only kick into action if we've saved and if
there is a session key of 'filtered', then we
delete the key.
"""
try:
if request.session['filtered'] is not None:
result['Location'] = request.session['filtered']
request.session['filtered'] = None
except:
pass
return result
the good thing is you don't have to hack anything.
This feature has been a request to the Django project for a long time (the ticket was opened 5 years ago).
Fortunately this annoying behavior was fixed in trunk. Expect it to be included in Django 1.6.
Here's what I did inside render_change_form to generate a back button with preserved_filters.
def generate_back_url(self, request):
opts = self.model._meta
post_url = reverse(
"admin:%s_%s_changelist" % (opts.app_label, opts.model_name),
current_app=self.admin_site.name,
)
preserved_filters = self.get_preserved_filters(request)
return add_preserved_filters(
{"preserved_filters": preserved_filters, "opts": opts}, post_url
)
Can I use a middleware to preserve several user selected choices in between page requests?
I have several values, namely vehicle year, make, model, series, style, color, and transmission. I want to have the user being able to select a choice while keeping the previously selected choice active. I do not want to use sessions for this as I want the URL to be bookmarkable.
I was thinking of something like:
def get_choices(self):
try:
return self.REQUEST["year"]
except (KeyError, ValueError, TypeError):
return 1
class ChoicesMiddleware(object):
def process_request(self, request):
.....
I'm also not sure how to return all the choices under get_choices().
EDIT 1
def get_choices(self):
user_choices = {}
for key in ["year", "model", "make", "series", "style"]:
user_choices[key] = self.REQUEST.get(key)
return user_choices
class ChoicesMiddleware(object):
def process_request(self, request):
return get_choices(self)
EDIT 2
My URLConf is as below:
(r'^inventory/(?P<year>\d{4})(?P<make>[-\w\s]+)
(?P<model>[-\w\s]+)(?P<series>[-\w\s]+)(?P<body>[-\w\s]+)
(?P<exterior>[-\w\s]+)(?P<interior>[-\w\s]+)
(?P<transmission>[-\w\s]+)$', 'inventory'),
Then the view is as below:
def inventory(request, page_by=None, year=None, make=None,
model=None, series=None, body=None, interior=None, exterior=None,
transmission=None):
#Initialize empty variable list.
kwargs = {}
if "year" in request.GET:
year = request.REQUEST["year"]
kwargs['common_vehicle__year__year__exact'] = year
....The rest of the vars are populated in the same way.
Do you want to automatically add the user choices as GET parameters to the URL?
I do not think you would be able to add GET request parameters to a URL via middleware.
You can store them in GET, no problem there. Return via dict. I didn't understand the part about preserving user's choice - you want to have several options for year, for example? Then you need arrays in GET, not values. But for values its simple:
def get_choices(self):
user_choices = {}
for key in ["year", "model", "maker"]:
user_choices[key] = self.REQUEST.get(key)
return user_choices