Django Forms - Proper way to validate user? - django

I'm having a bit of trouble finding an answer on this:
Using Django forms, what's the proper way to provide the current/requesting user?
My form looks like this:
class NewProjectForm(forms.Form):
project_name = forms.CharField(max_length=100)
def save(self):
project = Project(name=self.cleaned_data[project_name])
project.status = 'Working'
#TODO project.created_by = ???
project.save()
return project
Do I need to pass a second argument of user into the save function and I get that from the request or?

Yes, you do. However, you could probably save yourself some time by turning your form into a ModelForm and then handling the model save in your view:
class NewProjectForm(forms.ModelForm):
class Meta:
model = Project
fields = ['name',]
And then in your view:
if form.is_valid():
new_project = form.save(commit=False)
new_project.created_by = request.user
new_project.save()
That way you don't have to worry about passing around your user object, and the form itself will take care of setting the other properties for you (for project.status, you might try a default argument in your field definition).

Related

Django - modelform + model property

I am trying to solve one issue about saving data in db.
This is an example how I think of it:
class MyModel(models.Model):
id = models.AutoField(primary_key=True)
fieldX = models.SomeFieldType()
#property:
def foo(self):
return self._foo
#foo.setter
def foo(self, var):
self._foo=var
class MyModelForm(models.Modelform):
class Meta:
model = models.MyModel
fields = '__all__'
The thing is I have dict that I am passing to this form (so I am not using view or any provided interaction with user directly. In dict I have some fields and what I want to do is one field that is passed to use it as model property but I do not want it to be saved in db.
So I tried something like:
form = MyModelForm(data_dict)
if form.is_valid():
form.foo = data_dict['data_for_property_not_db']
form.save()
Form does not know that my model has this property.
So basiclly what I want is to write some parts of my data_dict normaly to form and db as always ->works fine
and then I want some data_info pass to that property and use it somewhere in save() as needed without saving it to db itself.
Can you help me with this?

django cbv dynamically exclude field from form based on is_staff / is_superuser

Been trying to determine the "most" elegant solution to dropping a field from a from if the user is not is_staff/is_superuser. Found one that works, with a minimal amount of code. Originally I though to add 'close' to the 'exclude' meta or use two different forms. But this seems to document what's going on. The logic is in the 'views.py' which is where I feel it blongs.
My question: Is this safe? I've not seen forms manipulated in this fashion, it works.
models.py
class Update(models.Model):
denial = models.ForeignKey(Denial)
user = models.ForeignKey(User)
action = models.CharField(max_length=1, choices=ACTION_CHOICES)
notes = models.TextField(blank=True, null=True)
timestamp = models.DateTimeField(default=datetime.datetime.utcnow().replace(tzinfo=utc))
close = models.BooleanField(default=False)
forms.py
class UpdateForm(ModelForm):
class Meta:
model = Update
exclude = ['user', 'timestamp', 'denial', ]
views.py
class UpdateView(CreateView):
model = Update
form_class = UpdateForm
success_url = '/denials/'
template_name = 'denials/update_detail.html'
def get_form(self, form_class):
form = super(UpdateView, self).get_form(form_class)
if not self.request.user.is_staff:
form.fields.pop('close') # ordinary users cannot close tickets.
return form
Yes, your approach is perfectly valid. The FormMixin was designed so you can override methods related to managing the form in the view and it is straightforward to test.
However, should yours or someone else's dynamic modifications of the resulting form object become too extensive, it would probably be best to define several form classes and use get_form_class() to pick the correct form class to instantiate the form object from.

can I use duck-typing with class-based views

I may be completely off the reservation here. (Feel free to tell me if I am.)
My use case is that I have a list of schools. The school model is pretty simple:
class School(models.Model):
name = models.CharField(max_length=100)
mascot = models.CharField(max_length=100, null=True, blank=True)
When my user wants to edit one of these schools, I don't want them editing the master copy. Instead, I want to give them their own copy which they can play with. When they are done editing their copy, they can submit their change, and someone else will approve it. So I have another class for the user's copy of the school:
class UserSchool(models.Model):
name = models.CharField(max_length=100)
mascot = models.CharField(max_length=100, null=True, blank=True)
master_school = models.ForeignKey(School)
user = models.ForeignKey(settings.AUTH_USER_MODEL)
So I set up a form to handle the editing of the UserSchool:
class UserSchoolForm(forms.ModelForm):
class Meta:
model = UserSchool
fields = ['name','mascot']
And now I have my EditSchool form:
class EditSchool(UpdateView):
model = School
success_url = reverse_lazy('list_schools')
form_class = UserSchoolForm
def get(self, request, *args, **kwargs):
school = self.get_object()
# make a copy of the school for this user
user_school, created = UserSchool.objects.get_or_create(
master_school=school, user=request.user,
defaults={'name' : school.name, 'mascot' : school.mascot})
self.object = user_school
form = UserSchoolForm()
context = self.get_context_data(form=form)
return self.render_to_response(context)
I know that get() is making the copy correctly, but when the form displays, there are no values listed in the "name" or "default" fields. My suspicion is that the problem is with the fact that cls.model = School, but self.object is an instance of UserSchool.
Am I close but missing something? Am I completely on the wrong path? Is there a better model for this (like having a single School instance with a special user for "master")?
(And one small complication -- since I'm an old hand at Django, but new a class-based views, I'm trying to use Vanilla Views because I find it easier to figure out what's going on.)
Just to rule out the obvious - you're not passing anything to the form constructor. Have you tried it with instance=user_school? There might be more that needs work but I'd start there.
To expand on this a bit - in your view, you're completely overriding the built in get method. That's fine, but it means that you're bypassing some of the automated behavior of your view superclass. Specifically, the get method of ProcessFormView (one of your ancestor classes) instantiates the form using the get_form method of the view class. FormMixin, another ancestor, defines get_form:
return form_class(**self.get_form_kwargs())
And get_form_kwargs on ModelFormMixin adds self.object to the form's kwargs:
kwargs.update({'instance': self.object})
Because your overridden get method does not call get_form, it also doesn't call get_form_kwargs and therefore doesn't go through the whole path that provides an initial binding for the form.
I personally would try to handle this by modifying the get_object method of your custom view and leaving the rest alone:
class EditSchool(UpdateView):
model = School
success_url = reverse_lazy('list_schools')
form_class = UserSchoolForm
def get_object(self, queryset=None):
school = super(EditSchool, self).get_object(queryset=queryset)
user_school, created = UserSchool.objects.get_or_create(
master_school=school, user=self.request.user,
defaults={'name' : school.name, 'mascot' : school.mascot})
return user_school
There may be more changes needed - I haven't tested this - but both the get and set methods use get_object, and bind it to the form as appropriate.

Django "Enter a list of values" form error when rendering a ManyToManyField as a Textarea

I'm trying to learn Django and I've ran into some confusing points. I'm currently having trouble creating a movie using a form. The idea of the form is to give the user any field he'd like to fill out. Any field that the user fills out will be updated in its respective sql table (empty fields will be ignored). But, the form keeps giving me the error "Enter a list of values" when I submit the form. To address this, I thought stuffing the data from the form into a list and then returning that list would solve this.
The first idea was to override the clean() in my ModelForm. However, because the form fails the is_valid() check in my views, the cleaned_data variable in clean() doesn't contain anything. Next, I tried to override the to_python(). However, to_python() doesn't seem to be called.
If I put __metaclass__ = models.SubfieldBase in the respective model, I receive the runtime error
"TypeError: Error when calling the
metaclass bases
metaclass conflict: the metaclass of a derived class must be a
(non-strict) subclass of the
metaclasses of all its bases"
My approach doesn't seem to work. I'm not sure how to get around the 'Enter a list of values" error! Any advice?
Here is the relevant code (updated):
models.py
""" Idea:
A movie consists of many equipments, actors, and lighting techniques. It also has a rank for the particular movie, as well as a title.
A Theater consists of many movies.
A nation consists of many theaters.
"""
from django.db import models
from django.contrib.auth.models import User
class EquipmentModel(models.Model):
equip = models.CharField(max_length=20)
# user = models.ForeignKey(User)
class ActorModel(models.Model):
actor = models.CharField(max_length=20)
# user = models.ForeignKey(User)
class LightModel(models.Model):
light = models.CharField(max_length=20)
# user = models.ForeignKey(User)
class MovieModel(models.Model):
# __metaclass__ = models.SubfieldBase
rank = models.DecimalField(max_digits=5000, decimal_places=3)
title = models.CharField(max_length=20)
equipments = models.ManyToManyField(EquipmentModel, blank=True, null=True)
actors = models.ManyToManyField(ActorModel, blank=True, null=True)
lights = models.ManyToManyField(LightModel, blank=True, null=True)
class TheaterModel(models.Model):
movies = models.ForeignKey(MovieModel)
class NationModel(models.Model):
theaters = models.ForeignKey(TheaterModel)
=====================================
forms.py
"""
These Modelforms tie in the models from models.py
Users will be able to write to any of the fields in MovieModel when creating a movie.
Users may leave any field blank (empty fields should be ignored, ie: no updates to database).
"""
from django import forms
from models import MovieModel
from django.forms.widgets import Textarea
class MovieModelForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(MovieModelForm, self).__init__(*args, **kwargs)
self.fields["actors"].widget = Textarea()
self.fields["equipments"].widget = Textarea()
self.fields["lights"].widget = Textarea()
def clean_actors(self):
data = self.cleaned_data.get('actors')
print 'cleaning actors'
return [data]
class Meta:
model = MovieModel
=============================================
views.py
""" This will display the form used to create a MovieModel """
from django.shortcuts import render_to_response
from django.template import RequestContext
from forms import MovieModelForm
def add_movie(request):
if request.method == "POST":
form = MovieModelForm(request.POST)
if form.is_valid():
new_moviemodel = form.save()
return HttpResponseRedirect('/data/')
else:
form = MovieModelForm()
return render_to_response('add_movie_form.html', {form:form,}, context_instance=RequestContext(request))
The probable problem is that the list of values provided in the text area can not be normalized into a list of Models.
See the ModelMultipleChoiceField documentation.
The field is expecting a list of valid IDs, but is probably receiving a list of text values, which django has no way of converting to the actual model instances. The to_python will be failing within the form field, not within the form itself. Therefore, the values never even reach the form.
Is there something wrong with using the built in ModelMultipleChoiceField? It will provide the easiest approach, but will require your users to scan a list of available actors (I'm using the actors field as the example here).
Before I show an example of how I'd attempt to do what you want, I must ask; how do you want to handle actors that have been entered that don't yet exist in your database? You can either create them if they exist, or you can fail. You need to make a decision on this.
# only showing the actor example, you can use something like this for other fields too
class MovieModelForm(forms.ModelForm):
actors_list = fields.CharField(required=False, widget=forms.Textarea())
class Meta:
model = MovieModel
exclude = ('actors',)
def clean_actors_list(self):
data = self.cleaned_data
actors_list = data.get('actors_list', None)
if actors_list is not None:
for actor_name in actors_list.split(','):
try:
actor = Actor.objects.get(actor=actor_name)
except Actor.DoesNotExist:
if FAIL_ON_NOT_EXIST: # decide if you want this behaviour or to create it
raise forms.ValidationError('Actor %s does not exist' % actor_name)
else: # create it if it doesnt exist
Actor(actor=actor_name).save()
return actors_list
def save(self, commit=True):
mminstance = super(MovieModelForm, self).save(commit=commit)
actors_list = self.cleaned_data.get('actors_list', None)
if actors_list is not None:
for actor_name in actors_list.split(","):
actor = Actor.objects.get(actor=actor_name)
mminstance.actors.add(actor)
mminstance.save()
return mminstance
The above is all untested code, but something approaching this should work if you really want to use a Textarea for a ModelMultipleChoiceField. If you do go down this route, and you discover errors in my code above, please either edit my answer, or provide a comment so I can. Good luck.
Edit:
The other option is to create a field that understands a comma separated list of values, but behaves in a similar way to ModelMultipleChoiceField. Looking at the source code for ModelMultipleChoiceField, it inhertis from ModelChoiceField, which DOES allow you to define which value on the model is used to normalize.
## removed code because it's no longer relevant. See Last Edit ##
Edit:
Wow, I really should have checked the django trac to see if this was already fixed. It is. See the following ticket for information. Essentially, they've done the same thing I have. They've made ModelMutipleChoiceField respect the to_field_name argument. This is only applicable for django 1.3!
The problem is, the regular ModelMultipleChoiceField will see the comma separated string, and fail because it isn't a List or Tuple. So, our job becomes a little more difficult, because we have to change the string to a list or tuple, before the regular clean method can run.
class ModelCommaSeparatedChoiceField(ModelMultipleChoiceField):
widget = Textarea
def clean(self, value):
if value is not None:
value = [item.strip() for item in value.split(",")] # remove padding
return super(ModelCommaSeparatedChoiceField, self).clean(value)
So, now your form should look like this:
class MovieModelForm(forms.ModelForm):
actors = ModelCommaSeparatedChoiceField(
required=False,
queryset=Actor.objects.filter(),
to_field_name='actor')
equipments = ModelCommaSeparatedChoiceField(
required=False,
queryset=Equipment.objects.filter(),
to_field_name='equip')
lights = ModelCommaSeparatedChoiceField(
required=False,
queryset=Light.objects.filter(),
to_field_name='light')
class Meta:
model = MovieModel
to_python AFAIK is a method for fields, not forms.
clean() occurs after individual field cleaning, so your ModelMultipleChoiceFields clean() methods are raising validation errors and thus cleaned_data does not contain anything.
You haven't provided examples for what kind of data is being input, but the answer lies in form field cleaning.
http://docs.djangoproject.com/en/dev/ref/forms/validation/#cleaning-a-specific-field-attribute
You need to write validation specific to that field that either returns the correct data in the format your field is expecting, or raises a ValidationError so your view can re-render the form with error messages.
update: You're probably missing the ModelForm __init__ -- see if that fixes it.
class MovieModelForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(MovieModelForm, self).__init__(*args, **kwargs)
self.fields["actors"].widget = Textarea()
def clean_actors(self):
data = self.cleaned_data.get('actors')
# validate incoming data. Convert the raw incoming string
# to a list of ids this field is expecting.
# if invalid, raise forms.ValidationError("Error MSG")
return data.split(',') # just an example if data was '1,3,4'

How to change multiple select field to multiple input fields for a many-to-many case?

When I display the ToolBoxEditForm it uses a multiple select field.
But what I want is a form that lets the user edit each tool he has in the toolbox as a text field. I cant figure out how to do this with the many-to-many field.
class Tool(models.Model):
tool_name = models.CharField(unique=True, max_length=200)
......
class ToolBox(models.Model):
tools = models.ManyToManyField(Tool,max_length=300)
class ToolBoxEditForm (ModelForm):
tools = ???
class Meta:
model = ToolBox
exclude = ('user', 'popularity',)
Sexiest Solution
You could use one of the jquery autocommplete tools described here: Facebook style JQuery autocomplete plugin
Then in the form:
class ToolBoxEditForm (ModelForm):
tools = forms.CharField(widget=forms.Textarea, required=False)
def clean_tools(self):
tool_data = self.cleaned_data.get('tools',None)
tools = []
#here, a comma is used a delim, so it's not allowed in the tool name.
for td in tool_data.split(','):
t, _ = Tool.objects.get_or_create(name=td)
tools.append(t)
return tools
class Meta:
model = ToolBox
exclude = ('user', 'popularity',)
You'd have to figure out how to modify the JavaScript so that new items could be entered (i.e. not just ones already in the database).
Alternative Solution
This is sort of what the inline formsets were created for, so Narendra's solution will work.
Something like:
from django.forms.models import inlineformset_factory
def manage_toolbox(request, toolbox_id):
toolbox = Toolbox.objects.get(pk=toolbox_id)
ToolInlineFormSet = inlineformset_factory(Toolbox, Tool)
if request.method == "POST":
formset = ToolInlineFormSet(request.POST, request.FILES, instance=toolbox)
if formset.is_valid():
formset.save()
# Do something.
else:
formset = ToolInlineFormSet(instance=toolbox)
return render_to_response("manage_toolbox.html", {
"formset": formset,
})
Not that this form is only for editing the items within the toolbox. If you want the user to be able to edit other aspects of the Toolbox -- say, its name or description -- you would create a separate form and output both of them inside the same <form></form> tags.
I'm not sure, since not tested it, but here is the logic goes.
Create formset for ToolBoxEditForm via formset_factory
Change tool_name field type to CharField
Set number of rows in formset to precisely the number of Tool objects available in db
Pass in initials to formset constructor to fill in tool_name textboxes.
# TODO: following data must be generated dynamically
initial_data=[{'tool_name': u'first_tool_name', },
{'tool_name': u'second_tool_name',}]
formset = ToolBoxFormSet(extra=0, initial=initial_data)
Not sure about the validation part. Here we are placing tool_name as value for textbox. During validation, Form may expect ID (because it is supposed to be listbox). But, you can handle that also.
for more info about formset refer: http://docs.djangoproject.com/en/dev/topics/forms/formsets/