This commit is contained in:
@@ -263,7 +263,12 @@ async def login_user(user_login: UserLogin, request: Request):
|
||||
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
try:
|
||||
login_data = user_login.model_dump()
|
||||
# Преобразуем формат данных для совместимости с сервисом пользователей
|
||||
login_data = {
|
||||
"email": user_login.email,
|
||||
"username": user_login.username,
|
||||
"password": user_login.password
|
||||
}
|
||||
print(f"Sending login data to user service: {login_data}")
|
||||
|
||||
response = await client.post(
|
||||
@@ -618,6 +623,7 @@ async def location_service_proxy(request: Request):
|
||||
@app.api_route("/api/v1/calendar/settings", methods=["GET"], operation_id="calendar_settings_get")
|
||||
@app.api_route("/api/v1/calendar/settings", methods=["PUT"], operation_id="calendar_settings_put")
|
||||
# Мобильное API для календаря
|
||||
@app.api_route("/api/v1/calendar/entries/mobile", methods=["POST"], operation_id="calendar_entries_mobile_post")
|
||||
@app.api_route("/api/v1/entry", methods=["POST"], operation_id="mobile_calendar_entry_post")
|
||||
@app.api_route("/api/v1/entries", methods=["GET"], operation_id="mobile_calendar_entries_get")
|
||||
@app.api_route("/api/v1/entries", methods=["POST"], operation_id="mobile_calendar_entries_post")
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from datetime import date, datetime, timedelta
|
||||
from typing import Dict, List, Optional
|
||||
from typing import Dict, List, Optional, Any
|
||||
|
||||
from fastapi import Depends, FastAPI, HTTPException, Query
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import and_, desc, select
|
||||
from sqlalchemy import and_, desc, select, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from services.calendar_service.models import CalendarEntry, CycleData, HealthInsights
|
||||
@@ -14,6 +14,10 @@ from services.calendar_service.schemas import (CalendarEntryCreate, CalendarEntr
|
||||
FlowIntensity, HealthInsightResponse, MoodType,
|
||||
CalendarEventCreate)
|
||||
from services.calendar_service.mobile_endpoint import MobileCalendarEntryCreate, mobile_create_calendar_entry
|
||||
from services.calendar_service.mobile_responses import (MobileCalendarEntryResponse,
|
||||
MobileCalendarPeriodInfo,
|
||||
MobilePredictionInfo,
|
||||
MobileCalendarResponse)
|
||||
from shared.auth import get_current_user_from_token as get_current_user
|
||||
from shared.config import settings
|
||||
from shared.database import get_db
|
||||
@@ -349,7 +353,7 @@ async def get_all_calendar_entries(
|
||||
if end_date:
|
||||
query = query.filter(CalendarEntry.entry_date <= end_date)
|
||||
if entry_type:
|
||||
query = query.filter(CalendarEntry.entry_type == entry_type)
|
||||
query = query.filter(CalendarEntry.entry_type == entry_type.value)
|
||||
|
||||
query = query.order_by(CalendarEntry.entry_date.desc()).limit(limit)
|
||||
|
||||
@@ -679,7 +683,191 @@ async def create_mobile_calendar_entry(
|
||||
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
|
||||
|
||||
|
||||
from .utils import safe_int, safe_str, safe_bool, safe_date, safe_datetime, safe_get_column_value
|
||||
|
||||
# Новый эндпоинт для мобильного приложения для получения записей календаря с фильтрацией по датам
|
||||
@app.get("/api/v1/calendar/entries/mobile", response_model=MobileCalendarResponse)
|
||||
async def get_mobile_calendar_entries(
|
||||
start_date: Optional[date] = Query(None, description="Начальная дата для фильтрации (включительно)"),
|
||||
end_date: Optional[date] = Query(None, description="Конечная дата для фильтрации (включительно)"),
|
||||
entry_type: Optional[str] = Query(None, description="Тип записи (MENSTRUATION, OVULATION и т.д.)"),
|
||||
current_user: Dict = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
limit: int = Query(100, ge=1, le=500, description="Максимальное количество записей"),
|
||||
):
|
||||
"""Получить записи календаря для мобильного приложения с фильтрацией по датам"""
|
||||
import logging
|
||||
logging.info(f"Запрос мобильных данных. start_date={start_date}, end_date={end_date}, entry_type={entry_type}")
|
||||
|
||||
try:
|
||||
# Получаем записи из базы данных
|
||||
query = select(CalendarEntry).filter(CalendarEntry.user_id == current_user["user_id"])
|
||||
|
||||
# Фильтрация по датам
|
||||
if start_date:
|
||||
query = query.filter(CalendarEntry.entry_date >= start_date)
|
||||
if end_date:
|
||||
query = query.filter(CalendarEntry.entry_date <= end_date)
|
||||
|
||||
# Фильтрация по типу записи
|
||||
if entry_type:
|
||||
# Преобразуем тип из мобильного формата в серверный
|
||||
server_entry_type = None
|
||||
if entry_type == "MENSTRUATION":
|
||||
server_entry_type = EntryType.PERIOD.value
|
||||
elif entry_type == "OVULATION":
|
||||
server_entry_type = EntryType.OVULATION.value
|
||||
elif entry_type == "SYMPTOMS":
|
||||
server_entry_type = EntryType.SYMPTOMS.value
|
||||
|
||||
if server_entry_type:
|
||||
query = query.filter(CalendarEntry.entry_type == server_entry_type)
|
||||
|
||||
# Сортировка и ограничение количества записей
|
||||
query = query.order_by(desc(CalendarEntry.entry_date)).limit(limit)
|
||||
|
||||
# Выполнение запроса
|
||||
result = await db.execute(query)
|
||||
entries = result.scalars().all()
|
||||
|
||||
# Преобразуем записи в формат мобильного приложения
|
||||
mobile_entries = []
|
||||
for entry in entries:
|
||||
# Преобразуем тип записи из серверного формата в мобильный
|
||||
mobile_type = "SYMPTOMS" # По умолчанию
|
||||
entry_type_value = safe_str(entry.entry_type, "")
|
||||
|
||||
if entry_type_value == EntryType.PERIOD.value:
|
||||
mobile_type = "MENSTRUATION"
|
||||
elif entry_type_value == EntryType.OVULATION.value:
|
||||
mobile_type = "OVULATION"
|
||||
|
||||
# Преобразуем симптомы из строки в список
|
||||
symptoms_list = []
|
||||
entry_symptoms = safe_str(entry.symptoms, "")
|
||||
if entry_symptoms:
|
||||
symptoms_list = [s.strip() for s in entry_symptoms.split(",")]
|
||||
|
||||
# Преобразуем flow_intensity, если есть
|
||||
flow_intensity_value = None
|
||||
entry_flow = safe_str(entry.flow_intensity, "")
|
||||
if entry_flow in ['1', '2', '3', '4', '5']:
|
||||
flow_intensity_value = int(entry_flow)
|
||||
|
||||
# Преобразуем mood, если есть
|
||||
mood_value = None
|
||||
entry_mood = safe_str(entry.mood, "")
|
||||
if entry_mood:
|
||||
mood_value = entry_mood.upper()
|
||||
|
||||
# Преобразуем notes, если есть
|
||||
notes_value = safe_str(entry.notes, "")
|
||||
|
||||
# Получаем created_at
|
||||
created_at_value = safe_datetime(
|
||||
getattr(entry, 'created_at', None),
|
||||
datetime.now()
|
||||
).isoformat()
|
||||
|
||||
# Получаем is_predicted
|
||||
is_predicted_value = safe_bool(
|
||||
getattr(entry, 'is_predicted', None),
|
||||
False
|
||||
)
|
||||
|
||||
# Создаем мобильную запись
|
||||
mobile_entry = MobileCalendarEntryResponse(
|
||||
id=safe_int(entry.id),
|
||||
uuid=str(safe_int(entry.id)), # Используем ID как UUID
|
||||
date=safe_date(entry.entry_date, date.today()).isoformat(),
|
||||
type=mobile_type,
|
||||
flow_intensity=flow_intensity_value,
|
||||
mood=mood_value,
|
||||
symptoms=symptoms_list,
|
||||
notes=notes_value,
|
||||
created_at=created_at_value,
|
||||
is_predicted=is_predicted_value
|
||||
)
|
||||
mobile_entries.append(mobile_entry)
|
||||
|
||||
# Получаем информацию о текущем цикле
|
||||
current_cycle = await db.execute(
|
||||
select(CycleData)
|
||||
.filter(CycleData.user_id == current_user["user_id"])
|
||||
.order_by(desc(CycleData.cycle_start_date))
|
||||
.limit(1)
|
||||
)
|
||||
cycle_data = current_cycle.scalars().first()
|
||||
|
||||
# Создаем информацию о периоде
|
||||
period_info = MobileCalendarPeriodInfo()
|
||||
prediction_info = MobilePredictionInfo()
|
||||
|
||||
if cycle_data:
|
||||
# Заполняем информацию о текущем цикле
|
||||
cycle_start_date = safe_date(cycle_data.cycle_start_date)
|
||||
if cycle_start_date:
|
||||
period_info.current_cycle_start = cycle_start_date.isoformat()
|
||||
|
||||
cycle_length = safe_int(cycle_data.cycle_length)
|
||||
if cycle_length > 0:
|
||||
period_info.cycle_length = cycle_length
|
||||
|
||||
avg_cycle_length = safe_int(cycle_data.avg_cycle_length)
|
||||
if avg_cycle_length > 0:
|
||||
period_info.average_cycle_length = avg_cycle_length
|
||||
|
||||
# Если есть информация о периоде, рассчитываем дату окончания
|
||||
period_length = safe_int(cycle_data.period_length)
|
||||
if period_length > 0 and cycle_start_date:
|
||||
end_date_value = cycle_start_date + timedelta(days=period_length)
|
||||
period_info.expected_period_end = end_date_value.isoformat()
|
||||
|
||||
# Заполняем информацию о фертильном окне и овуляции
|
||||
if cycle_start_date:
|
||||
ovulation_day = avg_cycle_length // 2
|
||||
ovulation_date = cycle_start_date + timedelta(days=ovulation_day)
|
||||
period_info.ovulation_date = ovulation_date.isoformat()
|
||||
|
||||
# Фертильное окно начинается за 5 дней до овуляции и заканчивается через 1 день после
|
||||
period_info.fertility_window_start = (ovulation_date - timedelta(days=5)).isoformat()
|
||||
period_info.fertility_window_end = (ovulation_date + timedelta(days=1)).isoformat()
|
||||
|
||||
# Заполняем прогноз
|
||||
next_period = safe_date(cycle_data.next_period_predicted)
|
||||
if next_period:
|
||||
prediction_info.next_period_date = next_period.isoformat()
|
||||
prediction_info.confidence_level = 80 # Приблизительное значение уверенности
|
||||
|
||||
# Рассчитываем следующее фертильное окно и овуляцию
|
||||
if avg_cycle_length > 0:
|
||||
next_ovulation = next_period - timedelta(days=avg_cycle_length // 2)
|
||||
prediction_info.next_ovulation_date = next_ovulation.isoformat()
|
||||
prediction_info.next_fertile_window_start = (next_ovulation - timedelta(days=5)).isoformat()
|
||||
prediction_info.next_fertile_window_end = (next_ovulation + timedelta(days=1)).isoformat()
|
||||
|
||||
# Собираем полный ответ
|
||||
response = MobileCalendarResponse(
|
||||
entries=mobile_entries,
|
||||
period_info=period_info,
|
||||
prediction=prediction_info
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Ошибка при получении записей календаря: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Ошибка сервера: {str(e)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
from .mobile_responses import (
|
||||
MobileCalendarEntryResponse,
|
||||
MobileCalendarPeriodInfo,
|
||||
MobilePredictionInfo,
|
||||
MobileCalendarResponse
|
||||
)
|
||||
from .schemas_mobile import MobileFlowIntensity, MobileMood
|
||||
|
||||
uvicorn.run(app, host="0.0.0.0", port=8004)
|
||||
|
||||
47
services/calendar_service/mobile_responses.py
Normal file
47
services/calendar_service/mobile_responses.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from datetime import date, datetime
|
||||
from typing import List, Dict, Optional, Any
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class MobileCalendarEntryResponse(BaseModel):
|
||||
"""Формат ответа для мобильного приложения"""
|
||||
id: int
|
||||
uuid: str
|
||||
date: str # ISO формат даты YYYY-MM-DD
|
||||
type: str # Тип записи в формате мобильного приложения (MENSTRUATION, OVULATION, etc.)
|
||||
flow_intensity: Optional[int] = None # Шкала интенсивности 1-5
|
||||
mood: Optional[str] = None
|
||||
symptoms: List[str] = Field(default_factory=list)
|
||||
notes: Optional[str] = None
|
||||
created_at: str # ISO формат даты и времени
|
||||
is_predicted: bool = False
|
||||
|
||||
|
||||
class MobileCalendarPeriodInfo(BaseModel):
|
||||
"""Информация о текущем цикле для мобильного приложения"""
|
||||
current_cycle_start: Optional[str] = None # ISO формат даты YYYY-MM-DD
|
||||
expected_period_end: Optional[str] = None # ISO формат даты YYYY-MM-DD
|
||||
cycle_length: Optional[int] = None
|
||||
period_length: Optional[int] = None
|
||||
average_cycle_length: Optional[int] = None
|
||||
fertility_window_start: Optional[str] = None # ISO формат даты YYYY-MM-DD
|
||||
fertility_window_end: Optional[str] = None # ISO формат даты YYYY-MM-DD
|
||||
ovulation_date: Optional[str] = None # ISO формат даты YYYY-MM-DD
|
||||
|
||||
|
||||
class MobilePredictionInfo(BaseModel):
|
||||
"""Информация о прогнозах для мобильного приложения"""
|
||||
next_period_date: Optional[str] = None # ISO формат даты YYYY-MM-DD
|
||||
confidence_level: Optional[int] = None # 0-100 уровень уверенности
|
||||
next_fertile_window_start: Optional[str] = None # ISO формат даты YYYY-MM-DD
|
||||
next_fertile_window_end: Optional[str] = None # ISO формат даты YYYY-MM-DD
|
||||
next_ovulation_date: Optional[str] = None # ISO формат даты YYYY-MM-DD
|
||||
|
||||
|
||||
class MobileCalendarResponse(BaseModel):
|
||||
"""Полный ответ для мобильного приложения"""
|
||||
entries: List[MobileCalendarEntryResponse]
|
||||
period_info: MobileCalendarPeriodInfo = Field(default_factory=MobileCalendarPeriodInfo)
|
||||
prediction: MobilePredictionInfo = Field(default_factory=MobilePredictionInfo)
|
||||
@@ -1,6 +1,6 @@
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import Boolean, Column, Date, ForeignKey, Integer, String, Text
|
||||
from sqlalchemy import Boolean, Column, Date, Integer, String, Text
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
|
||||
from shared.database import BaseModel
|
||||
@@ -10,7 +10,7 @@ class CalendarEntry(BaseModel):
|
||||
__tablename__ = "calendar_entries"
|
||||
|
||||
uuid = Column(UUID(as_uuid=True), default=uuid.uuid4, unique=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
|
||||
user_id = Column(Integer, nullable=False, index=True) # Убран ForeignKey
|
||||
|
||||
entry_date = Column(Date, nullable=False, index=True)
|
||||
entry_type = Column(
|
||||
@@ -42,7 +42,7 @@ class CalendarEntry(BaseModel):
|
||||
class CycleData(BaseModel):
|
||||
__tablename__ = "cycle_data"
|
||||
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
|
||||
user_id = Column(Integer, nullable=False, index=True) # Убран ForeignKey
|
||||
cycle_start_date = Column(Date, nullable=False)
|
||||
cycle_length = Column(Integer) # Length of this cycle
|
||||
period_length = Column(Integer) # Length of period in this cycle
|
||||
@@ -65,7 +65,7 @@ class CycleData(BaseModel):
|
||||
class HealthInsights(BaseModel):
|
||||
__tablename__ = "health_insights"
|
||||
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
|
||||
user_id = Column(Integer, nullable=False, index=True) # Убран ForeignKey
|
||||
insight_type = Column(
|
||||
String(50), nullable=False
|
||||
) # cycle_pattern, symptom_pattern, etc.
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from datetime import date, datetime
|
||||
from enum import Enum
|
||||
from typing import List, Optional, Union
|
||||
from typing import List, Optional, Union, Any
|
||||
import uuid
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, Field, field_serializer, ConfigDict
|
||||
|
||||
|
||||
class EntryType(str, Enum):
|
||||
@@ -175,29 +177,36 @@ class CalendarEntryCreate(CalendarEntryBase):
|
||||
|
||||
class CalendarEntryResponse(BaseModel):
|
||||
id: int
|
||||
uuid: str
|
||||
uuid: UUID # Используем UUID из uuid модуля
|
||||
entry_date: date
|
||||
entry_type: str
|
||||
flow_intensity: Optional[str]
|
||||
period_symptoms: Optional[str]
|
||||
mood: Optional[str]
|
||||
energy_level: Optional[int]
|
||||
sleep_hours: Optional[int]
|
||||
symptoms: Optional[str]
|
||||
medications: Optional[str]
|
||||
notes: Optional[str]
|
||||
flow_intensity: Optional[str] = None
|
||||
period_symptoms: Optional[str] = None
|
||||
mood: Optional[str] = None
|
||||
energy_level: Optional[int] = None
|
||||
sleep_hours: Optional[int] = None
|
||||
symptoms: Optional[str] = None
|
||||
medications: Optional[str] = None
|
||||
notes: Optional[str] = None
|
||||
is_predicted: bool
|
||||
confidence_score: Optional[int]
|
||||
confidence_score: Optional[int] = None
|
||||
created_at: datetime
|
||||
updated_at: Optional[datetime] = None
|
||||
is_active: Optional[bool] = None
|
||||
user_id: Optional[int] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@field_serializer('uuid')
|
||||
def serialize_uuid(self, uuid_val: UUID) -> str:
|
||||
"""Преобразование UUID в строку для JSON-сериализации"""
|
||||
return str(uuid_val)
|
||||
|
||||
|
||||
# Модель ответа для мобильного приложения
|
||||
class CalendarEvent(BaseModel):
|
||||
id: int
|
||||
uuid: str
|
||||
uuid: UUID # Используем тип UUID из модуля uuid
|
||||
date: date
|
||||
type: str
|
||||
flow_intensity: Optional[int] = None
|
||||
@@ -206,6 +215,8 @@ class CalendarEvent(BaseModel):
|
||||
notes: Optional[str] = None
|
||||
created_at: datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@classmethod
|
||||
def from_server_response(cls, entry: CalendarEntryResponse) -> 'CalendarEvent':
|
||||
"""Преобразовать из серверной модели в модель для мобильного приложения"""
|
||||
|
||||
103
services/calendar_service/utils.py
Normal file
103
services/calendar_service/utils.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""Утилиты для работы с SQLAlchemy моделями"""
|
||||
|
||||
from datetime import date, datetime
|
||||
from typing import Any, Optional, TypeVar, Type, cast
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
def safe_get_column_value(obj: Any, column_name: str, default_value: Optional[T] = None) -> Optional[T]:
|
||||
"""
|
||||
Безопасное получение значения колонки из SQLAlchemy модели.
|
||||
|
||||
Args:
|
||||
obj: Объект SQLAlchemy модели
|
||||
column_name: Имя колонки
|
||||
default_value: Значение по умолчанию, если колонка отсутствует или значение None
|
||||
|
||||
Returns:
|
||||
Значение колонки или значение по умолчанию
|
||||
"""
|
||||
if not hasattr(obj, column_name):
|
||||
return default_value
|
||||
|
||||
value = getattr(obj, column_name)
|
||||
if value is None:
|
||||
return default_value
|
||||
|
||||
# Если значение - дата, преобразуем его в Python date
|
||||
if hasattr(value, 'isoformat'): # Дата или дата-время
|
||||
return cast(T, value)
|
||||
|
||||
# Для других типов просто возвращаем значение
|
||||
return cast(T, value)
|
||||
|
||||
def safe_int(value: Any, default: int = 0) -> int:
|
||||
"""Безопасно преобразует значение в целое число"""
|
||||
if value is None:
|
||||
return default
|
||||
|
||||
try:
|
||||
return int(value)
|
||||
except (ValueError, TypeError):
|
||||
return default
|
||||
|
||||
def safe_str(value: Any, default: str = "") -> str:
|
||||
"""Безопасно преобразует значение в строку"""
|
||||
if value is None:
|
||||
return default
|
||||
|
||||
try:
|
||||
return str(value)
|
||||
except (ValueError, TypeError):
|
||||
return default
|
||||
|
||||
def safe_bool(value: Any, default: bool = False) -> bool:
|
||||
"""Безопасно преобразует значение в булево значение"""
|
||||
if value is None:
|
||||
return default
|
||||
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
|
||||
try:
|
||||
return bool(value)
|
||||
except (ValueError, TypeError):
|
||||
return default
|
||||
|
||||
def safe_date(value: Any, default: Optional[date] = None) -> date:
|
||||
"""Безопасно преобразует значение в дату"""
|
||||
if default is None:
|
||||
default = date.today()
|
||||
|
||||
if value is None:
|
||||
return default
|
||||
|
||||
if isinstance(value, date) and not isinstance(value, datetime):
|
||||
return value
|
||||
|
||||
if isinstance(value, datetime):
|
||||
return value.date()
|
||||
|
||||
try:
|
||||
return date.fromisoformat(str(value))
|
||||
except (ValueError, TypeError):
|
||||
return default
|
||||
|
||||
def safe_datetime(value: Any, default: Optional[datetime] = None) -> datetime:
|
||||
"""Безопасно преобразует значение в дату-время"""
|
||||
if default is None:
|
||||
default = datetime.now()
|
||||
|
||||
if value is None:
|
||||
return default
|
||||
|
||||
if isinstance(value, datetime):
|
||||
return value
|
||||
|
||||
if isinstance(value, date):
|
||||
return datetime.combine(value, datetime.min.time())
|
||||
|
||||
try:
|
||||
return datetime.fromisoformat(str(value))
|
||||
except (ValueError, TypeError):
|
||||
return default
|
||||
Reference in New Issue
Block a user