the thing
This commit is contained in:
parent
8951e3bdb4
commit
c2c0717f0d
26 changed files with 285 additions and 3 deletions
0
library_api/api_v1/__init__.py
Normal file
0
library_api/api_v1/__init__.py
Normal file
BIN
library_api/api_v1/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
library_api/api_v1/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
library_api/api_v1/__pycache__/admin.cpython-314.pyc
Normal file
BIN
library_api/api_v1/__pycache__/admin.cpython-314.pyc
Normal file
Binary file not shown.
BIN
library_api/api_v1/__pycache__/apps.cpython-314.pyc
Normal file
BIN
library_api/api_v1/__pycache__/apps.cpython-314.pyc
Normal file
Binary file not shown.
BIN
library_api/api_v1/__pycache__/models.cpython-314.pyc
Normal file
BIN
library_api/api_v1/__pycache__/models.cpython-314.pyc
Normal file
Binary file not shown.
BIN
library_api/api_v1/__pycache__/serializers.cpython-314.pyc
Normal file
BIN
library_api/api_v1/__pycache__/serializers.cpython-314.pyc
Normal file
Binary file not shown.
BIN
library_api/api_v1/__pycache__/urls.cpython-314.pyc
Normal file
BIN
library_api/api_v1/__pycache__/urls.cpython-314.pyc
Normal file
Binary file not shown.
BIN
library_api/api_v1/__pycache__/views.cpython-314.pyc
Normal file
BIN
library_api/api_v1/__pycache__/views.cpython-314.pyc
Normal file
Binary file not shown.
71
library_api/api_v1/admin.py
Normal file
71
library_api/api_v1/admin.py
Normal file
|
|
@ -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")
|
||||||
|
}),
|
||||||
|
)
|
||||||
5
library_api/api_v1/apps.py
Normal file
5
library_api/api_v1/apps.py
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ApiV1Config(AppConfig):
|
||||||
|
name = 'api_v1'
|
||||||
40
library_api/api_v1/migrations/0001_initial.py
Normal file
40
library_api/api_v1/migrations/0001_initial.py
Normal file
|
|
@ -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')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
0
library_api/api_v1/migrations/__init__.py
Normal file
0
library_api/api_v1/migrations/__init__.py
Normal file
Binary file not shown.
Binary file not shown.
28
library_api/api_v1/models.py
Normal file
28
library_api/api_v1/models.py
Normal file
|
|
@ -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
|
||||||
15
library_api/api_v1/serializers.py
Normal file
15
library_api/api_v1/serializers.py
Normal file
|
|
@ -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__"
|
||||||
3
library_api/api_v1/tests.py
Normal file
3
library_api/api_v1/tests.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
8
library_api/api_v1/urls.py
Normal file
8
library_api/api_v1/urls.py
Normal file
|
|
@ -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
|
||||||
84
library_api/api_v1/views.py
Normal file
84
library_api/api_v1/views.py
Normal file
|
|
@ -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
|
||||||
0
library_api/db.sqlite3
Normal file
0
library_api/db.sqlite3
Normal file
BIN
library_api/library_api/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
library_api/library_api/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
library_api/library_api/__pycache__/settings.cpython-314.pyc
Normal file
BIN
library_api/library_api/__pycache__/settings.cpython-314.pyc
Normal file
Binary file not shown.
BIN
library_api/library_api/__pycache__/urls.cpython-314.pyc
Normal file
BIN
library_api/library_api/__pycache__/urls.cpython-314.pyc
Normal file
Binary file not shown.
BIN
library_api/library_api/__pycache__/wsgi.cpython-314.pyc
Normal file
BIN
library_api/library_api/__pycache__/wsgi.cpython-314.pyc
Normal file
Binary file not shown.
|
|
@ -1,3 +1,9 @@
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Django settings for library_api project.
|
Django settings for library_api project.
|
||||||
|
|
||||||
|
|
@ -31,14 +37,28 @@ ALLOWED_HOSTS = []
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
|
'api_v1',
|
||||||
|
"rest_framework",
|
||||||
'django.contrib.admin',
|
'django.contrib.admin',
|
||||||
'django.contrib.auth',
|
'django.contrib.auth',
|
||||||
'django.contrib.contenttypes',
|
'django.contrib.contenttypes',
|
||||||
'django.contrib.sessions',
|
'django.contrib.sessions',
|
||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'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 = [
|
MIDDLEWARE = [
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
|
@ -74,8 +94,12 @@ WSGI_APPLICATION = 'library_api.wsgi.application'
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
'ENGINE': 'django.db.backends.postgresql',
|
||||||
'NAME': BASE_DIR / 'db.sqlite3',
|
"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"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,12 @@ Including another URLconf
|
||||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
"""
|
"""
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path
|
from django.urls import path, include
|
||||||
|
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
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'),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue