Django form validation, raise error on fieldset - django

I know how to raise an error on a specific field in my django admin form, but I would like the error to be raised on a fieldset. I currently have a list of check boxes in a fieldset and would like an error to be raised, not on a specific field (aka specific check box), but on the entire fieldset.
here is my django admin.py
class EventAdmin(admin.ModelAdmin):
form = EventForm
fieldsets = [
(None, {'fields': [
'approval_state',
'title',
'description'
]
}
),
('Group Owner', {'fields': [
'grpOwner_vcoe',
'grpOwner_cssi',
'grpOwner_essc',
'grpOwner_tmscc',
'grpOwner_inmc',
'grpOwner_cc7',
'grpOwner_ias',
]
}
), ...
class EventForm(forms.ModelForm):
# Form validation
def clean(self):
# Collect data
start = self.cleaned_data.get('start')
end = self.cleaned_data.get('end')
grpOwner_vcoe = self.cleaned_data.get('grpOwner_vcoe')
grpOwner_cssi = self.cleaned_data.get('grpOwner_cssi')
grpOwner_essc = self.cleaned_data.get('grpOwner_essc')
grpOwner_tmscc = self.cleaned_data.get('grpOwner_tmscc')
grpOwner_inmc = self.cleaned_data.get('grpOwner_inmc')
grpOwner_cc7 = self.cleaned_data.get('grpOwner_cc7')
grpOwner_ias = self.cleaned_data.get('grpOwner_ias')
if not (grpOwner_vcoe or grpOwner_cssi or grpOwner_essc or grpOwner_tmscc or grpOwner_inmc or grpOwner_cc7 or grpOwner_ias):
if not self._errors.has_key('Group Owner'):
self._errors['Group Owner'] = ErrorList()
self._errors['Group Owner'].append('Test')
# Check start & end data
if start > end:
if not self._errors.has_key('start'):
self._errors['start'] = ErrorList()
self._errors['start'].append('Event start must occur before event end')
return self.cleaned_data
But this doesn't, I know I can just raise it on each field but I find it much more elegant if I could do it around the fielset

Django forms don't have a concept of fieldsets, they belong to the ModelAdmin class. Therefore there isn't an established way to assign errors to a fieldset instead of a particular field.
You could try overriding the admin templates, in particular, includes/fieldset.html. You could add some code to your form's clean method to make it easy to access the fieldset errors in the template.

Related

(Hidden field id) Select a valid choice. That choice is not one of the available choices. (Django)

I'm receiving this error when I try to submit two formsets. After I fill the form and click the save button, it gives the error:
(Hidden field id) Select a valid choice. That choice is not one of the available choices.
I'm trying to create dynamic form so that the user can add new sections and also new lectures inside the section when they click "Add" button. The adding new form function works well, I just have problem saving it to the database.
Views.py
def addMaterials(request, pk):
course = Course.objects.get(id=pk)
sections = CourseSection.objects.filter(course_id=pk)
materials = CourseMaterial.objects.filter(section__in=sections)
SectionFormSet = modelformset_factory(CourseSection, form=SectionForm, extra=0)
sectionformset = SectionFormSet(request.POST or None, queryset=sections)
MaterialFormSet = modelformset_factory(CourseMaterial, form=MaterialForm, extra=0)
materialformset = MaterialFormSet(request.POST or None, queryset=materials)
context = {
'course': course,
'sectionformset': sectionformset,
'materialformset': materialformset,
}
if request.method == "POST":
if all([sectionformset.is_valid() and materialformset.is_valid()]):
for sectionform in sectionformset:
section = sectionform.save(commit=False)
section.course_id = course.id
section.save()
for materialform in materialformset:
material = materialform.save(commit=False)
print(material)
material.section_id = section #section.id or section.pk also doesn't work
material.save()
return('success')
return render(request, 'courses/add_materials.html', context)
Forms.py
class SectionForm(forms.ModelForm):
class Meta:
model = CourseSection
fields = ['section_name', ]
exclude = ('course_id', )
class MaterialForm(forms.ModelForm):
class Meta:
model = CourseMaterial
fields = ['lecture_name', 'contents']
The second formset which is materialformset need the section id from the first formset hence why there is two loop in views.
Can someone help me to solve this. I'm not sure how to fix it.
This is the what I'm trying to do.
I'm new to django but I had to face with the same problem. My solution was to handle singularly each formset inside 'views.py'.
In the template.html, create a tag for each formset you have, than inside that tag put <input type="submit" name="form1">(Note that name is important and must be different with the respect of the form you are submitting).
Then in views.py, instead for writing if all([sectionformset.is_valid() and materialformset.is_valid()]), try like this:
if 'form1' in request.POST:
if sectionformset.is_valid():
sectionformset.save()
# other rows of your code
return('success')
if 'form2' in request.POST:
if materialformset.is_valid():
materialformset.save()
# etc. etc.

How to change serializer field name when validation error is triggered

I need to change the view of the error displayed when I validate the field.
serializer.py
class ElementCommonInfoSerializer(serializers.ModelSerializer):
self_description = serializers.CharField(required=False, allow_null=True,
validators=[RegexValidator(regex=r'^[a-zA-Z0-9,.!? -/*()]*$',
message='The system detected that the data is not in English. '
'Please correct the error and try again.')]
)
....
class Meta:
model = Elements
fields = ('self_description',......)
This error is displayed
{
"self_description": [
"The system detected that the data is not in English. Please correct the error and try again."
]
}
The key of error dict is field name - self_description. For FE I need to send another format like:
{
"general_errors": [
"The system detected that the data is not in English. Please correct the error and try again."
]
}
How to change this?
One way this could be achieved is via custom exception handler
from copy import deepcopy
from rest_framework.views import exception_handler
def genelalizing_exception_handler(exc, context):
# Call REST framework's default exception handler first,
# to get the standard error response.
response = exception_handler(exc, context)
# Now add the HTTP status code to the response.
if 'self_description' in response.data:
data = deepcopy(response.data)
general_errors = data.pop('self_description')
data['general_errors'] = general_errors
response.data = data
return response
in settings
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'my_project.my_app.utils. genelalizing_exception_handler'
}
Another solution is to rewrite the validate method.
def validate(self, data):
self_description = str((data['self_description']))
analyst_notes = str((data['analyst_notes']))
if re.match(r'^[a-zA-Z0-9,.!? -/*()]*$', self_description) or re.match(r'^[a-zA-Z0-9,.!? -/*()]*$', analyst_notes):
raise serializers.ValidationError({
"general_errors": [
"The system detected that the data is not in English. Please correct the error and try again."
]
})
return data
The solution is very simple.
you can rename the key field by using serializer method (source attribute)
below you can find an example code.
class QuestionSerializer(serializers.ModelSerializer):
question_importance = serializers.IntegerField(source='importance')
question_importance = serializers.IntegerField(required=False)
class Meta:
model = create_question
fields = ('id','question_importance','complexity','active')
Above you can see I have an importance field which is present in django model But here I renamed this field to question_importance by using source attribute .
In your case it will be like below,
class ElementCommonInfoSerializer(serializers.ModelSerializer):
general_errors = serializer.CharField(source="self_description")
general_error = serializers.CharField(required=False, allow_null=True,
validators=[])
class Meta:
model = Elements
fields = ('general_error',......)

