django test a modelform - ValidationError not a valid UUID - django

I am testing a modelform and getting a ValidationError. My model, view and test are as follows:
model
class Course(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL,
on_delete=models.CASCADE)
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
course_name = models.CharField(max_length=30)
grade_level = models.CharField(max_length=4, default="SEC")
view
# method_decorator([login_required, teacher_required], name='dispatch')
class CourseUpdateView(PermissionRequiredMixin, UpdateView):
raise_exceptions = True
permission_required = 'gradebook.change_course'
permission_denied_message = "You don't have access to this."
model = Course
fields = ['course_name', 'grade_level', ]
template_name_suffix = '_update'
def get_success_url(self, *args, **kwargs):
return reverse('gradebook:coursedetail', kwargs={'course_pk': self.object.pk})
form
class CourseForm(ModelForm):
class Meta:
model = Course
fields = ('course_name', 'grade_level',)
def __init__(self, user, *args, **kwargs):
super().__init__(*args, **kwargs)
self.qc = Course.objects.filter(user=user)
def clean(self):
super(CourseForm, self).clean()
course_name = self.cleaned_data.get('course_name')
if course_name and self.qc.filter(course_name__iexact=course_name).exists():
raise ValidationError("A course with that name already exists.")
if len(course_name) > 20:
if len(course_name) > 10:
raise ValidationError(
"Your course name cannot be longer than 20 characters")
return self.cleaned_data
Test
class CourseUpdateTests(TestCase):
#classmethod
def setUpTestData(cls):
cls.user = CustomUser.objects.create_user(
username='tester',
email='tester#email.com',
password='tester123',
is_teacher=True,
is_active=True,
)
cls.user.save()
def test_CourseUpdate_valid(self):
request = HttpRequest()
request.POST = {
'user': self.user,
'id': '4d192045-07fa-477f-bac2-5a99fe2e7d46',
'course_name': "Science",
'grade_level': "SEC"
}
form = CourseForm(request.POST)
self.assertTrue(form.is_valid())
The error I get:
Raise exceptions.ValidationError(
django.core.exceptions.ValidationError: ["“{'user': <CustomUser: tester>, 'id': '4d192045-07fa-477f-bac2-5a99fe2e7d46', 'course_name': 'Science', 'grade_level': 'SEC'}” is not a valid UUID."]
I have tried not putting the id in the request.POST but get the same error.
I originally tried to test for a valid form by using:
def test_CourseUpdate_valid(self):
form = CourseForm(data={
'user': self.user,
'id': '4d192045-07fa-477f-bac2-5a99fe2e7c04',
'course_name': "Science",
'grade_level': "SEC"
},)
self.assertTrue(form.is_valid())
This did not work though, giving me the error TypeError: __init__() missing 1 required positional argument: 'user'

Your original solution was not good because you were missing the user positional argument in the form init function.
Secondly, your CourseForm class should specify the rest of the fields (id, and user) if you want to pass them to the form.
You could probably just not pass id and user to the CourseForm data in the test as they aren't relevant.
This should work:
def test_CourseUpdate_valid(self):
form = CourseForm(self.user, data={
'course_name': "Science",
'grade_level': "SEC"
},)
self.assertTrue(form.is_valid())
Can you try that and let me know if the problem persists?

Related

Django form test fails

I'm trying to perform a simple test on my form to confirm that it's not valid when there is no data given and is valid when data is given.
When running tests with pytest (py.test) the test with no data works fine but I'm getting this error for the test with data present:
AssertionError: Should be valid if data is given
E assert False is True
E + where False = <bound method BaseForm.is_valid of <PostForm bound=True, valid=False, fields=(title;content;author;image;published;draft;category;read_time)>>()
E + where <bound method BaseForm.is_valid of <PostForm bound=True, valid=False, fields=(title;content;author;image;published;draft;category;read_time)>> = <PostForm bound=True, valid=False, fields=(title;content;author;image;published;draft;category;read_time)>.is_valid
posts/tests/test_forms.py:21: AssertionError
my models.py:
from django.db import models
from django.core.urlresolvers import reverse
from django.conf import settings
from django.db.models.signals import pre_save
from django.utils import timezone
from django.utils.text import slugify
from .utils import read_time
class Category(models.Model):
name = models.CharField(max_length=120, unique=True)
timestamp = models.DateTimeField(auto_now_add=True, auto_now=False)
updated = models.DateTimeField(auto_now_add=False, auto_now=True)
slug = models.SlugField(unique=True)
def __str__(self):
return self.name
def save(self, *args, **kwargs):
if not self.id: # to prevent changing slug on updates
self.slug = slugify(self.name)
return super(Category, self).save(*args, **kwargs)
def upload_location(instance, filename):
return '%s/%s'%(instance.id, filename)
class PostManager(models.Manager):
def active(self):
return super(PostManager, self).filter(draft=False, published__lte=timezone.now())
class Post(models.Model):
title = models.CharField(max_length=120)
slug = models.SlugField(unique=True)
content = models.TextField()
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
image = models.ImageField(
upload_to=upload_location,
null=True,
blank=True)
timestamp = models.DateTimeField(auto_now_add=True, auto_now=False)
updated = models.DateTimeField(auto_now_add=False, auto_now=True)
published = models.DateField(auto_now=False, auto_now_add=False)
draft = models.BooleanField(default=False)
category = models.ManyToManyField(Category)
read_time = models.IntegerField(default=0)
objects = PostManager()
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('posts:detail', kwargs={'pk': self.pk})
def save_no_img(self):
self.image = None
return super(Post, self).save()
def create_slug(instance, new_slug=None):
slug = slugify(instance.title)
if new_slug is not None:
slug = new_slug
qs = Post.objects.filter(slug=slug).order_by("-id")
exists = qs.exists()
if exists:
new_slug = "%s-%s" %(slug, qs.first().id)
return create_slug(instance, new_slug=new_slug)
return slug
def pre_save_post_receiver(sender, instance, *args, **kwargs):
if not instance.slug:
instance.slug = create_slug(instance)
html_content = instance.content
instance.read_time = read_time(html_content)
pre_save.connect(pre_save_post_receiver, sender=Post)
my forms.py:
from django import forms
from .models import Post
from pagedown.widgets import PagedownWidget
class PostForm(forms.ModelForm):
published = forms.DateField(widget=forms.SelectDateWidget)
content = forms.CharField(widget=PagedownWidget())
class Meta:
model = Post
# fields = ['author', 'title', 'content', 'image', 'draft', 'published', 'category']
exclude = ['objects', 'updated', 'timestamp', 'slug']
test_forms.py:
import pytest
from .. import forms
from posts.models import Category
from mixer.backend.django import mixer
pytestmark = pytest.mark.django_db
class TestPostForm():
def test_empty_form(self):
form = forms.PostForm(data={})
assert form.is_valid() is False, 'Should be invalid if no data is given'
def test_not_empty_form(self):
staff_user = mixer.blend('auth.User', is_staff=True)
category = mixer.blend('posts.Category')
data={'content': 'some content',
'author': staff_user,
'title': 'some title',
'category': category,}
form = forms.PostForm(data=data)
assert form.is_valid() is True, 'Should be valid if data is given'
update:
collected more specific errors using:
assert form.errors == {}, 'should be empty'
errors:
{'author': ['Select a valid choice. That choice is not one of the
available choices.'],
'category': ['Enter a list of values.'],
'published': ['This field is required.'],
'read_time': ['This field is required.']}
how to address them?
update 2:
as Nadège suggested I modified data to include published and read_time, changed category into a list and created a user without mixer.
staff_user = User.objects.create_superuser(is_staff=True,
email='oo#gm.com',
username='staffuser',
password='somepass')
category = mixer.blend('posts.Category')
today = date.today()
data={'content': 'some content',
'author': staff_user,
'title': 'some title',
'published': today,
'read_time': 1,
'category': [category],}
There is still error regarding the 'author':
{'author': ['Select a valid choice. That choice is not one of the
available choices.']}
update 3:
for some reason 'author' had to be provided as an id, the working code for this test looks like this:
class TestPostForm():
def test_empty_form(self):
form = forms.PostForm(data={})
assert form.is_valid() is False, 'Should be invalid if no data is given'
def test_not_empty_form(self):
staff_user = mixer.blend('auth.User')
category = mixer.blend('posts.Category')
today = date.today()
data={'content': 'some content',
'author': staff_user.id,
'title': 'some title',
'published': today,
'read_time': 1,
'category': [category],}
form = forms.PostForm(data=data)
assert form.errors == {}, 'shoud be empty'
assert form.is_valid() is True, 'Should be valid if data is given'
Ok so when you have an invalid form, first thing is to check why, so the errors of the form. With this new information we can fix each problem.
Your form has 4 validations errors. The last two are pretty straightforward.
'published': ['This field is required.'],
'read_time': ['This field is required.']
Those two fields in your form are required but you didn't filled them.
So you have two options,
Add a value for those fields in the data you give to the form
Remove the fields from the form: add them to exclude
You can also set the published field a not required like this:
published = forms.DateField(widget=forms.SelectDateWidget, required=False)
for read_time, the field is required or not, depending on the corresponding field in the model. If the model field is not nullable, the field in the form is set as required.
Next there is
'category': ['Enter a list of values.']
You provided a value but the type is not what was expected.
category in your model is ManyToMany so you can't give just one category, it must be a list (even if it has only one element!)
'category': [category],
Finally the author,
'author': ['Select a valid choice. That choice is not one of the available choices.']
There too you provided a value that is not valid. The validation doesn't recognize the value as a proper auth.User. I'm not familiar with Mixer, so maybe ask a new question specifically about Mixer and Django Forms with a ForeignKey.

DJANGO REST API: Not NULL Constraint Failed

I'm working on a User Preferences viewset in DJANGO REST API in which a user can get a list of preferences along with updating the preferences. In Postman i can get the user's preferences, but when i go to 'put' i'm getting the following error: Integrity Error --NOT NULL constraint failed: pugorugh_userpref.age --any reason this might be happening?
The UserPref Model is below:
class UserPref(models.Model):
user = models.ForeignKey(User)
age = models.CharField(choices=AGE, max_length=7, default='b,y,a,s')
gender = models.CharField(choices=GENDER_PREF, max_length=3, default='m,f')
size = models.CharField(choices=SIZE_PREF, max_length=8, default='s,m,l,xl')
def __str__(self):
return '{} preferences'.format(self.user)
def create_user_preference(sender, **kwargs):
user = kwargs['instance']
if kwargs['created']:
user_pref = UserPref(user=user)
user_pref.save()
post_save.connect(create_user_preference, sender=User)
Here is my ViewSet:
class UserPrefViewSet(
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
viewsets.GenericViewSet):
"""View to get and update User Preferences."""
permission_classes = (permissions.IsAuthenticated,)
queryset = models.UserPref.objects.all()
serializer_class = serializers.UserPrefSerializer
# /api/user/preferences/
#list_route(methods=['get', 'put'])
def preferences(self, request, pk=None):
user = request.user
user_pref = models.UserPref.objects.get(user=user)
if request.method == 'PUT':
data = request.data
user_pref.age = data.get('age')
user_pref.gender = data.get('gender')
user_pref.size = data.get('size')
user_pref.save()
serializer = serializers.UserPrefSerializer(user_pref)
return Response(serializer.data)
and SERIALIZER
class UserPrefSerializer(serializers.ModelSerializer):
extra_kwargs = {
'user': {'write_only': True}
}
class Meta:
fields = (
'age',
'gender',
'size'
)
model = models.UserPref
Looks like PUT data doesnt contain age value. Since age field is not nullable, blank age value raise error.
Try to fix this:
user_pref.age = data.get('age') or user_pref.age
...
user_pref.save()
this allows not to change age if value not in request data.

How to raise forms validation error when ever the user try's to create an object with the object twice

I'v used the unique together model meta, it works but it just comes up with this error. i want to raise a forms validation error, rather than a IntegrityError.
IntegrityError at /name/
UNIQUE constraint failed: canvas_canvas.user_id, canvas_canvas.canvas_name
Request Method: POST
Request URL: http://127.0.0.1:8000/name/
Exception Type: IntegrityError
Exception Value:
UNIQUE constraint failed: canvas_canvas.user_id, canvas_canvas.canvas_name
Exception Location: C:\Users\AppData\Local\Programs\Python\Python35-32\lib\site-packages\django\db\backends\sqlite3\base.py in execute, line 337
class Canvas(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, default=1)
canvas_name = models.CharField(
max_length=100,
validators=[
# validate_canvas_title,
RegexValidator(
regex=CANVAS_REGEX,
message='Canvas must only contain Alpahnumeric characters',
code='invalid_canvas_title'
)],
)
slug = models.SlugField(max_length=100, blank=True)
background_image = models.ImageField(
upload_to=upload_location,
null=True,
blank=True,
)
# sort catergory into alphabetical order
category = models.ForeignKey('category.Category', default=1, blank=True)
followers = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='canvas_followed_by', blank=True)
class Meta:
unique_together = ['user', 'canvas_name']
form
class CanvasModelForm(ModelForm):
class Meta:
model = Canvas
fields = ['canvas_name', 'category', 'background_image']
widgets = {
'canvas_name': TextInput(attrs={'class': 'form-input'}),
'category': Select(attrs={'class': 'form-input'}),
}
view
user = get_object_or_404(User, username=username)
form_create = CanvasModelCreateForm(request.POST or None)
if form_create.is_valid():
instance = form_create.save(commit=False)
instance.user = request.user
instance.save()
return redirect('canvases:canvas', username=request.user.username, slug=instance.slug)
template = 'pages/profile.html'
context = {
'user': user,
'form_create': form_create,
}
return render(request, template, context)
You could do this by passing request.user into the form and use it for validating the canvas_name.
You need to override the form's __init__ method to take an extra keyword argument, user. This stores the user in the form, where it's required, and from where you can access it in your clean method.
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user', None)
super(CanvasModelCreateForm, self).__init__(*args, **kwargs)
def clean_canvas_name(self):
canvas_name = self.cleaned_data.get('canvas_name')
if Canvas.objects.get(user=self.user, canvas_name=canvas_name).exists():
raise forms.ValidationError(u'Canvas with same name already exists.')
return canvas_name
And you should change in your view like this so,
form_create = CanvasModelCreateForm(request.POST, user=request.user)

How to pass request.user to a model's clean method for a foreignkey unique_together validation?

I'm trying to run a validation where a user can't enter the same name_field twice but other users entering the same name will not interfere.
I tried using "unique_together = (("username","name_field"))" but when a user enters the same value twice the server generates an integrity error as opposed to rendering a warning message next to the form field.
then I tried overriding the clean() method in my model, Which runs fine if I only check "field_name" like so:
def clean(self):
existing = self.__class__.objects.filter(
name_field=self.name_field).count()
if existing > 0:
raise ValidationError({'name_field':self.username })
But I am running into trouble when checking the username value, for instance:
def clean(self):
existing = self.__class__.objects.filter(
username=self.username, ###This part crashes!!! (username not found)
name_field=self.name_field).count()
if existing > 0:
raise ValidationError({'name_field':self.username })
I'm guessing due to it not being an actual field in the form its not present during the call to clean(). So my question is am I doing the validation correctly for this kind of problem? And how can I pass or where can I find the value for the current user from within a models clean method (in a safe way hopefully without adding fields to my form)?
views.py
def add_stuff(request):
if request.user.is_authenticated():
form = addStuff(request.POST or None)
if request.method == 'POST':
if form.is_valid():
sub_form = form.save(commit=False)
sub_form.username = request.user
sub_form.save()
return redirect('../somewhere_else/')
forms.py
class addStuff(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(addStuff, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_tag = False
self.helper.layout = Layout(
'name_field',
'type_field',
ButtonHolder(
Submit('Save', 'Save')
),
)
class Meta:
model = UserStuff
fields = ('name_field',
'type_field',
)
models.py
class UserStuff(models.Model):
username = models.ForeignKey(User)
name_field = models.CharField(max_length=24, blank=False,null=False)
type_field = models.CharField(max_length=24, blank=True,null=True)
def clean(self):
existing = self.__class__.objects.filter(
username=self.username, ###This part crashes!!! (username not found)
name_field=self.name_field).count()
if existing > 0:
raise ValidationError({'name_field':self.username })
def __unicode__(self):
return "%s For User: \"%s\" " % (self.name_field, self.username)
class Meta:
managed = True
db_table = 'my_db_table'
unique_together = (("username","name_field"))
Thanks for any insight!
I now am running the clean override from the form instead of the model (as recommended by Daniel). This has solved a bunch of issues and I now have a working concept:
models.py
class UserStuff(models.Model):
username = models.ForeignKey(User)
name_field = models.CharField(max_length=24, blank=False,null=False)
type_field = models.CharField(max_length=24, blank=True,null=True)
def clean(self):
existing = self.__class__.objects.filter(
username=self.username, ###This part crashes!!! (username not found)
name_field=self.name_field).count()
if existing > 0:
raise ValidationError({'name_field':self.username })
def __unicode__(self):
return "%s For User: \"%s\" " % (self.name_field, self.username)
class Meta:
managed = True
db_table = 'my_db_table'
forms.py
class addStuff(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(addStuff, self).__init__(*args, **kwargs)
initial = kwargs.pop('initial')
self.username = initial['user']
self.helper = FormHelper()
self.helper.form_tag = False
self.helper.layout = Layout(
'name_field',
'type_field',
ButtonHolder(
Submit('Save', 'Save')
),
)
def clean(self):
cleaned_data = super(addStuff, self).clean()
name_field = self.cleaned_data['name_field']
obj = UserStuff.objects.filter(username_id=self.username.id,
name_field=name_field,
)
if len(obj) > 0:
raise ValidationError({'name_field':
"This name already exists!" } )
return cleaned_data
class Meta:
model = UserStuff
fields = ('name_field',
'type_field',
)
views.py
def add_stuff(request):
if request.user.is_authenticated():
form = addStuff(request.POST or None,
initial={'user':request.user})
if request.method == 'POST':
if form.is_valid():
sub_form = form.save(commit=False)
sub_form.username = request.user
sub_form.save()
return redirect('../somewhere_else/')
best of luck!

Django -- form validation

I have a model that can access Api and return json data
class Video(models.Model):
url = models.URLField(_('URL'), blank=True)
type = models.CharField(max_length=10, null=True, blank=True)
def get_oembed_info(self, url):
api_url = 'http://api.embed.ly/1/oembed?'
params = {'url': url, 'format': 'json'}
fetch_url = 'http://api.embed.ly/1/oembed?%s' % urllib.urlencode(params)
result = urllib.urlopen(fetch_url).read()
result = json.loads(result)
return result
def get_video_info(self):
url = self.url
result = self.get_oembed_info(url)
KEYS = ('type', 'title', 'description', 'author_name')
for key in KEYS:
if result.has_key(key):
setattr(self, key, result[key])
def save(self, *args, **kwargs):
if not self.pk:
self.get_video_info()
super(Video, self).save(*args, **kwargs)
class VideoForm(forms.ModelForm):
def clean(self):
if not self.cleaned_data['url'] and not self.cleaned_data['slide_url']:
raise forms.ValidationError('Please provide either a video url or a slide url')
return self.cleaned_data
I want to access the type field while submitting the form, so if the type is other than "something" raise an Error like in the above clean method. Or how can I access get_oembed_info method result in VideoForm Class.
Solution
Well as Thomas said to call the model's clean method and then do the magic
def clean(self):
self.get_video_info()
if self.type == 'something':
raise ValidationError("Message")
A ModelForm is going to going to call your model's clean method during its validation process. That method can raise ValidationError's which will be added to your form's errors.
You could therefore implement your validation logic in your model's clean method, where the get_oembed_info method is available using self.get_oembed_info().