Compare commits

..

No commits in common. "a061813715158773a990ed2ce8ab4d382b816edd" and "8951e3bdb4e086b33192f28d98d74f46a2e5f651" have entirely different histories.

29 changed files with 3 additions and 352 deletions

7
.env
View file

@ -1,7 +0,0 @@
DB_NAME=botanical_garden
DB_USER=garden_admin
DB_PASSWORD=supersecret
DB_HOST=localhost
DB_PORT=5432
DEBUG=True
SECRET_KEY=django-insecure-change-me

View file

@ -1,40 +0,0 @@
REST API для учёта растений и теплиц ботанического сада. Реализовано на Django + Django REST Framework с использованием PostgreSQL в качестве базы данных.
Система хранит данные о:
- **Теплицах (Greenhouse)** — название, локация, климатическая зона.
- **Растениях (Plant)** — вид, семейство, теплица, высота, частота полива, статус цветения, дата посадки, страна происхождения, статус исчезающего вида.
## API docs (больше на /api/schema/swagger-ui)
| Метод | URL | Описание |
|------------------------|------------------------------------------------|-------------------------------------------------|
| GET | `/api_v1/plants/{id}/` | Получить одно растение |
| GET | `/api_v1/plants/?greenhouse=1&is_flowering=true` | Получить группу растений с фильтрами |
| POST | `/api_v1/plants/` | Создать одно растение |
| POST | `/api_v1/plants/bulk-create/` | Создать группу растений |
| PATCH | `/api_v1/plants/{id}/` | Обновить одно растение |
| PATCH | `/api_v1/plants/bulk-update/` | Обновить группу растений по списку id |
| DELETE | `/api_v1/plants/{id}/` | Удалить одно растение |
| DELETE | `/api_v1/plants/bulk-delete/?family=...` | Удалить группу растений по фильтру |
| GET/POST/PATCH/DELETE | `/api_v1/greenhouses/{id}/` | CRUD по теплицам |
### 1. clone
```bash
git clone <your-repo-url>
cd botanical-garden-api
```
### 2. .venv
```bash
python -m venv .venv
source .venv/bin/activate # Linux/macOS
.venv\Scripts\activate # Windows
```
### 3. dependencies
```bash
pip install -r requirements.txt
```

View file

@ -1,71 +0,0 @@
# 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")
}),
)

View file

@ -1,5 +0,0 @@
from django.apps import AppConfig
class ApiV1Config(AppConfig):
name = 'api_v1'

View file

@ -1,40 +0,0 @@
# 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')),
],
),
]

View file

@ -1,28 +0,0 @@
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

View file

@ -1,15 +0,0 @@
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__"

View file

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View file

@ -1,8 +0,0 @@
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

View file

@ -1,84 +0,0 @@
# 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

View file

View file

@ -1,9 +1,3 @@
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.
@ -37,28 +31,14 @@ 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',
@ -94,12 +74,8 @@ WSGI_APPLICATION = 'library_api.wsgi.application'
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.postgresql', 'ENGINE': 'django.db.backends.sqlite3',
"NAME": os.getenv("DB_NAME"), 'NAME': BASE_DIR / 'db.sqlite3',
"USER": os.getenv("DB_USER"),
"PASSWORD": os.getenv("DB_PASSWORD"),
"HOST": os.getenv("DB_HOST", "localhost"),
"PORT": os.getenv("DB_PORT", "5432"),
} }
} }

View file

@ -15,12 +15,8 @@ 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, include from django.urls import path
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'),
] ]

View file

@ -1,20 +0,0 @@
asgiref==3.12.1
attrs==26.1.0
certifi==2026.7.22
charset-normalizer==3.5.1
Django==6.1.1
djangorestframework==3.18.0
drf-spectacular==0.30.0
idna==3.19
inflection==0.5.1
jsonschema==4.26.0
jsonschema-specifications==2025.9.1
psycopg2-binary==2.9.12
python-dotenv==1.2.3
PyYAML==6.0.3
referencing==0.37.0
requests==2.34.2
rpds-py==2026.6.3
sqlparse==0.6.0
uritemplate==4.2.0
urllib3==2.7.0