Validate a dynamic select field in Django - django

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()

Related

Looking for format for KeywordsField.save_form_data

I have a Mezzanine Project and am trying to update the keywords on a blog entry. I am having difficulty getting the format correct to call KeywordsField.save_form_data this invokes a js that will update the keywords on a blog post. See below:
From Messanine/generic/fields.py
class KeywordsField(BaseGenericRelation):
"""
Stores the keywords as a single string into the
``KEYWORDS_FIELD_NAME_string`` field for convenient access when
searching.
"""
default_related_model = "generic.AssignedKeyword"
fields = {"%s_string": CharField(editable=False, blank=True,
max_length=500)}
def __init__(self, *args, **kwargs):
"""
Mark the field as editable so that it can be specified in
admin class fieldsets and pass validation, and also so that
it shows up in the admin form.
"""
super(KeywordsField, self).__init__(*args, **kwargs)
self.editable = True
def formfield(self, **kwargs):
"""
Provide the custom form widget for the admin, since there
isn't a form field mapped to ``GenericRelation`` model fields.
"""
from mezzanine.generic.forms import KeywordsWidget
kwargs["widget"] = KeywordsWidget
return super(KeywordsField, self).formfield(**kwargs)
def save_form_data(self, instance, data):
"""
The ``KeywordsWidget`` field will return data as a string of
comma separated IDs for the ``Keyword`` model - convert these
into actual ``AssignedKeyword`` instances. Also delete
``Keyword`` instances if their last related ``AssignedKeyword``
instance is being removed.
"""
from mezzanine.generic.models import Keyword
related_manager = getattr(instance, self.name)
# Get a list of Keyword IDs being removed.
old_ids = [str(a.keyword_id) for a in related_manager.all()]
new_ids = data.split(",")
removed_ids = set(old_ids) - set(new_ids)
# Remove current AssignedKeyword instances.
related_manager.all().delete()
# Convert the data into AssignedKeyword instances.
if data:
data = [related_manager.create(keyword_id=i) for i in new_ids]
# Remove keywords that are no longer assigned to anything.
Keyword.objects.delete_unused(removed_ids)
super(KeywordsField, self).save_form_data(instance, data)
From my Views.py
class PubForm(forms.ModelForm):
class Meta:
model = BlogPost
fields = ['keywords']
def UpdatePub(request, slug):
blog_post = BlogPost.objects.get(id=slug)
if request.method == 'POST':
form = PubForm(request.POST)
if form.is_valid():
publish_date = datetime.datetime.now()
blog_post.status = CONTENT_STATUS_PUBLISHED
publish_date=publish_date
tags=form.cleaned_data['keywords']
blog_post.save()
KeywordsField.save_form_data(user,blog_post,tags)
return HttpResponseRedirect('/write/')
else:
form = PubForm(instance=blog_post)
return render(request, 'blog_my_pub.html', {'form' : form})
It complains that the field 'user' has no attribute 'name'. I have tried many different values for this parameter and cannot figure it out. Any help would be appreciated.
Thanks for any input.

How to access run-time request.session values in forms.py definition?

