Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: data cell + table comments #1279

Merged
merged 14 commits into from
Jun 27, 2023
Merged
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "querybook",
"version": "3.25.3",
"version": "3.26.0",
"description": "A Big Data Webapp",
"private": true,
"scripts": {
Expand Down
79 changes: 79 additions & 0 deletions querybook/migrations/versions/4c70dae378f2_add_comment_feature.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""add comment feature

Revision ID: 4c70dae378f2
Revises: 7f6cdb3621f7
Create Date: 2023-06-21 19:45:30.708378

"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql

# revision identifiers, used by Alembic.
revision = "4c70dae378f2"
down_revision = "7f6cdb3621f7"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"comment",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=True),
sa.Column("updated_at", sa.DateTime(), nullable=True),
sa.Column("created_by", sa.Integer(), nullable=True),
sa.Column("text", sa.Text(length=16777215), nullable=True),
sa.Column("parent_comment_id", sa.Integer(), nullable=True),
sa.Column("archived", sa.Boolean(), nullable=False),
sa.ForeignKeyConstraint(["created_by"], ["user.id"], ondelete="SET NULL"),
sa.ForeignKeyConstraint(
["parent_comment_id"], ["comment.id"], ondelete="CASCADE"
),
sa.PrimaryKeyConstraint("id"),
mysql_charset="utf8mb4",
mysql_engine="InnoDB",
)
op.create_table(
"comment_reaction",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("comment_id", sa.Integer(), nullable=False),
sa.Column("reaction", sa.String(length=255), nullable=False),
sa.Column("created_by", sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(["comment_id"], ["comment.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["created_by"], ["user.id"], ondelete="SET NULL"),
sa.PrimaryKeyConstraint("id"),
mysql_charset="utf8mb4",
mysql_engine="InnoDB",
)
op.create_table(
"data_cell_comment",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("data_cell_id", sa.Integer(), nullable=False),
sa.Column("comment_id", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(["comment_id"], ["comment.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["data_cell_id"], ["data_cell.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"data_table_comment",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("data_table_id", sa.Integer(), nullable=False),
sa.Column("comment_id", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(["comment_id"], ["comment.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(
["data_table_id"], ["data_table.id"], ondelete="CASCADE"
),
sa.PrimaryKeyConstraint("id"),
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("data_table_comment")
op.drop_table("data_cell_comment")
op.drop_table("comment_reaction")
op.drop_table("comment")
# ### end Alembic commands ###
14 changes: 14 additions & 0 deletions querybook/server/const/comment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from typing import List, Optional, TypedDict
from models.comment import CommentReaction


class CommentDict(TypedDict):
id: int
created_at: int
updated_at: int
created_by: str
text: str
parent_comment_id: Optional[int]
archived: bool
child_comment_ids: Optional[List[int]]
reactions: List[CommentReaction]
2 changes: 2 additions & 0 deletions querybook/server/datasources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from . import tag
from . import event_log
from . import data_element
from . import comment
from . import ai_assistant

# Keep this at the end of imports to make sure the plugin APIs override the default ones
Expand All @@ -41,5 +42,6 @@
tag
event_log
data_element
comment
ai_assistant
api_plugin
103 changes: 103 additions & 0 deletions querybook/server/datasources/comment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
from flask_login import current_user
from app.datasource import register
from logic import comment as logic
from app.auth.permission import verify_data_table_permission
from logic.comment_permission import (
assert_can_edit_and_delete,
assert_can_read_datadoc,
)


@register(
"/data_cell/<int:data_cell_id>/comment/",
methods=["GET"],
)
def get_comments_by_cell_id(data_cell_id: int):
return logic.get_comments_by_data_cell_id(data_cell_id=data_cell_id)


@register(
"/data_table/<int:data_table_id>/comment/",
methods=["GET"],
)
def get_comments_by_table_id(data_table_id: int):
return logic.get_comments_by_data_table_id(data_table_id=data_table_id)


@register(
"/data_cell/<int:data_cell_id>/comment/",
methods=["POST"],
)
def add_comment_to_cell(data_cell_id: int, text):
assert_can_read_datadoc(data_cell_id=data_cell_id)
return logic.add_comment_to_data_cell(
data_cell_id=data_cell_id, uid=current_user.id, text=text
)


@register(
"/data_table/<int:data_table_id>/comment/",
methods=["POST"],
)
def add_comment_to_table(data_table_id: int, text):
verify_data_table_permission(table_id=data_table_id)
return logic.add_comment_to_data_table(
data_table_id=data_table_id,
uid=current_user.id,
text=text,
)


@register(
"/comment/<int:parent_comment_id>/thread/",
methods=["GET"],
)
def get_thread_comments(parent_comment_id: int):
return logic.get_thread_comments(parent_comment_id=parent_comment_id)


@register(
"/comment/<int:parent_comment_id>/thread/",
methods=["POST"],
)
def add_thread_comment(parent_comment_id: int, text):
return logic.add_thread_comment(
parent_comment_id=parent_comment_id, uid=current_user.id, text=text
)


@register(
"/comment/<int:comment_id>/",
methods=["PUT"],
)
def edit_comment_text(comment_id: int, **fields):
assert_can_edit_and_delete(comment_id=comment_id)
return logic.edit_comment(comment_id=comment_id, **fields)


@register("/comment/<int:comment_id>/", methods=["DELETE"])
def soft_delete_comment(comment_id: int):
assert_can_edit_and_delete(comment_id=comment_id)
logic.edit_comment(comment_id=comment_id, archived=True)
return


# reactions
@register(
"/comment/<int:comment_id>/reaction/",
methods=["POST"],
)
def add_reaction(comment_id: int, reaction: str):
return logic.add_reaction(
comment_id=comment_id,
reaction=reaction,
uid=current_user.id,
)


@register(
"/comment/<int:reaction_id>/reaction/",
methods=["DELETE"],
)
def remove_reaction(reaction_id: int):
return logic.remove_reaction(reaction_id=reaction_id)
108 changes: 108 additions & 0 deletions querybook/server/logic/comment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
from app.db import with_session
from models.comment import Comment, CommentReaction, DataTableComment, DataCellComment


@with_session
def get_comment_by_id(comment_id: int, session=None):
return Comment.get(id=comment_id, session=session)


@with_session
def get_comments_by_data_cell_id(data_cell_id: int, session=None):
cell_comments = (
session.query(DataCellComment)
.filter(DataCellComment.data_cell_id == data_cell_id)
.all()
)

return [
get_comment_by_id(cell_comment.comment_id, session=session)
for cell_comment in cell_comments
]


@with_session
def get_comments_by_data_table_id(data_table_id: int, session=None):
table_comments = (
session.query(DataTableComment)
.filter(DataTableComment.data_table_id == data_table_id)
.all()
)

return [
get_comment_by_id(table_comment.comment_id, session=session)
for table_comment in table_comments
]


@with_session
def add_comment_to_data_cell(data_cell_id: int, uid: int, text, session=None):
comment = Comment.create(
{"created_by": uid, "text": text}, commit=False, session=session
)
DataCellComment.create(
{"data_cell_id": data_cell_id, "comment_id": comment.id},
commit=False,
session=session,
)
session.commit()
return comment


@with_session
def add_comment_to_data_table(data_table_id: int, uid: int, text, session=None):
comment = Comment.create(
{"created_by": uid, "text": text}, commit=False, session=session
)
DataTableComment.create(
{"data_table_id": data_table_id, "comment_id": comment.id},
commit=False,
session=session,
)
session.commit()
return comment


@with_session
def get_thread_comments(parent_comment_id: int, session=None):
return (
session.query(Comment)
.filter(Comment.parent_comment_id == parent_comment_id)
.order_by(Comment.created_at)
.all()
)


@with_session
def add_thread_comment(parent_comment_id: int, uid: int, text, session=None):
return Comment.create(
{"created_by": uid, "text": text, "parent_comment_id": parent_comment_id},
session=session,
)


@with_session
def edit_comment(comment_id: int, session=None, **fields):
return Comment.update(
id=comment_id,
fields=fields,
field_names=["text", "archived"],
commit=True,
session=session,
)


@with_session
def add_reaction(comment_id: int, reaction: str, uid: int, session=None):
return CommentReaction.create(
{"comment_id": comment_id, "reaction": reaction, "created_by": uid},
session=session,
)


@with_session
def remove_reaction(reaction_id: int, session=None):
CommentReaction.delete(
reaction_id,
session=session,
)
40 changes: 40 additions & 0 deletions querybook/server/logic/comment_permission.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from flask_login import current_user
from app.datasource import api_assert
from app.db import with_session
from const.datasources import (
RESOURCE_NOT_FOUND_STATUS_CODE,
UNAUTHORIZED_STATUS_CODE,
)

from logic import comment as logic
from logic.datadoc_permission import assert_can_read
from models.datadoc import DataDocDataCell


class CommentDoesNotExist(Exception):
pass


@with_session
def assert_can_read_datadoc(data_cell_id, session=None):
data_cell = (
session.query(DataDocDataCell)
.filter(DataDocDataCell.data_cell_id == data_cell_id)
.first()
)
assert_can_read(data_cell.data_doc_id)


@with_session
def assert_can_edit_and_delete(comment_id, session=None):
try:
comment = logic.get_comment_by_id(comment_id=comment_id, session=session)
if comment is None:
raise CommentDoesNotExist
api_assert(
comment.created_by == current_user.id,
"NOT_COMMENT_AUTHOR",
UNAUTHORIZED_STATUS_CODE,
)
except CommentDoesNotExist:
api_assert(False, "COMMENT_DNE", RESOURCE_NOT_FOUND_STATUS_CODE)
1 change: 1 addition & 0 deletions querybook/server/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@
from .tag import *
from .event_log import *
from .data_element import *
from .comment import *
Loading