From 1ed71d0365b3014f87d970fb4e2ebad1d4762857 Mon Sep 17 00:00:00 2001 From: Jake Rosenberg Date: Wed, 29 Jan 2025 14:32:27 -0600 Subject: [PATCH] Serve published projects at a world-readable /publications URL --- .../CombinedBreadcrumbs.jsx | 1 + .../DataFilesDropdown/DataFilesDropdown.jsx | 3 +- .../DataFilesListing/DataFilesListing.jsx | 4 +- .../DataFilesListingCells.jsx | 3 +- .../DataFilesProjectFileListing.jsx | 3 +- .../DataFilesPublicationsList.jsx | 9 ++-- .../PublicationDetailPublicView.jsx | 43 +++++++++++++++++++ .../Publications/PublicationsPublicView.jsx | 14 ++++++ client/src/components/Workbench/AppRouter.jsx | 15 ++++++- .../DataFilesProjectFileListingAddon.jsx | 2 +- ...esProjectFileListingMetadataTitleAddon.jsx | 2 +- server/conf/nginx/nginx.conf | 2 +- server/portal/apps/datafiles/views.py | 6 ++- server/portal/apps/projects/views.py | 9 ++-- server/portal/urls.py | 1 + 15 files changed, 102 insertions(+), 15 deletions(-) create mode 100644 client/src/components/Publications/PublicationDetailPublicView.jsx create mode 100644 client/src/components/Publications/PublicationsPublicView.jsx diff --git a/client/src/components/DataFiles/CombinedBreadcrumbs/CombinedBreadcrumbs.jsx b/client/src/components/DataFiles/CombinedBreadcrumbs/CombinedBreadcrumbs.jsx index 7f5ce80c5..914773b18 100644 --- a/client/src/components/DataFiles/CombinedBreadcrumbs/CombinedBreadcrumbs.jsx +++ b/client/src/components/DataFiles/CombinedBreadcrumbs/CombinedBreadcrumbs.jsx @@ -20,6 +20,7 @@ CombinedBreadcrumbs.propTypes = { path: PropTypes.string.isRequired, section: PropTypes.string.isRequired, isPublic: PropTypes.bool, + basePath: PropTypes.string, className: PropTypes.string, }; diff --git a/client/src/components/DataFiles/DataFilesDropdown/DataFilesDropdown.jsx b/client/src/components/DataFiles/DataFilesDropdown/DataFilesDropdown.jsx index 33affeef4..e91ebd317 100644 --- a/client/src/components/DataFiles/DataFilesDropdown/DataFilesDropdown.jsx +++ b/client/src/components/DataFiles/DataFilesDropdown/DataFilesDropdown.jsx @@ -17,6 +17,7 @@ const BreadcrumbsDropdown = ({ scheme, system, path, + basePath, section, isPublic, }) => { @@ -36,7 +37,7 @@ const BreadcrumbsDropdown = ({ : null; const handleNavigation = (targetPath) => { - const basePath = isPublic ? '/public-data' : '/workbench/data'; + if (!basePath) basePath = isPublic ? '/public-data' : '/workbench/data'; let url; if (scheme === 'projects' && targetPath === systemName) { diff --git a/client/src/components/DataFiles/DataFilesListing/DataFilesListing.jsx b/client/src/components/DataFiles/DataFilesListing/DataFilesListing.jsx index 0552a10a3..98991cda1 100644 --- a/client/src/components/DataFiles/DataFilesListing/DataFilesListing.jsx +++ b/client/src/components/DataFiles/DataFilesListing/DataFilesListing.jsx @@ -45,6 +45,7 @@ const DataFilesListing = ({ path, isPublic, rootSystem, + basePath, }) => { // Redux hooks const location = useLocation(); @@ -54,7 +55,7 @@ const DataFilesListing = ({ ); const sharedWorkspaces = systems.find((e) => e.scheme === 'projects'); const isPortalProject = scheme === 'projects'; - const hideSearchBar = isPortalProject && sharedWorkspaces.hideSearchBar; + const hideSearchBar = isPortalProject && sharedWorkspaces?.hideSearchBar; const showViewPath = useSelector( (state) => @@ -101,6 +102,7 @@ const DataFilesListing = ({ scheme={scheme} href={row.original._links.self.href} isPublic={isPublic} + basePath={basePath} length={row.original.length} metadata={row.original.metadata} /> diff --git a/client/src/components/DataFiles/DataFilesListing/DataFilesListingCells.jsx b/client/src/components/DataFiles/DataFilesListing/DataFilesListingCells.jsx index c3d3593b7..7899e3f8f 100644 --- a/client/src/components/DataFiles/DataFilesListing/DataFilesListingCells.jsx +++ b/client/src/components/DataFiles/DataFilesListing/DataFilesListingCells.jsx @@ -59,6 +59,7 @@ export const FileNavCell = React.memo( scheme, href, isPublic, + basePath, length, metadata, rootSystem, @@ -80,7 +81,7 @@ export const FileNavCell = React.memo( }); }; - const basePath = isPublic ? '/public-data' : '/workbench/data'; + if (!basePath) basePath = isPublic ? '/public-data' : '/workbench/data'; return ( <> diff --git a/client/src/components/DataFiles/DataFilesProjectFileListing/DataFilesProjectFileListing.jsx b/client/src/components/DataFiles/DataFilesProjectFileListing/DataFilesProjectFileListing.jsx index cde79cdee..95698a10a 100644 --- a/client/src/components/DataFiles/DataFilesProjectFileListing/DataFilesProjectFileListing.jsx +++ b/client/src/components/DataFiles/DataFilesProjectFileListing/DataFilesProjectFileListing.jsx @@ -54,7 +54,7 @@ const DataFilesProjectFileListing = ({ rootSystem, system, path }) => { metadata.members .filter((member) => member.user - ? member.user.username === state.authenticatedUser.user.username + ? member.user.username === state.authenticatedUser?.user?.username : { access: null } ) .map((currentUser) => currentUser.access === 'owner')[0] @@ -172,6 +172,7 @@ const DataFilesProjectFileListing = ({ rootSystem, system, path }) => { scheme="projects" system={system} path={path || '/'} + basePath="/publications" rootSystem={rootSystem} /> diff --git a/client/src/components/DataFiles/DataFilesPublicationsList/DataFilesPublicationsList.jsx b/client/src/components/DataFiles/DataFilesPublicationsList/DataFilesPublicationsList.jsx index e17c81016..85558fcd3 100644 --- a/client/src/components/DataFiles/DataFilesPublicationsList/DataFilesPublicationsList.jsx +++ b/client/src/components/DataFiles/DataFilesPublicationsList/DataFilesPublicationsList.jsx @@ -14,11 +14,13 @@ import './DataFilesPublicationsList.scss'; import Searchbar from '_common/Searchbar'; import { formatDate, formatDateTimeFromValue } from 'utils/timeFormat'; -const DataFilesPublicationsList = ({ rootSystem }) => { +const DataFilesPublicationsList = ({ rootSystem, basePath }) => { const { error, loading, publications } = useSelector( (state) => state.publications.listing ); + const _basePath = basePath ?? '/workbench/data'; + const query = queryStringParser.parse(useLocation().search); const systems = useSelector( @@ -38,7 +40,7 @@ const DataFilesPublicationsList = ({ rootSystem }) => { type: 'PUBLICATIONS_GET_PUBLICATIONS', payload: { queryString: query.query_string, - system: selectedSystem.system, + system: selectedSystem?.system, }, }); }, [dispatch, query.query_string]); @@ -60,7 +62,7 @@ const DataFilesPublicationsList = ({ rootSystem }) => { Cell: (el) => ( {el.value} @@ -141,6 +143,7 @@ const DataFilesPublicationsList = ({ rootSystem }) => { isLoading={loading} noDataText={noDataText} className="publications-listing" + columnMemoProps={[selectedSystem]} /> diff --git a/client/src/components/Publications/PublicationDetailPublicView.jsx b/client/src/components/Publications/PublicationDetailPublicView.jsx new file mode 100644 index 000000000..8db841f09 --- /dev/null +++ b/client/src/components/Publications/PublicationDetailPublicView.jsx @@ -0,0 +1,43 @@ +import React from 'react'; +import CombinedBreadcrumbs from '../DataFiles/CombinedBreadcrumbs/CombinedBreadcrumbs'; +import DataFilesPreviewModal from '../DataFiles/DataFilesModals/DataFilesPreviewModal'; +import DataFilesProjectCitationModal from '../DataFiles/DataFilesModals/DataFilesProjectCitationModal'; +import DataFilesProjectTreeModal from '../DataFiles/DataFilesModals/DataFilesProjectTreeModal'; +import DataFilesPublicationAuthorsModal from '../DataFiles/DataFilesModals/DataFilesPublicationAuthorsModal'; +import DataFilesShowPathModal from '../DataFiles/DataFilesModals/DataFilesShowPathModal'; +import DataFilesViewDataModal from '../DataFiles/DataFilesModals/DataFilesViewDataModal'; +import DataFilesProjectFileListing from '../DataFiles/DataFilesProjectFileListing/DataFilesProjectFileListing'; + +function PublicationDetailPublicView({params}) { + return ( +
+ + + + + + + + +
+ ); +} + +export default PublicationDetailPublicView; diff --git a/client/src/components/Publications/PublicationsPublicView.jsx b/client/src/components/Publications/PublicationsPublicView.jsx new file mode 100644 index 000000000..609ea73cf --- /dev/null +++ b/client/src/components/Publications/PublicationsPublicView.jsx @@ -0,0 +1,14 @@ +import React from 'react'; +import DataFilesPublicationsList from '../DataFiles/DataFilesPublicationsList/DataFilesPublicationsList'; +import DataFilesProjectDescriptionModal from '../DataFiles/DataFilesModals/DataFilesProjectDescriptionModal'; + +function PublicationsPublicView() { + return ( +
+ + +
+ ); +} + +export default PublicationsPublicView \ No newline at end of file diff --git a/client/src/components/Workbench/AppRouter.jsx b/client/src/components/Workbench/AppRouter.jsx index 945969607..66a8741bf 100644 --- a/client/src/components/Workbench/AppRouter.jsx +++ b/client/src/components/Workbench/AppRouter.jsx @@ -1,7 +1,7 @@ import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useSystems } from 'hooks/datafiles'; -import { BrowserRouter as Router, Route } from 'react-router-dom'; +import { BrowserRouter as Router, Route, Redirect } from 'react-router-dom'; import Workbench from './Workbench'; import * as ROUTES from '../../constants/routes'; import TicketStandaloneCreate from '../Tickets/TicketStandaloneCreate'; @@ -9,6 +9,9 @@ import PublicData from '../PublicData/PublicData'; import RequestAccess from '../RequestAccess/RequestAccess'; import GoogleDrivePrivacyPolicy from '../ManageAccount/GoogleDrivePrivacyPolicy'; import SiteSearch from '../SiteSearch'; +import PublicationsPublicView from '../Publications/PublicationsPublicView'; +import PublicationDetailPublicView from '../Publications/PublicationDetailPublicView'; + function AppRouter() { const dispatch = useDispatch(); @@ -45,6 +48,16 @@ function AppRouter() { + + + + + { + return ; + }} + /> { const { canEditDataset, canRequestPublication, canReviewPublication } = useSelector((state) => { const { members } = state.projects.metadata; - const { username } = state.authenticatedUser.user; + const { username } = state.authenticatedUser?.user ?? {}; const currentUser = members.find( (member) => member.user?.username === username ); diff --git a/client/src/components/_custom/drp/DataFilesProjectFileListingMetadataTitleAddon/DataFilesProjectFileListingMetadataTitleAddon.jsx b/client/src/components/_custom/drp/DataFilesProjectFileListingMetadataTitleAddon/DataFilesProjectFileListingMetadataTitleAddon.jsx index a1acd41c6..24b01ded0 100644 --- a/client/src/components/_custom/drp/DataFilesProjectFileListingMetadataTitleAddon/DataFilesProjectFileListingMetadataTitleAddon.jsx +++ b/client/src/components/_custom/drp/DataFilesProjectFileListingMetadataTitleAddon/DataFilesProjectFileListingMetadataTitleAddon.jsx @@ -23,7 +23,7 @@ const DataFilesProjectFileListingMetadataTitleAddon = ({ const userAccess = state.projects.metadata.members .filter((member) => member.user - ? member.user.username === state.authenticatedUser.user.username + ? member.user.username === state.authenticatedUser?.user?.username : { access: null } ) .map((currentUser) => { diff --git a/server/conf/nginx/nginx.conf b/server/conf/nginx/nginx.conf index 2ca304cb1..23631de4b 100644 --- a/server/conf/nginx/nginx.conf +++ b/server/conf/nginx/nginx.conf @@ -80,7 +80,7 @@ http { alias /srv/www/portal/server/docs; } - location ~ ^/(core|auth|workbench|tickets|googledrive-privacy-policy|public-data|request-access|accounts|api|login|webhooks|search) { + location ~ ^/(core|auth|workbench|tickets|googledrive-privacy-policy|public-data|publications|request-access|accounts|api|login|webhooks|search) { proxy_pass http://portal_core; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; diff --git a/server/portal/apps/datafiles/views.py b/server/portal/apps/datafiles/views.py index c3630ded1..76f8a6a7e 100644 --- a/server/portal/apps/datafiles/views.py +++ b/server/portal/apps/datafiles/views.py @@ -55,7 +55,7 @@ def get(self, request): response['default_host'] = system_def.host response['default_system'] = system_id else: - response['system_list'] = [sys for sys in portal_systems if sys['scheme'] == 'public'] + response['system_list'] = [sys for sys in portal_systems if sys['scheme'] == 'public' or sys['system'] == settings.PORTAL_PROJECTS_PUBLISHED_ROOT_SYSTEM_NAME] return JsonResponse(response) @@ -71,6 +71,8 @@ def get(self, request, systemId): public_sys = next((sys for sys in settings.PORTAL_DATAFILES_STORAGE_SYSTEMS if sys['scheme'] == 'public'), None) if public_sys and public_sys['system'] == systemId: client = service_account() + if systemId.startswith(settings.PORTAL_PROJECTS_PUBLISHED_SYSTEM_PREFIX): + client = service_account() else: return JsonResponse({'message': 'Unauthorized'}, status=401) system_def = client.systems.getSystem(systemId=systemId) @@ -92,6 +94,8 @@ def get(self, request, operation=None, scheme=None, system=None, path='/'): public_sys = next((sys for sys in settings.PORTAL_DATAFILES_STORAGE_SYSTEMS if sys['scheme'] == 'public'), None) if public_sys and public_sys['system'] == system and path.startswith(public_sys['homeDir'].strip('/')): client = service_account() + if system and system.startswith(settings.PORTAL_PROJECTS_PUBLISHED_SYSTEM_PREFIX): + client = service_account() else: return JsonResponse( {'message': 'This data requires authentication to view.'}, diff --git a/server/portal/apps/projects/views.py b/server/portal/apps/projects/views.py index e5b6246c1..a1ad7fd7e 100644 --- a/server/portal/apps/projects/views.py +++ b/server/portal/apps/projects/views.py @@ -10,6 +10,7 @@ from django.conf import settings from django.http import JsonResponse from django.utils.decorators import method_decorator +from portal.libs.agave.utils import service_account from portal.utils.decorators import agave_jwt_login from portal.exceptions.api import ApiException from portal.views.base import BaseApiView @@ -157,7 +158,6 @@ def post(self, request): # pylint: disable=no-self-use @method_decorator(agave_jwt_login, name='dispatch') -@method_decorator(login_required, name='dispatch') class ProjectInstanceApiView(BaseApiView): """Project Instance API view. @@ -178,8 +178,11 @@ def get(self, request, project_id=None, system_id=None): # Based on url mapping, either system_id or project_id is always available. if system_id is not None: project_id = system_id.split(f"{settings.PORTAL_PROJECTS_SYSTEM_PREFIX}.")[1] - - client = request.user.tapis_oauth.client + + if system_id and system_id.startswith(settings.PORTAL_PROJECTS_PUBLISHED_SYSTEM_PREFIX): + client = service_account() + else: + client = request.user.tapis_oauth.client prj = get_project(client, project_id) diff --git a/server/portal/urls.py b/server/portal/urls.py index f702a8c12..fa8294bae 100644 --- a/server/portal/urls.py +++ b/server/portal/urls.py @@ -103,6 +103,7 @@ namespace='googledrive-privacy-policy')), path('workbench/', include('portal.apps.workbench.urls', namespace='workbench')), path('public-data/', include('portal.apps.public_data.urls', namespace='public')), + path('publications/', include('portal.apps.public_data.urls', namespace='publications')), path('request-access/', include('portal.apps.request_access.urls', namespace='request_access')), path('search/', include('portal.apps.site_search.urls', namespace='site_search')),