I have an inventory management app that will be serving multiple locations (called contexts in my app). When a user is logged in, their current context is stored as a value in request.sessions.
I would like users to only be able to browse and retrieve records for their own location.
I've been trying to this by filtering the queryset that is called in the form definition to populate the select dropdown, i.e.
referenced_catalog = forms.ModelChoiceField(
queryset=Inventory_unit_catalog.objects.all().filter(parent_business_unit_context_id=user_context_id),
I've tried implementing several different (but similar) approaches from various SO posts, that involve defining an init block to the form, such as:
class InventoryStockAddForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.user_context_id = kwargs.pop('user_context_id', None)
super(InventoryStockAddForm, self).__init__(*args, **kwargs)
name = forms.CharField(max_length=96,widget=forms.TextInput(),required=True)
referenced_catalog = forms.ModelChoiceField(
queryset = Inventory_unit_catalog.objects.all().filter(parent_business_unit_context_id=self.user_context_id),
label = u"",
widget = ModelSelect2Widget(
model=Inventory_unit_catalog,
search_fields=['name__icontains'],
attrs={'data-placeholder': 'Select catalog...', 'data-width': '35em'},
required=False))
class Meta():
model = Inventory_unit_stock
fields = ('name',)
(Different SO answers had one way or the other.)
Then in views.py:
user_context_id = request.session.get('user_context_id')
...
add_form = InventoryStockAddForm(user_context_id=user_context_id)
I've even tried using the SessionStore per https://djangobook.com/using-sessions-views-2/:
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
s = SessionStore()
user_context_id = s['user_context_id']
but it always fails at the moment the forms.py is updated as Django validates the code and cannot find a key value at the moment of validation.
Any advice would be appreciated, thanks!
You can't access self.user_context_id inside referenced_catalog = forms.ModelChoiceField(...) - that code runs when the module is loaded, not when the form is initialised.
Instead, you should set the queryset inside the __init__ method.
class InventoryStockAddForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.user_context_id = kwargs.pop('user_context_id', None)
super(InventoryStockAddForm, self).__init__(*args, **kwargs)
self.fields['referenced_catalog'].queryset = Inventory_unit_catalog.objects.all().filter(parent_business_unit_context_id=self.user_context_id)
referenced_catalog = forms.ModelChoiceField(
queryset = Inventory_unit_catalog.objects.none(),
label = u"",
widget = ModelSelect2Widget(
model=Inventory_unit_catalog,
search_fields=['name__icontains'],
attrs={'data-placeholder': 'Select catalog...', 'data-width': '35em'},
required=False))

Django: Accessing parent object in new linline object

I have been combing through the internet for quite some while without finding any solution to this problem.
What I am trying to do...
I have the following models:
class TrackingEventType(models.Model):
required_previous_event = models.ForeignKey(TrackingEventType)
class TrackingEvent(models.Model):
tracking = models.ForeignKey(Tracking)
class Tracking(models.Model):
last_event = models.ForeignKey(TrackingEvent)
Now the main model is Tracking, so my admin for Tracking looks like this:
class TrackingEventInline(admin.TabularInline):
model = TrackingEvent
extra = 0
class TrackingAdmin(admin.ModelAdmin):
inlines = [TrackingEventInline]
That's it for the current setup.
Now my quest:
In the TrackingAdmin, when I add new TrackingEvent inlines, I want to limit the options of TrackingEventType to onlye those, that are allowed to follow on the last TrackingEvent of the Tracking. (Tracking.last_event == TrackingEventType.required_previous_event).
For this, I would need to be able to access the related Tracking on the InlineTrackingEvent, to access the last_event and filter the options for TrackingEventType accordingly.
So I found this: Accessing parent model instance from modelform of admin inline, but when I set up TrackingEventInline accordingly:
class MyFormSet(forms.BaseInlineFormSet):
def _construct_form(self, i, **kwargs):
kwargs['parent_object'] = self.instance
print self.instance
return super(MyFormSet, self)._construct_form(i, **kwargs)
class MyForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
print kwargs
self.parent_object = kwargs.pop('parent_object')
super(MyForm, self).__init__(*args, **kwargs)
class TrackingEventInline(admin.TabularInline):
form = MyForm
formset = MyFormSet
model = TrackingEvent
extra = 0
I get a KeyError at /admin/.../tracking/2/change/ 'parent_object' from self.parent_object = kwargs.pop('parent_object')
Does anyone know how to solve this? Am I approaching the problem the wrong way? I guess this would be pretty easy in a custom form in the frontend, but I really want to use the admin, because the whole application is built to be used from the admin, and it would be a hell lot of work to build a custom admin interface just because of this problem :)
Ok, so posting on StackOverflow is always helping to get the problem straight. I was able to put together a solution that works for me.
It includes defining my own Form in a outer function, as well as defining two InlineAdmin objects for TrackingEvent (one for update / edit, one just for insert).
Here's the code:
def create_trackingevent_form(tracking):
"""
"""
class TrackingEventForm(forms.ModelForm):
"""
Form for Tracking Event Inline
"""
def clean(self):
"""
May not be needed anymore, since event type choices are limited when creating new event.
"""
next_eventtype = self.cleaned_data['event_type']
tracking = self.cleaned_data['tracking']
# get last event, this also ensures last_event gets updated everytime the change form for TrackingEvent is loaded
last_eventtype = tracking.set_last_event()
if last_eventtype:
last_eventtype = last_eventtype.event_type
pk = self.instance.pk
insert = pk == None
# check if the event is updated or newly created
if insert:
if next_eventtype.required_previous_event == last_eventtype:
pass
else:
raise forms.ValidationError('"{}" requires "{}" as last event, "{}" found. Possible next events: {}'.format(
next_eventtype,
next_eventtype.required_previous_event,
last_eventtype,
'"%s" ' % ', '.join(map(str, [x.name for x in tracking.next_tracking_eventtype_options()]))
)
)
else:
pass
return self.cleaned_data
def __init__(self, *args, **kwargs):
# You can use the outer function's 'tracking' here
self.parent_object = tracking
super(TrackingEventForm, self).__init__(*args, **kwargs)
self.fields['event_type'].queryset = tracking.next_tracking_eventtype_options()
#self.fields['event_type'].limit_choices_to = tracking.next_tracking_eventtype_options()
return TrackingEventForm
class TrackingEventInline(admin.TabularInline):
#form = MyForm
#formset = MyFormSet
model = TrackingEvent
extra = 0
#readonly_fields = ['datetime', 'event_type', 'note']
def has_add_permission(self, request):
return False
class AddTrackingEventInline(admin.TabularInline):
model = TrackingEvent
extra = 0
def has_change_permission(self, request, obj=None):
return False
def queryset(self, request):
return super(AddTrackingEventInline, self).queryset(request).none()
def get_formset(self, request, obj=None, **kwargs):
if obj:
self.form = create_trackingevent_form(obj)
return super(AddTrackingEventInline, self).get_formset(request, obj, **kwargs)
I hope this helps other people with the same problem.. Some credit to the Stack Overflow threads that helped me come up with this:
Prepopulating inlines based on the parent model in the Django Admin
Limit foreign key choices in select in an inline form in admin
https://docs.djangoproject.com/en/1.9/ref/models/instances/#django.db.models.Model.clean_fields
Please do not hesitate to ask questions if you have any

