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

[Feature Branch] Analysis Discussion Section - Phase 1 #161

Merged
merged 19 commits into from
Feb 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
2867937
[Frontend] Create a new discussion component to be displayed in an An…
JmScherer Sep 26, 2023
5eb3b21
Created a discussions property in each of the analyses with several p…
SeriousHorncat Sep 27, 2023
ad1bed5
Fixed frontend unit test with AnalysisView
JmScherer Nov 13, 2023
1adb132
fixed line too long
JmScherer Nov 15, 2023
6dd4d08
Missed the updated json
JmScherer Nov 17, 2023
b185d1f
Changed formatting on the /etc/fixtures/analyses.json
JmScherer Nov 17, 2023
f222ae7
removed the etc/.certificate files
JmScherer Nov 27, 2023
2c0f543
Displaying discussion posts (#150)
JmScherer Dec 1, 2023
7d6036f
[Frontend] Clicking the "New Discussion" button opens a field below t…
JmScherer Dec 5, 2023
954bfeb
[Backend] Add a new route and repository to handle an incoming post a…
JmScherer Dec 11, 2023
e14d556
New migration script to ensure both supporting_evidence_files and dis…
JmScherer Dec 15, 2023
7fcd650
Updated phenotips importer to include supporting_evidence_files and d…
JmScherer Dec 15, 2023
006b10e
Adding context menu to discussion posts (#156)
JmScherer Jan 12, 2024
c663af1
[Backend] Remove and edit post route and methods to update and delete…
JmScherer Jan 22, 2024
4371100
[Frontend] The vertical '...' context menu functions to delete posts …
JmScherer Jan 29, 2024
3219282
[Frontend] The vertical '...' context menu functions to edit posts (#…
JmScherer Feb 5, 2024
045503c
Update .github/workflows/system-tests.yml
JmScherer Feb 8, 2024
7c17049
Update backend/src/routers/analysis_router.py
JmScherer Feb 8, 2024
e39e555
skipping the a test in rosalution_analysis to match main, will come b…
JmScherer Feb 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/src/core/phenotips_importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ def import_phenotips_json(self, phenotips_json_data):

analysis_data = self.import_analysis_data(phenotips_json_data, phenotips_variants, phenotips_json_data["genes"])

analysis_data['discussions'] = []
analysis_data['supporting_evidence_files'] = []
analysis_data['timeline'] = []
return analysis_data

Expand Down
27 changes: 27 additions & 0 deletions backend/src/models/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ class Analysis(BaseAnalysis):

genomic_units: List[GenomicUnit] = []
sections: List[Section] = []
discussions: List = []
supporting_evidence_files: List = []

def units_to_annotate(self):
Expand All @@ -105,3 +106,29 @@ def units_to_annotate(self):
})

return units

def find_discussion_post(self, discussion_post_id):
"""
Finds a specific discussion post in an analysis by the discussion post id otherwise returns none
if no discussion with that post id in the analysis.
"""
for discussion in self.discussions:
if discussion['post_id'] == discussion_post_id:
return discussion

return None

def find_authored_discussion_post(self, discussion_post_id, client_id):
"""
Finds a discussion post from a user that authored the post in an analysis otherwise returns none if the post
was found, but the user did not author the post
"""
discussion_post = self.find_discussion_post(discussion_post_id)

if discussion_post is None:
raise ValueError(f"Post '{discussion_post_id}' does not exist.")

if discussion_post['author_id'] == client_id:
return discussion_post

return None
2 changes: 1 addition & 1 deletion backend/src/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ class User(BaseModel):
full_name: Optional[str] = None
disabled: Optional[bool] = None
scope: Optional[str] = None
client_id: str


class AccessUserAPI(User):
""" This extends the user class to include the user's credentials for API access """
client_id: str
client_secret: Optional[str] = None


Expand Down
36 changes: 36 additions & 0 deletions backend/src/repository/analysis_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -486,3 +486,39 @@ def remove_section_supporting_evidence(self, analysis_name: str, section_name: s
return_field = {"header": section_name, "field": field_name}

return return_field

def add_discussion_post(self, analysis_name: str, discussion_post: object):
""" Appends a new discussion post to an analysis """

updated_document = self.collection.find_one_and_update({"name": analysis_name},
{"$push": {"discussions": discussion_post}},
return_document=ReturnDocument.AFTER)

updated_document.pop("_id", None)

return updated_document['discussions']

def updated_discussion_post(self, discussion_post_id: str, discussion_content: str, analysis_name: str):
""" Edits a discussion post from an analysis to update the discussion post's content """

updated_document = self.collection.find_one_and_update({"name": analysis_name}, {
"$set": {"discussions.$[item].content": discussion_content}
},
array_filters=[{"item.post_id": discussion_post_id}],
return_document=ReturnDocument.AFTER)

updated_document.pop("_id", None)

return updated_document['discussions']

def delete_discussion_post(self, discussion_post_id: str, analysis_name: str):
""" Removes a discussion post from an analysis """

updated_document = self.collection.find_one_and_update({"name": analysis_name}, {
"$pull": {"discussions": {"post_id": discussion_post_id}}
},
return_document=ReturnDocument.AFTER)

updated_document.pop("_id", None)

return updated_document['discussions']
139 changes: 139 additions & 0 deletions backend/src/routers/analysis_discussion_router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# pylint: disable=too-many-arguments
# Due to adding scope checks, it's adding too many arguments (7/6) to functions, so diabling this for now.
# Need to refactor later.
""" Analysis endpoint routes that provide an interface to interact with an Analysis' discussions """
from datetime import datetime, timezone
import logging
from uuid import uuid4

from fastapi import (APIRouter, Depends, Form, Security, HTTPException, status)

from ..dependencies import database
from ..models.user import VerifyUser
from ..models.analysis import Analysis
from ..security.security import get_current_user

logger = logging.getLogger(__name__)

router = APIRouter(tags=["analysis"], dependencies=[Depends(database)])


@router.get("/{analysis_name}/discussions")
def get_analysis_discussions(analysis_name: str, repositories=Depends(database)):
""" Returns a list of discussion posts for a given analysis """
logger.info("Retrieving the analysis '%s' discussions ", analysis_name)

found_analysis = repositories['analysis'].find_by_name(analysis_name)

if not found_analysis:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=f"Analysis '{analysis_name}' does not exist.'"
)

analysis = Analysis(**found_analysis)

return analysis.discussions


@router.post("/{analysis_name}/discussions")
def add_analysis_discussion(
analysis_name: str,
discussion_content: str = Form(...),
repositories=Depends(database),
client_id: VerifyUser = Security(get_current_user)
):
""" Adds a new analysis topic """
logger.info("Adding the analysis '%s' from user '%s'", analysis_name, client_id)
logger.info("The message: %s", discussion_content)

found_analysis = repositories['analysis'].find_by_name(analysis_name)

if not found_analysis:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=f"Analysis '{analysis_name}' does not exist.'"
)

