diff --git a/book/__init__.py b/book/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/book/admin.py b/book/admin.py new file mode 100644 index 0000000..3cbb9c6 --- /dev/null +++ b/book/admin.py @@ -0,0 +1,9 @@ +from django.contrib import admin +from .models import Book + + +@admin.register(Book) +class BookAdmin(admin.ModelAdmin): + list_display = ("title", "author", "cover", "inventory", "daily_fee") + list_filter = ("cover",) + search_fields = ("title", "author") diff --git a/book/apps.py b/book/apps.py new file mode 100644 index 0000000..d317b44 --- /dev/null +++ b/book/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class BookConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "book" diff --git a/book/migrations/0001_initial.py b/book/migrations/0001_initial.py new file mode 100644 index 0000000..5ade106 --- /dev/null +++ b/book/migrations/0001_initial.py @@ -0,0 +1,39 @@ +# Generated by Django 5.0.14 on 2026-05-17 11:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Book", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=255)), + ("author", models.CharField(max_length=255)), + ( + "cover", + models.CharField( + choices=[("HARD", "Hardcover"), ("SOFT", "Softcover")], + default="SOFT", + max_length=4, + ), + ), + ("inventory", models.PositiveIntegerField()), + ("daily_fee", models.DecimalField(decimal_places=2, max_digits=6)), + ], + ), + ] diff --git a/book/migrations/__init__.py b/book/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/book/models.py b/book/models.py new file mode 100644 index 0000000..bffe796 --- /dev/null +++ b/book/models.py @@ -0,0 +1,20 @@ +from django.db import models + + +class Book(models.Model): + class CoverChoices(models.TextChoices): + HARD = "HARD", "Hardcover" + SOFT = "SOFT", "Softcover" + + title = models.CharField(max_length=255) + author = models.CharField(max_length=255) + cover = models.CharField( + max_length=4, + choices=CoverChoices.choices, + default=CoverChoices.SOFT + ) + inventory = models.PositiveIntegerField() + daily_fee = models.DecimalField(max_digits=6, decimal_places=2) + + def __str__(self): + return f"{self.title} by {self.author} ({self.cover})" diff --git a/book/serializers.py b/book/serializers.py new file mode 100644 index 0000000..5a6cf3b --- /dev/null +++ b/book/serializers.py @@ -0,0 +1,8 @@ +from rest_framework import serializers +from .models import Book + + +class BookSerializer(serializers.ModelSerializer): + class Meta: + model = Book + fields = ("id", "title", "author", "cover", "inventory", "daily_fee") diff --git a/book/tests.py b/book/tests.py new file mode 100644 index 0000000..de8bdc0 --- /dev/null +++ b/book/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/book/urls.py b/book/urls.py new file mode 100644 index 0000000..e060c2d --- /dev/null +++ b/book/urls.py @@ -0,0 +1,12 @@ +from django.urls import path, include +from rest_framework.routers import DefaultRouter +from .views import BookViewSet + +router = DefaultRouter() +router.register("", BookViewSet) + +urlpatterns = [ + path("", include(router.urls)), +] + +app_name = "book" diff --git a/book/views.py b/book/views.py new file mode 100644 index 0000000..6fc873b --- /dev/null +++ b/book/views.py @@ -0,0 +1,14 @@ +from rest_framework import viewsets +from rest_framework.permissions import IsAdminUser, AllowAny +from .models import Book +from .serializers import BookSerializer + + +class BookViewSet(viewsets.ModelViewSet): + queryset = Book.objects.all() + serializer_class = BookSerializer + + def get_permissions(self): + if self.action in ["list", "retrieve"]: + return [AllowAny()] + return [IsAdminUser()] diff --git a/library_config/settings.py b/library_config/settings.py index c876e69..2295261 100644 --- a/library_config/settings.py +++ b/library_config/settings.py @@ -41,6 +41,9 @@ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + "rest_framework", + "user", + "book", ] MIDDLEWARE = [ @@ -125,3 +128,11 @@ # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +AUTH_USER_MODEL = "user.User" + +REST_FRAMEWORK = { + "DEFAULT_AUTHENTICATION_CLASSES": ( + "rest_framework_simplejwt.authentication.JWTAuthentication", + ), +} diff --git a/library_config/urls.py b/library_config/urls.py index eae93de..c331564 100644 --- a/library_config/urls.py +++ b/library_config/urls.py @@ -1,23 +1,8 @@ -""" -URL configuration for library_config project. - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/5.0/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) -""" - from django.contrib import admin -from django.urls import path +from django.urls import path, include urlpatterns = [ path("admin/", admin.site.urls), + path("api/books/", include("book.urls", namespace="book")), + path("api/user/", include("user.urls", namespace="user")), # Додали цей рядок ] diff --git a/requirements.txt b/requirements.txt index 9b8c276..c1315bf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,18 @@ -Django>=5.0,<5.1 -djangorestframework>=3.15.0 -djangorestframework-simplejwt>=5.3.0 -django-cors-headers>=4.3.0 -drf-spectacular>=0.27.0 -python-dotenv>=1.0.0 \ No newline at end of file +asgiref==3.11.1 +attrs==26.1.0 +Django==5.0.14 +django-cors-headers==4.9.0 +djangorestframework==3.17.1 +djangorestframework_simplejwt==5.5.1 +drf-spectacular==0.29.0 +inflection==0.5.1 +jsonschema==4.26.0 +jsonschema-specifications==2025.9.1 +PyJWT==2.12.1 +python-dotenv==1.2.2 +PyYAML==6.0.3 +referencing==0.37.0 +rpds-py==0.30.0 +sqlparse==0.5.5 +tzdata==2026.2 +uritemplate==4.2.0 diff --git a/user/__init__.py b/user/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/user/admin.py b/user/admin.py new file mode 100644 index 0000000..ea5d68b --- /dev/null +++ b/user/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/user/apps.py b/user/apps.py new file mode 100644 index 0000000..802a97f --- /dev/null +++ b/user/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class UserConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "user" diff --git a/user/migrations/0001_initial.py b/user/migrations/0001_initial.py new file mode 100644 index 0000000..af3134b --- /dev/null +++ b/user/migrations/0001_initial.py @@ -0,0 +1,112 @@ +# Generated by Django 5.0.14 on 2026-05-17 10:40 + +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("auth", "0012_alter_user_first_name_max_length"), + ] + + operations = [ + migrations.CreateModel( + name="User", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "first_name", + models.CharField( + blank=True, max_length=150, verbose_name="first name" + ), + ), + ( + "last_name", + models.CharField( + blank=True, max_length=150, verbose_name="last name" + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), + ), + ( + "email", + models.EmailField( + max_length=254, unique=True, verbose_name="email address" + ), + ), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.permission", + verbose_name="user permissions", + ), + ), + ], + options={ + "verbose_name": "user", + "verbose_name_plural": "users", + "abstract": False, + }, + ), + ] diff --git a/user/migrations/__init__.py b/user/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/user/models.py b/user/models.py new file mode 100644 index 0000000..de98423 --- /dev/null +++ b/user/models.py @@ -0,0 +1,42 @@ +from django.contrib.auth.models import ( + AbstractUser, + BaseUserManager, +) +from django.db import models +from django.utils.translation import gettext_lazy as _ + + +class UserManager(BaseUserManager): + + def create_user(self, email, password=None, **extra_fields): + if not email: + raise ValueError(_("The Email field must be set")) + email = self.normalize_email(email) + user = self.model(email=email, **extra_fields) + user.set_password(password) + user.save(using=self._db) + return user + + def create_superuser(self, email, password=None, **extra_fields): + extra_fields.setdefault("is_staff", True) + extra_fields.setdefault("is_superuser", True) + + if extra_fields.get("is_staff") is not True: + raise ValueError(_("Superuser must have is_staff=True.")) + if extra_fields.get("is_superuser") is not True: + raise ValueError(_("Superuser must have is_superuser=True.")) + + return self.create_user(email, password, **extra_fields) + + +class User(AbstractUser): + username = None + email = models.EmailField(_("email address"), unique=True) + + objects = UserManager() + + USERNAME_FIELD = "email" + REQUIRED_FIELDS = [] + + def __str__(self): + return self.email diff --git a/user/serializers.py b/user/serializers.py new file mode 100644 index 0000000..e5e09cf --- /dev/null +++ b/user/serializers.py @@ -0,0 +1,23 @@ +from django.contrib.auth import get_user_model +from rest_framework import serializers + + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = get_user_model() + fields = ("id", "email", "password", "is_staff") + extra_kwargs = { + "password": {"write_only": True, "min_length": 5}, + "is_staff": {"read_only": True}, + } + + def create(self, validated_data): + return get_user_model().objects.create_user(**validated_data) + + def update(self, instance, validated_data): + password = validated_data.pop("password", None) + user = super().update(instance, validated_data) + if password: + user.set_password(password) + user.save() + return user diff --git a/user/tests.py b/user/tests.py new file mode 100644 index 0000000..de8bdc0 --- /dev/null +++ b/user/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/user/urls.py b/user/urls.py new file mode 100644 index 0000000..0d4dd52 --- /dev/null +++ b/user/urls.py @@ -0,0 +1,15 @@ +from django.urls import path +from rest_framework_simplejwt.views import ( + TokenObtainPairView, + TokenRefreshView, +) +from .views import CreateUserView, ManageUserView + +urlpatterns = [ + path("register/", CreateUserView.as_view(), name="create"), + path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"), + path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), + path("me/", ManageUserView.as_view(), name="manage"), +] + +app_name = "user" diff --git a/user/views.py b/user/views.py new file mode 100644 index 0000000..67a11cd --- /dev/null +++ b/user/views.py @@ -0,0 +1,17 @@ +from rest_framework import generics +from rest_framework.permissions import IsAuthenticated +from rest_framework_simplejwt.authentication import JWTAuthentication +from .serializers import UserSerializer + + +class CreateUserView(generics.CreateAPIView): + serializer_class = UserSerializer + + +class ManageUserView(generics.RetrieveUpdateAPIView): + serializer_class = UserSerializer + authentication_classes = (JWTAuthentication,) + permission_classes = (IsAuthenticated,) + + def get_object(self): + return self.request.user