Compare commits
No commits in common. "a061813715158773a990ed2ce8ab4d382b816edd" and "8951e3bdb4e086b33192f28d98d74f46a2e5f651" have entirely different histories.
a061813715
...
8951e3bdb4
29 changed files with 3 additions and 352 deletions
7
.env
7
.env
|
|
@ -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
|
|
||||||
40
README.md
40
README.md
|
|
@ -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
|
|
||||||
```
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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")
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class ApiV1Config(AppConfig):
|
|
||||||
name = 'api_v1'
|
|
||||||
|
|
@ -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')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -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
|
|
||||||
|
|
@ -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__"
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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'),
|
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
Loading…
Reference in a new issue