Add Django backend, Next.js frontend, and deploy templates

This commit is contained in:
ogrechko committed 2026-08-27 15:38:31 +00:00
1 parent fc0c991335
commit e9cd22d36f
93 files changed
+7840 -1

No files matched your search

+1
View File
@@ -0,0 +1 @@
+42
View File
@@ -0,0 +1,42 @@
from django.contrib import admin
from .models import ContactMessage, Doctor, Document, Page, Service, SiteSettings
@admin.register(SiteSettings)
class SiteSettingsAdmin(admin.ModelAdmin):
list_display = ("site_name", "phone", "city", "updated_at")
@admin.register(Page)
class PageAdmin(admin.ModelAdmin):
list_display = ("title", "slug", "is_published", "nav_order", "updated_at")
list_editable = ("is_published", "nav_order")
prepopulated_fields = {"slug": ("title",)}
@admin.register(Service)
class ServiceAdmin(admin.ModelAdmin):
list_display = ("title", "slug", "sort_order", "updated_at")
list_editable = ("sort_order",)
prepopulated_fields = {"slug": ("title",)}
@admin.register(Doctor)
class DoctorAdmin(admin.ModelAdmin):
list_display = ("full_name", "position", "sort_order", "updated_at")
list_editable = ("sort_order",)
@admin.register(Document)
class DocumentAdmin(admin.ModelAdmin):
list_display = ("title", "category", "sort_order", "updated_at")
list_editable = ("sort_order",)
@admin.register(ContactMessage)
class ContactMessageAdmin(admin.ModelAdmin):
list_display = ("name", "phone", "email", "is_processed", "created_at")
list_editable = ("is_processed",)
readonly_fields = ("name", "phone", "email", "message", "created_at", "updated_at")
+7
View File
@@ -0,0 +1,7 @@
from django.apps import AppConfig
class ContentConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "content"
+237
View File
@@ -0,0 +1,237 @@
[
{
"model": "content.sitesettings",
"pk": 1,
"fields": {
"site_name": "Консультационное клиническое патологоанатомическое бюро",
"tagline": "Точная морфологическая диагностика и профессиональное сопровождение",
"phone": "+7 (8652) 22-66-77",
"email": "kkpab@bk.ru",
"address": "улица Балакирева, 5к7",
"city": "Ставрополь",
"working_hours": "Пн-Пт 08:00-17:00, Сб 09:00-14:00",
"hero_title": "Современная патоморфология с вниманием к клинической точности",
"hero_subtitle": "Переосмысленный цифровой сайт для бюро, где ключевая информация, специалисты, документы и направления на исследования доступны быстро и понятно.",
"hero_image": "https://images.unsplash.com/photo-1579154204601-01588f351e67?auto=format&fit=crop&w=1600&q=80",
"about_excerpt": "Бюро объединяет специалистов по патологической анатомии и гистологии, обеспечивая качественную диагностику и профессиональную коммуникацию с медицинскими организациями."
}
},
{
"model": "content.page",
"pk": 1,
"fields": {
"title": "О бюро",
"slug": "about",
"intro": "Специалисты разного уровня и современное оборудование для морфологической диагностики.",
"body": "В патологоанатомическом бюро работают врачи с большим стажем, квалификационными категориями и постоянным профессиональным развитием. Для гистологической обработки используется современное высокотехнологичное оборудование, а диагностический процесс поддерживается подготовленным средним и младшим медицинским персоналом.",
"meta_title": "О бюро",
"meta_description": "Информация о консультационном клиническом патологоанатомическом бюро.",
"is_published": true,
"nav_order": 1
}
},
{
"model": "content.page",
"pk": 2,
"fields": {
"title": "Сведения о медицинской организации",
"slug": "svedeniya-mo",
"intro": "Юридическая информация, лицензии, график и данные страховых организаций.",
"body": "Полное наименование: ООО \"Консультационное Клиническое Патологоанатомическое Бюро\". ОГРН 1182651002093. Режим работы: пн-пт с 8:00 до 17:00, суббота с 9:00 до 14:00. Генеральный директор: Балабеков Андрей Вагифович. Телефон: +7 (8652) 22-66-77. Электронная почта: kkpab@bk.ru.",
"meta_title": "Сведения МО",
"meta_description": "Официальные сведения о медицинской организации.",
"is_published": true,
"nav_order": 2
}
},
{
"model": "content.service",
"pk": 1,
"fields": {
"title": "Гистологические исследования",
"slug": "histology",
"short_description": "Базовый и наиболее распространенный метод исследования тканей.",
"description": "Гистологическое исследование используется для диагностики опухолевых и неопухолевых заболеваний. Материал проходит фиксацию, гистопроцессинг, заливку в парафин, микротомию, окрашивание и оценку врачом-патоморфологом.",
"icon": "microscope",
"sort_order": 1
}
},
{
"model": "content.service",
"pk": 2,
"fields": {
"title": "Консультация морфологических препаратов",
"slug": "consultations",
"short_description": "Профессиональная верификация и экспертная оценка сложных случаев.",
"description": "Консультация препаратов помогает уточнить диагноз, подтвердить сложные клинические случаи и обеспечить единый профессиональный стандарт интерпретации.",
"icon": "stethoscope",
"sort_order": 2
}
},
{
"model": "content.service",
"pk": 3,
"fields": {
"title": "Документационное сопровождение",
"slug": "documents-support",
"short_description": "Подготовка обязательной информации и доступ к официальным документам.",
"description": "Пациенты и партнеры получают быстрый доступ к сведениям о медицинской организации, лицензии, графику работы и контактам.",
"icon": "file-text",
"sort_order": 3
}
},
{
"model": "content.service",
"pk": 4,
"fields": {
"title": "Исследование биопсийно-операционного материала",
"slug": "biopsy-operative-material",
"short_description": "Исследование биопсийно-операционного материала органов и тканей при воспалительных процессах, дистрофиях, патологии артерий/вен, пороках развития.",
"description": "Исследование биопсийно-операционного материала органов и тканей при воспалительных процессах, дистрофиях, патологии артерий/вен, пороках развития.",
"icon": "microscope",
"sort_order": 4
}
},
{
"model": "content.service",
"pk": 5,
"fields": {
"title": "Исследование операционного материала",
"slug": "operative-material",
"short_description": "Исследование операционного материала органов и тканей при инфекционных заболеваниях, опухолях ясного гистогенеза без дисплазии, гранулематозном воспалении.",
"description": "Исследование операционного материала органов и тканей при инфекционных заболеваниях, опухолях ясного гистогенеза без дисплазии, гранулематозном воспалении.",
"icon": "file-text",
"sort_order": 5
}
},
{
"model": "content.service",
"pk": 6,
"fields": {
"title": "Исследование диагностических биопсий",
"slug": "diagnostic-biopsies",
"short_description": "Исследование диагностических биопсий (кроме пункционных) органов и тканей при воспалении, дисфункции, дисплазии, опухолях, требующих уточнения гистогенеза, степени инвазии, стадии прогрессирования.",
"description": "Исследование диагностических биопсий (кроме пункционных) органов и тканей при воспалении, дисфункции, дисплазии, опухолях, требующих уточнения гистогенеза, степени инвазии, стадии прогрессирования.",
"icon": "stethoscope",
"sort_order": 6
}
},
{
"model": "content.service",
"pk": 7,
"fields": {
"title": "Исследование диагностических пункционных биопсий",
"slug": "diagnostic-puncture-biopsies",
"short_description": "Исследование диагностических пункционных биопсий различных органов и тканей, биопсий и операционный материал при иммунопатологических процессах, опухолях и опухолеподобных поражениях.",
"description": "Исследование диагностических пункционных биопсий различных органов и тканей, биопсий и операционный материал при иммунопатологических процессах, опухолях и опухолеподобных поражениях.",
"icon": "microscope",
"sort_order": 7
}
},
{
"model": "content.service",
"pk": 8,
"fields": {
"title": "Экспресс-исследование диагностических биопсий",
"slug": "express-diagnostic-biopsies",
"short_description": "Экспресс-исследование диагностических биопсий (кроме пункционных) органов и тканей при воспалении, дисфункции, дисплазии, опухолях, требующих уточнения гистогенеза, степени инвазии, стадии прогрессирования.",
"description": "Экспресс-исследование диагностических биопсий (кроме пункционных) органов и тканей при воспалении, дисфункции, дисплазии, опухолях, требующих уточнения гистогенеза, степени инвазии, стадии прогрессирования.",
"icon": "stethoscope",
"sort_order": 8
}
},
{
"model": "content.service",
"pk": 9,
"fields": {
"title": "Исследование костно-хрящевых тканей",
"slug": "bone-cartilage-tissues",
"short_description": "Исследование костно-хрящевых тканей, включая декальцинацию операционного материала.",
"description": "Исследование костно-хрящевых тканей, включая декальцинацию операционного материала.",
"icon": "file-text",
"sort_order": 9
}
},
{
"model": "content.service",
"pk": 10,
"fields": {
"title": "Гистологическое исследование последов",
"slug": "placenta-histology",
"short_description": "Гистологическое исследование последов с целью диагностики внутриутробного инфицирования.",
"description": "Гистологическое исследование последов с целью диагностики внутриутробного инфицирования.",
"icon": "microscope",
"sort_order": 10
}
},
{
"model": "content.doctor",
"pk": 1,
"fields": {
"full_name": "Чуков Сергей Залимович",
"position": "Заведующий отделением, врач-патологоанатом",
"credentials": "Сертификат специалиста 2514 от 24.12.2018",
"bio": "Руководит отделением и участвует в диагностике сложных клинических случаев.",
"photo": "",
"sort_order": 1
}
},
{
"model": "content.doctor",
"pk": 2,
"fields": {
"full_name": "Балабеков Андрей Вагифович",
"position": "Врач-патологоанатом, генеральный директор",
"credentials": "Сертификат специалиста 307 от 07.03.2019",
"bio": "Координирует работу бюро и участвует в развитии диагностической службы.",
"photo": "",
"sort_order": 2
}
},
{
"model": "content.doctor",
"pk": 3,
"fields": {
"full_name": "Громова Юлия Владимировна",
"position": "Врач-патологоанатом",
"credentials": "Сертификат ВСБ № 0947855 от 30.09.2006",
"bio": "Работает с морфологической диагностикой и сопровождением исследований.",
"photo": "",
"sort_order": 3
}
},
{
"model": "content.doctor",
"pk": 4,
"fields": {
"full_name": "Чеккуева Зульфия Алиевна",
"position": "Врач-патологоанатом",
"credentials": "Сертификат специалиста 530 от 21.04.2021 г. Диплом об окончании ординатуры 1163 от 31.08.2020 г.",
"bio": "Врач-патологоанатом.",
"photo": "",
"sort_order": 4
}
},
{
"model": "content.document",
"pk": 1,
"fields": {
"title": "Лицензия на осуществление медицинской деятельности",
"category": "Лицензии",
"description": "Лицензия № ЛО-26-01-005362 от 20 марта 2020 года.",
"file_url": "",
"sort_order": 1
}
},
{
"model": "content.document",
"pk": 2,
"fields": {
"title": "Территориальная программа ОМС СК",
"category": "Документы",
"description": "Обязательная информация для пациентов и партнеров.",
"file_url": "",
"sort_order": 2
}
}
]
+129
View File
@@ -0,0 +1,129 @@
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="ContactMessage",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("name", models.CharField(max_length=255)),
("phone", models.CharField(blank=True, max_length=64)),
("email", models.EmailField(blank=True, max_length=254)),
("message", models.TextField()),
("is_processed", models.BooleanField(default=False)),
],
options={
"verbose_name": "Сообщение",
"verbose_name_plural": "Сообщения",
"ordering": ["-created_at"],
},
),
migrations.CreateModel(
name="Doctor",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("full_name", models.CharField(max_length=255)),
("position", models.CharField(max_length=255)),
("credentials", models.TextField(blank=True)),
("bio", models.TextField(blank=True)),
("photo", models.URLField(blank=True)),
("sort_order", models.PositiveIntegerField(default=0)),
],
options={
"verbose_name": "Специалист",
"verbose_name_plural": "Специалисты",
"ordering": ["sort_order", "full_name"],
},
),
migrations.CreateModel(
name="Document",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("title", models.CharField(max_length=255)),
("category", models.CharField(blank=True, max_length=120)),
("description", models.TextField(blank=True)),
("file_url", models.URLField(blank=True)),
("sort_order", models.PositiveIntegerField(default=0)),
],
options={
"verbose_name": "Документ",
"verbose_name_plural": "Документы",
"ordering": ["sort_order", "title"],
},
),
migrations.CreateModel(
name="Page",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("title", models.CharField(max_length=255)),
("slug", models.SlugField(unique=True)),
("intro", models.TextField(blank=True)),
("body", models.TextField()),
("meta_title", models.CharField(blank=True, max_length=255)),
("meta_description", models.CharField(blank=True, max_length=320)),
("is_published", models.BooleanField(default=True)),
("nav_order", models.PositiveIntegerField(default=0)),
],
options={
"verbose_name": "Страница",
"verbose_name_plural": "Страницы",
"ordering": ["nav_order", "title"],
},
),
migrations.CreateModel(
name="Service",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("title", models.CharField(max_length=255)),
("slug", models.SlugField(unique=True)),
("short_description", models.TextField()),
("description", models.TextField()),
("icon", models.CharField(blank=True, max_length=64)),
("sort_order", models.PositiveIntegerField(default=0)),
],
options={
"verbose_name": "Услуга",
"verbose_name_plural": "Услуги",
"ordering": ["sort_order", "title"],
},
),
migrations.CreateModel(
name="SiteSettings",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("site_name", models.CharField(max_length=255)),
("tagline", models.CharField(blank=True, max_length=255)),
("phone", models.CharField(max_length=64)),
("email", models.EmailField(blank=True, max_length=254)),
("address", models.CharField(max_length=255)),
("city", models.CharField(default="Ставрополь", max_length=120)),
("working_hours", models.CharField(blank=True, max_length=255)),
("hero_title", models.CharField(max_length=255)),
("hero_subtitle", models.TextField()),
("hero_image", models.URLField(blank=True)),
("about_excerpt", models.TextField(blank=True)),
],
options={
"verbose_name": "Настройки сайта",
"verbose_name_plural": "Настройки сайта",
},
),
]
+1
View File
@@ -0,0 +1 @@
+116
View File
@@ -0,0 +1,116 @@
from django.db import models
class TimestampedModel(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
class SiteSettings(TimestampedModel):
site_name = models.CharField(max_length=255)
tagline = models.CharField(max_length=255, blank=True)
phone = models.CharField(max_length=64)
email = models.EmailField(blank=True)
address = models.CharField(max_length=255)
city = models.CharField(max_length=120, default="Ставрополь")
working_hours = models.CharField(max_length=255, blank=True)
hero_title = models.CharField(max_length=255)
hero_subtitle = models.TextField()
hero_image = models.URLField(blank=True)
about_excerpt = models.TextField(blank=True)
class Meta:
verbose_name = "Настройки сайта"
verbose_name_plural = "Настройки сайта"
def __str__(self):
return self.site_name
class Page(TimestampedModel):
title = models.CharField(max_length=255)
slug = models.SlugField(unique=True)
intro = models.TextField(blank=True)
body = models.TextField()
meta_title = models.CharField(max_length=255, blank=True)
meta_description = models.CharField(max_length=320, blank=True)
is_published = models.BooleanField(default=True)
nav_order = models.PositiveIntegerField(default=0)
class Meta:
ordering = ["nav_order", "title"]
verbose_name = "Страница"
verbose_name_plural = "Страницы"
def __str__(self):
return self.title
class Service(TimestampedModel):
title = models.CharField(max_length=255)
slug = models.SlugField(unique=True)
short_description = models.TextField()
description = models.TextField()
icon = models.CharField(max_length=64, blank=True)
sort_order = models.PositiveIntegerField(default=0)
class Meta:
ordering = ["sort_order", "title"]
verbose_name = "Услуга"
verbose_name_plural = "Услуги"
def __str__(self):
return self.title
class Doctor(TimestampedModel):
full_name = models.CharField(max_length=255)
position = models.CharField(max_length=255)
credentials = models.TextField(blank=True)
bio = models.TextField(blank=True)
photo = models.URLField(blank=True)
sort_order = models.PositiveIntegerField(default=0)
class Meta:
ordering = ["sort_order", "full_name"]
verbose_name = "Специалист"
verbose_name_plural = "Специалисты"
def __str__(self):
return self.full_name
class Document(TimestampedModel):
title = models.CharField(max_length=255)
category = models.CharField(max_length=120, blank=True)
description = models.TextField(blank=True)
file_url = models.URLField(blank=True)
sort_order = models.PositiveIntegerField(default=0)
class Meta:
ordering = ["sort_order", "title"]
verbose_name = "Документ"
verbose_name_plural = "Документы"
def __str__(self):
return self.title
class ContactMessage(TimestampedModel):
name = models.CharField(max_length=255)
phone = models.CharField(max_length=64, blank=True)
email = models.EmailField(blank=True)
message = models.TextField()
is_processed = models.BooleanField(default=False)
class Meta:
ordering = ["-created_at"]
verbose_name = "Сообщение"
verbose_name_plural = "Сообщения"
def __str__(self):
return f"{self.name} ({self.created_at:%d.%m.%Y})"
+41
View File
@@ -0,0 +1,41 @@
from rest_framework import serializers
from .models import ContactMessage, Doctor, Document, Page, Service, SiteSettings
class SiteSettingsSerializer(serializers.ModelSerializer):
class Meta:
model = SiteSettings
fields = "__all__"
class PageSerializer(serializers.ModelSerializer):
class Meta:
model = Page
fields = "__all__"
class ServiceSerializer(serializers.ModelSerializer):
class Meta:
model = Service
fields = "__all__"
class DoctorSerializer(serializers.ModelSerializer):
class Meta:
model = Doctor
fields = "__all__"
class DocumentSerializer(serializers.ModelSerializer):
class Meta:
model = Document
fields = "__all__"
class ContactMessageSerializer(serializers.ModelSerializer):
class Meta:
model = ContactMessage
fields = ("id", "name", "phone", "email", "message", "created_at")
read_only_fields = ("id", "created_at")
+26
View File
@@ -0,0 +1,26 @@
from django.urls import path
from .views import (
ContactMessageCreateAPIView,
DoctorListAPIView,
DocumentListAPIView,
HomeAPIView,
PageDetailAPIView,
PageListAPIView,
ServiceDetailAPIView,
ServiceListAPIView,
SiteSettingsAPIView,
)
urlpatterns = [
path("home/", HomeAPIView.as_view(), name="home"),
path("site/", SiteSettingsAPIView.as_view(), name="site-settings"),
path("pages/", PageListAPIView.as_view(), name="page-list"),
path("pages/<slug:slug>/", PageDetailAPIView.as_view(), name="page-detail"),
path("services/", ServiceListAPIView.as_view(), name="service-list"),
path("services/<slug:slug>/", ServiceDetailAPIView.as_view(), name="service-detail"),
path("doctors/", DoctorListAPIView.as_view(), name="doctor-list"),
path("documents/", DocumentListAPIView.as_view(), name="document-list"),
path("contact/", ContactMessageCreateAPIView.as_view(), name="contact-create"),
]
+70
View File
@@ -0,0 +1,70 @@
from rest_framework import generics
from rest_framework.response import Response
from rest_framework.views import APIView
from .models import ContactMessage, Doctor, Document, Page, Service, SiteSettings
from .serializers import (
ContactMessageSerializer,
DoctorSerializer,
DocumentSerializer,
PageSerializer,
ServiceSerializer,
SiteSettingsSerializer,
)
class HomeAPIView(APIView):
def get(self, request):
settings_obj = SiteSettings.objects.order_by("id").first()
data = {
"site": SiteSettingsSerializer(settings_obj).data if settings_obj else None,
"pages": PageSerializer(Page.objects.filter(is_published=True)[:6], many=True).data,
"services": ServiceSerializer(Service.objects.all()[:6], many=True).data,
"doctors": DoctorSerializer(Doctor.objects.all(), many=True).data,
"documents": DocumentSerializer(Document.objects.all()[:4], many=True).data,
}
return Response(data)
class SiteSettingsAPIView(generics.RetrieveAPIView):
serializer_class = SiteSettingsSerializer
def get_object(self):
return SiteSettings.objects.order_by("id").first()
class PageListAPIView(generics.ListAPIView):
serializer_class = PageSerializer
queryset = Page.objects.filter(is_published=True)
class PageDetailAPIView(generics.RetrieveAPIView):
serializer_class = PageSerializer
lookup_field = "slug"
queryset = Page.objects.filter(is_published=True)
class ServiceListAPIView(generics.ListAPIView):
serializer_class = ServiceSerializer
queryset = Service.objects.all()
class ServiceDetailAPIView(generics.RetrieveAPIView):
serializer_class = ServiceSerializer
lookup_field = "slug"
queryset = Service.objects.all()
class DoctorListAPIView(generics.ListAPIView):
serializer_class = DoctorSerializer
queryset = Doctor.objects.all()
class DocumentListAPIView(generics.ListAPIView):
serializer_class = DocumentSerializer
queryset = Document.objects.all()
class ContactMessageCreateAPIView(generics.CreateAPIView):
serializer_class = ContactMessageSerializer
queryset = ContactMessage.objects.all()