ModelChoiceField in forms.Form won't validate if queryset is overridden

I have a django ModelChoiceField that won't validate if I override the queryset.
class PersonalNote(forms.Form):
tile = ModelChoiceField(queryset=Tile.objects.none())
note = forms.CharField()
form = PersonalNote()
form.fields['tile'].queryset = Tile.objects.filter(section__xxx=yyy)
The form.is_valid() error is: "Select a valid choice. That choice is not one of the available choices".
If Tile.objects.none() is replaced with Tile.objects.all() it validates, but loads far too much data from the database. I've also tried:
class PersonalNote(forms.Form):
tile = ModelChoiceField(queryset=Tile.objects.none())
note = forms.CharField()
def __init__(self, *args, **kwargs):
yyy = kwargs.pop('yyy', None)
super(PersonalNote, self).__init__(*args, **kwargs)
if yyy:
self.fields['tile'].queryset = Tile.objects.filter(section__xxx=yyy)
What might be wrong here? Note the real application also overrides the label, but that does not seem to be a factor here:
class ModelChoiceField2(forms.ModelChoiceField):
def label_from_instance(self, obj):
assert isinstance(obj,Tile)
return obj.child_title()
After 2 hours I found the solution. Because you specified a queryset of none in the class definition, when you instantiate that PersonalNote(request.POST) to be validated it is referenceing a null query set
class PersonalNote(forms.Form):
tile = ModelChoiceField(queryset=Tile.objects.none())
note = forms.CharField()
To fix this, when you create your form based on a POST request be sure to overwrite your queryset AGAIN before you check is_valid()
def some_view_def(request):
form = PersonalNote(request.POST)
**form.fields['tile'].queryset = Tile.objects.filter(section__xxx=yyy)**
if form.is_valid():
#Do whatever it is
When you pass an empty queryset to ModelChoiceField you're saying that nothing will be valid for that field. Perhaps you could filter the queryset so there aren't too many options.
I also had this problem. The idea is to dynamically change the queryset of a ModelChoiceField based on a condition (in my case it was a filter made by another ModelChoiceField).
So, having the next model as example:
class FilterModel(models.Model):
name = models.CharField()
class FooModel(models.Model):
filter_field = models.ForeignKey(FilterModel)
name = models.CharField()
class MyModel(models.Model):
foo_field = models.ForeignKey(FooModel)
As you can see, MyModel has a foreign key with FooModel, but not with FilterModel. So, in order to filter the FooModel options, I added a new ModelChoiceField on my form:
class MyForm(forms.ModelForm):
class Meta:
model = MyModel
def __init__(self, *args, **kwargs):
# your code here
self.fields['my_filter_field'] = forms.ModelChoiceField(FilterModel, initial=my_filter_field_selected)
self.fields['my_filter_field'].queryset = FilterModel.objects.all()
Then, on your Front-End you can use Ajax to load the options of foo_field, based on the selected value of my_filter_field. At this point everyting should be working. But, when the form is loaded, it will bring all the posible options from FooModel. To avoid this, you need to dynamically change the queryset of foo_field.
On my form view, I passed a new argument to MyForm:
id_filter_field = request.POST.get('my_filter_field', None)
form = MyForm(data=request.POST, id_filter_field=id_filter_field)
Now, you can use that argument on MyForm to change the queryset:
class MyForm(forms.ModelForm):
# your code here
def __init__(self, *args, **kwargs):
self.id_filter_field = kwargs.pop('id_filter_field', None)
# your code here
if self.id_filter_field:
self.fields['foo_field'].queryset = FooModel.objects.filter(filter_field_id=self.id_filter_field)
else:
self.fields['foo_field'].queryset = FooModel.objects.none()

