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).
Related
I am generating an object from a submitted form. I want to pass along the generated AutoField to the next form as a hidden form element. However, I cannot figure out how to pass a variable from post() to get_context_data(). I know that post() is called first, followed by get_context_data(), but adding the variable to self does not yield the results I expect (the attribute does not exist on self).
Here is an example of what I am experiencing:
def get_context_data(self, **kwargs):
context = super(MyView, self).get_context_data(**kwargs)
print self.hello
return context
def post(self, request, *args, **kwargs):
self.hello = "hello"
return HttpResponseRedirect(request.path)
self.hello is not valid when called in get_context_data. I feel as though I may be mistaken with the HttpResponseRedirect(request.path) call, but I'm not sure how else to render the template.
Is it a simple mistake, or should I be approaching this an entirely different way?
By defining post yourself, you've overridden the default behaviour of the view. You can see that there is no call to get_context_data, or any of the other class methods, so naturally they won't be called.
Generally you should not be overriding the specific get or post methods. You haven't shown the full view so it's not clear what behaviour you are trying to achieve, but for example in a form view you would want to define success_url to set the place the form redirects to after submission.
You have to find a way to pass the object ID to the next page. The options that come to mind are to put it into the URL or as solarissmoke has suggested save it in the session. If you are doing it in the url you can also put the page sequence there (meaning 1 for the forst form, 2 for the second...).
The nice thing about this approach is, that you can cover all functonailty in one view: depending on the page set the respective fields in the get_object methods (self.fields=[....]) and the template names in the get_template_names method.
So using an Updateview, it would look like this:
urls.py:
....
url(r'^mysite/(?P<object_no>\d+)/(?P<form_no>\d+)$', BaseView.as_view()),
views.py:
class BaseView(UpdateView):
def get_object(self):
obj=MyModel.objects.get(id=self.kwargs['object_no'])
form_no = self.kwargs['form_no']
if form_no=="1":
self_fields=["field1","field2"...]
.....
def get_object(self):
obj=MyModel.objects.get(id=self.kwargs['object_no'])
form_no = self.kwargs['form_no']
if form_no=="1":
self_fields=["field1","field2"...]
.....
return obj
def get_template_names(self):
from_no = self.kwargs['form_no']
if form_no=="1":
return ["template1.html"]
....
You have to make sure that all your fields can be null.
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)
I need a form that has a dynamically created part and a static part to it. So I'm thinking of subclassing ... but can't figure out how this would work with a dynamically created form.
I am creating my dynamic form in this way:
from views.py
def create_legumes_form_class(legumes):
form_fields={}
for legume in legumes:
field_id = 's_{}'.format(legume.id)
form_fields[field_id] = IntegerField(default=0 , validators = [InputRequired()])
return type('OrderForm', (Form,), form_fields)
Then instantiating the form in my view function :
legumes = Legumes.query.filter_by(disponible = True).all()
OrderForm = create_legumes_form_class(legumes)
form = OrderForm()
But to this OrderForm(), I need to add a couple BooleanField(s) that will always be the same (ie. not dynamically created from the Query).
So I am trying to add a (static) form in my forms.py and instantiate it like this in the view function:
form=OrderForm(StaticForm)
But I'm getting
TypeError: formdata should be a multidict-type wrapper that supports the 'getlist' method
And I don't know where to take it from here !
What am I doing wrong/not doing ?
You can't pass another class to the OrderForm constructor and have it subclass that class. You can, however, provide it when you call type.
return type('OrderForm', (Form, StaticForm), form_fields)
If StaticForm subclasses Form this can be further simplified.
return type('OrderForm', (StaticForm,), form_fields)
I now get the error below because of this line setattr(CsvModel, field.value(), CharField())
Error: 'dict' object is not callable
views.py
if request.method == 'POST':
form = ConfiguratorForm(data=request.POST)
# Send import to task.
# Clean all data and add to var data.
if form.is_valid():
data = form.cleaned_data
process_upload.delay(upload_id=upload.id, form=data)
tasks.py
#task
def process_upload(upload_id, form):
upload = Upload.objects.get(id=upload_id)
upload.process(form=form)
process
import_this(data=self.filepath, extra_fields=[
{'value': self.group_id, 'position': 5},
{'value': self.uploaded_by.id, 'position': 6}], form=form)
model.py
def import_this(form, *args, **kw):
# make custom ContactCSVModel
class ContactCSVModel(CsvModel):
for k, v in form:
setattr(CsvModel, v, CharField())
group = DjangoModelField(Group)
contact_owner = DjangoModelField(User)
class Meta:
delimiter = ","
dbModel = Contact
update = {'keys': ["mobile", "group"]}
return ContactCSVModel.import_data(*args, **kw)
In the second call you are passing in form.cleaned_data, which is a mapping (a dict), so you are looping over the keys, which are strings.
In the first call on the other hand, you are passing in the form itself. The second call is thus not the same; the following call would be:
form = ConfiguratorForm(data=request.POST)
if form.is_valid():
process_upload(upload_id=upload.id, form=form)
The question here is if that is what you actually meant to do.
In the first case your form argument is a form instance.
In the second your form argument is a dict instance.
Additionnally:
I think your import_this method (if it is, indeed, a method of your model) lacks a self first argument or should be declared as a #staticmethod. It may save you the trouble of calling it with import_this(self.xxx, self.yyy, ...) But I'm not quite sure how you go from upload.process() to a call to import_method() (my Django is a bit rusty).
And for what it's worth I think there are much simpler ways to achieve what you want to achieve (which if I guess well, is load CSV file):
Without form validation: have a look at csv.DictReader() will return a dict for each line in your file. Then do:
with csv.DictReader(...) as r:
for line in r:
instance = MyModel(**line)
instance.save()
With a form to validate data read from the file: do the same but instanciate a ModelForm, which you'll need to define:
with csv.DictReader(...) as r:
for line in r:
form = MyModelForm(**line)
form.save()
What you are doing looks like some kind of strange, brain-damaging, meta-programming... Are you trying to create a model on the fly, from what you find in a CSV file ?
Hope this helps.
So I have this custom ModelForm that I created that takes in a variable creator_list for the queryset like this:
class UserModelChoiceField(ModelChoiceField):
def label_from_instance(self, obj):
return obj.get_full_name()
class OrderCreateForm(ModelForm):
class Meta:
model=Order
fields=('work_type', 'comment',)
def __init__(self, creator_list=None, *args, **kwargs):
super(OrderCreateForm, self).__init__(*args, **kwargs)
if creator_list:
self.fields['creator'] = UserModelChoiceField(
queryset=creator_list,
empty_label="Select a user",
widget=Select(attrs={
'onchange': "Dajaxice.doors.orders_create_creator_changed(fill_other_fields, {'creator_pk': this.options[this.selectedIndex].value})"
})
)
self.fields['place'] = UserModelChoiceField(
queryset=User.objects.none(),
empty_label="Select a creator first"
)
When I am simply displaying the fields, everything works perfectly. However during a POST submission. I get errors that I don't know how to debug.
My views.py looks like this:
user = request.user
dictionary = get_order_create_dictionary(user)
if request.method == 'POST':
#import ipdb; ipdb.set_trace()
form = OrderCreateForm(request.POST)
if form.is_valid():
creator = form.cleaned_data['creator']
place = form.cleaned_data['place']
work_type = form.cleaned_data['work_type']
comment = form.cleaned_data['comment']
new_order = Order.objects.create(
creator =creator,
place =place,
work_type=work_type,
comment =comment
)
messages.success(request, "Your order #{} had been created!".format(new_order.pk))
logger.info("{user} created order #{pk}".format(user=user, pk=new_order.pk))
return HttpResponseRedirect(reverse('orders_detail', kwargs={'pk': new_order.pk}))
else:
return render(request, 'doors/orders/create.html', {'form': form, 'can_assign_creator': dictionary['can_assign_creator']})
else:
if dictionary:
return render(request, 'doors/orders/create.html', {
'form': OrderCreateForm(creator_list=dictionary['creator_list']),
'can_assign_creator': dictionary['can_assign_creator']
})
else:
return HttpResponseRedirect(reverse('orders_list'))
get_order_create_dictionary() simply returns a dictionary that looks like this:
dictionary = {
'creator_list': Order.objects.all(), # Or some kind of filtered Order.
'can_assign_order: 1, # Or 0. This variable is used in the template to control access to what gets displayed.
}
Currently with the above code I get an error like this when I try to POST something:
AttributeError: 'QueryDict' object has no attribute 'all'
on the line "return render(request, 'doors/orders/create.html', {'form': form, 'can_assign_creator': dictionary['can_assign_creator']})"
I thought it has something to do with the line form = OrderCreateForm(request.POST) so I changed that to form = OrderCreateForm(request.POST, creator_list=dictionary['creator_list']). But then I get this error:
TypeError: __init__() got multiple values for keyword argument 'creator_list'
on the line "form = OrderCreateForm(request.POST, creator_list=dictionary['creator_list'])"
I have no clue how to resolve this. I appreciate any help or tips! Thanks!
EDIT:
I changed the line to form = OrderCreateForm(dictionary['creator_list'], request.POST) and now the validation works, but it won't let me submit a valid POST. It keeps saying Select a valid choice. That choice is not one of the available choices. for the place. This probably has something to do with how I populate the <option> with place using Ajax depending on what the creator is.
You'd better instantiate Form instances with only named arguments, i.e.
form = OrderCreateForm(creator_list=dictionary['creator_list'], data=request.POST)
One exception is when form only has one argument - the data. This will help you to avoid messing up with arguments order (which is the reason of your errors here).