I have had some issue where a leak of User variables onto the registration form allowed users to set these on user creation.
Can I ensure through testing that this will not be possible again?
Let's say for example I don't want it to be possible that users can set their own 'coupon_code'. How would I test for that?
Accounts/forms.py:
from django import forms
from django.contrib.auth.forms import UserCreationForm
from .models import User
class RegisterUserForm(UserCreationForm):
email = forms.EmailField(required=True, help_text='Required.')
class Meta(UserCreationForm.Meta):
model = User
fields = ('username', 'password1', 'password2', 'email')
exclude = ('coupon_code',)
Accounts/tests/test_forms.py:
from django.test import TestCase
from Accounts.forms import RegisterUserForm, UpdateUserForm
# Create your tests here.
class RegisterUserFormTest(TestCase):
##classmethod
#def setUpTestData(cls):
# Set up non-modified objects used by all test methods
def valid_data(self):
return {'username':'abc', 'email':'abc#abc.com', 'password1': 'test123hello', 'password2':'test123hello'}
def test_register_user_form_cannot_set_coupon_code(self):
data = self.valid_data()
data['coupon_code'] = '42'
form = RegisterUserForm(data=data)
self.assertFalse(form.is_valid())
Currently the above test fails, as the form is considered valid, even though excluded field is present
From what I understand, the is_valid() method ignores extra data passed to Form. If is_valid() it returns True, that only means data was valid for the prescribed fields, and only those fields will be in the cleaned_data dictionary (Django docs about this).
I think the test you want to perform must be done on the view function that uses this form. It could look like this:
from django.test import Client
def test_your_user_creation_view(self):
c = Client()
c.post(reverse(<your view name>),<valid data with 'coupon_code':42>)
new_user = User.objects.get(username='abc')
self.assertTrue(new_user.coupon_code != '42')
self.assertEqual(new_user.coupon_code,<your default value for coupon_code>)
Related
I extend the default user model with a proxy model to add an extra method.
from django.contrib.auth.models import User
class Person(User):
class Meta:
proxy = True
def custom_method(self):
pass
The main purpose is to use the method in templates.
<div>{{ user.custom_method }}</div>
But since the user is pointing to the default user model, it has no access to the custom_method.
Is there any way to achieve this other than create a subclass of the User model?
=== UPDATE ==============
I ended up with custom backends:
(not sure if this solution has any drawbacks)
# user proxy
class ExtendedUser(get_user_model()):
class Meta:
proxy = True
def custom_method(self):
...
# backends
from django.contrib.auth.backends import ModelBackend
# since I'm using allauth
from allauth.account.auth_backends import AuthenticationBackend
from .models import ExtendedUser
class DjangoModelBackend(ModelBackend):
def get_user(self, user_id):
print("\n\n\ncustom user!!!")
try:
user = ExtendedUser.objects.get(pk=user_id)
except ExtendedUser.DoesNotExist:
return None
return user if self.user_can_authenticate(user) else None
class AuthModelBackend(DjangoModelBackend, AuthenticationBackend):
pass
And in settings.py, add these to AUTHENTICATION_BACKENDS.
It might be better to monkey patch the user model. Indeed, in one of the AppConfigs [Django-doc] we can implement this with:
# app_name/apps.py
from django.apps import AppConfig
from django.contrib.auth import get_user_model
def custom_method(self):
return 'Some value'
class MyAppConfig(AppConfig):
name = 'app_name'
def ready(self):
User = get_user_model()
User.custom_method = custom_method
Here we thus add a method to the user model that can then be called in views, templates, etc.
I am learning Django but cannot understand ModelForm ans models.Model, could anyone explain the difference between them? Thanks!
Adapted from django docs
A ModelForm is a Form that maps closely to the fields defined in a specific Model. Models are defined by subclassing models.Model.
A Model is the source of information about your data. It contains the essential fields and behaviours of the data you’re storing. Generally, each model maps to a single database table. When you are developing your webapp, chances are that you’ll have forms that map closely to Django models. For this reason, Django provides a helper class that lets you create a Form class from a Django model. This helper class is the ModelForm class
The ModelForms you create are rendered in your templates so that users can create or update the actual data that is stored in the database tables defined by your Models.
For instance lets say you want to store data about articles in your web application, you would first define a model called Article like so:
in your models.py:
from django.db import models
class Article(models.Model):
title = models.CharField(max_length=30)
body = models.TextField(max_length=300)
in your forms.py you would create a modelForm to correspond with the Article model you have just created.
from .models import Article
from django.forms import ModelForm
class ArticleForm(ModelForm):
class Meta:
model = Article
fields = ['title', 'body']
and then in your views.py you can render your articleform:
from django.shortcuts import render
from django.http import HttpResponseRedirect
from .forms import ArticleForm
def article_form_view(request):
# if this is a POST request we need to process the form data
if request.method == 'POST':
# create a form instance and populate it with data from the request:
form = ArticleForm(request.POST)
# check whether it's valid:
if form.is_valid():
# process the data in form.cleaned_data as required
# ...
# redirect to a new URL:
return HttpResponseRedirect('/')
# if a GET (or any other method) we'll create a blank form
else:
form = ArticleForm()
return render(request, 'article_form.html', {'form': form})
This is my serializers.py (I want to create a serializer for the built-in User model):
from rest_framework import serializers
from django.contrib.auth.models import User
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('username', 'password', 'email', )
I'm aware that Django Rest Framework has it's own field validators, because when I try to create a user using a username which already exists, it raises an error saying:
{'username': [u'This field must be unique.']}
I want to customize the error message and make it say "This username is already taken. Please try again" rather than saying "This field must be unique".
It also has a built-in regex validator, because when I create a username with an exclamation mark, it says:
{'username': [u'Enter a valid username. This value may contain only letters, numbers and #/./+/-/_ characters.']}
I want to customize the regex validator so that it just says "Invalid username".
How do I customize all of the error messages which each field has?
Note: according to this post: Custom error messages in Django Rest Framework serializer I can do:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
def __init__(self, *args, **kwargs):
super(UserSerializer, self).__init__(*args, **kwargs)
self.fields['username'].error_messages['required'] = u'My custom required msg'
But what do I do for the 'unique' and 'regex' validators? I tried doing
self.fields['username'].error_messages['regex'] = u'My custom required msg'
and
self.fields['username'].error_messages['validators'] = u'My custom required msg'
but neither worked.
In order to replace unique or regex error messages you should change message member of corresponding validator object. This could be done using separate mixin class:
from django.core.validators import RegexValidator
from rest_framework.validators import UniqueValidator
from django.utils.translation import ugettext_lazy as _
class SetCustomErrorMessagesMixin:
"""
Replaces built-in validator messages with messages, defined in Meta class.
This mixin should be inherited before the actual Serializer class in order to call __init__ method.
Example of Meta class:
>>> class Meta:
>>> model = User
>>> fields = ('url', 'username', 'email', 'groups')
>>> custom_error_messages_for_validators = {
>>> 'username': {
>>> UniqueValidator: _('This username is already taken. Please, try again'),
>>> RegexValidator: _('Invalid username')
>>> }
>>> }
"""
def __init__(self, *args, **kwargs):
# noinspection PyArgumentList
super(SetCustomErrorMessagesMixin, self).__init__(*args, **kwargs)
self.replace_validators_messages()
def replace_validators_messages(self):
for field_name, validators_lookup in self.custom_error_messages_for_validators.items():
# noinspection PyUnresolvedReferences
for validator in self.fields[field_name].validators:
if type(validator) in validators_lookup:
validator.message = validators_lookup[type(validator)]
#property
def custom_error_messages_for_validators(self):
meta = getattr(self, 'Meta', None)
return getattr(meta, 'custom_error_messages_for_validators', {})
Then you could just inherit this mixin and update Meta class:
class UserSerializer(SetCustomErrorMessagesMixin, serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = ('url', 'username', 'email', 'groups')
custom_error_messages_for_validators = {
'username': {
UniqueValidator: _('This username is already taken. Please, try again'),
RegexValidator: _('Invalid username')
}
}
You also can do that just customizing your language translation, look here:
https://docs.djangoproject.com/en/3.2/topics/i18n/translation/#message-files
All you have to do is to create message files, change translation in there and compile your message into .mo files.
This method is not so much unacceptable as i might seems. Look here:
https://www.django-rest-framework.org/topics/internationalization/
DRF docs say that its common practice customizing error messages (your own and default ones) via translation.
My tips for those who`d want to try path above:
Make default django.po file. Most of default errors will be there.
If you are using DRF, then open this file https://raw.githubusercontent.com/encode/django-rest-framework/master/rest_framework/locale/en_US/LC_MESSAGES/django.po , chose error that you want to customize and copy only that error into your django.po flle above.
Customize it.
Compile it.
(!) NOTE Make sure that your translation file named exacly as django.po, not mylovelydjango.po, not myproject.po but EXACTLY django.po, or nothing will work.
I'm using the Django-Registration package to have users create accounts, authenticate them and log in to my webapp.
However, the form/view for account creation doesn't ask the user for a firstname/last name (those fields are part of the model BTW). It only asks for their email address, login ID and password (twice). I would like it to ask for the user's first name/last name (these fields can be optional... but it should still ask). I cannot find the form and view files to modify such that it asks for this info. I have modified the template file. But without the form and view modifications that's useless. How is this done?
In your forms.py, extend the DjangoRegistration form like:
class MyExtendedForm(RegistrationForm):
first_name = forms.CharField(widget=forms.TextInput(label="first_name"))
last_name = forms.CharField(widget=forms.TextInput(label="last_name"))
In the urls.py, tell django-registration to use this extended form:
# using my registration form to override the default
(r'^register/$',
register,
{'backend': 'registration.backends.default.DefaultBackend',
'form_class': MyExtendedForm}),
Define user_created to save the extra information:
def user_created(sender, user, request, **kwargs):
"""
Called via signals when user registers. Creates different profiles and
associations
"""
form = MyExtendedForm(request.Post)
# Update first and last name for user
user.first_name=form.data['first_name']
user.last_name=form.data['last_name']
user.save()
Then, register for signals from django-registration to call your function after any registration is processed:
from registration.signals import user_registered
user_registered.connect(user_created)
Once you have everything set up like shown here use following CustomUserForm:
class CustomUserForm(RegistrationForm):
class Meta(RegistrationForm.Meta):
model = CustomUser
fields = ['first_name','last_name','username','email','password1','password2']
To add the fields first_name and last_name from the default Django User model provide your own formclass:
Prepend the two fields to Meta.fields of the default RegistrationForm:
from django_registration.forms import RegistrationForm
class RegistrationWithNameForm(RegistrationForm):
class Meta(RegistrationForm.Meta):
fields = ["first_name", "last_name"] + RegistrationForm.Meta.fields
Override the default RegistrationView by adding a path to urls.py:
from django_registration.backends.activation.views import RegistrationView
from yourapp.forms import RegistrationWithNameForm
path('accounts/register/',
RegistrationView.as_view(form_class=RegistrationWithNameForm),
name='django_registration_register',
),
path("accounts/", include("django_registration.backends.activation.urls")),
Testcase:
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse
class RegistrationTestCase(TestCase):
registration_url = reverse("django_registration_register")
test_username = "testuser"
post_data = {
"first_name": "TestFirstName",
"last_name": "TestLastName",
"username": test_username,
"email": "testuser#example.com",
"password1": "mypass",
"password2": "mypass"
}
def test_register(self):
response = self.client.post(self.registration_url, self.post_data)
self.assertRedirects(response, reverse("django_registration_complete"))
user = get_user_model().objects.get(username=self.test_username)
# Assert all fields are present on the newly registered user
for field in ["username", "first_name", "last_name", "email"]:
self.assertEqual(self.post_data[field], getattr(user, field))
I'm trying to extend Django registration to include my own registration form. In principle this is fairly simple. I just have to write my own form (CustomRegistrationForm) which is a child of the original one (RegistrationForm). Then I can process my specific input by using the user_registered signal of django registration.
So here is what I did:
urls.py:
from django.conf.urls import patterns, include, url
from registration.views import register
from forms import CustomRegistrationForm
from django.contrib import admin
import regbackend
admin.autodiscover()
urlpatterns = patterns('',
url(r'^register/$', register, {'backend': 'registration.backends.default.DefaultBackend', 'form_class': CustomRegistrationForm, 'template_name': 'custom_profile/registration_form.html'},
name='registration_register'),
)
regbackend.py:
from django import forms
from models import UserProfile
from forms import CustomRegistrationForm
def user_created(sender, user, request, **kwargs):
form = CustomRegistrationForm(data=request.POST, files=request.FILES)
if form.is_valid(): # HERE: always fails
user_profile = UserProfile()
user_profile.user = user
user_profile.matriculation_number = form.cleaned_data['matriculation_number']
user_profile.save()
from registration.signals import user_registered
user_registered.connect(user_created)
forms.py:
from models import UserProfile
from django import forms
from registration.forms import RegistrationForm
from django.utils.translation import ugettext_lazy as _
attrs_dict = {'class': 'required'}
class CustomRegistrationForm(RegistrationForm):
matriculation_number = forms.CharField(widget=forms.TextInput(attrs=attrs_dict),
label=_("Matriculation number"),
max_length=12,
error_messages={'invalid': _("This value has to be unique and may contain only numbers.")},
initial=108)
def clean_matriculation_number(self):
print "Validating matriculation number."
data = self.cleaned_data['matriculation_number']
if len(data) != 12:
raise forms.ValidationError(_("Matriculation number consists of exactly 12 digits."))
return data
So the problem is the is_valid() function, because it always returns False. Even if there are no errors! So what is wrong? I spent hours on this one and I have no idea anymore :(
Any help is much appreciated!!
The reason form.is_valid() fails is probably because the form's "clean_username" function checks if the username passed to it already exists. Since the signal is sent after the User object is created and added to the database, the form will fail to pass this test every time. My guess would be if you logged form.cleaned_data after is_valid() returns False, you'll get a list of all of the fields except the username.
form.data might not contain changed values for the fields if the form clean_ function makes any changes (I couldn't find much documentation on form.data).
To avoid the potential problems this might cause, I made two classes for my custom registration form:
# I use this one for validating in my create_user_profile function
class MyRegistrationFormInternal(forms.Form):
# Just an example with only one field that holds a name
name = forms.CharField(initial="Your name", max_length=100)
def clean_name(self):
# (Optional) Change the name in some way, but do not check to see if it already exists
return self.cleaned_data['name'] + ' foo '
# This one is actually displayed
class MyRegistrationForm (MyRegistrationFormInternal):
# Here is where we check if the user already exists
def clean_name(self):
modified_name = super(MyRegistrationForm, self).clean_name()
# Check if a user with this name already exists
duplicate = (User.objects.filter(name=modified_name)
if duplicate.exists():
raise forms.ValidationError("A user with that name already exists.")
else:
return modified_name
Then, instead of using the form.data (which may still be "unclean" in some ways), you can run your POST data through MyRegistrationFormInternal, and is_valid() shouldn't always return false.
I realize this isn't the cleanest solution, but it avoids having to use the (possibly raw) form.data.
Ok I think I solved it (more or less).
I'm still not really sure, why the form did not validate. But as I said I was extending django-registration and the 'register' view already called is_valid() of the form, so I can assume that the form is valid when I process the posted data any futher. The view then calls the backend
backend.register(request, **form.cleaned_data)
with the request and the cleaned data (which is just username, email and password). So I can't use it for registration because my additional information is missing. The backend then fires the signal that I am using and what I did is, is that I created the form again with the provided request. This form, however, will NOT validate (and I tried everything!!) I looked it up, I am doing the exact same thing as django-registration, but it's not working in my code.
So I did not really solve the problem, because the form is still not validating. But I found peace with this, when I realized that the form was already validated by the 'register' view. So I am using form.data[..] instead of form.cleaned_data[..] now which shouldn't be a problem...