how to pre-fill an HiddenInput in admin ModelForm - django

I'm sure that problems similar to the one I'm going to ask about were already discussed somewhere, but I always found very tricky solutions while I think there must be a very straightforward one (you can guess I'm a totally newbie).
I have a model (Lecturer) that is connected OneToOne to the User model:
class Lecturer(models.Model):
user = models.OneToOneField(User)
... other fiels follow ...
I'd like each user to create its own Lecturer object through the admin site.
My idea is to present to the user the add_view without the user field. Then I would something like obj.user=request.user when saving the model.
In other words, I don't want to give the user the possibility of selecting a different user among the registered for its own Lecturer object.
I modified the form by overriding the get_form method and by providing a custom form:
admin.py
class LecturerAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if is_lecturer(request.user):
kwargs['form'] = UserLecturerForm
return super(LecturerAdmin, self).get_form(request, obj, **kwargs)
class UserLecturerForm(forms.ModelForm):
class Meta:
model = Lecturer
fields = ('__all__')
widgets = {'user': forms.HiddenInput()}
I cannot just exclude the user field and give it a value at some other level (e.g. save_model or clean ...) because this raises an error at the template rendering level:
Django Version: 1.7.7
Exception Type: KeyError
Exception Value: u"Key 'user' not found in 'LecturerForm'"
Exception Location: /usr/lib/python2.7/dist-packages/django/forms/forms.py in getitem, line 147
Python Executable: /usr/bin/python
Python Version: 2.7.9
Error during template rendering
In template /usr/lib/python2.7/dist-packages/django/contrib/admin/templates/admin/includes/fieldset.html, error at line 7
[...]
7 <div class="form-row{% if line.fields|length_is:'1' and line.errors %} errors{% endif %}{% if not line.has_visible_field %} hidden{% endif %}{% for field in line %}{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% endfor %}">
I tried making the user field hidden in the form. But then the problem came of how to give it the correct request.user value, given that the form doesn't know anything about the request. If I don't fill it with a valid value, the form will not validate (and I cannot use solutions involving the save_model as suggested here)
I found solutions that involve changing the view, but I wouldn't do it in the admin context.
An alternative would be to change the validation behavior, but still validation is a method of the form and the form doesn't know request.
Any help is appreciated.

Related

Forms, Model, and updating with current user

I think this works, but I came across a couple of things before getting it to work that I want to understand better, so the question. It also looks like other people do this a variety of ways looking at other answers on stack overflow. What I am trying to avoid is having the user to have to select his username from the pulldown when creating a new search-profile. The search profile model is:
class Search_Profile(models.Model):
author_of_profile = models.ForeignKey(settings.AUTH_USER_MODEL,on_delete=models.CASCADE,blank=True)
keyword_string = models.CharField(max_length=200)
other_stuff = models.CharField(max_length=200)
The form I ended up with was:
class Search_Profile_Form(ModelForm):
class Meta:
model = Search_Profile
fields = [ 'keyword_string', 'other_stuff']
Where I deliberately left out 'author_of_profile' so that it wouldn't be shown or need to be validated. I tried hiding it, but then the form would not save to the model because a field hadn't been entered. If I was o.k. with a pulldown I guess I could have left it in.
I didn't have any issues with the HTML while testing for completeness:
<form action="" method="POST">
{% csrf_token %}
{{ form.author_of_profile}}
{{ form.keyword_string }}
{{ form.other_stuff }}
<input type="submit" value="Save and Return to Home Page">
</form>
And the View is where I ended up treating the form and the model separated, saving the form first, then updating the model, which is where I think there might be some other way people do it.
def New_Profile(request):
if request.method=='POST':
form = Search_Profile_Form(request.POST)
if form.is_valid():
post=form.save(commit=False)
# here is where I thought I could update the author of profile field somehow with USER
# but If I include the author_of_profile field in the form it seems I can't.
post.save()
#So instead I tried to update the author_of profile directly in the model
current_profile=Search_Profile.objects.last()
current_profile.author_of_profile=request.user
current_profile.save()
return(redirect('home'))
else:
form=Search_Profile_Form()
return render(request, 'mainapp/New_Profile.html', {'form': form})
So a few questions:
For the Foreign Key in author_of_profile field, is it better to use blank=True, or null=True
I ended up using request.user rather than importing from django.contrib.auth.models import User is there any difference?
My real question though, is the above a reasonable way to get form data and update the database with that data and the user? Or am I missing some other way that is more build in?
post=form.save()
current_profile.author_of_profile=request.user
post.save()
return(redirect('home'))
try something like this. save the form to db then change the author again. save(commit=False) will not save the date to db immediately.

Django form and field validation does not work?

When using the Django built-in Form classes the validation routine does not seem to work.
The form consists simply of firstname and lastname. Firstname is required, and for testing purposes I check if lastname is Smith and raise an exception. When I violate those requirements nothing happens, i.e. no exception is being raised - after submitting the form the defined action (POST to union/VIP_best/) is simply triggered without any validation. The form is called at union/contact/, directed from urls.py to views.ContactView.as_view()
Here is my setup so far:
views.py
from union.forms import ContactForm
class ContactView(generic.edit.FormView):
template_name = 'union/contact.html'
form_class = ContactForm
forms.py
from django import forms
from django.core.exceptions import ValidationError
class ContactForm(forms.Form):
firstname = forms.CharField(label='Vorname', max_length=20, required=True, error_messages={'required': 'Please enter first name!'})
lastname = forms.CharField(label='Nachname', max_length=20)
def clean_lastname(self):
data = self.cleaned_data['lastname']
if data != "Smith":
raise forms.ValidationError("Your last name is not Smith.")
else:
raise forms.ValidationError("Your last name is Smith.")
return data
templates/union/contact.html
<form action="/union/VIP_best/" method="post">
{% csrf_token %}
{{ form.as_table }}
<tr>
<td><input type="submit" value="Submit"></td>
</tr>
</form>
What am i missing so that the .clean() or .is_valid is being triggered?
Do I have to explicitly call Field.clean() and is_valid()? If so, where?
The tutorials and Django documentation do not seem to mention anything the like.
Django 1.7, Python 3.4.2
Your form should be submitting to itself. Meaning, if the form is on /union/contact/, you should be POSTing to /union/contact/. Currently, you have it submitting to a different view/url.
The problem is arising because your form processing is happening in the FormView, not at your success_url(). You need to POST to the view that is actually responsible for the validation of the form (in this case, your ContactView at /union/contact/).
As a side note, it would probably be better to modify the action of the form to use {% url 'your_form_url_name' %}, as opposed to hard-coding the url into the template.

(How) does Django prevent data injection by manipulating forms?

I'm using Django's built-in User model in one of my projects. Users should be editable, of course. Since it's the most convenient solution I started providing the form for User edits by using Django's own UserChangeForm. UserChangeForm is basically a ModelForm of the UserModel, so it makes it possible to change all the fields of the User model.
I don't want users to be able to change every field. So, my first idea was to pass the whole UserChangeForm into the template but render only the fields that I need (say 'username' and 'email'). I also want only superusers to be able to change a username, so this field is only rendered if you are a superuser.
The essential code looks like that:
view function
def edit_user(request, pk):
... #code to ensure not everyone can edit every user
user = User.objects.get(pk=pk)
if request.method == 'POST':
form = UserChangeForm(request.POST, instance=user)
if form.is_valid:
form.save()
... #redirect
else:
form = UserChangeForm(instance=user)
... # render template
form in the template
<form action="{{ request.path }}" method="post">
{% csrf_token %}
{% if user.is_superuser %}
{{ form.username }}
{% endif %}
{{ form.email }}
<button type="submit">Save</button>
</form>
Now, my question is: How about the security aspects of this solution? I do nothing to prevent an attacker to add the e.g. the username field even if he is no superuser. By that, he would populate the POST data with additional data, which is then send to the view function and used to update the User object. That could get really dangerous, since the original UserChangeForm also contains a field 'is_superuser'.
I tried the hack the form myself to test my suspicion. I logged in as a normal user, added the username input via the Developer Tools and submitted the form. The result was an exception:
Traceback:
File "/Library/Python/2.7/site-packages/django/core/handlers/base.py" in get_response
111. response = callback(request, *callback_args, **callback_kwargs)
File "/Library/Python/2.7/site-packages/django/contrib/auth/decorators.py" in _wrapped_view
20. return view_func(request, *args, **kwargs)
File "/Users/joker/Development/wiki2099/wiki2099/apps/accounts/views.py" in edit_user
69. form.save()
File "/Library/Python/2.7/site-packages/django/forms/models.py" in save
364. fail_message, commit, construct=False)
File "/Library/Python/2.7/site-packages/django/forms/models.py" in save_instance
74. " validate." % (opts.object_name, fail_message))
Exception Type: ValueError at /accounts/edit/12/
Exception Value: The User could not be changed because the data didn't validate.
I'm not sure if that means that this kind of attack is not possible or that I just didn't do it right. I think, the CSRF token could prevent such kind of hacks, but I found nothing about that matter in the docs. Could anyone enlighten me? Is there any mechanism to prevent attackers from using not-rendered form fields and how does it work?
Thanks in advance!
If you want to restrict the fields that the user can edit, then you need to define a form with a subset of the model's fields.
If you don't render a form field in the template, but the user submits data for it, then Django will process it as normal. Having looked at the traceback, I don't understand why your attempt failed, but an attack using the method you described is possible.
The csrf protection won't help you here. Its purpose is to prevent a third party tricking your users into submitting data to your site, not protect against hand crafted POST data with extra fields.
There's another issue to be aware of if you deliberately don't render fields in the template: If the omitted field is not required, then the missing POST values will be interpreted as empty strings, validate, and your data will be wiped.
As far as I know, CSRF token only protects from CSRF atacks and doesn't do any things about permissions to change data.
Though it isn't clear if you only not render the field in case the user is not a superuser, or you didn't add it to self.fields at all. You should use the second way, or even have a separate form for superusers.

