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.
|
||||
|
||||
|
|
@ -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"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
]
|
||||
|
|
|
|||
Loading…
Reference in a new issue