Django admin site modify save - python

In the admin site, I have a custom form. However, I want to override the save method so that if a certain keyword is entered, I do not save it into the database. Is this possible?
class MyCustomForm (forms.ModelForm):
def save(self, commit=True):
input_name = self.cleaned_data.get('input_name', None)
if input_name == "MyKeyword":
//Do not save
else:
return super(MyCustomForm, self).save(commit=commit)
but this returns the error:
AttributeError 'NoneType' object has no attribute 'save'
Edit
Following this: Overriding the save method in Django ModelForm
I tried:
def save(self, force_insert=False, force_update=False, commit=True):
m = super(MyCustomCategoryForm, self).save(commit=False)
input_name = self.cleaned_data.get('input_name', None)
if input_name != "MyKeyword":
m.save()
return m
But the admin site still creates a new entry if the input_name is "MyKeyword"

The save() method is supposed to return the object, whether saved or not. It should rather look like:
def save(self, commit=True):
input_name = self.cleaned_data.get('input_name')
if input_name == "MyKeyword":
commit = False
return super().save(commit=commit)
However, as you can see here and here, the form is already called with commit=False and the object is saved later by the save_model() method of your ModelAdmin. This is the reason why you got AttributeError 'NoneType' object has no attribute 'save'. If you check the traceback of the exception, I'm pretty sure the error comes from this line.
Thus you should, in addition, override your ModelAdmin's save_model() method:
def save_model(self, request, obj, form, change):
if form.cleaned_data.get('input_name') == 'MyKeyword':
return # Don't save in this case
super().save_model(request, obj, form, change)
# And you'll also have to override the `ModelAdmin`'s `save_related()` method
# in the same flavor:
def save_related(self, request, form, formsets, change):
if form.cleaned_data.get('input_name') == 'MyKeyword':
return # Don't save in this case
super().save_related(request, form, formsets, change)
Given your comment in this answer
It is working now but I just have one related question. The admin site will still display a green banner at the top saying "The model was added successfully". Can I override some method so that it is red and says "The model already exists"?
It looks that what you want to do is not overriding the save method but controlling form validation. Which is completely different. You just have to override your form's clean method.
from django import forms
def clean(self):
"""
super().clean()
if self.cleaned_data.get('input_name') == 'MyKeyword':
raise forms.ValidationError("The model already exists")
Or, even better since it looks like you want to clean a single field:
from django import form
def clean_input_name(self):
data = self.cleaned_data['input_name']
if data == 'MyKeyWord':
raise forms.ValidationError("The model already exists")
However, I guess input_name is also a field of your model and you don't want to raise this error only on one form but across all your project. In which case, what you are looking for are Model validators:
from django.core.exceptions import ValidationError
def validate_input_name(value):
if value == 'MyKeyword':
raise ValidationError("The model already exists")

Related

Setting the form value when the form is initiated

I am trying to set a field value when a form is initiated.
The value of this field is retrieved when we enter the view - the view being the timesheet. Then for each Time set in the view, I want to relate it back to the timesheet.
#login_required
#requires_csrf_token
def timesheet(request, timesheet_id):
timesheet = TimeSheet.objects.get(pk=timesheet_id)
NewTimeFormSet = modelformset_factory(Time, form=TimeForm, formset=RequiredFormSet)
if request.method == 'POST':
newtime_formset = NewTimeFormSet(request.POST, request.FILES)
for form in newtime_formset:
if form.is_valid():
form.save()
#then render template etc
So, to make sure the form validates I want to set this field when the form is initiated. When I try to set this field after POST in the view, I haven't been able to get the field to set or form to validate.
My code gets the timesheet_id when the model instance is initiated on entering the view
def __init__(self, *args, **kwargs):
# this allows it to get the timesheet_id
print "initiating a timesheet"
super(TimeSheet, self).__init__(*args, **kwargs)
And then the form is generated and I run the form init. So this is what I've tried
class TimeForm(forms.ModelForm):
class Meta:
model = Time
fields = ['project_id', 'date_worked', 'hours', 'description', 'timesheet_id',]
# some labels and widgets, the timesheet_id has a hidden input
def __init__(self, *args, **kwargs):
print "initiating form"
super(TimeForm, self).__init__(*args, **kwargs)
timesheet = TimeSheet.objects.get(id=timesheet_id)
self.fields['timesheet_id'] = timesheet
This raises the error
NameError: global name 'timesheet_id' is not defined
I don't know how to do this...
I've also attempted setting the field in the form clean() method, but it populates (shown by a print) and then still doesn't validate and I raise a formset error 'This field is required'.
Help!
You don't actually accept a timesheet_id parameter in the form init method, so that value is not defined hence the error.
However, this is the wrong approach. There is no point passing a value to a form, outputting it as a hidden field, then getting it back, when you had it all along. The way to do this is to exclude the value from the form's fields, and set it on save.
class TimeForm(forms.ModelForm):
class Meta:
model = Time
fields = ['project_id', 'date_worked', 'hours', 'description',]
...
if request.method == 'POST':
newtime_formset = NewTimeFormSet(request.POST, request.FILES)
if newtime_formset.is_valid():
for form in newtime_formset:
new_time = form.save(commit=False)
new_time.timesheet_id = 1 # or whatever
new_time.save()
Note, again, you should check the validity of the whole formset before iterating through to save; otherwise you might end up saving some of them before encountering an invalid form.

save() got an unexpected keyword argument 'commit' Django Error

im geting this error "save() got an unexpected keyword argument 'commit'"
what im trying to do is request user when user upload his files.
update i added my model.py and forms.py and also screen shot of error sorry my fisrt time learning python/django.
screen shot
model.py
class Document(models.Model):
fs = FileSystemStorage(location=settings.MEDIA_ROOT)
input_file = models.FileField(max_length=255, upload_to='uploads', storage=fs)
user = models.ForeignKey(User)
def __unicode__(self):
return self.input_file.name
#models.permalink
def get_absolute_url(self):
return ('upload-delete', )
forms.py
class BaseForm(FileFormMixin, django_bootstrap3_form.BootstrapForm):
title = django_bootstrap3_form.CharField()
class MultipleFileExampleForm(BaseForm):
input_file = MultipleUploadedFileField()
def save(self):
for f in self.cleaned_data['input_file']:
Document.objects.create(
input_file=f
)
here is my views.py
#login_required
def list(request):
# Handle file upload
if request.method == 'POST':
form = MultipleFileExampleForm(request.POST, request.FILES)
if form.is_valid():
newdoc = form.save(commit=False)
newdoc.user = request.user
newdoc.save()
# Redirect to the document list after POST
return HttpResponseRedirect(reverse('myfiles.views.list'))
else:
form = MultipleFileExampleForm() # A empty, unbound form
documents = Document.objects.all
return render_to_response(
'example_form.html',
{'documents': documents, 'form': form},
context_instance=RequestContext(request)
)
You are not sub classing django.forms.ModelForm, yet, you are writing your code like you are.
You need to subclass ModelForm (which has the save method with the commit argument).
Calling super will not work either, as the super class has no save method with that argument.
Remove the commit=False it will never work unless you rewrite your code to subclass django.forms.ModelForm
In any case the save method should always return an instance. I suggest you rename your method to save_all_files or something similar. You will not be able to use commit=False to save multiple object in your save method. It is not the intended use.
For further reading, you can read the source to know how the commit=False works in the ModelForm class at the following address :
https://github.com/django/django/blob/master/django/forms/models.py
I believe you are completely overriding the save method, which gets rid of the existing functionality (i.e. the commit arg). Try running a super() at the end so that it has the existing save functionality as well.
def save(self):
for f in self.cleaned_data['input_file']:
Document.objects.create(
input_file=f
)
super(MultipleFileExampleForm, self).save(*args, **kwargs)

How to instatiate a Django Form dynamically by string?

I'm trying to write an app for Django. I want my users to be able to collect certain types of data, for instance samples, videos, etc... The app is called collector and for each type of item there is a class and a form that goes along with it.
Example Class:
class CreateTextView(CreateItemView):
form_class = TextForm
model = Text
def get_context_data(self, **kwargs):
context = super(CreateTextView, self).get_context_data(**kwargs)
context['item_type'] = 'text'
return context
Example Form:
class TextForm(forms.ModelForm):
class Meta:
model = Text
fields = COMMON_FIELDS + ('text',)
As you can see, the actual view is inheriting from CreateItemView. I want as much of the functionality to be defined for CreateItemView so that I don't have to do it individually for all item classes. That has been working for the most part, but it gets a bit tricky when I try to process forms with data.
def post(self, request, *args, **kwargs):
form = TextForm(request.POST) # line 2
form = getattr(TextForm, '__init__')(data=request.POST) # line 3
if form.is_valid():
# Add owner information.
item = form.save(commit=False)
item.owner = request.user
item.save()
return HttpResponseRedirect(reverse('collector:index'))
return render(request, self.template_name, {'form': form})
In Line 2 you can see how I would handle the form if there was only one type of form. Line 3 is what I'm trying to do. I want to be able to use the context['item_type'] to dynamically choose the right form and instantiate it with the given data.
Now the problem lies with the __init__-method which I have never defined anywhere. When I pass only POST.request to __init__, it complains about not having a self. When I pass the additional self, it complains about how CreateTextView has no _meta-attribute and so on. I just can't find the right combination of argumentes to satisfy the __init__-method. I can't look up it's definition either, because I didn't define it. I then followed the definition of the parent classes in the django framework which led me to a couple of complex functions that looked like factories. That didn't really help me...
Now I know how to use the TextForm()-initiation. Is there a way to fetch this method dynamically with getattr()? That would save me the hassle with __init__. If not, how do I provide __init__ with the correct self-instance?
As mentioned below, I have changed my classes a little bit. I no longer use context to store the item_type, instead I use a class variable to have easy acces to the item_type within a view. My post method is defined in the mother class CreateItemView and looks like this now:
def post(self, request, *args, **kwargs):
try:
form_cls = ITEM_TYPE_MAP[self.item_type]
except KeyError:
# invalid item_type. raise a meaningful error here
raise Exception('Invalid item type.')
form = form_cls(request.POST)
if form.is_valid():
# Add owner information.
item = form.save(commit=False)
item.owner = request.user
item.save()
return HttpResponseRedirect(reverse('collector:index'))
return render(request, self.template_name, {'form': form})
A clean and quite simple solution to look for is using a dictionary to map the item_type values to actual form classes:
ITEM_TYPE_MAP = {
"foo": TextForm,
"bar": SomeOtherForm,
}
You’d put that dictionary at some global place and use it from within the controller like this:
item_type = context['item_type']
try:
form_cls = ITEM_TYPE_MAP[item_type]
except KeyError:
# invalid item_type. raise a meaningful error here
raise
form = form_cls(request.POST)
You cannot directly call __init__ usually, because there’s more than that to instanciate an object. Python will also call __new__ on the class of the object, so the only way to be sure is to go through the actual constructor, which is calling the type.
This is what happens above, by first fetching the type into form_cls and then calling the type (i.e. the constructor).

Django wizard: Form instantiated and clean called multiple times

I have a form wizard with 3 forms. The first form has only one field and uses that field to do a lookup in an external API to validate the data.
I noticed that the requests were taking unusually long, so I just added a print statement to
the form's init method and to the external API client call.
It seems that my first form is being initialized and cleaned exactly 28 times every time I execute that step in the form wizard.
My first form looks like this:
forms.py
class MyForm1(forms.Form):
issue_id = forms.CharField()
def __init__(self, *args, **kwargs):
print "init form 1"
super(MyForm1, self).__init__(*args, **kwargs)
def clean(self):
cleaned_data = super(MyForm1, self).clean()
print "clean issue id"
issue_id = cleaned_data.get("issue_id")
if issue_id is not None:
try:
issue = ApiCLient.get_issue(issue_id)
except APIError:
raise forms.ValidationError("Issue not found. Try again!")
else:
return cleaned_data
raise forms.ValidationError("Issue not found. Try again!")
The wizard stuff is pretty standard from the Django Docs, but here it goes:
views.py
class MyWizard(SessionWizardView):
def done(self, form_list, **kwargs):
# some logic at the end of the wizard
urls.py
wizard_forms = [MyForm1, MyForm2, MyForm3]
...
url(r'wizard/$', login_required(views.MyWizard.as_view(wizard_forms))),
As I said before, executing the first step results in
init form 1
clean issue id
Being printed to console exactly 28 times.
Any ideas? Is this a feature or a bug in the Django Form Wizard?

Django: Edit Function while not changing the Image data

I have an edit function that I want the user to be able to edit the Picture object (tags), while keeping the old image. The form is looking for a photo but I do want the user to be able to change the image - just the other information.
How do you pass the original image data from the picture object into the PictureForm so it validates?
My view:
#csrf_protect
#login_required
def edit_picture(request, picture_id, template_name="picture/newuserpicture.html"):
picture = get_object_or_404(Picture, id=picture_id)
if request.user != picture.user:
return HttpResponseForbidden()
if request.method == 'POST':
form = PictureForm(request.POST or None, request.FILES or None, instance=picture)
if form.is_valid():
form.save()
return HttpResponseRedirect('/picture/%d/' % picture.id )
else:
form = PictureForm(instance=picture)
data = { "picture":picture, "form":form }
return render_to_response(template_name,
data,
context_instance=RequestContext(request))
I think this thread should give you a clue how to make existing fields readonly:
In a Django form, how do I make a field readonly (or disabled) so that it cannot be edited?
I you want to hide the picture completely and stumble across validation errors because the field is marked as required in your model definition (blank=True) another option would be to override the form's save method and tweak the field's required attribute.
Something along these lines:
def __init__(self, *args, **kwargs):
super(PictureForm, self).__init__(*args, **kwargs)
for key in self.fields:
self.fields[key].required = False
I guess I am not sure how to just comment on the original question, but what do you mean by validate? If you are just needing to ensure that the picture object's picture is actually the same after the form is done can you not make some custom clean methods for the form? in a clean method you could compare all metadata and the image then proceed to the forms save.

Categories

Resources