Django user injection in model properties

I have this models in Django:
News
Comments
Reactions
Relations are:
a News has various Comments
a Comment has various Reactions
The problem is the user (in request / session): the user may subscribe to a reaction, or a comment; he may be logged in or not. (it's a foo example, it doesn't have much sense)
I can't do in template:
{% for reaction in this_news.comments.reactions %}
{{ reaction.name }}
{% if reaction.user_subscribed %} #reaction.user_subscribed(request.user)...
You have subscribed this reaction!
{% endif %}
{% endfor %}
Problems are:
I can't call the method in the template with a parameter (see the comment above)
Models don't have access to request
Now i'm calling an init_user method in News Model, passing the request. Then i have the same method in Comment and Reaction model, and i have to set the user_subscribed property cycling the children of each model.
Isn't there a smarter way to do this?
EDIT: thanks to the Ignacio's hint about using custom tag i'm trying to do a generic mode to pass the user (avoiding the use of closures because i don't know how to use them atm):
def inject_user(parser, token):
try:
# split_contents() knows not to split quoted strings.
tag_name, method_injected, user = token.split_contents()
except ValueError:
raise template.TemplateSyntaxError("%r tag requires exactly three arguments" % token.contents.split()[0])
return InjectUserNode(method_injected, user)
class InjectUserNode(template.Node):
def __init__(self, method_injected, user):
self.method_injected = template.Variable(method_injected)
self.user = template.Variable(user)
def render(self, context):
try:
method_injected = self.method_injected.resolve(context)
user = self.user.resolve(context)
return method_injected(user)
except template.VariableDoesNotExist:
return ''
When i use it {% inject_user object.method_that_receives_a_user request.user %} i come to this error 'str' object is not callable in method_injected(user); how i can fix that?
Write custom template tags that take the user and set a context variable to indicate presence or absence of the criterion.
I've resolved it in a less elegant way, but it worked for me.
I've created a sort of singleton in my User defined class, with a property that i set in every view i need it.
The property is User.current.
Then, inside the models, where i need that i get the current user looking in User.current.

Django: How to check if there are field errors from within custom widget definition?

I'd like to create widgets that add specific classes to element markup when the associated field has errors.
I'm having a hard time finding information on how to check whether a field has errors associated with it, from within widget definition code.
At the moment I have the following stub widget code (the final widget will use more complex markup).
from django import forms
from django.utils.safestring import mark_safe
class CustomTextWidget(forms.Widget):
def render(self, name, value, attrs):
field_has_errors=False # change to dynamically reflect field errors, somehow
if field_has_errors:
error_class_string="error"
else:
error_class_string=""
return mark_safe(
"<input type=\"text\" class=\"%s\" value=\"%s\" id=\"id_%s\" name=\"%s\">" % (error_class_string, value, name, name)
)
Can anyone shed light on a sensible way to populate the field_has_errors Boolean here? (or perhaps suggest a better way to accomplish what I'm trying to do). Thanks in advance.
As Jason says, the widget has no access to the field itself. I think a better solution though is to use the cascading nature of CSS.
{% for field in form %}
<div class="field{% if field.errors %} field_error{% endif %}">
{{ field }}
</div>
{% endfor %}
Now in your CSS you can do:
div.field_error input { color: red }
or whatever you need.
The widget has no knowledge of the field to which it is being applied. It is the field that maintains information about errors. You can check for error_messages in the init method of your form, and inject an error class to your widget accordingly:
class YourForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(YourForm, self).__init__(*args, **kwargs)
attrs = {}
if self.fields['your_field'].error_messages is not None:
attrs['class'] = 'errors'
self.fields['your_field'].widget = YourWidget(attrs=attrs)