Django custom validation of multiple fields

for which I want to validate a number of fields in a custom clean method.
I have this so far:
class ProjectInfoForm(forms.Form):
module = forms.ModelChoiceField(
queryset=Module.objects.all(),
)
piece = forms.CharField(
widget=forms.Select(),
required=False,
)
span = forms.IntegerField(
max_value=100,
initial=48
)
max_span = forms.IntegerField(
max_value=100,
initial=0
)
def clean(self):
span = self.cleaned_data['span']
max_span = self.cleaned_data['max_span']
piece = self.cleaned_data.['piece']
# validate piece
try:
Piece.objects.get(pk=m)
except Piece.DoesNotExist:
raise forms.ValidationError(
'Illegal Piece selected!'
)
self._errors["piece"] = "Please enter a valid model"
# validate spans
if span > max_span:
raise forms.ValidationError(
'Span must be less than or equal to Maximum Span'
)
self._errors["span"] = "Please enter a valid span"
return self.cleaned_data
However, this only gives me one of the messages if both clauses invalidate. How can I get all the invalid messages. Also I do not get the field-specific messages - how do I include a message to be displayed for the specific field?
Any help much appreciated.
Store the errors and don't raise them until the end of the method:
def clean(self):
span = self.cleaned_data['span']
max_span = self.cleaned_data['max_span']
piece = self.cleaned_data.['piece']
error_messages = []
# validate piece
try:
Piece.objects.get(pk=m)
except Piece.DoesNotExist:
error_messages.append('Illegal Piece selected')
self._errors["piece"] = "Please enter a valid model"
# validate spans
if span > max_span:
error_messages.append('Span must be less than or equal to Maximum Span')
self._errors["span"] = "Please enter a valid span"
if len(error_messages):
raise forms.ValidationError(' & '.join(error_messages))
return self.cleaned_data
You should write a custom clean_FIELDNAME method in this case. That way, field centric validation errors can later be displayed as such when using {{form.errors}} in your template. The clean method o.t.h. is for validating logic that spans more than one field. Take a look through the link I posted above, everything you need to know about validating django forms is in there.
It happens because you are using raise.
Try replace it by these two line in your code:
del self.cleaned_data['piece']
and
del self.cleaned_data['span']
It appears this has changed in later versions of Django (this seems to work in 2.1 and later):
from django import forms
class ContactForm(forms.Form):
# Everything as before.
...
def clean(self):
cleaned_data = super().clean()
cc_myself = cleaned_data.get("cc_myself")
subject = cleaned_data.get("subject")
if cc_myself and subject and "help" not in subject:
msg = "Must put 'help' in subject when cc'ing yourself."
self.add_error('cc_myself', msg)
self.add_error('subject', msg)
https://docs.djangoproject.com/en/dev/ref/forms/validation/#raising-multiple-errors has more details.

