diff --git a/designsafe/apps/api/filemeta/tests.py b/designsafe/apps/api/filemeta/tests.py
index b80fedafa8..aea44dd3a1 100644
--- a/designsafe/apps/api/filemeta/tests.py
+++ b/designsafe/apps/api/filemeta/tests.py
@@ -85,6 +85,23 @@ def test_get_file_meta(
}
+@pytest.mark.django_db
+def test_get_file_meta_using_jwt(
+ regular_user_using_jwt, client, filemeta_db_mock, mock_access_success
+):
+ system_id, path, file_meta = filemeta_db_mock
+ response = client.get(f"/api/filemeta/{system_id}/{path}")
+ assert response.status_code == 200
+
+ assert response.json() == {
+ "value": file_meta.value,
+ "name": "designsafe.file",
+ "lastUpdated": file_meta.last_updated.isoformat(
+ timespec="milliseconds"
+ ).replace("+00:00", "Z"),
+ }
+
+
@pytest.mark.django_db
def test_create_file_meta_no_access(
client, authenticated_user, filemeta_value_mock, mock_access_failure
@@ -122,6 +139,21 @@ def test_create_file_meta(
assert file_meta.value == filemeta_value_mock
+@pytest.mark.django_db
+def test_create_file_meta_using_jwt(
+ client, regular_user_using_jwt, filemeta_value_mock, mock_access_success
+):
+ response = client.post(
+ "/api/filemeta/",
+ data=json.dumps(filemeta_value_mock),
+ content_type="application/json",
+ )
+ assert response.status_code == 200
+
+ file_meta = FileMetaModel.objects.first()
+ assert file_meta.value == filemeta_value_mock
+
+
@pytest.mark.django_db
def test_create_file_meta_update_existing_entry(
client,
diff --git a/designsafe/apps/api/filemeta/views.py b/designsafe/apps/api/filemeta/views.py
index f80d04da9e..904fd96f0c 100644
--- a/designsafe/apps/api/filemeta/views.py
+++ b/designsafe/apps/api/filemeta/views.py
@@ -5,7 +5,7 @@
from designsafe.apps.api.datafiles.operations.tapis_operations import listing
from designsafe.apps.api.exceptions import ApiException
from designsafe.apps.api.filemeta.models import FileMetaModel
-from designsafe.apps.api.views import AuthenticatedApiView
+from designsafe.apps.api.views import AuthenticatedAllowJwtApiView
logger = logging.getLogger(__name__)
@@ -36,8 +36,7 @@ def check_access(request, system_id: str, path: str, check_for_writable_access=F
raise ApiException("User forbidden to access metadata", status=403) from exc
-# TODO_V3 update to allow JWT access DES-2706: https://github.com/DesignSafe-CI/portal/pull/1192
-class FileMetaView(AuthenticatedApiView):
+class FileMetaView(AuthenticatedAllowJwtApiView):
"""View for creating and getting file metadata"""
def get(self, request: HttpRequest, system_id: str, path: str):
@@ -64,8 +63,7 @@ def get(self, request: HttpRequest, system_id: str, path: str):
return JsonResponse(result, safe=False)
-# TODO_V3 update to allow JWT access DES-2706: https://github.com/DesignSafe-CI/portal/pull/1192
-class CreateFileMetaView(AuthenticatedApiView):
+class CreateFileMetaView(AuthenticatedAllowJwtApiView):
"""View for creating (and updating) file metadata"""
def post(self, request: HttpRequest):
diff --git a/designsafe/apps/api/views.py b/designsafe/apps/api/views.py
index a6870fd7ba..73996febe2 100644
--- a/designsafe/apps/api/views.py
+++ b/designsafe/apps/api/views.py
@@ -1,11 +1,14 @@
+from django.views.decorators.csrf import csrf_exempt
from django.http.response import HttpResponse, HttpResponseForbidden, JsonResponse
from django.views.generic import View
from django.http import JsonResponse
+from django.utils.decorators import method_decorator
from requests.exceptions import ConnectionError, HTTPError
from .exceptions import ApiException
import logging
from logging import getLevelName
import json
+from designsafe.apps.api.decorators import tapis_jwt_login
logger = logging.getLogger(__name__)
@@ -59,14 +62,16 @@ def dispatch(self, request, *args, **kwargs):
return super(AuthenticatedApiView, self).dispatch(request, *args, **kwargs)
-class AuthenticatedApiView(BaseApiView):
+class AuthenticatedAllowJwtApiView(AuthenticatedApiView):
+ """
+ Extends AuthenticatedApiView to also allow JWT access in addition to django session cookie
+ """
+ @method_decorator(csrf_exempt, name="dispatch")
+ @method_decorator(tapis_jwt_login)
def dispatch(self, request, *args, **kwargs):
- """Returns 401 if user is not authenticated."""
-
- if not request.user.is_authenticated:
- return JsonResponse({"message": "Unauthenticated user"}, status=401)
- return super(AuthenticatedApiView, self).dispatch(request, *args, **kwargs)
+ """Returns 401 if user is not authenticated like AuthenticatedApiView but allows JWT access."""
+ return super(AuthenticatedAllowJwtApiView, self).dispatch(request, *args, **kwargs)
class LoggerApi(BaseApiView):
diff --git a/designsafe/apps/workspace/templates/designsafe/apps/workspace/app_card.html b/designsafe/apps/workspace/templates/designsafe/apps/workspace/app_card.html
index e31df8ea1f..fa08217502 100644
--- a/designsafe/apps/workspace/templates/designsafe/apps/workspace/app_card.html
+++ b/designsafe/apps/workspace/templates/designsafe/apps/workspace/app_card.html
@@ -3,11 +3,15 @@
{{app
{{app.description}}
+ {% if app.tags|length > 0 %}
{% for tag in app.tags %}
- {{tag}}
{% endfor %}
+ {% else %}
+
+ {% endif %}
{% if app.is_popular %}
diff --git a/designsafe/conftest.py b/designsafe/conftest.py
index aaa811fb3c..d32b49dc54 100644
--- a/designsafe/conftest.py
+++ b/designsafe/conftest.py
@@ -3,6 +3,7 @@
import pytest
import os
import json
+from unittest.mock import patch
from django.conf import settings
from designsafe.apps.auth.models import TapisOAuthToken
@@ -37,6 +38,19 @@ def regular_user(django_user_model, mock_tapis_client):
yield user
+@pytest.fixture
+def regular_user_using_jwt(regular_user, client):
+ """Fixture for regular user who is using jwt for authenticated requests"""
+ with patch('designsafe.apps.api.decorators.Tapis') as mock_tapis:
+ # Mock the Tapis's validate_token method within the tapis_jwt_login decorator
+ mock_validate_token = mock_tapis.return_value.validate_token
+ mock_validate_token.return_value = {"tapis/username": regular_user.username}
+
+ client.defaults['HTTP_X_TAPIS_TOKEN'] = 'fake_token_string'
+
+ yield client
+
+
@pytest.fixture
def project_admin_user(django_user_model):
django_user_model.objects.create_user(
diff --git a/designsafe/static/styles/DesignSafe-Icons.css b/designsafe/static/styles/DesignSafe-Icons.css
index 29fc145a9d..5059e1480b 100644
--- a/designsafe/static/styles/DesignSafe-Icons.css
+++ b/designsafe/static/styles/DesignSafe-Icons.css
@@ -1,125 +1,30 @@
-@font-face {
- font-family: 'DesignSafe-Icons';
- src: url('/static/fonts/DesignSafe-Icons.eot?nvbv6c');
- src: url('/static/fonts/DesignSafe-Icons.eot?nvbv6c#iefix') format('embedded-opentype'),
- url('/static/fonts/DesignSafe-Icons.ttf?nvbv6c') format('truetype'),
- url('/static/fonts/DesignSafe-Icons.woff2') format('woff2'),
- url('/static/fonts/DesignSafe-Icons.woff?nvbv6c') format('woff'),
- url('/static/fonts/DesignSafe-Icons.svg?nvbv6c#DesignSafe-Icons') format('svg');
- font-weight: normal;
- font-style: normal;
- font-display: block;
-}
-
-[class^='ds-icon-'],
-[class*=' ds-icon-'] {
- /* use !important to prevent issues with browser extensions that change fonts */
- font-family: 'DesignSafe-Icons' !important;
- speak: never;
- font-style: normal;
- font-weight: normal;
- font-variant: normal;
- text-transform: none;
- line-height: 1;
+@import url('./DesignSafe-Icons.font.css');
- /* Better Font Rendering =========== */
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
+/* TODO: Consider isolating and importing main.css & main-ef.css .ds-icon's */
-.ds-icon-Contract:before {
- content: '\e91d';
-}
-.ds-icon-Expand:before {
- content: '\e923';
-}
-.ds-icon-Job-Status:before {
- content: '\e929';
-}
-.ds-icon-All-Hazards:before {
- content: '\e908';
-}
-.ds-icon-New-Tab:before {
- content: '\ea7e';
-}
-.ds-icon-NGL-without-text:before {
- content: '\e903';
-}
-.ds-icon-Potree:before {
- content: '\e906';
-}
-.ds-icon-Generic-Vis:before {
- content: '\e907';
-}
-.ds-icon-HVSR:before {
- content: '\e90d';
-}
-.ds-icon-MPM:before {
- content: '\e90e';
-}
-.ds-icon-Generic-App:before {
- content: '\e960';
-}
-.ds-icon-GiD:before {
- content: '\e90f';
-}
-.ds-icon-Earth:before {
- content: '\e910';
-}
-.ds-icon-Water:before {
- content: '\e912';
-}
-.ds-icon-Wind:before {
- content: '\e915';
-}
-.ds-icon-rWHALE:before {
- content: '\e905';
-}
-.ds-icon-Extract:before {
- content: '\e959';
-}
-.ds-icon-Hazmapper:before {
- content: '\e904';
-}
-.ds-icon-Compress:before {
- content: '\e958';
-}
-.ds-icon-STKO:before {
- content: '\e91c';
-}
-.ds-icon-OpenFOAM:before {
- content: '\e900';
-}
-.ds-icon-Blender:before {
- content: '\e901';
-}
-.ds-icon-MATLAB:before {
- content: '\e902';
-}
-.ds-icon-Paraview:before {
- content: '\e911';
-}
-.ds-icon-Jupyter:before {
- content: '\e913';
-}
-.ds-icon-QGIS:before {
- content: '\e914';
-}
-.ds-icon-OpenSees:before {
- content: '\e916';
-}
-.ds-icon-LS-DYNA:before {
- content: '\e917';
-}
-.ds-icon-Dakota:before {
- content: '\e918';
-}
-.ds-icon-Clawpack:before {
- content: '\e919';
-}
-.ds-icon-Ansys:before {
- content: '\e91a';
-}
-.ds-icon-SWBatch:before {
- content: '\e91b';
+/* To globally adjust sizes of specific font icons */
+.ds-icon-All-Hazards::before,
+.ds-icon-Earth::before,
+.ds-icon-Water::before,
+.ds-icon-Wind::before,
+.ds-icon-OpenSees::before {
+ display: block;
+}
+.ds-icon-All-Hazards::before,
+.ds-icon-Earth::before,
+.ds-icon-Water::before,
+.ds-icon-Wind::before {
+ font-size: 1.2em;
+ /* To restore alignment after font-size changes it */
+ margin-top: -0.15em; /* FAQ: -0.2em did not re-align it perfectly */
+}
+.ds-icon-OpenSees::before {
+ font-size: 1.5em;
+ /* To restore alignment after font-size changes it */
+ margin-top: -0.3em; /* FAQ: -0.2em did not re-align it perfectly */
+}
+.ds-icon-All-Hazards::before,
+.ds-icon-OpenSees::before {
+ /* To move icon down */
+ translate: 0 0.25em;
}
diff --git a/designsafe/static/styles/DesignSafe-Icons.font.css b/designsafe/static/styles/DesignSafe-Icons.font.css
new file mode 100644
index 0000000000..29fc145a9d
--- /dev/null
+++ b/designsafe/static/styles/DesignSafe-Icons.font.css
@@ -0,0 +1,125 @@
+@font-face {
+ font-family: 'DesignSafe-Icons';
+ src: url('/static/fonts/DesignSafe-Icons.eot?nvbv6c');
+ src: url('/static/fonts/DesignSafe-Icons.eot?nvbv6c#iefix') format('embedded-opentype'),
+ url('/static/fonts/DesignSafe-Icons.ttf?nvbv6c') format('truetype'),
+ url('/static/fonts/DesignSafe-Icons.woff2') format('woff2'),
+ url('/static/fonts/DesignSafe-Icons.woff?nvbv6c') format('woff'),
+ url('/static/fonts/DesignSafe-Icons.svg?nvbv6c#DesignSafe-Icons') format('svg');
+ font-weight: normal;
+ font-style: normal;
+ font-display: block;
+}
+
+[class^='ds-icon-'],
+[class*=' ds-icon-'] {
+ /* use !important to prevent issues with browser extensions that change fonts */
+ font-family: 'DesignSafe-Icons' !important;
+ speak: never;
+ font-style: normal;
+ font-weight: normal;
+ font-variant: normal;
+ text-transform: none;
+ line-height: 1;
+
+ /* Better Font Rendering =========== */
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+.ds-icon-Contract:before {
+ content: '\e91d';
+}
+.ds-icon-Expand:before {
+ content: '\e923';
+}
+.ds-icon-Job-Status:before {
+ content: '\e929';
+}
+.ds-icon-All-Hazards:before {
+ content: '\e908';
+}
+.ds-icon-New-Tab:before {
+ content: '\ea7e';
+}
+.ds-icon-NGL-without-text:before {
+ content: '\e903';
+}
+.ds-icon-Potree:before {
+ content: '\e906';
+}
+.ds-icon-Generic-Vis:before {
+ content: '\e907';
+}
+.ds-icon-HVSR:before {
+ content: '\e90d';
+}
+.ds-icon-MPM:before {
+ content: '\e90e';
+}
+.ds-icon-Generic-App:before {
+ content: '\e960';
+}
+.ds-icon-GiD:before {
+ content: '\e90f';
+}
+.ds-icon-Earth:before {
+ content: '\e910';
+}
+.ds-icon-Water:before {
+ content: '\e912';
+}
+.ds-icon-Wind:before {
+ content: '\e915';
+}
+.ds-icon-rWHALE:before {
+ content: '\e905';
+}
+.ds-icon-Extract:before {
+ content: '\e959';
+}
+.ds-icon-Hazmapper:before {
+ content: '\e904';
+}
+.ds-icon-Compress:before {
+ content: '\e958';
+}
+.ds-icon-STKO:before {
+ content: '\e91c';
+}
+.ds-icon-OpenFOAM:before {
+ content: '\e900';
+}
+.ds-icon-Blender:before {
+ content: '\e901';
+}
+.ds-icon-MATLAB:before {
+ content: '\e902';
+}
+.ds-icon-Paraview:before {
+ content: '\e911';
+}
+.ds-icon-Jupyter:before {
+ content: '\e913';
+}
+.ds-icon-QGIS:before {
+ content: '\e914';
+}
+.ds-icon-OpenSees:before {
+ content: '\e916';
+}
+.ds-icon-LS-DYNA:before {
+ content: '\e917';
+}
+.ds-icon-Dakota:before {
+ content: '\e918';
+}
+.ds-icon-Clawpack:before {
+ content: '\e919';
+}
+.ds-icon-Ansys:before {
+ content: '\e91a';
+}
+.ds-icon-SWBatch:before {
+ content: '\e91b';
+}
diff --git a/designsafe/static/styles/app-card.css b/designsafe/static/styles/app-card.css
index 9c2e91cabf..39361923c4 100644
--- a/designsafe/static/styles/app-card.css
+++ b/designsafe/static/styles/app-card.css
@@ -7,11 +7,16 @@
/* Title */
.c-app-card__title {
- margin-top: 1em;
- margin-bottom: 0;
+ margin-block: 1em 0;
+ padding-inline: 1rem;
}
-.c-app-card__title > .icon::before {
- font-size: unset; /* to undo 5vw from ng-designsafe.css */
+.c-app-card__title > .ds-icon {
+ width: unset; /* overwrite .ds-icon from main.css */
+ height: unset; /* overwrite .ds-icon from main.css */
+
+ /* To add space between icon and text */
+ /* FAQ: Using `rem` cuz some icons are globally resized via `em` */
+ margin-right: 0.5rem;
}
/* Description */
@@ -53,14 +58,24 @@
box-shadow: 3px 2px 2px #00000029;
}
.c-app-card:hover {
- outline: 2px solid #0000FF;
+ outline: 2px solid var(--ds-active-color);
outline-offset: calc( var(--border-width) * -1 );
box-shadow: none;
}
+.c-app-card:hover,
+.c-app-card:focus {
+ color: inherit;
+ text-decoration: none;
+}
a.c-app-card:active {
outline-width: 1px;
}
+/* Title */
+.c-app-card:hover .c-app-card__title {
+ color: var(--ds-active-color);
+}
+
/* Description */
.c-app-card__desc {
line-height: 1.8;
diff --git a/designsafe/static/styles/app-page.css b/designsafe/static/styles/app-page.css
index 039c62188d..0425565049 100644
--- a/designsafe/static/styles/app-page.css
+++ b/designsafe/static/styles/app-page.css
@@ -2,9 +2,10 @@
@import url("./app-grid.css");
@import url("./app-version-list.css");
-/* To make width of page content line up with width of header. */
-/* TODO: Verify whether this is safe to put into `main.css` */
-.s-app-page > .container-fluid {
+/* To make width of page content line up with width of header */
+.s-app-page > .container-fluid /* FAQ: To support Style wrapper (as fallback) */,
+.s-app-page main > .container-fluid {
+ /* HELP: Is this safe to replace the .container-fluid margin in main.css? */
margin: 0 50px;
}
diff --git a/designsafe/static/styles/main.css b/designsafe/static/styles/main.css
index bb8716405b..09b83558c8 100644
--- a/designsafe/static/styles/main.css
+++ b/designsafe/static/styles/main.css
@@ -1,5 +1,6 @@
:root {
--ds-accent-color: #47a59d;
+ --ds-active-color: #337AB7;
}
body, html {
@@ -977,4 +978,4 @@ li .popover.right {
.reg-left-col {
padding-right:75px;
}
-}
\ No newline at end of file
+}