Pytest-django - testing creation and passing a required User object - django

Apologies if this has already been answered elsewhere. I cannot find an answer which I can retrofit into my situation.
I'm new to django so I feel the problem is me not getting a fundamental grasp of a presumably basic concept here...
Using DRF and pytest-django, i'm trying to be diligent and write tests along the way before it becomes too time consuming to manually test. I can see it snowballing pretty quickly.
The issue I face is when I try to test the creation of a Catalogue, I can't get it to pass an User instance to the mandatory field 'created_by'. The logic works fine when I test manually, but writing the test itself is causing me headaches.
Many thanks in advance!
The error is:
TypeError: Cannot encode None for key 'created_by' as POST data. Did you mean to pass an empty string or omit the value?
Code provided.
# core/models.py
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
email = models.EmailField(unique=True)
# workshop/models.py
from django.conf import settings
from django.db import models
class Catalogue(models.Model):
STATE_DRAFT = 'DRAFT'
STATE_PUBLISHED_PRIVATE = 'PUBPRIV'
STATE_PUBLISHED_PUBLIC = 'PUBPUB'
STATE_CHOICES = [
(STATE_DRAFT, 'Draft'),
(STATE_PUBLISHED_PRIVATE, 'Published (Private)'),
(STATE_PUBLISHED_PUBLIC, 'Published (Public)')
]
company = models.ForeignKey(Company, on_delete=models.PROTECT)
title = models.CharField(max_length=255)
description = models.TextField(null=True, blank=True)
state = models.CharField(
max_length=10, choices=STATE_CHOICES, default=STATE_DRAFT)
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
created_on = models.DateTimeField(auto_now_add=True)
def __str__(self) -> str:
return self.title
class CatalogueItem(models.Model):
TYPE_GOODS = 'GOODS'
TYPE_SERVICES = 'SERVICES'
TYPE_GOODS_AND_SERVICES = 'GOODS_AND_SERVICES'
TYPE_CHOICES = [
(TYPE_GOODS, 'Goods'),
(TYPE_SERVICES, 'Services'),
(TYPE_GOODS_AND_SERVICES, 'Goods & Services')
]
catalogue = models.ForeignKey(
Catalogue, on_delete=models.CASCADE, related_name='catalogueitems')
type = models.CharField(
max_length=50, choices=TYPE_CHOICES, default=TYPE_GOODS)
name = models.CharField(max_length=255)
description = models.TextField()
unit_price = models.DecimalField(max_digits=9, decimal_places=2)
can_be_discounted = models.BooleanField(default=True)
def __str__(self) -> str:
return self.name
#property
def item_type(self):
return self.get_type_display()
# workshop/serializers.py
class CatalogueSerializer(serializers.ModelSerializer):
catalogueitems = SimpleCatalogueItemSerializer(
many=True, read_only=True)
created_on = serializers.DateTimeField(read_only=True)
created_by = serializers.PrimaryKeyRelatedField(read_only=True)
class Meta:
# depth = 1
model = Catalogue
fields = ['id', 'title', 'description',
'state', 'catalogueitems', 'created_by', 'created_on']
def create(self, validated_data):
company_id = self.context['company_id']
user = self.context['user']
return Catalogue.objects.create(company_id=company_id, created_by=user, **validated_data)
# workshop/views.py
class CatalogueViewSet(ModelViewSet):
serializer_class = CatalogueSerializer
def get_permissions(self):
if self.request.method in ['PATCH', 'PUT', 'DELETE', 'POST']:
return [IsAdminUser()]
return [IsAuthenticated()]
def get_queryset(self):
user = self.request.user
if user.is_staff:
return Catalogue.objects.prefetch_related('catalogueitems__catalogue').filter(company_id=self.kwargs['company_pk'])
elif user.is_authenticated:
return Catalogue.objects.filter(company_id=self.kwargs['company_pk'], state='PUBPUB')
def get_serializer_context(self):
company_id = self.kwargs['company_pk']
return {'company_id': company_id, 'user': self.request.user}
# workshop/tests/conftest.py
from core.models import User
from rest_framework.test import APIClient
import pytest
#pytest.fixture
def api_client():
return APIClient()
#pytest.fixture
def authenticate(api_client):
def do_authenticate(is_staff=False):
return api_client.force_authenticate(user=User(is_staff=is_staff))
return do_authenticate
# workshop/tests/test_catalogues.py
from core.models import User
from workshop.models import Catalogue
from rest_framework import status
import pytest
#pytest.fixture
def create_catalogue(api_client):
def do_create_catalogue(catalogue):
return api_client.post('/companies/1/catalogues/', catalogue)
return do_create_catalogue
class TestCreateCatalogue:
def test_if_admin_can_create_catalogue_returns_201(self, authenticate, create_catalogue):
user = authenticate(is_staff=True)
response = create_catalogue(
{'title': 'a', 'description': 'a', 'state': 'DRAFT','created_by':user})
assert response.status_code == status.HTTP_201_CREATED