Django-selectable with dynamic inlines

I'm using django-selectable ( https://bitbucket.org/mlavin/django-selectable ) with
an admin tabularInline to get autocomplete functionality on one of the inline fields. It works for inlines added at creation time. The problem I'm having is that the autocomplete functionality isn't added when the user adds another row to the inline.
There's a bug and fix for this issue here
https://bitbucket.org/mlavin/django-selectable/issue/12/make-it-work-with-dynamically-added-forms
And looking at jquery.dj.selectable.js near the bottom is :
if (typeof(django) != "undefined" && typeof(django.jQuery) != "undefined") {
if (django.jQuery.fn.formset) {
var oldformset = django.jQuery.fn.formset;
django.jQuery.fn.formset = function(opts) {
var options = $.extend({}, opts);
var addedevent = function(row) {
bindSelectables($(row));
};
var added = null;
if (options.added) {
var oldadded = options.added;
added = function(row) { oldadded(row); addedevent(row); };
}
options.added = added || addedevent;
return oldformset.call(this, options);
};
}
}
It looks like this should make the autocomplete work with dynamically added rows, but I can't work out what to do for this to work.
The admin tabularInline.html has inline_admin_formset so should I be checking for that and not django.jQuery.fn.formset as in the code above ? Or somehow adding inline_admin_formset to django.jQuery.fn ?
Thanks very much for any suggestions.
I'm using version 0.2.
In forms.py there is the inline form :
class GrammarInlineForm(forms.ModelForm):
class Meta:
model = Grammar
widgets = {
'description' :forms.Textarea(attrs={'cols': 80, 'rows': 10, 'class': 'grammarInline'}),
'title' : selectable.AutoCompleteSelectWidget(lookup_class=GrammarLookup, allow_new=True),
}
exclude = ('creation_date', 'creator', 'plan')
def __init__(self, *args, **kwargs):
super(GrammarInlineForm, self).__init__(*args, **kwargs)
In admin.py the inline admin is made and added to the main admin ( PlanAdmin ) :
class GrammarInline(admin.TabularInline):
form = GrammarInlineForm
model = Grammar
extra = 2
def save_formset(self, request,form, formset, change):
instances = formset.save(commit=False)
for instance in instances:
instance.creator = request.user
instance.save()
formset.save_m2m()
class PlanAdmin(admin.ModelAdmin):
form = PlanForm
list_display = ('title', 'topic', 'level', 'description','public', )
inlines = [ ActivityInline, GrammarInline, ]
After reading your ticket http://code.djangoproject.com/ticket/15760 I tried binding to the inlines formsetadd event, like this
django.jQuery('.ui-autocomplete-input').live('formsetadd', function(e, row) {
console.log('Formset add!');
console.log($(row));
});
but looking at django/contrib/admin/media/js/inlines.js
it seems that these triggers aren't in version 1.3.1 of django. Is it necessary to bind to an event that gets triggered when an inline is added? There is a similar case here
https://bitbucket.org/mlavin/django-selectable/issue/31/dynamically-added-forms
but that's using the formset plugin. Is there a way to use bindSelectable(row) to the admin inline ?
The jquery.dj.selectable.js code you posted is there to patch django/contrib/admin/media/js/inlines.js to call bindSelectable(row) when a new row is added. http://code.djangoproject.com/ticket/15760 was opened so that this monkey patch isn't necessary but has not been closed and likely will not be closed for Django 1.4. Again you shouldn't need to do anything to make this work. You don't need to change the template. You don't need to write any additional JS.
The project source has a working example of using a dynamic tabular inline: https://bitbucket.org/mlavin/django-selectable/src/33e4e93b3fb3/example/core/admin.py#cl-39

