From 9bdfa7f2691e79a0f269bdbdb5d9c207f4f95d35 Mon Sep 17 00:00:00 2001 From: Incomplite Date: Sat, 21 Dec 2024 21:10:46 +0300 Subject: [PATCH 1/2] =?UTF-8?q?=D0=9F=D0=BE=D0=BB=D0=BD=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D1=8C=D1=8E=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BD=D0=B5=D1=81=20?= =?UTF-8?q?=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D1=83=20=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D1=8B=20=D1=81=20=D0=B1=D0=B4=20=D0=B2=20DAO=20=D0=BA?= =?UTF-8?q?=D0=BB=D0=B0=D1=81=D1=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/.gitignore | 8 + .../inspectionProfiles/profiles_settings.xml | 6 + .idea/misc.xml | 7 + .idea/modules.xml | 8 + .idea/tg_bot.iml | 22 +++ .idea/vcs.xml | 6 + manicure_bot.db | Bin 86016 -> 86016 bytes requirements.txt | 1 + src/api/dao.py | 116 +++++++++++ src/api/router.py | 183 ++++++++---------- src/api/utils.py | 13 +- src/bot/handlers/examples_of_works_router.py | 2 +- src/bot/handlers/reminder_router.py | 49 +++-- src/bot/handlers/user_router.py | 19 +- src/dao/base.py | 66 +++++++ src/database/db.py | 18 +- src/middlewares/scheduler.py | 6 +- src/pages/router.py | 74 +++---- src/utils.py | 14 +- 19 files changed, 388 insertions(+), 230 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/tg_bot.iml create mode 100644 .idea/vcs.xml create mode 100644 src/api/dao.py create mode 100644 src/dao/base.py diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..e915e74 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..75d992e --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/tg_bot.iml b/.idea/tg_bot.iml new file mode 100644 index 0000000..818b503 --- /dev/null +++ b/.idea/tg_bot.iml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/manicure_bot.db b/manicure_bot.db index 7f4d47a1e3e9e98ecb97d51527c30378398218ca..955b065012a1e863d92fa2df9d67dadaf4f579e3 100644 GIT binary patch delta 2481 zcmcIlZ)_7~7{BN4dhM-U-;F;4DM(kuQ3l)H^{#&qm}g~b#S#e^6%5KWs%GK>Keg)b(8iBS^F;`4TE2MI~#gZHHO zy!X7n_j!NM?|tr?K5a>#wv4&p{`m4OaR2O{Gi`O(7;=q#Mq(s3)!49$OeET$+u>z- zrfG{JDe8JzSuaby9$DESNuG6XkL=svn;mF$@VXZsgeq#UM+(hBaWDs!rtY`f-3?s~ z+2!bRE~!6Lzmhyl1l*8yOf20W;174Qbra)*-%hy(euJHDRvWY0yNYFO#j)bWVjh1N zi|2}ybX>}mpYxjHE_nsStzH3hxO1h<5{xW%HR8$_`;BNk7VVG4_i9@GvWZ;>F7V_A znZ`XYk%{R(?-ZmtcLUSFFyt$8h^%pbgmwa_4?0s4rr{ShwXn<9eX#X*M@NSRpWEs4 z(??>H#ScsA;`!3y(vjl%;+Ms|`|jk4qjx7yot_s=rsALsty1~2_v?{l;!TrbnCbtJ zCsj=(%T&YpM66Bk;2xI?J-Yc6wry1|WAj|vSLi`pEKoIENVnNUXo~L7u zqj#3&I8m%FzDMqm+vFbFRa{#jeKw8STy(%!B3uQ-S~*^%;Q9)eUr5DH zl1CbDVv>}lvBGUHSa@6<0Sk-nHIeT2D)7{G0Y_uoL=NxI150Dih%}^uKo_-wK&P5P zph2uo6wxmQ3S5Bi8N)b$0t8^I_1+}6$gSn{MpAS3HF?OU*8cnoNm+TI>NVo>gGdqE zo_PZ~67&hMIMEr%lGeQ&X*$sN0B1)RMxu1LEx=MX8y#gsfJ3D7Wqe%Ql%Ad0oG!OGov~IT<<5@xY%a{Kgs_#xLAV)Yo^=ShLoE=rA~tP# zZ)2{z8CDd2Y=On)sXyA_@~T>u;Xb+V!r*b&0v5=2_#RtI$ldIMrd+)SUL=x3BA&u- z4M=&*eIW=|qu+#PqlM{Bh;0;{3=4V2{t4u%@lTu`{}@`1_c|<{{Mb;fHmLHf1$>yN zj4hq*P325s9_FrJDOp6xE4H{;bWtNk!+@EWH{2Q4<%YhlZpK!8}#8wKmJ4X z=|e*Y5`)PBb1<3Pyd6AiqI%NIX6#$hy*!+V$IapFMAmya`*wCsKZGG;8y+3(Lr|{N z25yv!Ub%O-L(>9jZJ5b$ojb$XWVR=J41c-OT6nT0%pJ{Ugne)6!|@SJli>&T0o-EY zqbQtcgAQB&ve8!j9$h{={wK)qghkZ-Sa+>Ai}q|)mlZG7cn|+)S<2t@TC%W@CrcZZ V<#?~H+=aD17S%4S1$?**{{=Wnp)vpf delta 1112 zcmbVJPe>F|7=LeO_P@KI5}~Ey{;3vQ&6(MqnO#XtOfbw6!h?y>y6%>=k#z8=OAvN+ zTVkY3=D};k9)=Dc@?eG(6&|wEg3@CeN=b_J&CFVMD53B1-uJ!V_rCZ0{$$22nQ_Z# zIUUJXG}DnsmnQ3+O9Yl6rLTNRaTnzJ8=JD@kxDP+h))oCF;XiCwSwI37o_?y@2~Ot z1tq`L=<*ho5$Gx^^OU+L-BlQCEy|UpmE5iLpzQrG$#11yOl~%PA=h>NF@E?O@}Gyj zUMGbq0#h&q3WB9@=nSzy_?~T7U%%n1yq}-2h#CeqT6-bx@9@crrQ>1M7P9 zI())45Az7QaO*Ce%8uJcNdEZ8P8jB6SdTFay|98&n2w+$J=G4JU-Rz#_;9!XZ!tYO zfv;2KQP<$!BW5!SfR%Gl$}tvH`JGj^xD;_K1PHu`I}iX3u0boF8dm+p!$7N=_0T{; z2h||Zqb(^uCdU$TuOf?LBqB;NG07+5eTtY03%yb_Vn4#R(-a?$rzBpKWkrrCq7apn zeZ0uW1vwE(#+6<*3v|FFpChmc%P4ot_)Ere_oUhpq?Ov+5ItzPzShy1 zz<#MCpZdIw`n~%-)Zi?A!RnMB&QqW2mS{C5qGBR`?V?1%pu8HR(O>A(n2Bm{8t|=N hJ#|eG*910X%oV!bZMU>@qXYX5U{8Yop46WS{si>f^2GoE diff --git a/requirements.txt b/requirements.txt index 421d6f1..6b0002e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ aiogram-calendar==0.5.0 aiohappyeyeballs==2.4.0 aiohttp==3.10.5 aiosignal==1.3.1 +aiosqlite==0.20.0 alembic==1.13.2 annotated-types==0.7.0 anyio==4.6.0 diff --git a/src/api/dao.py b/src/api/dao.py new file mode 100644 index 0000000..d92cc50 --- /dev/null +++ b/src/api/dao.py @@ -0,0 +1,116 @@ +import json +import logging +from datetime import date + +from sqlalchemy import select +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.orm import joinedload + +from src.dao.base import BaseDAO +from src.database import User, Service, Appointment, Photo, AvailableTimeSlot +from src.database.db import async_session_maker +from src.database.models import AppointmentStatus + +logger = logging.getLogger(__name__) + + +class UserDAO(BaseDAO): + model = User + + +class ServiceDAO(BaseDAO): + model = Service + + +class AppointmentDAO(BaseDAO): + model = Appointment + + @classmethod + async def get_appointments(cls, user_id: int = None): + async with async_session_maker() as session: + filters = [] + if user_id is not None: + filters.append(cls.model.user_id == user_id) + + active_appointments = await session.execute( + select(cls.model).filter( + *filters, + cls.model.status.in_([AppointmentStatus.ACTIVE.value, AppointmentStatus.CONFIRMED.value]) + ).options(joinedload(cls.model.services)) + ) + archived_appointments = await session.execute( + select(cls.model).filter( + *filters, + cls.model.status == AppointmentStatus.ARCHIVED.value + ).options(joinedload(cls.model.services)) + ) + return active_appointments.unique().scalars().all(), archived_appointments.unique().scalars().all() + + @classmethod + async def add_service(cls, appointment_id: int, service: Service): + """Добавляет услугу к записи""" + async with async_session_maker() as session: + try: + # Загружаем запись вместе с услугами + stmt = select(cls.model).options( + joinedload(cls.model.services) + ).where(cls.model.id == appointment_id) + result = await session.execute(stmt) + appointment = result.unique().scalar_one_or_none() + + if appointment: + # Получаем service из текущей сессии + service = await session.merge(service) + if service not in appointment.services: + appointment.services.append(service) + await session.commit() + return True + return True + return False + except SQLAlchemyError: + await session.rollback() + return False + + @classmethod + async def delete_with_services(cls, appointment_id: int): + """Удаляет запись вместе со всеми связанными услугами""" + async with async_session_maker() as session: + try: + # Загружаем запись вместе с услугами + stmt = select(cls.model).options( + joinedload(cls.model.services) + ).where(cls.model.id == appointment_id) + result = await session.execute(stmt) + appointment = result.unique().scalar_one_or_none() + + if appointment: + # Очищаем связи с услугами + appointment.services = [] + # Удаляем саму запись + await session.delete(appointment) + await session.commit() + return True + return False + except SQLAlchemyError: + await session.rollback() + return False + + +class PhotoDAO(BaseDAO): + model = Photo + + +class AvailableTimeSlotDAO(BaseDAO): + model = AvailableTimeSlot + + @classmethod + async def add_or_update(cls, date: date, slots: list[str]): + existing_slot = await cls.find_one_or_none(date=date) + time_slots = json.dumps(slots) + + if existing_slot: + # Обновляем существующую запись + await cls.update(existing_slot.id, time_slots=time_slots) + else: + # Добавляем новую запись + await cls.add(date=date, time_slots=time_slots) diff --git a/src/api/router.py b/src/api/router.py index ab99f8e..67cdbda 100644 --- a/src/api/router.py +++ b/src/api/router.py @@ -4,14 +4,14 @@ from fastapi.requests import Request from fastapi.responses import JSONResponse -from src.api.schemas import AppointmentData, Schedule, ServiceData, ServiceResponse +from src.api.dao import AppointmentDAO, AvailableTimeSlotDAO, ServiceDAO, UserDAO + +from src.api.schemas import AppointmentData, Schedule, ServiceData from src.api.utils import archive_appointment from src.bot.bot_instance import bot from src.bot.handlers.reminder_router import schedule_reminder from src.bot.keyboards import contact_button, main_keyboard from src.config import settings -from src.database import Appointment, AvailableTimeSlot, Service, User -from src.database.db import get_db from src.database.models import AppointmentStatus from src.middlewares.scheduler import scheduler @@ -28,10 +28,9 @@ async def create_appointment(request: Request): formatted_time = validated_data.appointment_time.strftime("%H:%M") formatted_date = validated_data.appointment_date.strftime("%d.%m.%Y") - with get_db() as db: - user = db.query(User).filter(User.id == validated_data.user_id).first() - if not user: - raise HTTPException(status_code=404, detail="Пользователь не найден") + user = await UserDAO.find_one_or_none(id=validated_data.user_id) + if not user: + raise HTTPException(status_code=404, detail="Пользователь не найден") # Формируем сообщение для пользователя message = ( @@ -62,32 +61,28 @@ async def create_appointment(request: Request): ) # Добавление заявки в базу данных - with get_db() as db: - appointment = Appointment( - user_id=validated_data.user_id, - name=validated_data.name, - date=validated_data.appointment_date, - time=validated_data.appointment_time, - total_price=validated_data.total_price, - status=AppointmentStatus.ACTIVE.value - ) - db.add(appointment) - db.commit() # Сохраняем изменения - - # Добавление услуг в ассоциативную таблицу - for service_name in validated_data.services: - service = db.query(Service).filter(Service.name == service_name).first() - if service: - appointment.services.append(service) # Добавляем услуги к записи - - db.commit() # Сохраняем изменения с услугами - - change_time = datetime.combine( - validated_data.appointment_date, - validated_data.appointment_time - ) + timedelta(hours=2) - scheduler.add_job(archive_appointment, "date", run_date=change_time, args=[appointment.id]) - schedule_reminder(appointment) + appointment = await AppointmentDAO.add( + user_id=validated_data.user_id, + name=validated_data.name, + date=validated_data.appointment_date, + time=validated_data.appointment_time, + total_price=validated_data.total_price, + status=AppointmentStatus.ACTIVE.value + ) + + # Добавление услуг к заявке + for service_name in validated_data.services: + service = await ServiceDAO.find_one_or_none(name=service_name) + if service: + await AppointmentDAO.add_service(appointment.id, service) + + change_time = datetime.combine( + validated_data.appointment_date, + validated_data.appointment_time + ) + timedelta(hours=2) + # + timedelta(hours=2) + scheduler.add_job(archive_appointment, "date", run_date=change_time, args=[appointment.id]) + schedule_reminder(appointment) kb = main_keyboard(user_id=validated_data.user_id, first_name=validated_data.name) inline_kb = contact_button() @@ -102,119 +97,95 @@ async def create_appointment(request: Request): @router.delete("/appointment/{appointment_id}") async def delete_appointment(appointment_id: int): - with get_db() as db: - appointment = db.query(Appointment).filter(Appointment.id == appointment_id).first() - if appointment: - db.delete(appointment) - db.commit() - return JSONResponse(status_code=200, content={"message": "Запись удалена"}) - return JSONResponse(status_code=404, content={"message": "Запись не найдена"}) + is_deleted = await AppointmentDAO.delete_with_services(appointment_id) + if is_deleted: + return JSONResponse(status_code=200, content={"message": "Запись удалена"}) + return JSONResponse(status_code=404, content={"message": "Запись не найдена"}) @router.get("/all-slots/{date}") async def get_all_slots(date: date): - with get_db() as db: - slot = db.query(AvailableTimeSlot).filter(AvailableTimeSlot.date == date).first() - if slot: - return {"slots": slot.get_time_slots()} - return JSONResponse(status_code=404, content={"message": "Слоты не найдены"}) + slot = await AvailableTimeSlotDAO.find_one_or_none(date=date) + if slot: + return {"slots": slot.get_time_slots()} + return JSONResponse(status_code=404, content={"message": "Слоты не найдены"}) @router.get("/available-slots/{date}") async def get_available_slots(date: date): - with get_db() as db: - slot = db.query(AvailableTimeSlot).filter(AvailableTimeSlot.date == date).first() + slot = await AvailableTimeSlotDAO.find_one_or_none(date=date) + if slot: # Отфильтровываем уже забронированные места - booked_times = db.query(Appointment.time).filter(Appointment.date == date).all() - booked_times = {t[0].strftime("%H:%M") for t in booked_times} - # Удаляем забронированные слоты из списка доступных слотов - if slot: - available_slots = [time for time in slot.get_time_slots() if time not in booked_times] - return {"slots": available_slots} - return JSONResponse(status_code=404, content={"message": "Слоты не найдены"}) + booked_times = await AppointmentDAO.find_all(date=date) + booked_times = {time.time.strftime("%H:%M") for time in booked_times} + available_slots = [time for time in slot.get_time_slots() if time not in booked_times] + return {"slots": available_slots} + return JSONResponse(status_code=404, content={"message": "Слоты не найдены"}) @router.post("/schedule") async def save_schedule(schedule: Schedule): - with get_db() as db: - existing_slots = db.query(AvailableTimeSlot).filter(AvailableTimeSlot.date == schedule.date).first() + if not schedule.slots or len(schedule.slots) == 0: + # Если слоты не выбраны, удаляем запись для данной даты + await AvailableTimeSlotDAO.delete(date=schedule.date) + return {"status": "deleted"} - if not schedule.slots or len(schedule.slots) == 0: - # Если слоты не выбраны, удаляем запись для данной даты - if existing_slots: - db.query(AvailableTimeSlot).filter(AvailableTimeSlot.date == schedule.date).delete() - db.commit() - - # Если слоты выбраны, обновляем существующую запись - if existing_slots: - existing_slots.set_time_slots(schedule.slots) - else: - # Если записи не существует, создаем новую - new_slot = AvailableTimeSlot(date=schedule.date) - new_slot.set_time_slots(schedule.slots) - db.add(new_slot) - - db.commit() - - return {"status": "success"} + # Если слоты выбраны, добавляем или обновляем запись + await AvailableTimeSlotDAO.add_or_update(schedule.date, schedule.slots) + return {"status": "success"} @router.get("/schedules") async def get_schedules(): - with get_db() as db: - schedules = db.query(AvailableTimeSlot).all() - response = {} - for slot in schedules: - response[slot.date] = slot.get_time_slots() + schedules = await AvailableTimeSlotDAO.find_all() + response = {} + for slot in schedules: + response[slot.date] = slot.get_time_slots() - return [{"date": date, "slots": times} for date, times in response.items()] + return [{"date": date, "slots": times} for date, times in response.items()] -@router.post("/services", response_model=ServiceResponse, status_code=201) +@router.post("/services", status_code=201) async def create_service(request: Request): data = await request.json() validated_data = ServiceData(**data) - service = Service( + service = await ServiceDAO.add( name=validated_data.name, price=validated_data.price, duration=validated_data.duration, description=validated_data.description ) - with get_db() as db: - db.add(service) - db.commit() - db.refresh(service) + if not service: + raise HTTPException(status_code=400, detail="Failed to create service") return service -@router.put("/services/{service_id}", response_model=ServiceResponse, status_code=200) +@router.put("/services/{service_id}", status_code=200) async def update_service(service_id: int, request: Request): data = await request.json() - with get_db() as db: - service = db.query(Service).filter(Service.id == service_id).first() - if not service: - raise HTTPException(status_code=404, detail="Service not found") + # Найти существующую услугу + service = await ServiceDAO.find_one_or_none(id=service_id) + if not service: + raise HTTPException(status_code=404, detail="Service not found") - service.name = data['name'] - service.price = data['price'] - service.duration = data['duration'] - service.description = data['description'] + # Обновить атрибуты + for key, value in data.items(): + setattr(service, key, value) - db.commit() - db.refresh(service) - - return service + # Сохранить изменения + result = await ServiceDAO.update(service_id, **data) # Убедитесь, что передаете нужные данные + if result: + return await ServiceDAO.find_one_or_none(id=service_id) # Возвращаем обновленную запись + else: + raise HTTPException(status_code=500, detail="Failed to update service") @router.delete("/services/{service_id}") async def delete_service(service_id: int): - with get_db() as db: - service = db.query(Service).filter(Service.id == service_id).first() - if service: - db.delete(service) - db.commit() - return JSONResponse(status_code=200, content={"message": "Услуга удалена"}) - return JSONResponse(status_code=404, content={"message": "Услуга не найдена"}) + service = await ServiceDAO.delete(id=service_id) + if service: + return JSONResponse(status_code=200, content={"message": "Услуга удалена"}) + return JSONResponse(status_code=404, content={"message": "Услуга не найдена"}) diff --git a/src/api/utils.py b/src/api/utils.py index 76992aa..f576cb3 100644 --- a/src/api/utils.py +++ b/src/api/utils.py @@ -1,13 +1,10 @@ -from src.database import Appointment -from src.database.db import get_db +from src.api.dao import AppointmentDAO from src.database.models import AppointmentStatus async def archive_appointment(appointment_id: int): - with get_db() as db: - appointment = db.query(Appointment).filter(Appointment.id == appointment_id).first() - if appointment: - appointment.status = AppointmentStatus.ARCHIVED.value - db.commit() - print("Запись перенесена в архив") + appointment = await AppointmentDAO.update(appointment_id, status=AppointmentStatus.ARCHIVED.value) + if appointment: + print("Запись перенесена в архив") + else: print("Запись не найдена") diff --git a/src/bot/handlers/examples_of_works_router.py b/src/bot/handlers/examples_of_works_router.py index 35f1d4f..d36b486 100644 --- a/src/bot/handlers/examples_of_works_router.py +++ b/src/bot/handlers/examples_of_works_router.py @@ -8,7 +8,7 @@ @router.message(F.text == "💅Примеры работ") async def show_photo(message: types.Message): - photos = get_photos() + photos = await get_photos() media = [types.InputMediaPhoto(media=photo.photo_url) for photo in photos] await message.answer("Вот фото нескольких работ:") await message.answer_media_group(media, reply_markup=main_keyboard( diff --git a/src/bot/handlers/reminder_router.py b/src/bot/handlers/reminder_router.py index 6bb78a6..9c6404f 100644 --- a/src/bot/handlers/reminder_router.py +++ b/src/bot/handlers/reminder_router.py @@ -7,10 +7,9 @@ InlineKeyboardMarkup ) +from src.api.dao import AppointmentDAO from src.bot.bot_instance import bot from src.config import settings -from src.database import Appointment -from src.database.db import get_db from src.database.models import AppointmentStatus from src.middlewares.scheduler import scheduler @@ -47,31 +46,27 @@ async def process_callback_button(callback_query: CallbackQuery): action, appointment_id = callback_query.data.split('_') appointment_id = int(appointment_id) - with get_db() as db: - appointment = db.query(Appointment).filter(Appointment.id == appointment_id).first() - - if appointment is None: - return - - user_id = appointment.user_id - appointment_time = appointment.time.strftime('%H:%M') - client_name = appointment.name - - if action == 'confirm': - appointment.status = AppointmentStatus.CONFIRMED.value - client_message = "Спасибо за подтверждение! Жду Вас в назначенное время. 🌼" - elif action == 'cancel': - db.delete(appointment) - client_message = "Ваша запись отменена. Спасибо! 🌼" - - admin_message = ( - f"🔔 {'Клиент подтвердил' if action == 'confirm' else 'Клиент отменил'} запись:\n\n" - f"👤 Имя клиента: {client_name}\n" - f"👤 Профиль: @{callback_query.from_user.username}\n" - f"⏰ Время: {appointment_time}\n" - ) - - db.commit() + appointment = await AppointmentDAO.find_one_or_none(id=appointment_id) + if appointment is None: + return + + user_id = appointment.user_id + appointment_time = appointment.time.strftime('%H:%M') + client_name = appointment.name + + if action == 'confirm': + await AppointmentDAO.update(appointment_id, status=AppointmentStatus.CONFIRMED.value) + client_message = "Спасибо за подтверждение! Жду Вас в назначенное время. 🌼" + elif action == 'cancel': + await AppointmentDAO.delete(id=appointment_id) + client_message = "Ваша запись отменена. Спасибо! 🌼" + + admin_message = ( + f"🔔 {'Клиент подтвердил' if action == 'confirm' else 'Клиент отменил'} запись:\n\n" + f"👤 Имя клиента: {client_name}\n" + f"👤 Профиль: @{callback_query.from_user.username}\n" + f"⏰ Время: {appointment_time}\n" + ) await bot.send_message(chat_id=user_id, text=client_message) await bot.send_message(chat_id=settings.MASTER_CHAT_ID, text=admin_message) diff --git a/src/bot/handlers/user_router.py b/src/bot/handlers/user_router.py index ccf5709..4048882 100644 --- a/src/bot/handlers/user_router.py +++ b/src/bot/handlers/user_router.py @@ -2,8 +2,7 @@ from aiogram.filters import CommandStart from aiogram.types import Message -from src.database import User -from src.database.db import get_db +from src.api.dao import UserDAO from src.utils import greet_user router = Router() @@ -14,15 +13,13 @@ async def cmd_start(message: Message) -> None: """ Обрабатывает команду /start. """ - with get_db() as db: - user = db.query(User).filter(User.id == message.from_user.id).first() + user = await UserDAO.find_one_or_none(id=message.from_user.id) - if not user: - db.add(User( - id=message.from_user.id, - name=message.from_user.first_name, - username=message.from_user.username - )) - db.commit() + if not user: + user = UserDAO.add( + id=message.from_user.id, + name=message.from_user.first_name, + username=message.from_user.username + ) await greet_user(message, is_new_user=not user) diff --git a/src/dao/base.py b/src/dao/base.py new file mode 100644 index 0000000..138ca07 --- /dev/null +++ b/src/dao/base.py @@ -0,0 +1,66 @@ +from sqlalchemy import delete, insert, select, update +from sqlalchemy.exc import SQLAlchemyError + +from src.database.db import async_session_maker +import logging + + +class BaseDAO: + model = None + + # Метод было решено скрестить с find_one_or_none, т.к. они выполняют одну и ту же функцию + # @classmethod + # async def find_by_id(cls, model_id: int): + # async with async_session_maker() as session: + # query = select(cls.model).filter_by(id=model_id) + # result = await session.execute(query) + # return result.mappings().one_or_none() + + @classmethod + async def find_one_or_none(cls, **filter_by): + async with async_session_maker() as session: + query = select(cls.model).filter_by(**filter_by) + result = await session.execute(query) + return result.scalar_one_or_none() + + @classmethod + async def find_all(cls, **filter_by): + async with async_session_maker() as session: + query = select(cls.model).filter_by(**filter_by) + result = await session.execute(query) + return result.scalars().all() + + @classmethod + async def add(cls, **data): + async with async_session_maker() as session: + async with session.begin(): + new_instance = cls.model(**data) + session.add(new_instance) + try: + await session.commit() + except SQLAlchemyError as e: + await session.rollback() + return new_instance + + @classmethod + async def delete(cls, **filter_by): + async with async_session_maker() as session: + query = delete(cls.model).filter_by(**filter_by) + result = await session.execute(query) + await session.commit() + return result.rowcount > 0 + + @classmethod + async def update(cls, id: int, **data): + try: + query = update(cls.model).where(cls.model.id == id).values(**data) + async with async_session_maker() as session: + await session.execute(query) + await session.commit() + return True + except SQLAlchemyError as e: + logging.error(f"SQLAlchemyError: {e}") + return False + except Exception as e: + logging.error(f"Exception: {e}") + return False diff --git a/src/database/db.py b/src/database/db.py index 18fc79d..ea5b4fa 100644 --- a/src/database/db.py +++ b/src/database/db.py @@ -1,18 +1,6 @@ -from contextlib import contextmanager - -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker +from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine, AsyncSession from src.config import settings -engine = create_engine(settings.DATABASE_URL) -SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) - - -@contextmanager -def get_db(): - db = SessionLocal() - try: - yield db - finally: - db.close() +engine = create_async_engine(settings.DATABASE_URL) +async_session_maker = async_sessionmaker(engine, autoflush=False, expire_on_commit=False, class_=AsyncSession) diff --git a/src/middlewares/scheduler.py b/src/middlewares/scheduler.py index 8a9ce6c..ed37ed7 100644 --- a/src/middlewares/scheduler.py +++ b/src/middlewares/scheduler.py @@ -11,11 +11,7 @@ MOSCOW_TZ = pytz.timezone("Europe/Moscow") -jobstores = { - 'default': SQLAlchemyJobStore(url=settings.DATABASE_URL) -} - -scheduler = AsyncIOScheduler(jobstores=jobstores, timezone=MOSCOW_TZ) +scheduler = AsyncIOScheduler(timezone=MOSCOW_TZ) class SchedulerMiddleware(BaseMiddleware): diff --git a/src/pages/router.py b/src/pages/router.py index ca47fdd..1f1bc84 100644 --- a/src/pages/router.py +++ b/src/pages/router.py @@ -3,12 +3,8 @@ from fastapi.responses import HTMLResponse from fastapi.templating import Jinja2Templates -from sqlalchemy.orm import joinedload - +from src.api.dao import ServiceDAO, AppointmentDAO, UserDAO from src.config import settings -from src.database import Appointment, Service, User -from src.database.db import get_db -from src.database.models import AppointmentStatus router = APIRouter(prefix='', tags=['Фронтенд']) templates = Jinja2Templates(directory='src/templates') @@ -24,8 +20,7 @@ async def get_home_page(request: Request): @router.get("/form", response_class=HTMLResponse) async def get_service_form(request: Request, user_id: int = None, first_name: str = None): - with get_db() as db: - services = db.query(Service).all() + services = await ServiceDAO.find_all() data_page = {"request": request, "user_id": user_id, "first_name": first_name, @@ -37,31 +32,24 @@ async def get_service_form(request: Request, user_id: int = None, first_name: st @router.get("/appointments", response_class=HTMLResponse) async def get_user_appointments(request: Request, user_id: int = None): data_page = {"request": request, "access": False, 'title_h1': "Мои записи"} - with get_db() as db: - user_check = db.query(User).filter(User.id == user_id).first() - services = db.query(Service).all() - data_page['services'] = services + user_check = await UserDAO.find_one_or_none(id=user_id) - if user_id is None or user_check is None: - data_page['message'] = 'Пользователь не указан или не найден в базе данных' - return templates.TemplateResponse("appointments.html", data_page) - else: - active_appointments = db.query(Appointment).filter( - Appointment.user_id == user_id, Appointment.status.in_([ - AppointmentStatus.ACTIVE.value, - AppointmentStatus.CONFIRMED.value - ])).all() - archived_appointments = db.query(Appointment).filter( - Appointment.user_id == user_id, Appointment.status == AppointmentStatus.ARCHIVED.value - ).all() - data_page['access'] = True - data_page['active_appointments'] = active_appointments - data_page['archived_appointments'] = archived_appointments + if user_id is None or user_check is None: + data_page['message'] = 'Пользователь не указан или не найден в базе данных' + return templates.TemplateResponse("appointments.html", data_page) - if not active_appointments and not archived_appointments: - data_page['message'] = 'У вас нет записей!' + active_appointments, archived_appointments = await AppointmentDAO.get_appointments(user_id) + services = await ServiceDAO.find_all() - return templates.TemplateResponse("appointments.html", data_page) + data_page['access'] = True + if not active_appointments and not archived_appointments: + data_page['message'] = 'У вас нет записей!' + else: + data_page['services'] = services + data_page['active_appointments'] = active_appointments + data_page['archived_appointments'] = archived_appointments + + return templates.TemplateResponse("appointments.html", data_page) @router.get("/admin/appointments", response_class=HTMLResponse) @@ -71,17 +59,10 @@ async def get_admin_panel(request: Request, admin_id: int = None): data_page['message'] = 'У вас нет прав для получения информации о записях!' return templates.TemplateResponse("appointments.html", data_page) else: - data_page['access'] = True - with get_db() as db: - active_appointments = db.query(Appointment).filter( - Appointment.status.in_([AppointmentStatus.ACTIVE.value, AppointmentStatus.CONFIRMED.value]) - ).options(joinedload(Appointment.services)).all() - archived_appointments = db.query(Appointment).filter( - Appointment.status == AppointmentStatus.ARCHIVED.value - ).options(joinedload(Appointment.services)).all() - - services = db.query(Service).all() + active_appointments, archived_appointments = await AppointmentDAO.get_appointments() + services = await ServiceDAO.find_all() + data_page['access'] = True data_page['active_appointments'] = active_appointments data_page['archived_appointments'] = archived_appointments data_page['services'] = services @@ -104,8 +85,7 @@ async def get_admin_interface(request: Request, admin_id: int = None): @router.get("/services", response_class=HTMLResponse) async def get_services(request: Request, user_id: int = None): data_page = {"request": request, "access": True, "title": "Прайс-лист"} - with get_db() as db: - services = db.query(Service).all() + services = await ServiceDAO.find_all() data_page['services'] = services return templates.TemplateResponse("services.html", data_page) @@ -113,12 +93,12 @@ async def get_services(request: Request, user_id: int = None): @router.get("/admin/services", response_class=HTMLResponse) async def get_admin_services(request: Request, admin_id: int = None): data_page = {"request": request, "access": False, "title": "Администрирование прайс-листа"} - with get_db() as db: - services = db.query(Service).all() - data_page['services'] = services if admin_id is None or admin_id != settings.MASTER_USER_ID: data_page['message'] = 'У вас нет прав!' return templates.TemplateResponse("services.html", data_page) - else: - data_page['access'] = True - return templates.TemplateResponse("services.html", data_page) + + services = await ServiceDAO.find_all() + + data_page['access'] = True + data_page['services'] = services + return templates.TemplateResponse("services.html", data_page) diff --git a/src/utils.py b/src/utils.py index 3fa42d1..881d94e 100644 --- a/src/utils.py +++ b/src/utils.py @@ -2,14 +2,12 @@ from aiogram.types import Message +from src.api.dao import PhotoDAO, AvailableTimeSlotDAO from src.bot.keyboards import main_keyboard -from src.database import AvailableTimeSlot, Photo -from src.database.db import get_db -def get_photos(): - with get_db() as db: - photos = db.query(Photo).all() +async def get_photos(): + photos = await PhotoDAO.find_all() return photos @@ -30,8 +28,4 @@ async def greet_user(message: Message, is_new_user: bool) -> None: async def delete_old_schedule_entries(): threshold_date = datetime.now().date() + timedelta(days=1) - print(f"Запуск задачи на удаление записей старше {threshold_date}") - with get_db() as db: - db.query(AvailableTimeSlot).filter(AvailableTimeSlot.date < threshold_date).delete() - db.commit() - print("Старые записи успешно удалены.") + await AvailableTimeSlotDAO.delete(date__lt=threshold_date) From 440897fd40c2cd3b326aef51924904cdb5d0ddd6 Mon Sep 17 00:00:00 2001 From: Incomplite Date: Sat, 21 Dec 2024 21:16:01 +0300 Subject: [PATCH 2/2] =?UTF-8?q?=D0=98=D1=81=D0=BA=D0=BB=D1=8E=D1=87=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BF=D0=B0=D0=BF=D0=BA=D1=83=20.idea=20=D0=B8=D0=B7?= =?UTF-8?q?=20=D1=80=D0=B5=D0=BF=D0=BE=D0=B7=D0=B8=D1=82=D0=BE=D1=80=D0=B8?= =?UTF-8?q?=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/.gitignore | 8 ------- .../inspectionProfiles/profiles_settings.xml | 6 ----- .idea/misc.xml | 7 ------ .idea/modules.xml | 8 ------- .idea/tg_bot.iml | 22 ------------------- .idea/vcs.xml | 6 ----- 6 files changed, 57 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/inspectionProfiles/profiles_settings.xml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/tg_bot.iml delete mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 105ce2d..0000000 --- a/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index e915e74..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 75d992e..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/tg_bot.iml b/.idea/tg_bot.iml deleted file mode 100644 index 818b503..0000000 --- a/.idea/tg_bot.iml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file