Skip to content

Commit fd90762

Browse files
authored
adds comment logic (#59)
* Adds creation and collection logic * Update & delte logic * Adds more comment options * Adds id information to websocket stuff * Adds export import for comments * Fixes import export mapping issues * Resolves pr comments
1 parent d156239 commit fd90762

File tree

17 files changed

+251
-44
lines changed

17 files changed

+251
-44
lines changed

api/transfer.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ async def post(self, request) -> PlainTextResponse:
5454
init_file_import(task, project_id, is_global_update)
5555
except Exception:
5656
file_import_error_handling(task, project_id, is_global_update)
57-
notification.send_organization_update(project_id, "project_update", True)
57+
notification.send_organization_update(
58+
project_id, f"project_update:{project_id}", True
59+
)
5860
return PlainTextResponse("OK")
5961

6062

controller/auth/manager.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ def get_user_id_by_info(info) -> str:
3434
return get_user_by_info(info).id
3535

3636

37+
def get_user_role_by_info(info) -> str:
38+
return get_user_by_info(info).role
39+
40+
41+
def get_user_role_by_id(user_id: str) -> str:
42+
return user_manager.get_user(user_id).role
43+
44+
3745
def get_organization_by_user_id(user_id: str) -> Organization:
3846
organization: Organization = user_manager.get_or_create_user(user_id).organization
3947
if not organization:

controller/comment/manager.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from typing import Any, Dict, List, Optional, Union
2+
from controller.auth import kratos
3+
from controller.auth import manager as user_manager
24
from submodules.model import enums
35
from submodules.model.models import CommentData, User
46

@@ -25,7 +27,7 @@ def get_comments(
2527
user_id: str,
2628
xfkey: Optional[str] = None,
2729
project_id: Optional[str] = None,
28-
) -> str:
30+
) -> List[CommentData]:
2931

3032
try:
3133
xftype_parsed = enums.CommentCategory[xftype.upper()]
@@ -37,6 +39,36 @@ def get_comments(
3739
)
3840

3941