custom html field in the django admin changelist_view

I'd like to some little customisation with the django admin -- particularly the changelist_view
class FeatureAdmin(admin.ModelAdmin):
list_display = (
'content_object_change_url',
'content_object',
'content_type',
'active',
'ordering',
'is_published',
)
list_editable = (
'active',
'ordering',
)
list_display_links = (
'content_object_change_url',
)
admin.site.register(get_model('features', 'feature'), FeatureAdmin)
The idea is that the 'content_object_change_url' could be a link to another object's change_view... a convenience for the admin user to quickly navigate directly to the item.
The other case I'd have for this kind of thing is adding links to external sources, or thumbnails of image fields.
I had thought I'd heard of a 'insert html' option -- but maybe I'm getting ahead of myself.
Thank you for your help!
You can provide a custom method on the FeatureAdmin class which returns HTML for content_object_change_url:
class FeatureAdmin(admin.ModelAdmin):
[...]
def content_object_change_url(self, obj):
return 'Click to change' % obj.get_absolute_url()
content_object_change_url.allow_tags=True
See the documentation.
Pay attention and use format_html (See docs here) as the mark_safe util has been deprecated since version 1.10. Moreover, support for the allow_tags attribute on ModelAdmin methods will be removed since version 1.11.
from django.utils.html import format_html
from django.contrib import admin
class FeatureAdmin(admin.ModelAdmin):
list_display = (
'change_url',
[...]
)
def change_url(self, obj):
return format_html('<a target="_blank" href="{}">Change</a>', obj.get_absolute_url())
change_url.short_description='URL'
It took me two hours to find out why Daniel Roseman's solution doesn't work for me. Even though he is right, there's one exception: when you want to make custom calculated fields (read only) in the Admin. This wont work. The very easy solution (but hard to find) is to return your string in a special constructor: SafeText(). Maybe this is linked with Django 2 or with readonly_fields (which behave differently from classical fields)
Here's a working sample that works but doesn't without SafeText():
from django.utils.safestring import SafeText
class ModelAdminWithData(admin.ModelAdmin):
def decrypt_bin_as_json(self, obj):
if not obj:
return _("Mode insert, nothing to display")
if not obj.data:
return _("No data in the game yet")
total = '<br/><pre>{}</pre>'.format(
json.dumps(json.loads(obj.data),
indent=4).replace(' ', ' '))
return SafeText(total) # !! working solution !! <------------------
decrypt_bin_as_json.short_description = _("Data")
decrypt_bin_as_json.allow_tags = True
readonly_fields = ('decrypt_bin_as_json',)
fieldsets = (
(_('Data dump'), {
'classes': ('collapse',),
'fields': ('decrypt_bin_as_json',)
}),
)