analysis = Analysis(**found_analysis)
current_user = repositories["user"].find_by_client_id(client_id)

new_discussion_post = {
"post_id": str(uuid4()),
"author_id": client_id,
"author_fullname": current_user["full_name"],
"publish_timestamp": datetime.now(timezone.utc),
"content": discussion_content,
"attachments": [],
"thread": [],
}

return repositories['analysis'].add_discussion_post(analysis.name, new_discussion_post)


@router.put("/{analysis_name}/discussions/{discussion_post_id}")
def update_analysis_discussion_post(
analysis_name: str,
discussion_post_id: str,
discussion_content: str = Form(...),
repositories=Depends(database),
client_id: VerifyUser = Security(get_current_user)
):
""" Updates a discussion post's content in an analysis by the discussion post id """
logger.info(
"Editing post '%s' by user '%s' from the analysis '%s' with new content: '%s'", discussion_post_id, client_id,
analysis_name, discussion_content
)

found_analysis = repositories['analysis'].find_by_name(analysis_name)

if not found_analysis:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Analysis '{analysis_name}' does not exist. Unable to update discussion post.'"
)

analysis = Analysis(**found_analysis)

try:
valid_post = analysis.find_authored_discussion_post(discussion_post_id, client_id)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e)) from e

if not valid_post:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="User cannot update post they did not author."
)

return repositories['analysis'].updated_discussion_post(valid_post['post_id'], discussion_content, analysis.name)


@router.delete("/{analysis_name}/discussions/{discussion_post_id}")
def delete_analysis_discussion(
analysis_name: str,
discussion_post_id: str,
repositories=Depends(database),
client_id: VerifyUser = Security(get_current_user)
):
""" Deletes a discussion post in an analysis by the discussion post id """
logger.info("Deleting post %s by user '%s' from the analysis '%s'", discussion_post_id, client_id, analysis_name)

found_analysis = repositories['analysis'].find_by_name(analysis_name)

if not found_analysis:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Analysis '{analysis_name}' does not exist. Unable to delete discussion post.'"
)

analysis = Analysis(**found_analysis)

try:
valid_post = analysis.find_authored_discussion_post(discussion_post_id, client_id)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e)) from e

if not valid_post:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="User cannot delete post they did not author."
)

return repositories['analysis'].delete_discussion_post(valid_post['post_id'], analysis.name)
2 changes: 2 additions & 0 deletions backend/src/routers/analysis_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@
from ..models.phenotips_json import BasePhenotips
from ..models.user import VerifyUser
from ..security.security import get_authorization, get_current_user
from . import analysis_discussion_router

logger = logging.getLogger(__name__)

router = APIRouter(prefix="/analysis", tags=["analysis"], dependencies=[Depends(database)])
router.include_router(analysis_discussion_router.router)


@router.get("/", response_model=List[Analysis])
Expand Down
1 change: 1 addition & 0 deletions backend/tests/fixtures/analysis-CPAM0002.json
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@
]
}
],
"discussions":[],
"supporting_evidence_files": [
{
"name": "test.txt",
Expand Down
1 change: 1 addition & 0 deletions backend/tests/fixtures/empty-pedigree.json
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@
]
}
],
"discussions":[],
"supporting_evidence_files": [
{
"name": "test.txt",
Expand Down
2 changes: 1 addition & 1 deletion backend/tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def mock_current_user():
@pytest.fixture(name="mock_user")
def test_auth_user():
"""A mocked user that can be used to generate an OAuth2 access token"""
return {"sub": "johndoe", "scopes": ["read", "write"]}
return {"sub": "johndoe-client-id", "scopes": ["read", "write"]}


@pytest.fixture(name="mock_access_token")
Expand Down
Loading
Loading