I think you may have a problem with the user that you are using to do the test,
when you call authenticate it returns a client which is not the same as a user.
then you run the authenticate and log in as a generic user. Try making another fixture that creates a user first, authenticate with that user to return the client and then post that user you created to create_catalogue
from django.conf import settings
#pytest.fixture
def create_user() -> User:
return settings.AUTH_USER_MODEL.objects.create(
username="Test User", password="Test Password", email="testuser#example.com"
)
#pytest.fixture
def authenticate(api_client):
def do_authenticate(create_user):
return api_client.force_authenticate(create_user)
return do_authenticate
class TestCreateCatalogue:
def test_if_admin_can_create_catalogue_returns_201(self, authenticate, create_user create_catalogue):
user = authenticate(create_user)
response = create_catalogue(
{'title': 'a', 'description': 'a', 'state': 'DRAFT','created_by':create_user})
assert response.status_code == status.HTTP_201_CREATED

Related

Django tests AssertionError for update view

I tried to create Django test for UpdateView
but I have such problem as:
self.assertEqual(application.credit_purpose, 'House Loan')
AssertionError: 'Car Loan' != 'House Loan'
Car Loan
House Loa
def test_application_update(self):
application = Application.objects.create(customer=self.customer, credit_amount=10000, credit_term=12,
credit_purpose='Car Loan', credit_pledge=self.pledge,
product=self.product,
number_request=2, date_posted='2020-01-01', reason='None',
repayment_source=self.repayment, possible_payment=1000,
date_refuse='2020-01-02', protokol_number='123457',
status=self.status,
language=0, day_of_payment=1, credit_user=self.user)
response = self.client.post(
reverse('application_update', kwargs={'pk': application.id}),
{'credit_purpose': 'House Loan'})
self.assertEqual(response.status_code, 200)
application.refresh_from_db()
self.assertEqual(application.credit_purpose, 'House Loan')
This is my model
class Application(AbstractCredit):
number_request = models.IntegerField(verbose_name='Номер заявки', unique=True, default=number_auto) # Добавить автоинкремент
date_posted = models.DateField(verbose_name='Дата заявки', auto_now_add=True)
reason = models.CharField(max_length=200, null=True, blank=True, verbose_name='Причина отказа/Условия одобрения')
repayment_source = models.ForeignKey(Repayment, on_delete=models.CASCADE, verbose_name='Источник погашения')
possible_payment = models.IntegerField(verbose_name='Желаемая сумма ежемесячного взноса')
date_refuse = models.DateField(default=one_day_more, null=True, blank=True, verbose_name='Дата отказа/одобрения')
protokol_number = models.CharField(max_length=20, unique=True, null=True, blank=True,
verbose_name='Номер протокола')
status = models.ForeignKey(Status, on_delete=models.CASCADE, default=1, verbose_name='Статус')
language = models.IntegerField(choices=LANGUAGES_CHOICES, verbose_name='Язык договора', blank=True, null=True)
day_of_payment = models.IntegerField(choices=DAY_OF_PAYMENT_CHOICES,
verbose_name='Предпочитаемый день оплаты по кредиту')
credit_user = models.ForeignKey(User, on_delete=models.SET(0), verbose_name='Кредитный специалист')
This is my view
class ApplicationUpdate(BasePermissionMixin, SuccessMessageMixin, UpdateView):
model = Application
form_class = ApplicationForm
template_name = 'standart_form.html'
permission_required = 'Изменение заявки'
success_message = 'Заявка успешно изменена'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['button_name'] = 'Сохранить'
return context
def get_success_url(self):
return reverse_lazy('application_detail', kwargs={'pk': self.get_object().id})
So, I also got stuck in this for a while. The main problem is at:
...
response = self.client.post(
reverse('application_update', kwargs={'pk': application.id}),
{'credit_purpose': 'House Loan'}
)
...
I was able to understand thanks to this answer. It happens because you are posting to a form and it expects to have all fields filled, otherwise it will fail validation and thus will not call .save().
What you can do is, after creating the object copy its data into a dictionary, then modify it before posting:
tests.py
from django.forms.models import model_to_dict
class ApplicationTestCase(TestCase):
def setUp(self):
customer = Customer.objects.create(...)
...
self.data = {
'customer': customer, 'credit_amount': 10000, 'credit_term': 12,
'credit_purpose': 'Car Loan', ...
}
def test_application_update(self):
application = Application.objects.create(**self.data)
post_data = model_to_dict(application)
post_data['credit_purpose'] = 'House Loan'
response = self.client.post(
reverse(
'app:view-name', kwargs={'pk': application.id}),
post_data
)
# print(application.credit_purpose)
# application.refresh_from_db()
# print(application.credit_purpose)
# It returns a redirect so code is 302 not 200.
self.assertEqual(response.status_code, 302)
self.assertEqual(application.credit_purpose, 'House Loan')
Also, get_absolute_url() is set in the wrong place should be under models:
models.py
class Application(AbstractCredit):
...
def get_absolute_url(self):
return reverse('app:view-name', kwargs={'pk': self.pk})

