diff --git a/library_api/api_v1/__init__.py b/library_api/api_v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/library_api/api_v1/__pycache__/__init__.cpython-314.pyc b/library_api/api_v1/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..61e3e94 Binary files /dev/null and b/library_api/api_v1/__pycache__/__init__.cpython-314.pyc differ diff --git a/library_api/api_v1/__pycache__/admin.cpython-314.pyc b/library_api/api_v1/__pycache__/admin.cpython-314.pyc new file mode 100644 index 0000000..a4991f4 Binary files /dev/null and b/library_api/api_v1/__pycache__/admin.cpython-314.pyc differ diff --git a/library_api/api_v1/__pycache__/apps.cpython-314.pyc b/library_api/api_v1/__pycache__/apps.cpython-314.pyc new file mode 100644 index 0000000..5a5cbbe Binary files /dev/null and b/library_api/api_v1/__pycache__/apps.cpython-314.pyc differ diff --git a/library_api/api_v1/__pycache__/models.cpython-314.pyc b/library_api/api_v1/__pycache__/models.cpython-314.pyc new file mode 100644 index 0000000..7d75acc Binary files /dev/null and b/library_api/api_v1/__pycache__/models.cpython-314.pyc differ diff --git a/library_api/api_v1/__pycache__/serializers.cpython-314.pyc b/library_api/api_v1/__pycache__/serializers.cpython-314.pyc new file mode 100644 index 0000000..bb77064 Binary files /dev/null and b/library_api/api_v1/__pycache__/serializers.cpython-314.pyc differ diff --git a/library_api/api_v1/__pycache__/urls.cpython-314.pyc b/library_api/api_v1/__pycache__/urls.cpython-314.pyc new file mode 100644 index 0000000..292acde Binary files /dev/null and b/library_api/api_v1/__pycache__/urls.cpython-314.pyc differ diff --git a/library_api/api_v1/__pycache__/views.cpython-314.pyc b/library_api/api_v1/__pycache__/views.cpython-314.pyc new file mode 100644 index 0000000..86acd5e Binary files /dev/null and b/library_api/api_v1/__pycache__/views.cpython-314.pyc differ diff --git a/library_api/api_v1/admin.py b/library_api/api_v1/admin.py new file mode 100644 index 0000000..b58444d --- /dev/null +++ b/library_api/api_v1/admin.py @@ -0,0 +1,71 @@ +# garden/admin.py +from django.contrib import admin +from .models import Plant, Greenhouse + + +class PlantInline(admin.TabularInline): + """Позволяет редактировать растения прямо на странице теплицы.""" + model = Plant + extra = 0 + fields = ( + "name", + "species", + "family", + "height_cm", + "is_flowering", + "endangered", + ) + show_change_link = True + + +@admin.register(Greenhouse) +class GreenhouseAdmin(admin.ModelAdmin): + list_display = ("id", "name", "location", "climate_zone", "plant_count") + list_filter = ("climate_zone",) + search_fields = ("name", "location") + ordering = ("name",) + inlines = [PlantInline] + + @admin.display(description="Кол-во растений") + def plant_count(self, obj): + return obj.plants.count() + + +@admin.register(Plant) +class PlantAdmin(admin.ModelAdmin): + list_display = ( + "id", + "name", + "species", + "family", + "greenhouse", + "height_cm", + "is_flowering", + "endangered", + "planted_at", + ) + list_filter = ( + "greenhouse", + "family", + "is_flowering", + "endangered", + "origin_country", + ) + search_fields = ("name", "species", "family", "origin_country") + date_hierarchy = "planted_at" + ordering = ("-planted_at",) + list_select_related = ("greenhouse",) + autocomplete_fields = ("greenhouse",) + list_per_page = 25 + + fieldsets = ( + ("Основное", { + "fields": ("name", "species", "family", "greenhouse") + }), + ("Характеристики", { + "fields": ("height_cm", "watering_frequency_days", "is_flowering", "endangered") + }), + ("Происхождение", { + "fields": ("planted_at", "origin_country") + }), + ) diff --git a/library_api/api_v1/apps.py b/library_api/api_v1/apps.py new file mode 100644 index 0000000..8e78bcf --- /dev/null +++ b/library_api/api_v1/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ApiV1Config(AppConfig): + name = 'api_v1' diff --git a/library_api/api_v1/migrations/0001_initial.py b/library_api/api_v1/migrations/0001_initial.py new file mode 100644 index 0000000..f1045f9 --- /dev/null +++ b/library_api/api_v1/migrations/0001_initial.py @@ -0,0 +1,40 @@ +# Generated by Django 6.1.1 on 2026-09-07 00:26 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Greenhouse', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('location', models.CharField(max_length=200)), + ('climate_zone', models.CharField(choices=[('tropical', 'Тропики'), ('temperate', 'Умеренная'), ('arid', 'Аридная')], max_length=20)), + ], + ), + migrations.CreateModel( + name='Plant', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=150)), + ('species', models.CharField(max_length=150)), + ('family', models.CharField(blank=True, max_length=100)), + ('height_cm', models.FloatField()), + ('watering_frequency_days', models.PositiveIntegerField()), + ('is_flowering', models.BooleanField(default=False)), + ('planted_at', models.DateField()), + ('origin_country', models.CharField(blank=True, max_length=100)), + ('endangered', models.BooleanField(default=False)), + ('greenhouse', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='plants', to='api_v1.greenhouse')), + ], + ), + ] diff --git a/library_api/api_v1/migrations/__init__.py b/library_api/api_v1/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/library_api/api_v1/migrations/__pycache__/0001_initial.cpython-314.pyc b/library_api/api_v1/migrations/__pycache__/0001_initial.cpython-314.pyc new file mode 100644 index 0000000..0604113 Binary files /dev/null and b/library_api/api_v1/migrations/__pycache__/0001_initial.cpython-314.pyc differ diff --git a/library_api/api_v1/migrations/__pycache__/__init__.cpython-314.pyc b/library_api/api_v1/migrations/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..78d0807 Binary files /dev/null and b/library_api/api_v1/migrations/__pycache__/__init__.cpython-314.pyc differ diff --git a/library_api/api_v1/models.py b/library_api/api_v1/models.py new file mode 100644 index 0000000..def70d8 --- /dev/null +++ b/library_api/api_v1/models.py @@ -0,0 +1,28 @@ +from django.db import models + +class Greenhouse(models.Model): + name = models.CharField(max_length=100) + location = models.CharField(max_length=200) + climate_zone = models.CharField( + max_length=20, + choices=[("tropical", "Тропики"), ("temperate", "Умеренная"), ("arid", "Аридная")] + ) + + def __str__(self): + return self.name + + +class Plant(models.Model): + name = models.CharField(max_length=150) + species = models.CharField(max_length=150) + family = models.CharField(max_length=100, blank=True) + greenhouse = models.ForeignKey(Greenhouse, on_delete=models.CASCADE, related_name="plants") + height_cm = models.FloatField() + watering_frequency_days = models.PositiveIntegerField() + is_flowering = models.BooleanField(default=False) + planted_at = models.DateField() + origin_country = models.CharField(max_length=100, blank=True) + endangered = models.BooleanField(default=False) + + def __str__(self): + return self.name diff --git a/library_api/api_v1/serializers.py b/library_api/api_v1/serializers.py new file mode 100644 index 0000000..fb64468 --- /dev/null +++ b/library_api/api_v1/serializers.py @@ -0,0 +1,15 @@ +from rest_framework import serializers +from .models import Plant, Greenhouse + +class PlantSerializer(serializers.ModelSerializer): + class Meta: + model = Plant + fields = "__all__" + + +class GreenhouseSerializer(serializers.ModelSerializer): + plants = PlantSerializer(many=True, read_only=True) + + class Meta: + model = Greenhouse + fields = "__all__" diff --git a/library_api/api_v1/tests.py b/library_api/api_v1/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/library_api/api_v1/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/library_api/api_v1/urls.py b/library_api/api_v1/urls.py new file mode 100644 index 0000000..c111155 --- /dev/null +++ b/library_api/api_v1/urls.py @@ -0,0 +1,8 @@ +from rest_framework.routers import DefaultRouter +from .views import PlantViewSet, GreenhouseViewSet + +router = DefaultRouter() +router.register(r"plants", PlantViewSet) +router.register(r"greenhouses", GreenhouseViewSet) + +urlpatterns = router.urls diff --git a/library_api/api_v1/views.py b/library_api/api_v1/views.py new file mode 100644 index 0000000..c567931 --- /dev/null +++ b/library_api/api_v1/views.py @@ -0,0 +1,84 @@ +# views.py +from rest_framework import viewsets, status +from rest_framework.decorators import action +from rest_framework.response import Response +from .models import Plant, Greenhouse +from .serializers import PlantSerializer, GreenhouseSerializer + + +class PlantViewSet(viewsets.ModelViewSet): + queryset = Plant.objects.all() + serializer_class = PlantSerializer + + def get_queryset(self): + qs = Plant.objects.all() + params = self.request.query_params + + greenhouse = params.get("greenhouse") + if greenhouse is not None: + qs = qs.filter(greenhouse_id=greenhouse) + + family = params.get("family") + if family is not None: + qs = qs.filter(family__iexact=family) + + origin_country = params.get("origin_country") + if origin_country is not None: + qs = qs.filter(origin_country__iexact=origin_country) + + is_flowering = params.get("is_flowering") + if is_flowering is not None: + qs = qs.filter(is_flowering=is_flowering.lower() in ("true", "1", "yes")) + + endangered = params.get("endangered") + if endangered is not None: + qs = qs.filter(endangered=endangered.lower() in ("true", "1", "yes")) + + min_height = params.get("min_height") + if min_height is not None: + qs = qs.filter(height_cm__gte=float(min_height)) + + max_height = params.get("max_height") + if max_height is not None: + qs = qs.filter(height_cm__lte=float(max_height)) + + planted_after = params.get("planted_after") + if planted_after is not None: + qs = qs.filter(planted_at__gte=planted_after) + + planted_before = params.get("planted_before") + if planted_before is not None: + qs = qs.filter(planted_at__lte=planted_before) + + return qs + + @action(detail=False, methods=["post"], url_path="bulk-create") + def bulk_create(self, request): + serializer = self.get_serializer(data=request.data, many=True) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + + @action(detail=False, methods=["patch"], url_path="bulk-update") + def bulk_update_plants(self, request): + ids = request.data.get("ids", []) + data = request.data.get("data", {}) + if not ids or not data: + return Response( + {"detail": "Нужно передать 'ids' и 'data'."}, + status=status.HTTP_400_BAD_REQUEST, + ) + updated = Plant.objects.filter(id__in=ids).update(**data) + return Response({"updated_count": updated}) + + @action(detail=False, methods=["delete"], url_path="bulk-delete") + def bulk_delete(self, request): + qs = self.get_queryset() # фильтры берутся из query_params + count = qs.count() + qs.delete() + return Response({"deleted_count": count}) + + +class GreenhouseViewSet(viewsets.ModelViewSet): + queryset = Greenhouse.objects.all() + serializer_class = GreenhouseSerializer diff --git a/library_api/db.sqlite3 b/library_api/db.sqlite3 new file mode 100644 index 0000000..e69de29 diff --git a/library_api/library_api/__pycache__/__init__.cpython-314.pyc b/library_api/library_api/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..18ce2dd Binary files /dev/null and b/library_api/library_api/__pycache__/__init__.cpython-314.pyc differ diff --git a/library_api/library_api/__pycache__/settings.cpython-314.pyc b/library_api/library_api/__pycache__/settings.cpython-314.pyc new file mode 100644 index 0000000..e297da2 Binary files /dev/null and b/library_api/library_api/__pycache__/settings.cpython-314.pyc differ diff --git a/library_api/library_api/__pycache__/urls.cpython-314.pyc b/library_api/library_api/__pycache__/urls.cpython-314.pyc new file mode 100644 index 0000000..4c244b7 Binary files /dev/null and b/library_api/library_api/__pycache__/urls.cpython-314.pyc differ diff --git a/library_api/library_api/__pycache__/wsgi.cpython-314.pyc b/library_api/library_api/__pycache__/wsgi.cpython-314.pyc new file mode 100644 index 0000000..7875c15 Binary files /dev/null and b/library_api/library_api/__pycache__/wsgi.cpython-314.pyc differ diff --git a/library_api/library_api/settings.py b/library_api/library_api/settings.py index bd081ba..7c36c7a 100644 --- a/library_api/library_api/settings.py +++ b/library_api/library_api/settings.py @@ -1,3 +1,9 @@ + +import os +from pathlib import Path +from dotenv import load_dotenv +load_dotenv() + """ Django settings for library_api project. @@ -31,14 +37,28 @@ ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ + 'api_v1', + "rest_framework", 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'drf_spectacular' ] +REST_FRAMEWORK = { + 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', +} + +SPECTACULAR_SETTINGS = { + 'TITLE': 'Library API', + 'DESCRIPTION': 'API для управления книгами, авторами и комментариями.', + 'VERSION': '1.0.0', + 'SERVE_INCLUDE_SCHEMA': False, +} + MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', @@ -74,8 +94,12 @@ WSGI_APPLICATION = 'library_api.wsgi.application' DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', + 'ENGINE': 'django.db.backends.postgresql', + "NAME": os.getenv("DB_NAME"), + "USER": os.getenv("DB_USER"), + "PASSWORD": os.getenv("DB_PASSWORD"), + "HOST": os.getenv("DB_HOST", "localhost"), + "PORT": os.getenv("DB_PORT", "5432"), } } diff --git a/library_api/library_api/urls.py b/library_api/library_api/urls.py index 80a3253..9798a8c 100644 --- a/library_api/library_api/urls.py +++ b/library_api/library_api/urls.py @@ -15,8 +15,12 @@ Including another URLconf 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 +from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView urlpatterns = [ path('admin/', admin.site.urls), + path('api_v1/', include('api_v1.urls')), + path('api/schema/', SpectacularAPIView.as_view(), name='schema'), + path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), ]