Django mutable POST doesn't appear in cleaned_data

I have been searching around for this for two days now, but I couldn't find any realiable solution.
form:
class SMSSettingsForm(forms.ModelForm):
smsQuota = forms.IntegerField(label=_("Account Quota"), max_value=432000, min_value=1, required=True, help_text=_('(mins)'), error_messages={'required': _('This field cannot be empty')})
smsTimeout = forms.IntegerField(label=_("Timeout"), max_value=9999999, min_value=1,required=False, help_text=_("(mins)"))
class Meta:
model = Settings
fields = ("smsQuota", "smsTimeout")
def __init__(self, *args, **kwargs):
super(SMSSettingsForm, self).__init__(*args, **kwargs)
def save(self):
settings = SettingsManager.get()
settings.smsQuota = self.cleaned_data['smsQuota']
settings.smsTimeout = self.cleaned_data['smsTimeout']
# Following lines are extra fields, rendered by JS in HTML
settings.ck = self.cleaned_data['ck']
settings.ck_per = self.cleand_data['ck_per']
settings.save()
view:
form_with_extra_elem = request.POST.copy()
form_with_extra_elem['ck'] = request.POST.get("ck")
form_with_extra_elem['ck_per'] = request.POST.get("ck_per")
# The two lines above didn't work, so I tried the following, but didn't work again
#form_with_extra_elem.update({'ck': request.POST.get("ck")})
#form_with_extra_elem.update({'ckper': request.POST.get("ck_per")})
form = SMSSettingsForm(form_with_extra_elem)
Do you have any idea how to solve this? What I think is the new element doesn't pass by the validation, so I cannot use them. But how to make them to do so? Actually, I don't need any validation - is there any other way than cleaned_data, to access form parameters?
I don't understand why you want to add extra fields via JS only. If you want them to appear in cleaned_data, they have to be part of the form. You can declare extra fields on a ModelForm simply by specifying them like you have with the other fields:
class SMSSettingsForm(forms.ModelForm):
smsQuota = forms.IntegerField(...)
smsTimeout = forms.IntegerField(...)
ck_per = forms.IntegerField()
ck = forms.IntegerField()