42+
def get_comment(xftype: str, user_id: str, comment_id: str) -> CommentData:
43+
# check to have some level of access control
44+
try:
45+
xftype_parsed = enums.CommentCategory[xftype.upper()]
46+
except KeyError:
47+
raise ValueError(f"Invalid comment category: {xftype}")
48+
49+
return comments.get_as_json(comment_id, user_id)
50+
51+
52+
def get_add_info(
53+
xftype: str,
54+
project_id: Optional[str] = None,
55+
xfkey: Optional[str] = None,
56+
) -> str:
57+
58+
try:
59+
xftype_parsed = enums.CommentCategory[xftype.upper()]
60+
except KeyError:
61+
raise ValueError(f"Invalid comment category: {xftype}")
62+
63+
values = comments.get_add_info_category(xftype_parsed, project_id, xfkey)
64+
65+
if xftype_parsed == enums.CommentCategory.USER:
66+
values = [
67+
{"id": r.id, "name": kratos.resolve_user_mail_by_id(r.id)} for r in values
68+
]
69+
return values
70+
71+
4072
def create_comment(
4173
xfkey: str,
4274
xftype: str,
@@ -49,7 +81,7 @@ def create_comment(
4981
xftype = enums.CommentCategory[xftype.upper()].value
5082
except KeyError:
5183
raise ValueError(f"Invalid comment type: {xftype}")
52-
comments.create(
84+
return comments.create(
5385
xfkey,
5486
xftype,
5587
comment,
@@ -76,3 +108,17 @@ def update_comment(
76108
if user.role != enums.UserRoles.ENGINEER.value and user.id != item.created_by:
77109
raise ValueError(f"Can't update comment")
78110
comments.change(item, changes, with_commit=True)
111+
return item
112+
113+
114+
def delete_comment(comment_id: str, user_id: str) -> CommentData:
115+
item = comments.get(comment_id)
116+
if not item:
117+
raise ValueError(f"Can't find comment")
118+
119+
if (
120+
item.created_by != user_id
121+
and user_manager.get_user_role_by_id(user_id) != enums.UserRoles.ENGINEER.value
122+
):
123+
raise ValueError(f"Can't delete comment")
124+
comments.remove(comment_id, with_commit=True)

controller/transfer/association_transfer_manager.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ def import_associations(
5454
description,
5555
type,
5656
)
57-
notification.send_organization_update(project_id, f"information_source_created")
57+
notification.send_organization_update(
58+
project_id, f"information_source_created:{str(information_source.id)}"
59+
)
5860

5961
attribute_names = list(indices[0].keys())
6062
attribute_list = attribute_manager.get_all_attributes_by_names(

controller/transfer/manager.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,10 @@ def export_records(
120120
return sql_df.to_json(orient="records")
121121

122122

123-
def export_project(project_id: str, export_options: Dict[str, bool]) -> str:
124-
return get_project_export_dump(project_id, export_options)
123+
def export_project(
124+
project_id: str, user_id: str, export_options: Dict[str, bool]
125+
) -> str:
126+
return get_project_export_dump(project_id, user_id, export_options)
125127

126128

127129
def export_knowledge_base(project_id: str, base_id: str) -> str:
@@ -145,13 +147,15 @@ def export_knowledge_base(project_id: str, base_id: str) -> str:
145147
)
146148

147149

148-
def prepare_project_export(project_id: str, export_options: Dict[str, bool]) -> bool:
150+
def prepare_project_export(
151+
project_id: str, user_id: str, export_options: Dict[str, bool]
152+
) -> bool:
149153
org_id = organization.get_id_by_project_id(project_id)
150154
objects = s3.get_bucket_objects(org_id, project_id + "/download/project_export_")
151155
for o in objects:
152156
s3.delete_object(org_id, o)
153157

154-
data = get_project_export_dump(project_id, export_options)
158+
data = get_project_export_dump(project_id, user_id, export_options)
155159
file_name_base = "project_export_" + datetime.now().strftime("%Y_%m_%d_%H_%M_%S")
156160
file_name_local = file_name_base + ".zip"
157161
file_name_download = project_id + "/download/" + file_name_local

controller/transfer/project_transfer_manager.py

Lines changed: 82 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
user,
2424
knowledge_term,
2525
weak_supervision,
26+
comments as comment,
2627
)
2728
from submodules.model.enums import NotificationType
2829
from controller.labeling_access_link import manager as link_manager
@@ -117,7 +118,7 @@ def import_sample_project(
117118
project_item.id,
118119
)
119120
notification.send_organization_update(
120-
project_item.id, "project_update", is_global=True
121+
project_item.id, f"project_update:{str(project_item.id)}", is_global=True
121122
)
122123
data = extract_first_zip_data(file_name)
123124
import_file(project_item.id, user_id, data)
@@ -129,14 +130,14 @@ def import_sample_project(
129130
)
130131

131132
notification.send_organization_update(
132-
project_item.id, "project_update", is_global=True
133+
project_item.id, f"project_update:{str(project_item.id)}", is_global=True
133134
)
134135
return project_item
135136

136137

137138
def import_file(
138139
project_id: str,
139-
user_id: str,
140+
import_user_id: str,
140141
data: Dict[str, Dict[str, Any]],
141142
task_id: Optional[str] = None,
142143
) -> None:
@@ -490,7 +491,7 @@ def import_file(
490491
)
491492
] = data_slice_object.id
492493
link_manager.generate_data_slice_access_link(
493-
project_id, user_id, data_slice_object.id, with_commit=False
494+
project_id, import_user_id, data_slice_object.id, with_commit=False
494495
)
495496

496497
for information_source_payload_item in data.get(
@@ -778,6 +779,42 @@ def import_file(
778779
"blacklisted",
779780
),
780781
)
782+
comment_data = data.get("comments")
783+
if comment_data:
784+
for comment_item in comment_data:
785+
old_xfkey = comment_item.get("xfkey")
786+
new_xfkey = None
787+
xftype = comment_item.get("xftype")
788+
if xftype == enums.CommentCategory.RECORD.value:
789+
new_xfkey = record_ids.get(old_xfkey)
790+
if xftype == enums.CommentCategory.LABELING_TASK.value:
791+
new_xfkey = labeling_task_ids.get(old_xfkey)
792+
if xftype == enums.CommentCategory.ATTRIBUTE.value:
793+
new_xfkey = attribute_ids_by_old_id.get(old_xfkey)
794+
if xftype == enums.CommentCategory.LABEL.value:
795+
new_xfkey = labeling_task_labels_ids.get(old_xfkey)
796+
if xftype == enums.CommentCategory.DATA_SLICE.value:
797+
new_xfkey = data_slice_ids.get(old_xfkey)
798+
if xftype == enums.CommentCategory.EMBEDDING.value:
799+
new_xfkey = embedding_ids.get(old_xfkey)
800+
if xftype == enums.CommentCategory.HEURISTIC.value:
801+
new_xfkey = information_source_ids.get(old_xfkey)
802+
if xftype == enums.CommentCategory.KNOWLEDGE_BASE.value:
803+
new_xfkey = knowledge_base_ids.get(old_xfkey)
804+
if not new_xfkey:
805+
continue
806+
807+
comment.create(
808+
xfkey=new_xfkey,
809+
xftype=comment_item.get("xftype"),
810+
comment=comment_item.get("comment"),
811+
created_by=import_user_id,
812+
project_id=project_id,
813+
order_key=comment_item.get("order_key"),
814+
is_markdown=comment_item.get("is_markdown"),
815+
created_at=comment_item.get("created_at"),
816+
is_private=comment_item.get("is_private"),
817+
)
781818

782819
general.commit()
783820

@@ -787,7 +824,7 @@ def import_file(
787824
):
788825
embedding_manager.create_embeddings_one_by_one(
789826
project_id,
790-
user_id,
827+
import_user_id,
791828
data.get(
792829
"embeddings_data",
793830
),
@@ -802,7 +839,9 @@ def import_file(
802839
logger.info(f"Finished import of project {project_id}")
803840

804841

805-
def get_project_export_dump(project_id: str, export_options: Dict[str, bool]) -> str:
842+
def get_project_export_dump(
843+
project_id: str, user_id: str, export_options: Dict[str, bool]
844+
) -> str:
806845
"""Exports data of a project in JSON-String format. Queries all useful database entries and
807846
puts them in a format which again fits for import. For some entities database joins
808847
are useful, so there are vanilla SQL statements in execute-blocks.
@@ -829,6 +868,7 @@ def get_project_export_dump(project_id: str, export_options: Dict[str, bool]) ->
829868
knowledge_bases = []
830869
weak_supervision_task = []
831870
terms = []
871+
comments = []
832872
# -------------------- READ OF ENTITIES BY SQLALCHEMY --------------------
833873

834874
if "basic project data" in export_options:
@@ -874,6 +914,38 @@ def get_project_export_dump(project_id: str, export_options: Dict[str, bool]) ->
874914
knowledge_bases = knowledge_base.get_all(project_id)
875915
terms = knowledge_term.get_terms_by_project_id(project_id)
876916

917+
if "comment data" in export_options:
918+
comments = []
919+
comments += comment.get_by_all_by_category(
920+
enums.CommentCategory.LABELING_TASK, user_id, None, project_id, True
921+
)
922+
comments += comment.get_by_all_by_category(
923+
enums.CommentCategory.ATTRIBUTE, user_id, None, project_id, True
924+
)
925+
comments += comment.get_by_all_by_category(
926+
enums.CommentCategory.LABEL, user_id, None, project_id, True
927+
)
928+
comments += comment.get_by_all_by_category(
929+
enums.CommentCategory.DATA_SLICE, user_id, None, project_id, True
930+
)
931+
if "records" in export_options:
932+
comments += comment.get_by_all_by_category(
933+
enums.CommentCategory.RECORD, user_id, None, project_id, True
934+
)
935+
# only makes sense with records
936+
if "embeddings" in export_options:
937+
comments += comment.get_by_all_by_category(
938+
enums.CommentCategory.EMBEDDING, user_id, None, project_id, True
939+
)
940+
if "information sources" in export_options:
941+
comments += comment.get_by_all_by_category(
942+
enums.CommentCategory.HEURISTIC, user_id, None, project_id, True
943+
)
944+
if "knowledge bases" in export_options:
945+
comments += comment.get_by_all_by_category(
946+
enums.CommentCategory.KNOWLEDGE_BASE, user_id, None, project_id, True
947+
)
948+
877949
# -------------------- FORMATTING OF ENTITIES --------------------
878950
project_details_data = {
879951
"id": project_item.id,
@@ -1026,6 +1098,9 @@ def get_project_export_dump(project_id: str, export_options: Dict[str, bool]) ->
10261098
for association_item in data_slice_record_association
10271099
]
10281100

1101+
# no need to format since db reteurns as json :)
1102+
comment_data = comments
1103+
10291104
# -------------------- READ AND FORMATTING OF ENTITIES WITH SQL JOINS --------------------
10301105

10311106
record_label_association_tokens_data = [
@@ -1116,6 +1191,7 @@ def get_project_export_dump(project_id: str, export_options: Dict[str, bool]) ->
11161191
"terms_data": terms_data,
11171192
"data_slice_data": data_slice_data,
11181193
"data_slice_record_association_data": data_slice_record_association_data,
1194+
"comments": comment_data,
11191195
}
11201196

11211197
logger.info(f"Finished export of project {project_id}")

graphql_api/mutation/comment.py

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class CreateComment(graphene.Mutation):
1010
class Arguments:
1111
comment = graphene.String(required=True)
1212
xftype = graphene.String(required=True)
13-
xfkey = graphene.String(required=True)
13+
xfkey = graphene.ID(required=True)
1414
project_id = graphene.ID(required=False)
1515
is_private = graphene.Boolean(required=False)
1616

@@ -31,7 +31,17 @@ def mutate(
3131
else:
3232
auth.check_admin_access(info)
3333
user_id = auth.get_user_id_by_info(info)
34-
manager.create_comment(xfkey, xftype, comment, user_id, project_id, is_private)
34+
item = manager.create_comment(
35+
xfkey, xftype, comment, user_id, project_id, is_private
36+
)
37+
if item and project_id:
38+
# without project_id its a admin dashboard comment -> no websocket integration planned atm
39+
# global notification since the data is collected globally -> further handling in frontend
40+
notification.send_organization_update(
41+
project_id,
42+
f"comment_created:{project_id}:{xftype}:{xfkey}:{str(item.id)}",
43+
True,
44+
)
3545
return CreateComment(ok=True)
3646

3747

@@ -56,21 +66,44 @@ def mutate(
5666
else:
5767
auth.check_admin_access(info)
5868
user = auth.get_user_by_info(info)
59-
manager.update_comment(comment_id, user, changes)
69+
item = manager.update_comment(comment_id, user, changes)
70+
if item and project_id:
71+
# without project_id its a admin dashboard comment -> no websocket integration planned atm
72+
# global notification since the data is collected globally -> further handling in frontend
73+
notification.send_organization_update(
74+
project_id,
75+
f"comment_updated:{project_id}:{comment_id}:{item.xftype}:{item.xfkey}",
76+
True,
77+
)
6078
return UpdateComment(ok=True)
6179

6280

6381
class DeleteComment(graphene.Mutation):
6482
class Arguments:
65-
link_id = graphene.ID(required=True)
83+
comment_id = graphene.ID(required=True)
6684
project_id = graphene.ID(required=False)
6785

6886
ok = graphene.Boolean()
6987

70-
def mutate(self, info, project_id: str, attribute_id: str):
88+
def mutate(
89+
self,
90+
info,
91+
comment_id: str,
92+
project_id: Optional[str] = None,
93+
):
7194
auth.check_demo_access(info)
72-
auth.check_project_access(info, project_id)
73-
manager.delete_attribute(project_id, attribute_id)
95+
if project_id:
96+
auth.check_project_access(info, project_id)
97+
else:
98+
auth.check_admin_access(info)
99+
user_id = auth.get_user_id_by_info(info)
100+
manager.delete_comment(comment_id, user_id)
101+
if project_id:
102+
# without project_id its a admin dashboard comment -> no websocket integration planned atm
103+
# global notification since the data is collected globally -> further handling in frontend
104+
notification.send_organization_update(
105+
project_id, f"comment_deleted:{project_id}:{comment_id}", True
106+
)
74107
return DeleteComment(ok=True)
75108

76109

0 commit comments

Comments
 (0)