diff --git a/backend/alembic/versions/04e02312343d_add_user_question_models.py b/backend/alembic/versions/04e02312343d_add_user_question_models.py new file mode 100644 index 00000000..ef318702 --- /dev/null +++ b/backend/alembic/versions/04e02312343d_add_user_question_models.py @@ -0,0 +1,66 @@ +"""Add user question models + + +Revision ID: 04e02312343d +Revises: d369cd69a23b +Create Date: 2024-09-22 21:35:48.778303 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "04e02312343d" +down_revision: Union[str, None] = "d369cd69a23b" +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( + "user_question", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("question", sa.String(), nullable=False), + sa.Column("user_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["user_id"], + ["user.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "answer", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("user_question_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["user_question_id"], + ["user_question.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "point", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("title", sa.String(), nullable=False), + sa.Column("body", sa.String(), nullable=False), + sa.Column("answer_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["answer_id"], + ["answer.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("point") + op.drop_table("answer") + op.drop_table("user_question") + # ### end Alembic commands ### diff --git a/backend/alembic/versions/680d9bea2168_add_note_models.py b/backend/alembic/versions/680d9bea2168_add_note_models.py new file mode 100644 index 00000000..1735c4b7 --- /dev/null +++ b/backend/alembic/versions/680d9bea2168_add_note_models.py @@ -0,0 +1,40 @@ +"""Add note models + +Revision ID: 680d9bea2168 +Revises: 04e02312343d +Create Date: 2024-09-22 22:07:11.979617 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "680d9bea2168" +down_revision: Union[str, None] = "04e02312343d" +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( + "note", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("content", sa.String(), nullable=False), + sa.Column("start_index", sa.Integer(), nullable=False), + sa.Column("end_index", sa.Integer(), nullable=False), + sa.Column("parent_id", sa.Integer(), nullable=False), + sa.Column("parent_type", sa.String(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("note") + # ### end Alembic commands ### diff --git a/backend/alembic/versions/8912e0e896bf_add_analysis_table.py b/backend/alembic/versions/8912e0e896bf_add_analysis_table.py new file mode 100644 index 00000000..60c73613 --- /dev/null +++ b/backend/alembic/versions/8912e0e896bf_add_analysis_table.py @@ -0,0 +1,58 @@ +"""Add Analysis table + +Revision ID: 8912e0e896bf +Revises: ff5af4d7ce04 +Create Date: 2024-09-22 18:29:26.096419 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "8912e0e896bf" +down_revision: Union[str, None] = "ff5af4d7ce04" +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( + "analysis", + sa.Column("event_id", sa.Integer(), nullable=False), + sa.Column("category_id", sa.Integer(), nullable=False), + sa.Column("content", sa.String(), nullable=False), + sa.ForeignKeyConstraint( + ["category_id"], + ["category.id"], + ), + sa.ForeignKeyConstraint( + ["event_id"], + ["event.id"], + ), + sa.PrimaryKeyConstraint("event_id", "category_id"), + ) + op.drop_table("event_category") + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "event_category", + sa.Column("event_id", sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column("category_id", sa.INTEGER(), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint( + ["category_id"], ["category.id"], name="event_category_category_id_fkey" + ), + sa.ForeignKeyConstraint( + ["event_id"], ["event.id"], name="event_category_event_id_fkey" + ), + sa.PrimaryKeyConstraint("event_id", "category_id", name="event_category_pkey"), + ) + op.drop_table("analysis") + # ### end Alembic commands ### diff --git a/backend/alembic/versions/8c458d0adb18_add_gp_questions_tables.py b/backend/alembic/versions/8c458d0adb18_add_gp_questions_tables.py new file mode 100644 index 00000000..fc0812a3 --- /dev/null +++ b/backend/alembic/versions/8c458d0adb18_add_gp_questions_tables.py @@ -0,0 +1,57 @@ +"""Add GP Questions tables + +Revision ID: 8c458d0adb18 +Revises: 8912e0e896bf +Create Date: 2024-09-22 18:49:41.011371 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "8c458d0adb18" +down_revision: Union[str, None] = "8912e0e896bf" +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( + "gp_question", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("question", sa.String(), nullable=False), + sa.Column("is_llm_generated", sa.Boolean(), nullable=False), + sa.Column("event_id", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["event_id"], + ["event.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "gp_question_categories", + sa.Column("gp_question_id", sa.Integer(), nullable=False), + sa.Column("category_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["category_id"], + ["category.id"], + ), + sa.ForeignKeyConstraint( + ["gp_question_id"], + ["gp_question.id"], + ), + sa.PrimaryKeyConstraint("gp_question_id", "category_id"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("gp_question_categories") + op.drop_table("gp_question") + # ### end Alembic commands ### diff --git a/backend/alembic/versions/b8f3f95c78b6_add_image_url_to_article.py b/backend/alembic/versions/b8f3f95c78b6_add_image_url_to_article.py new file mode 100644 index 00000000..7f3e49a8 --- /dev/null +++ b/backend/alembic/versions/b8f3f95c78b6_add_image_url_to_article.py @@ -0,0 +1,31 @@ +"""Add image url to article + +Revision ID: b8f3f95c78b6 +Revises: 8c458d0adb18 +Create Date: 2024-09-22 18:56:47.024304 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "b8f3f95c78b6" +down_revision: Union[str, None] = "8c458d0adb18" +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.add_column("article", sa.Column("image_url", sa.String(), nullable=False)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("article", "image_url") + # ### end Alembic commands ### diff --git a/backend/alembic/versions/bc5fcbe47713_remove_analysis_field_from_event.py b/backend/alembic/versions/bc5fcbe47713_remove_analysis_field_from_event.py new file mode 100644 index 00000000..28215d4a --- /dev/null +++ b/backend/alembic/versions/bc5fcbe47713_remove_analysis_field_from_event.py @@ -0,0 +1,34 @@ +"""Remove analysis field from event + +Revision ID: bc5fcbe47713 +Revises: f624d58d3f42 +Create Date: 2024-09-22 22:33:38.823000 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "bc5fcbe47713" +down_revision: Union[str, None] = "f624d58d3f42" +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.drop_column("event", "analysis") + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "event", + sa.Column("analysis", sa.VARCHAR(), autoincrement=False, nullable=False), + ) + # ### end Alembic commands ### diff --git a/backend/alembic/versions/d369cd69a23b_add_rating_to_event.py b/backend/alembic/versions/d369cd69a23b_add_rating_to_event.py new file mode 100644 index 00000000..d359ffaa --- /dev/null +++ b/backend/alembic/versions/d369cd69a23b_add_rating_to_event.py @@ -0,0 +1,31 @@ +"""Add rating to event + +Revision ID: d369cd69a23b +Revises: b8f3f95c78b6 +Create Date: 2024-09-22 18:59:11.283743 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "d369cd69a23b" +down_revision: Union[str, None] = "b8f3f95c78b6" +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.add_column("event", sa.Column("rating", sa.Integer(), nullable=False)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("event", "rating") + # ### end Alembic commands ### diff --git a/backend/alembic/versions/f624d58d3f42_add_user_id_to_note.py b/backend/alembic/versions/f624d58d3f42_add_user_id_to_note.py new file mode 100644 index 00000000..44142a66 --- /dev/null +++ b/backend/alembic/versions/f624d58d3f42_add_user_id_to_note.py @@ -0,0 +1,33 @@ +"""Add user id to note + +Revision ID: f624d58d3f42 +Revises: 680d9bea2168 +Create Date: 2024-09-22 22:29:21.220643 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "f624d58d3f42" +down_revision: Union[str, None] = "680d9bea2168" +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.add_column("note", sa.Column("user_id", sa.Integer(), nullable=False)) + op.create_foreign_key(None, "note", "user", ["user_id"], ["id"]) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, "note", type_="foreignkey") + op.drop_column("note", "user_id") + # ### end Alembic commands ### diff --git a/backend/alembic/versions/ff5af4d7ce04_add_articleevent_join_table.py b/backend/alembic/versions/ff5af4d7ce04_add_articleevent_join_table.py new file mode 100644 index 00000000..f16b394d --- /dev/null +++ b/backend/alembic/versions/ff5af4d7ce04_add_articleevent_join_table.py @@ -0,0 +1,44 @@ +"""Add ArticleEvent join table + +Revision ID: ff5af4d7ce04 +Revises: a73902039c96 +Create Date: 2024-09-22 18:24:05.699420 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "ff5af4d7ce04" +down_revision: Union[str, None] = "a73902039c96" +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( + "article_event", + sa.Column("article_id", sa.Integer(), nullable=False), + sa.Column("event_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["article_id"], + ["article.id"], + ), + sa.ForeignKeyConstraint( + ["event_id"], + ["event.id"], + ), + sa.PrimaryKeyConstraint("article_id", "event_id"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("article_event") + # ### end Alembic commands ### diff --git a/backend/src/auth/models.py b/backend/src/auth/models.py index c0385446..d5257a6b 100644 --- a/backend/src/auth/models.py +++ b/backend/src/auth/models.py @@ -3,6 +3,7 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship from src.common.base import Base from src.events.models import Category +from src.notes.models import Note class AccountType(str, Enum): @@ -27,3 +28,4 @@ class User(Base): account_type: Mapped[AccountType] categories: Mapped[list[Category]] = relationship(secondary=user_category_table) + notes: Mapped[list[Note]] = relationship("Note", backref="user") diff --git a/backend/src/events/models.py b/backend/src/events/models.py index 07fed17b..6209b518 100644 --- a/backend/src/events/models.py +++ b/backend/src/events/models.py @@ -1,8 +1,9 @@ from enum import Enum -from sqlalchemy.orm import Mapped, mapped_column, relationship -from sqlalchemy import ForeignKey +from sqlalchemy.orm import Mapped, mapped_column, relationship, foreign +from sqlalchemy import Column, ForeignKey, Table, and_ from datetime import datetime from src.common.base import Base +from src.notes.models import Note class ArticleSource(str, Enum): @@ -10,6 +11,14 @@ class ArticleSource(str, Enum): GUARDIAN = "GUARDIAN" +article_event_table = Table( + "article_event", + Base.metadata, + Column("article_id", ForeignKey("article.id"), primary_key=True), + Column("event_id", ForeignKey("event.id"), primary_key=True), +) + + class Article(Base): __tablename__ = "article" @@ -20,8 +29,21 @@ class Article(Base): url: Mapped[str] source: Mapped[ArticleSource] date: Mapped[datetime] + image_url: Mapped[str] + + original_events: Mapped[list["Event"]] = relationship( + back_populates="original_article" + ) - events: Mapped[list["Event"]] = relationship(back_populates="original_article") + events: Mapped[list["Event"]] = relationship( + back_populates="articles", secondary=article_event_table + ) + + notes = relationship( + "Note", + primaryjoin=and_(id == foreign(Note.parent_id), Note.parent_type == "article"), + backref="article", + ) class Event(Base): @@ -30,17 +52,27 @@ class Event(Base): id: Mapped[int] = mapped_column(primary_key=True) title: Mapped[str] description: Mapped[str] - analysis: Mapped[str] duplicate: Mapped[bool] date: Mapped[datetime] is_singapore: Mapped[bool] original_article_id: Mapped[int] = mapped_column(ForeignKey("article.id")) + rating: Mapped[int] categories: Mapped[list["Category"]] = relationship( - back_populates="events", secondary="event_category" + back_populates="events", secondary="analysis" ) - original_article: Mapped[Article] = relationship(back_populates="events") + original_article: Mapped[Article] = relationship(back_populates="original_events") + articles: Mapped[list[Article]] = relationship( + back_populates="events", secondary=article_event_table + ) + gp_questions: Mapped["GPQuestion"] = relationship(back_populates="event") + + notes = relationship( + "Note", + primaryjoin=and_(id == foreign(Note.parent_id), Note.parent_type == "note"), + backref="event", + ) class Category(Base): @@ -50,14 +82,41 @@ class Category(Base): name: Mapped[str] events: Mapped[list[Event]] = relationship( - secondary="event_category", back_populates="categories" + secondary="analysis", back_populates="categories" ) -class EventCategory(Base): - __tablename__ = "event_category" +class Analysis(Base): + __tablename__ = "analysis" event_id: Mapped[int] = mapped_column(ForeignKey("event.id"), primary_key=True) category_id: Mapped[int] = mapped_column( ForeignKey("category.id"), primary_key=True ) + content: Mapped[str] + + +class GPQuestion(Base): + __tablename__ = "gp_question" + + id: Mapped[int] = mapped_column(primary_key=True) + question: Mapped[str] + is_llm_generated: Mapped[bool] = mapped_column(default=True) + event_id = mapped_column(ForeignKey("event.id")) + + categories: Mapped[list["Category"]] = relationship( + secondary="gp_question_categories" + ) + + event: Mapped[Event] = relationship(back_populates="gp_questions") + + +class GPQuestionCategories(Base): + __tablename__ = "gp_question_categories" + + gp_question_id: Mapped[int] = mapped_column( + ForeignKey("gp_question.id"), primary_key=True + ) + category_id: Mapped[int] = mapped_column( + ForeignKey("category.id"), primary_key=True + ) diff --git a/backend/src/notes/models.py b/backend/src/notes/models.py new file mode 100644 index 00000000..9b27cf94 --- /dev/null +++ b/backend/src/notes/models.py @@ -0,0 +1,26 @@ +from sqlalchemy import Enum, ForeignKey +from sqlalchemy.orm import Mapped, mapped_column +from src.common.base import Base + + +class NoteType(str, Enum): + EVENT = "event" + ARTICLE = "article" + POINT = "point" + + +class Note(Base): + __tablename__ = "note" + + id: Mapped[int] = mapped_column(primary_key=True) + content: Mapped[str] + + start_index: Mapped[int] + end_index: Mapped[int] + + parent_id: Mapped[int] + parent_type: Mapped[str] + + user_id: Mapped[int] = mapped_column(ForeignKey("user.id")) + + __mapper_args__ = {"polymorphic_on": "parent_type", "polymorphic_identity": "note"} diff --git a/backend/src/scripts/seed.py b/backend/src/scripts/seed.py index 834e1b78..664c5320 100644 --- a/backend/src/scripts/seed.py +++ b/backend/src/scripts/seed.py @@ -44,7 +44,6 @@ def test_associations(): event = Event( title="test event 1", description="x", - analysis="x", duplicate=False, date=datetime.now(), is_singapore=False, diff --git a/backend/src/user_questions/models.py b/backend/src/user_questions/models.py new file mode 100644 index 00000000..70a551eb --- /dev/null +++ b/backend/src/user_questions/models.py @@ -0,0 +1,41 @@ +from sqlalchemy import ForeignKey, and_ +from src.common.base import Base +from sqlalchemy.orm import Mapped, mapped_column, relationship, foreign +from src.notes.models import Note + + +class UserQuestion(Base): + __tablename__ = "user_question" + + id: Mapped[int] = mapped_column(primary_key=True) + question: Mapped[str] + + answer: Mapped["Answer"] = relationship(back_populates="user_question") + + user_id: Mapped[int] = mapped_column(ForeignKey("user.id")) + + +class Answer(Base): + __tablename__ = "answer" + id: Mapped[int] = mapped_column(primary_key=True) + user_question_id: Mapped[int] = mapped_column(ForeignKey("user_question.id")) + + user_question: Mapped[UserQuestion] = relationship(back_populates="answer") + + points: Mapped[list["Point"]] = relationship(back_populates="answer") + + +class Point(Base): + __tablename__ = "point" + id: Mapped[int] = mapped_column(primary_key=True) + title: Mapped[str] + body: Mapped[str] + answer_id: Mapped[int] = mapped_column(ForeignKey("answer.id")) + + answer: Mapped[Answer] = relationship(back_populates="points") + + notes = relationship( + "Note", + primaryjoin=and_(id == foreign(Note.parent_id), Note.parent_type == "point"), + backref="point", + ) diff --git a/backend/src/utils/models.py b/backend/src/utils/models.py index 41503e91..42c71f0d 100644 --- a/backend/src/utils/models.py +++ b/backend/src/utils/models.py @@ -2,3 +2,5 @@ from src.auth import models as auth_models # noqa: F401 from src.events import models as event_models # noqa: F401 +from src.user_questions import models as user_question_models # noqa: F401 +from src.notes import models as note_models # noqa: F401