Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions borrowing/filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from django_filters import rest_framework as filters

from borrowing.models import Borrowing


class CustomFilter(filters.FilterSet):
"""
FilterSet for CustomFilter
"""
is_active = filters.BooleanFilter(method="filter_is_active", label="Active")
user_id = filters.NumberFilter(method="filter_user_id")

class Meta:
model = Borrowing
fields = ["is_active", "user_id"]

def filter_is_active(self, queryset, name, value):
if value:
return queryset.filter(actual_return_date__isnull=True)
else:
return queryset.filter(actual_return_date__isnull=False)

def filter_user_id(self, queryset, name, value):
request = self.request
if request.user.is_staff:
if value:
return queryset.filter(user_id=value)
return queryset
else:
return queryset.filter(user=request.user)
7 changes: 7 additions & 0 deletions borrowing/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,15 @@


class Borrowing(models.Model):
"""
Borrowing model with attributes:
borrow_date, expected_return_date, actual_return_date, book, user
"""
borrow_date = models.DateField(auto_now_add=True)
expected_return_date = models.DateField()
actual_return_date = models.DateField(null=True, blank=True)
book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name="borrowings")
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="borrowings")

def __str__(self):
return str(self.borrow_date)
96 changes: 96 additions & 0 deletions borrowing/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
from rest_framework import serializers

from book.models import Book
from book.serializers import BookSerializer
from borrowing.models import Borrowing


class BorrowingSerializer(serializers.ModelSerializer):
"""
Borrowing Serializer with validation for only one active borrowing.
"""
book = serializers.SlugRelatedField(
queryset=Book.objects.all(),
slug_field="title"
)

class Meta:
model = Borrowing
fields = [
"id",
"expected_return_date",
"book"
]

def validate(self, attrs):
user = self.context["request"].user
active_borrowings = Borrowing.objects.filter(
user=user,
actual_return_date__isnull=True
)

if active_borrowings.exists():
raise serializers.ValidationError(
"You already have an active borrowing. "
"Please return the current book before borrowing another."
)
return attrs


class BorrowingReturnBookSerializer(serializers.ModelSerializer):
"""
Borrowing Serializer for returning a borrowing book.
"""
return_book = serializers.BooleanField()

class Meta:
model = Borrowing
fields = ["return_book"]


class BorrowingListSerializer(serializers.ModelSerializer):
"""
Borrowing Serializer for borrowing list.
"""
book = serializers.SlugRelatedField(
many=False, read_only=True, slug_field="title"
)

class Meta:
model = Borrowing
fields = [
"id",
"borrow_date",
"expected_return_date",
"actual_return_date",
"book",
]

def to_representation(self, instance):
representation = super().to_representation(instance)
if not instance.actual_return_date:
representation.pop("actual_return_date", None)
return representation


class BorrowingDetailSerializer(serializers.ModelSerializer):
"""
Borrowing Serializer for detail of a borrowing book.
"""
book = BookSerializer(many=False, read_only=True)

class Meta:
model = Borrowing
fields = [
"id",
"borrow_date",
"expected_return_date",
"actual_return_date",
"book",
]

def to_representation(self, instance):
representation = super().to_representation(instance)
if not instance.actual_return_date:
representation.pop("actual_return_date", None)
return representation
1 change: 0 additions & 1 deletion borrowing/tests.py

This file was deleted.

Empty file added borrowing/tests/__init__.py
Empty file.
106 changes: 106 additions & 0 deletions borrowing/tests/test_borrowing_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
from rest_framework.test import APITestCase
from rest_framework import status
from django.urls import reverse
from datetime import timedelta
from django.utils.timezone import now
from django.contrib.auth import get_user_model

from book.models import Book
from borrowing.models import Borrowing

User = get_user_model()

class BorrowingViewSetTest(APITestCase):
def setUp(self):
self.user = User.objects.create_user(email="test@user.com", password="password123")
self.staff_user = User.objects.create_user(email="test@admin.com", password="adminpassword", is_staff=True)
self.book = Book.objects.create(title="Test Book", author="Author", inventory=5, daily_fee=1)
self.client.force_authenticate(user=self.user)

def test_create_borrowing(self):
"""Test creating a borrowing record."""
data = {
"expected_return_date": (now().date() + timedelta(days=7)),
"book": self.book.title,
}
url = reverse("borrowings:borrowings-list")
response = self.client.post(url, data, format="json")
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Borrowing.objects.count(), 1)
self.assertEqual(Borrowing.objects.first().user, self.user)

