diff --git a/backend/src/routers/analysis_discussion_router.py b/backend/src/routers/analysis_discussion_router.py
new file mode 100644
index 00000000..b5f4c862
--- /dev/null
+++ b/backend/src/routers/analysis_discussion_router.py
@@ -0,0 +1,67 @@
+# 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)
+
+from ..dependencies import database
+from ..models.user import VerifyUser
+from ..security.security import get_current_user
+
+logger = logging.getLogger(__name__)
+
+router = APIRouter(tags=["analysis"], dependencies=[Depends(database)])
+
+
+def mock_discussion_fixture():
+ """Mock dicussion fixture to fake the discussions being returned"""
+ return [{
+ "post_id": "90jkflsd8d-6298-4afb-add5-6ef710eb5e98", "author_id": "3bghhsmnyqi6uxovazy07ryn9q1tqbnt",
+ "author_fullname": "Hello Person", "publish_timestamp": "2023-10-09T21:13:22.687000",
+ "content": "Nulla diam mi, semper vitae.", "attachments": [], "thread": []
+ }, {
+ "post_id": "a677fdsfsacf8-4ff9-a406-b113a7952f7e", "author_id": "kw0g790fdx715xsr1ead2jk0pqubtlyz",
+ "author_fullname": "Developer Person", "publish_timestamp": "2023-10-10T21:13:22.687000",
+ "content": "Morbi laoreet justo.", "attachments": [], "thread": []
+ }, {
+ "post_id": "e602ffdsfa-fdsfdsa-9f42-862c826255ef", "author_id": "exqkhvidr7uh2ndslsdymbzfbmqjlunk",
+ "author_fullname": "Variant Review Report Preparer Person", "publish_timestamp": "2023-10-13T21:13:22.687000",
+ "content": "Mauris pretium sem at nunc sollicitudin also.", "attachments": [], "thread": []
+ }]
+
+
+@router.get("/{analysis_name}/discussions")
+def get_analysis_discussions(analysis_name: str):
+ """ Returns a list of genomic units for a given analysis """
+ logger.info("Retrieving the analysis '%s' discussions ", analysis_name)
+ return mock_discussion_fixture()
+
+
+@router.post("/{analysis_name}/discussions")
+def add_analysis_discussions(
+ 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)
+
+ current_user = repositories["user"].find_by_client_id(client_id)
+
+ current_discussions = mock_discussion_fixture()
+ current_discussions.append({
+ "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 current_discussions
diff --git a/backend/src/routers/analysis_router.py b/backend/src/routers/analysis_router.py
index 93b15536..c5de595c 100644
--- a/backend/src/routers/analysis_router.py
+++ b/backend/src/routers/analysis_router.py
@@ -21,10 +21,13 @@
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)])
+logger.info('including the api roter for discussions')
+router.include_router(analysis_discussion_router.router)
@router.get("/", response_model=List[Analysis])
diff --git a/frontend/src/components/AnalysisView/DiscussionPost.vue b/frontend/src/components/AnalysisView/DiscussionPost.vue
new file mode 100644
index 00000000..a01b5662
--- /dev/null
+++ b/frontend/src/components/AnalysisView/DiscussionPost.vue
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/components/AnalysisView/DiscussionSection.vue b/frontend/src/components/AnalysisView/DiscussionSection.vue
index 9c665103..a2a1f631 100644
--- a/frontend/src/components/AnalysisView/DiscussionSection.vue
+++ b/frontend/src/components/AnalysisView/DiscussionSection.vue
@@ -4,37 +4,116 @@
- Content
+
+
+
+
+
+
+
+
diff --git a/frontend/src/models/analyses.js b/frontend/src/models/analyses.js
index ccf46ccc..2c57c2dc 100644
--- a/frontend/src/models/analyses.js
+++ b/frontend/src/models/analyses.js
@@ -212,7 +212,16 @@ export default {
const success = await Requests.putForm(url, attachmentForm);
return success;
},
+ async postNewDiscussionThread(analysisName, postContent) {
+ const url = `/rosalution/api/analysis/${analysisName}/discussions`;
+ const attachmentForm = {
+ 'discussion_content': postContent,
+ };
+
+ const success = await Requests.postForm(url, attachmentForm);
+ return success;
+ },
};
const annotationRenderingTemporary = [
diff --git a/frontend/src/stores/authStore.js b/frontend/src/stores/authStore.js
index c4fccb54..4761890f 100644
--- a/frontend/src/stores/authStore.js
+++ b/frontend/src/stores/authStore.js
@@ -33,6 +33,9 @@ const authStore = {
getUsername() {
return this.state.username;
},
+ getClientId() {
+ return this.state.clientId;
+ },
hasWritePermissions() {
return this.hasRole('write');
},
diff --git a/frontend/src/styles/main.css b/frontend/src/styles/main.css
index 3c582f0e..3e11f4ee 100644
--- a/frontend/src/styles/main.css
+++ b/frontend/src/styles/main.css
@@ -54,8 +54,6 @@ html {
--p-28: 1.75rem;
scroll-padding-top: 4rem;
-
-
}
/** || General Styles **/
diff --git a/frontend/src/views/AnalysisView.vue b/frontend/src/views/AnalysisView.vue
index ac009ee4..7d34d6bd 100644
--- a/frontend/src/views/AnalysisView.vue
+++ b/frontend/src/views/AnalysisView.vue
@@ -41,6 +41,8 @@
/>
{
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = shallowMount(DiscussionPost, {
+ global: {
+ components: {
+ 'font-awesome-icon': FontAwesomeIcon,
+ },
+ },
+ });
+ });
+
+ it('Vue instance exists and it is an object', () => {
+ expect(typeof wrapper).toBe('object');
+ });
+});
diff --git a/frontend/test/components/AnalysisView/DiscussionSection.spec.js b/frontend/test/components/AnalysisView/DiscussionSection.spec.js
index 5f40a389..fa6a7587 100644
--- a/frontend/test/components/AnalysisView/DiscussionSection.spec.js
+++ b/frontend/test/components/AnalysisView/DiscussionSection.spec.js
@@ -1,4 +1,4 @@
-import {it, expect, describe, beforeEach} from 'vitest';
+import {it, expect, describe, beforeEach, vi} from 'vitest';
import {shallowMount} from '@vue/test-utils';
import DiscussionSection from '../../../src/components/AnalysisView/DiscussionSection.vue';
@@ -20,4 +20,24 @@ describe('DiscussionSection.vue', () => {
it('Vue instance exists and it is an object', () => {
expect(typeof wrapper).toBe('object');
});
+
+ it('Should emit a discussion:new-post event when the publish button is pressed', async () => {
+ await wrapper.setData({newPostContent: 'Test post content'});
+
+ const publishNewDiscussionButton = wrapper.find('[data-test=new-discussion-publish]');
+ await publishNewDiscussionButton.trigger('click');
+
+ const emittedObjects = wrapper.emitted()['discussion:new-post'][0];
+
+ expect(emittedObjects[0]).toBe('Test post content');
+ });
+
+ // Placeholder test
+ it('Should print to console when the cancelled button is pressed', async () => {
+ const cancelNewDiscussionButton = wrapper.find('[data-test=new-discussion-cancel]');
+
+ vi.spyOn(console, 'log');
+ await cancelNewDiscussionButton.trigger('click');
+ expect(console.log).toHaveBeenCalled();
+ });
});
diff --git a/frontend/test/views/AnalysisView.spec.js b/frontend/test/views/AnalysisView.spec.js
index 9442dc0d..8983dc9a 100644
--- a/frontend/test/views/AnalysisView.spec.js
+++ b/frontend/test/views/AnalysisView.spec.js
@@ -7,6 +7,7 @@ import Analyses from '@/models/analyses.js';
import GeneBox from '@/components/AnalysisView/GeneBox.vue';
import InputDialog from '@/components/Dialogs/InputDialog.vue';
import NotificationDialog from '@/components/Dialogs/NotificationDialog.vue';
+import DiscussionSection from '@/components/AnalysisView/DiscussionSection.vue';
import SupplementalFormList from '@/components/AnalysisView/SupplementalFormList.vue';
import SaveModal from '@/components/AnalysisView/SaveModal.vue';
@@ -89,6 +90,7 @@ describe('AnalysisView', () => {
let mockedAttachThirdPartyLink;
let markReadyMock;
let updateAnalysisSectionsMock;
+ let postNewDiscussionThreadMock;
let mockAuthWritePermissions;
let mockedAttachSectionSupportingEvidence;
let mockedRemoveSectionSupportingEvidenceFile;
@@ -115,6 +117,8 @@ describe('AnalysisView', () => {
updateAnalysisSectionsMock = sandbox.stub(Analyses, 'updateAnalysisSections');
+ postNewDiscussionThreadMock = sandbox.stub(Analyses, 'postNewDiscussionThread');
+
mockAuthWritePermissions = sandbox.stub(authStore, 'hasWritePermissions');
mockAuthWritePermissions.returns(true);
@@ -314,6 +318,46 @@ describe('AnalysisView', () => {
});
});
+ describe('discussions', () => {
+ it('Should display a discussion section with three posts', () => {
+ const discussionSectionComponent = wrapper.getComponent(DiscussionSection);
+
+ expect(typeof discussionSectionComponent).toBe('object');
+
+ expect(discussionSectionComponent.props('discussions').length).to.equal(3);
+ });
+
+ it('Should recieve an new post publish emit and add a new discussion post', async () => {
+ const discussionSectionComponent = wrapper.getComponent(DiscussionSection);
+ const newPostContent = 'Hello world';
+
+ const discussionFixtureData = fixtureData()['discussions'];
+
+ const newDiscussionPost = {
+ post_id: 'e60239a34-h941-44aa-912e-912a993255fe',
+ author_id: 'exqkhvidr7uh2ndslsdymbzfbmqjlunk',
+ author_fullname: 'Variant Review Report Preparer Person',
+ publish_timestamp: '2023-11-01T21:13:22.687000',
+ content: newPostContent,
+ attachments: [],
+ thread: [],
+ };
+
+ discussionFixtureData.push(newDiscussionPost);
+
+ postNewDiscussionThreadMock.returns(discussionFixtureData);
+
+ expect(discussionSectionComponent.props('discussions').length).to.equal(3);
+
+ discussionSectionComponent.vm.$emit('discussion:new-post', newPostContent);
+
+ await wrapper.vm.$nextTick();
+ await wrapper.vm.$nextTick();
+
+ expect(discussionSectionComponent.props('discussions').length).to.equal(4);
+ });
+ });
+
describe('supporting evidence', () => {
describe('when adding supporting evidence as an attachment', () => {
it('displays the attachment modal when the supplemental form list requests dialog', async () => {
@@ -843,6 +887,35 @@ function fixtureData() {
],
},
],
+ discussions: [
+ {
+ 'post_id': '9027ec8d-6298-4afb-add5-6ef710eb5e98',
+ 'author_id': '3bghhsmnyqi6uxovazy07ryn9q1tqbnt',
+ 'author_fullname': 'Developer Person',
+ 'publish_timestamp': '2023-10-09T21:13:22.687000',
+ 'content': 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
+ 'attachments': [],
+ 'thread': [],
+ },
+ {
+ 'post_id': 'a677bb36-acf8-4ff9-a406-b113a7952f7e',
+ 'author_id': 'kw0g790fdx715xsr1ead2jk0pqubtlyz',
+ 'author_fullname': 'Researcher Person',
+ 'publish_timestamp': '2023-10-10T21:13:22.687000',
+ 'content': 'Mauris at mauris eu neque varius suscipit.',
+ 'attachments': [],
+ 'thread': [],
+ },
+ {
+ 'post_id': 'e6023fa7-b598-416a-9f42-862c826255ef',
+ 'author_id': 'exqkhvidr7uh2ndslsdymbzfbmqjlunk',
+ 'author_fullname': 'Variant Review Report Preparer Person',
+ 'publish_timestamp': '2023-10-13T21:13:22.687000',
+ 'content': 'Mauris at mauris eu neque varius suscipit.',
+ 'attachments': [],
+ 'thread': [],
+ },
+ ],
supporting_evidence_files: [
{
name: 'fake.txt',
diff --git a/system-tests/e2e/discussions_analysis.cy.js b/system-tests/e2e/discussions_analysis.cy.js
new file mode 100644
index 00000000..6e692de4
--- /dev/null
+++ b/system-tests/e2e/discussions_analysis.cy.js
@@ -0,0 +1,19 @@
+describe('discussions_analysis.cy.js', () => {
+ beforeEach(() => {
+ cy.resetDatabase();
+ cy.visit('/');
+ cy.get('.analysis-card').first().click();
+ cy.get('[href="#Discussion"]').click();
+ });
+
+ it('should publish a new post to the discussion section', () => {
+ cy.get('#Discussion').should('exist');
+
+ cy.get('[data-test="discussion-post"]').should('have.length', 3);
+
+ cy.get('[data-test="new-discussion-input"]').type("System Test Text");
+ cy.get('[data-test="new-discussion-publish"]').click();
+
+ cy.get('[data-test="discussion-post"]').should('have.length', 4);
+ })
+});
\ No newline at end of file