How to write test for Django image url

I'm writing my first test as a Django developer.
How do I write a test for the image property of this model shown below
class Project(models.Model):
engineer = models.ForeignKey(Engineer, on_delete=models.CASCADE)
name = models.CharField(max_length=100)
tech_used = models.CharField(max_length=200)
updated = models.DateTimeField(auto_now=True)
created = models.DateTimeField(auto_now_add=True)
image = models.ImageField(default="project.png")
description = models.TextField(null=True, blank=True)
repo_url = models.URLField(null=True, blank=True)
live_url = models.URLField(null=True, blank=True)
make_public = models.BooleanField(default=False)
def __str__(self):
return self.name
#property
def image_url(self):
try:
url = self.image.url
except:
url = ""
return url
class Meta:
ordering = ["-created"]
I have tried to conjure some patches from here and there although I don't fully understand what my test.py code I just thought to include it to my question to show my efforts
class TestModels(TestCase):
def setUp(self):
new_image = BytesIO()
self.image = Project.objects.create(
image=ImageFile(new_image)
)
def test_image_url(self):
self.assertEquals(self.image.image_url, '')
Please a help on the above question and an explanation of whatever code given as help will be highly appreciated.
Following this answer, one possible way is:
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase
from django.utils import timezone
from .models import Engineer, Project
class ProjectImageUploadTest(TestCase):
def setUp(self):
self.engineer = Engineer.objects.create(name='Engineer')
self.project = {
'engineer': self.engineer,
'name': 'Test project',
'tech_used': 'Test tech',
'updated': timezone.now(),
'created': timezone.now(),
'image': None,
'description': 'Test description',
'repo_url': 'https://www.repo.example.com',
'live_url': 'https://www.live.example.com',
'make_public': False
}
self.obj = Project.objects.create(**self.project)
def tearDown(self):
self.obj.image.delete()
def test_upload_image(self):
img_url = r'X:\Path\to\your\image.png'
self.obj.image= SimpleUploadedFile(
name='test_image.jpg',
content=open(img_url, 'rb').read(),
content_type='image/png')
self.obj.save()
self.obj.refresh_from_db()
self.assertNotEqual(self.obj.image, None)
I would like to complement saying that is also possible to implement the test using Python's tempfile.

Django Admin Inline Form Fails in Test but Works in Admin Interface

