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.
|
||||
|
||||
|
|
@ -37,28 +31,14 @@ 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',
|
||||
|
|
@ -94,12 +74,8 @@ WSGI_APPLICATION = 'library_api.wsgi.application'
|
|||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'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"),
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'db.sqlite3',
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,12 +15,8 @@ Including another URLconf
|
|||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
|
||||
from django.urls import path
|
||||
|
||||
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'),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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