From af9aa243bff730508c35bc2963135d8d5dd4a9fd Mon Sep 17 00:00:00 2001
From: Dmitry <ksenon.104@gmail.com>
Date: Tue, 13 Aug 2024 15:48:13 +0300
Subject: [PATCH] Many to many relationship between user and announcement
 tables implemented, book endpoint changed accordingly

---
 back/api.py                                   | 33 ++++++++++------
 back/main.py                                  |  2 +-
 back/orm_models.py                            | 37 +++++++++++++++---
 ...1_create_constructor_to_make_object_if_.py | 30 +++++++++++++++
 ...e7_booked_by_colomn_changed_to_integer_.py | 38 +++++++++++++++++++
 5 files changed, 121 insertions(+), 19 deletions(-)
 create mode 100644 migrations/versions/19dbd9793f11_create_constructor_to_make_object_if_.py
 create mode 100644 migrations/versions/945c70aa70e7_booked_by_colomn_changed_to_integer_.py

diff --git a/back/api.py b/back/api.py
index a014046..0c571b6 100644
--- a/back/api.py
+++ b/back/api.py
@@ -98,7 +98,7 @@ async def put_in_db(name: Annotated[str, Form()], category: Annotated[str, Form(
     # создаем объект Announcement
     temp_ancmt = orm_models.Announcement(user_id=current_user.id, name=name, category=category, best_by=bestBy,
         address=address, longtitude=longtitude, latitude=latitude, description=description, metro=metro,
-        trashId=trashId, src=uploaded_name, booked_by=0)
+        trashId=trashId, src=uploaded_name, booked_counter=0)
     try:
         db.add(temp_ancmt)     # добавляем в бд
         await db.commit()     # сохраняем изменения
@@ -139,19 +139,28 @@ async def change_book_status(data: pydantic_schemas.Book, current_user: Annotate
     if not announcement_to_change:
         raise HTTPException(status_code=404, detail="Item not found")
     # Проверяем, что объявление бронирует не владелец
-    if announcement_to_change.booked_by.Comparator.contains(current_user.id):
+    if current_user.id == announcement_to_change.user_id:
         raise HTTPException(status_code=403, detail="A user can't book his announcement")
     else:
-        # Инкрементируем поле booked_counter на 1
-        announcement_to_change.booked_counter += 1
-        # вставляем индекс забронировавшего пользователя в поле booked_by
-        await db.execute(mytable.insert(), data=[1,2,3])
-        
-        announcement_to_change.booked_by 
-        # фиксируем изменения в бд
-        await db.commit()
-        await db.refresh(announcement_to_change)
-        return {"Success": True}   
+        # ищем пару с заданными id объявления и пользователя
+        query = await db.execute(select(orm_models.AnnouncementUser).where(
+            orm_models.AnnouncementUser.announcement_id == announcement_to_change.id).where(
+                orm_models.AnnouncementUser.booking_user_id == current_user.id))
+        pair_found = query.scalars().first()
+        # если не найдена
+        if not pair_found:
+            # создаем новый объект таблицы AnnouncementUser
+            new_pair = orm_models.AnnouncementUser(announcement_to_change.id, current_user.id)
+            # Инкрементируем поле booked_counter на 1
+            announcement_to_change.booked_counter += 1
+            # вставляем индекс забронировавшего пользователя в поле booked_by
+            db.add(new_pair)
+
+            # фиксируем изменения в бд
+            await db.commit()
+            await db.refresh(announcement_to_change)
+            return {"Success": True}   
+        raise HTTPException(status_code=403, detail="The announcement is already booked by this user")
 
 
 # reginstration
diff --git a/back/main.py b/back/main.py
index 23d2504..7457411 100644
--- a/back/main.py
+++ b/back/main.py
@@ -21,7 +21,7 @@ async def main():
 
     await init_models()
 
-    server = Server(config=uvicorn.Config(app_fastapi, workers=1, loop="asyncio", host="0.0.0.0"))
+    server = Server(config=uvicorn.Config(app_fastapi, workers=1, loop="asyncio", host="127.0.0.1"))
 
     api = asyncio.create_task(server.serve())
     sched = asyncio.create_task(app_rocketry.serve())
diff --git a/back/orm_models.py b/back/orm_models.py
index f8a2a31..68ec5bd 100644
--- a/back/orm_models.py
+++ b/back/orm_models.py
@@ -1,6 +1,7 @@
-from sqlalchemy import Column, Integer, String, Boolean, Float, Date, ForeignKey
-from sqlalchemy.orm import relationship
+from sqlalchemy import Column, Integer, String, Boolean, Float, Date, ForeignKey, ForeignKeyConstraint
+from sqlalchemy.orm import relationship, mapped_column, composite, Mapped
 from sqlalchemy.dialects import postgresql
+import dataclasses
 
 from .db import Base, engine
 
@@ -18,7 +19,7 @@ class User(Base):#класс пользователя
     num_of_ratings = Column(Integer, default=0) # количество оценок (т.е. то, сколько раз другие пользователи оценили текущего)
     reg_date = Column(Date) # дата регистрации
 
-    announcements = relationship("Announcement", back_populates="user", lazy='selectin')
+    announcements = relationship("Announcement", secondary="announcementuser", back_populates="user", lazy='selectin')
     trashboxes_chosen = relationship("Trashbox", back_populates="user", lazy='selectin')
 
 class Announcement(Base): #класс объявления
@@ -36,11 +37,35 @@ class Announcement(Base): #класс объявления
     src = Column(String, nullable=True) #изображение продукта в объявлении
     metro = Column(String) #ближайщее метро от адреса нахождения продукта
     trashId = Column(Integer, nullable=True)
-    booked_by = postgresql.ARRAY(Integer, dimensions=2) #массив с id пользователей, забронировавших объявление
-    booked_counter = Column(Integer) #количество забронировавших (0 - никто не забронировал)
+    booked_by = Column(Integer, nullable=True) # id пользователя, забронировавшего объявление
+    booked_counter = Column(Integer, nullable=True) #количество забронировавших (0 - никто не забронировал)
     obsolete = Column(Boolean, default=False) # состояние объявления (по-умолчанию считаем его актуальным)
 
-    user = relationship("User", back_populates="announcements")
+    user = relationship("User", secondary="announcementuser", back_populates="announcements")
+
+# Класс пары, хранящей id объявления и id забронировавшего юзера
+@dataclasses.dataclass
+class UserAnouncementPair:
+    announcement_id: int
+    booking_user_id: int
+
+class AnnouncementUser(Base):
+    __tablename__ = "announcementuser"
+
+    def __init__(self, an_id, b_u_id):
+        self.announcement_id = an_id
+        self.booking_user_id = b_u_id
+
+    announcement_id = Column(Integer, ForeignKey("announcements.id"), primary_key=True) # id забронированного объявления
+    booking_user_id = Column(Integer, ForeignKey("users.id"), primary_key=True) # id пользователя, забронировавшего объявление
+    
+
+
+# class Ratings(Base):
+#     __tablename__ = "ratings"
+
+#     rated_user_id = Column(Integer, primary_key=True) # id пользователя, оставившего оценку
+#     rating_value = Column(Integer, primary_key=True) # оценка
 
 
 class Trashbox(Base): #класс мусорных баков
diff --git a/migrations/versions/19dbd9793f11_create_constructor_to_make_object_if_.py b/migrations/versions/19dbd9793f11_create_constructor_to_make_object_if_.py
new file mode 100644
index 0000000..9c13c97
--- /dev/null
+++ b/migrations/versions/19dbd9793f11_create_constructor_to_make_object_if_.py
@@ -0,0 +1,30 @@
+"""create constructor to make object (if leave as is, with two primary keys, says that one argument required, but two were given). Booked_counter can be nullable
+
+Revision ID: 19dbd9793f11
+Revises: 945c70aa70e7
+Create Date: 2024-08-13 15:29:21.542539
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision: str = '19dbd9793f11'
+down_revision: Union[str, None] = '945c70aa70e7'
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+    # ### commands auto generated by Alembic - please adjust! ###
+    pass
+    # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+    # ### commands auto generated by Alembic - please adjust! ###
+    pass
+    # ### end Alembic commands ###
diff --git a/migrations/versions/945c70aa70e7_booked_by_colomn_changed_to_integer_.py b/migrations/versions/945c70aa70e7_booked_by_colomn_changed_to_integer_.py
new file mode 100644
index 0000000..d1d23ff
--- /dev/null
+++ b/migrations/versions/945c70aa70e7_booked_by_colomn_changed_to_integer_.py
@@ -0,0 +1,38 @@
+"""booked_by colomn changed to Integer, added new table AnnouncementUser implementing one to many relationships
+
+Revision ID: 945c70aa70e7
+Revises: 5a8105ac1a4f
+Create Date: 2024-08-12 20:36:08.669574
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision: str = '945c70aa70e7'
+down_revision: Union[str, None] = '5a8105ac1a4f'
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.create_table('announcementuser',
+    sa.Column('announcement_id', sa.Integer(), nullable=False),
+    sa.Column('booking_user_id', sa.Integer(), nullable=False),
+    sa.ForeignKeyConstraint(['announcement_id'], ['announcements.id'], ),
+    sa.ForeignKeyConstraint(['booking_user_id'], ['users.id'], ),
+    sa.PrimaryKeyConstraint('announcement_id', 'booking_user_id')
+    )
+    op.add_column('announcements', sa.Column('booked_by', sa.Integer(), nullable=True))
+    # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_column('announcements', 'booked_by')
+    op.drop_table('announcementuser')
+    # ### end Alembic commands ###