So I am at a loss as to why the following example fails in a test case, but works perfectly fine in the admin interface.
I have a very basic User and Login models. The Login simply records user logins into the system via Django signal.
class User(AbstractUser):
company = models.CharField(
max_length=100, blank=True, null=True
)
class Login(models.Model):
"""Represents a record of a user login event."""
user = models.ForeignKey(
get_user_model(), on_delete=models.CASCADE, related_name="logins"
)
ip = models.GenericIPAddressField()
user_agent = models.TextField()
date = models.DateTimeField(auto_now_add=True, db_index=True)
domain = models.CharField(max_length=255)
http_host = models.CharField(null=True, max_length=255)
remote_host = models.CharField(null=True, max_length=255)
server_name = models.CharField(null=True, max_length=255)
In the admin.py I define a UserAdmin with inlines = [LoginInline]. Basically when viewing the user I can see the login history.
from django.contrib import admin
from django.contrib.auth import admin as auth_admin
from django.contrib.auth import get_user_model
from django.urls import reverse
from django.utils.html import format_html
from users.models import Login
User = get_user_model()
class ReadOnlyModelMixin:
def has_add_permission(self, request, obj=None):
return False
def has_change_permission(self, request, obj=None):
return False
def has_delete_permission(self, request, obj=None):
return False
class LoginAdmin(ReadOnlyModelMixin, admin.ModelAdmin):
list_display = (
'id',
'user_link_with_name',
'user_email',
'user_company',
'domain',
'date',
'ip',
'remote_host',
'http_host',
'server_name',
'user_agent',
)
#admin.display(description='Name', ordering='user__first_name')
def user_link_with_name(self, obj):
url = reverse("admin:users_user_change", args=[obj.user.id])
return format_html(f'{obj.user}')
#admin.display(description='Email', ordering='user__email')
def user_email(self, obj):
return format_html(
f'{obj.user.email}'
)
#admin.display(description='Company', ordering="user__company")
def user_company(self, obj):
return obj.user.company
class LoginInline(ReadOnlyModelMixin, admin.TabularInline):
model = Login
class UserAdmin(auth_admin.UserAdmin):
inlines = [LoginInline]
admin.site.register(User, UserAdmin)
admin.site.register(Login, LoginAdmin)
It works perfectly fine in the Admin interface, I can view and ADD users no problem.
I then introduced test as follows:
class TestUserAdmin:
def test_add(self, admin_client):
url = reverse("admin:users_user_add")
response = admin_client.get(url)
assert response.status_code == 200
response = admin_client.post(
url,
data={
"username": "test",
"password1": "My_R#ndom-P#ssw0rd",
"password2": "My_R#ndom-P#ssw0rd",
},
)
# below FAILS when UserAdmin includes inlines = [LoginInline]
assert User.objects.filter(username="test").exists()
assert response.status_code == 302
the line assert User.objects.filter(username="test").exists() will FAIL as long as I have inlines = [LoginInline] included in the UserAdmin. If I comment out the inline then the test passes.
I really want to understand what I am missing, and why this test is failing, given that it works perfectly fine in the Admin Interface.
A repo that reproduces this problem: https://github.com/oizik/django_admin_inline_test_problem
Thank you.

DRF is not updating the database

Here's the model:
from django.db import models
from datetime import datetime, timedelta
# Create your models here.
def set_expiration():
return datetime.today().date() + timedelta(days=30)
class Customer(models.Model):
email = models.EmailField(max_length=254, unique=True)
created_on = models.DateField(auto_now_add=True)
expires_on = models.DateField(editable=False, default=set_expiration())
def __str__(self):
return self.email
And this is the view:
#api_view(['POST'])
def update_customer(request):
try:
customer = Customer.objects.get(email=request.data['email'])
except ObjectDoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
serializer = CustomerSerializer(instance=customer, data=request.data)
if serializer.is_valid(raise_exception=True):
serializer.save()
return Response(serializer.data)
And the serilizer:
from rest_framework import serializers
from .models import Customer
class CustomerSerializer(serializers.ModelSerializer):
class Meta:
model = Customer
fields = '__all__'
At this moment I have a record in database with expires_on set to 2022-03-14 and I want to update that to 2024-12-12 so I call the endpoint with the following data:
{
"email": "myemail#mydomain.com",
"expires_on": "2024-12-12"
}
The view returns this:
{
"id": 1,
"email": "myemail#mydomain.com",
"created_on": "2022-02-12",
"expires_on": "2022-03-14"
}
This is the existing data. expires_on is not updated with the new value.
I get no error and no exception. It just doesn't work.
Remove editable=False from expires_on field.
class Customer(models.Model):
email = models.EmailField(max_length=254, unique=True)
created_on = models.DateField(auto_now_add=True)
expires_on = models.DateField(default=set_expiration())
def __str__(self):
return self.email

