school-library-api 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. school_library_api-0.1.0/PKG-INFO +17 -0
  2. school_library_api-0.1.0/README.md +0 -0
  3. school_library_api-0.1.0/backend/Dockerfile +22 -0
  4. school_library_api-0.1.0/backend/core/__init__.py +0 -0
  5. school_library_api-0.1.0/backend/core/asgi.py +16 -0
  6. school_library_api-0.1.0/backend/core/settings.py +143 -0
  7. school_library_api-0.1.0/backend/core/urls.py +23 -0
  8. school_library_api-0.1.0/backend/core/wsgi.py +16 -0
  9. school_library_api-0.1.0/backend/db.sqlite3 +0 -0
  10. school_library_api-0.1.0/backend/docker-compose.yml +33 -0
  11. school_library_api-0.1.0/backend/libraly/__init__.py +0 -0
  12. school_library_api-0.1.0/backend/libraly/admin.py +46 -0
  13. school_library_api-0.1.0/backend/libraly/apps.py +5 -0
  14. school_library_api-0.1.0/backend/libraly/migrations/0001_initial.py +43 -0
  15. school_library_api-0.1.0/backend/libraly/migrations/0002_initial.py +41 -0
  16. school_library_api-0.1.0/backend/libraly/migrations/__init__.py +0 -0
  17. school_library_api-0.1.0/backend/libraly/models.py +61 -0
  18. school_library_api-0.1.0/backend/libraly/permissions.py +31 -0
  19. school_library_api-0.1.0/backend/libraly/serializers.py +53 -0
  20. school_library_api-0.1.0/backend/libraly/tests/conftest.py +59 -0
  21. school_library_api-0.1.0/backend/libraly/tests/test_models.py +46 -0
  22. school_library_api-0.1.0/backend/libraly/tests.py +3 -0
  23. school_library_api-0.1.0/backend/libraly/urls.py +16 -0
  24. school_library_api-0.1.0/backend/libraly/views.py +178 -0
  25. school_library_api-0.1.0/backend/manage.py +22 -0
  26. school_library_api-0.1.0/backend/users/__init__.py +0 -0
  27. school_library_api-0.1.0/backend/users/admin.py +3 -0
  28. school_library_api-0.1.0/backend/users/apps.py +5 -0
  29. school_library_api-0.1.0/backend/users/authentication.py +6 -0
  30. school_library_api-0.1.0/backend/users/migrations/0001_initial.py +34 -0
  31. school_library_api-0.1.0/backend/users/migrations/__init__.py +0 -0
  32. school_library_api-0.1.0/backend/users/models.py +43 -0
  33. school_library_api-0.1.0/backend/users/serializers.py +27 -0
  34. school_library_api-0.1.0/backend/users/tests.py +3 -0
  35. school_library_api-0.1.0/backend/users/views.py +57 -0
  36. school_library_api-0.1.0/pyproject.toml +34 -0
  37. school_library_api-0.1.0/pytest.ini +3 -0
  38. school_library_api-0.1.0/requirements.txt +0 -0
  39. school_library_api-0.1.0//320/237/320/276/320/264/320/263/320/276/321/202/320/276/320/262/320/272/320/260.md +304 -0
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: school-library-api
3
+ Version: 0.1.0
4
+ Summary: REST API школьной библиотеки учебников (Django + DRF)
5
+ Project-URL: Homepage, https://github.com/yourname/school-library-api
6
+ Author-email: Your Name <you@example.com>
7
+ License: MIT
8
+ Classifier: Framework :: Django
9
+ Classifier: Framework :: Django :: 5.0
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Requires-Python: >=3.11
14
+ Requires-Dist: django-filter>=24.0
15
+ Requires-Dist: django>=5.0
16
+ Requires-Dist: djangorestframework>=3.15
17
+ Requires-Dist: psycopg[binary]>=3.1
File without changes
@@ -0,0 +1,22 @@
1
+ # syntax=docker/dockerfile:1
2
+ FROM python:3.11-slim
3
+
4
+ ENV PYTHONDONTWRITEBYTECODE=1
5
+ ENV PYTHONUNBUFFERED=1
6
+
7
+ WORKDIR /app
8
+
9
+ RUN apt-get update && apt-get install -y \
10
+ build-essential \
11
+ libpq-dev \
12
+ && rm -rf /var/lib/apt/lists/*
13
+
14
+ COPY requirements.txt /app/
15
+ RUN pip install --upgrade pip && pip install -r requirements.txt
16
+
17
+ COPY ./backend /app/backend
18
+ WORKDIR /app/backend
19
+
20
+ ENV DJANGO_SETTINGS_MODULE=core.settings
21
+
22
+ CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
File without changes
@@ -0,0 +1,16 @@
1
+ """
2
+ ASGI config for core project.
3
+
4
+ It exposes the ASGI callable as a module-level variable named ``application``.
5
+
6
+ For more information on this file, see
7
+ https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/
8
+ """
9
+
10
+ import os
11
+
12
+ from django.core.asgi import get_asgi_application
13
+
14
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
15
+
16
+ application = get_asgi_application()
@@ -0,0 +1,143 @@
1
+ """
2
+ Django settings for core project.
3
+
4
+ Generated by 'django-admin startproject' using Django 6.0.6.
5
+
6
+ For more information on this file, see
7
+ https://docs.djangoproject.com/en/6.0/topics/settings/
8
+
9
+ For the full list of settings and their values, see
10
+ https://docs.djangoproject.com/en/6.0/ref/settings/
11
+ """
12
+
13
+ from pathlib import Path
14
+ import os
15
+ import dotenv
16
+
17
+ dotenv.load_dotenv()
18
+
19
+ # Build paths inside the project like this: BASE_DIR / 'subdir'.
20
+ BASE_DIR = Path(__file__).resolve().parent.parent
21
+
22
+
23
+ # Quick-start development settings - unsuitable for production
24
+ # See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
25
+
26
+ # SECURITY WARNING: keep the secret key used in production secret!
27
+ SECRET_KEY = 'django-insecure-=ac++6&wy5gthazc(e(!^5ss3=wuwjb@hn**uj&&y(8$&4gns1'
28
+
29
+ # SECURITY WARNING: don't run with debug turned on in production!
30
+ DEBUG = True
31
+
32
+ ALLOWED_HOSTS = []
33
+
34
+
35
+ # Application definition
36
+
37
+ INSTALLED_APPS = [
38
+ 'django.contrib.admin',
39
+ 'django.contrib.auth',
40
+ 'django.contrib.contenttypes',
41
+ 'django.contrib.sessions',
42
+ 'django.contrib.messages',
43
+ 'django.contrib.staticfiles',
44
+ 'rest_framework',
45
+ 'rest_framework.authtoken',
46
+ 'school_library.users',
47
+ 'school_library.libraly'
48
+ ]
49
+
50
+ AUTH_USER_MODEL = 'users.User'
51
+
52
+ REST_FRAMEWORK = {
53
+ "DEFAULT_AUTHENTICATION_CLASSES":[
54
+ 'users.authentication.BearerToken'
55
+ ],
56
+ "DEFAULT_PERMISSION_CLASSES": [
57
+ "rest_framework.permissions.IsAuthenticatedOrReadOnly",
58
+ ],
59
+ "DEFAULT_FILTER_BACKENDS": [
60
+ "django_filters.rest_framework.DjangoFilterBackend",
61
+ ],
62
+ }
63
+
64
+ MIDDLEWARE = [
65
+ 'django.middleware.security.SecurityMiddleware',
66
+ 'django.contrib.sessions.middleware.SessionMiddleware',
67
+ 'django.middleware.common.CommonMiddleware',
68
+ 'django.middleware.csrf.CsrfViewMiddleware',
69
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
70
+ 'django.contrib.messages.middleware.MessageMiddleware',
71
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
72
+ ]
73
+
74
+ ROOT_URLCONF = 'core.urls'
75
+
76
+ TEMPLATES = [
77
+ {
78
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
79
+ 'DIRS': [],
80
+ 'APP_DIRS': True,
81
+ 'OPTIONS': {
82
+ 'context_processors': [
83
+ 'django.template.context_processors.request',
84
+ 'django.contrib.auth.context_processors.auth',
85
+ 'django.contrib.messages.context_processors.messages',
86
+ ],
87
+ },
88
+ },
89
+ ]
90
+
91
+ WSGI_APPLICATION = 'core.wsgi.application'
92
+
93
+
94
+ # Database
95
+ # https://docs.djangoproject.com/en/6.0/ref/settings/#databases
96
+
97
+ DATABASES = {
98
+ "default": {
99
+ "ENGINE": "django.db.backends.postgresql",
100
+ "NAME": os.environ.get("POSTGRES_DB", "school_library"),
101
+ "USER": os.environ.get("POSTGRES_USER", "library_user"),
102
+ "PASSWORD": os.environ.get("POSTGRES_PASSWORD", "library_password"),
103
+ "HOST": os.environ.get("POSTGRES_HOST", "db"),
104
+ "PORT": os.environ.get("POSTGRES_PORT", "5432"),
105
+ }
106
+ }
107
+
108
+
109
+ # Password validation
110
+ # https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
111
+
112
+ AUTH_PASSWORD_VALIDATORS = [
113
+ {
114
+ 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
115
+ },
116
+ {
117
+ 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
118
+ },
119
+ {
120
+ 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
121
+ },
122
+ {
123
+ 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
124
+ },
125
+ ]
126
+
127
+
128
+ # Internationalization
129
+ # https://docs.djangoproject.com/en/6.0/topics/i18n/
130
+
131
+ LANGUAGE_CODE = 'en-us'
132
+
133
+ TIME_ZONE = 'UTC'
134
+
135
+ USE_I18N = True
136
+
137
+ USE_TZ = True
138
+
139
+
140
+ # Static files (CSS, JavaScript, Images)
141
+ # https://docs.djangoproject.com/en/6.0/howto/static-files/
142
+
143
+ STATIC_URL = 'static/'
@@ -0,0 +1,23 @@
1
+ """
2
+ URL configuration for core project.
3
+
4
+ The `urlpatterns` list routes URLs to views. For more information please see:
5
+ https://docs.djangoproject.com/en/6.0/topics/http/urls/
6
+ Examples:
7
+ Function views
8
+ 1. Add an import: from my_app import views
9
+ 2. Add a URL to urlpatterns: path('', views.home, name='home')
10
+ Class-based views
11
+ 1. Add an import: from other_app.views import Home
12
+ 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
13
+ Including another URLconf
14
+ 1. Import the include() function: from django.urls import include, path
15
+ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
16
+ """
17
+ from django.contrib import admin
18
+ from django.urls import path, include
19
+
20
+ urlpatterns = [
21
+ path('admin/', admin.site.urls),
22
+ path('api/', include('libraly.urls'))
23
+ ]
@@ -0,0 +1,16 @@
1
+ """
2
+ WSGI config for core project.
3
+
4
+ It exposes the WSGI callable as a module-level variable named ``application``.
5
+
6
+ For more information on this file, see
7
+ https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/
8
+ """
9
+
10
+ import os
11
+
12
+ from django.core.wsgi import get_wsgi_application
13
+
14
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
15
+
16
+ application = get_wsgi_application()
@@ -0,0 +1,33 @@
1
+ version: "3.9"
2
+
3
+ services:
4
+ web:
5
+ build: .
6
+ container_name: library_web
7
+ command: python manage.py runserver 0.0.0.0:8000
8
+ volumes:
9
+ - ./backend:/app/backend
10
+ env_file:
11
+ - .env
12
+ depends_on:
13
+ - db
14
+ ports:
15
+ - "8000:8000"
16
+
17
+ db:
18
+ image: postgres:14
19
+ container_name: library_db
20
+ restart: always
21
+ env_file:
22
+ - .env
23
+ environment:
24
+ POSTGRES_DB: ${POSTGRES_DB}
25
+ POSTGRES_USER: ${POSTGRES_USER}
26
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
27
+ volumes:
28
+ - postgres_data:/var/lib/postgresql/data
29
+ ports:
30
+ - "5432:5432"
31
+
32
+ volumes:
33
+ postgres_data:
File without changes
@@ -0,0 +1,46 @@
1
+ from django.contrib import admin
2
+ from .models import Subject, Textbook, Borrow
3
+
4
+
5
+ class BorrowInline(admin.TabularInline):
6
+ model = Borrow
7
+ extra = 0
8
+
9
+
10
+ @admin.register(Subject)
11
+ class SubjectAdmin(admin.ModelAdmin):
12
+ list_display = ("id", "name", "created_at")
13
+ search_fields = ("name",)
14
+ list_filter = ("created_at",)
15
+
16
+
17
+ @admin.register(Textbook)
18
+ class TextbookAdmin(admin.ModelAdmin):
19
+ list_display = ("id", "title", "subject", "shelf_code", "is_available_admin")
20
+ list_filter = ("subject",)
21
+ search_fields = ("title", "shelf_code")
22
+ inlines = [BorrowInline]
23
+ list_select_related = ("subject",)
24
+
25
+ @admin.display(boolean=True, description="Доступен")
26
+ def is_available_admin(self, obj):
27
+ return not obj.borrows.filter(status=Borrow.STATUS_ACTIVE).exists()
28
+
29
+
30
+ @admin.action(description="Отметить как возвращённые")
31
+ def mark_returned(modeladmin, request, queryset):
32
+ from django.utils import timezone
33
+
34
+ queryset.filter(status=Borrow.STATUS_ACTIVE).update(
35
+ status=Borrow.STATUS_RETURNED, returned_at=timezone.now()
36
+ )
37
+
38
+
39
+ @admin.register(Borrow)
40
+ class BorrowAdmin(admin.ModelAdmin):
41
+ list_display = ("id", "student", "textbook", "status", "due_date", "borrowed_at")
42
+ list_filter = ("status", "due_date", "student")
43
+ search_fields = ("student__email", "textbook__title", "textbook__shelf_code")
44
+ actions = [mark_returned]
45
+ date_hierarchy = "borrowed_at"
46
+ list_select_related = ("student", "textbook", "textbook__subject")
@@ -0,0 +1,5 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class LibralyConfig(AppConfig):
5
+ name = 'libraly'
@@ -0,0 +1,43 @@
1
+ # Generated by Django 6.0.6 on 2026-06-15 01:06
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ initial = True
9
+
10
+ dependencies = [
11
+ ]
12
+
13
+ operations = [
14
+ migrations.CreateModel(
15
+ name='Borrow',
16
+ fields=[
17
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18
+ ('borrowed_at', models.DateTimeField(auto_now_add=True)),
19
+ ('due_date', models.DateField()),
20
+ ('returned_at', models.DateTimeField(blank=True, null=True)),
21
+ ('status', models.PositiveSmallIntegerField(choices=[(0, 'active'), (1, 'returned'), (2, 'overdue')], default=0)),
22
+ ('created_at', models.DateTimeField(auto_now_add=True)),
23
+ ],
24
+ ),
25
+ migrations.CreateModel(
26
+ name='Subject',
27
+ fields=[
28
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
29
+ ('name', models.CharField(max_length=255, unique=True)),
30
+ ('created_at', models.DateTimeField(auto_now_add=True)),
31
+ ],
32
+ ),
33
+ migrations.CreateModel(
34
+ name='Textbook',
35
+ fields=[
36
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
37
+ ('title', models.CharField(db_index=True, max_length=255)),
38
+ ('author', models.CharField(max_length=255)),
39
+ ('shelf_code', models.CharField(max_length=64)),
40
+ ('created_at', models.DateTimeField(auto_now_add=True)),
41
+ ],
42
+ ),
43
+ ]
@@ -0,0 +1,41 @@
1
+ # Generated by Django 6.0.6 on 2026-06-15 01:06
2
+
3
+ import django.db.models.deletion
4
+ from django.conf import settings
5
+ from django.db import migrations, models
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+
10
+ initial = True
11
+
12
+ dependencies = [
13
+ ('libraly', '0001_initial'),
14
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
15
+ ]
16
+
17
+ operations = [
18
+ migrations.AddField(
19
+ model_name='borrow',
20
+ name='student',
21
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='borrows', to=settings.AUTH_USER_MODEL),
22
+ ),
23
+ migrations.AddField(
24
+ model_name='textbook',
25
+ name='subject',
26
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='textbooks', to='libraly.subject'),
27
+ ),
28
+ migrations.AddField(
29
+ model_name='borrow',
30
+ name='textbook',
31
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='borrows', to='libraly.textbook'),
32
+ ),
33
+ migrations.AddIndex(
34
+ model_name='textbook',
35
+ index=models.Index(fields=['subject', 'title'], name='libraly_tex_subject_eefda8_idx'),
36
+ ),
37
+ migrations.AlterUniqueTogether(
38
+ name='textbook',
39
+ unique_together={('subject', 'shelf_code')},
40
+ ),
41
+ ]
@@ -0,0 +1,61 @@
1
+ from django.db import models
2
+ from django.conf import settings
3
+ from django.db.models import Q
4
+ from rest_framework.authentication import get_user_model
5
+
6
+ User = get_user_model()
7
+
8
+
9
+ class Subject(models.Model):
10
+ name = models.CharField(max_length=255, unique=True)
11
+ created_at = models.DateTimeField(auto_now_add=True)
12
+
13
+ def __str__(self):
14
+ return self.name
15
+
16
+
17
+ class Textbook(models.Model):
18
+ subject = models.ForeignKey(Subject, on_delete=models.CASCADE, related_name="textbooks")
19
+ title = models.CharField(max_length=255, db_index=True)
20
+ author = models.CharField(max_length=255)
21
+ shelf_code = models.CharField(max_length=64)
22
+ created_at = models.DateTimeField(auto_now_add=True)
23
+
24
+ class Meta:
25
+ unique_together = ("subject", "shelf_code")
26
+ indexes = [
27
+ models.Index(fields=["subject", "title"]),
28
+ ]
29
+
30
+ def __str__(self):
31
+ return f"{self.title} ({self.shelf_code})"
32
+
33
+
34
+ class Borrow(models.Model):
35
+ STATUS_ACTIVE = 0
36
+ STATUS_RETURNED = 1
37
+ STATUS_OVERDUE = 2
38
+ STATUS_CHOICES = (
39
+ (STATUS_ACTIVE, "active"),
40
+ (STATUS_RETURNED, "returned"),
41
+ (STATUS_OVERDUE, "overdue"),
42
+ )
43
+
44
+ student = models.ForeignKey(
45
+ User,
46
+ on_delete=models.CASCADE,
47
+ related_name="borrows",
48
+ )
49
+ textbook = models.ForeignKey(
50
+ Textbook,
51
+ on_delete=models.CASCADE,
52
+ related_name="borrows",
53
+ )
54
+ borrowed_at = models.DateTimeField(auto_now_add=True)
55
+ due_date = models.DateField()
56
+ returned_at = models.DateTimeField(null=True, blank=True)
57
+ status = models.PositiveSmallIntegerField(choices=STATUS_CHOICES, default=STATUS_ACTIVE)
58
+ created_at = models.DateTimeField(auto_now_add=True)
59
+
60
+ def __str__(self):
61
+ return f"{self.student} -> {self.textbook}"
@@ -0,0 +1,31 @@
1
+ from rest_framework.permissions import BasePermission, SAFE_METHODS
2
+ from django.contrib.auth import get_user_model
3
+
4
+ User = get_user_model()
5
+
6
+
7
+ class IsAdmin(BasePermission):
8
+ def has_permission(self, request, view):
9
+ return (
10
+ request.user
11
+ and request.user.is_authenticated
12
+ and request.user.role == User.ROLE_ADMIN
13
+ )
14
+
15
+
16
+ class IsLibrarian(BasePermission):
17
+ def has_permission(self, request, view):
18
+ return (
19
+ request.user
20
+ and request.user.is_authenticated
21
+ and request.user.role in (User.ROLE_LIBRARIAN, User.ROLE_ADMIN)
22
+ )
23
+
24
+
25
+ class IsStudent(BasePermission):
26
+ def has_permission(self, request, view):
27
+ return (
28
+ request.user
29
+ and request.user.is_authenticated
30
+ and request.user.role == User.ROLE_STUDENT
31
+ )
@@ -0,0 +1,53 @@
1
+ from rest_framework import serializers
2
+ from django.contrib.auth import get_user_model
3
+
4
+ from .models import Subject, Textbook, Borrow
5
+
6
+ User = get_user_model()
7
+
8
+
9
+ class SubjectSerializer(serializers.ModelSerializer):
10
+ class Meta:
11
+ model = Subject
12
+ fields = [
13
+ "id",
14
+ "name",
15
+ "created_at"
16
+ ]
17
+
18
+
19
+ class TextbookSerializer(serializers.ModelSerializer):
20
+ subject_name = serializers.CharField(source="subject.name", read_only=True)
21
+ is_available = serializers.SerializerMethodField()
22
+
23
+ class Meta:
24
+ model = Textbook
25
+ fields = [
26
+ "id",
27
+ "subject",
28
+ "subject_name",
29
+ "title",
30
+ "author",
31
+ "shelf_code",
32
+ "is_available",
33
+ "created_at",
34
+ ]
35
+
36
+ def get_is_available(self, obj):
37
+ return not obj.borrows.filter(status=Borrow.STATUS_ACTIVE).exists()
38
+
39
+
40
+ class BorrowSerializer(serializers.ModelSerializer):
41
+ class Meta:
42
+ model = Borrow
43
+ fields = [
44
+ "id",
45
+ "student",
46
+ "textbook",
47
+ "borrowed_at",
48
+ "due_date",
49
+ "returned_at",
50
+ "status",
51
+ "created_at",
52
+ ]
53
+ read_only_fields = ["borrowed_at", "returned_at", "status", "created_at"]
@@ -0,0 +1,59 @@
1
+ import pytest
2
+ from django.contrib.auth import get_user_model
3
+ from rest_framework.test import APIClient
4
+ from libraly.models import Subject, Textbook, Borrow
5
+
6
+ User = get_user_model()
7
+
8
+
9
+ @pytest.fixture
10
+ def api_client():
11
+ return APIClient()
12
+
13
+
14
+ @pytest.fixture
15
+ def admin_user(db):
16
+ user = User.objects.create_user(
17
+ email="admin@test.ru",
18
+ password="admin123",
19
+ role=User.ROLE_ADMIN,
20
+ is_staff=True,
21
+ is_superuser=True,
22
+ )
23
+ return user
24
+
25
+
26
+ @pytest.fixture
27
+ def librarian_user(db):
28
+ user = User.objects.create_user(
29
+ email="librarian@test.ru",
30
+ password="lib123",
31
+ role=User.ROLE_LIBRARIAN,
32
+ is_staff=True,
33
+ )
34
+ return user
35
+
36
+
37
+ @pytest.fixture
38
+ def student_user(db):
39
+ user = User.objects.create_user(
40
+ email="student@test.ru",
41
+ password="student123",
42
+ role=User.ROLE_STUDENT,
43
+ )
44
+ return user
45
+
46
+
47
+ @pytest.fixture
48
+ def subject(db):
49
+ return Subject.objects.create(name="Математика")
50
+
51
+
52
+ @pytest.fixture
53
+ def textbook(db, subject):
54
+ return Textbook.objects.create(
55
+ subject=subject,
56
+ title="Алгебра 8 класс",
57
+ author="Мордкович",
58
+ shelf_code="MAT-001",
59
+ )
@@ -0,0 +1,46 @@
1
+ import pytest
2
+ from django.db import IntegrityError
3
+ from django.contrib.auth import get_user_model
4
+ from libraly.models import Subject, Textbook, Borrow
5
+
6
+ User = get_user_model()
7
+
8
+
9
+ @pytest.mark.django_db
10
+ def test_textbook_unique_together(subject):
11
+ Textbook.objects.create(
12
+ subject=subject,
13
+ title="Алгебра 8 класс",
14
+ author="Мордкович",
15
+ shelf_code="MAT-001",
16
+ )
17
+ with pytest.raises(IntegrityError):
18
+ Textbook.objects.create(
19
+ subject=subject,
20
+ title="Алгебра 8 класс",
21
+ author="Мордкович",
22
+ shelf_code="MAT-001",
23
+ )
24
+
25
+
26
+ @pytest.mark.django_db
27
+ def test_borrow_unique_constraints_active(textbook):
28
+ student = User.objects.create_user(
29
+ email="stud1@test.ru", password="pass", role=User.ROLE_STUDENT
30
+ )
31
+
32
+ Borrow.objects.create(
33
+ student=student,
34
+ textbook=textbook,
35
+ due_date="2026-06-30",
36
+ status=Borrow.STATUS_ACTIVE,
37
+ )
38
+
39
+ # Вторая активная для того же student+textbook -> IntegrityError
40
+ with pytest.raises(IntegrityError):
41
+ Borrow.objects.create(
42
+ student=student,
43
+ textbook=textbook,
44
+ due_date="2026-06-30",
45
+ status=Borrow.STATUS_ACTIVE,
46
+ )
@@ -0,0 +1,3 @@
1
+ from django.test import TestCase
2
+
3
+ # Create your tests here.