def test_create_borrowing_no_inventory(self):
"""Test creating a borrowing record when no copies are available."""
self.book.inventory = 0
self.book.save()

data = {
"expected_return_date": (now().date() + timedelta(days=7)),
"book": self.book.title,
}
url = reverse("borrowings:borrowings-list")
response = self.client.post(url, data, format="json")

self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

def test_list_borrowings_for_user(self):
"""Test retrieving the list of borrowings for the authenticated user."""
borrowing = Borrowing.objects.create(
expected_return_date=now().date() + timedelta(days=7),
book=self.book,
user=self.user,
)
url = reverse("borrowings:borrowings-list")
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0]["id"], borrowing.id)

def test_list_borrowings_for_staff(self):
"""Test retrieving the list of borrowings for an admin user."""
self.client.force_authenticate(user=self.staff_user)
Borrowing.objects.create(
expected_return_date=now().date() + timedelta(days=7),
book=self.book,
user=self.user,
)
url = reverse("borrowings:borrowings-list")
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 1)

def test_return_book(self):
"""Test returning a borrowed book."""
self.book.inventory = 5
self.book.save()

borrowing = Borrowing.objects.create(
expected_return_date=now().date() + timedelta(days=7),
book=self.book,
user=self.user,
)
response = self.client.post(f"/api/borrowings/{borrowing.id}/return/")
borrowing.refresh_from_db()
self.book.refresh_from_db()
self.assertEqual(response.status_code, status.HTTP_302_FOUND)
self.assertIsNotNone(borrowing.actual_return_date)
self.assertEqual(self.book.inventory, 6)

def test_return_book_already_returned(self):
"""Test trying to return a book that has already been returned."""
borrowing = Borrowing.objects.create(
expected_return_date=now().date() + timedelta(days=7),
book=self.book,
user=self.user,
actual_return_date=now(),
)
response = self.client.post(f"/api/borrowings/{borrowing.id}/return/")
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data[0], "This borrowing has already been returned.")

def test_permission_denied_for_non_authenticated_user(self):
"""Test that non-authenticated users are denied access."""
self.client.logout()
url = reverse("borrowings:borrowings-list")
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
57 changes: 57 additions & 0 deletions borrowing/tests/test_borrowing_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from django.test import TestCase
from django.contrib.auth import get_user_model
from django.utils.timezone import now
from datetime import timedelta
from borrowing.models import Borrowing, Book

User = get_user_model()

class BorrowingModelTest(TestCase):
def setUp(self):
self.user = User.objects.create_user(
email="test@user.com",
password="password123"
)
self.book = Book.objects.create(
title="Test Book",
author="Test Author",
inventory=1,
daily_fee=1
)
self.expected_return_date = now().date() + timedelta(days=7)
self.borrowing = Borrowing.objects.create(
expected_return_date=self.expected_return_date,
book=self.book,
user=self.user,
)

def test_borrowing_creation(self):
"""Test if Borrowing object is created properly."""
self.assertEqual(Borrowing.objects.count(), 1)
borrowing = Borrowing.objects.first()
self.assertEqual(borrowing.user, self.user)
self.assertEqual(borrowing.book, self.book)
self.assertEqual(borrowing.expected_return_date, self.expected_return_date)
self.assertIsNone(borrowing.actual_return_date)
self.assertEqual(borrowing.borrow_date, now().date())

def test_str_method(self):
"""Test the __str__ method of Borrowing."""
borrowing = Borrowing.objects.first()
self.assertEqual(str(borrowing), str(borrowing.borrow_date))

def test_book_relation(self):
"""Test the ForeignKey relation with Book."""
self.assertEqual(self.book.borrowings.count(), 1)
self.assertIn(self.borrowing, self.book.borrowings.all())

def test_user_relation(self):
"""Test the ForeignKey relation with User."""
self.assertEqual(self.user.borrowings.count(), 1)
self.assertIn(self.borrowing, self.user.borrowings.all())

def test_actual_return_date_blank(self):
"""Test that actual_return_date can be blank."""
self.borrowing.actual_return_date = now().date()
self.borrowing.save()
self.assertEqual(self.borrowing.actual_return_date, now().date())
Loading
Loading