Compare commits

...

2 Commits

Author SHA1 Message Date
1541aba89f feat: Добавлен запрос истории линз по пациенту
Добавлен API endpoint и query для получения истории линз по ID пациента.
2025-07-04 07:06:57 +05:00
12eea23299 feat: Добавлена каскадное удаление для моделей. 2025-07-03 15:50:02 +05:00
21 changed files with 145 additions and 56 deletions

View File

@ -1,10 +1,22 @@
FROM python:3.10-slim FROM python:3.10-slim
RUN apt-get update && apt-get install -y \
lsb-release \
wget \
gnupg \
&& rm -rf /var/lib/apt/lists/*
RUN echo "deb http://apt.postgresql.org/pub/repos/apt bookworm-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
&& wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
libpq-dev \ libpq-dev \
libmagic1 \ libmagic1 \
postgresql-client-17 \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
RUN mkdir -p /app/backups && chmod 777 /app/backups
WORKDIR /app WORKDIR /app
COPY req.txt . COPY req.txt .
@ -15,4 +27,4 @@ COPY . .
EXPOSE 8000 EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host=0.0.0.0"] CMD ["uvicorn", "app.main:app", "--host=0.0.0.0"]

View File

@ -13,13 +13,13 @@ class LensIssuesRepository:
self.db = db self.db = db
async def get_all( async def get_all(
self, self,
skip: int = 0, skip: int = 0,
limit: int = 10, limit: int = 10,
search: Optional[str] = None, search: Optional[str] = None,
sort_order: Literal["asc", "desc"] = "desc", sort_order: Literal["asc", "desc"] = "desc",
start_date: Optional[date] = None, start_date: Optional[date] = None,
end_date: Optional[date] = None end_date: Optional[date] = None
) -> Tuple[Sequence[LensIssue], int]: ) -> Tuple[Sequence[LensIssue], int]:
stmt = ( stmt = (
select(LensIssue) select(LensIssue)
@ -76,6 +76,15 @@ class LensIssuesRepository:
return issues, total_count return issues, total_count
async def get_by_patient_id(self, patient_id: int) -> Sequence[LensIssue]:
stmt = (
select(LensIssue)
.filter_by(patient_id=patient_id)
.options(joinedload(LensIssue.lens))
)
result = await self.db.execute(stmt)
return result.scalars().all()
async def get_by_id(self, lens_issue_id: int) -> Optional[LensIssue]: async def get_by_id(self, lens_issue_id: int) -> Optional[LensIssue]:
stmt = select(LensIssue).filter_by(id=lens_issue_id) stmt = select(LensIssue).filter_by(id=lens_issue_id)
result = await self.db.execute(stmt) result = await self.db.execute(stmt)

View File

@ -20,14 +20,14 @@ router = APIRouter()
description="Returns a paginated list of lens issues with optional filtering and sorting", description="Returns a paginated list of lens issues with optional filtering and sorting",
) )
async def get_all_lens_issues( async def get_all_lens_issues(
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
page_size: int = Query(10, ge=1, le=100), page_size: int = Query(10, ge=1, le=100),
search: Optional[str] = Query(None), search: Optional[str] = Query(None),
sort_order: Literal["asc", "desc"] = Query("desc"), sort_order: Literal["asc", "desc"] = Query("desc"),
start_date: Optional[date] = Query(None), start_date: Optional[date] = Query(None),
end_date: Optional[date] = Query(None), end_date: Optional[date] = Query(None),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
user=Depends(get_current_user), user=Depends(get_current_user),
): ):
lens_issues_service = LensIssuesService(db) lens_issues_service = LensIssuesService(db)
skip = (page - 1) * page_size skip = (page - 1) * page_size
@ -44,6 +44,22 @@ async def get_all_lens_issues(
total_count=total_count total_count=total_count
) )
@router.get(
'/by-patient/{patient_id}/',
response_model=list[LensIssueEntity],
summary="Get lens issues by patients id",
description="Returns a paginated list of lens issues by patients id",
)
async def get_lens_issues_by_patient(
patient_id: int,
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
):
lens_issues_service = LensIssuesService(db)
return await lens_issues_service.get_lens_issues_by_patient_id(patient_id)
@router.post( @router.post(
"/", "/",
response_model=LensIssueEntity, response_model=LensIssueEntity,

View File

@ -11,5 +11,5 @@ class AppointmentType(BaseModel):
title = Column(VARCHAR(150), nullable=False, unique=True) title = Column(VARCHAR(150), nullable=False, unique=True)
appointments = relationship('Appointment', back_populates='type') appointments = relationship('Appointment', back_populates='type', cascade="all, delete")
scheduled_appointments = relationship('ScheduledAppointment', back_populates='type') scheduled_appointments = relationship('ScheduledAppointment', back_populates='type', cascade="all, delete")

View File

@ -22,4 +22,4 @@ class Appointment(BaseModel):
doctor = relationship('User', back_populates='appointments') doctor = relationship('User', back_populates='appointments')
type = relationship('AppointmentType', back_populates='appointments') type = relationship('AppointmentType', back_populates='appointments')
files = relationship('AppointmentFile', back_populates='appointment') files = relationship('AppointmentFile', back_populates='appointment', cascade="all, delete")

View File

@ -30,4 +30,4 @@ class Lens(BaseModel):
type = relationship('LensType', back_populates='lenses') type = relationship('LensType', back_populates='lenses')
lens_issues = relationship('LensIssue', back_populates='lens') lens_issues = relationship('LensIssue', back_populates='lens', cascade="all, delete")

View File

@ -11,5 +11,5 @@ class LensType(BaseModel):
title = Column(VARCHAR(150), nullable=False, unique=True) title = Column(VARCHAR(150), nullable=False, unique=True)
lenses = relationship('Lens', back_populates='type') lenses = relationship('Lens', back_populates='type', cascade="all, delete")
contents = relationship('SetContent', back_populates='type') contents = relationship('SetContent', back_populates='type', cascade="all, delete")

View File

@ -18,5 +18,5 @@ class Mailing(BaseModel):
user = relationship('User', back_populates='mailing') user = relationship('User', back_populates='mailing')
recipients = relationship('Recipient', back_populates='mailing') recipients = relationship('Recipient', back_populates='mailing', cascade="all, delete")
mailing_options = relationship('MailingOption', back_populates='mailing') mailing_options = relationship('MailingOption', back_populates='mailing', cascade="all, delete")

View File

@ -11,4 +11,4 @@ class MailingDeliveryMethod(BaseModel):
title = Column(VARCHAR(200), nullable=False) title = Column(VARCHAR(200), nullable=False)
mailing = relationship('MailingOption', back_populates='method') mailing = relationship('MailingOption', back_populates='method', cascade="all, delete")

View File

@ -13,5 +13,5 @@ class MailingOption(BaseModel):
nullable=False) nullable=False)
mailing_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.mailing.id', ondelete='CASCADE'), nullable=False) mailing_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.mailing.id', ondelete='CASCADE'), nullable=False)
method = relationship('MailingDeliveryMethod', back_populates='mailing') method = relationship('MailingDeliveryMethod', back_populates='mailing', cascade="all, delete")
mailing = relationship('Mailing', back_populates='mailing_options') mailing = relationship('Mailing', back_populates='mailing_options', cascade="all, delete")

View File

@ -19,7 +19,7 @@ class Patient(BaseModel):
diagnosis = Column(String) diagnosis = Column(String)
correction = Column(String) correction = Column(String)
lens_issues = relationship('LensIssue', back_populates='patient') lens_issues = relationship('LensIssue', back_populates='patient', cascade="all, delete")
appointments = relationship('Appointment', back_populates='patient') appointments = relationship('Appointment', back_populates='patient', cascade="all, delete")
mailing = relationship('Recipient', back_populates='patient') mailing = relationship('Recipient', back_populates='patient', cascade="all, delete")
scheduled_appointments = relationship('ScheduledAppointment', back_populates='patient') scheduled_appointments = relationship('ScheduledAppointment', back_populates='patient', cascade="all, delete")

View File

@ -12,5 +12,5 @@ class Recipient(BaseModel):
patient_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.patients.id', ondelete='CASCADE'), nullable=False) patient_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.patients.id', ondelete='CASCADE'), nullable=False)
mailing_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.mailing.id', ondelete='CASCADE'), nullable=False) mailing_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.mailing.id', ondelete='CASCADE'), nullable=False)
patient = relationship('Patient', back_populates='mailing') patient = relationship('Patient', back_populates='mailing', cascade="all, delete")
mailing = relationship('Mailing', back_populates='recipients') mailing = relationship('Mailing', back_populates='recipients', cascade="all, delete")

View File

@ -11,4 +11,4 @@ class Set(BaseModel):
title = Column(VARCHAR(150), nullable=False, unique=True) title = Column(VARCHAR(150), nullable=False, unique=True)
contents = relationship('SetContent', back_populates='set') contents = relationship('SetContent', back_populates='set', cascade="all, delete")

View File

@ -1,16 +1,14 @@
import datetime import datetime
import io import io
import os import os
import shutil
import subprocess import subprocess
import tarfile import tarfile
from typing import Optional from typing import Optional
from fastapi_maintenance import maintenance_mode_on
import aiofiles import aiofiles
import magic
from fastapi import HTTPException, UploadFile from fastapi import HTTPException, UploadFile
from magic import magic from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.ext.asyncio import AsyncSession, AsyncEngine
from starlette.responses import FileResponse from starlette.responses import FileResponse
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename

View File

@ -24,13 +24,13 @@ class LensIssuesService:
self.lenses_repository = LensesRepository(db) self.lenses_repository = LensesRepository(db)
async def get_all_lens_issues( async def get_all_lens_issues(
self, self,
skip: int = 0, skip: int = 0,
limit: int = 10, limit: int = 10,
search: Optional[str] = None, search: Optional[str] = None,
sort_order: Literal["asc", "desc"] = "desc", sort_order: Literal["asc", "desc"] = "desc",
start_date: Optional[date] = None, start_date: Optional[date] = None,
end_date: Optional[date] = None end_date: Optional[date] = None
) -> Tuple[list[LensIssueEntity], int]: ) -> Tuple[list[LensIssueEntity], int]:
lens_issues, total_count = await self.lens_issues_repository.get_all( lens_issues, total_count = await self.lens_issues_repository.get_all(
skip=skip, skip=skip,
@ -45,6 +45,24 @@ class LensIssuesService:
total_count total_count
) )
async def get_lens_issues_by_patient_id(self, patient_id: int) -> list[LensIssueEntity]:
patient = await self.patient_repository.get_by_id(patient_id)
if not patient:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail='Пациент с таким ID не найден',
)
lens_issues = await self.lens_issues_repository.get_by_patient_id(patient.id)
print(lens_issues)
return [
self.model_to_entity(lens_issue)
for lens_issue in lens_issues
]
async def create_lens_issue(self, lens_issue: LensIssueEntity, user_id: int) -> Optional[LensIssueEntity]: async def create_lens_issue(self, lens_issue: LensIssueEntity, user_id: int) -> Optional[LensIssueEntity]:
patient = await self.patient_repository.get_by_id(lens_issue.patient_id) patient = await self.patient_repository.get_by_id(lens_issue.patient_id)

View File

@ -19,6 +19,13 @@ spec:
ports: ports:
- containerPort: {{ .Values.service.port }} - containerPort: {{ .Values.service.port }}
env: env:
- name: PG_DUMP_PATH
value: "{{ .Values.env.PG_DUMP_PATH }}"
- name: BACKUP_DB_URL
valueFrom:
secretKeyRef:
name: visus-api-secret
key: BACKUP_DB_URL
- name: SECRET_KEY - name: SECRET_KEY
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:

View File

@ -32,4 +32,5 @@ ingress:
env: env:
LOG_LEVEL: info LOG_LEVEL: info
LOG_FILE: logs/app.log LOG_FILE: logs/app.log
ALGORITHM: HS256 ALGORITHM: HS256
PG_DUMP_PATH: pg_dump

View File

@ -12,4 +12,6 @@ pyjwt==2.10.1
python-magic==0.4.27 python-magic==0.4.27
aiofiles==24.1.0 aiofiles==24.1.0
python-multipart==0.0.20 python-multipart==0.0.20
fastapi-maintenance==0.0.4 fastapi-maintenance==0.0.4
python-magic==0.4.27
libmagic==1.0

View File

@ -1,5 +1,5 @@
import { createApi } from "@reduxjs/toolkit/query/react"; import {createApi} from "@reduxjs/toolkit/query/react";
import { baseQueryWithAuth } from "./baseQuery.js"; import {baseQueryWithAuth} from "./baseQuery.js";
export const lensIssuesApi = createApi({ export const lensIssuesApi = createApi({
reducerPath: 'lensIssuesApi', reducerPath: 'lensIssuesApi',
@ -7,7 +7,7 @@ export const lensIssuesApi = createApi({
tagTypes: ['LensIssues'], tagTypes: ['LensIssues'],
endpoints: (builder) => ({ endpoints: (builder) => ({
getLensIssues: builder.query({ getLensIssues: builder.query({
query: ({ page, pageSize, search, sortOrder, startDate, endDate }) => ({ query: ({page, pageSize, search, sortOrder, startDate, endDate}) => ({
url: '/lens_issues/', url: '/lens_issues/',
params: { params: {
page, page,
@ -22,16 +22,16 @@ export const lensIssuesApi = createApi({
transformResponse: (response) => { transformResponse: (response) => {
if (!response) { if (!response) {
console.warn('Empty lens issues API response:', response); console.warn('Empty lens issues API response:', response);
return { issues: [], total_count: 0 }; return {issues: [], total_count: 0};
} }
if (Array.isArray(response.results) && typeof response.count === 'number') { if (Array.isArray(response.results) && typeof response.count === 'number') {
return { issues: response.results, total_count: response.count }; return {issues: response.results, total_count: response.count};
} }
if (Array.isArray(response.issues) && typeof response.total_count === 'number') { if (Array.isArray(response.issues) && typeof response.total_count === 'number') {
return response; return response;
} }
console.warn('Unexpected lens issues API response:', response); console.warn('Unexpected lens issues API response:', response);
return { issues: [], total_count: 0 }; return {issues: [], total_count: 0};
}, },
transformErrorResponse: (response) => { transformErrorResponse: (response) => {
console.error('Lens issues API error:', response); console.error('Lens issues API error:', response);
@ -46,10 +46,15 @@ export const lensIssuesApi = createApi({
}), }),
invalidatesTags: ['LensIssues'], invalidatesTags: ['LensIssues'],
}), }),
getLensIssuesByPatient: builder.query({
query: (patientId) => `/lens_issues/by-patient/${patientId}/`,
providesTags: ['LensIssues'],
}),
}), }),
}); });
export const { export const {
useGetLensIssuesQuery, useGetLensIssuesQuery,
useAddLensIssuesMutation, useAddLensIssuesMutation,
useGetLensIssuesByPatientQuery,
} = lensIssuesApi; } = lensIssuesApi;

View File

@ -7,8 +7,8 @@ import {useAddPatientMutation, useUpdatePatientMutation} from "../../../Api/pati
const usePatientFormModal = () => { const usePatientFormModal = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const [addPatient, { isLoading: isAdding }] = useAddPatientMutation(); const [addPatient, {isLoading: isAdding}] = useAddPatientMutation();
const [updatePatient, { isLoading: isUpdating }] = useUpdatePatientMutation(); const [updatePatient, {isLoading: isUpdating}] = useUpdatePatientMutation();
const { const {
selectedPatient, selectedPatient,
@ -19,7 +19,7 @@ const usePatientFormModal = () => {
try { try {
if (selectedPatient) { if (selectedPatient) {
await updatePatient({ id: selectedPatient.id, ...patientData }).unwrap(); await updatePatient({id: selectedPatient.id, ...patientData}).unwrap();
notification.success({ notification.success({
message: "Пациент обновлён", message: "Пациент обновлён",
description: `Данные пациента ${patientData.first_name} ${patientData.last_name} успешно обновлены.`, description: `Данные пациента ${patientData.first_name} ${patientData.last_name} успешно обновлены.`,

View File

@ -0,0 +1,21 @@
import { useGetLensIssuesByPatientQuery } from "../../../../../Api/lensIssuesApi.js";
const usePatientsViewModal = (patient, visible) => {
const {
data: lensIssues = [],
isLoading: isLensIssuesLoading,
isError: isLensIssuesError,
} = useGetLensIssuesByPatientQuery(patient?.id, {
skip: !visible || !patient?.id,
pollingInterval: 60000,
refetchOnMountOrArgChange: true,
});
return {
lensIssues,
isLensIssuesLoading,
isLensIssuesError,
};
};
export default usePatientsViewModal;