Django Model Terms of Service With Timestamp, Username, and Prompt User To Agree To Updated Terms of Service

I would like to create a model that contains a timestamp and the allauth currently logged in user who agreed to the Terms of Service. Then, on every page (if the user is logged in), annotate if the user has agreed to the latest Terms of Service (by comparing the timestamp of their last agreement to the timestamp of the latest updated Terms of Service), and if the user has not agreed to the most recent Terms of Service version they are redirected to a page that requires them to agree to the updated version. Then it redirects the user back to whence they came after they agree.
How does one go about creating something like this?
What I have so far is below.
Models.py:
from django.contrib.auth.models import User
class TermsOfService(models.Model):
agreement = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True,blank=True, null=True)
user = models.ForeignKey(User, blank=True, null=True, on_delete=models.CASCADE)
def __str__(self):
return self.agreement
class UserMembership(models.Model):
user = models.OneToOneField(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
stripe_customer_id = models.CharField(max_length=40, unique=True)
membership = models.ForeignKey(
Membership, on_delete=models.SET_NULL, null=True)
def __str__(self):
return self.user.username
Forms.py:
from .models import TermsOfService
class TermsOfServiceForm(forms.ModelForm):
class Meta:
model = TermsOfService
fields = ('agreement',)
def __init__(self, *args, **kwargs):
super(TermsOfServiceForm, self).__init__(*args, **kwargs)
self.fields['agreement'].widget.attrs={ 'id': 'agreement_field', 'class': 'form-control', 'required': 'true', 'autocomplete':'off'}
App Urls.py:
from django.urls import path
from .views import ( terms_of_service_view )
app_name = 'app'
urlpatterns = [ path('terms_of_service_view/', terms_of_service_view, name='terms_of_service_view'), ]
Views.py:
def get_user_membership(request):
user_membership_qs = UserMembership.objects.filter(user=request.user)
if user_membership_qs.exists():
return user_membership_qs.first()
return None
def terms_of_service_view(request):
if request.method == 'POST':
form = TermsOfServiceForm(request.POST)
if form.is_valid():
user_membership = get_user_membership(request)
instance = form.save(commit=False)
instance.user = request.user
instance.save()
context = {
'user_membership': user_membership,
'form':form
}
return render(request, "index.html", context)
else:
form = TermsOfServiceForm()
context = {
'user_membership': user_membership,
'form': form,
}
return render(request, "index.html", context)
A question arises from your code, like how are you going to determine when user needs to agree to agreement, do you create a bunch of new entry in TermsOfService. Rather than that, why not create a new model named Terms and add it as ForeignKey.
class Term(models.Model):
text = models.TextField()
created_at = models.DateTimeField(auto_now_add=True,blank=True, null=True)
# blah blah
class TermsOfService(models.Model):
term = models.ForeignKey(Term, on_delete=models.DO_NOTHING)
agreement = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True,blank=True, null=True)
user = models.ForeignKey(User, blank=True, null=True, on_delete=models.CASCADE)
There is an advantage of taking this approach, that is all you need to do is create a new Term object, and rest can be taken care of by middleware. For example:
from django.urls import reverse
from django.shortcuts import redirect
class TermAgreeMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
if not request.user.is_authenticated:
return response
term_date = Term.objects.last().created_at
user_term_date = request.user.termofservice_set.filter(created_at__gte=term_date).exists()
if not user_term_date:
return redirect(reverse('app:terms_of_service_view')+'?next='+request.path)
return response
And update the view:
def terms_of_service_view(request):
if request.method == 'POST':
form = TermsOfServiceForm(request.POST)
if form.is_valid():
user_membership = request.user.usermembership # you don't need another view as User has OneToOne relation with UserMembership
instance = form.save(commit=False)
instance.user = request.user
instance.term = Term.objects.last()
instance.save()
go_next = request.GET.get('next', None) # handle redirection
if go_next:
return redirect(go_next)
context = {
'user_membership': user_membership,
'form':form
}
return render(request, "index.html", context)
else:
form = TermsOfServiceForm()
context = {
'user_membership': user_membership,
'form': form,
}
return render(request, "index.html", context)
Finally add that TermAgreeMiddleware in MIDDLEWARE settings. So everytime you want users to agree a new term, just create a new Term instance(from admin site or shell).