Skip to content

Commit

Permalink
[Feature Branch] Analysis Discussion Section - Phase 1 (#161)
Browse files Browse the repository at this point in the history
* [Frontend] Create a new discussion component to be displayed in an Analysis view (#139)

* Updating the paper to include the country for each affiliation which is required for publication.

* Updated the system tests workflow to use electron (#140)

* Added a DiscussionsSection.vue in AnalysisView components, updated the analysis model to return an injected discussion object, and updating the styling of the buttons to match figma more closely

* The discussion collapse works

* Looks ready for a pull request

* Tests should be working now

* Left out the node test

* Updating CSS in a few places

* Updating the DiscussionSection values

---------

Co-authored-by: Angelina Uno-Antonison <ange.unoantonison@gmail.com>

* Created a discussions property in each of the analyses with several posts within them. Removed the temp fixture used in the frontend (#141)

* Fixed frontend unit test with AnalysisView

* fixed line too long

* Missed the updated json

* Changed formatting on the /etc/fixtures/analyses.json

* removed the etc/.certificate files

* Displaying discussion posts (#150)

* New Discussions post button, text field, and save/publish buttons

* Creating temporary discussion API mock api endpoints to test integrating them into the frontend

* Displaying posts from an analysis

* Hooked up the discussion post to the backend and return a mock discussions post. Added styling to the discussion posts

* Updated CSS to alternate discussion post colors

* Added more unit test coverage on the frontend and linted

* Added system tests and data-test attributes to go along with it

---------

Co-authored-by: Angelina Uno-Antonison <ange.unoantonison@gmail.com>

* [Frontend] Clicking the "New Discussion" button opens a field below the header line divider to enter an opinion (#151)

* New Discussions post button, text field, and save/publish buttons

* Displaying posts from an analysis

* Added more unit test coverage on the frontend and linted

* Toggles New Discussion Field

* added system and unit tests for the discussion section

* forgot to add the system test

* Minor CSS fixes

* [Backend] Add a new route and repository to handle an incoming post and save it in Mongo (#153)

* Added a new collection to update a new post along with a test and linting

* Fixing python unit test

* Integration test and linting

* Updated the analysis_collection add_discussion_post function to use pymongo find_one_and_update function to simplify how posts are added to discussions in analyses

* linting

* New migration script to ensure both supporting_evidence_files and discussions keys are added to each analysis object in the analyses collection (#154)

* Updated phenotips importer to include supporting_evidence_files and discussions field. Updated the analyses fixture to include these fields as well (#155)

* Adding context menu to discussion posts (#156)

* added actions to the discussion post context menus

* Changed the backend user object to send back the clientId in the basic user object, this is used to check if the user made a post and present a context menu

* new ContextMenu.vue duplicates the DropDownMenu.vue functionality and turns it from hover to click. This is not quite right

* lots of changes: swapped dropdown and contextmenu icons, contextmenu will now open on click and close when unfocused or an action is taken on the menu. ContextMenu styling changes

* Fixed frontend test

* Lots of fixes, trying to figure out how to test this

* Frontend linting

* Removed a .only in front end test

* [Backend] Remove and edit post route and methods to update and delete a post from the discussion section (#157)

* Added a delete route and delete post analysis collection function

* Added a new route for editing a post and matching collection function, also added error checking for different situations in modifying discussion posts in an analysis

* Change responsibilities of routes and put helper function to find discussion post in analysis model

* proper error handling for discussion posts in progress

* Removed the discussion fixtures and properly gets the discussion posts from the analysis

* Finished integration and unit tests for updating and deleting a discussion post

* Formatting/linting

* Fixed integration test

* Added unit and integration tests for analysis model and routers

* Update backend/src/routers/analysis_discussion_router.py

Co-authored-by: Angelina Uno-Antonison <ange.unoantonison@gmail.com>
Signed-off-by: James Scherer <james.m.scherer@gmail.com>

* Update backend/tests/integration/test_analysis_routers.py

Co-authored-by: Angelina Uno-Antonison <ange.unoantonison@gmail.com>
Signed-off-by: James Scherer <james.m.scherer@gmail.com>

* linting

---------

Signed-off-by: James Scherer <james.m.scherer@gmail.com>
Co-authored-by: Angelina Uno-Antonison <ange.unoantonison@gmail.com>

* [Frontend] The vertical '...' context menu functions to delete posts (#158)

* Frontend discussion post deletion working, complete with notification dialog

* Adding a system test for successfully deleting a new discussion post

* fixing frontend unit tests and adding an extra system test

* Linting

* Changed the delete action in the context menu to have an emit key and chain the emits down the section to be called in the view to delete a post

* Linting and changing var names

* Lots of frontend unit tests, but not yet finished

* Added more tests and linting

* Fixed issue with contextId being renamed

* Removed console log

* [Frontend] The vertical '...' context menu functions to edit posts (#160)

* Frontend discussion post deletion working, complete with notification dialog

* Adding a system test for successfully deleting a new discussion post

* fixing frontend unit tests and adding an extra system test

* Linting

* Changed the delete action in the context menu to have an emit key and chain the emits down the section to be called in the view to delete a post

* Linting and changing var names

* Lots of frontend unit tests, but not yet finished

* Added more tests and linting

* Fixed issue with contextId being renamed

* Removed console log

* Edit post emit chain working and backend is saving properly, it's just the discussion content is the same as what's posted

* Now editing a post works, it edits the post in place

* More styling for the post editing

* Frontend linting

* Frontend tests and linting

* Added system tests for post editing

* Update .github/workflows/system-tests.yml

Co-authored-by: Angelina Uno-Antonison <ange.unoantonison@gmail.com>
Signed-off-by: James Scherer <james.m.scherer@gmail.com>

* Update backend/src/routers/analysis_router.py

Co-authored-by: Angelina Uno-Antonison <ange.unoantonison@gmail.com>
Signed-off-by: James Scherer <james.m.scherer@gmail.com>

* skipping the a test in rosalution_analysis to match main, will come back later

---------

Signed-off-by: James Scherer <james.m.scherer@gmail.com>
Co-authored-by: Angelina Uno-Antonison <ange.unoantonison@gmail.com>
  • Loading branch information
JmScherer and SeriousHorncat committed Feb 9, 2024
1 parent d0ce762 commit 82cda52
Show file tree
Hide file tree
Showing 32 changed files with 3,569 additions and 1,845 deletions.
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

0 comments on commit 82cda52

Please sign in to comment.