diff --git a/docker-compose.yml b/docker-compose.yml index aa4cddb7bb..acb34df5a4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -455,4 +455,3 @@ volumes: mongo_data: # letsencrypt_etc: # letsencrypt_html: - diff --git a/docker-compose2.yml b/docker-compose2.yml index 64cbb9a4a0..95f4e36fab 100644 --- a/docker-compose2.yml +++ b/docker-compose2.yml @@ -455,4 +455,3 @@ volumes: mongo_data: # letsencrypt_etc: # letsencrypt_html: - diff --git a/modules/invenio-oauth2server/invenio_oauth2server/templates/invenio_oauth2server/authorize.html b/modules/invenio-oauth2server/invenio_oauth2server/templates/invenio_oauth2server/authorize.html index 921dc581b7..b4e90f95a9 100644 --- a/modules/invenio-oauth2server/invenio_oauth2server/templates/invenio_oauth2server/authorize.html +++ b/modules/invenio-oauth2server/invenio_oauth2server/templates/invenio_oauth2server/authorize.html @@ -3,8 +3,8 @@ This file is part of Invenio. Copyright (C) 2015-2018 CERN. - Invenio is free software; you can redistribute it and/or modify it - under the terms of the MIT License; see LICENSE file for more details. +Invenio is free software; you can redistribute it and/or modify it +under the terms of the MIT License; see LICENSE file for more details. #} {%- extends config.OAUTH2SERVER_COVER_TEMPLATE %} @@ -13,54 +13,64 @@ {% block page_body %}
-
-
-{{ helpers.panel_start(_('Authorize application'), icon='fa fa-shield fa-fw') }} -

{{ _('Authorize application') }}

-

{{ _("Application '%(client_name)s' by '%(client_user)s' wants permission to access your '%(current_user)s' account.", - client_name=client.name, client_user=client.user.nickname or client.user.email, current_user=current_user.nickname or current_user.email) }}

-
-
-
-
+
+
+ {{ helpers.panel_start(_('Authorize application'), icon='fa fa-shield fa-fw') }} +

{{ _('Authorize application') }}

+

{{ _("Application '%(client_name)s' by '%(client_user)s' wants permission to access your + '%(current_user)s' account.", + client_name=client.name, client_user=client.user.nickname or client.user.email, + current_user=current_user.nickname or current_user.email) }}

+
+
+
+
-

{{ _('Review permissions') }}

- {%- for group in scopes|groupby('group') %} - {%- if loop.first %}{% endif %} - - - - - {%- if loop.last %}
{{group.grouper}}
    {% for scope in group.list %}
  • {{scope.help_text}}
  • {% endfor %}
{% endif %} - {%- else %} -

No permissions granted.

- {%- endfor %} -
-
-
-
-
- {{ _('Application') }} -

{{client.name}}

- {%- if client.description %}

{{client.description}}

{% endif %} - {%- if client.website %}

{{ _('Visit application website') }}

{% endif %} -
-

- {{ client.get_users }} {{ 'user' if client.get_users == 1 else 'users' }} -

+

{{ _('Review permissions') }}

+ {%- for group in scopes|groupby('group') %} + {%- if loop.first %} + {% endif %} + + + + + {%- if loop.last %} + +
{{group.grouper}} +
    {% for scope in group.list %}
  • {{scope.help_text}}
  • {% endfor %}
+
{% endif %} + {%- else %} +

{{ _("No permissions granted.") }}

+ {%- endfor %} +
+
+
+
+
+ {{ _('Application') }} +

{{client.name}}

+ {%- if client.description %}

{{client.description}}

{% endif %} + {%- if client.website %}

{{ _("Visit application website") }}

{% + endif %} +
+

+ {{ client.get_users }} {{ 'user' if client.get_users == 1 else 'users' }} +

+
+
+
+
+
+
+
+ + +
+
+ {{ helpers.panel_end() }} +
-
-
-
-
-
-
- - -
-
-{{ helpers.panel_end() }} -
-
{% endblock %} diff --git a/modules/invenio-oauth2server/invenio_oauth2server/templates/invenio_oauth2server/errors.html b/modules/invenio-oauth2server/invenio_oauth2server/templates/invenio_oauth2server/errors.html index 13cea2a2d6..d2380e73ad 100644 --- a/modules/invenio-oauth2server/invenio_oauth2server/templates/invenio_oauth2server/errors.html +++ b/modules/invenio-oauth2server/invenio_oauth2server/templates/invenio_oauth2server/errors.html @@ -3,8 +3,8 @@ This file is part of Invenio. Copyright (C) 2015-2018 CERN. - Invenio is free software; you can redistribute it and/or modify it - under the terms of the MIT License; see LICENSE file for more details. +Invenio is free software; you can redistribute it and/or modify it +under the terms of the MIT License; see LICENSE file for more details. #} {%- extends config.OAUTH2SERVER_COVER_TEMPLATE %} @@ -14,24 +14,36 @@ {%- block page_body %}
- {{ helpers.panel_start( + {{ helpers.panel_start( _('Invalid authorization request'), icon='fa fa-warning fa-fw' - ) }} - - {{ _('Invalid authorization request') }} - - {{ _('The service that redirected your here made an invalid authorization request (error code: %(x_error)s).', - x_error=error.error) }} - -
-
-
- - {{ _('Get me out of here!') }} - + ) }} + +

+ {{ _('You cannot access the service because there is an error in the authentication request.') }} + {% if error.error == unsupported_response_type %} + ({{ _('unsupported_response_type') }}) + {% elif error.error == 'invalid_request' %} + {% if error.description is string and 'client_id' in error.description %} + ({{ _('invalid_client_id') }}) + {% else %} + ({{ _('invalid_request') }}) + {% endif %} + {% elif error.error == 'invalid_scope' %} + ({{ _('invalid_scope')}}) + {% elif error.error == 'access_denied' %} + ({{ _('access_denied') }}) + {% endif %} +

+ +
+
+ + {{ helpers.panel_end() }}
- {{ helpers.panel_end() }}
-
-{%- endblock %} + {%- endblock %} diff --git a/modules/invenio-oauth2server/invenio_oauth2server/translations/en/LC_MESSAGES/messages.mo b/modules/invenio-oauth2server/invenio_oauth2server/translations/en/LC_MESSAGES/messages.mo index 37cc8629ab..ddda3415eb 100644 Binary files a/modules/invenio-oauth2server/invenio_oauth2server/translations/en/LC_MESSAGES/messages.mo and b/modules/invenio-oauth2server/invenio_oauth2server/translations/en/LC_MESSAGES/messages.mo differ diff --git a/modules/invenio-oauth2server/invenio_oauth2server/translations/en/LC_MESSAGES/messages.po b/modules/invenio-oauth2server/invenio_oauth2server/translations/en/LC_MESSAGES/messages.po index 0906c42036..bd5a324cfb 100644 --- a/modules/invenio-oauth2server/invenio_oauth2server/translations/en/LC_MESSAGES/messages.po +++ b/modules/invenio-oauth2server/invenio_oauth2server/translations/en/LC_MESSAGES/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: invenio-oauth2server 1.0.0\n" "Report-Msgid-Bugs-To: info@inveniosoftware.org\n" -"POT-Creation-Date: 2025-10-03 14:53+0900\n" +"POT-Creation-Date: 2025-12-17 12:38+0900\n" "PO-Revision-Date: 2025-05-07 18:18+0900\n" "Last-Translator: FULL NAME \n" "Language: en\n" @@ -112,8 +112,8 @@ msgstr "" #: invenio_oauth2server/templates/invenio_oauth2server/authorize.html:18 #: invenio_oauth2server/templates/invenio_oauth2server/authorize.html:19 -#: invenio_oauth2server/templates/invenio_oauth2server/authorize.html:58 -#: invenio_oauth2server/views/server.py:69 +#: invenio_oauth2server/templates/invenio_oauth2server/authorize.html:67 +#: invenio_oauth2server/views/server.py:71 msgid "Authorize application" msgstr "" @@ -121,41 +121,64 @@ msgstr "" #, python-format msgid "" "Application '%(client_name)s' by '%(client_user)s' wants permission to " -"access your '%(current_user)s' account." +"access your\n" +" '%(current_user)s' account." msgstr "" -#: invenio_oauth2server/templates/invenio_oauth2server/authorize.html:27 +#: invenio_oauth2server/templates/invenio_oauth2server/authorize.html:29 #: invenio_oauth2server/templates/invenio_oauth2server/settings/token_permission_view.html:29 msgid "Review permissions" msgstr "" #: invenio_oauth2server/templates/invenio_oauth2server/authorize.html:43 +msgid "No permissions granted." +msgstr "" + +#: invenio_oauth2server/templates/invenio_oauth2server/authorize.html:50 #: invenio_oauth2server/templates/invenio_oauth2server/settings/token_permission_view.html:43 msgid "Application" msgstr "" -#: invenio_oauth2server/templates/invenio_oauth2server/authorize.html:46 +#: invenio_oauth2server/templates/invenio_oauth2server/authorize.html:53 #: invenio_oauth2server/templates/invenio_oauth2server/settings/token_permission_view.html:46 msgid "Visit application website" msgstr "" -#: invenio_oauth2server/templates/invenio_oauth2server/authorize.html:59 +#: invenio_oauth2server/templates/invenio_oauth2server/authorize.html:69 msgid "Reject" msgstr "" #: invenio_oauth2server/templates/invenio_oauth2server/errors.html:18 -#: invenio_oauth2server/templates/invenio_oauth2server/errors.html:22 msgid "Invalid authorization request" msgstr "" -#: invenio_oauth2server/templates/invenio_oauth2server/errors.html:24 -#, python-format +#: invenio_oauth2server/templates/invenio_oauth2server/errors.html:23 msgid "" -"The service that redirected your here made an invalid authorization " -"request (error code: %(x_error)s)." +"You cannot access the service because there is an error in the " +"authentication request." msgstr "" -#: invenio_oauth2server/templates/invenio_oauth2server/errors.html:31 +#: invenio_oauth2server/templates/invenio_oauth2server/errors.html:25 +msgid "unsupported_response_type" +msgstr "error: This response type is not supported." + +#: invenio_oauth2server/templates/invenio_oauth2server/errors.html:28 +msgid "invalid_client_id" +msgstr "error: The client ID is incorrect." + +#: invenio_oauth2server/templates/invenio_oauth2server/errors.html:30 +msgid "invalid_request" +msgstr "error: The request is invalid." + +#: invenio_oauth2server/templates/invenio_oauth2server/errors.html:33 +msgid "invalid_scope" +msgstr "error: The scope is incorrect." + +#: invenio_oauth2server/templates/invenio_oauth2server/errors.html:35 +msgid "access_denied" +msgstr "error: Access has been denied." + +#: invenio_oauth2server/templates/invenio_oauth2server/errors.html:43 msgid "Get me out of here!" msgstr "" diff --git a/modules/invenio-oauth2server/invenio_oauth2server/translations/ja/LC_MESSAGES/messages.mo b/modules/invenio-oauth2server/invenio_oauth2server/translations/ja/LC_MESSAGES/messages.mo index d9eceed768..29454e17ea 100644 Binary files a/modules/invenio-oauth2server/invenio_oauth2server/translations/ja/LC_MESSAGES/messages.mo and b/modules/invenio-oauth2server/invenio_oauth2server/translations/ja/LC_MESSAGES/messages.mo differ diff --git a/modules/invenio-oauth2server/invenio_oauth2server/translations/ja/LC_MESSAGES/messages.po b/modules/invenio-oauth2server/invenio_oauth2server/translations/ja/LC_MESSAGES/messages.po index 2e6ae3943b..a5c11db316 100644 --- a/modules/invenio-oauth2server/invenio_oauth2server/translations/ja/LC_MESSAGES/messages.po +++ b/modules/invenio-oauth2server/invenio_oauth2server/translations/ja/LC_MESSAGES/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: invenio-oauth2server 1.0.0\n" "Report-Msgid-Bugs-To: info@inveniosoftware.org\n" -"POT-Creation-Date: 2025-10-03 14:53+0900\n" +"POT-Creation-Date: 2025-12-17 12:38+0900\n" "PO-Revision-Date: 2025-05-07 18:18+0900\n" "Last-Translator: FULL NAME \n" "Language: ja\n" @@ -112,50 +112,73 @@ msgstr "" #: invenio_oauth2server/templates/invenio_oauth2server/authorize.html:18 #: invenio_oauth2server/templates/invenio_oauth2server/authorize.html:19 -#: invenio_oauth2server/templates/invenio_oauth2server/authorize.html:58 -#: invenio_oauth2server/views/server.py:69 +#: invenio_oauth2server/templates/invenio_oauth2server/authorize.html:67 +#: invenio_oauth2server/views/server.py:71 msgid "Authorize application" -msgstr "" +msgstr "アプリの使用を承諾する" #: invenio_oauth2server/templates/invenio_oauth2server/authorize.html:20 #, python-format msgid "" "Application '%(client_name)s' by '%(client_user)s' wants permission to " -"access your '%(current_user)s' account." -msgstr "" +"access your\n" +" '%(current_user)s' account." +msgstr "「%(client_user)s」による「%(client_name)s」アプリケーションがあなたのアカウントへのアクセス許可を求めています。" -#: invenio_oauth2server/templates/invenio_oauth2server/authorize.html:27 +#: invenio_oauth2server/templates/invenio_oauth2server/authorize.html:29 #: invenio_oauth2server/templates/invenio_oauth2server/settings/token_permission_view.html:29 msgid "Review permissions" -msgstr "" +msgstr "権限を確認" #: invenio_oauth2server/templates/invenio_oauth2server/authorize.html:43 +msgid "No permissions granted." +msgstr "権限が付与されていません。" + +#: invenio_oauth2server/templates/invenio_oauth2server/authorize.html:50 #: invenio_oauth2server/templates/invenio_oauth2server/settings/token_permission_view.html:43 msgid "Application" -msgstr "" +msgstr "アプリケーション" -#: invenio_oauth2server/templates/invenio_oauth2server/authorize.html:46 +#: invenio_oauth2server/templates/invenio_oauth2server/authorize.html:53 #: invenio_oauth2server/templates/invenio_oauth2server/settings/token_permission_view.html:46 msgid "Visit application website" -msgstr "" +msgstr "アプリケーションのウェブサイト閲覧" -#: invenio_oauth2server/templates/invenio_oauth2server/authorize.html:59 +#: invenio_oauth2server/templates/invenio_oauth2server/authorize.html:69 msgid "Reject" -msgstr "" +msgstr "拒否" #: invenio_oauth2server/templates/invenio_oauth2server/errors.html:18 -#: invenio_oauth2server/templates/invenio_oauth2server/errors.html:22 msgid "Invalid authorization request" -msgstr "" +msgstr "無効な認証リクエスト" -#: invenio_oauth2server/templates/invenio_oauth2server/errors.html:24 -#, python-format +#: invenio_oauth2server/templates/invenio_oauth2server/errors.html:23 msgid "" -"The service that redirected your here made an invalid authorization " -"request (error code: %(x_error)s)." -msgstr "" +"You cannot access the service because there is an error in the " +"authentication request." +msgstr "認証リクエストに誤りがあるため、アクセスできません。" + +#: invenio_oauth2server/templates/invenio_oauth2server/errors.html:25 +msgid "unsupported_response_type" +msgstr "エラー: このレスポンスタイプはサポートされていません。" + +#: invenio_oauth2server/templates/invenio_oauth2server/errors.html:28 +msgid "invalid_client_id" +msgstr "エラー: クライアントIDに誤りがあります。" + +#: invenio_oauth2server/templates/invenio_oauth2server/errors.html:30 +msgid "invalid_request" +msgstr "エラー: 無効なリクエストです。" + +#: invenio_oauth2server/templates/invenio_oauth2server/errors.html:33 +msgid "invalid_scope" +msgstr "エラー: スコープに誤りがあります。" + +#: invenio_oauth2server/templates/invenio_oauth2server/errors.html:35 +msgid "access_denied" +msgstr "エラー: アクセスが拒否されました。" -#: invenio_oauth2server/templates/invenio_oauth2server/errors.html:31 +#: invenio_oauth2server/templates/invenio_oauth2server/errors.html:43 msgid "Get me out of here!" msgstr "" diff --git a/modules/invenio-oauth2server/invenio_oauth2server/translations/messages.pot b/modules/invenio-oauth2server/invenio_oauth2server/translations/messages.pot index 92fb442353..2a2a8546ab 100644 --- a/modules/invenio-oauth2server/invenio_oauth2server/translations/messages.pot +++ b/modules/invenio-oauth2server/invenio_oauth2server/translations/messages.pot @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: invenio-oauth2server 1.0.0\n" "Report-Msgid-Bugs-To: info@inveniosoftware.org\n" -"POT-Creation-Date: 2025-10-03 14:53+0900\n" +"POT-Creation-Date: 2025-12-17 12:38+0900\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -111,8 +111,8 @@ msgstr "" #: invenio_oauth2server/templates/invenio_oauth2server/authorize.html:18 #: invenio_oauth2server/templates/invenio_oauth2server/authorize.html:19 -#: invenio_oauth2server/templates/invenio_oauth2server/authorize.html:58 -#: invenio_oauth2server/views/server.py:69 +#: invenio_oauth2server/templates/invenio_oauth2server/authorize.html:67 +#: invenio_oauth2server/views/server.py:71 msgid "Authorize application" msgstr "" @@ -120,41 +120,64 @@ msgstr "" #, python-format msgid "" "Application '%(client_name)s' by '%(client_user)s' wants permission to " -"access your '%(current_user)s' account." +"access your\n" +" '%(current_user)s' account." msgstr "" -#: invenio_oauth2server/templates/invenio_oauth2server/authorize.html:27 +#: invenio_oauth2server/templates/invenio_oauth2server/authorize.html:29 #: invenio_oauth2server/templates/invenio_oauth2server/settings/token_permission_view.html:29 msgid "Review permissions" msgstr "" #: invenio_oauth2server/templates/invenio_oauth2server/authorize.html:43 +msgid "No permissions granted." +msgstr "" + +#: invenio_oauth2server/templates/invenio_oauth2server/authorize.html:50 #: invenio_oauth2server/templates/invenio_oauth2server/settings/token_permission_view.html:43 msgid "Application" msgstr "" -#: invenio_oauth2server/templates/invenio_oauth2server/authorize.html:46 +#: invenio_oauth2server/templates/invenio_oauth2server/authorize.html:53 #: invenio_oauth2server/templates/invenio_oauth2server/settings/token_permission_view.html:46 msgid "Visit application website" msgstr "" -#: invenio_oauth2server/templates/invenio_oauth2server/authorize.html:59 +#: invenio_oauth2server/templates/invenio_oauth2server/authorize.html:69 msgid "Reject" msgstr "" #: invenio_oauth2server/templates/invenio_oauth2server/errors.html:18 -#: invenio_oauth2server/templates/invenio_oauth2server/errors.html:22 msgid "Invalid authorization request" msgstr "" -#: invenio_oauth2server/templates/invenio_oauth2server/errors.html:24 -#, python-format +#: invenio_oauth2server/templates/invenio_oauth2server/errors.html:23 msgid "" -"The service that redirected your here made an invalid authorization " -"request (error code: %(x_error)s)." +"You cannot access the service because there is an error in the " +"authentication request." +msgstr "" + +#: invenio_oauth2server/templates/invenio_oauth2server/errors.html:25 +msgid "unsupported_response_type" +msgstr "" + +#: invenio_oauth2server/templates/invenio_oauth2server/errors.html:28 +msgid "invalid_client_id" +msgstr "" + +#: invenio_oauth2server/templates/invenio_oauth2server/errors.html:30 +msgid "invalid_request" +msgstr "" + +#: invenio_oauth2server/templates/invenio_oauth2server/errors.html:33 +msgid "invalid_scope" +msgstr "" + +#: invenio_oauth2server/templates/invenio_oauth2server/errors.html:35 +msgid "access_denied" msgstr "" -#: invenio_oauth2server/templates/invenio_oauth2server/errors.html:31 +#: invenio_oauth2server/templates/invenio_oauth2server/errors.html:43 msgid "Get me out of here!" msgstr "" diff --git a/modules/invenio-oauth2server/invenio_oauth2server/views/server.py b/modules/invenio-oauth2server/invenio_oauth2server/views/server.py index 5e59143bb1..066c560b68 100644 --- a/modules/invenio-oauth2server/invenio_oauth2server/views/server.py +++ b/modules/invenio-oauth2server/invenio_oauth2server/views/server.py @@ -13,12 +13,14 @@ from functools import wraps from flask import Blueprint, _request_ctx_stack, abort, current_app, jsonify, \ - redirect, render_template, request, session + redirect, render_template, request, make_response from flask_babelex import lazy_gettext as _ from flask_breadcrumbs import register_breadcrumb from flask_login import login_required from flask_principal import Identity, identity_changed -from oauthlib.oauth2.rfc6749.errors import InvalidClientError, OAuth2Error +from oauthlib.oauth2.rfc6749.errors import InvalidClientError, OAuth2Error, \ + AccessDeniedError, raise_from_error + from invenio_db import db from ..models import Client @@ -81,10 +83,14 @@ def authorize(*args, **kwargs): abort(404) scopes = current_oauth2server.scopes + scopes_list = [scopes[x] for x in kwargs.get('scopes', [])] + if not scopes_list: + return redirect('/oauth/errors?error=invalid_scope') + ctx = dict( client=client, oauth_request=kwargs.get('request'), - scopes=[scopes[x] for x in kwargs.get('scopes', [])], + scopes=scopes_list ) return render_template('invenio_oauth2server/authorize.html', **ctx) @@ -120,14 +126,22 @@ def access_token(): @blueprint.route('/errors') def errors(): """Error view in case of invalid oauth requests.""" - from oauthlib.oauth2.rfc6749.errors import raise_from_error + status_code = 200 try: error = None - raise_from_error(request.values.get('error'), params=dict()) + error_code = request.values.get('error') + description = request.values.get('error_description') + params = {} + if description: + params['error_description'] = description + + raise_from_error(error_code, params=params) except OAuth2Error as raised: error = raised - return render_template('invenio_oauth2server/errors.html', error=error) - + if not isinstance(error, AccessDeniedError): + status_code = 400 + response = make_response(render_template('invenio_oauth2server/errors.html', error=error), status_code) + return response @blueprint.route('/ping', methods=['GET', 'POST']) @oauth2.require_oauth() @@ -163,10 +177,11 @@ def invalid(): @blueprint.teardown_request def dbsession_clean(exception): + """Clean up the database session after each request.""" current_app.logger.debug("invenio_oauth2server dbsession_clean: {}".format(exception)) if exception is None: try: db.session.commit() except: db.session.rollback() - db.session.remove() \ No newline at end of file + db.session.remove() diff --git a/modules/invenio-oauth2server/tests/conftest.py b/modules/invenio-oauth2server/tests/conftest.py index 05e8fd0860..b7f35001ae 100644 --- a/modules/invenio-oauth2server/tests/conftest.py +++ b/modules/invenio-oauth2server/tests/conftest.py @@ -314,10 +314,20 @@ def provider_fixture(app): 'oauth2test.authorized', _external=True ), _default_scopes='email') + c5 = Client(client_id='no-scopes', + client_secret='no-scopes', + name='no-scopes', + description='', + is_confidential=False, + user=user1, + _redirect_uris=url_for( + 'oauth2test.authorized', _external=True + )) db.session.add(c1) db.session.add(c2) db.session.add(c3) db.session.add(c4) + db.session.add(c5) personal_token = Token.create_personal('test-personal', user1.id, scopes=[], diff --git a/modules/invenio-oauth2server/tests/test_provider.py b/modules/invenio-oauth2server/tests/test_provider.py index ab4963ffab..117d524668 100644 --- a/modules/invenio-oauth2server/tests/test_provider.py +++ b/modules/invenio-oauth2server/tests/test_provider.py @@ -136,7 +136,7 @@ def test_invalid_authorize_requests(provider_fixture): assert error_url in next_url r = client.get(next_url, query_string=data) - assert 'invalid_request' in str(r.data) + assert 'an error in the authentication request' in str(r.data) # Invalid redirect uri r = client.get(url_for( @@ -151,6 +151,27 @@ def test_invalid_authorize_requests(provider_fixture): assert data['error'] == 'invalid_request' assert error_url in next_url + for client_id in ['no-scopes']: + response_type = 'code' + error_url = url_for('invenio_oauth2server.errors', + _external=True) + # Missing scope + r = client.get( + url_for('invenio_oauth2server.authorize'), + data={ + 'redirect_uri': redirect_uri, + 'response_type': response_type, + 'client_id': client_id, + }) + next_url, data = parse_redirect(r.location) + assert r.status_code == 302 + assert data['error'] == 'invalid_scope' + assert url_for('invenio_oauth2server.errors') in next_url + + r = client.get( + url_for('invenio_oauth2server.errors')) + assert r.status_code == 400 + def test_refresh_flow(provider_fixture): app = provider_fixture diff --git a/modules/weko-accounts/tests/test_views.py b/modules/weko-accounts/tests/test_views.py index a91aa5ee1d..658ba64681 100644 --- a/modules/weko-accounts/tests/test_views.py +++ b/modules/weko-accounts/tests/test_views.py @@ -2,7 +2,7 @@ import pytest import json import redis -from invenio_accounts.models import Role +from invenio_accounts.models import Role, User from flask import url_for,request,make_response,current_app,Flask from flask_login.utils import login_user,logout_user from flask_menu import current_menu @@ -18,6 +18,7 @@ find_user_by_email, shib_sp_login, _adjust_shib_admin_DB, + generate_ams_login_url, urlencode ) from weko_admin.models import AdminSettings @@ -26,6 +27,11 @@ def set_session(client,data): with client.session_transaction() as session: for k, v in data.items(): session[k] = v + +def del_session(client,key): + with client.session_transaction() as session: + del session[key] + #def _has_admin_access(): # .tox/c1/bin/pytest --cov=weko_accounts tests/test_views.py::test_has_admin_access -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-workflow/.tox/c1/tmp #def test_has_admin_access(request_context,users): @@ -35,12 +41,12 @@ def set_session(client,data): # logout_user() # login_user(users[4]["obj"]) # result = _has_admin_access() -# assert result == False +# assert result is False #def init_menu(): # .tox/c1/bin/pytest --cov=weko_accounts tests/test_views.py::test_init_menu -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-workflow/.tox/c1/tmp def test_init_menu(request_context): init_menu() - assert current_menu.submenu("setting.admin").active == True + assert current_menu.submenu("setting.admin").active is True assert current_menu.submenu("settings.admin").url == "/admin/" assert current_menu.submenu("settings.admin").text == ' Administration' @@ -96,6 +102,59 @@ def test_redirect_method(app,mocker): _redirect_method(True) mock_render.assert_called_with("http://test_server.localdomain/secure/login.py?next="+url) + url = 'ams' + with app.test_request_context(url): + current_app.config['WEKO_ACCOUNTS_SHIB_AMS_LOGIN_URL'] = '{}ams/login' + # Login is blocked. + mock_render = mocker.patch('weko_accounts.views.redirect',return_value=make_response()) + ams_error = 'Login is blocked.' + _redirect_method(True, ams_error) + mock_render.assert_called_with(\ + 'http://TEST_SERVER.localdomain/ams/login?error=Login+is+blocked.') + # There is no user information. + mock_render = mocker.patch('weko_accounts.views.redirect',return_value=make_response()) + ams_error = 'There is no user information.' + _redirect_method(True, ams_error) + mock_render.assert_called_with( + 'http://TEST_SERVER.localdomain/ams/login?error=There+is+no+user+information.') + # server error + mock_render = mocker.patch('weko_accounts.views.redirect',return_value=make_response()) + ams_error = 'Server error has occurred. Please contact server administrator.' + _redirect_method(True, ams_error) + mock_render.assert_called_with(\ + 'http://TEST_SERVER.localdomain/ams/login?'\ + 'error=Server+error+has+occurred.+Please+contact+server+administrator.') + # other error + mock_render = mocker.patch('weko_accounts.views.redirect',return_value=make_response()) + ams_error = 'Error Message' + _redirect_method(True, ams_error) + mock_render.assert_called_with(\ + 'http://TEST_SERVER.localdomain/ams/login?error=Error+Message') + +# .tox/c1/bin/pytest --cov=weko_accounts tests/test_views.py::test_generate_ams_login_url -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-workflow/.tox/c1/tmp +def test_generate_ams_login_url(app): + with app.test_request_context(): + current_app.config['WEKO_ACCOUNTS_SHIB_AMS_LOGIN_URL'] = '{}ams/login' + # Missing Shib-Session-ID! + ams_error = 'Missing Shib-Session-ID!' + url = generate_ams_login_url(ams_error) + assert url == '/ams/login?error=Missing+Shib-Session-ID%21' + + # Missing SHIB_ATTRs! + ams_error = 'Missing SHIB_ATTRs!' + url = generate_ams_login_url(ams_error) + assert url == '/ams/login?error=Missing+SHIB_ATTRs%21' + + # Login is blocked. + ams_error = 'Login is blocked.' + url = generate_ams_login_url(ams_error) + assert url == '/ams/login?error=Login+is+blocked.' + + # Server error has occurred. Please contact server administrator. + ams_error = 'Server error has occurred. Please contact server administrator.' + url = generate_ams_login_url(ams_error) + assert url == '/ams/login?error=Server+error+has+occurred.+Please+contact+server+administrator.' + #def index(): # .tox/c1/bin/pytest --cov=weko_accounts tests/test_views.py::test_index -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-workflow/.tox/c1/tmp def test_index(client,mocker): @@ -109,53 +168,101 @@ def test_shib_auto_login(client,redis_connect,mocker): url = url_for("weko_accounts.shib_auto_login") set_session(client,{"shib_session_id":None}) # not exist shib_session_id - mock_redirect_ = mocker.patch("weko_accounts.views._redirect_method",return_value=make_response()) + mock_redirect_ = mocker.patch("weko_accounts.views._redirect_method", + return_value=make_response()) client.get(url) mock_redirect_.assert_called_once() + # not exist shib_session_id(AMS) + mock_redirect_ = mocker.patch("weko_accounts.views._redirect_method", + return_value=make_response()) + client.get(url+"?next=ams") + mock_redirect_.assert_called_once() + called_args, called_kwargs = mock_redirect_.call_args + assert called_args[0] is True + assert "Missing Shib-Session-ID!" in called_kwargs.get("ams_error", "") - mocker.patch("weko_accounts.views.RedisConnection.connection",return_value=redis_connect) # not exist cache - mock_redirect_ = mocker.patch("weko_accounts.views._redirect_method",return_value=make_response()) + mocker.patch("weko_accounts.views.RedisConnection.connection", + return_value=redis_connect) + mock_redirect_ = mocker.patch("weko_accounts.views._redirect_method", + return_value=make_response()) client.get(url+"?Shib-Session-ID=2222") mock_redirect_.assert_called_once() + # not exist cache(AMS) + mocker.patch("weko_accounts.views.RedisConnection.connection", + return_value=redis_connect) + mock_redirect_ = mocker.patch("weko_accounts.views._redirect_method", + return_value=make_response()) + client.get(url+"?next=ams&Shib-Session-ID=2222") + mock_redirect_.assert_called_once() + called_args, called_kwargs = mock_redirect_.call_args + assert called_args[0] is True + assert "Missing SHIB_CACHE_PREFIX!" in called_kwargs.get("ams_error", "") - - redis_connect.put("Shib-Session-1111",bytes("","utf-8")) # not cache_val - mock_redirect_ = mocker.patch("weko_accounts.views._redirect_method",return_value=make_response()) + redis_connect.put("Shib-Session-1111",bytes("","utf-8")) + mock_redirect_ = mocker.patch("weko_accounts.views._redirect_method", + return_value=make_response()) client.get(url+"?Shib-Session-ID=1111") mock_redirect_.assert_called_once() - assert redis_connect.redis.exists("Shib-Session-1111") == False + assert redis_connect.redis.exists("Shib-Session-1111") is False + + # not cache_val(AMS) + redis_connect.put("Shib-Session-1111",bytes("","utf-8")) + mock_redirect_ = mocker.patch("weko_accounts.views._redirect_method", + return_value=make_response()) + client.get(url+"?next=ams&Shib-Session-ID=1111") + mock_redirect_.assert_called_once() + called_args, called_kwargs = mock_redirect_.call_args + assert called_args[0] is True + assert "Missing SHIB_ATTR!" in called_kwargs.get("ams_error", "") + # is_auto_bind is false, check_in is error mock_get_relation = mocker.patch("weko_accounts.views.ShibUser.get_relation_info") mock_new_relation = mocker.patch("weko_accounts.views.ShibUser.new_relation_info") mock_shib_login = mocker.patch("weko_accounts.views.ShibUser.shib_user_login") redis_connect.put("Shib-Session-1111",bytes('{"shib_eppn":"test_eppn"}',"utf-8")) - # is_auto_bind is false, check_in is error - mock_redirect_ = mocker.patch("weko_accounts.views._redirect_method",return_value=make_response()) + mock_redirect_ = mocker.patch("weko_accounts.views._redirect_method", + return_value=make_response()) with patch("weko_accounts.views.ShibUser.check_in",return_value="test_error"): client.get(url+"?Shib-Session-ID=1111") mock_get_relation.assert_called_once() mock_redirect_.assert_called_once() - assert redis_connect.redis.exists("Shib-Session-1111") == False + assert redis_connect.redis.exists("Shib-Session-1111") is False + + # is_auto_bind is false, check_in is error(AMS) + mock_get_relation = mocker.patch("weko_accounts.views.ShibUser.get_relation_info") + mock_new_relation = mocker.patch("weko_accounts.views.ShibUser.new_relation_info") + mock_shib_login = mocker.patch("weko_accounts.views.ShibUser.shib_user_login") redis_connect.put("Shib-Session-1111",bytes('{"shib_eppn":"test_eppn"}',"utf-8")) + mock_redirect_ = mocker.patch("weko_accounts.views._redirect_method", + return_value=make_response()) + with patch("weko_accounts.views.ShibUser.check_in",return_value="test_error"): + client.get(url+"?next=ams&Shib-Session-ID=1111") + mock_get_relation.assert_called_once() + mock_redirect_.assert_called_once() + called_args, called_kwargs = mock_redirect_.call_args + assert called_args[0] is True + assert "test_error" in called_kwargs.get("ams_error", "") + redis_connect.put("Shib-Session-1111",bytes('{"shib_eppn":"test_eppn"}',"utf-8")) set_session(client,{"shib_session_id":"1111"}) with patch("weko_accounts.views.ShibUser.check_in",return_value=None): # is_auto_bind is true,shib_user is None shibuser = ShibUser({}) shibuser.user = User(id=1) with patch("weko_accounts.views.ShibUser",return_value=shibuser): - mock_redirect = mocker.patch("weko_accounts.views.redirect",return_value=make_response()) + mock_redirect = mocker.patch("weko_accounts.views.redirect", + return_value=make_response()) client.get(url) mock_new_relation.assert_called_once() mock_shib_login.assert_not_called() mock_redirect.assert_called_with("/") - assert redis_connect.redis.exists("Shib-Session-1111") == False + assert redis_connect.redis.exists("Shib-Session-1111") is False # is_auto_bind is true,shib_user exis redis_connect.put("Shib-Session-1111",bytes('{"shib_eppn":"test_eppn"}',"utf-8")) @@ -165,19 +272,50 @@ def test_shib_auto_login(client,redis_connect,mocker): shibuser.shib_user = "test_user" shibuser.user = User(id=1) with patch("weko_accounts.views.ShibUser",return_value=shibuser): - mock_redirect = mocker.patch("weko_accounts.views.redirect",return_value=make_response()) + mock_redirect = mocker.patch("weko_accounts.views.redirect", + return_value=make_response()) client.get(url+'?next=/next_page') mock_redirect.assert_called_with("/next_page") mock_shib_login.assert_called_once() - assert redis_connect.redis.exists("Shib-Session-1111") == False + assert redis_connect.redis.exists("Shib-Session-1111") is False + + # is_auto_bind is true,shib_user exis(AMS) + mock_shib_login.reset_mock() + redis_connect.put("Shib-Session-1111",bytes('{"shib_eppn":"test_eppn"}',"utf-8")) + set_session(client,{"shib_session_id":"1111","next":"/next_page"}) + + shibuser = ShibUser({}) + shibuser.shib_user = "test_user" + shibuser.user = User(id=1) + with patch("weko_accounts.views.ShibUser",return_value=shibuser): + mock_redirect = mocker.patch("weko_accounts.views.redirect", + return_value=make_response()) + client.get(url+'?next=ams') + called_args, _ = mock_redirect.call_args + mock_redirect.assert_called_with("/?next=ams") + mock_shib_login.assert_called_once() + assert redis_connect.redis.exists("Shib-Session-1111") is False + # raise BaseException with patch("weko_accounts.views.RedisConnection",side_effect=BaseException("test_error")): res = client.get(url+"?Shib-Session-ID=1111") assert res.status_code == 400 + + mock_redirect_ = mocker.patch("weko_accounts.views._redirect_method", + return_value=make_response()) + with patch("weko_accounts.views.RedisConnection",side_effect=BaseException("test_error")): + res = client.get(url+"?next=ams&Shib-Session-ID=1111") + mock_redirect_.assert_called_once() + called_args, called_kwargs = mock_redirect_.call_args + assert called_args[0] is True + assert "Server error has occurred. Please contact server " \ + "administrator." in called_kwargs.get("ams_error", "") + #def confirm_user(): # .tox/c1/bin/pytest --cov=weko_accounts tests/test_views.py::test_confirm_user -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-workflow/.tox/c1/tmp def test_confirm_user(client,redis_connect,mocker): - mocker.patch("weko_accounts.views.RedisConnection.connection",return_value=redis_connect) + mocker.patch("weko_accounts.views.RedisConnection.connection", + return_value=redis_connect) mocker.patch("weko_accounts.views.ShibUser.shib_user_login") url = url_for("weko_accounts.confirm_user") @@ -188,19 +326,53 @@ def test_confirm_user(client,redis_connect,mocker): client.post(url,data=form) mock_flash.assert_called_with("csrf_random",category="error") + # not correct csrf_random(AMS) + form = {"csrf_random":"test_csrf"} + mock_redirect_ = mocker.patch("weko_accounts.views._redirect_method", + return_value=make_response()) + set_session(client,{"next": "ams"}) + client.post(url, data=form) + mock_redirect_.assert_called_once() + called_args, called_kwargs = mock_redirect_.call_args + assert called_args[0] is True + assert "csrf_random" in called_kwargs.get("ams_error", "") + # not exist shib_session_id set_session(client,{"csrf_random":"test_csrf","shib_session_id":None}) + del_session(client, "next") mock_flash = mocker.patch("weko_accounts.views.flash") client.post(url,data=form) mock_flash.assert_called_with("shib_session_id",category="error") + # not exist shib_session_id(AMS) + mock_redirect_ = mocker.patch("weko_accounts.views._redirect_method", + return_value=make_response()) + set_session(client,{"next": "ams"}) + client.post(url, data=form) + mock_redirect_.assert_called_once() + called_args, called_kwargs = mock_redirect_.call_args + assert called_args[0] is True + assert "Missing Shib-Session-ID!" in called_kwargs.get("ams_error", "") + # not exist cache_key set_session(client,{"csrf_random":"test_csrf","shib_session_id":"2222"}) + del_session(client, "next") mock_flash = mocker.patch("weko_accounts.views.flash") client.post(url,data=form) mock_flash.assert_called_with("cache_key",category="error") + # not exist cache_key(AMS) + mock_redirect_ = mocker.patch("weko_accounts.views._redirect_method", + return_value=make_response()) + set_session(client,{"next": "ams"}) + client.post(url, data=form) + mock_redirect_.assert_called_once() + called_args, called_kwargs = mock_redirect_.call_args + assert called_args[0] is True + assert "Missing SHIB_CACHE_PREFIX!" in called_kwargs.get("ams_error", "") + set_session(client,{"csrf_random":"test_csrf","shib_session_id":"1111"}) + del_session(client, "next") # not exist cache_value redis_connect.put("Shib-Session-1111",bytes("","utf-8")) mock_flash = mocker.patch("weko_accounts.views.flash") @@ -208,20 +380,45 @@ def test_confirm_user(client,redis_connect,mocker): mock_flash.assert_called_with("cache_val",category="error") assert redis_connect.redis.exists("Shib-Session-1111") is False + # not exist cache_value(AMS) + redis_connect.put("Shib-Session-1111",bytes("","utf-8")) + mock_redirect_ = mocker.patch("weko_accounts.views._redirect_method", + return_value=make_response()) + set_session(client,{"next": "ams"}) + client.post(url, data=form) + mock_redirect_.assert_called_once() + called_args, called_kwargs = mock_redirect_.call_args + assert called_args[0] is True + assert "Missing SHIB_ATTR!" in called_kwargs.get("ams_error", "") + # shib_user.check_weko_user is false redis_connect.put("Shib-Session-1111",bytes('{"shib_eppn":"test_eppn"}',"utf-8")) with patch("weko_accounts.views.ShibUser.check_weko_user",return_value=False): mock_flash = mocker.patch("weko_accounts.views.flash") + del_session(client, "next") client.post(url,data=form) mock_flash.assert_called_with("check_weko_user",category="error") assert redis_connect.redis.exists("Shib-Session-1111") is False + # shib_user.check_weko_user is false(AMS) + redis_connect.put("Shib-Session-1111",bytes('{"shib_eppn":"test_eppn"}',"utf-8")) + with patch("weko_accounts.views.ShibUser.check_weko_user",return_value=False): + mock_redirect_ = mocker.patch("weko_accounts.views._redirect_method", + return_value=make_response()) + set_session(client, {"next": "ams"}) + client.post(url,data=form) + mock_redirect_.assert_called_once() + called_args, called_kwargs = mock_redirect_.call_args + assert called_args[0] is True + assert "There is no user information." in called_kwargs.get("ams_error", "") + redis_connect.put("Shib-Session-1111",bytes('{"shib_eppn":"test_eppn"}',"utf-8")) with patch("weko_accounts.views.ShibUser.check_weko_user",return_value=True): # shib_user.bind_relation_info is false with patch("weko_accounts.views.ShibUser.bind_relation_info",return_value=False): redis_connect.put("Shib-Session-1111",bytes('{"shib_eppn":"test_eppn"}',"utf-8")) mock_flash = mocker.patch("weko_accounts.views.flash") + del_session(client, "next") client.post(url,data=form) mock_flash.assert_called_with("FAILED bind_relation_info!",category="error") with patch("weko_accounts.views.ShibUser.bind_relation_info",return_value=True): @@ -236,35 +433,87 @@ def test_confirm_user(client,redis_connect,mocker): redis_connect.put("Shib-Session-1111",bytes('{"shib_eppn":"test_eppn"}',"utf-8")) shibuser = ShibUser({}) shibuser.user = User(id=1) - with patch("weko_accounts.views.ShibUser",return_value=shibuser): - mock_redirect = mocker.patch("weko_accounts.views.redirect",return_value=make_response()) + with patch("weko_accounts.views.ShibUser", + return_value=shibuser): + mock_redirect = mocker.patch("weko_accounts.views.redirect", + return_value=make_response()) client.post(url,data=form) mock_redirect.assert_called_with("/") assert redis_connect.redis.exists("Shib-Session-1111") is False - # exist ShibUser.shib_user - set_session(client,{"csrf_random":"test_csrf","shib_session_id":"1111","next":"/next_page"}) - redis_connect.put("Shib-Session-1111",bytes('{"shib_eppn":"test_eppn"}',"utf-8")) - + # exist ShibUser.shib_user + set_session(client,{"csrf_random":"test_csrf", + "shib_session_id":"1111","next":"/next_page"}) + redis_connect.put("Shib-Session-1111",bytes('{"shib_eppn":"test_eppn"}',"utf-8")) shibuser = ShibUser({}) shibuser.shib_user = "test_user" shibuser.user = User(id=1) with patch("weko_accounts.views.ShibUser",return_value=shibuser): - mock_redirect = mocker.patch("weko_accounts.views.redirect",return_value=make_response()) + mock_redirect = mocker.patch("weko_accounts.views.redirect", + return_value=make_response()) mock_flash = mocker.patch("weko_accounts.views.flash") client.post(url,data=form) mock_redirect.assert_called_with("/next_page") assert redis_connect.redis.exists("Shib-Session-1111") is False + redis_connect.put("Shib-Session-1111",bytes('{"shib_eppn":"test_eppn"}',"utf-8")) + with patch("weko_accounts.views.ShibUser.check_weko_user",return_value=True): + # shib_user.bind_relation_info is false + with patch("weko_accounts.views.ShibUser.bind_relation_info",return_value=False): + redis_connect.put("Shib-Session-1111",bytes('{"shib_eppn":"test_eppn"}',"utf-8")) + mock_redirect_ = mocker.patch("weko_accounts.views._redirect_method", + return_value=make_response()) + set_session(client, {"next": "ams"}) + client.post(url,data=form) + mock_redirect_.assert_called_once() + called_args, called_kwargs = mock_redirect_.call_args + assert called_args[0] is True + assert "FAILED bind_relation_info!" in called_kwargs.get("ams_error", "") + with patch("weko_accounts.views.ShibUser.bind_relation_info",return_value=True): + # ShibUser.check_in is error + with patch("weko_accounts.views.ShibUser.check_in",return_value="test_error"): + mock_redirect_ = mocker.patch("weko_accounts.views._redirect_method", + return_value=make_response()) + client.post(url,data=form) + mock_redirect_.assert_called_once() + called_args, called_kwargs = mock_redirect_.call_args + assert called_args[0] is True + assert "test_error" in called_kwargs.get("ams_error", "") + with patch("weko_accounts.views.ShibUser.check_in",return_value=None): + # ShibUser.shib_user is None,not exist next in session + redis_connect.put("Shib-Session-1111",bytes('{"shib_eppn":"test_eppn"}',"utf-8")) + shibuser = ShibUser({}) + shibuser.user = User(id=1) + with patch("weko_accounts.views.ShibUser",return_value=shibuser): + mock_redirect = mocker.patch("weko_accounts.views.redirect", + return_value=make_response()) + client.post(url,data=form) + called_args, _ = mock_redirect.call_args + mock_redirect.assert_called_with("/?next=ams") + assert redis_connect.redis.exists("Shib-Session-1111") is False + # raise BaseException with patch("weko_accounts.views._redirect_method",side_effect=BaseException("test_error")): + del_session(client, "next") res = client.post(url,data=form) assert res.status_code == 400 + mock_redirect_ = mocker.patch("weko_accounts.views._redirect_method", + return_value=make_response()) + with patch("weko_accounts.views.RedisConnection",side_effect=BaseException("test_error")): + set_session(client, {"next": "ams"}) + res = client.post(url,data=form) + mock_redirect_.assert_called_once() + called_args, called_kwargs = mock_redirect_.call_args + assert called_args[0] is True + assert "Server error has occurred. Please contact server " \ + "administrator." in called_kwargs.get("ams_error", "") + #def confirm_user_without_page(): # .tox/c1/bin/pytest --cov=weko_accounts tests/test_views.py::test_confirm_user_without_page -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-workflow/.tox/c1/tmp def test_confirm_user_without_page(client,redis_connect,mocker): - mocker.patch("weko_accounts.views.RedisConnection.connection",return_value=redis_connect) + mocker.patch("weko_accounts.views.RedisConnection.connection", + return_value=redis_connect) mocker.patch("weko_accounts.views.ShibUser.shib_user_login") url = url_for("weko_accounts.confirm_user_without_page") @@ -273,11 +522,29 @@ def test_confirm_user_without_page(client,redis_connect,mocker): client.get(url, query_string={"Shib-Session-ID":None}) mock_flash.assert_called_with("shib_session_id",category="error") + # not exist shib_session_id(AMS) + mock_redirect_ = mocker.patch("weko_accounts.views._redirect_method", + return_value=make_response()) + client.get(url+"?next=ams") + mock_redirect_.assert_called_once() + called_args, called_kwargs = mock_redirect_.call_args + assert called_args[0] is True + assert "Missing Shib-Session-ID!" in called_kwargs.get("ams_error", "") + # not exist cache_key mock_flash = mocker.patch("weko_accounts.views.flash") client.get(url, query_string={"Shib-Session-ID":"2222"}) mock_flash.assert_called_with("cache_key",category="error") + # not exist cache_key(AMS) + mock_redirect_ = mocker.patch("weko_accounts.views._redirect_method", + return_value=make_response()) + client.get(url, query_string={"next":"ams","Shib-Session-ID":"2222"}) + mock_redirect_.assert_called_once() + called_args, called_kwargs = mock_redirect_.call_args + assert called_args[0] is True + assert "Missing SHIB_CACHE_PREFIX!" in called_kwargs.get("ams_error", "") + # not exist cache_value redis_connect.put("Shib-Session-1111",bytes("","utf-8")) mock_flash = mocker.patch("weko_accounts.views.flash") @@ -285,6 +552,16 @@ def test_confirm_user_without_page(client,redis_connect,mocker): mock_flash.assert_called_with("cache_val",category="error") assert redis_connect.redis.exists("Shib-Session-1111") is False + # not exist cache_value(AMS) + redis_connect.put("Shib-Session-1111",bytes("","utf-8")) + mock_redirect_ = mocker.patch("weko_accounts.views._redirect_method", + return_value=make_response()) + client.get(url, query_string={"next":"ams","Shib-Session-ID":"1111"}) + mock_redirect_.assert_called_once() + called_args, called_kwargs = mock_redirect_.call_args + assert called_args[0] is True + assert "Missing SHIB_ATTR!" in called_kwargs.get("ams_error", "") + redis_connect.put("Shib-Session-1111",bytes('{"shib_eppn":"test_eppn"}',"utf-8")) with patch("weko_accounts.views.ShibUser.check_weko_user",return_value=True): # shib_user.bind_relation_info is false @@ -305,7 +582,8 @@ def test_confirm_user_without_page(client,redis_connect,mocker): with patch("weko_accounts.views.ShibUser.check_in",return_value=None): # ShibUser.shib_user is None,not exist next in session redis_connect.put("Shib-Session-1111",bytes('{"shib_eppn":"test_eppn"}',"utf-8")) - mock_redirect = mocker.patch("weko_accounts.views.redirect",return_value=make_response()) + mock_redirect = mocker.patch("weko_accounts.views.redirect", + return_value=make_response()) client.get(url, query_string={"Shib-Session-ID":"1111"}) mock_redirect.assert_called_with("/") assert redis_connect.redis.exists("Shib-Session-1111") is False @@ -316,7 +594,8 @@ def test_confirm_user_without_page(client,redis_connect,mocker): shibuser = ShibUser({}) shibuser.shib_user = "test_user" with patch("weko_accounts.views.ShibUser",return_value=shibuser): - mock_redirect = mocker.patch("weko_accounts.views.redirect",return_value=make_response()) + mock_redirect = mocker.patch("weko_accounts.views.redirect", + return_value=make_response()) mock_flash = mocker.patch("weko_accounts.views.flash") client.get(url, query_string={"Shib-Session-ID":"1111"}) mock_redirect.assert_called_with("/") @@ -328,22 +607,96 @@ def test_confirm_user_without_page(client,redis_connect,mocker): shibuser = ShibUser({}) shibuser.shib_user = "test_user" with patch("weko_accounts.views.ShibUser",return_value=shibuser): - mock_redirect = mocker.patch("weko_accounts.views.redirect",return_value=make_response()) + mock_redirect = mocker.patch("weko_accounts.views.redirect", + return_value=make_response()) mock_flash = mocker.patch("weko_accounts.views.flash") client.get(url, query_string={"Shib-Session-ID":"1111","next":"/next_page"}) mock_redirect.assert_called_with("/next_page") assert redis_connect.redis.exists("Shib-Session-1111") is False + redis_connect.put("Shib-Session-1111",bytes('{"shib_eppn":"test_eppn"}',"utf-8")) + with patch("weko_accounts.views.ShibUser.check_weko_user",return_value=True): + # shib_user.bind_relation_info is false + with patch("weko_accounts.views.ShibUser.bind_relation_info",return_value=False): + redis_connect.put("Shib-Session-1111",bytes('{"shib_eppn":"test_eppn"}',"utf-8")) + mock_redirect_ = mocker.patch("weko_accounts.views._redirect_method", + return_value=make_response()) + client.get(url, query_string={"next":"ams","Shib-Session-ID":"1111"}) + mock_redirect_.assert_called_once() + called_args, called_kwargs = mock_redirect_.call_args + assert called_args[0] is True + assert "FAILED bind_relation_info!" in called_kwargs.get("ams_error", "") + with patch("weko_accounts.views.ShibUser.bind_relation_info",return_value=True): + # ShibUser.check_in is error + with patch("weko_accounts.views.ShibUser.check_in",return_value="test_error"): + redis_connect.put("Shib-Session-1111",bytes('{"shib_eppn":"test_eppn"}',"utf-8")) + mock_redirect_ = mocker.patch("weko_accounts.views._redirect_method", + return_value=make_response()) + client.get(url, query_string={"next":"ams","Shib-Session-ID":"1111"}) + mock_redirect_.assert_called_once() + called_args, called_kwargs = mock_redirect_.call_args + assert called_args[0] is True + assert "test_error" in called_kwargs.get("ams_error", "") + with patch("weko_accounts.views.ShibUser.check_in",return_value=None): + # ShibUser.shib_user is None,not exist next in session + redis_connect.put("Shib-Session-1111",bytes('{"shib_eppn":"test_eppn"}',"utf-8")) + shibuser = ShibUser({}) + shibuser.user = User(id=1) + with patch("weko_accounts.views.ShibUser",return_value=shibuser): + mock_redirect = mocker.patch("weko_accounts.views.redirect", + return_value=make_response()) + client.get(url, query_string={"next":"ams","Shib-Session-ID":"1111"}) + called_args, _ = mock_redirect.call_args + mock_redirect.assert_called_with("/?next=ams") + assert redis_connect.redis.exists("Shib-Session-1111") is False + + # exist ShibUser.shib_user + redis_connect.put("Shib-Session-1111",bytes('{"shib_eppn":"test_eppn"}',"utf-8")) + + shibuser = ShibUser({}) + shibuser.shib_user = "test_user" + shibuser.user = User(id=1) + with patch("weko_accounts.views.ShibUser",return_value=shibuser): + mock_redirect = mocker.patch("weko_accounts.views.redirect", + return_value=make_response()) + client.get(url, query_string={"next":"ams","Shib-Session-ID":"1111"}) + called_args, _ = mock_redirect.call_args + mock_redirect.assert_called_with("/?next=ams") + assert redis_connect.redis.exists("Shib-Session-1111") is False + + # exist ShibUser.shib_user + redis_connect.put("Shib-Session-1111",bytes('{"shib_eppn":"test_eppn"}',"utf-8")) + shibuser = ShibUser({}) + shibuser.shib_user = "test_user" + shibuser.user = User(id=1) + with patch("weko_accounts.views.ShibUser",return_value=shibuser): + mock_redirect = mocker.patch("weko_accounts.views.redirect", + return_value=make_response()) + client.get(url, query_string={"next":"ams","Shib-Session-ID":"1111"}) + called_args, _ = mock_redirect.call_args + mock_redirect.assert_called_with("/?next=ams") + assert redis_connect.redis.exists("Shib-Session-1111") is False + # raise BaseException with patch("weko_accounts.views._redirect_method",side_effect=BaseException("test_error")): res = client.get(url) assert res.status_code == 400 + mock_redirect_ = mocker.patch("weko_accounts.views._redirect_method", + return_value=make_response()) + with patch("weko_accounts.views.RedisConnection",side_effect=BaseException("test_error")): + client.get(url, query_string={"next":"ams","Shib-Session-ID":"1111"}) + mock_redirect_.assert_called_once() + called_args, called_kwargs = mock_redirect_.call_args + assert called_args[0] is True + assert "Server error has occurred. Please contact server " \ + "administrator." in called_kwargs.get("ams_error", "") #def shib_login(): # .tox/c1/bin/pytest --cov=weko_accounts tests/test_views.py::test_shib_login -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-workflow/.tox/c1/tmp def test_shib_login(client,redis_connect,users,mocker): - mocker.patch("weko_accounts.views.RedisConnection.connection",return_value=redis_connect) + mocker.patch("weko_accounts.views.RedisConnection.connection", + return_value=redis_connect) mocker.patch("weko_accounts.views.generate_random_str",return_value="asdfghjkl") url_base = url_for("weko_accounts.shib_login") @@ -352,13 +705,32 @@ def test_shib_login(client,redis_connect,users,mocker): client.get(url_base) mock_flash.assert_called_with("Missing Shib-Session-ID!",category="error") - url = url_base+"?Shib-Session-ID=2222" + # not shib_session_id(AMS) + mock_redirect_ = mocker.patch("weko_accounts.views._redirect_method", + return_value=make_response()) + client.get(url_base+"?next=ams") + mock_redirect_.assert_called_once() + called_args, called_kwargs = mock_redirect_.call_args + assert called_args[0] is True + assert "Missing Shib-Session-ID!" in called_kwargs.get("ams_error", "") + url = url_base+"?Shib-Session-ID=2222" # not exist cache_key mock_flash = mocker.patch("weko_accounts.views.flash") client.get(url) mock_flash.assert_called_with("Missing SHIB_CACHE_PREFIX!",category="error") + # not exist cache_key(AMS) + mocker.patch("weko_accounts.views.RedisConnection.connection", + return_value=redis_connect) + mock_redirect_ = mocker.patch("weko_accounts.views._redirect_method", + return_value=make_response()) + client.get(url+"&next=ams") + mock_redirect_.assert_called_once() + called_args, called_kwargs = mock_redirect_.call_args + assert called_args[0] is True + assert "Missing SHIB_CACHE_PREFIX!" in called_kwargs.get("ams_error", "") + url = url_base+"?Shib-Session-ID=1111" # not cache_val redis_connect.put("Shib-Session-1111",bytes('',"utf-8")) @@ -367,23 +739,50 @@ def test_shib_login(client,redis_connect,users,mocker): mock_flash.assert_called_with("Missing SHIB_ATTR!",category="error") assert redis_connect.redis.exists("Shib-Session-1111") is False + # not cache_val(AMS) + redis_connect.put("Shib-Session-1111",bytes('',"utf-8")) + mock_redirect_ = mocker.patch("weko_accounts.views._redirect_method", + return_value=make_response()) + client.get(url+"&next=ams") + mock_redirect_.assert_called_once() + called_args, called_kwargs = mock_redirect_.call_args + assert called_args[0] is True + assert "Missing SHIB_ATTR!" in called_kwargs.get("ams_error", "") + # exist user - redis_connect.put("Shib-Session-1111",bytes('{"shib_eppn":"test_eppn","shib_mail":"user@test.org"}',"utf-8")) - mock_render = mocker.patch("weko_accounts.views.render_template",return_value=make_response()) + redis_connect.put("Shib-Session-1111", + bytes('{"shib_eppn":"test_eppn","shib_mail":"user@test.org"}',"utf-8")) + mock_render = mocker.patch("weko_accounts.views.render_template", + return_value=make_response()) client.get(url) - mock_render.assert_called_with('weko_accounts/confirm_user.html',csrf_random="asdfghjkl",email="user@test.org") + mock_render.assert_called_with('weko_accounts/confirm_user.html', + csrf_random="asdfghjkl",email="user@test.org") # not exist user - redis_connect.put("Shib-Session-1111",bytes('{"shib_eppn":"test_eppn","shib_mail":"not_exist_user@test.org"}',"utf-8")) - mock_render = mocker.patch("weko_accounts.views.render_template",return_value=make_response()) + redis_connect.put("Shib-Session-1111", + bytes('{"shib_eppn":"test_eppn",' \ + '"shib_mail":"not_exist_user@test.org"}',"utf-8")) + mock_render = mocker.patch("weko_accounts.views.render_template", + return_value=make_response()) client.get(url) - mock_render.assert_called_with('weko_accounts/confirm_user.html',csrf_random="asdfghjkl",email="") + mock_render.assert_called_with('weko_accounts/confirm_user.html', + csrf_random="asdfghjkl",email="") # raise BaseException with patch("weko_accounts.views.flash",side_effect=BaseException("test_error")): res = client.get(url_base) assert res.status_code == 400 + mock_redirect_ = mocker.patch("weko_accounts.views._redirect_method", + return_value=make_response()) + with patch("weko_accounts.views.RedisConnection",side_effect=BaseException("test_error")): + res = client.get(url_base+"?Shib-Session-ID=1111&next=ams") + mock_redirect_.assert_called_once() + called_args, called_kwargs = mock_redirect_.call_args + assert called_args[0] is True + assert "Server error has occurred. Please contact server " \ + "administrator." in called_kwargs.get("ams_error", "") + #def shib_sp_login(): # .tox/c1/bin/pytest --cov=weko_accounts tests/test_views.py::test_shib_sp_login -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-workflow/.tox/c1/tmp def test_shib_sp_login(client, redis_connect,mocker, db, users): @@ -397,6 +796,14 @@ def test_shib_sp_login(client, redis_connect,mocker, db, users): mock_flash.assert_called_with("Missing Shib-Session-ID!",category="error") mock_redirect.assert_called_with(url_for("security.login")) + # not shib_session_id(AMS) + mock_generate_ams_login_url = mocker.patch("weko_accounts.views.generate_ams_login_url", + return_value=make_response()) + client.post(url+"?next=ams") + mock_generate_ams_login_url.assert_called_once() + called_args, _ = mock_generate_ams_login_url.call_args + assert "Missing Shib-Session-ID!" in called_args[0] + current_app.config.update( WEKO_ACCOUNTS_SHIB_LOGIN_ENABLED=True ) @@ -410,6 +817,15 @@ def test_shib_sp_login(client, redis_connect,mocker, db, users): client.post(url,data=form) mock_flash.assert_called_with("Missing SHIB_ATTRs!",category="error") + # parse_attribute is error(AMS) + with patch("weko_accounts.views.parse_attributes",return_value=("attr",True)): + mock_generate_ams_login_url = mocker.patch("weko_accounts.views.generate_ams_login_url", + return_value=make_response()) + client.post(url+"?next=ams",data=form) + mock_generate_ams_login_url.assert_called_once() + called_args, _ = mock_generate_ams_login_url.call_args + assert "Missing SHIB_ATTRs!" in called_args[0] + # Check if shib_eppn is not included in the blocked user list try: db.session.add(AdminSettings( @@ -418,7 +834,7 @@ def test_shib_sp_login(client, redis_connect,mocker, db, users): settings='{"blocked_ePPNs": ["ePPN1", "ePPN2", "ePPN3", "ePPN5", "ePPP*"]}' )) db.session.commit() - except Exception as e: + except Exception: db.session.rollback() raise finally: @@ -432,7 +848,16 @@ def test_shib_sp_login(client, redis_connect,mocker, db, users): } client.post(url,data=form) mock_flash.assert_called_with("Failed to login.",category="error") - mock_redirect_ = mocker.patch("weko_accounts.views._redirect_method",return_value=make_response()) + mock_redirect_ = mocker.patch("weko_accounts.views._redirect_method", + return_value=make_response()) + + # Match with blocked user(AMS) + mock_generate_ams_login_url = mocker.patch("weko_accounts.views.generate_ams_login_url", + return_value=make_response()) + client.post(url+"?next=ams",data=form) + mock_generate_ams_login_url.assert_called_once() + called_args, _ = mock_generate_ams_login_url.call_args + assert "Login is blocked." in called_args[0] # Match found with a blocked user from the wildcard mock_flash = mocker.patch("weko_accounts.views.flash") @@ -442,7 +867,14 @@ def test_shib_sp_login(client, redis_connect,mocker, db, users): } client.post(url,data=form) mock_flash.assert_called_with("Failed to login.",category="error") - mock_redirect_ = mocker.patch("weko_accounts.views._redirect_method",return_value=make_response()) + + # Match found with a blocked user from the wildcard(AMS) + mock_generate_ams_login_url = mocker.patch("weko_accounts.views.generate_ams_login_url", + return_value=make_response()) + client.post(url+"?next=ams",data=form) + mock_generate_ams_login_url.assert_called_once() + called_args, _ = mock_generate_ams_login_url.call_args + assert "Login is blocked." in called_args[0] # Not a blocked user form = { @@ -450,17 +882,24 @@ def test_shib_sp_login(client, redis_connect,mocker, db, users): "eppn":"test_eppn" } + mock_redirect_ = mocker.patch("weko_accounts.views._redirect_method", + return_value=make_response()) # WEKO_ACCOUNTS_SHIB_BIND_GAKUNIN_MAP_GROUPSがTrueの場合のテスト current_app.config.update( WEKO_ACCOUNTS_SHIB_BIND_GAKUNIN_MAP_GROUPS=True ) - mock_sync_shib_gakunin_map_groups = mocker.patch("weko_accounts.views.sync_shib_gakunin_map_groups", return_value=None) + mock_sync_shib_gakunin_map_groups = \ + mocker.patch("weko_accounts.views.sync_shib_gakunin_map_groups", + return_value=None) client.post(url, data=form) mock_sync_shib_gakunin_map_groups.assert_called_once() # sync_shib_gakunin_map_groupsが例外をスローする場合のテスト - mock_sync_shib_gakunin_map_groups = mocker.patch("weko_accounts.views.sync_shib_gakunin_map_groups", side_effect=Exception("test_exception")) - mock_redirect_method = mocker.patch("weko_accounts.views._redirect_method", return_value=make_response()) + mock_sync_shib_gakunin_map_groups = mocker.patch( + "weko_accounts.views.sync_shib_gakunin_map_groups", + side_effect=Exception("test_exception")) + mock_redirect_method = mocker.patch("weko_accounts.views._redirect_method", + return_value=make_response()) res = client.post(url, data=form) mock_redirect_method.assert_called_once() assert res.status_code == 200 # _redirect_methodが呼び出されることを確認 @@ -474,22 +913,61 @@ def test_shib_sp_login(client, redis_connect,mocker, db, users): mock_sync_shib_gakunin_map_groups.assert_not_called() # shib_user.get_relation_info is None - with patch("weko_accounts.views.ShibUser.get_relation_info",return_value=None)\ - and patch("weko_accounts.views.redirect",return_value=make_response()): + with patch("weko_accounts.views.ShibUser.get_relation_info", + return_value=None)\ + and patch("weko_accounts.views.redirect", + return_value=make_response()): res = client.post(url,data=form) assert res.status_code == 200 assert res.data.decode() == "/weko/shib/login?Shib-Session-ID=1111&next=%2F" with client.session_transaction() as session: assert 'shib_session_id' not in session # shib_user.get_relation_info is not None - with patch("weko_accounts.views.ShibUser.get_relation_info",return_value="chib_user")\ - and patch("weko_accounts.views.redirect",return_value=make_response()): + with patch("weko_accounts.views.ShibUser.get_relation_info", + return_value="chib_user")\ + and patch("weko_accounts.views.redirect", + return_value=make_response()): + res = client.post(url,data=form) + assert res.status_code == 200 + assert res.data.decode() == "/weko/shib/login?Shib-Session-ID=1111&next=%2F" + with client.session_transaction() as session: + assert 'shib_session_id' not in session + + # test without blocked_user_settings + AdminSettings.query.filter_by(id=11).delete() + db.session.commit() + with patch("weko_accounts.views.ShibUser.get_relation_info", + return_value=None)\ + and patch("weko_accounts.views.redirect", + return_value=make_response()): res = client.post(url,data=form) assert res.status_code == 200 assert res.data.decode() == "/weko/shib/login?Shib-Session-ID=1111&next=%2F" with client.session_transaction() as session: assert 'shib_session_id' not in session + # test with blocked_user_setting dict + db.session.add(AdminSettings( + id=11, + name="blocked_user_settings", + settings={"blocked_ePPNs": ["ePPN1", "ePPN2", "ePPN3", "ePPN5", "ePPP*"]} + )) + db.session.commit() + mock_flash = mocker.patch("weko_accounts.views.flash") + form_blocked = { + "Shib-Session-ID":"1111", + "eppn":"ePPN3" + } + client.post(url,data=form_blocked) + mock_flash.assert_called_with("Failed to login.",category="error") + AdminSettings.query.filter_by(id=11).delete() + db.session.add(AdminSettings( + id=11, + name="blocked_user_settings", + settings='{"blocked_ePPNs": ["ePPN1", "ePPN2", "ePPN3", "ePPN5", "ePPP*"]}' + )) + db.session.commit() + current_app.config.update( WEKO_ACCOUNTS_SHIB_LOGIN_ENABLED=True, WEKO_ACCOUNTS_SKIP_CONFIRMATION_PAGE=True @@ -522,10 +1000,25 @@ def test_shib_sp_login(client, redis_connect,mocker, db, users): # raise BaseException with patch("weko_accounts.views.flash",side_effect=BaseException("test_error"))\ - and patch("weko_accounts.views._redirect_method",return_value=make_response()) as mock_redirect_: + and patch("weko_accounts.views._redirect_method", + return_value=make_response()) as mock_redirect_: res = client.post(url,data={}) mock_redirect_.assert_called_once() + # raise BaseException(AMS) + mock_generate_ams_login_url = mocker.patch("weko_accounts.views.generate_ams_login_url", + return_value=make_response()) + with patch("weko_accounts.views.RedisConnection",side_effect=BaseException("test_error")): + form = { + "Shib-Session-ID":"1111", + "eppn":"test_eppn" + } + res = client.post(url+"?next=ams",data=form) + mock_generate_ams_login_url.assert_called_once() + called_args, _ = mock_generate_ams_login_url.call_args + assert "Server error has occurred. Please contact server " \ + "administrator." in called_args[0] + # all attributes have value and some shibboleth_user records don't have target eppn current_app.config.update( WEKO_ACCOUNTS_SHIB_LOGIN_ENABLED=True, @@ -737,7 +1230,8 @@ def test_shib_stub_login(client,mocker): WEKO_ACCOUNTS_SHIB_IDP_LOGIN_ENABLED=False ) # WEKO_ACCOUNTS_SHIB_IDP_LOGIN_ENABLED is true - mock_render_template = mocker.patch("weko_accounts.views.render_template",return_value=make_response()) + mock_render_template = mocker.patch("weko_accounts.views.render_template", + return_value=make_response()) res = client.get(url) mock_render_template.assert_called_with( 'weko_accounts/login_shibuser_pattern_1.html', diff --git a/modules/weko-accounts/weko_accounts/config.py b/modules/weko-accounts/weko_accounts/config.py index 069f93dc57..4805604d67 100644 --- a/modules/weko-accounts/weko_accounts/config.py +++ b/modules/weko-accounts/weko_accounts/config.py @@ -65,6 +65,8 @@ WEKO_ACCOUNTS_SHIB_IDP_LOGIN_URL = '{}secure/login.py' """Login proxy URL.""" +WEKO_ACCOUNTS_SHIB_AMS_LOGIN_URL = '{}ams/login' + WEKO_ACCOUNTS_SSO_ATTRIBUTE_MAP = { 'SHIB_ATTR_EPPN': (False, 'shib_eppn'), # 'SHIB_ATTR_LOGIN_ID': (False, 'shib_uid'), @@ -264,4 +266,4 @@ """Disable user registration via WEKO-Accounts.""" SECURITY_RECOVERABLE = False -"""Disable password recovery via WEKO-Accounts.""" \ No newline at end of file +"""Disable password recovery via WEKO-Accounts.""" diff --git a/modules/weko-accounts/weko_accounts/views.py b/modules/weko-accounts/weko_accounts/views.py index a26e313928..23247701fa 100644 --- a/modules/weko-accounts/weko_accounts/views.py +++ b/modules/weko-accounts/weko_accounts/views.py @@ -143,7 +143,7 @@ def _adjust_shib_admin_DB(): db.session.commit() -def _redirect_method(has_next=False): +def _redirect_method(has_next=False, ams_error=None): """Redirect method for instance login to IdP.""" shib_login = current_app.config['WEKO_ACCOUNTS_SHIB_LOGIN_ENABLED'] shib_login_url = current_app.config['WEKO_ACCOUNTS_SHIB_IDP_LOGIN_URL'] @@ -151,7 +151,13 @@ def _redirect_method(has_next=False): idp_login_inst = current_app.config[ 'WEKO_ACCOUNTS_SHIB_INST_LOGIN_DIRECTLY_ENABLED'] - if shib_login and idp_login and idp_login_inst: + if ams_error: + shib_ams_login_url = current_app.config['WEKO_ACCOUNTS_SHIB_AMS_LOGIN_URL'] + ams_url = shib_ams_login_url.format(request.url_root) + encoded_error = quote_plus(str(ams_error), encoding='utf-8', errors='replace') + url = f'{ams_url}?error={encoded_error}' + return redirect(url) + elif shib_login and idp_login and idp_login_inst: url = shib_login_url.format(request.url_root) if has_next: url += '?next=' + request.full_path @@ -162,6 +168,28 @@ def _redirect_method(has_next=False): next=request.full_path if has_next else None)) +def generate_ams_login_url(ams_error): + """ + Generate a redirect URL for the AMS login page with error details. + + The returned URL starts with a '/'. + + Args: + ams_error (str): Error message for the AMS login page to include + in the redirect URL. + + Returns: + str: URL for the AMS login page including the error + information as a query parameter. + """ + shib_ams_login_url = current_app.config['WEKO_ACCOUNTS_SHIB_AMS_LOGIN_URL'] + ams_url = shib_ams_login_url.format('/') + encoded_error = quote_plus( + str(ams_error), encoding='utf-8', errors='replace') + url = f'{ams_url}?error={encoded_error}' + return url + + @blueprint.route('/') def index(): """Render a basic view.""" @@ -176,29 +204,42 @@ def shib_auto_login(): :return: next url """ + ams_login = False try: is_auto_bind = False shib_session_id = request.args.get('Shib-Session-ID', None) - session['next'] = request.args.get('next', '/') - + next_value = request.args.get('next') + if next_value != 'ams': + next_value = session.get('next', '/') + session['next'] = next_value + ams_login = next_value == 'ams' if not shib_session_id: shib_session_id = session['shib_session_id'] is_auto_bind = True if not shib_session_id: - return _redirect_method() + if ams_login: + return _redirect_method(True, ams_error=_('Missing Shib-Session-ID!')) + else: + return _redirect_method() redis_connection = RedisConnection() datastore = redis_connection.connection(db=current_app.config['CACHE_REDIS_DB'], kv = True) cache_key = current_app.config[ 'WEKO_ACCOUNTS_SHIB_CACHE_PREFIX'] + shib_session_id if not datastore.redis.exists(cache_key): - return _redirect_method() + if ams_login: + return _redirect_method(True, ams_error=_('Missing SHIB_CACHE_PREFIX!')) + else: + return _redirect_method() cache_val = datastore.get(cache_key) if not cache_val: datastore.delete(cache_key) - return _redirect_method() + if ams_login: + return _redirect_method(True, ams_error=_('Missing SHIB_ATTR!')) + else: + return _redirect_method() cache_val = json.loads(str(cache_val, encoding='utf-8')) shib_user = ShibUser(cache_val) @@ -212,8 +253,11 @@ def shib_auto_login(): if error: datastore.delete(cache_key) current_app.logger.error(error) - flash(error, category='error') - return _redirect_method() + if ams_login: + return _redirect_method(True, ams_error=error) + else: + flash(error, category='error') + return _redirect_method() if shib_user.shib_user: shib_user.shib_user_login() @@ -225,7 +269,10 @@ def shib_auto_login(): target_key=shib_user.user.id, remarks="Shibboleth login" ) - return redirect(session['next'] if 'next' in session else '/') + if ams_login: + return redirect('/?next=ams') + else: + return redirect(session['next'] if 'next' in session else '/') except BaseException: db.session.rollback() current_app.logger.error("Unexpected error: {}".format(sys.exc_info())) @@ -235,6 +282,9 @@ def shib_auto_login(): operation="LOGIN", remarks=tb_info[0] ) + if ams_login: + return _redirect_method(True, ams_error=_( + 'Server error has occurred. Please contact server administrator.')) return abort(400) @@ -244,15 +294,25 @@ def confirm_user(): :return: """ + ams_login = False try: + next_value = session['next'] if 'next' in session else '' + if next_value == 'ams': + ams_login = True if request.form.get('csrf_random', '') != session['csrf_random']: - flash('csrf_random', category='error') - return _redirect_method() + if ams_login: + return _redirect_method(True, ams_error=_('csrf_random')) + else: + flash('csrf_random', category='error') + return _redirect_method() shib_session_id = session['shib_session_id'] if not shib_session_id: - flash('shib_session_id', category='error') - return _redirect_method() + if ams_login: + return _redirect_method(True, ams_error=_('Missing Shib-Session-ID!')) + else: + flash('shib_session_id', category='error') + return _redirect_method() redis_connection = RedisConnection() datastore = redis_connection.connection(db=current_app.config['CACHE_REDIS_DB'], kv = True) @@ -260,34 +320,49 @@ def confirm_user(): 'WEKO_ACCOUNTS_SHIB_CACHE_PREFIX'] + shib_session_id if not datastore.redis.exists(cache_key): - flash('cache_key', category='error') - return _redirect_method() + if ams_login: + return _redirect_method(True, ams_error=_('Missing SHIB_CACHE_PREFIX!')) + else: + flash('cache_key', category='error') + return _redirect_method() cache_val = datastore.get(cache_key) if not cache_val: - flash('cache_val', category='error') - datastore.delete(cache_key) - return _redirect_method() + if ams_login: + return _redirect_method(True, ams_error=_('Missing SHIB_ATTR!')) + else: + flash('cache_val', category='error') + datastore.delete(cache_key) + return _redirect_method() cache_val = json.loads(str(cache_val, encoding='utf-8')) shib_user = ShibUser(cache_val) account = request.form.get('WEKO_ATTR_ACCOUNT', None) password = request.form.get('WEKO_ATTR_PWD', None) if not shib_user.check_weko_user(account, password): - flash('check_weko_user', category='error') - datastore.delete(cache_key) - return _redirect_method() + if ams_login: + return _redirect_method(True, ams_error=_('There is no user information.')) + else: + flash('check_weko_user', category='error') + datastore.delete(cache_key) + return _redirect_method() if not shib_user.bind_relation_info(account): - flash('FAILED bind_relation_info!', category='error') - return _redirect_method() + if ams_login: + return _redirect_method(True, ams_error=_('FAILED bind_relation_info!')) + else: + flash('FAILED bind_relation_info!', category='error') + return _redirect_method() error = shib_user.check_in() if error: datastore.delete(cache_key) - flash(error, category='error') - return _redirect_method() + if ams_login: + return _redirect_method(True, ams_error=error) + else: + flash(error, category='error') + return _redirect_method() if shib_user.shib_user: shib_user.shib_user_login() @@ -297,7 +372,10 @@ def confirm_user(): target_key=shib_user.user.id, remarks="Shibboleth login" ) - return redirect(session['next'] if 'next' in session else '/') + if ams_login: + return redirect('/?next=ams') + else: + return redirect(session['next'] if 'next' in session else '/') except BaseException: current_app.logger.error("Unexpected error: {}".format(sys.exc_info())) exec_info = sys.exc_info() @@ -306,6 +384,9 @@ def confirm_user(): operation="LOGIN", remarks=tb_info[0] ) + if ams_login: + return _redirect_method(True, ams_error=_( + 'Server error has occurred. Please contact server administrator.')) return abort(400) @@ -315,12 +396,20 @@ def confirm_user_without_page(): :return: """ + ams_login = False try: + next_value = request.args.get('next', '') + if next_value == 'ams': + ams_login = True + # get shib_session_id from session shib_session_id = request.args.get('Shib-Session-ID', None) if not shib_session_id: - flash('shib_session_id', category='error') - return _redirect_method() + if ams_login: + return _redirect_method(True, ams_error=_('Missing Shib-Session-ID!')) + else: + flash('shib_session_id', category='error') + return _redirect_method() # get cache from redis redis_connection = RedisConnection() @@ -330,38 +419,56 @@ def confirm_user_without_page(): # check cache if not datastore.redis.exists(cache_key): - flash('cache_key', category='error') - return _redirect_method() + if ams_login: + return _redirect_method(True, ams_error=_('Missing SHIB_CACHE_PREFIX!')) + else: + flash('cache_key', category='error') + return _redirect_method() cache_val = datastore.get(cache_key) if not cache_val: - flash('cache_val', category='error') - datastore.delete(cache_key) - return _redirect_method() + if ams_login: + return _redirect_method(True, ams_error=_('Missing SHIB_ATTR!')) + else: + flash('cache_val', category='error') + datastore.delete(cache_key) + return _redirect_method() cache_val = json.loads(str(cache_val, encoding='utf-8')) shib_user = ShibUser(cache_val) # bind relation info if not shib_user.bind_relation_info(cache_val.get('shib_mail')): - flash('FAILED bind_relation_info!', category='error') datastore.delete(cache_key) - return _redirect_method() + if ams_login: + return _redirect_method(True, ams_error=_('FAILED bind_relation_info!')) + else: + flash('FAILED bind_relation_info!', category='error') + return _redirect_method() # check in error = shib_user.check_in() if error: datastore.delete(cache_key) - flash(error, category='error') - return _redirect_method() + if ams_login: + return _redirect_method(True, ams_error=error) + else: + flash(error, category='error') + return _redirect_method() if shib_user.shib_user: shib_user.shib_user_login() datastore.delete(cache_key) - return redirect(request.args.get('next', '/')) + if ams_login: + return redirect('/?next=ams') + else: + return redirect(request.args.get('next', '/')) except BaseException: current_app.logger.error("Unexpected error: {}".format(sys.exc_info())) + if ams_login: + return _redirect_method(True, ams_error=_( + 'Server error has occurred. Please contact server administrator.')) return abort(400) @@ -371,14 +478,19 @@ def shib_login(): :return: confirm user page when relation is empty """ + ams_login = False try: shib_session_id = request.args.get('Shib-Session-ID', None) session['next'] = request.args.get('next', '/') - + if session['next'] == 'ams': + ams_login = True if not shib_session_id: - current_app.logger.error(_("Missing Shib-Session-ID!")) - flash(_("Missing Shib-Session-ID!"), category='error') - return _redirect_method() + current_app.logger.error(_('Missing Shib-Session-ID!')) + if ams_login: + return _redirect_method(True, ams_error=_('Missing Shib-Session-ID!')) + else: + flash(_('Missing Shib-Session-ID!'), category='error') + return _redirect_method() redis_connection = RedisConnection() datastore = redis_connection.connection(db=current_app.config['CACHE_REDIS_DB'], kv = True) @@ -386,17 +498,23 @@ def shib_login(): 'WEKO_ACCOUNTS_SHIB_CACHE_PREFIX'] + shib_session_id if not datastore.redis.exists(cache_key): - current_app.logger.error(_("Missing SHIB_CACHE_PREFIX!")) - flash(_("Missing SHIB_CACHE_PREFIX!"), category='error') - return _redirect_method() + current_app.logger.error(_('Missing SHIB_CACHE_PREFIX!')) + flash(_('Missing SHIB_CACHE_PREFIX!'), category='error') + if ams_login: + return _redirect_method(True, ams_error=_('Missing SHIB_CACHE_PREFIX!')) + else: + return _redirect_method() cache_val = datastore.get(cache_key) if not cache_val: - current_app.logger.error(_("Missing SHIB_ATTR!")) - flash(_("Missing SHIB_ATTR!"), category='error') + current_app.logger.error(_('Missing SHIB_ATTR!')) datastore.delete(cache_key) - return _redirect_method() + if ams_login: + return _redirect_method(True, ams_error=_('Missing SHIB_ATTR!')) + else: + flash(_('Missing SHIB_ATTR!'), category='error') + return _redirect_method() cache_val = json.loads(str(cache_val, encoding='utf-8')) session['shib_session_id'] = shib_session_id @@ -412,6 +530,9 @@ def shib_login(): ) except BaseException: current_app.logger.error("Unexpected error: {}".format(sys.exc_info())) + if ams_login: + return _redirect_method(True, ams_error=_( + 'Server error has occurred. Please contact server administrator.')) return abort(400) def find_user_by_email(shib_attributes): @@ -432,7 +553,9 @@ def shib_sp_login(): _shib_username_config = current_app.config[ 'WEKO_ACCOUNTS_SHIB_ALLOW_USERNAME_INST_EPPN'] next = request.args.get('next', '/') - + ams_login = False + if next == 'ams': + ams_login = True try: # WEKO_ACCOUNTS_SHIB_BIND_GAKUNIN_MAP_GROUPSがTrueのときの処理 if current_app.config['WEKO_ACCOUNTS_SHIB_BIND_GAKUNIN_MAP_GROUPS']: @@ -440,8 +563,11 @@ def shib_sp_login(): shib_session_id = request.form.get('Shib-Session-ID', None) if not shib_session_id and not _shib_enable: - flash(_("Missing Shib-Session-ID!"), category='error') - return redirect(url_for_security('login')) + if ams_login: + return generate_ams_login_url(_('Missing Shib-Session-ID!')) + else: + flash(_('Missing Shib-Session-ID!'), category='error') + return redirect(url_for_security('login')) shib_attr, error = parse_attributes() @@ -449,8 +575,11 @@ def shib_sp_login(): if error or not ( shib_attr.get('shib_eppn', None) or _shib_username_config and shib_attr.get('shib_user_name')): - flash(_("Missing SHIB_ATTRs!"), category='error') - return _redirect_method() + if ams_login: + return generate_ams_login_url(_('Missing SHIB_ATTRs!')) + else: + flash(_('Missing SHIB_ATTRs!'), category='error') + return _redirect_method() # Check if shib_eppn is not included in the blocked user list if AdminSettings.query.filter_by(name='blocked_user_settings').first(): @@ -468,8 +597,11 @@ def _wildcard_to_regex(pattern): blocked = any(_wildcard_to_regex(pattern).match(shib_eppn) or pattern == shib_eppn for pattern in block_user_list) if blocked: - flash(_("Failed to login."), category='error') - return _redirect_method() + if ams_login: + return generate_ams_login_url(_('Login is blocked.')) + else: + flash(_('Failed to login.'), category='error') + return _redirect_method() # Redis connection redis_connection = RedisConnection() @@ -509,7 +641,11 @@ def _wildcard_to_regex(pattern): return url_for(next_url, **query_string) except BaseException: current_app.logger.error("Unexpected error: {}".format(sys.exc_info())) - return _redirect_method() + if ams_login: + return generate_ams_login_url( + _('Server error has occurred. Please contact server administrator.')) + else: + return _redirect_method() @blueprint.route('/shib/sp/login', methods=['GET']) diff --git a/modules/weko-deposit/requirements2.txt b/modules/weko-deposit/requirements2.txt index 43d2bf48e9..b01f66293a 100644 --- a/modules/weko-deposit/requirements2.txt +++ b/modules/weko-deposit/requirements2.txt @@ -288,4 +288,4 @@ xmlschema==0.9.30 xmltodict==0.12.0 zipp==3.6.0 zope.interface==5.5.2 -pypdfium2==4.30.0 \ No newline at end of file +pypdfium2==4.30.0 diff --git a/modules/weko-deposit/tests/test_utils.py b/modules/weko-deposit/tests/test_utils.py index 130d61fba7..e16fda9f57 100644 --- a/modules/weko-deposit/tests/test_utils.py +++ b/modules/weko-deposit/tests/test_utils.py @@ -18,14 +18,14 @@ # Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, # MA 02111-1307, USA. -from weko_deposit.api import WekoDeposit -from weko_deposit.utils import update_pdf_contents_es, extract_text_from_pdf, extract_text_with_tika -from sqlalchemy.orm.exc import NoResultFound +import os import types + +import pytest +from sqlalchemy.orm.exc import NoResultFound from unittest.mock import patch, MagicMock -import uuid -from tests.helpers import create_record_with_pdf +from weko_deposit.utils import update_pdf_contents_es, extract_text_from_pdf, extract_text_with_tika # .tox/c1/bin/pytest --cov=weko_deposit tests/test_utils.py::test_update_pdf_contents_es_with_index_api -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-deposit/.tox/c1/tmp def test_update_pdf_contents_es(app, mocker): @@ -60,9 +60,6 @@ def get_pdf_info(self): raise NoResultFound() assert hasattr(dummy_trace, 'called') -import os - -import pytest # .tox/c1/bin/pytest --cov=weko_deposit tests/test_utils.py::test_extract_text_from_pdf -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-deposit/.tox/c1/tmp def test_extract_text_from_pdf(): filepath = os.path.join(os.path.dirname(__file__),"data","test_files","test_file_1.2M.pdf") @@ -94,21 +91,23 @@ def test_extract_text_with_tika(): data = extract_text_with_tika(filepath, 100) assert str(e.value) == "not exist tika jar file." - # error with subprocess - mock_run = MagicMock() - mock_run.returncode.return_value=1 - mock_run.stderr.decode.return_value="test_error" - with patch("weko_deposit.utils.subprocess.run", return_value=mock_run): - with pytest.raises(Exception) as e: - data = extract_text_with_tika(filepath, 100) - assert str(e.value) == "raise in tika: test_error" + mock_env_exist_tika = {"TIKA_JAR_FILE_PATH": "/code/tika/tika-app-2.6.0.jar"} + with patch.dict(os.environ, mock_env_exist_tika, clear=False): + # error with subprocess + mock_run = MagicMock() + mock_run.returncode.return_value=1 + mock_run.stderr.decode.return_value="test_error" + with patch("weko_deposit.utils.subprocess.run", return_value=mock_run): + with pytest.raises(Exception) as e: + data = extract_text_with_tika(filepath, 100) + assert str(e.value) == "raise in tika: test_error" - # file size > max_size - data = extract_text_with_tika(filepath, 50) - assert len(data.encode('utf-8')) < 50 - assert data == "これはテスト用のサンプルwordファイ" + # file size > max_size + data = extract_text_with_tika(filepath, 50) + assert len(data.encode('utf-8')) < 50 + assert data == "これはテスト用のサンプルwordファイ" - # file size <= max_size - data = extract_text_with_tika(filepath, 5000) - assert len(data.encode('utf-8')) > 50 - assert data == "これはテスト用のサンプルwordファイルです中身は特に意味がありません" + # file size <= max_size + data = extract_text_with_tika(filepath, 5000) + assert len(data.encode('utf-8')) > 50 + assert data == "これはテスト用のサンプルwordファイルです中身は特に意味がありません" diff --git a/modules/weko-index-tree/tests/test_rest.py b/modules/weko-index-tree/tests/test_rest.py index 0320996b6d..25d7e4d217 100644 --- a/modules/weko-index-tree/tests/test_rest.py +++ b/modules/weko-index-tree/tests/test_rest.py @@ -1438,3 +1438,14 @@ def run_delete_index_server_error(self, app, client_rest, auth_headers): with patch.object(Indexes, "delete", side_effect=SQLAlchemyError): response = client_rest.delete(url, headers=auth_headers) assert response.status_code == 500, f"{response.json}" + +# class GetIndex: +# .tox/c1/bin/pytest --cov=weko_index_tree tests/test_rest.py::TestGetIndex -vv -s --cov-branch --cov-report=term --cov-report=html --basetemp=/code/modules/weko_index_tree/.tox/c1/tmp --full-trace +class TestGetIndex: + # .tox/c1/bin/pytest --cov=weko_index_tree tests/test_rest.py::TestGetIndex::test_get_v1 -vv -s --cov-branch --cov-report=term --cov-report=html --basetemp=/code/modules/weko_index_tree/.tox/c1/tmp --full-trace + def test_get_v1(self, client_rest, users, test_indices, auth_headers_sysadmin): + res = client_rest.get('/v1/tree/index/1', headers=auth_headers_sysadmin) + assert res.status_code == 200 + data = json.loads(res.get_data()) + assert data['index']['name'] == 'Test index 1' + assert data['index']['public_date'] == '20220101' diff --git a/modules/weko-index-tree/weko_index_tree/rest.py b/modules/weko-index-tree/weko_index_tree/rest.py index 8a4edb36c5..271dc0d3d2 100644 --- a/modules/weko-index-tree/weko_index_tree/rest.py +++ b/modules/weko-index-tree/weko_index_tree/rest.py @@ -712,6 +712,21 @@ def get(self, **kwargs): raise VersionNotFoundRESTError() def get_v1(self, **kwargs): + + def json_serialize(obj): + """Serialize object to JSON. + + Args: + obj: The object to serialize. + + Returns: + str: The serialized JSON string. + """ + if isinstance(obj, (datetime, date)): + return obj.strftime("%Y%m%d") + else: + return str(obj) + try: pid = kwargs.get('index_id') @@ -1531,7 +1546,7 @@ def _get_allowed_group_roles(self, index_info): Returns: dict: A dictionary with allowed group roles. """ - def _get_allowed_list(self, group_str): + def _get_allowed_list(self, group_str): """Convert group string to allowed group roles.""" return { "allow": [{"id": role} for role in group_str.split(",")] diff --git a/modules/weko-records-ui/weko_records_ui/rest.py b/modules/weko-records-ui/weko_records_ui/rest.py index 23d964d489..7e3fb0cd7f 100644 --- a/modules/weko-records-ui/weko_records_ui/rest.py +++ b/modules/weko-records-ui/weko_records_ui/rest.py @@ -67,6 +67,11 @@ InvalidTokenError, InvalidWorkflowError, ModeNotFoundRESTError, PermissionError, \ RecordsNotFoundRESTError, RequiredItemNotExistError, VersionNotFoundRESTError from .permissions import page_permission_factory, file_permission_factory +from .errors import AvailableFilesNotFoundRESTError, ContentsNotFoundError, \ + InvalidRequestError, VersionNotFoundRESTError, InternalServerError, \ + RecordsNotFoundRESTError, PermissionError, DateFormatRESTError, \ + FilesNotFoundRESTError, ModeNotFoundRESTError, RequiredItemNotExistError, \ + AuthenticationRequiredError from .scopes import file_read_scope from .views import escape_str, get_usage_workflow @@ -711,7 +716,10 @@ def get_v1(self, **kwargs): # Check Permission if not page_permission_factory(record).can(): - raise PermissionError() + if current_user.is_authenticated: + raise PermissionError() + else: + raise AuthenticationRequiredError() # Convert RO-Crate format from .utils import RoCrateConverter @@ -756,8 +764,10 @@ def get_v1(self, **kwargs): return res - except (PermissionError, SameContentException) as e: - raise e + except (PermissionError, + SameContentException, + AuthenticationRequiredError) as e: + raise e except PIDDoesNotExistError: raise RecordsNotFoundRESTError() diff --git a/modules/weko-records-ui/weko_records_ui/templates/weko_records_ui/output_detail_data.html b/modules/weko-records-ui/weko_records_ui/templates/weko_records_ui/output_detail_data.html index 0c5c412a5c..44ed8e0f40 100644 --- a/modules/weko-records-ui/weko_records_ui/templates/weko_records_ui/output_detail_data.html +++ b/modules/weko-records-ui/weko_records_ui/templates/weko_records_ui/output_detail_data.html @@ -83,7 +83,7 @@ {% if parrent_name %} {%- set labels = parrent_name.split('.') -%} {%- if labels|length == 1 -%} - {{ child_data(parrent_name, '', level) }} + {{ child_data(parrent_name, ' ', level) }} {%- else -%} {%- set displayflag = False -%} {%- endif -%} @@ -118,7 +118,7 @@ {{ content | escape_str }} {%- elif content|url_to_link -%} {{ content | escape_str }} - {%- else -%} + {%- else -%} {{ content | escape_str }} {%- endif -%} {% endautoescape %} diff --git a/modules/weko-search-ui/requirements2.txt b/modules/weko-search-ui/requirements2.txt index c4702c5184..b01f66293a 100644 --- a/modules/weko-search-ui/requirements2.txt +++ b/modules/weko-search-ui/requirements2.txt @@ -288,3 +288,4 @@ xmlschema==0.9.30 xmltodict==0.12.0 zipp==3.6.0 zope.interface==5.5.2 +pypdfium2==4.30.0 diff --git a/modules/weko-search-ui/tests/conftest.py b/modules/weko-search-ui/tests/conftest.py index f134eb98ca..a81cf2ee36 100644 --- a/modules/weko-search-ui/tests/conftest.py +++ b/modules/weko-search-ui/tests/conftest.py @@ -137,7 +137,13 @@ from weko_deposit.api import WekoDeposit from weko_deposit.api import WekoDeposit as aWekoDeposit from weko_deposit.api import WekoIndexer, WekoRecord -from weko_deposit.config import WEKO_BUCKET_QUOTA_SIZE, WEKO_MAX_FILE_SIZE +from weko_deposit.config import ( + WEKO_BUCKET_QUOTA_SIZE, + WEKO_MAX_FILE_SIZE, + WEKO_DEPOSIT_FILESIZE_LIMIT, + WEKO_MIMETYPE_WHITELIST_FOR_ES, + WEKO_DEPOSIT_TEXTMIMETYPE_WHITELIST_FOR_ES +) from weko_groups import WekoGroups from weko_index_tree import WekoIndexTree, WekoIndexTreeREST from weko_index_tree.api import Indexes @@ -692,6 +698,9 @@ def base_app(instance_path, search_class, request): WEKO_SEARCH_UI_BULK_EXPORT_RETRY = 5, WEKO_SEARCH_UI_BULK_EXPORT_LIMIT = 100, RECORDS_UI_ENDPOINTS = RECORDS_UI_ENDPOINTS, + WEKO_DEPOSIT_FILESIZE_LIMIT = WEKO_DEPOSIT_FILESIZE_LIMIT, + WEKO_MIMETYPE_WHITELIST_FOR_ES = WEKO_MIMETYPE_WHITELIST_FOR_ES, + WEKO_DEPOSIT_TEXTMIMETYPE_WHITELIST_FOR_ES = WEKO_DEPOSIT_TEXTMIMETYPE_WHITELIST_FOR_ES, WEKO_SCHEMA_JPCOAR_V2_RESOURCE_TYPE_REPLACE={ "periodical": "journal", "interview": "other", diff --git a/modules/weko-search-ui/tests/data/ams/broken_word.docx b/modules/weko-search-ui/tests/data/ams/broken_word.docx new file mode 100644 index 0000000000..3fa5f4e582 Binary files /dev/null and b/modules/weko-search-ui/tests/data/ams/broken_word.docx differ diff --git a/modules/weko-search-ui/tests/data/ams/png_file.pdf b/modules/weko-search-ui/tests/data/ams/png_file.pdf new file mode 100644 index 0000000000..cc8d7bda54 Binary files /dev/null and b/modules/weko-search-ui/tests/data/ams/png_file.pdf differ diff --git a/modules/weko-search-ui/tests/data/ams/png_file.txt b/modules/weko-search-ui/tests/data/ams/png_file.txt new file mode 100644 index 0000000000..cc8d7bda54 Binary files /dev/null and b/modules/weko-search-ui/tests/data/ams/png_file.txt differ diff --git a/modules/weko-search-ui/tests/data/ams/sample.txt b/modules/weko-search-ui/tests/data/ams/sample.txt new file mode 100644 index 0000000000..990383c64b --- /dev/null +++ b/modules/weko-search-ui/tests/data/ams/sample.txt @@ -0,0 +1,2 @@ +This is a +text file. diff --git a/modules/weko-search-ui/tests/data/ams/with_two_extended_metadata.json b/modules/weko-search-ui/tests/data/ams/with_two_extended_metadata.json new file mode 100644 index 0000000000..eb70def6c2 --- /dev/null +++ b/modules/weko-search-ui/tests/data/ams/with_two_extended_metadata.json @@ -0,0 +1,99 @@ +{ + "@id": "./", + "@type": "Dataset", + "datePublished": "2025-10-22", + "name": "extract", + "description": "Item metadata for Item ID: 2000036. Title: extract.", + "wk:index": [ + "1623632832836" + ], + "wk:publishStatus": "public", + "wk:feedbackMail": [], + "wk:requestMail": [], + "wk:grant": [], + "wk:editMode": "Keep", + "dc:title": [ + { + "@id": "#:title_0", + "@type": "PropertyValue", + "value": "extract", + "language": "ja" + } + ], + "dc:type": { + "@id": "#:type_1", + "@type": "PropertyValue", + "rdf:resource": "departmental bulletin paper", + "value": "http://purl.org/coar/resource_type/c_6501" + }, + "hasPart": [ + { + "@id": "data/guide.pdf", + "@type": "File", + "dcterms:accessRights": "open_access", + "datePublished": "2025-10-22", + "name": "guide.pdf", + "jpcoar:extent": [ + { + "@id": "#:extent_3", + "@type": "File", + "value": "844 KB" + } + ], + "jpcoar:mimeType": "application/pdf", + "jpcoar:URI": { + "@id": "#:URI_4", + "@type": "URL", + "value": "https://192.168.56.102/record/2000036/files/guide.pdf" + }, + "wk:textExtraction": true, + "wk:extendedMetadata": true + }, + { + "@id": "data/sample.txt", + "@type": "File", + "dcterms:accessRights": "open_access", + "datePublished": "2025-10-22", + "name": "sample.txt", + "jpcoar:extent": [ + { + "@id": "#:extent_6", + "@type": "File", + "value": "51 B" + } + ], + "jpcoar:mimeType": "text/plain", + "jpcoar:URI": { + "@id": "#:URI_7", + "@type": "URL", + "value": "https://192.168.56.102/record/2000036/files/sample.txt" + }, + "wk:textExtraction": true, + "wk:extendedMetadata": true + }, + { + "@id": "data/pp.pptx", + "@type": "File", + "dcterms:accessRights": "open_access", + "datePublished": "2025-10-22", + "name": "pp.pptx", + "jpcoar:extent": [ + { + "@id": "#:extent_9", + "@type": "File", + "value": "432 KB" + } + ], + "jpcoar:mimeType": "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "jpcoar:URI": { + "@id": "#:URI_10", + "@type": "URL", + "value": "https://192.168.56.102/record/2000036/files/pp.pptx" + }, + "wk:textExtraction": true, + "wk:extendedMetadata": false + } + ], + "wk:itemLinks": [], + "wk:metadataAutoFill": false +} diff --git a/modules/weko-search-ui/tests/data/ams/without_hasPart.json b/modules/weko-search-ui/tests/data/ams/without_hasPart.json new file mode 100644 index 0000000000..a4a18e1ba1 --- /dev/null +++ b/modules/weko-search-ui/tests/data/ams/without_hasPart.json @@ -0,0 +1,31 @@ +{ + "@id": "./", + "@type": "Dataset", + "datePublished": "2025-10-22", + "name": "extract", + "description": "Item metadata for Item ID: 2000036. Title: extract.", + "wk:index": [ + "1623632832836" + ], + "wk:publishStatus": "public", + "wk:feedbackMail": [], + "wk:requestMail": [], + "wk:grant": [], + "wk:editMode": "Keep", + "dc:title": [ + { + "@id": "#:title_0", + "@type": "PropertyValue", + "value": "extract", + "language": "ja" + } + ], + "dc:type": { + "@id": "#:type_1", + "@type": "PropertyValue", + "rdf:resource": "departmental bulletin paper", + "value": "http://purl.org/coar/resource_type/c_6501" + }, + "wk:itemLinks": [], + "wk:metadataAutoFill": false +} diff --git "a/modules/weko-search-ui/tests/data/ams/\343\202\265\343\203\263\343\203\227\343\203\2532.txt" "b/modules/weko-search-ui/tests/data/ams/\343\202\265\343\203\263\343\203\227\343\203\2532.txt" new file mode 100644 index 0000000000..793629ceea --- /dev/null +++ "b/modules/weko-search-ui/tests/data/ams/\343\202\265\343\203\263\343\203\227\343\203\2532.txt" @@ -0,0 +1 @@ +上限:8バイト diff --git a/modules/weko-search-ui/tests/data/list_records/b4_handle_check_exist_record4.json b/modules/weko-search-ui/tests/data/list_records/b4_handle_check_exist_record4.json new file mode 100644 index 0000000000..c7838c8a48 --- /dev/null +++ b/modules/weko-search-ui/tests/data/list_records/b4_handle_check_exist_record4.json @@ -0,0 +1 @@ +[{"pos_index": ["IndexA"], "publish_status": "public", "feedback_mail": ["wekosoftware@nii.ac.jp"], "edit_mode": "Keep", "metadata": {"pubdate": "2021-08-07", "item_1617186331708": [{"subitem_1551255647225": "ja_conference paperITEM00000001(public_open_access_open_access_simple)", "subitem_1551255648112": "ja"}, {"subitem_1551255647225": "en_conference paperITEM00000001(public_open_access_simple)", "subitem_1551255648112": "en"}], "item_1617186385884": [{"subitem_1551255720400": "Alternative Title", "subitem_1551255721061": "en"}, {"subitem_1551255720400": "Alternative Title", "subitem_1551255721061": "ja"}], "item_1617186419668": [{"creatorAffiliations": [{"affiliationNameIdentifiers": [{"affiliationNameIdentifier": "0000000121691048", "affiliationNameIdentifierScheme": "ISNI", "affiliationNameIdentifierURI": "http://isni.org/isni/0000000121691048"}], "affiliationNames": [{"affiliationName": "University", "affiliationNameLang": "en"}]}], "creatorMails": [{"creatorMail": "wekosoftware@nii.ac.jp"}], "creatorNames": [{"creatorName": "情報, 太郎", "creatorNameLang": "ja"}, {"creatorName": "ジョウホウ, タロウ", "creatorNameLang": "ja-Kana"}, {"creatorName": "Joho, Taro", "creatorNameLang": "en"}], "familyNames": [{"familyName": "情報", "familyNameLang": "ja"}, {"familyName": "ジョウホウ", "familyNameLang": "ja-Kana"}, {"familyName": "Joho", "familyNameLang": "en"}], "givenNames": [{"givenName": "太郎", "givenNameLang": "ja"}, {"givenName": "タロウ", "givenNameLang": "ja-Kana"}, {"givenName": "Taro", "givenNameLang": "en"}], "nameIdentifiers": [{"nameIdentifier": "4", "nameIdentifierScheme": "WEKO"}, {"nameIdentifier": "xxxxxxx", "nameIdentifierScheme": "ORCID", "nameIdentifierURI": "https://orcid.org/"}, {"nameIdentifier": "xxxxxxx", "nameIdentifierScheme": "CiNii", "nameIdentifierURI": "https://ci.nii.ac.jp/"}, {"nameIdentifier": "zzzzzzz", "nameIdentifierScheme": "KAKEN2", "nameIdentifierURI": "https://kaken.nii.ac.jp/"}]}, {"creatorMails": [{"creatorMail": "wekosoftware@nii.ac.jp"}], "creatorNames": [{"creatorName": "情報, 太郎", "creatorNameLang": "ja"}, {"creatorName": "ジョウホウ, タロウ", "creatorNameLang": "ja-Kana"}, {"creatorName": "Joho, Taro", "creatorNameLang": "en"}], "familyNames": [{"familyName": "情報", "familyNameLang": "ja"}, {"familyName": "ジョウホウ", "familyNameLang": "ja-Kana"}, {"familyName": "Joho", "familyNameLang": "en"}], "givenNames": [{"givenName": "太郎", "givenNameLang": "ja"}, {"givenName": "タロウ", "givenNameLang": "ja-Kana"}, {"givenName": "Taro", "givenNameLang": "en"}], "nameIdentifiers": [{"nameIdentifier": "xxxxxxx", "nameIdentifierScheme": "ORCID", "nameIdentifierURI": "https://orcid.org/"}, {"nameIdentifier": "xxxxxxx", "nameIdentifierScheme": "CiNii", "nameIdentifierURI": "https://ci.nii.ac.jp/"}, {"nameIdentifier": "zzzzzzz", "nameIdentifierScheme": "KAKEN2", "nameIdentifierURI": "https://kaken.nii.ac.jp/"}]}, {"creatorMails": [{"creatorMail": "wekosoftware@nii.ac.jp"}], "creatorNames": [{"creatorName": "情報, 太郎", "creatorNameLang": "ja"}, {"creatorName": "ジョウホウ, タロウ", "creatorNameLang": "ja-Kana"}, {"creatorName": "Joho, Taro", "creatorNameLang": "en"}], "familyNames": [{"familyName": "情報", "familyNameLang": "ja"}, {"familyName": "ジョウホウ", "familyNameLang": "ja-Kana"}, {"familyName": "Joho", "familyNameLang": "en"}], "givenNames": [{"givenName": "太郎", "givenNameLang": "ja"}, {"givenName": "タロウ", "givenNameLang": "ja-Kana"}, {"givenName": "Taro", "givenNameLang": "en"}], "nameIdentifiers": [{"nameIdentifier": "xxxxxxx", "nameIdentifierScheme": "ORCID", "nameIdentifierURI": "https://orcid.org/"}, {"nameIdentifier": "xxxxxxx", "nameIdentifierScheme": "CiNii", "nameIdentifierURI": "https://ci.nii.ac.jp/"}, {"nameIdentifier": "zzzzzzz", "nameIdentifierScheme": "KAKEN2", "nameIdentifierURI": "https://kaken.nii.ac.jp/"}]}], "item_1617349709064": [{"contributorMails": [{"contributorMail": "wekosoftware@nii.ac.jp"}], "contributorNames": [{"contributorName": "情報, 太郎", "lang": "ja"}, {"contributorName": "ジョウホウ, タロウ", "lang": "ja-Kana"}, {"contributorName": "Joho, Taro", "lang": "en"}], "contributorType": "ContactPerson", "familyNames": [{"familyName": "情報", "familyNameLang": "ja"}, {"familyName": "ジョウホウ", "familyNameLang": "ja-Kana"}, {"familyName": "Joho", "familyNameLang": "en"}], "givenNames": [{"givenName": "太郎", "givenNameLang": "ja"}, {"givenName": "タロウ", "givenNameLang": "ja-Kana"}, {"givenName": "Taro", "givenNameLang": "en"}], "nameIdentifiers": [{"nameIdentifier": "xxxxxxx", "nameIdentifierScheme": "ORCID", "nameIdentifierURI": "https://orcid.org/"}, {"nameIdentifier": "xxxxxxx", "nameIdentifierScheme": "CiNii", "nameIdentifierURI": "https://ci.nii.ac.jp/"}, {"nameIdentifier": "xxxxxxx", "nameIdentifierScheme": "KAKEN2", "nameIdentifierURI": "https://kaken.nii.ac.jp/"}]}], "item_1617186476635": {"subitem_1522299639480": "open access", "subitem_1600958577026": "http://purl.org/coar/access_right/c_abf2"}, "item_1617351524846": {"subitem_1523260933860": "Unknown"}, "item_1617186499011": [{"subitem_1522650717957": "ja", "subitem_1522650727486": "http://localhost", "subitem_1522651041219": "Rights Information"}], "item_1617610673286": [{"nameIdentifiers": [{"nameIdentifier": "xxxxxx", "nameIdentifierScheme": "ORCID", "nameIdentifierURI": "https://orcid.org/"}], "rightHolderNames": [{"rightHolderLanguage": "ja", "rightHolderName": "Right Holder Name"}]}], "item_1617186609386": [{"subitem_1522299896455": "ja", "subitem_1522300014469": "Other", "subitem_1522300048512": "http://localhost/", "subitem_1523261968819": "Sibject1"}], "item_1617186626617": [{"subitem_description": "Description\nDescription
Description", "subitem_description_language": "en", "subitem_description_type": "Abstract"}, {"subitem_description": "概要\n概要\n概要\n概要", "subitem_description_language": "ja", "subitem_description_type": "Abstract"}], "item_1617186643794": [{"subitem_1522300295150": "en", "subitem_1522300316516": "Publisher"}], "item_1617186660861": [{"subitem_1522300695726": "Available", "subitem_1522300722591": "2021-06-30"}], "item_1617186702042": [{"subitem_1551255818386": "jpn"}], "item_1617258105262": {"resourcetype": "conference paper", "resourceuri": "http://purl.org/coar/resource_type/c_5794"}, "item_1617349808926": {"subitem_1523263171732": "Version"}, "item_1617265215918": {"subitem_1522305645492": "AO", "subitem_1600292170262": "http://purl.org/coar/version/c_b1a7d7d4d402bcce"}, "item_1617186783814": [{"subitem_identifier_type": "URI", "subitem_identifier_uri": "http://localhost"}], "item_1617353299429": [{"subitem_1522306207484": "isVersionOf", "subitem_1522306287251": {"subitem_1522306382014": "arXiv", "subitem_1522306436033": "xxxxx"}, "subitem_1523320863692": [{"subitem_1523320867455": "en", "subitem_1523320909613": "Related Title"}]}], "item_1617186859717": [{"subitem_1522658018441": "en", "subitem_1522658031721": "Temporal"}], "item_1617186882738": [{"subitem_geolocation_place": [{"subitem_geolocation_place_text": "Japan"}]}], "item_1617186901218": [{"subitem_1522399143519": {"subitem_1522399281603": "ISNI", "subitem_1522399333375": "http://xxx"}, "subitem_1522399412622": [{"subitem_1522399416691": "en", "subitem_1522737543681": "Funder Name"}], "subitem_1522399571623": {"subitem_1522399585738": "Award URI", "subitem_1522399628911": "Award Number"}, "subitem_1522399651758": [{"subitem_1522721910626": "en", "subitem_1522721929892": "Award Title"}]}], "item_1617186920753": [{"subitem_1522646500366": "ISSN", "subitem_1522646572813": "xxxx-xxxx-xxxx"}], "item_1617186941041": [{"subitem_1522650068558": "en", "subitem_1522650091861": "Source Title"}], "item_1617186959569": {"subitem_1551256328147": "1"}, "item_1617186981471": {"subitem_1551256294723": "111"}, "item_1617186994930": {"subitem_1551256248092": "12"}, "item_1617187024783": {"subitem_1551256198917": "1"}, "item_1617187045071": {"subitem_1551256185532": "3"}, "item_1617187112279": [{"subitem_1551256126428": "Degree Name", "subitem_1551256129013": "en"}], "item_1617187136212": {"subitem_1551256096004": "2021-06-30"}, "item_1617944105607": [{"subitem_1551256015892": [{"subitem_1551256027296": "xxxxxx", "subitem_1551256029891": "kakenhi"}], "subitem_1551256037922": [{"subitem_1551256042287": "Degree Grantor Name", "subitem_1551256047619": "en"}]}], "item_1617187187528": [{"subitem_1599711633003": [{"subitem_1599711636923": "Conference Name", "subitem_1599711645590": "ja"}], "subitem_1599711655652": "1", "subitem_1599711660052": [{"subitem_1599711680082": "Sponsor", "subitem_1599711686511": "ja"}], "subitem_1599711699392": {"subitem_1599711704251": "2020/12/11", "subitem_1599711712451": "1", "subitem_1599711727603": "12", "subitem_1599711731891": "2000", "subitem_1599711735410": "1", "subitem_1599711739022": "12", "subitem_1599711743722": "2020", "subitem_1599711745532": "ja"}, "subitem_1599711758470": [{"subitem_1599711769260": "Conference Venue", "subitem_1599711775943": "ja"}], "subitem_1599711788485": [{"subitem_1599711798761": "Conference Place", "subitem_1599711803382": "ja"}], "subitem_1599711813532": "JPN"}], "item_1617605131499": [{"accessrole": "open_access", "date": [{"dateType": "Available", "dateValue": "2021-07-12"}], "displaytype": "simple", "filename": "1KB.pdf", "filesize": [{"value": "1 KB"}], "format": "text/plain"}, {"filename": ""}], "item_1617620223087": [{"subitem_1565671149650": "ja", "subitem_1565671169640": "Banner Headline", "subitem_1565671178623": "Subheading"}, {"subitem_1565671149650": "en", "subitem_1565671169640": "Banner Headline", "subitem_1565671178623": "Subheding"}]}, "file_path": ["file00000001/1KB.pdf", ""], "item_type_name": "デフォルトアイテムタイプ(フル)", "item_type_id": 15, "$schema": "https://localhost:8443/items/jsonschema/15", "identifier_key": "item_1617186819068", "errors": null}] \ No newline at end of file diff --git a/modules/weko-search-ui/tests/test_mapper.py b/modules/weko-search-ui/tests/test_mapper.py index c59f587991..c3fe1173d0 100644 --- a/modules/weko-search-ui/tests/test_mapper.py +++ b/modules/weko-search-ui/tests/test_mapper.py @@ -2,9 +2,11 @@ import pytest import xmltodict import uuid +import json from datetime import date from unittest.mock import patch, MagicMock from collections import OrderedDict +from pypdfium2 import PdfiumError from weko_records.api import Mapping from weko_records.serializers.utils import get_full_mapping @@ -4817,9 +4819,10 @@ def test_to_item_metadata(self, app, db, item_type2, item_type_mapping2): # def deconstruct_json_ld(json_ld): # .tox/c1/bin/pytest --cov=weko_search_ui tests/test_mapper.py::TestJsonLdMapper::test__deconstruct_json_ld -v -vv -s --cov-branch --cov-report=xml --basetemp=/code/modules/weko-search-ui/.tox/c1/tmp - def test__deconstruct_json_ld(self, app): + def test__deconstruct_json_ld(self, app, item_type2): json_ld = json_data("data/jsonld/ro-crate-metadata.json") - deconstructed_metadata, format = JsonLdMapper._deconstruct_json_ld(json_ld) + mapper = JsonLdMapper(item_type2.model.id, None) + deconstructed_metadata, format = mapper._deconstruct_json_ld(json_ld) metadata, system_info = deconstructed_metadata[0] assert format == "ro-crate" @@ -4847,7 +4850,8 @@ def test__deconstruct_json_ld(self, app): assert not any("@type" in key for key in metadata.keys()) json_ld = json_data("data/jsonld/ro-crate-metadata2.json") - deconstructed_metadata, format = JsonLdMapper._deconstruct_json_ld(json_ld) + mapper = JsonLdMapper(item_type2.model.id, None) + deconstructed_metadata, format = mapper._deconstruct_json_ld(json_ld) thesis, system_info = deconstructed_metadata[0] assert format == "ro-crate" @@ -4877,7 +4881,8 @@ def test__deconstruct_json_ld(self, app): assert evidence["dc:type.@id"] == "http://purl.org/coar/resource_type/c_1843" with pytest.raises(ValueError) as ex: - deconstructed_metadata, format = JsonLdMapper._deconstruct_json_ld({}) + mapper = JsonLdMapper(item_type2.model.id, None) + deconstructed_metadata, format = mapper._deconstruct_json_ld({}) ex.match('Invalid json-ld format: "@context" is invalid.') # def to_rocrate_metadata(self, metadata): @@ -5279,6 +5284,103 @@ def test_to_item_metadata_ams_dict(self, app, db, item_type2): assert item_metadata["item_1749689698804"]["subitem_relation_type"] == "isVersionOf" + # def extract_extended_metadata(self, list_extracted): + # .tox/c1/bin/pytest --cov=weko_search_ui tests/test_mapper.py::TestJsonLdMapper::test_extract_extended_metadata -v -vv -s --cov-branch --cov-report=html --basetemp=/code/modules/weko-search-ui/.tox/c1/tmp + def test_extract_extended_metadata(self, app, db, item_type2, mocker): + mapper = JsonLdMapper(item_type2.model.id, None) + values = ["first", "second"] + mocker.patch.object(mapper, "extract_text_from_files", + side_effect = values) + + rocrate = json_data("data/ams/with_two_extended_metadata.json") + rocrate = mapper.extract_extended_metadata([rocrate])[0] + + ids = [part["@id"] + for part in rocrate.get("hasPart", []) + if "@id" in part] + assert ids == ["data/pp.pptx"] + + ext = rocrate["extended_metadata"]["value"] + ext = json.loads(ext) + + assert len(ext) == 2 + assert ext["data/sample.txt"] == "first" + assert ext["data/guide.pdf"] == "second" + + # without hasPart + rocrate = json_data("data/ams/without_hasPart.json") + rocrate = mapper.extract_extended_metadata([rocrate])[0] + assert "extended_metadata" not in rocrate + + # def extract_text_from_files(self, filename): + # .tox/c1/bin/pytest --cov=weko_search_ui tests/test_mapper.py::TestJsonLdMapper::test_extract_text_from_files -v -vv -s --cov-branch --cov-report=html --basetemp=/code/modules/weko-search-ui/.tox/c1/tmp + def test_extract_text_from_files(self, app, db, item_type2, mocker, tmp_path): + mapper = JsonLdMapper(item_type2.model.id, None) + + mapper.data_path = str(tmp_path) + file_content = "これは\r\nテキストファイルです\r\n" + + file_name = "サンプル.txt" + tmpfile = tmp_path / file_name + tmpfile.write_text(file_content, encoding="shift_jis") + extract_text = mapper.extract_text_from_files(file_name) + assert extract_text == file_content + + file_name = "サンプル.TXT" + tmpfile = tmp_path / file_name + tmpfile.write_text(file_content, encoding="utf-8") + extract_text = mapper.extract_text_from_files(file_name) + assert extract_text == file_content + + file_name = "サンプル.txt" + tmpfile = tmp_path / file_name + tmpfile.write_text(file_content, encoding="utf-8") + extract_text = mapper.extract_text_from_files(file_name) + assert extract_text == "" + + mapper.data_path = "tests/data/ams" + + app.config.update({"WEKO_DEPOSIT_FILESIZE_LIMIT": 8}) + extract_text = mapper.extract_text_from_files("sample.txt") + assert extract_text == "This is " + + app.config.update({"WEKO_DEPOSIT_FILESIZE_LIMIT": 8}) + extract_text = mapper.extract_text_from_files("サンプル2.txt") + assert extract_text == "上限:8" + + app.config.update({"WEKO_DEPOSIT_FILESIZE_LIMIT": 2 * 1024 * 1024}) + extract_text = mapper.extract_text_from_files("sample.txt") + assert extract_text == "This is a\ntext file.\n" + + with pytest.raises(ValueError) as e: + extract_text = mapper.extract_text_from_files("png_file.txt") + assert str(e.value) == "Failed to load text file: png_file.txt" + + with pytest.raises(PdfiumError) as e: + extract_text = mapper.extract_text_from_files("png_file.pdf") + assert str(e.value) == "Failed to load PDF file: png_file.pdf" + + with pytest.raises(ValueError) as e: + extract_text = mapper.extract_text_from_files("broken_word.docx") + assert str(e.value) == "Failed to load document: broken_word.docx" + + with pytest.raises(FileNotFoundError) as e: + extract_text = mapper.extract_text_from_files("not_exist.txt") + assert str(e.value) == "File Not Found: not_exist.txt" + + mocker.patch("weko_search_ui.mapper.extract_text_from_pdf", + return_value="This is a pdf file.") + mocker.patch("os.path.isfile", return_value=True) + extract_text = mapper.extract_text_from_files("pdffile.pdf") + assert extract_text == "This is a pdf file." + + mocker.patch("weko_search_ui.mapper.extract_text_with_tika", + return_value="This is a pptx file.") + extract_text = mapper.extract_text_from_files("powerpoint.pptx") + assert extract_text == "This is a pptx file." + + extract_text = mapper.extract_text_from_files("sample.other") + assert extract_text == "" def test_set_by_jsonpath(): data = {} diff --git a/modules/weko-search-ui/tests/test_rest.py b/modules/weko-search-ui/tests/test_rest.py index d7255a5356..60d366e3e2 100644 --- a/modules/weko-search-ui/tests/test_rest.py +++ b/modules/weko-search-ui/tests/test_rest.py @@ -398,6 +398,10 @@ def test_IndexSearchResourceAPI(client_rest, db_register2, db_rocrate_mapping): res = client_rest.get('/v1/records', headers=headers) assert res.status_code == 200 + headers = {'Accept-Language': 'ja,en-US;q=0.9,en;q=0.8'} + res = client_rest.get('/v1/records', headers=headers) + assert res.status_code == 200 + with patch('weko_search_ui.rest.SearchSetting.get_sort_key', return_value=False): res = client_rest.get('/v1/records') assert res.status_code == 200 diff --git a/modules/weko-search-ui/tests/test_utils.py b/modules/weko-search-ui/tests/test_utils.py index 850ebb8521..d00b32afff 100644 --- a/modules/weko-search-ui/tests/test_utils.py +++ b/modules/weko-search-ui/tests/test_utils.py @@ -1006,7 +1006,8 @@ def test_handle_convert_validate_msg_to_jp(i18n_app): # def handle_validate_item_import(list_record, schema) -> list: -def test_handle_validate_item_import(app, mocker_itemtype): +# .tox/c1/bin/pytest --cov=weko_search_ui tests/test_utils.py::test_handle_validate_item_import -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-search-ui/.tox/c1/tmp +def test_handle_validate_item_import(app, mocker_itemtype, mocker): filepath = os.path.join( os.path.dirname(os.path.realpath(__file__)), "data", "csv", "data.json" ) @@ -1040,6 +1041,96 @@ def test_handle_validate_item_import(app, mocker_itemtype): == result ) + schema = { + "type": "object", + "properties": { + "item_xxx": { + "type": "object", + "properties": { + "subitem_yyy": { + "type": "array", + "items": { + "type": "object", + "properties": { + "subitem_zzz": { + "type": "string", + } + } + } + } + } + } + } + } + + list_record = [ + { + "metadata": { + 'item_xxx': { + 'subitem_yyy':[ + {"subitem_zzz": 123} + ] + } + } + } + ] + with app.test_request_context(): + with set_locale("en"): + result = handle_validate_item_import(list_record, schema) + warnings = result[0].get("warnings", []) + target = list_record[0]["metadata"]['item_xxx']['subitem_yyy'][0]["subitem_zzz"] + assert any("Replace value of" in w for w in warnings) + assert any("is different from existing" in w for w in warnings) + assert type(target) == str + + list_record[0]["metadata"]['item_xxx']['subitem_yyy'][0]["subitem_zzz"] = 456 + + with app.test_request_context(): + with set_locale("ja"): + result = handle_validate_item_import(list_record, schema) + warnings = result[0].get("warnings", []) + assert any("へ置き換えました。" in w for w in warnings) + assert any("と異なっています。" in w for w in warnings) + + schema = { + "type": "object", + "properties": { + "item_aaa": { + "type": "object", + "properties": { + "subitem_bbb": { + "type": "object", + "properties": { + "subitem_ccc": { + "enum": [None, "Yes|Yes", "No|No"], + } + } + } + } + } + } + } + + list_record = [ + { + "metadata": { + 'item_aaa': { + 'subitem_bbb': { + "subitem_ccc": "Yes" + } + } + } + } + ] + with app.test_request_context(): + with set_locale("en"): + result = handle_validate_item_import(list_record, schema) + assert "errors" in result[0] + + with app.test_request_context(): + with set_locale("ja"): + result = handle_validate_item_import(list_record, schema) + assert "errors" in result[0] # def represents_int(s): def test_represents_int(): @@ -1101,6 +1192,7 @@ def test_handle_check_duplicate_record(app): # def handle_check_exist_record(list_record) -> list: +# .tox/c1/bin/pytest --cov=weko_search_ui tests/test_utils.py::test_handle_check_exist_record -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-search-ui/.tox/c1/tmp def test_handle_check_exist_record(app): case = unittest.TestCase() # case 1 import new items @@ -1122,7 +1214,8 @@ def test_handle_check_exist_record(app): with open(filepath, encoding="utf-8") as f: result = json.load(f) - case.assertCountEqual(handle_check_exist_record(list_record), result) + with app.test_request_context(): + case.assertCountEqual(handle_check_exist_record(list_record), result) # case 2 import items with id filepath = os.path.join( @@ -1196,6 +1289,127 @@ def test_handle_check_exist_record(app): with set_locale("en"): case.assertCountEqual(handle_check_exist_record(list_record), result) +# .tox/c1/bin/pytest --cov=weko_search_ui tests/test_utils.py::test_handle_check_exist_record_put_auto_fill -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-search-ui/.tox/c1/tmp +def test_handle_check_exist_record_put_auto_fill(app,db): + class DummyRecord: + def __init__(self, recid="10", deleted=False): + self._recid = recid + self._deleted = deleted + self.pid = self + + def is_deleted(self): + return self._deleted + + def get(self, key): + if key == "recid": + return self._recid + return None + # case 5 no id,uri + with patch("weko_deposit.api.WekoRecord.get_record_by_pid") as mock_get_record: + mock_get_record.return_value = DummyRecord(recid="10", deleted=False) + + filepath = os.path.join( + os.path.dirname(os.path.realpath(__file__)), + "data", + "list_records", + "b4_handle_check_exist_record4.json", + ) + with open(filepath, encoding="utf-8") as f: + list_record = json.load(f) + + recid = "10" + with app.test_request_context("/sword/deposit/{}".format(recid), method="PUT"): + request.view_args = {"recid": recid} + result = handle_check_exist_record(list_record) + assert result[0]["id"] == recid + assert result[0]["uri"].endswith("/records/{}".format(recid)) + assert result[0]["status"] == "keep" + +class DummyPid: + def __init__(self, deleted=False): + self._deleted = deleted + def is_deleted(self): + return self._deleted + +class DummyRecord: + def __init__(self, recid="", deleted=False): + self.pid = DummyPid(deleted) + self._recid = recid + def get(self, key): + if key == "recid": + return self._recid + return None + +# .tox/c1/bin/pytest --cov=weko_search_ui tests/test_utils.py::test_handle_check_exist_record_branches -vv -s --cov-branch --cov-report=html --basetemp=/code/modules/weko-search-ui/.tox/c1/tmp +def test_handle_check_exist_record_cases(app, db): + from weko_search_ui.utils import handle_check_exist_record + + # 1. item_id is None, recid is set + list_record = [{"id": None, "uri": None}] + with app.test_request_context("/sword/deposit/100", method="PUT"): + request.view_args = {"recid": "100"} + result = handle_check_exist_record(list_record) + assert result[0]["id"] == "100" + assert result[0]["status"] is None + + # 2. uri and system_url are different + list_record = [{"id": "101", "uri": "http://dummy/other/101"}] + with app.test_request_context("/sword/deposit/101", method="PUT"): + request.view_args = {"recid": "101"} + result = handle_check_exist_record(list_record) + assert result[0]["status"] is None + assert any("Specified URI and system URI do not match." in err for err in result[0].get("errors", [])) + + # 3. PIDDoesNotExistError occurs + list_record = [{"id": "102", "uri": None}] + with patch("weko_deposit.api.WekoRecord.get_record_by_pid", side_effect=PIDDoesNotExistError("recid", "102")): + with app.test_request_context("/sword/deposit/102", method="PUT"): + request.view_args = {"recid": "102"} + result = handle_check_exist_record(list_record) + assert result[0]["status"] is None + assert any("Item does not exist in the system." in err for err in result[0].get("errors", [])) + + # 4. item_exist.pid.is_deleted() is True + list_record = [{"id": "103", "uri": None}] + with patch("weko_deposit.api.WekoRecord.get_record_by_pid", return_value=DummyRecord(recid="103", deleted=True)): + with app.test_request_context("/sword/deposit/103", method="PUT"): + request.view_args = {"recid": "103"} + result = handle_check_exist_record(list_record) + assert result[0]["status"] is None + + # 5. edit_mode is None + list_record = [{"id": "104", "uri": None, "edit_mode": None}] + with patch("weko_deposit.api.WekoRecord.get_record_by_pid", return_value=DummyRecord(recid="104", deleted=False)): + with app.test_request_context("/sword/deposit/104", method="PUT"): + request.view_args = {"recid": "104"} + result = handle_check_exist_record(list_record) + assert result[0]["status"] is None + assert any("Please specify either \"Keep\" or \"Upgrade\"." in err for err in result[0].get("errors", [])) + + # 6. edit_mode is invalid value + list_record = [{"id": "105", "uri": None, "edit_mode": "delete"}] + with patch("weko_deposit.api.WekoRecord.get_record_by_pid", return_value=DummyRecord(recid="105", deleted=False)): + with app.test_request_context("/sword/deposit/105", method="PUT"): + request.view_args = {"recid": "105"} + result = handle_check_exist_record(list_record) + assert result[0]["status"] is None + assert any("Please specify either \"Keep\" or \"Upgrade\"." in err for err in result[0].get("errors", [])) + + # 7. edit_mode is correct value + list_record = [{"id": "106", "uri": None, "edit_mode": "keep"}] + with patch("weko_deposit.api.WekoRecord.get_record_by_pid", return_value=DummyRecord(recid="106", deleted=False)): + with app.test_request_context("/sword/deposit/106", method="PUT"): + request.view_args = {"recid": "106"} + result = handle_check_exist_record(list_record) + assert result[0]["status"] == "keep" + + # 8. When item_id is an empty string + list_record = [{"id": "", "uri": None}] + with app.test_request_context("/sword/deposit/", method="PUT"): + request.view_args = {"recid": ""} + result = handle_check_exist_record(list_record) + assert result[0]["id"] is None + assert result[0]["status"] == "new" # def make_file_by_line(lines): def test_make_file_by_line(i18n_app): diff --git a/modules/weko-search-ui/weko_search_ui/mapper.py b/modules/weko-search-ui/weko_search_ui/mapper.py index aaa343878d..bfa57ac489 100644 --- a/modules/weko-search-ui/weko_search_ui/mapper.py +++ b/modules/weko-search-ui/weko_search_ui/mapper.py @@ -11,6 +11,9 @@ import os import re import json +import chardet +import mimetypes +from pypdfium2 import PdfiumError import itertools import xmltodict import traceback @@ -28,6 +31,7 @@ Mapping, ItemTypes, FeedbackMailList, RequestMailList, ItemLink ) from weko_records.serializers.utils import get_full_mapping +from weko_deposit.utils import extract_text_from_pdf, extract_text_with_tika from .config import ROCRATE_METADATA_FILE, ROCRATE_METADATA_WK_CONTEXT_V1 @@ -1571,8 +1575,7 @@ def is_url(s: str) -> bool: # } return mapped_metadata, system_info - @classmethod - def _deconstruct_json_ld(cls, json_ld): + def _deconstruct_json_ld(self, json_ld): """Deconstruct json-ld. Deconstructing json-ld metadata values ​​one by one @@ -1682,10 +1685,11 @@ def _resolve_link(value): list_extracted = [ extracted ] else: list_extracted = [ extracted ] + self.extract_extended_metadata(list_extracted) list_deconstructed = [] for extracted in list_extracted: - metadata = cls._deconstruct_dict(extracted) + metadata = self._deconstruct_dict(extracted) system_info = {} system_info.update( {"id": metadata.pop("identifier")} @@ -1863,6 +1867,91 @@ def _check_settable_path(self, json_path): return settable_path + def extract_extended_metadata(self, list_extracted): + """ + Store the content of files with wk:extendedMetadata set to True in extended_metadata, + and remove files with wk:extendedMetadata set to True from hasPart. + + Args: + list_extracted (list): List of extracted metadata dictionaries. + Returns: + list: The updated list of extracted metadata with extended metadata merged. + """ + for extracted in list_extracted: + extracted.pop('extended_metadata', None) + if 'hasPart' not in extracted: + continue + file_indices = [ + idx for idx, item in enumerate(extracted['hasPart']) + if item.get('wk:extendedMetadata') is True + ] + if not file_indices: + continue + extended_metadatas = {} + extracted['extended_metadata'] = {} + for idx in reversed(file_indices): + filename = extracted['hasPart'].pop(idx).get('@id') + content = self.extract_text_from_files(filename) + extended_metadatas[filename] = content + extracted['extended_metadata']['value'] = json.dumps( + extended_metadatas, ensure_ascii=False) + return list_extracted + + def extract_text_from_files(self, filename): + """ + Extract text content from the specified file, + only if the file is of a specific MIME type. + Args: + filename (str): The name of the file to extract text from. + + Returns: + str: The extracted text content from the file. + """ + data_path = self.data_path + try: + file_path = os.path.join(data_path, filename) + if not os.path.isfile(file_path): + raise FileNotFoundError + data = "" + mimetype = mimetypes.guess_type(filename)[0] + file_size_limit = current_app.config['WEKO_DEPOSIT_FILESIZE_LIMIT'] + # List of text-based MIME types allowed for text extraction and processing. + text_mimetypes = current_app.config["WEKO_DEPOSIT_TEXTMIMETYPE_WHITELIST_FOR_ES"] + # All mimetypes subject to text extraction (including text_mimetypes) + extract_mimetypes = current_app.config["WEKO_MIMETYPE_WHITELIST_FOR_ES"] + if mimetype not in extract_mimetypes: + return data + + # Extract content from file + current_app.logger.debug(f"extracting content from {filename}") + if mimetype in text_mimetypes: + with open(file_path, "rb") as fp: + data = fp.read(file_size_limit) + inf = chardet.detect(data) + if inf["encoding"] is None: + raise ValueError( + f"Failed to load text file: {filename}") + data = data.decode(inf["encoding"], errors="replace") + elif mimetype == 'application/pdf': + data = extract_text_from_pdf(file_path, file_size_limit) + else: + try: + data = extract_text_with_tika(file_path, file_size_limit) + except Exception as e: + current_app.logger.error(e) + traceback.print_exc() + raise ValueError( + f"Failed to load document: {filename}") from e + except FileNotFoundError as e: + current_app.logger.error(e) + traceback.print_exc() + raise FileNotFoundError(f"File Not Found: {filename}") from e + except PdfiumError as e: + current_app.logger.error(e) + traceback.print_exc() + raise PdfiumError(f"Failed to load PDF file: {filename}") from e + return data + def to_rocrate_metadata( self, record_metadata=None, tsv_row_metadata=None, **kwargs ): @@ -2461,4 +2550,3 @@ def tokenize_jsonpath(json_path): index = None tokens.append((element, index, current_path)) return tokens - diff --git a/modules/weko-search-ui/weko_search_ui/rest.py b/modules/weko-search-ui/weko_search_ui/rest.py index ee0095b6c8..2e797275cc 100644 --- a/modules/weko-search-ui/weko_search_ui/rest.py +++ b/modules/weko-search-ui/weko_search_ui/rest.py @@ -600,6 +600,8 @@ def get_v1(self, **kwargs): try: # Language setting language = request.headers.get('Accept-Language') + if isinstance(language, str): + language = language.split(',')[0].split(';')[0].strip() if language: get_locale().language = language diff --git a/modules/weko-search-ui/weko_search_ui/translations/en/LC_MESSAGES/messages.mo b/modules/weko-search-ui/weko_search_ui/translations/en/LC_MESSAGES/messages.mo index 084df4f48b..cb7fc7ab93 100644 Binary files a/modules/weko-search-ui/weko_search_ui/translations/en/LC_MESSAGES/messages.mo and b/modules/weko-search-ui/weko_search_ui/translations/en/LC_MESSAGES/messages.mo differ diff --git a/modules/weko-search-ui/weko_search_ui/translations/en/LC_MESSAGES/messages.po b/modules/weko-search-ui/weko_search_ui/translations/en/LC_MESSAGES/messages.po index d4ba275c83..5a50d17255 100644 --- a/modules/weko-search-ui/weko_search_ui/translations/en/LC_MESSAGES/messages.po +++ b/modules/weko-search-ui/weko_search_ui/translations/en/LC_MESSAGES/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: weko-search-ui 0.1.0.dev20170000\n" "Report-Msgid-Bugs-To: wekosoftware@nii.ac.jp\n" -"POT-Creation-Date: 2025-11-11 20:49+0900\n" +"POT-Creation-Date: 2025-12-17 13:33+0900\n" "PO-Revision-Date: 2025-04-09 23:22+0900\n" "Last-Translator: FULL NAME \n" "Language: en\n" @@ -19,69 +19,69 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.5.1\n" -#: tests/conftest.py:400 +#: tests/conftest.py:406 msgid "write your own license" msgstr "" -#: tests/conftest.py:405 +#: tests/conftest.py:411 msgid "Creative Commons CC0 1.0 Universal Public Domain Designation" msgstr "" -#: tests/conftest.py:421 +#: tests/conftest.py:427 msgid "Creative Commons Attribution 3.0 Unported (CC BY 3.0)" msgstr "" -#: tests/conftest.py:433 +#: tests/conftest.py:439 msgid "Creative Commons Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0)" msgstr "" -#: tests/conftest.py:448 +#: tests/conftest.py:454 msgid "Creative Commons Attribution-NoDerivs 3.0 Unported (CC BY-ND 3.0)" msgstr "" -#: tests/conftest.py:462 +#: tests/conftest.py:468 msgid "Creative Commons Attribution-NonCommercial 3.0 Unported (CC BY-NC 3.0)" msgstr "" -#: tests/conftest.py:477 +#: tests/conftest.py:483 msgid "" "Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported (CC " "BY-NC-SA 3.0)" msgstr "" -#: tests/conftest.py:492 +#: tests/conftest.py:498 msgid "" "Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported (CC BY-" "NC-ND 3.0)" msgstr "" -#: tests/conftest.py:508 +#: tests/conftest.py:514 msgid "Creative Commons Attribution 4.0 International (CC BY 4.0)" msgstr "" -#: tests/conftest.py:520 +#: tests/conftest.py:526 msgid "Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)" msgstr "" -#: tests/conftest.py:535 +#: tests/conftest.py:541 msgid "" "Creative Commons Attribution-NoDerivatives 4.0 International (CC BY-ND " "4.0)" msgstr "" -#: tests/conftest.py:550 +#: tests/conftest.py:556 msgid "" "Creative Commons Attribution-NonCommercial 4.0 International (CC BY-NC " "4.0)" msgstr "" -#: tests/conftest.py:565 +#: tests/conftest.py:571 msgid "" "Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International " "(CC BY-NC-SA 4.0)" msgstr "" -#: tests/conftest.py:580 +#: tests/conftest.py:586 msgid "" "Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 " "International (CC BY-NC-ND 4.0)" @@ -215,42 +215,44 @@ msgstr "" msgid "Import Result" msgstr "" -#: weko_search_ui/mapper.py:1347 +#: weko_search_ui/mapper.py:1351 msgid "\"{key}\" is required." msgstr "" -#: weko_search_ui/mapper.py:1368 +#: weko_search_ui/mapper.py:1372 +#, python-brace-format msgid "\"{key}\" is not in itemtype." msgstr "" -#: weko_search_ui/mapper.py:1370 +#: weko_search_ui/mapper.py:1374 +#, python-brace-format msgid "\"{key}\" is not in itemtype, did you mean \"{similar_key}\"?" msgstr "" -#: weko_search_ui/mapper.py:1491 +#: weko_search_ui/mapper.py:1495 #, python-format msgid "" "Cannot map to item type from json-ld; Mapping is not defined for the " "metadata, \"%(key)s\": \"%(value)s\"" msgstr "" -#: weko_search_ui/mapper.py:1517 +#: weko_search_ui/mapper.py:1521 #, python-format msgid "" "Cannot map to item type from json-ld; Not found mapping destination for " "the metadata, \"%(key)s\": \"%(value)s\"" msgstr "" -#: weko_search_ui/mapper.py:1527 +#: weko_search_ui/mapper.py:1531 #, python-format msgid "Failed to set metadata for json-ld, \"%(key)s\": \"%(value)s\"" msgstr "" -#: weko_search_ui/mapper.py:1547 +#: weko_search_ui/mapper.py:1553 msgid "Metadata which could not be mapped to item type will be set in 'Extra'." msgstr "" -#: weko_search_ui/mapper.py:1551 +#: weko_search_ui/mapper.py:1557 msgid "Metadata which could not be mapped to item type will be discarded." msgstr "" @@ -269,6 +271,7 @@ msgid "" msgstr "" #: weko_search_ui/utils.py:615 +#, python-brace-format msgid "" "The csv/tsv file was not found in the specified file {}. Check if the " "directory structure is correct." @@ -281,6 +284,7 @@ msgid "" msgstr "" #: weko_search_ui/utils.py:731 +#, python-brace-format msgid "" "The xml file was not found in the specified file {}. Check if the " "directory structure is correct." @@ -311,6 +315,7 @@ msgid "" msgstr "" #: weko_search_ui/utils.py:1244 +#, python-brace-format msgid "The item type ID specified in the {} file does not exist." msgstr "" @@ -319,24 +324,29 @@ msgid "Cannot register because the specified item type is not the latest version msgstr "" #: weko_search_ui/utils.py:1268 +#, python-brace-format msgid "The following metadata keys are duplicated.
{}" msgstr "" #: weko_search_ui/utils.py:1289 +#, python-brace-format msgid "The item does not consistent with the specified item type.
{}" msgstr "" #: weko_search_ui/utils.py:1317 +#, python-brace-format msgid "Cannot read {} file correctly." msgstr "" #: weko_search_ui/utils.py:1341 +#, python-brace-format msgid "" "The following items are not registered because they do not exist in the " "specified item type. {}" msgstr "" #: weko_search_ui/utils.py:1351 +#, python-brace-format msgid "" "The {} file could not be read. Make sure the file format is {} and that " "the file is UTF-8 encoded." @@ -348,259 +358,269 @@ msgid "" " the file is UTF-8 encoded." msgstr "" -#: weko_search_ui/utils.py:1454 +#: weko_search_ui/utils.py:1455 msgid "Please specify item ID by half-width number." msgstr "" -#: weko_search_ui/utils.py:1469 +#: weko_search_ui/utils.py:1477 +#, python-format +msgid "Replace value of %(target_path)s from %(old_value)s to '%(new_value)s'." +msgstr "" + +#: weko_search_ui/utils.py:1489 msgid "Specified item type does not exist." msgstr "" -#: weko_search_ui/utils.py:1540 +#: weko_search_ui/utils.py:1495 +#, python-format +msgid "Specified %(type)s is different from existing %(existing_type)s." +msgstr "" + +#: weko_search_ui/utils.py:1567 msgid "The same item may have been registered." msgstr "" -#: weko_search_ui/utils.py:1570 +#: weko_search_ui/utils.py:1597 msgid "Specified URI and system URI do not match." msgstr "" -#: weko_search_ui/utils.py:1582 +#: weko_search_ui/utils.py:1609 msgid "Item does not exist in the system." msgstr "" -#: weko_search_ui/utils.py:1589 +#: weko_search_ui/utils.py:1616 msgid "Item already DELETED in the system." msgstr "" -#: weko_search_ui/utils.py:1606 +#: weko_search_ui/utils.py:1633 msgid "Please specify either \"Keep\" or \"Upgrade\"." msgstr "" -#: weko_search_ui/utils.py:2565 +#: weko_search_ui/utils.py:2592 msgid "Title is required item." msgstr "" -#: weko_search_ui/utils.py:2582 +#: weko_search_ui/utils.py:2609 msgid "{} is required item." msgstr "" -#: weko_search_ui/utils.py:2585 +#: weko_search_ui/utils.py:2612 msgid "Please set \"public\" or \"private\" for {}." msgstr "" -#: weko_search_ui/utils.py:2626 +#: weko_search_ui/utils.py:2653 msgid "The specified {} does not exist in system." msgstr "" -#: weko_search_ui/utils.py:2636 +#: weko_search_ui/utils.py:2663 msgid "Specified {} does not match with existing index." msgstr "" -#: weko_search_ui/utils.py:2680 +#: weko_search_ui/utils.py:2707 msgid "Your role cannot register items in this index." msgstr "" -#: weko_search_ui/utils.py:2701 +#: weko_search_ui/utils.py:2728 msgid "Both of IndexID and POS_INDEX are not being set." msgstr "" -#: weko_search_ui/utils.py:2747 weko_search_ui/utils.py:2772 +#: weko_search_ui/utils.py:2774 weko_search_ui/utils.py:2799 msgid "Specified {} is invalid." msgstr "" -#: weko_search_ui/utils.py:2806 +#: weko_search_ui/utils.py:2833 msgid "If there is a info of content file, terms of use cannot be set." msgstr "If there is a info of content file, terms of use cannot be set." -#: weko_search_ui/utils.py:2813 weko_search_ui/utils.py:6063 +#: weko_search_ui/utils.py:2840 weko_search_ui/utils.py:6090 msgid "The specified provinding method does not exist in the system" msgstr "" -#: weko_search_ui/utils.py:2818 weko_search_ui/utils.py:6058 +#: weko_search_ui/utils.py:2845 weko_search_ui/utils.py:6085 msgid "The specified terms does not exist in the system" msgstr "" -#: weko_search_ui/utils.py:2910 weko_search_ui/utils.py:2943 -#: weko_search_ui/utils.py:3035 weko_search_ui/utils.py:3147 -#: weko_search_ui/utils.py:3169 weko_search_ui/utils.py:3204 +#: weko_search_ui/utils.py:2937 weko_search_ui/utils.py:2970 +#: weko_search_ui/utils.py:3062 weko_search_ui/utils.py:3174 +#: weko_search_ui/utils.py:3196 weko_search_ui/utils.py:3231 msgid "Please specify {}." msgstr "" -#: weko_search_ui/utils.py:2913 weko_search_ui/utils.py:3150 +#: weko_search_ui/utils.py:2940 weko_search_ui/utils.py:3177 msgid "The specified {} exceeds the maximum length." msgstr "" -#: weko_search_ui/utils.py:2928 weko_search_ui/utils.py:3085 -#: weko_search_ui/utils.py:3096 weko_search_ui/utils.py:3165 +#: weko_search_ui/utils.py:2955 weko_search_ui/utils.py:3112 +#: weko_search_ui/utils.py:3123 weko_search_ui/utils.py:3192 msgid "Specified Prefix of {} is incorrect." msgstr "" -#: weko_search_ui/utils.py:2936 weko_search_ui/utils.py:3079 -#: weko_search_ui/utils.py:3089 +#: weko_search_ui/utils.py:2963 weko_search_ui/utils.py:3106 +#: weko_search_ui/utils.py:3116 msgid "{} cannot be set." msgstr "" -#: weko_search_ui/utils.py:2945 weko_search_ui/utils.py:2949 -#: weko_search_ui/utils.py:3018 weko_search_ui/utils.py:3206 +#: weko_search_ui/utils.py:2972 weko_search_ui/utils.py:2976 +#: weko_search_ui/utils.py:3045 weko_search_ui/utils.py:3233 msgid "Specified {} is different from existing {}." msgstr "" -#: weko_search_ui/utils.py:2969 +#: weko_search_ui/utils.py:2996 msgid "" "When assigning a DOI to an item, it must be associated with an index " "whose index status is \"Public\" and Harvest Publishing is \"Public\"." msgstr "" -#: weko_search_ui/utils.py:2974 +#: weko_search_ui/utils.py:3001 msgid "" "Since the item has a DOI, it must be associated with an index whose index" " status is \"Public\" and whose Harvest Publishing is \"Public\"." msgstr "" -#: weko_search_ui/utils.py:2985 +#: weko_search_ui/utils.py:3012 msgid "You cannot keep an item private because it has a DOI." msgstr "" -#: weko_search_ui/utils.py:3039 weko_search_ui/utils.py:4335 +#: weko_search_ui/utils.py:3066 weko_search_ui/utils.py:4362 msgid "DOI_RA should be set by one of JaLC, Crossref, DataCite, NDL JaLC." msgstr "" -#: weko_search_ui/utils.py:3122 +#: weko_search_ui/utils.py:3149 msgid "Specified DOI was withdrawn. Please specify another DOI." msgstr "" -#: weko_search_ui/utils.py:3130 +#: weko_search_ui/utils.py:3157 msgid "" "Specified DOI has been used already for another item. Please specify " "another DOI." msgstr "" -#: weko_search_ui/utils.py:3226 +#: weko_search_ui/utils.py:3253 msgid "" "Specified DOI is duplicated with another import item. Please specify " "another DOI." msgstr "" -#: weko_search_ui/utils.py:3259 +#: weko_search_ui/utils.py:3286 msgid "Item Link type: '{}' is not one of {}." msgstr "" -#: weko_search_ui/utils.py:3263 +#: weko_search_ui/utils.py:3290 msgid "Please specify Item URL for item link." msgstr "" -#: weko_search_ui/utils.py:3277 +#: weko_search_ui/utils.py:3304 msgid "Specified Item Link URI and system URI do not match." msgstr "" -#: weko_search_ui/utils.py:3283 +#: weko_search_ui/utils.py:3310 msgid "Linking item does not exist in the system." msgstr "" -#: weko_search_ui/utils.py:3286 +#: weko_search_ui/utils.py:3313 msgid "Linking item already deleted in the system." msgstr "" -#: weko_search_ui/utils.py:3325 +#: weko_search_ui/utils.py:3352 msgid "It is not allowed to create links to the item itself." msgstr "" -#: weko_search_ui/utils.py:3330 +#: weko_search_ui/utils.py:3357 msgid "It is not allowed to create links other than {} between split items." msgstr "" -#: weko_search_ui/utils.py:3366 +#: weko_search_ui/utils.py:3393 msgid "Duplicate Item Link." msgstr "" -#: weko_search_ui/utils.py:3403 +#: weko_search_ui/utils.py:3430 msgid "'wk:metadataReplace' flag cannot be used when registering an item." msgstr "" -#: weko_search_ui/utils.py:3720 +#: weko_search_ui/utils.py:3747 msgid "" "One of the following required values ​​has not been " "registered.
{}
" msgstr "" -#: weko_search_ui/utils.py:3725 +#: weko_search_ui/utils.py:3752 msgid "" "The mapping of required items for DOI validation is not set. Please " "recheck the following mapping settings.
{}" msgstr "" -#: weko_search_ui/utils.py:3735 +#: weko_search_ui/utils.py:3762 msgid "The following metadata are required.
{}" msgstr "" -#: weko_search_ui/utils.py:3740 +#: weko_search_ui/utils.py:3767 msgid "One of the following metadata is required.
{}
" msgstr "" -#: weko_search_ui/utils.py:3803 weko_search_ui/utils.py:3815 +#: weko_search_ui/utils.py:3830 weko_search_ui/utils.py:3842 msgid "Please specify the date with any format of YYYY-MM-DD, YYYY-MM, YYYY." msgstr "" -#: weko_search_ui/utils.py:3809 +#: weko_search_ui/utils.py:3836 msgid "Replace value of {} from {} to {}." msgstr "" -#: weko_search_ui/utils.py:3832 +#: weko_search_ui/utils.py:3859 msgid "Please specify PubDate with YYYY-MM-DD." msgstr "" -#: weko_search_ui/utils.py:3862 +#: weko_search_ui/utils.py:3889 msgid "ID is specified for the newly registered item. Ignore the ID and register." msgstr "" -#: weko_search_ui/utils.py:3945 +#: weko_search_ui/utils.py:3972 msgid "Please specify Open Access Date with YYYY-MM-DD." msgstr "" -#: weko_search_ui/utils.py:4172 weko_search_ui/utils.py:4320 -#: weko_search_ui/utils.py:4326 +#: weko_search_ui/utils.py:4199 weko_search_ui/utils.py:4347 +#: weko_search_ui/utils.py:4353 msgid "Please specify DOI prefix/suffix." msgstr "" -#: weko_search_ui/utils.py:4312 +#: weko_search_ui/utils.py:4339 msgid "The specified DOI is wrong and fixed with the registered DOI." msgstr "" -#: weko_search_ui/utils.py:4315 +#: weko_search_ui/utils.py:4342 msgid "" "The specified DOI RA is wrong and fixed with the correct DOI RA of the " "registered DOI." msgstr "" -#: weko_search_ui/utils.py:4322 weko_search_ui/utils.py:4328 +#: weko_search_ui/utils.py:4349 weko_search_ui/utils.py:4355 msgid "Please specify DOI suffix." msgstr "" -#: weko_search_ui/utils.py:4331 +#: weko_search_ui/utils.py:4358 msgid "Do not specify DOI suffix." msgstr "" -#: weko_search_ui/utils.py:4337 +#: weko_search_ui/utils.py:4364 msgid "Specified Prefix of DOI is incorrect." msgstr "" -#: weko_search_ui/utils.py:4369 +#: weko_search_ui/utils.py:4396 msgid "" "Please specify the image file(gif, jpg, jpe, jpeg, png, bmp, tiff, tif) " "for the thumbnail." msgstr "" -#: weko_search_ui/utils.py:5490 +#: weko_search_ui/utils.py:5517 msgid "The file specified in ({}) does not exist." msgstr "" -#: weko_search_ui/utils.py:5494 +#: weko_search_ui/utils.py:5521 msgid "" "The file specified in ({}) does not exist.
The file will not be " "updated. Update only the metadata with csv/tsv contents." msgstr "" -#: weko_search_ui/utils.py:5781 +#: weko_search_ui/utils.py:5808 msgid "The file name specified in {} and {} do not match." msgstr "" @@ -1181,3 +1201,51 @@ msgstr "" msgid "Failed to get mapping list." msgstr "" +#~ msgid "Bagging a parent of the current directory is not supported" +#~ msgstr "" + +#~ msgid "Creating tag for directory %s" +#~ msgstr "" + +#~ msgid "Bag directory %s does not exist" +#~ msgstr "" + +#~ msgid "Bag directory %s does not contain a data directory" +#~ msgstr "" + +#~ msgid "" +#~ "Unable to write to the following directories and files:\n" +#~ "%s" +#~ msgstr "" + +#~ msgid "Missing permissions to move all files and directories" +#~ msgstr "" + +#~ msgid "" +#~ "The following directories do not have read permissions:\n" +#~ "%s" +#~ msgstr "" + +#~ msgid "" +#~ "The following files do not have read permissions:\n" +#~ "%s" +#~ msgstr "" + +#~ msgid "Read permissions are required to calculate file fixities" +#~ msgstr "" + +#~ msgid "Creating data directory" +#~ msgstr "" + +#~ msgid "Creating bagit.txt" +#~ msgstr "" + +#~ msgid "Creating bag-info.txt" +#~ msgstr "" + +#~ msgid "An error occurred creating a bag in %s" +#~ msgstr "" + +#~ msgid "`wk:metadata_replace` flag cannot be used in RO-Crate Import." +#~ msgstr "" + diff --git a/modules/weko-search-ui/weko_search_ui/translations/ja/LC_MESSAGES/messages.mo b/modules/weko-search-ui/weko_search_ui/translations/ja/LC_MESSAGES/messages.mo index 00ad3d906f..1c69f06441 100644 Binary files a/modules/weko-search-ui/weko_search_ui/translations/ja/LC_MESSAGES/messages.mo and b/modules/weko-search-ui/weko_search_ui/translations/ja/LC_MESSAGES/messages.mo differ diff --git a/modules/weko-search-ui/weko_search_ui/translations/ja/LC_MESSAGES/messages.po b/modules/weko-search-ui/weko_search_ui/translations/ja/LC_MESSAGES/messages.po index 0d30ef2d92..4a7b646a60 100644 --- a/modules/weko-search-ui/weko_search_ui/translations/ja/LC_MESSAGES/messages.po +++ b/modules/weko-search-ui/weko_search_ui/translations/ja/LC_MESSAGES/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: weko-search-ui 0.1.0.dev20170000\n" "Report-Msgid-Bugs-To: wekosoftware@nii.ac.jp\n" -"POT-Creation-Date: 2025-11-11 20:49+0900\n" +"POT-Creation-Date: 2025-12-17 13:33+0900\n" "PO-Revision-Date: 2025-04-09 22:35+0900\n" "Last-Translator: FULL NAME \n" "Language: ja\n" @@ -19,69 +19,69 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.5.1\n" -#: tests/conftest.py:400 +#: tests/conftest.py:406 msgid "write your own license" msgstr "" -#: tests/conftest.py:405 +#: tests/conftest.py:411 msgid "Creative Commons CC0 1.0 Universal Public Domain Designation" msgstr "" -#: tests/conftest.py:421 +#: tests/conftest.py:427 msgid "Creative Commons Attribution 3.0 Unported (CC BY 3.0)" msgstr "" -#: tests/conftest.py:433 +#: tests/conftest.py:439 msgid "Creative Commons Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0)" msgstr "" -#: tests/conftest.py:448 +#: tests/conftest.py:454 msgid "Creative Commons Attribution-NoDerivs 3.0 Unported (CC BY-ND 3.0)" msgstr "" -#: tests/conftest.py:462 +#: tests/conftest.py:468 msgid "Creative Commons Attribution-NonCommercial 3.0 Unported (CC BY-NC 3.0)" msgstr "" -#: tests/conftest.py:477 +#: tests/conftest.py:483 msgid "" "Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported (CC " "BY-NC-SA 3.0)" msgstr "" -#: tests/conftest.py:492 +#: tests/conftest.py:498 msgid "" "Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported (CC BY-" "NC-ND 3.0)" msgstr "" -#: tests/conftest.py:508 +#: tests/conftest.py:514 msgid "Creative Commons Attribution 4.0 International (CC BY 4.0)" msgstr "" -#: tests/conftest.py:520 +#: tests/conftest.py:526 msgid "Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)" msgstr "" -#: tests/conftest.py:535 +#: tests/conftest.py:541 msgid "" "Creative Commons Attribution-NoDerivatives 4.0 International (CC BY-ND " "4.0)" msgstr "" -#: tests/conftest.py:550 +#: tests/conftest.py:556 msgid "" "Creative Commons Attribution-NonCommercial 4.0 International (CC BY-NC " "4.0)" msgstr "" -#: tests/conftest.py:565 +#: tests/conftest.py:571 msgid "" "Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International " "(CC BY-NC-SA 4.0)" msgstr "" -#: tests/conftest.py:580 +#: tests/conftest.py:586 msgid "" "Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 " "International (CC BY-NC-ND 4.0)" @@ -215,42 +215,42 @@ msgstr "ステータス" msgid "Import Result" msgstr "インポート結果" -#: weko_search_ui/mapper.py:1347 +#: weko_search_ui/mapper.py:1351 msgid "\"{key}\" is required." msgstr "「{key}」 に対応するマッピングが必要です。" -#: weko_search_ui/mapper.py:1368 +#: weko_search_ui/mapper.py:1372 msgid "\"{key}\" is not in itemtype." msgstr "「{key}」 はアイテムタイプに存在しません。" -#: weko_search_ui/mapper.py:1370 +#: weko_search_ui/mapper.py:1374 msgid "\"{key}\" is not in itemtype, did you mean \"{similar_key}\"?" msgstr "「{key}」 はアイテムタイプに存在しません。「{similar_key}」であれば存在します。" -#: weko_search_ui/mapper.py:1491 +#: weko_search_ui/mapper.py:1495 #, python-format msgid "" "Cannot map to item type from json-ld; Mapping is not defined for the " "metadata, \"%(key)s\": \"%(value)s\"" msgstr "対応するマッピングが定義されてないため、このメタデータはアイテムタイプに変換できません。\"%(key)s\": \"%(value)s\"" -#: weko_search_ui/mapper.py:1517 +#: weko_search_ui/mapper.py:1521 #, python-format msgid "" "Cannot map to item type from json-ld; Not found mapping destination for " "the metadata, \"%(key)s\": \"%(value)s\"" msgstr "マッピング先が見つからないため、このメタデータはアイテムタイプに変換できません。\"%(key)s\": \"%(value)s\"" -#: weko_search_ui/mapper.py:1527 +#: weko_search_ui/mapper.py:1531 #, python-format msgid "Failed to set metadata for json-ld, \"%(key)s\": \"%(value)s\"" msgstr "アイテムタイプへのマッピングに失敗しました。\"%(key)s\": \"%(value)s\"" -#: weko_search_ui/mapper.py:1547 +#: weko_search_ui/mapper.py:1553 msgid "Metadata which could not be mapped to item type will be set in 'Extra'." msgstr "アイテムタイプにマッピングできなかったメタデータは、「Extra」プロパティに登録されます。" -#: weko_search_ui/mapper.py:1551 +#: weko_search_ui/mapper.py:1557 msgid "Metadata which could not be mapped to item type will be discarded." msgstr "アイテムタイプにマッピングできなかったメタデータは破棄されます。" @@ -348,259 +348,269 @@ msgid "" " the file is UTF-8 encoded." msgstr "" -#: weko_search_ui/utils.py:1454 +#: weko_search_ui/utils.py:1455 msgid "Please specify item ID by half-width number." msgstr "アイテムIDは半角数字で指定してください。" -#: weko_search_ui/utils.py:1469 +#: weko_search_ui/utils.py:1477 +#, python-format +msgid "Replace value of %(target_path)s from %(old_value)s to '%(new_value)s'." +msgstr "%(target_path)sの値を%(old_value)sから'%(new_value)s'へ置き換えました。" + +#: weko_search_ui/utils.py:1489 msgid "Specified item type does not exist." msgstr "指定されたアイテムタイプが存在していません。" -#: weko_search_ui/utils.py:1540 +#: weko_search_ui/utils.py:1495 +#, python-brace-format, python-format +msgid "Specified %(type)s is different from existing %(existing_type)s." +msgstr "指定された%(type)sは登録している%(existing_type)sと異なっています。" + +#: weko_search_ui/utils.py:1567 msgid "The same item may have been registered." msgstr "同じアイテムが登録されている可能性があります。" -#: weko_search_ui/utils.py:1570 +#: weko_search_ui/utils.py:1597 msgid "Specified URI and system URI do not match." msgstr "指定されたURIとシステムURIが一致しません。" -#: weko_search_ui/utils.py:1582 +#: weko_search_ui/utils.py:1609 msgid "Item does not exist in the system." msgstr "アイテムがシステムに存在しません。" -#: weko_search_ui/utils.py:1589 +#: weko_search_ui/utils.py:1616 msgid "Item already DELETED in the system." msgstr "アイテムは削除済です。" -#: weko_search_ui/utils.py:1606 +#: weko_search_ui/utils.py:1633 msgid "Please specify either \"Keep\" or \"Upgrade\"." msgstr "Keep、Upgradeのいずれかを指定してください。" -#: weko_search_ui/utils.py:2565 +#: weko_search_ui/utils.py:2592 msgid "Title is required item." msgstr "タイトルは必須項目です。" -#: weko_search_ui/utils.py:2582 +#: weko_search_ui/utils.py:2609 msgid "{} is required item." msgstr "{}は必須項目です。" -#: weko_search_ui/utils.py:2585 +#: weko_search_ui/utils.py:2612 msgid "Please set \"public\" or \"private\" for {}." msgstr "{}はpublic、privateのいずれかを設定してください。" -#: weko_search_ui/utils.py:2626 +#: weko_search_ui/utils.py:2653 msgid "The specified {} does not exist in system." msgstr "指定された{}はシステムに存在しません。" -#: weko_search_ui/utils.py:2636 +#: weko_search_ui/utils.py:2663 msgid "Specified {} does not match with existing index." msgstr "指定された{}はシステムのものと一致していません。" -#: weko_search_ui/utils.py:2680 +#: weko_search_ui/utils.py:2707 msgid "Your role cannot register items in this index." msgstr "ロールの権限が足りずこのインデックスにアイテム登録ができません。" -#: weko_search_ui/utils.py:2701 +#: weko_search_ui/utils.py:2728 msgid "Both of IndexID and POS_INDEX are not being set." msgstr "IndexID, POS_INDEXがどちらも設定されていません。" -#: weko_search_ui/utils.py:2747 weko_search_ui/utils.py:2772 +#: weko_search_ui/utils.py:2774 weko_search_ui/utils.py:2799 msgid "Specified {} is invalid." msgstr "指定された{}は不正です。" -#: weko_search_ui/utils.py:2806 +#: weko_search_ui/utils.py:2833 msgid "If there is a info of content file, terms of use cannot be set." msgstr "コンテンツファイル情報がある場合、利用規約は設定できません。" -#: weko_search_ui/utils.py:2813 weko_search_ui/utils.py:6063 +#: weko_search_ui/utils.py:2840 weko_search_ui/utils.py:6090 msgid "The specified provinding method does not exist in the system" msgstr "指定する提供方法はシステムに存在しません。" -#: weko_search_ui/utils.py:2818 weko_search_ui/utils.py:6058 +#: weko_search_ui/utils.py:2845 weko_search_ui/utils.py:6085 msgid "The specified terms does not exist in the system" msgstr "指定する利用規約はシステムに存在しません。" -#: weko_search_ui/utils.py:2910 weko_search_ui/utils.py:2943 -#: weko_search_ui/utils.py:3035 weko_search_ui/utils.py:3147 -#: weko_search_ui/utils.py:3169 weko_search_ui/utils.py:3204 +#: weko_search_ui/utils.py:2937 weko_search_ui/utils.py:2970 +#: weko_search_ui/utils.py:3062 weko_search_ui/utils.py:3174 +#: weko_search_ui/utils.py:3196 weko_search_ui/utils.py:3231 msgid "Please specify {}." msgstr "{}を設定してください。" -#: weko_search_ui/utils.py:2913 weko_search_ui/utils.py:3150 +#: weko_search_ui/utils.py:2940 weko_search_ui/utils.py:3177 msgid "The specified {} exceeds the maximum length." msgstr "指定された{}が最大長を超えています。" -#: weko_search_ui/utils.py:2928 weko_search_ui/utils.py:3085 -#: weko_search_ui/utils.py:3096 weko_search_ui/utils.py:3165 +#: weko_search_ui/utils.py:2955 weko_search_ui/utils.py:3112 +#: weko_search_ui/utils.py:3123 weko_search_ui/utils.py:3192 msgid "Specified Prefix of {} is incorrect." msgstr "指定された{}のPrefixが誤っています。" -#: weko_search_ui/utils.py:2936 weko_search_ui/utils.py:3079 -#: weko_search_ui/utils.py:3089 +#: weko_search_ui/utils.py:2963 weko_search_ui/utils.py:3106 +#: weko_search_ui/utils.py:3116 msgid "{} cannot be set." msgstr "{}は設定できません。" -#: weko_search_ui/utils.py:2945 weko_search_ui/utils.py:2949 -#: weko_search_ui/utils.py:3018 weko_search_ui/utils.py:3206 +#: weko_search_ui/utils.py:2972 weko_search_ui/utils.py:2976 +#: weko_search_ui/utils.py:3045 weko_search_ui/utils.py:3233 msgid "Specified {} is different from existing {}." msgstr "指定された{}は登録している{}と異なっています。" -#: weko_search_ui/utils.py:2969 +#: weko_search_ui/utils.py:2996 msgid "" "When assigning a DOI to an item, it must be associated with an index " "whose index status is \"Public\" and Harvest Publishing is \"Public\"." msgstr "" -#: weko_search_ui/utils.py:2974 +#: weko_search_ui/utils.py:3001 msgid "" "Since the item has a DOI, it must be associated with an index whose index" " status is \"Public\" and whose Harvest Publishing is \"Public\"." msgstr "アイテムにDOIが付与されているため、インデックス状態が「公開」かつハーベスト公開が「公開」のインデックスに関連付けが必要です。" -#: weko_search_ui/utils.py:2985 +#: weko_search_ui/utils.py:3012 msgid "You cannot keep an item private because it has a DOI." msgstr "アイテムにDOIが付与されているため、アイテムを非公開にすることはできません。" -#: weko_search_ui/utils.py:3039 weko_search_ui/utils.py:4335 +#: weko_search_ui/utils.py:3066 weko_search_ui/utils.py:4362 msgid "DOI_RA should be set by one of JaLC, Crossref, DataCite, NDL JaLC." msgstr "DOI_RAはJaLC,Crossref,DataCite,NDL JaLCのいずれかを設定してください。" -#: weko_search_ui/utils.py:3122 +#: weko_search_ui/utils.py:3149 msgid "Specified DOI was withdrawn. Please specify another DOI." msgstr "指定されたDOIは取り下げられました。別のDOIを指定してください。" -#: weko_search_ui/utils.py:3130 +#: weko_search_ui/utils.py:3157 msgid "" "Specified DOI has been used already for another item. Please specify " "another DOI." msgstr "指定されたDOIは既に別のアイテムに付与されています。別のDOIを指定してください。" -#: weko_search_ui/utils.py:3226 +#: weko_search_ui/utils.py:3253 msgid "" "Specified DOI is duplicated with another import item. Please specify " "another DOI." msgstr "指定されたDOIはインポートファイル内で重複しています。別のDOIを指定してください。" -#: weko_search_ui/utils.py:3259 +#: weko_search_ui/utils.py:3286 msgid "Item Link type: '{}' is not one of {}." msgstr "リンクタイプ'{}'は次の決められた選択肢に含まれていません。{}" -#: weko_search_ui/utils.py:3263 +#: weko_search_ui/utils.py:3290 msgid "Please specify Item URL for item link." msgstr "アイテムリンク先のURLを指定してください。" -#: weko_search_ui/utils.py:3277 +#: weko_search_ui/utils.py:3304 msgid "Specified Item Link URI and system URI do not match." msgstr "指定されたアイテムリンク先URIとシステムURIが一致しません。" -#: weko_search_ui/utils.py:3283 +#: weko_search_ui/utils.py:3310 msgid "Linking item does not exist in the system." msgstr "リンク先アイテムがシステムに存在しません。" -#: weko_search_ui/utils.py:3286 +#: weko_search_ui/utils.py:3313 msgid "Linking item already deleted in the system." msgstr "リンク先アイテムは削除済です。" -#: weko_search_ui/utils.py:3325 +#: weko_search_ui/utils.py:3352 msgid "It is not allowed to create links to the item itself." msgstr "自身へのアイテムリンクを作成することはできません。" -#: weko_search_ui/utils.py:3330 +#: weko_search_ui/utils.py:3357 msgid "It is not allowed to create links other than {} between split items." msgstr "分割したアイテム間で{}以外のリンクを作成することはできません。" -#: weko_search_ui/utils.py:3366 +#: weko_search_ui/utils.py:3393 msgid "Duplicate Item Link." msgstr "重複するアイテムリンクを作成することはできません。" -#: weko_search_ui/utils.py:3403 +#: weko_search_ui/utils.py:3430 msgid "'wk:metadataReplace' flag cannot be used when registering an item." msgstr "RO-Crate インポートでは、`wk:metadataReplace`フラグを有効にできません。" -#: weko_search_ui/utils.py:3720 +#: weko_search_ui/utils.py:3747 msgid "" "One of the following required values ​​has not been " "registered.
{}
" msgstr "次のいずれかの必要値が登録していません。
{}
" -#: weko_search_ui/utils.py:3725 +#: weko_search_ui/utils.py:3752 msgid "" "The mapping of required items for DOI validation is not set. Please " "recheck the following mapping settings.
{}" msgstr "" -#: weko_search_ui/utils.py:3735 +#: weko_search_ui/utils.py:3762 msgid "The following metadata are required.
{}" msgstr "{}は必須項目です。" -#: weko_search_ui/utils.py:3740 +#: weko_search_ui/utils.py:3767 msgid "One of the following metadata is required.
{}
" msgstr "{}のいずれかを設定してください。" -#: weko_search_ui/utils.py:3803 weko_search_ui/utils.py:3815 +#: weko_search_ui/utils.py:3830 weko_search_ui/utils.py:3842 msgid "Please specify the date with any format of YYYY-MM-DD, YYYY-MM, YYYY." msgstr "日付はYYYY-MM-DD、YYYY-MM、YYYYのいずれかで指定してください。" -#: weko_search_ui/utils.py:3809 +#: weko_search_ui/utils.py:3836 msgid "Replace value of {} from {} to {}." msgstr "{}の値を{}から{}へ置き換えました。" -#: weko_search_ui/utils.py:3832 +#: weko_search_ui/utils.py:3859 msgid "Please specify PubDate with YYYY-MM-DD." msgstr "公開日はYYYY-MM-DDで指定してください。" -#: weko_search_ui/utils.py:3862 +#: weko_search_ui/utils.py:3889 msgid "ID is specified for the newly registered item. Ignore the ID and register." msgstr "新規登録アイテムにIDが指定されています。IDを無視して登録を行います。" -#: weko_search_ui/utils.py:3945 +#: weko_search_ui/utils.py:3972 msgid "Please specify Open Access Date with YYYY-MM-DD." msgstr "オープンアクセスの日付はYYYY-MM-DDで指定してください。" -#: weko_search_ui/utils.py:4172 weko_search_ui/utils.py:4320 -#: weko_search_ui/utils.py:4326 +#: weko_search_ui/utils.py:4199 weko_search_ui/utils.py:4347 +#: weko_search_ui/utils.py:4353 msgid "Please specify DOI prefix/suffix." msgstr "DOIは prefix/suffix の形式で指定して下さい。" -#: weko_search_ui/utils.py:4312 +#: weko_search_ui/utils.py:4339 msgid "The specified DOI is wrong and fixed with the registered DOI." msgstr "" -#: weko_search_ui/utils.py:4315 +#: weko_search_ui/utils.py:4342 msgid "" "The specified DOI RA is wrong and fixed with the correct DOI RA of the " "registered DOI." msgstr "" -#: weko_search_ui/utils.py:4322 weko_search_ui/utils.py:4328 +#: weko_search_ui/utils.py:4349 weko_search_ui/utils.py:4355 msgid "Please specify DOI suffix." msgstr "" -#: weko_search_ui/utils.py:4331 +#: weko_search_ui/utils.py:4358 msgid "Do not specify DOI suffix." msgstr "" -#: weko_search_ui/utils.py:4337 +#: weko_search_ui/utils.py:4364 msgid "Specified Prefix of DOI is incorrect." msgstr "" -#: weko_search_ui/utils.py:4369 +#: weko_search_ui/utils.py:4396 msgid "" "Please specify the image file(gif, jpg, jpe, jpeg, png, bmp, tiff, tif) " "for the thumbnail." msgstr "サムネイルは画像ファイル(gif, jpg, jpe, jpeg, png, bmp, tiff, tif)を指定してください。" -#: weko_search_ui/utils.py:5490 +#: weko_search_ui/utils.py:5517 msgid "The file specified in ({}) does not exist." msgstr "({})に指定したファイルが存在しません。" -#: weko_search_ui/utils.py:5494 +#: weko_search_ui/utils.py:5521 msgid "" "The file specified in ({}) does not exist.
The file will not be " "updated. Update only the metadata with csv/tsv contents." msgstr "({})に指定したファイルが存在しません。
ファイルの更新はしません。csv/tsv内容でメタデータのみ更新します。" -#: weko_search_ui/utils.py:5781 +#: weko_search_ui/utils.py:5808 msgid "The file name specified in {} and {} do not match." msgstr "{}に指定されたファイル名と{}が一致しません。" @@ -1181,3 +1191,48 @@ msgstr "マッピング" msgid "Failed to get mapping list." msgstr "マッピング定義リストの取得に失敗しました。" +#~ msgid "Bagging a parent of the current directory is not supported" +#~ msgstr "" + +#~ msgid "Creating tag for directory %s" +#~ msgstr "" + +#~ msgid "Bag directory %s does not exist" +#~ msgstr "" + +#~ msgid "Bag directory %s does not contain a data directory" +#~ msgstr "" + +#~ msgid "" +#~ "Unable to write to the following directories and files:\n" +#~ "%s" +#~ msgstr "" + +#~ msgid "Missing permissions to move all files and directories" +#~ msgstr "" + +#~ msgid "" +#~ "The following directories do not have read permissions:\n" +#~ "%s" +#~ msgstr "" + +#~ msgid "" +#~ "The following files do not have read permissions:\n" +#~ "%s" +#~ msgstr "" + +#~ msgid "Read permissions are required to calculate file fixities" +#~ msgstr "" + +#~ msgid "Creating data directory" +#~ msgstr "" + +#~ msgid "Creating bagit.txt" +#~ msgstr "" + +#~ msgid "Creating bag-info.txt" +#~ msgstr "" + +#~ msgid "An error occurred creating a bag in %s" +#~ msgstr "" + diff --git a/modules/weko-search-ui/weko_search_ui/translations/messages.pot b/modules/weko-search-ui/weko_search_ui/translations/messages.pot index 0a30a6c702..66522c42fb 100644 --- a/modules/weko-search-ui/weko_search_ui/translations/messages.pot +++ b/modules/weko-search-ui/weko_search_ui/translations/messages.pot @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: weko-search-ui 0.1.0.dev20170000\n" "Report-Msgid-Bugs-To: wekosoftware@nii.ac.jp\n" -"POT-Creation-Date: 2025-11-11 20:49+0900\n" +"POT-Creation-Date: 2025-12-17 13:33+0900\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,69 +18,69 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.5.1\n" -#: tests/conftest.py:400 +#: tests/conftest.py:406 msgid "write your own license" msgstr "" -#: tests/conftest.py:405 +#: tests/conftest.py:411 msgid "Creative Commons CC0 1.0 Universal Public Domain Designation" msgstr "" -#: tests/conftest.py:421 +#: tests/conftest.py:427 msgid "Creative Commons Attribution 3.0 Unported (CC BY 3.0)" msgstr "" -#: tests/conftest.py:433 +#: tests/conftest.py:439 msgid "Creative Commons Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0)" msgstr "" -#: tests/conftest.py:448 +#: tests/conftest.py:454 msgid "Creative Commons Attribution-NoDerivs 3.0 Unported (CC BY-ND 3.0)" msgstr "" -#: tests/conftest.py:462 +#: tests/conftest.py:468 msgid "Creative Commons Attribution-NonCommercial 3.0 Unported (CC BY-NC 3.0)" msgstr "" -#: tests/conftest.py:477 +#: tests/conftest.py:483 msgid "" "Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported (CC " "BY-NC-SA 3.0)" msgstr "" -#: tests/conftest.py:492 +#: tests/conftest.py:498 msgid "" "Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported (CC BY-" "NC-ND 3.0)" msgstr "" -#: tests/conftest.py:508 +#: tests/conftest.py:514 msgid "Creative Commons Attribution 4.0 International (CC BY 4.0)" msgstr "" -#: tests/conftest.py:520 +#: tests/conftest.py:526 msgid "Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)" msgstr "" -#: tests/conftest.py:535 +#: tests/conftest.py:541 msgid "" "Creative Commons Attribution-NoDerivatives 4.0 International (CC BY-ND " "4.0)" msgstr "" -#: tests/conftest.py:550 +#: tests/conftest.py:556 msgid "" "Creative Commons Attribution-NonCommercial 4.0 International (CC BY-NC " "4.0)" msgstr "" -#: tests/conftest.py:565 +#: tests/conftest.py:571 msgid "" "Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International " "(CC BY-NC-SA 4.0)" msgstr "" -#: tests/conftest.py:580 +#: tests/conftest.py:586 msgid "" "Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 " "International (CC BY-NC-ND 4.0)" @@ -214,42 +214,42 @@ msgstr "" msgid "Import Result" msgstr "" -#: weko_search_ui/mapper.py:1347 +#: weko_search_ui/mapper.py:1351 msgid "\"{key}\" is required." msgstr "" -#: weko_search_ui/mapper.py:1368 +#: weko_search_ui/mapper.py:1372 msgid "\"{key}\" is not in itemtype." msgstr "" -#: weko_search_ui/mapper.py:1370 +#: weko_search_ui/mapper.py:1374 msgid "\"{key}\" is not in itemtype, did you mean \"{similar_key}\"?" msgstr "" -#: weko_search_ui/mapper.py:1491 +#: weko_search_ui/mapper.py:1495 #, python-format msgid "" "Cannot map to item type from json-ld; Mapping is not defined for the " "metadata, \"%(key)s\": \"%(value)s\"" msgstr "" -#: weko_search_ui/mapper.py:1517 +#: weko_search_ui/mapper.py:1521 #, python-format msgid "" "Cannot map to item type from json-ld; Not found mapping destination for " "the metadata, \"%(key)s\": \"%(value)s\"" msgstr "" -#: weko_search_ui/mapper.py:1527 +#: weko_search_ui/mapper.py:1531 #, python-format msgid "Failed to set metadata for json-ld, \"%(key)s\": \"%(value)s\"" msgstr "" -#: weko_search_ui/mapper.py:1547 +#: weko_search_ui/mapper.py:1553 msgid "Metadata which could not be mapped to item type will be set in 'Extra'." msgstr "" -#: weko_search_ui/mapper.py:1551 +#: weko_search_ui/mapper.py:1557 msgid "Metadata which could not be mapped to item type will be discarded." msgstr "" @@ -347,259 +347,269 @@ msgid "" " the file is UTF-8 encoded." msgstr "" -#: weko_search_ui/utils.py:1454 +#: weko_search_ui/utils.py:1455 msgid "Please specify item ID by half-width number." msgstr "" -#: weko_search_ui/utils.py:1469 +#: weko_search_ui/utils.py:1477 +#, python-format +msgid "Replace value of %(target_path)s from %(old_value)s to '%(new_value)s'." +msgstr "" + +#: weko_search_ui/utils.py:1489 msgid "Specified item type does not exist." msgstr "" -#: weko_search_ui/utils.py:1540 +#: weko_search_ui/utils.py:1495 +#, python-format +msgid "Specified %(type)s is different from existing %(existing_type)s." +msgstr "" + +#: weko_search_ui/utils.py:1567 msgid "The same item may have been registered." msgstr "" -#: weko_search_ui/utils.py:1570 +#: weko_search_ui/utils.py:1597 msgid "Specified URI and system URI do not match." msgstr "" -#: weko_search_ui/utils.py:1582 +#: weko_search_ui/utils.py:1609 msgid "Item does not exist in the system." msgstr "" -#: weko_search_ui/utils.py:1589 +#: weko_search_ui/utils.py:1616 msgid "Item already DELETED in the system." msgstr "" -#: weko_search_ui/utils.py:1606 +#: weko_search_ui/utils.py:1633 msgid "Please specify either \"Keep\" or \"Upgrade\"." msgstr "" -#: weko_search_ui/utils.py:2565 +#: weko_search_ui/utils.py:2592 msgid "Title is required item." msgstr "" -#: weko_search_ui/utils.py:2582 +#: weko_search_ui/utils.py:2609 msgid "{} is required item." msgstr "" -#: weko_search_ui/utils.py:2585 +#: weko_search_ui/utils.py:2612 msgid "Please set \"public\" or \"private\" for {}." msgstr "" -#: weko_search_ui/utils.py:2626 +#: weko_search_ui/utils.py:2653 msgid "The specified {} does not exist in system." msgstr "" -#: weko_search_ui/utils.py:2636 +#: weko_search_ui/utils.py:2663 msgid "Specified {} does not match with existing index." msgstr "" -#: weko_search_ui/utils.py:2680 +#: weko_search_ui/utils.py:2707 msgid "Your role cannot register items in this index." msgstr "" -#: weko_search_ui/utils.py:2701 +#: weko_search_ui/utils.py:2728 msgid "Both of IndexID and POS_INDEX are not being set." msgstr "" -#: weko_search_ui/utils.py:2747 weko_search_ui/utils.py:2772 +#: weko_search_ui/utils.py:2774 weko_search_ui/utils.py:2799 msgid "Specified {} is invalid." msgstr "" -#: weko_search_ui/utils.py:2806 +#: weko_search_ui/utils.py:2833 msgid "If there is a info of content file, terms of use cannot be set." msgstr "" -#: weko_search_ui/utils.py:2813 weko_search_ui/utils.py:6063 +#: weko_search_ui/utils.py:2840 weko_search_ui/utils.py:6090 msgid "The specified provinding method does not exist in the system" msgstr "" -#: weko_search_ui/utils.py:2818 weko_search_ui/utils.py:6058 +#: weko_search_ui/utils.py:2845 weko_search_ui/utils.py:6085 msgid "The specified terms does not exist in the system" msgstr "" -#: weko_search_ui/utils.py:2910 weko_search_ui/utils.py:2943 -#: weko_search_ui/utils.py:3035 weko_search_ui/utils.py:3147 -#: weko_search_ui/utils.py:3169 weko_search_ui/utils.py:3204 +#: weko_search_ui/utils.py:2937 weko_search_ui/utils.py:2970 +#: weko_search_ui/utils.py:3062 weko_search_ui/utils.py:3174 +#: weko_search_ui/utils.py:3196 weko_search_ui/utils.py:3231 msgid "Please specify {}." msgstr "" -#: weko_search_ui/utils.py:2913 weko_search_ui/utils.py:3150 +#: weko_search_ui/utils.py:2940 weko_search_ui/utils.py:3177 msgid "The specified {} exceeds the maximum length." msgstr "" -#: weko_search_ui/utils.py:2928 weko_search_ui/utils.py:3085 -#: weko_search_ui/utils.py:3096 weko_search_ui/utils.py:3165 +#: weko_search_ui/utils.py:2955 weko_search_ui/utils.py:3112 +#: weko_search_ui/utils.py:3123 weko_search_ui/utils.py:3192 msgid "Specified Prefix of {} is incorrect." msgstr "" -#: weko_search_ui/utils.py:2936 weko_search_ui/utils.py:3079 -#: weko_search_ui/utils.py:3089 +#: weko_search_ui/utils.py:2963 weko_search_ui/utils.py:3106 +#: weko_search_ui/utils.py:3116 msgid "{} cannot be set." msgstr "" -#: weko_search_ui/utils.py:2945 weko_search_ui/utils.py:2949 -#: weko_search_ui/utils.py:3018 weko_search_ui/utils.py:3206 +#: weko_search_ui/utils.py:2972 weko_search_ui/utils.py:2976 +#: weko_search_ui/utils.py:3045 weko_search_ui/utils.py:3233 msgid "Specified {} is different from existing {}." msgstr "" -#: weko_search_ui/utils.py:2969 +#: weko_search_ui/utils.py:2996 msgid "" "When assigning a DOI to an item, it must be associated with an index " "whose index status is \"Public\" and Harvest Publishing is \"Public\"." msgstr "" -#: weko_search_ui/utils.py:2974 +#: weko_search_ui/utils.py:3001 msgid "" "Since the item has a DOI, it must be associated with an index whose index" " status is \"Public\" and whose Harvest Publishing is \"Public\"." msgstr "" -#: weko_search_ui/utils.py:2985 +#: weko_search_ui/utils.py:3012 msgid "You cannot keep an item private because it has a DOI." msgstr "" -#: weko_search_ui/utils.py:3039 weko_search_ui/utils.py:4335 +#: weko_search_ui/utils.py:3066 weko_search_ui/utils.py:4362 msgid "DOI_RA should be set by one of JaLC, Crossref, DataCite, NDL JaLC." msgstr "" -#: weko_search_ui/utils.py:3122 +#: weko_search_ui/utils.py:3149 msgid "Specified DOI was withdrawn. Please specify another DOI." msgstr "" -#: weko_search_ui/utils.py:3130 +#: weko_search_ui/utils.py:3157 msgid "" "Specified DOI has been used already for another item. Please specify " "another DOI." msgstr "" -#: weko_search_ui/utils.py:3226 +#: weko_search_ui/utils.py:3253 msgid "" "Specified DOI is duplicated with another import item. Please specify " "another DOI." msgstr "" -#: weko_search_ui/utils.py:3259 +#: weko_search_ui/utils.py:3286 msgid "Item Link type: '{}' is not one of {}." msgstr "" -#: weko_search_ui/utils.py:3263 +#: weko_search_ui/utils.py:3290 msgid "Please specify Item URL for item link." msgstr "" -#: weko_search_ui/utils.py:3277 +#: weko_search_ui/utils.py:3304 msgid "Specified Item Link URI and system URI do not match." msgstr "" -#: weko_search_ui/utils.py:3283 +#: weko_search_ui/utils.py:3310 msgid "Linking item does not exist in the system." msgstr "" -#: weko_search_ui/utils.py:3286 +#: weko_search_ui/utils.py:3313 msgid "Linking item already deleted in the system." msgstr "" -#: weko_search_ui/utils.py:3325 +#: weko_search_ui/utils.py:3352 msgid "It is not allowed to create links to the item itself." msgstr "" -#: weko_search_ui/utils.py:3330 +#: weko_search_ui/utils.py:3357 msgid "It is not allowed to create links other than {} between split items." msgstr "" -#: weko_search_ui/utils.py:3366 +#: weko_search_ui/utils.py:3393 msgid "Duplicate Item Link." msgstr "" -#: weko_search_ui/utils.py:3403 +#: weko_search_ui/utils.py:3430 msgid "'wk:metadataReplace' flag cannot be used when registering an item." msgstr "" -#: weko_search_ui/utils.py:3720 +#: weko_search_ui/utils.py:3747 msgid "" "One of the following required values ​​has not been " "registered.
{}
" msgstr "" -#: weko_search_ui/utils.py:3725 +#: weko_search_ui/utils.py:3752 msgid "" "The mapping of required items for DOI validation is not set. Please " "recheck the following mapping settings.
{}" msgstr "" -#: weko_search_ui/utils.py:3735 +#: weko_search_ui/utils.py:3762 msgid "The following metadata are required.
{}" msgstr "" -#: weko_search_ui/utils.py:3740 +#: weko_search_ui/utils.py:3767 msgid "One of the following metadata is required.
{}
" msgstr "" -#: weko_search_ui/utils.py:3803 weko_search_ui/utils.py:3815 +#: weko_search_ui/utils.py:3830 weko_search_ui/utils.py:3842 msgid "Please specify the date with any format of YYYY-MM-DD, YYYY-MM, YYYY." msgstr "" -#: weko_search_ui/utils.py:3809 +#: weko_search_ui/utils.py:3836 msgid "Replace value of {} from {} to {}." msgstr "" -#: weko_search_ui/utils.py:3832 +#: weko_search_ui/utils.py:3859 msgid "Please specify PubDate with YYYY-MM-DD." msgstr "" -#: weko_search_ui/utils.py:3862 +#: weko_search_ui/utils.py:3889 msgid "ID is specified for the newly registered item. Ignore the ID and register." msgstr "" -#: weko_search_ui/utils.py:3945 +#: weko_search_ui/utils.py:3972 msgid "Please specify Open Access Date with YYYY-MM-DD." msgstr "" -#: weko_search_ui/utils.py:4172 weko_search_ui/utils.py:4320 -#: weko_search_ui/utils.py:4326 +#: weko_search_ui/utils.py:4199 weko_search_ui/utils.py:4347 +#: weko_search_ui/utils.py:4353 msgid "Please specify DOI prefix/suffix." msgstr "" -#: weko_search_ui/utils.py:4312 +#: weko_search_ui/utils.py:4339 msgid "The specified DOI is wrong and fixed with the registered DOI." msgstr "" -#: weko_search_ui/utils.py:4315 +#: weko_search_ui/utils.py:4342 msgid "" "The specified DOI RA is wrong and fixed with the correct DOI RA of the " "registered DOI." msgstr "" -#: weko_search_ui/utils.py:4322 weko_search_ui/utils.py:4328 +#: weko_search_ui/utils.py:4349 weko_search_ui/utils.py:4355 msgid "Please specify DOI suffix." msgstr "" -#: weko_search_ui/utils.py:4331 +#: weko_search_ui/utils.py:4358 msgid "Do not specify DOI suffix." msgstr "" -#: weko_search_ui/utils.py:4337 +#: weko_search_ui/utils.py:4364 msgid "Specified Prefix of DOI is incorrect." msgstr "" -#: weko_search_ui/utils.py:4369 +#: weko_search_ui/utils.py:4396 msgid "" "Please specify the image file(gif, jpg, jpe, jpeg, png, bmp, tiff, tif) " "for the thumbnail." msgstr "" -#: weko_search_ui/utils.py:5490 +#: weko_search_ui/utils.py:5517 msgid "The file specified in ({}) does not exist." msgstr "" -#: weko_search_ui/utils.py:5494 +#: weko_search_ui/utils.py:5521 msgid "" "The file specified in ({}) does not exist.
The file will not be " "updated. Update only the metadata with csv/tsv contents." msgstr "" -#: weko_search_ui/utils.py:5781 +#: weko_search_ui/utils.py:5808 msgid "The file name specified in {} and {} do not match." msgstr "" diff --git a/modules/weko-search-ui/weko_search_ui/utils.py b/modules/weko-search-ui/weko_search_ui/utils.py index 7782a5e6d5..c9d077f515 100644 --- a/modules/weko-search-ui/weko_search_ui/utils.py +++ b/modules/weko-search-ui/weko_search_ui/utils.py @@ -574,7 +574,7 @@ def check_tsv_import_items( list_record = handle_check_exist_record(list_record) handle_item_title(list_record) - + list_record = handle_check_date(list_record) handle_check_id(list_record) @@ -594,7 +594,7 @@ def check_tsv_import_items( handle_check_authors_prefix(list_record) handle_check_authors_affiliation(list_record) - + if not is_gakuninrdm: handle_check_cnri(list_record) handle_check_doi_indexes(list_record) @@ -768,7 +768,7 @@ def unpackage_import_file(data_path: str, file_name: str, file_format: str, forc record["uri"] = None current_app.logger.debug('list_record2: {}'.format(list_record)) - + handle_set_change_identifier_flag(list_record, is_change_identifier) handle_fill_system_item(list_record) @@ -930,7 +930,8 @@ def check_jsonld_import_items( with open(f"{data_path}/{json_name}", "r") as f: json_ld = json.load(f) - item_metadatas, _fromat = mapper.to_item_metadata(json_ld) + mapper.data_path = data_path + item_metadatas, _ = mapper.to_item_metadata(json_ld) list_record = [ { "$schema": f"/items/jsonschema/{item_type.id}", @@ -1262,7 +1263,7 @@ def read_stats_file(file_path: str, file_name: str, file_format: str) -> dict: current_app.logger.debug( "duplication_item_ids: {}".format(duplication_item_ids) ) - + if duplication_item_ids: msg = _("The following metadata keys are duplicated." "
{}") raise Exception( @@ -1283,7 +1284,7 @@ def read_stats_file(file_path: str, file_name: str, file_format: str) -> dict: current_app.logger.debug( "not_consistent_list: {}".format(not_consistent_list) ) - + if not_consistent_list: msg = _( "The item does not consistent with the " @@ -1302,7 +1303,7 @@ def read_stats_file(file_path: str, file_name: str, file_format: str) -> dict: current_app.logger.debug( "item_path_not_existed: {}".format(item_path_not_existed) ) - + elif (num == 4 or num == 5) and data_row[0].startswith("#"): continue @@ -1310,7 +1311,7 @@ def read_stats_file(file_path: str, file_name: str, file_format: str) -> dict: data_parse_metadata = parse_to_json_form( zip(item_path, data_row), item_path_not_existed ) - + if not data_parse_metadata: raise Exception( {"error_msg": _("Cannot read {} file correctly.".format(file_format.upper()))} @@ -1446,6 +1447,7 @@ def handle_validate_item_import(list_record, schema) -> list: v2 = Draft4Validator(schema) if schema else None for record in list_record: errors = record.get("errors") or [] + warnings = [] record_id = record.get("id") if record_id and ( not represents_int(record_id) or re.search(r"([0-9])", record_id) @@ -1457,19 +1459,45 @@ def handle_validate_item_import(list_record, schema) -> list: if record.get("metadata"): if v2: a = v2.iter_errors(record.get("metadata")) + for error in a: + if ( + error.validator == "type" + and error.validator_value == "string" + and isinstance(error.instance, int) + ): + target = record["metadata"] + path_list = list(error.path) + for key in path_list[:-1]: + target = target[key] + last_key = path_list[-1] + target[last_key] = str(target[last_key]) + target_path = ".".join([str(p) for p in path_list[:-2]]) + warnings.append( + _("Replace value of %(target_path)s from %(old_value)s to '%(new_value)s'.", + target_path=target_path, old_value=target[last_key], new_value=str(target[last_key]) + ) + ) + b = v2.iter_errors(record.get("metadata")) if current_i18n.language == "ja": _errors = [] - for error in a: + for error in b: _errors.append(handle_convert_validate_msg_to_jp(error.message)) errors = errors + _errors else: - errors = errors + [error.message for error in a] + errors = errors + [error.message for error in b] else: errors = errors = errors + [_("Specified item type does not exist.")] - item_error = dict(**record) - item_error["errors"] = errors if len(errors) else None - result.append(item_error) + records = dict(**record) + records["errors"] = errors if len(errors) else None + if len(warnings) > 0: + warnings.append( + _("Specified %(type)s is different from existing %(existing_type)s.", + type="type:integer", existing_type="type:string" + ) + ) + records["warnings"] = warnings if len(warnings) else None + result.append(records) return result @@ -1561,7 +1589,17 @@ def handle_check_exist_record(list_record) -> list: item = dict(**item, **{"status": "new"}) # current_app.logger.debug("item:{}".format(item)) errors = item.get("errors") or [] + recid = request.view_args.get("recid") item_id = item.get("id") + if item_id is None and recid is not None: + item["id"] = recid + item_id = recid + system_url = ( + request.host_url + "records/" + str(item_id) + if item_id is not None else None + ) + if item.get("uri") is None and system_url is not None: + item["uri"] = system_url # current_app.logger.debug("item_id:{}".format(item_id)) if item_id and item_id is not "": system_url = request.host_url + "records/" + str(item_id) diff --git a/modules/weko-swordserver/tests/data/records/test_items.json b/modules/weko-swordserver/tests/data/records/test_items.json index c1ea3057a0..6e8e8427d3 100644 --- a/modules/weko-swordserver/tests/data/records/test_items.json +++ b/modules/weko-swordserver/tests/data/records/test_items.json @@ -99,4 +99,35 @@ "resourcetype": "conference paper" } } -] \ No newline at end of file +, + { + "id": "filetest", + "pid": { "type": "depid", "value": "filetest", "revision_id": 0 }, + "lang": "ja", + "owner": "99", + "title": "filetest_title", + "owners": [99], + "status": "published", + "$schema": "/items/jsonschema/15", + "pubdate": "2026-02-09", + "created_by": 99, + "owners_ext": { + "email": "filetest@nii.ac.jp", + "username": "fileuser", + "displayname": "File Test User" + }, + "shared_user_ids": [], + "item_1617186331708": [ + { "subitem_1551255647225": "filetest_title", "subitem_1551255648112": "ja" } + ], + "item_file": [ + { + "url": { + "url": "http://TEST_SERVER.localdomain/files/filetest.pdf", + "label": "filetest.pdf" + }, + "mimetype": "application/pdf" + } + ] + } +] diff --git a/modules/weko-swordserver/tests/data/records/test_records.json b/modules/weko-swordserver/tests/data/records/test_records.json index 7a03568086..715786b861 100644 --- a/modules/weko-swordserver/tests/data/records/test_records.json +++ b/modules/weko-swordserver/tests/data/records/test_records.json @@ -202,5 +202,55 @@ ] }, "relation_version_is_last": true + }, + { + "_oai": { "id": "oai:weko3.example.org:filetest", "sets": ["99"] }, + "path": ["99"], + "owner": "99", + "recid": "filetest", + "title": ["filetest_title"], + "pubdate": { "attribute_name": "PubDate", "attribute_value": "2026-02-09" }, + "_buckets": { "deposit": "filetest-bucket-uuid" }, + "_deposit": { + "id": "filetest", + "pid": { "type": "depid", "value": "filetest", "revision_id": 0 }, + "owner": "99", + "owners": [99], + "status": "published", + "created_by": 99, + "owners_ext": { + "email": "filetest@nii.ac.jp", + "username": "fileuser", + "displayname": "File Test User" + } + }, + "item_title": "filetest_title", + "author_link": [], + "item_type_id": "99", + "publish_date": "2026-02-09", + "publish_status": "0", + "weko_shared_ids": [], + "item_1617186331708": { + "attribute_name": "Title", + "attribute_value_mlt": [ + { + "subitem_1551255647225": "filetest_title", + "subitem_1551255648112": "ja" + } + ] + }, + "item_file": { + "attribute_type": "file", + "attribute_value_mlt": [ + { + "url": { + "url": "http://TEST_SERVER.localdomain/files/filetest.pdf", + "label": "filetest.pdf" + }, + "mimetype": "application/pdf" + } + ] + }, + "relation_version_is_last": true } ] diff --git a/modules/weko-swordserver/tests/test_views.py b/modules/weko-swordserver/tests/test_views.py index cd1333289e..c621e19e73 100644 --- a/modules/weko-swordserver/tests/test_views.py +++ b/modules/weko-swordserver/tests/test_views.py @@ -49,8 +49,26 @@ def update_location_size(): loc = db.session.query(Location).filter( Location.id == 1).one() loc.size = 1547 - mocker.patch("weko_swordserver.views._get_status_document", side_effect=lambda id:{"recid": id}) - mocker.patch("weko_swordserver.views._get_status_workflow_document", side_effect=lambda aid, id:{"activity": aid,"recid": id}) + mocker.patch( + "weko_swordserver.views._get_status_document", + side_effect=lambda id: { + "recid": id, + "links": [ + {"@id": f"/records/{id}", "contentType": "text/html"} + ] + } + ) + mocker.patch( + "weko_swordserver.views._get_status_workflow_document", + side_effect=lambda aid, id: { + "activity": aid, + "recid": id, + "links": [ + {"@id": f"/workflow/activity/detail/{aid}", "contentType": "text/html"}, + {"@id": f"/records/{id}", "contentType": "text/html"} + ] + } + ) mocker.patch("weko_search_ui.utils.find_and_update_location_size", side_effect=update_location_size) mocker.patch("weko_swordserver.views.dbsession_clean") @@ -83,7 +101,13 @@ def update_location_size(): result = client.post(url, data={"file": storage}, content_type="multipart/form-data", headers=headers) assert result.status_code == 201 - assert result.json.get("recid") == "2000001" + # Verify not only recid but also the links array + resp = result.json + assert resp.get("recid") == "2000001" + assert "links" in resp + # The links array should contain the HTML link for the recid + html_links = [l for l in resp["links"] if l.get("@id", "").endswith("/records/2000001") and l.get("contentType") == "text/html"] + assert html_links assert not os.path.exists("/var/tmp/test"), os.rmdir("/var/tmp/test") # Workflow registration, duplicate check @@ -113,7 +137,15 @@ def update_location_size(): result = client.post(url, data={"file": storage}, content_type="multipart/form-data", headers=headers) assert result.status_code == 201 - assert result.json.get("recid") == "2000001" + + resp = result.json + assert resp.get("recid") == "2000001" + assert "links" in resp + # The links array should contain both the activity link and the HTML link for the recid + activity_links = [l for l in resp["links"] if "/workflow/activity/detail/" in l.get("@id", "")] + html_links = [l for l in resp["links"] if l.get("@id", "").endswith("/records/2000001") and l.get("contentType") == "text/html"] + assert activity_links + assert html_links # invalid Content-Disposition's filename @@ -352,6 +384,116 @@ def update_location_size(): assert result.status_code == 412 assert result.json.get("error") == "Failed to verify request body and digest." +# .tox/c1/bin/pytest --cov=weko_swordserver tests/test_views.py::test_post_service_document_multi_recid -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-swordserver/.tox/c1/tmp +def test_post_service_document_multi_recid(app, client, db, users, make_zip, tokens, mocker): + mocker.patch("invenio_pidstore.resolver.Resolver.resolve", return_value=(MagicMock(), MagicMock())) + url = url_for("weko_swordserver.post_service_document") + token_direct = tokens[0]["token"].access_token + login_user_via_session(client=client, email=users[0]["email"]) + app.config["WEKO_SWORDSERVER_DIGEST_VERIFICATION"] = False + headers = { + "Authorization": f"Bearer {token_direct}", + "Content-Disposition": "attachment; filename=payload.zip", + "Packaging": "http://purl.org/net/sword/3.0/package/SimpleZip", + "On-Behalf-Of": "test_on_behalf_of", + } + zip = make_zip() + storage = FileStorage(filename="payload.zip", stream=zip) + mocker.patch("weko_swordserver.views.check_import_file_format", return_value="TSV/CSV") + mocker.patch("weko_swordserver.views.get_shared_ids_from_on_behalf_of", return_value=[]) + mocker_check_item = mocker.patch("weko_swordserver.views.check_import_items") + mocker_check_item.return_value = { + "data_path": "/var/tmp/test", + "register_type": "Direct", + "list_record": [ + {"status": "new", "metadata": {}, "recid": "2000001"}, + {"status": "new", "metadata": {}, "recid": "2000002"} + ] + } + mocker.patch("weko_swordserver.views.import_items_to_system", return_value={"success": True, "recid": ["2000001", "2000002"]}) + mocker.patch("weko_items_ui.utils.send_mail_direct_registered") + os.makedirs("/var/tmp/test", exist_ok=True) + + mocker.patch("weko_swordserver.views._get_status_multi_document", return_value={ + "@context": "https://swordapp.github.io/swordv3/swordv3.jsonld", + "@type": "Status", + "@id": "/records/2000002", + "links": [ + {"@id": "/records/2000001", "contentType": "text/html"}, + {"@id": "/records/2000002", "contentType": "text/html"} + ] + }) + result = client.post(url, data={"file": storage}, content_type="multipart/form-data", headers=headers) + assert result.status_code == 201 + resp = result.json + assert "links" in resp + html_links = [l for l in resp["links"] if l.get("@id", "").endswith("/records/2000001") or l.get("@id", "").endswith("/records/2000002")] + assert len(html_links) == 2 + assert resp["@id"].endswith("/records/2000002") or resp["@id"].endswith("/sword/deposit/2000002") + assert not os.path.exists("/var/tmp/test"), os.rmdir("/var/tmp/test") + + +# .tox/c1/bin/pytest --cov=weko_swordserver tests/test_views.py::test_post_service_document_multi_activity_id -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-swordserver/.tox/c1/tmp +def test_post_service_document_multi_activity_id(app, client, db, users, make_zip, tokens, mocker): + url = url_for("weko_swordserver.post_service_document") + token_workflow = tokens[1]["token"].access_token + login_user_via_session(client=client, email=users[1]["email"]) + app.config["WEKO_SWORDSERVER_DIGEST_VERIFICATION"] = False + zip = make_zip() + storage = FileStorage(filename="payload.zip", stream=zip) + headers = { + "Authorization": f"Bearer {token_workflow}", + "Content-Disposition": "attachment; filename=payload.zip", + "Packaging": "http://purl.org/net/sword/3.0/package/SimpleZip", + "On-Behalf-Of": "test_on_behalf_of", + } + mocker.patch("weko_swordserver.views.check_import_file_format", return_value="TSV/CSV") + mocker.patch("weko_swordserver.views.get_shared_ids_from_on_behalf_of", return_value=[]) + mocker_check_item = mocker.patch("weko_swordserver.views.check_import_items") + mocker_check_item.return_value = { + "data_path": "/var/tmp/test", + "register_type": "Workflow", + "workflow_id": [1001, 1002], + "list_record": [ + {"status": "new", "metadata": {}, "activity_id": "A-TEST-00001"}, + {"status": "new", "metadata": {}, "activity_id": "A-TEST-00002"} + ], + "duplicate_check": True + } + mocker.patch("weko_items_ui.utils.check_duplicate", return_value=(False, [], [])) + def import_items_to_activity_side_effect(*args, **kwargs): + if not hasattr(import_items_to_activity_side_effect, "count"): + import_items_to_activity_side_effect.count = 0 + import_items_to_activity_side_effect.count += 1 + if import_items_to_activity_side_effect.count == 1: + return (url_for("weko_workflow.display_activity", activity_id="A-TEST-00001"), "2000001", "end_action", None) + else: + return (url_for("weko_workflow.display_activity", activity_id="A-TEST-00002"), "2000002", "end_action", None) + mocker.patch("weko_swordserver.views.import_items_to_activity", side_effect=import_items_to_activity_side_effect) + + mocker.patch("weko_swordserver.views._get_status_multi_document", return_value={ + "@context": "https://swordapp.github.io/swordv3/swordv3.jsonld", + "@type": "Status", + "@id": "/records/2000002", + "links": [ + {"@id": "/workflow/activity/detail/A-TEST-00001", "contentType": "text/html"}, + {"@id": "/workflow/activity/detail/A-TEST-00002", "contentType": "text/html"}, + {"@id": "/records/2000001", "contentType": "text/html"}, + {"@id": "/records/2000002", "contentType": "text/html"} + ] + }) + os.makedirs("/var/tmp/test", exist_ok=True) + result = client.post(url, data={"file": storage}, content_type="multipart/form-data", headers=headers) + assert result.status_code == 201 + resp = result.json + assert "links" in resp + activity_links = [l for l in resp["links"] if "/workflow/activity/detail/" in l.get("@id", "")] + assert len(activity_links) == 2 + html_links = [l for l in resp["links"] if l.get("@id", "").endswith("/records/2000001") or l.get("@id", "").endswith("/records/2000002")] + assert len(html_links) == 2 + assert resp["@id"].endswith("/records/2000002") or resp["@id"].endswith("/sword/deposit/2000002") + assert not os.path.exists("/var/tmp/test"), os.rmdir("/var/tmp/test") + # def put_object(recid): # .tox/c1/bin/pytest --cov=weko_swordserver tests/test_views.py::test_put_object -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-swordserver/.tox/c1/tmp @@ -788,14 +930,13 @@ def test_get_status_document(client, users, tokens): assert res.status_code == 200 assert res.json == {"recid":"test_recid"} - # def _get_status_document(recid): # .tox/c1/bin/pytest --cov=weko_swordserver tests/test_views.py::test__get_status_document -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-swordserver/.tox/c1/tmp def test__get_status_document(app,records): recid_doi = records[0][0].pid_value recid_not_doi = records[2][0].pid_value recid_sysdoi = records[3][0].pid_value - + recid_file = records[4][0].pid_value test_doi = { "@context": "https://swordapp.github.io/swordv3/swordv3.jsonld", "@type": "Status", @@ -873,6 +1014,35 @@ def test__get_status_document(app,records): } ] } + test_file = { + "@context": "https://swordapp.github.io/swordv3/swordv3.jsonld", + "@type": "Status", + "@id" : url_for('weko_swordserver.get_status_document', recid=recid_file, _external=True), + "actions" : {"getMetadata" : False,"getFiles" : False,"appendMetadata" : False,"appendFiles" : False,"replaceMetadata" : False,"replaceFiles" : False,"deleteMetadata" : False,"deleteFiles" : False,"deleteObject" : True,}, + "eTag" : str(1), + "fileSet" : {}, + "metadata" : {}, + "service" : url_for('weko_swordserver.get_service_document',_external=False), + "state" : [ + { + "@id" : "http://purl.org/net/sword/3.0/state/ingested", + "description" : "" + } + ], + "links" : [ + { + "@id" : "http://TEST_SERVER.localdomain/records/{}".format(recid_file), + "rel" : ["alternate"], + "contentType" : "text/html" + }, + { + "@id" : "http://TEST_SERVER.localdomain/files/filetest.pdf", + "contentType" : "application/pdf", + "rel" : ["http://purl.org/net/sword/3.0/terms/fileSetFile"], + "derivedFrom" : "http://TEST_SERVER.localdomain/records/{}".format(recid_file) + } + ] + } with app.test_request_context("/test_req"): # exist permalink result = _get_status_document(recid_doi) @@ -886,18 +1056,43 @@ def test__get_status_document(app,records): result = _get_status_document(recid_sysdoi) assert result == test_sysdoi + # exist file information + result = _get_status_document(recid_file) + assert result == test_file + # raise WekoSwordserverException with pytest.raises(WekoSwordserverException) as e: _get_status_document("not_exist_recid") assert e.message == "Item not found. (recid=not_exist_recid)" assert e.errorType == ErrorType.NotFound +# .tox/c1/bin/pytest --cov=weko_swordserver tests/test_views.py::test_status_document_files_info_none -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-swordserver/.tox/c1/tmp +def test_status_document_files_info_none(app, mocker): + """ + Test the branch when files_info is None + """ + from weko_swordserver.views import _get_status_document + recid = "dummy_recid" + with app.test_request_context("/test_req"): + # Mock _get_file_info to return None + mocker.patch("weko_swordserver.views._get_file_info", return_value=None) + # Mock import_string, Resolver, get_record_permalink minimally + mock_record = type("MockRecord", (), {"revision_id": 1, "get": lambda self, k, d=None: None})() + mocker.patch("weko_swordserver.views.import_string", return_value=type("Dummy", (), {"get_record": lambda x: mock_record})()) + mocker.patch("weko_swordserver.views.Resolver", side_effect=lambda **kwargs: type("DummyResolver", (), {"resolve": lambda self, recid: (None, mock_record)})()) + mocker.patch("weko_swordserver.views.get_record_permalink", return_value=None) + result = _get_status_document(recid) + # No file links should be added to links + file_links = [l for l in result["links"] if l.get("rel") and any("file" in r for r in l.get("rel"))] + assert not file_links + # def _get_status_workflow_document(activity, recid): # .tox/c1/bin/pytest --cov=weko_swordserver tests/test_views.py::test__get_status_workflow_document -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-swordserver/.tox/c1/tmp def test__get_status_workflow_document(app, records): recid_doi = records[0][0].pid_value recid_not_doi = records[2][0].pid_value + recid_file = records[4][0].pid_value expected_activity_id = "A-20240301-00001" @@ -921,6 +1116,11 @@ def test__get_status_workflow_document(app, records): "rel" : ["alternate"], "contentType" : "text/html" }, + { + "@id" : url_for('weko_swordserver.get_status_document', recid=recid_doi, _external=True), + "rel" : ["alternate"], + "contentType" : "text/html" + } ] } test_doi_no_recid = { @@ -937,11 +1137,49 @@ def test__get_status_workflow_document(app, records): "description" : "" } ], - "links" : [ + "links": [ { "@id" : url_for('weko_workflow.display_activity', activity_id=expected_activity_id, _external=True), "rel" : ["alternate"], "contentType" : "text/html" + }, + { + "@id": url_for('weko_swordserver.get_status_document', recid=recid_not_doi, _external=True), + "rel": ["alternate"], + "contentType": "text/html" + } + ] + } + test_file = { + "@id": url_for('weko_swordserver.get_status_document', recid=recid_file, _external=True), + "@context": "https://swordapp.github.io/swordv3/swordv3.jsonld", + "@type": "ServiceDocument", + "actions": {"getMetadata": False, "getFiles": False, "appendMetadata": False, "appendFiles": False, "replaceMetadata": False, "replaceFiles": False, "deleteMetadata": False, "deleteFiles": False, "deleteObject": True}, + "fileSet": {}, + "metadata": {}, + "service": url_for('weko_swordserver.get_service_document', _external=False), + "state": [ + { + "@id": "http://purl.org/net/sword/3.0/state/inWorkflow", + "description": "" + } + ], + "links": [ + { + "@id": url_for('weko_workflow.display_activity', activity_id=expected_activity_id, _external=True), + "rel": ["alternate"], + "contentType": "text/html" + }, + { + "@id": url_for('weko_swordserver.get_status_document', recid=recid_file, _external=True), + "rel": ["alternate"], + "contentType": "text/html" + }, + { + "@id": "http://TEST_SERVER.localdomain/files/filetest.pdf", + "contentType": "application/pdf", + "rel": ["http://purl.org/net/sword/3.0/terms/fileSetFile"], + "derivedFrom": url_for('weko_swordserver.get_status_document', recid=recid_file, _external=True) } ] } @@ -951,9 +1189,13 @@ def test__get_status_workflow_document(app, records): result = _get_status_workflow_document(expected_activity_id, recid_doi) assert result == test_doi + # exist file + result = _get_status_workflow_document(expected_activity_id, recid_file) + assert result == test_file + # not exist recid - result = _get_status_workflow_document(expected_activity_id, None) - assert result == test_doi_no_recid + with pytest.raises(WekoSwordserverException): + _get_status_workflow_document(expected_activity_id, None) # raise WekoSwordserverException with pytest.raises(WekoSwordserverException) as e: @@ -961,6 +1203,372 @@ def test__get_status_workflow_document(app, records): assert e.message == "Activity created, but not found." assert e.errorType == ErrorType.NotFound + # not exist activity_id + recid_valid = records[0][0].pid_value + with app.test_request_context("/test_req"): + with pytest.raises(WekoSwordserverException) as e: + _get_status_workflow_document(None, recid_valid) + assert e.value.errorType == ErrorType.NotFound + assert "Activity created, but not found" in e.value.message + +# .tox/c1/bin/pytest --cov=weko_swordserver tests/test_views.py::test_status_workflow_document_files_info_none -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-swordserver/.tox/c1/tmp +def test_status_workflow_document_files_info_none(app, mocker): + from weko_swordserver.views import _get_status_workflow_document + activity_id = "A-20240301-00001" + recid = "dummy_recid" + with app.test_request_context("/test_req"): + # Mock _get_file_info to return None + mocker.patch("weko_swordserver.views._get_file_info", return_value=None) + # Mock import_string, Resolver, get_record_permalink minimally + mock_record = type("MockRecord", (), {"revision_id": 1, "get": lambda self, k, d=None: None})() + mocker.patch("weko_swordserver.views.import_string", return_value=type("Dummy", (), {"get_record": lambda x: mock_record})()) + mocker.patch("weko_swordserver.views.Resolver", side_effect=lambda **kwargs: type("DummyResolver", (), {"resolve": lambda self, recid: (None, mock_record)})()) + mocker.patch("weko_swordserver.views.get_record_permalink", return_value=None) + # Mock url_for minimally + mocker.patch("weko_swordserver.views.url_for", side_effect=lambda endpoint, **kwargs: f"/dummy/{endpoint}/{kwargs.get('recid', '') or kwargs.get('activity_id', '')}") + result = _get_status_workflow_document(activity_id, recid) + # No file links should be added to links + file_links = [l for l in result["links"] if l.get("rel") and any("file" in r for r in l.get("rel"))] + assert not file_links + +import os + +from flask import Flask +from weko_swordserver.views import _get_file_info + +# .tox/c1/bin/pytest --cov=weko_swordserver tests/test_views.py::test__get_file_info -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-swordserver/.tox/c1/tmp +def test__get_file_info(app): + # With file attribute + record = { + "file_attr": { + "attribute_type": "file", + "attribute_value_mlt": [ + { + "url": {"url": "http://example.com/files/test.pdf", "label": "test.pdf"}, + "mimetype": "application/pdf", + "format": None + } + ] + } + } + record_url = "http://example.com/records/1" + with app.app_context(): + current_app = app + current_app.config["WEKO_SWORDSERVER_SWORD_VERSION"] = "http://purl.org/net/sword/3.0" + current_app.config["WEKO_SWORDSERVER_FILE_SET_FILE"] = "/terms/fileSetFile" + result = _get_file_info(record, record_url) + expected = { + "test.pdf": { + "@id": "http://example.com/files/test.pdf", + "contentType": "application/pdf", + "rel": ["http://purl.org/net/sword/3.0/terms/fileSetFile"], + "derivedFrom": record_url + } + } + assert result == expected + + # Without file attribute + record_no_file = { + "title": {"attribute_type": "title", "attribute_value_mlt": ["test title"]} + } + with app.app_context(): + result = _get_file_info(record_no_file, record_url) + assert result == {} + + # When url or label does not exist + record = { + "file": { + "attribute_type": "file", + "attribute_value_mlt": [ + {"url": None, "mimetype": "application/pdf"}, + {"url": {"url": None, "label": None}, "mimetype": "application/pdf"}, + {"url": {"url": "", "label": None}, "mimetype": "application/pdf"}, + {"url": {"url": None, "label": ""}, "mimetype": "application/pdf"}, + {"url": {"url": None, "label": "label1"}, "mimetype": "application/pdf"}, + {"url": {"url": "http://example.com/file1.pdf", "label": None}, "mimetype": "application/pdf"}, + ] + } + } + record_url = "http://example.com/records/1" + files_info = _get_file_info(record, record_url) + # None have both url and label, so should be empty + assert files_info == {} + + +from weko_swordserver.views import _sort_links_for_status + +# .tox/c1/bin/pytest --cov=weko_swordserver tests/test_views.py::test__sort_links_for_status -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-swordserver/.tox/c1/tmp +def test__sort_links_for_status(): + links = [ + { + "@id": "http://example.com/records/2", + "rel": ["alternate"], + "contentType": "text/html" + }, + { + "@id": "http://example.com/files/file1.pdf", + "rel": ["http://purl.org/net/sword/3.0/terms/fileSetFile"], + "derivedFrom": "http://example.com/records/1", + "contentType": "application/pdf" + }, + { + "@id": "http://example.com/workflow/activity/detail/A-20260101-00001", + "rel": ["alternate"], + "contentType": "text/html" + }, + { + "@id": "http://example.com/other", + "rel": ["other"], + "contentType": "text/plain" + }, + { + "@id": "http://example.com/records/1", + "rel": ["alternate"], + "contentType": "text/html" + }, + { + "@id": "http://example.com/files/file2.pdf", + "rel": ["http://purl.org/net/sword/3.0/terms/fileSetFile"], + "derivedFrom": "http://example.com/records/2", + "contentType": "application/pdf" + }, + { + "@id": "http://example.com/workflow/activity/detail/A-20260101-00002", + "rel": ["alternate"], + "contentType": "text/html" + } + ] + sorted_links = _sort_links_for_status(links) + assert sorted_links[0]["@id"] == "http://example.com/workflow/activity/detail/A-20260101-00001" + assert sorted_links[1]["@id"] == "http://example.com/workflow/activity/detail/A-20260101-00002" + assert sorted_links[2]["@id"] == "http://example.com/records/1" + assert sorted_links[3]["@id"] == "http://example.com/records/2" + assert sorted_links[4]["@id"] == "http://example.com/files/file1.pdf" + assert sorted_links[5]["@id"] == "http://example.com/files/file2.pdf" + assert sorted_links[6]["@id"] == "http://example.com/other" + + +# .tox/c1/bin/pytest --cov=weko_swordserver tests/test_views.py::test__get_status_multi_document -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-swordserver/.tox/c1/tmp +def test__get_status_multi_document(app, mocker): + from weko_swordserver.views import _get_status_multi_document + + # Common url_for + def url_for_side_effect(endpoint, **kwargs): + if endpoint == "weko_workflow.display_activity": + return f"/workflow/activity/detail/{kwargs.get('activity_id', '')}" + if endpoint == "weko_swordserver.get_status_document": + return f"/dummy/status/{kwargs.get('recid', '')}" + if endpoint == "weko_swordserver.get_service_document": + return "/dummy/service" + return f"/dummy/{endpoint}/{kwargs.get('recid', '') or kwargs.get('activity_id', '')}" + mocker.patch("weko_swordserver.views.url_for", side_effect=url_for_side_effect) + + # 1. With file, with permalink, reverse reference as int type + class MockRecord1: + revision_id = 1 + def get(self, key, default=None): + if key == "system_identifier_doi": + return None + return default + def items(self): + return [ + ("file_attr", { + "attribute_type": "file", + "attribute_value_mlt": [ + {"url": {"url": "http://TEST_SERVER.localdomain/files/test.pdf", "label": "test.pdf"}, "mimetype": "application/pdf", "format": None} + ] + }) + ] + mocker.patch("weko_swordserver.views.import_string", return_value=type("Dummy", (), {"get_record": lambda recid: MockRecord1()})()) + mocker.patch("weko_swordserver.views.Resolver", side_effect=lambda **kwargs: type("DummyResolver", (), {"resolve": lambda self, recid: (None, MockRecord1())})()) + mocker.patch("weko_swordserver.views.get_record_permalink", return_value="http://example.com/permalink") + mocker.patch("weko_swordserver.views._get_file_info", return_value={ + "test.pdf": { + "@id": "http://TEST_SERVER.localdomain/files/test.pdf", + "contentType": "application/pdf", + "rel": ["http://purl.org/net/sword/3.0//terms/fileSetFile"], + "derivedFrom": "/dummy/records/1" + } + }) + MockRef = type("MockRef", (), {"src_item_pid": "1", "reference_type": "cites"}) + mocker.patch("weko_records.models.ItemReference.get_dst_references", return_value=[MockRef()]) + with app.test_request_context("/test_req"): + result = _get_status_multi_document(["1"], [], register_type="Direct") + import json + expected_links = [ + { + "@id": "http://TEST_SERVER.localdomain/records/1", + "contentType": "text/html", + "log": json.dumps([ + {"type": "cites", "url": "http://TEST_SERVER.localdomain/records/1"} + ]), + "rel": ["alternate"] + }, + { + "@id": "http://TEST_SERVER.localdomain/files/test.pdf", + "contentType": "application/pdf", + "rel": ["http://purl.org/net/sword/3.0//terms/fileSetFile"], + "derivedFrom": "/dummy/records/1" + }, + { + "@id": "http://example.com/permalink", + "contentType": "text/html", + "rel": ["alternate"] + } + ] + assert "links" in result + assert len(result["links"]) == len(expected_links) + assert all(link in expected_links for link in result["links"]) + assert all(link in result["links"] for link in expected_links) + + # Pattern where log contains multiple entries + MockRef2 = type("MockRef2", (), {"src_item_pid": "2", "reference_type": "isReferencedBy"}) + MockRef3 = type("MockRef3", (), {"src_item_pid": "3", "reference_type": "isSupplementedBy"}) + MockRef4 = type("MockRef4", (), {"src_item_pid": "4", "reference_type": "otherType"}) + class MockRecordEmpty: + revision_id = 1 + def get(self, key, default=None): return default + def items(self): return [] + def get_record_multi(recid): + return MockRecordEmpty() + mocker.patch("weko_swordserver.views.import_string", return_value=type("Dummy", (), {"get_record": get_record_multi})()) + mocker.patch("weko_swordserver.views.Resolver", side_effect=lambda **kwargs: type("DummyResolver", (), {"resolve": lambda self, recid: (None, get_record_multi(recid))})()) + mocker.patch("weko_swordserver.views.get_record_permalink", return_value=None) + mocker.patch("weko_swordserver.views._get_file_info", return_value=None) + mocker.patch("weko_records.models.ItemReference.get_dst_references", return_value=[MockRef(), MockRef2(), MockRef3(), MockRef4()]) + with app.test_request_context("/test_req"): + result_multi = _get_status_multi_document(["1", "2", "3"], [], register_type="Direct") + expected_links = [ + { + "@id": "http://TEST_SERVER.localdomain/records/1", + "contentType": "text/html", + "log": json.dumps([ + {"type": "cites", "url": "http://TEST_SERVER.localdomain/records/1"}, + {"type": "isReferencedBy", "url": "http://TEST_SERVER.localdomain/records/2"}, + {"type": "isSupplementedBy", "url": "http://TEST_SERVER.localdomain/records/3"} + ]), + "rel": ["alternate"] + }, + { + "@id": "http://TEST_SERVER.localdomain/records/2", + "contentType": "text/html", + "log": json.dumps([ + {"type": "cites", "url": "http://TEST_SERVER.localdomain/records/1"}, + {"type": "isReferencedBy", "url": "http://TEST_SERVER.localdomain/records/2"}, + {"type": "isSupplementedBy", "url": "http://TEST_SERVER.localdomain/records/3"} + ]), + "rel": ["alternate"] + }, + { + "@id": "http://TEST_SERVER.localdomain/records/3", + "contentType": "text/html", + "log": json.dumps([ + {"type": "cites", "url": "http://TEST_SERVER.localdomain/records/1"}, + {"type": "isReferencedBy", "url": "http://TEST_SERVER.localdomain/records/2"}, + {"type": "isSupplementedBy", "url": "http://TEST_SERVER.localdomain/records/3"} + ]), + "rel": ["alternate"] + } + ] + assert "links" in result_multi + assert len(result_multi["links"]) == len(expected_links) + assert all(link in expected_links for link in result_multi["links"]) + assert all(link in result_multi["links"] for link in expected_links) + + # 3. No file, no permalink, with system_identifier_doi (permalink supplement) + class MockRecord2: + revision_id = 2 + def get(self, key, default=None): + if key == "system_identifier_doi": + return {"attribute_value_mlt": [{"subitem_systemidt_identifier": "http://example.com/doi_subitem"}]} + return default + def __getitem__(self, key): + if key == "system_identifier_doi": + return {"attribute_value_mlt": [{"subitem_systemidt_identifier": "http://example.com/doi_subitem"}]} + raise KeyError(key) + def items(self): return [] + mocker.patch("weko_swordserver.views.import_string", return_value=type("Dummy", (), {"get_record": lambda recid: MockRecord2()})()) + mocker.patch("weko_swordserver.views.Resolver", side_effect=lambda **kwargs: type("DummyResolver", (), {"resolve": lambda self, recid: (None, MockRecord2())})()) + mocker.patch("weko_swordserver.views.get_record_permalink", return_value=None) + mocker.patch("weko_swordserver.views._get_file_info", return_value=None) + mocker.patch("weko_records.models.ItemReference.get_dst_references", return_value=[]) + with app.test_request_context("/test_req"): + result = _get_status_multi_document(["2"], [], register_type="Direct") + expected_links = [ + { + "@id": "http://TEST_SERVER.localdomain/records/2", + "contentType": "text/html", + "rel": ["alternate"] + }, + { + "@id": "http://example.com/doi_subitem", + "contentType": "text/html", + "rel": ["alternate"] + } + ] + assert "links" in result + assert len(result["links"]) == len(expected_links) + assert all(link in expected_links for link in result["links"]) + assert all(link in result["links"] for link in expected_links) + + # 4. Reverse reference as float type (continue branch) + class MockRecord3: + revision_id = 3 + def get(self, key, default=None): return default + def items(self): return [] + MockRefFloat = type("MockRefFloat", (), {"src_item_pid": "10.5", "reference_type": "cites"}) + mocker.patch("weko_swordserver.views.import_string", return_value=type("Dummy", (), {"get_record": lambda recid: MockRecord3()})()) + mocker.patch("weko_swordserver.views.Resolver", side_effect=lambda **kwargs: type("DummyResolver", (), {"resolve": lambda self, recid: (None, MockRecord3())})()) + mocker.patch("weko_swordserver.views.get_record_permalink", return_value=None) + mocker.patch("weko_swordserver.views._get_file_info", return_value=None) + mocker.patch("weko_records.models.ItemReference.get_dst_references", return_value=[MockRefFloat()]) + with app.test_request_context("/test_req"): + result = _get_status_multi_document(["3"], [], register_type="Direct") + expected_links = [ + { + "@id": "http://TEST_SERVER.localdomain/records/3", + "contentType": "text/html", + "rel": ["alternate"] + } + ] + assert "links" in result + assert len(result["links"]) == len(expected_links) + assert all(link in expected_links for link in result["links"]) + assert all(link in result["links"] for link in expected_links) + + # 5. Workflow (with activity_ids) + class MockRecord4: + revision_id = 4 + def get(self, key, default=None): return default + def items(self): return [] + mocker.patch("weko_swordserver.views.import_string", return_value=type("Dummy", (), {"get_record": lambda recid: MockRecord4()})()) + mocker.patch("weko_swordserver.views.Resolver", side_effect=lambda **kwargs: type("DummyResolver", (), {"resolve": lambda self, recid: (None, MockRecord4())})()) + mocker.patch("weko_swordserver.views.get_record_permalink", return_value=None) + mocker.patch("weko_swordserver.views._get_file_info", return_value=None) + mocker.patch("weko_records.models.ItemReference.get_dst_references", return_value=[]) + with app.test_request_context("/test_req"): + result = _get_status_multi_document(["4"], ["A-0001", "A-0002"], register_type="Workflow") + expected_links = [ + { + "@id": "/workflow/activity/detail/A-0001", + "contentType": "text/html", + "rel": ["alternate"] + }, + { + "@id": "/workflow/activity/detail/A-0002", + "contentType": "text/html", + "rel": ["alternate"] + }, + { + "@id": "http://TEST_SERVER.localdomain/records/4", + "contentType": "text/html", + "rel": ["alternate"] + } + ] + assert "links" in result + assert len(result["links"]) == len(expected_links) + assert all(link in expected_links for link in result["links"]) + assert all(link in result["links"] for link in expected_links) # def delete_item(recid): # .tox/c1/bin/pytest --cov=weko_swordserver tests/test_views.py::test_delete_item -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-swordserver/.tox/c1/tmp diff --git a/modules/weko-swordserver/weko_swordserver/config.py b/modules/weko-swordserver/weko_swordserver/config.py index 4efff94f9e..35b1638be7 100644 --- a/modules/weko-swordserver/weko_swordserver/config.py +++ b/modules/weko-swordserver/weko_swordserver/config.py @@ -118,3 +118,6 @@ "Contributor" ] """ Roles that can deposit items with token authentication. """ + +WEKO_SWORDSERVER_FILE_SET_FILE = "/terms/fileSetFile" +""" File path of file set file in SWORD server. """ diff --git a/modules/weko-swordserver/weko_swordserver/utils.py b/modules/weko-swordserver/weko_swordserver/utils.py index eded609e20..7bb2346672 100644 --- a/modules/weko-swordserver/weko_swordserver/utils.py +++ b/modules/weko-swordserver/weko_swordserver/utils.py @@ -570,4 +570,3 @@ def notify_about_item(case, recid, user_id, record=None, shared_ids=[]): user_id, recid, user_id, record["item_title"], shared_ids ) send_mail_item_deleted(recid, record, user_id, shared_ids) - \ No newline at end of file diff --git a/modules/weko-swordserver/weko_swordserver/views.py b/modules/weko-swordserver/weko_swordserver/views.py index e4bce0bbe4..c3f727a9fd 100644 --- a/modules/weko-swordserver/weko_swordserver/views.py +++ b/modules/weko-swordserver/weko_swordserver/views.py @@ -14,6 +14,7 @@ from datetime import datetime, timedelta import sys import traceback +import json from flask import Blueprint, current_app, jsonify, request, url_for, abort, Response from flask_login import current_user @@ -31,6 +32,8 @@ from invenio_oauth2server.decorators import require_oauth_scopes from invenio_oauth2server.ext import verify_oauth_token_and_set_current_user from invenio_oauth2server.provider import oauth2 +from invenio_pidstore.resolver import Resolver +from werkzeug.utils import import_string from weko_accounts.utils import roles_required from weko_admin.api import TempDirInfo @@ -398,12 +401,17 @@ def process_item(item, request_info): warns = [] activity_id = None recid = None + recids = [] + activity_ids = [] action = None # Process and register items for item in check_result["list_record"]: item["root_path"] = os.path.join(data_path, "data") try: activity_id, recid, action, error = process_item(item, request_info) + recids.append(recid) + activity_ids.append(activity_id) + if error: warns.append((activity_id, recid, error)) if file_format == "JSON": @@ -452,11 +460,22 @@ def process_item(item, request_info): .format(request.oauth.client.name, recid) ) if register_type == "Direct": - response = jsonify(_get_status_document(recid)), 201 + if len(recids) > 1: + response = jsonify(_get_status_multi_document(recids, None, register_type)), 201 + else: + recid = recids[0] + response = jsonify(_get_status_document(recid)), 201 else: - response = jsonify( - _get_status_workflow_document(activity_id, recid) - ), 201 if action == "end_action" else 202 + if len(activity_ids) > 1: + response = jsonify( + _get_status_multi_document(recids, activity_ids, register_type) + ), 201 if action == "end_action" else 202 + else: + activity_id = activity_ids[0] + recid = recids[0] + response = jsonify( + _get_status_workflow_document(activity_id, recid) + ), 201 if action == "end_action" else 202 return response @@ -797,8 +816,6 @@ def _get_status_document(recid): """ # Get record - from invenio_pidstore.resolver import Resolver - from werkzeug.utils import import_string record_class = import_string("weko_deposit.api:WekoRecord") try: resolver = Resolver(pid_type="recid", object_type="rec", @@ -819,6 +836,9 @@ def _get_status_document(recid): "attribute_value_mlt"][0][ "subitem_systemidt_identifier"] + # Get file info + files_info = _get_file_info(record, record_uri) + """ Set raw data to StatusDocument @@ -865,6 +885,10 @@ def _get_status_document(recid): }, ] } + if files_info is not None: + for _, file_info in files_info.items(): + raw_data["links"].append(file_info) + if permalink: raw_data["links"].append({ "@id" : permalink, @@ -876,6 +900,138 @@ def _get_status_document(recid): return statusDocument.data +def _get_status_multi_document(recids, activity_ids, register_type="Direct"): + """Generate a Status Document for multiple records. + + Args: + recids (list): List of item identifiers (recid). + activity_ids (list): List of activity identifiers. + register_type (str): Type of registration, either "Direct" or "Workflow". + + Returns: + dict: A Status Document. + """ + from weko_records.models import ItemReference + + record_class = import_string("weko_deposit.api:WekoRecord") + records = [] + for recid in recids: + resolver = Resolver(pid_type="recid", object_type="rec", + getter=record_class.get_record) + pid, record = resolver.resolve(recid) + records.append({recid: record}) + + all_links = [] + last_record = None + last_recid = recids[-1] + for record_val in records: + recid = next(iter(record_val)) + record = record_val[recid] + record_uri = "{}records/{}".format(request.url_root, recid) + + permalink = get_record_permalink(record) + if ( + not permalink + and record.get("system_identifier_doi") + and record.get("system_identifier_doi").get("attribute_value_mlt")[0] + ): + permalink = record["system_identifier_doi"][ + "attribute_value_mlt"][0]["subitem_systemidt_identifier"] + + files_info = _get_file_info(record, record_uri) + + inverse_refs = ItemReference.get_dst_references(recid) + logs = [] + for ref in inverse_refs: + src_pid = ref.src_item_pid + if not float(src_pid).is_integer(): + continue + if str(int(src_pid)) not in [str(int(float(r))) for r in recids]: + continue + src_uri = "{}records/{}".format(request.url_root, src_pid) + ref_type = ref.reference_type + logs.append({"type": ref_type, "url": src_uri}) + + # Add record URI to links + all_links.append({ + "@id": record_uri, + "rel": ["alternate"], + "contentType": "text/html" + }) + + if logs: + all_links[-1]["log"] = json.dumps(logs) + + # Add file links + if files_info is not None: + for _, file_info in files_info.items(): + all_links.append(file_info) + + if permalink: + all_links.append({ + "@id": permalink, + "rel": ["alternate"], + "contentType": "text/html" + }) + + if recid == last_recid: + last_record = record + + if register_type == "Workflow": + for activity_id in activity_ids: + all_links.append({ + "@id": url_for( + "weko_workflow.display_activity", + activity_id=activity_id, + _external=True + ), + "rel": ["alternate"], + "contentType": "text/html" + }) + raw_data = { + "@context": constants.JSON_LD_CONTEXT, + "@type": constants.DocumentType.Status[0], + "@id": url_for( + "weko_swordserver.get_status_document", + recid=last_recid, + _external=True + ), + "actions": { + "getMetadata": False, + "getFiles": False, + "appendMetadata": False, + "appendFiles": False, + "replaceMetadata": False, + "replaceFiles": False, + "deleteMetadata": False, + "deleteFiles": False, + "deleteObject": True, + }, + "fileSet": {}, + "metadata": {}, + "service": url_for("weko_swordserver.get_service_document"), + "links": _sort_links_for_status(all_links) + } + + if register_type == "Workflow": + raw_data["state"] = [ + { + "@id": SwordState.inWorkflow, + "description": "" + } + ] + else: + raw_data["eTag"] = str(last_record.revision_id) + raw_data["state"] = [ + { + "@id": SwordState.ingested, + "description": "" + } + ] + + statusDocument = StatusDocument(raw=raw_data) + return statusDocument.data + def _get_status_workflow_document(activity_id, recid): """ :param recid: Record Identifier. @@ -889,13 +1045,49 @@ def _get_status_workflow_document(activity_id, recid): # "@context" # "@type" """ + record_class = import_string("weko_deposit.api:WekoRecord") + try: + resolver = Resolver(pid_type="recid", object_type="rec", + getter=record_class.get_record) + pid, record = resolver.resolve(recid) + except Exception: + raise WekoSwordserverException("Item not found. (recid={})".format(recid), ErrorType.NotFound) if not activity_id: raise WekoSwordserverException("Activity created, but not found.", ErrorType.NotFound) # Get record uri - record_url = "" - if recid: - record_url = url_for("weko_swordserver.get_status_document", recid=recid, _external=True) + record_url = url_for("weko_swordserver.get_status_document", recid=recid, _external=True) + links_record_url = "{}records/{}".format(request.url_root, recid) + # Get file info + files_info = None + from weko_workflow.models import Activity + activity = Activity.query.filter_by(activity_id=activity_id).first() + if activity and activity.temp_data: + decoded = activity.temp_data.encode().decode('unicode_escape') + temp_data = json.loads(decoded) + files = temp_data.get("files") + files_info = {} + if files: + for file in files: + label = file.get("filename") + host_name = os.environ.get("INVENIO_WEB_HOST_NAME") + url = f"https://{host_name}/record/{recid}/files/{label}" + content_type = file.get("mimetype") + file_rel = ( + current_app.config["WEKO_SWORDSERVER_SWORD_VERSION"] + + current_app.config["WEKO_SWORDSERVER_FILE_SET_FILE"] + ) + if label: + files_info[label] = { + "@id": url, + "contentType": content_type, + "rel": [file_rel], + "derivedFrom": links_record_url + } + if not files_info: + files_info = None + else: + files_info = _get_file_info(record, links_record_url) raw_data = { "@id": record_url, @@ -933,13 +1125,70 @@ def _get_status_workflow_document(activity_id, recid): "rel" : ["alternate"], "contentType" : "text/html" }, + { + "@id" : links_record_url, + "rel" : ["alternate"], + "contentType" : "text/html" + } ] } - + if files_info is not None: + for _, file_info in files_info.items(): + raw_data["links"].append(file_info) statusDocument = StatusDocument(raw=raw_data) return statusDocument.data +def _get_file_info(record, record_url): + files_info = {} + file_rel = ( + current_app.config["WEKO_SWORDSERVER_SWORD_VERSION"] + + current_app.config["WEKO_SWORDSERVER_FILE_SET_FILE"] + ) + for _, attr_val in record.items(): + if isinstance(attr_val, dict) and attr_val.get("attribute_type", None) == "file": + file_mlt = attr_val.get("attribute_value_mlt") + for file in file_mlt: + url_info = file.get("url", None) + url = url_info.get("url") if isinstance(url_info, dict) else None + label = url_info.get("label", None) if isinstance(url_info, dict) else None + content_type = file.get("mimetype") or file.get("format") + if url and label: + files_info[label] = { + "@id": url, + "contentType": content_type, + "rel": [file_rel], + "derivedFrom": record_url + } + return files_info + +def _sort_links_for_status(links): + import re + def link_key(link): + link_id = link.get("@id", "") + rel = link.get("rel", []) + # 1. Workflow activity link + if "/workflow/activity/detail/" in link_id: + group = 0 + m = re.search(r'/workflow/activity/detail/[^-]+-\d+-0*(\d+)', link_id) + order = int(m.group(1)) if m else 0 + # 2. Record HTML link + elif "/records/" in link_id and "alternate" in rel: + group = 1 + m = re.search(r'/records/(\d+)', link_id) + order = int(m.group(1)) if m else 0 + # 3. File link + elif "fileSetFile" in "".join(rel): + group = 2 + derived = link.get("derivedFrom", "") + m = re.search(r'/records/(\d+)', derived) + order = int(m.group(1)) if m else 0 + else: + group = 3 + order = 0 + return (group, order) + return sorted(links, key=link_key) + @blueprint.route("/deposit/", methods=["DELETE"]) @oauth2.require_oauth() @limiter.limit("") diff --git a/nginx/Dockerfile.ams b/nginx/Dockerfile.ams index 7e7e78e016..c514df78e1 100644 --- a/nginx/Dockerfile.ams +++ b/nginx/Dockerfile.ams @@ -37,7 +37,8 @@ RUN cd nginx-1.20.1 && dpkg-buildpackage -b FROM ubuntu:focal AS nginx-stage_1 ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update && apt-get install -y openssl wget curl unzip supervisor shibboleth-sp-common shibboleth-sp-utils php-fpm php-curl +RUN apt-get update && apt-get install -y openssl wget curl unzip supervisor shibboleth-sp-common shibboleth-sp-utils php-fpm php-curl fcgiwrap python3-pip +RUN pip3 install requests WORKDIR /tmp #COPY --from=nginx-with-shib /tmp/nginx_1.18.0-2~focal_amd64.deb . @@ -76,7 +77,8 @@ RUN chmod 400 /etc/nginx/server.key RUN mkdir /usr/share/nginx/html/secure #ADD ./phpinfo.php /usr/share/nginx/html/secure/phpinfo.php #ADD ./index.php /usr/share/nginx/html/secure/index.php -ADD ./login.php /usr/share/nginx/html/secure/login.php +ADD ./login.py /usr/share/nginx/html/secure/login.py +RUN chmod 755 /usr/share/nginx/html/secure/login.py ADD ./shib_clear_headers /etc/nginx/shib_clear_headers ADD ./shib_fastcgi_params /etc/nginx/shib_fastcgi_params @@ -97,4 +99,3 @@ RUN chmod +x /usr/local/startup.sh RUN chmod +x /usr/local/ecosystem.config.js CMD ["/usr/local/startup.sh"] - diff --git a/nginx/ams/weko-frontend/app.config.ts b/nginx/ams/weko-frontend/app.config.ts index e9b36479b3..e38ac33a0b 100644 --- a/nginx/ams/weko-frontend/app.config.ts +++ b/nginx/ams/weko-frontend/app.config.ts @@ -3,6 +3,9 @@ const weko = 'ams-dev.ir.rcos.nii.ac.jp'; export default defineAppConfig({ wekoOrigin: 'https://' + weko, wekoApi: 'https://' + weko + '/api/v1', + amsImage: '/img/ams', + amsPath: '/ams', + amsApi: '/api/ams', export: { jpcoar: 'https://' + weko + '/oai?verb=GetRecord&metadataPrefix=jpcoar_1.0&identifier=oai:ams-dev.ir.rcos.nii.ac.jp:', @@ -24,27 +27,24 @@ export default defineAppConfig({ releaseRange: 'accessMode', // 公開日 releaseDate: 'dateCreated', - // 作成日 + // メタデータ作成日 createDate: 'dateCreated', - // 更新日 + // メタデータ更新日 updateDate: 'reviews', - // タイトル + // データセットの名称 title: 'subjectOf', - // 分野 + // データセットの分野 field: 'genre', - // 作成者氏名 + // データ作成者氏名 authorName: 'creator', - // 作成者所属 + // データ作成者所属 authorAffiliation: 'creativeWorkStatus', - // ヒト/動物/その他 + // 取得データの対象種別 target: 'character', // アクセス権 accessMode: 'accessMode', // キーワード keywords: 'keywords', - // プロジェクトID - // TODO: valueはマッピング整理後に修正予定 - projectId: 'text', // ファイル情報 file: { // 格納場所 @@ -97,6 +97,10 @@ export default defineAppConfig({ } } }, + grdm: { + url: '', + relationType: 'isVersionOf' + }, /** CC license setting */ cc: { /** @@ -147,5 +151,23 @@ export default defineAppConfig({ by_nc_nd_4: 'https://creativecommons.org/licenses/by-nc-nd/4.0/', by_nc_nd_4_ja: 'https://creativecommons.org/licenses/by-nc-nd/4.0/deed.ja' } + }, + /** + * 閲覧権限が必要なアイテム詳細画面からログイン画面へ遷移するまでの時間設定 + */ + transitionTimeMs: 10000, // ミリ秒 + /** + * フロントのShibboleth Login設定 + */ + shibLogin: { + // 本番環境 + dsURL: 'https://ds.gakunin.nii.ac.jp/WAYF', + orthrosURL: 'https://core.orthros.gakunin.nii.ac.jp/idp', + // テスト環境 + // dsURL: 'https://test-ds.gakunin.nii.ac.jp/WAYF', + // orthrosURL: 'https://core-stg.orthros.gakunin.nii.ac.jp/idp', + entityID: 'https://' + weko + '/shibboleth', + handlerURL: 'https://' + weko + '/Shibboleth.sso', + returnURL: 'https://' + weko + '/secure/login.py?next=ams' } }); diff --git a/nginx/ams/weko-frontend/assets/data/amsAlert.json b/nginx/ams/weko-frontend/assets/data/amsAlert.json new file mode 100644 index 0000000000..93ba069761 --- /dev/null +++ b/nginx/ams/weko-frontend/assets/data/amsAlert.json @@ -0,0 +1,556 @@ +{ + "INDEX_MESSAGE_ERROR_AUTH": { + "msgid": "INDEX_E_0001", + "msgstr": "message.error.auth", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "INDEX_MESSAGE_ERROR_SERVER": { + "msgid": "INDEX_E_0002", + "msgstr": "message.error.server", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "INDEX_MESSAGE_ERROR_GET_LATEST_ITEM": { + "msgid": "INDEX_E_0003", + "msgstr": "message.error.getLatestItem", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "INDEX_MESSAGE_ERROR_FETCH": { + "msgid": "INDEX_E_0004", + "msgstr": "message.error.fetch", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "INDEX_MESSAGE_ERROR": { + "msgid": "INDEX_E_0005", + "msgstr": "message.error.error", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "CONTACT_MESSAGE_SENDING_FAILED": { + "msgid": "CONTACT_E_0001", + "msgstr": "message.sendingFailed", + "position": "toast-top pt-20", + "width": "w-auto", + "loglevel": "error" + }, + "DETAIL_ITEM_MESSAGE_ERROR_AUTH": { + "msgid": "DETAIL_E_0001", + "msgstr": "message.error.noItemAuth", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "DETAIL_ITEM_MESSAGE_OAUTH_ERROR": { + "msgid": "DETAIL_E_0002", + "msgstr": "message.error.oauthError", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "DETAIL_ITEM_MESSAGE_ERROR_SERVER": { + "msgid": "DETAIL_E_0003", + "msgstr": "message.error.server", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "DETAIL_ITEM_MESSAGE_ERROR_GET_ITEM_DETAIL": { + "msgid": "DETAIL_E_0004", + "msgstr": "message.error.getItemDetail", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "DETAIL_ITEM_MESSAGE_ERROR_FETCH": { + "msgid": "DETAIL_E_0005", + "msgstr": "message.error.fetch", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "DETAIL_SEARCH_MESSAGE_ERROR_AUTH": { + "msgid": "DETAIL_E_0006", + "msgstr": "message.error.auth", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "DETAIL_SEARCH_MESSAGE_OAUTH_ERROR": { + "msgid": "DETAIL_E_0007", + "msgstr": "message.error.oauthError", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "DETAIL_SEARCH_MESSAGE_ERROR_SERVER": { + "msgid": "DETAIL_E_0008", + "msgstr": "message.error.server", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "DETAIL_SEARCH_MESSAGE_ERROR_GET_INDEX": { + "msgid": "DETAIL_E_0009", + "msgstr": "message.error.getIndex", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "DETAIL_SEARCH_MESSAGE_ERROR_FETCH": { + "msgid": "DETAIL_E_0010", + "msgstr": "message.error.fetch", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "DETAIL_INDEX_MESSAGE_ERROR_AUTH": { + "msgid": "DETAIL_E_0011", + "msgstr": "message.error.auth", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "DETAIL_INDEX_MESSAGE_OAUTH_ERROR": { + "msgid": "DETAIL_E_0012", + "msgstr": "message.error.oauthError", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "DETAIL_INDEX_MESSAGE_ERROR_SERVER": { + "msgid": "DETAIL_E_0013", + "msgstr": "message.error.server", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "DETAIL_INDEX_MESSAGE_ERROR_GET_INDEX": { + "msgid": "DETAIL_E_0014", + "msgstr": "message.error.getIndex", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "DETAIL_INDEX_MESSAGE_ERROR_FETCH": { + "msgid": "DETAIL_E_0015", + "msgstr": "message.error.fetch", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "DETAIL_MESSAGE_SENDING_FAILED": { + "msgid": "DETAIL_E_0016", + "msgstr": "message.sendingFailed", + "position": "toast-top pt-20", + "width": "w-auto", + "loglevel": "error" + }, + "DETAIL_MESSAGE_ERROR": { + "msgid": "DETAIL_E_0017", + "msgstr": "message.error.error", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "FILES_DETAIL_MESSAGE_ERROR_AUTH": { + "msgid": "FILES_E_0001", + "msgstr": "message.error.auth", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "FILES_DETAIL_MESSAGE_ERROR_SERVER": { + "msgid": "FILES_E_0002", + "msgstr": "message.error.server", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "FILES_DETAIL_MESSAGE_ERROR_GET_ITEM_DETAIL": { + "msgid": "FILES_E_0003", + "msgstr": "message.error.getItemDetail", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "FILES_DETAIL_MESSAGE_ERROR_FETCH": { + "msgid": "FILES_E_0004", + "msgstr": "message.error.fetch", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "FILES_ALL_MESSAGE_ERROR_AUTH": { + "msgid": "FILES_E_0005", + "msgstr": "message.error.auth", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "FILES_ALL_MESSAGE_ERROR_SERVER": { + "msgid": "FILES_E_0006", + "msgstr": "message.error.server", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "FILES_ALL_MESSAGE_ERROR_DOWNLOAD_ALL": { + "msgid": "FILES_E_0007", + "msgstr": "message.error.downloadAll", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "FILES_ALL_MESSAGE_ERROR_FETCH": { + "msgid": "FILES_E_0008", + "msgstr": "message.error.fetch", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "FILES_SELECT_MESSAGE_ERROR_AUTH": { + "msgid": "FILES_E_0009", + "msgstr": "message.error.auth", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "FILES_SELECT_MESSAGE_ERROR_SERVER": { + "msgid": "FILES_E_0010", + "msgstr": "message.error.server", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "FILES_SELECT_MESSAGE_ERROR_DOWNLOAD_SELECTED": { + "msgid": "FILES_E_0011", + "msgstr": "message.error.downloadSelected", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "FILES_SELECT_MESSAGE_ERROR_FETCH": { + "msgid": "FILES_E_0012", + "msgstr": "message.error.fetch", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "FILES_MESSAGE_ERROR": { + "msgid": "FILES_E_0013", + "msgstr": "message.error.error", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "LOGIN_MESSAGE_ERROR_SERVER": { + "msgid": "LOGIN_E_0001", + "msgstr": "message.error.server", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "LOGIN_MESSAGE_ERROR_REQUEST": { + "msgid": "LOGIN_E_0002", + "msgstr": "message.error.login", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "LOGIN_MESSAGE_ERROR_FETCH": { + "msgid": "LOGIN_E_0003", + "msgstr": "message.error.fetch", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "LOGIN_MESSAGE_ERROR_LOGIN_BLOCK": { + "msgid": "LOGIN_E_0004", + "msgstr": "message.error.loginFailed", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "LOGIN_MESSAGE_ERROR_NO_USER_INFORMATION": { + "msgid": "LOGIN_E_0005", + "msgstr": "message.error.noUserInformation", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "LOGIN_SHIB_MESSAGE_ERROR_SERVER": { + "msgid": "LOGIN_E_0006", + "msgstr": "message.error.server", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "LOGIN_MESSAGE_ERROR_SHIB_CACHE_PREFIX": { + "msgid": "LOGIN_E_0007", + "msgstr": "message.error.loginFailed", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "LOGIN_MESSAGE_ERROR_SHIB_SESSION_ID": { + "msgid": "LOGIN_E_0008", + "msgstr": "message.error.loginFailed", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "LOGIN_MESSAGE_ERROR_SHIB_ATTRS": { + "msgid": "LOGIN_E_0009", + "msgstr": "message.error.loginFailed", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "LOGIN_MESSAGE_ERROR_BIND_RELATION_INFO": { + "msgid": "LOGIN_E_0010", + "msgstr": "message.error.loginFailed", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "LOGIN_MESSAGE_ERROR_FAILED_GET_RELATION": { + "msgid": "LOGIN_E_0011", + "msgstr": "message.error.loginFailed", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "LOGIN_SHIB_MESSAGE_ERROR_LOGIN_FAILED": { + "msgid": "LOGIN_E_0012", + "msgstr": "message.error.loginFailed", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "ID_SEARCH_MESSAGE_ERROR_AUTH": { + "msgid": "ID_E_0001", + "msgstr": "message.error.auth", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "ID_SEARCH_MESSAGE_ERROR_SERVER": { + "msgid": "ID_E_0002", + "msgstr": "message.error.server", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "ID_SEARCH_MESSAGE_ERROR_REQUEST": { + "msgid": "ID_E_0003", + "msgstr": "message.error.search", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "ID_SEARCH_MESSAGE_ERROR_FETCH": { + "msgid": "ID_E_0004", + "msgstr": "message.error.fetch", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "ID_INDEX_MESSAGE_ERROR_AUTH": { + "msgid": "ID_E_0005", + "msgstr": "message.error.auth", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "ID_INDEX_MESSAGE_ERROR_SERVER": { + "msgid": "ID_E_0006", + "msgstr": "message.error.server", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "ID_INDEX_MESSAGE_ERROR_GET_INDEX": { + "msgid": "ID_E_0007", + "msgstr": "message.error.getIndex", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "ID_INDEX_MESSAGE_ERROR_FETCH": { + "msgid": "ID_E_0008", + "msgstr": "message.error.fetch", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "ID_DOWNLOAD_MESSAGE_ERROR_AUTH": { + "msgid": "ID_E_0009", + "msgstr": "message.error.auth", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "ID_DOWNLOAD_MESSAGE_ERROR_SERVER": { + "msgid": "ID_E_0010", + "msgstr": "message.error.server", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "ID_DOWNLOAD_MESSAGE_ERROR_DOWNLOAD_RESULT": { + "msgid": "ID_E_0011", + "msgstr": "message.error.downloadResult", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "ID_DOWNLOAD_MESSAGE_ERROR_FETCH": { + "msgid": "ID_E_0012", + "msgstr": "message.error.fetch", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "ID_MESSAGE_ERROR": { + "msgid": "ID_E_0013", + "msgstr": "message.error.error", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + + "SEARCH_ITEM_MESSAGE_ERROR_AUTH": { + "msgid": "SEARCH_INDEX_E_0001", + "msgstr": "message.error.auth", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "SEARCH_ITEM_MESSAGE_ERROR_SERVER": { + "msgid": "SEARCH_INDEX_E_0002", + "msgstr": "message.error.server", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "SEARCH_ITEM_MESSAGE_ERROR_REQUEST": { + "msgid": "SEARCH_INDEX_E_0003", + "msgstr": "message.error.search", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "SEARCH_ITEM_MESSAGE_ERROR_FETCH": { + "msgid": "SEARCH_INDEX_E_0004", + "msgstr": "message.error.fetch", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "SEARCH_DOWNLOAD_MESSAGE_ERROR_AUTH": { + "msgid": "SEARCH_INDEX_E_0005", + "msgstr": "message.error.auth", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "SEARCH_DOWNLOAD_MESSAGE_ERROR_SERVER": { + "msgid": "SEARCH_INDEX_E_0006", + "msgstr": "message.error.server", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "SEARCH_DOWNLOAD_MESSAGE_ERROR_RESULT": { + "msgid": "SEARCH_INDEX_E_0007", + "msgstr": "message.error.downloadResult", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "SEARCH_DOWNLOAD_MESSAGE_ERROR_FETCH": { + "msgid": "SEARCH_INDEX_E_0008", + "msgstr": "message.error.fetch", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "SEARCH_INDEX_MESSAGE_ERROR": { + "msgid": "SEARCH_INDEX_E_0009", + "msgstr": "message.error.error", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "DOWNLOAD_RANK_MESSAGE_ERROR_DOWNLOAD": { + "msgid": "DOWNLOAD_RANK_E_0001", + "msgstr": "message.error.download", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "DOWNLOAD_RANK_MESSAGE_ERROR_FETCH": { + "msgid": "DOWNLOAD_RANK_E_0002", + "msgstr": "message.error.fetch", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "TABLE_STYLE_MESSAGE_ERROR_DOWNLOAD": { + "msgid": "TABLE_STYLE_E_0001", + "msgstr": "message.error.download", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "TABLE_STYLE_MESSAGE_ERROR_FETCH": { + "msgid": "TABLE_STYLE_E_0002", + "msgstr": "message.error.fetch", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "TABLE_STYLE_MESSAGE_ERROR_PREVIEW": { + "msgid": "TABLE_STYLE_E_0003", + "msgstr": "message.error.preview", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "TABLE_STYLE_MESSAGE_ERROR_FETCH_PREVIEW": { + "msgid": "TABLE_STYLE_E_0004", + "msgstr": "message.error.fetch", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "VIEWS_NUMBER_MESSAGE_ERROR": { + "msgid": "VIEWS_NUMBER_E_0001", + "msgstr": "message.error.error", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "VIEWS_NUMBER_MESSAGE_ERROR_GET_ITEM": { + "msgid": "VIEWS_NUMBER_E_0002", + "msgstr": "message.error.getItemViewsNumber", + "position": "", + "width": "w-full", + "loglevel": "error" + }, + "VIEWS_NUMBER_MESSAGE_ERROR_FETCH": { + "msgid": "VIEWS_NUMBER_E_0003", + "msgstr": "message.error.fetch", + "position": "", + "width": "w-full", + "loglevel": "error" + } +} diff --git a/nginx/ams/weko-frontend/assets/sass/common.scss b/nginx/ams/weko-frontend/assets/sass/common.scss index 21c110977f..ee53f5d30f 100644 --- a/nginx/ams/weko-frontend/assets/sass/common.scss +++ b/nginx/ams/weko-frontend/assets/sass/common.scss @@ -7,7 +7,7 @@ @include icons; width: 20px; height: 20px; - background-image: url(/img/icon/icon_language.svg); + background-image: url(/img/ams/icon/icon_language.svg); padding-left: 0px; // vertical-align: baseline; margin-bottom: 3px; @@ -24,7 +24,7 @@ @include icons; width: 10px; height: 7px; - background-image: url(/img/icon/icon_down-arrow.svg); + background-image: url(/img/ams/icon/icon_down-arrow.svg); margin-left: 5px; vertical-align: middle; @@ -40,7 +40,7 @@ @include icons; width: 14px; height: 14px; - background-image: url(/img/icon/icon_language02.svg); + background-image: url(/img/ams/icon/icon_language02.svg); padding-right: 10px; vertical-align: baseline; } @@ -59,7 +59,7 @@ @include icons; width: 10px; height: 7px; - background-image: url(/img/icon/icon_down-arrow-bk.svg); + background-image: url(/img/ams/icon/icon_down-arrow-bk.svg); margin-left: 5px; vertical-align: middle; } @@ -68,7 +68,7 @@ @include icons; width: 10px; height: 7px; - background-image: url(/img/icon/icon_up-arrow-bk.svg); + background-image: url(/img/ams/icon/icon_up-arrow-bk.svg); margin-left: 5px; vertical-align: middle; } @@ -77,7 +77,7 @@ @include icons; width: 10px; height: 10px; - background-image: url(/img/icon/icon_left-arrow-bk.svg); + background-image: url(/img/ams/icon/icon_left-arrow-bk.svg); margin-right: 5px; vertical-align: baseline; } @@ -86,7 +86,7 @@ @include icons; width: 10px; height: 10px; - background-image: url(/img/icon/icon_right-arrow-bk.svg); + background-image: url(/img/ams/icon/icon_right-arrow-bk.svg); margin-left: 5px; margin-right: 0; vertical-align: baseline; @@ -96,21 +96,21 @@ @include icons; width: 17px; height: 17px; - background-image: url(/img/icon/icon_login.svg); + background-image: url(/img/ams/icon/icon_login.svg); } &.icon-out::before { @include icons; width: 17px; height: 17px; - background-image: url(/img/icon/icon_logout.svg); + background-image: url(/img/ams/icon/icon_logout.svg); } &.icon-menu::before { @include icons; width: 17px; height: 17px; - background-image: url(/img/icon/icon_menu.svg); + background-image: url(/img/ams/icon/icon_menu.svg); } &.icon-search::before { @@ -118,63 +118,63 @@ width: 16px; height: 16px; vertical-align: baseline; - background-image: url(/img/icon/icon_search.svg); + background-image: url(/img/ams/icon/icon_search.svg); } &.icon-listsearch::before { @include icons; width: 22px; height: 15px; - background-image: url(/img/icon/icon_detailed-search.svg); + background-image: url(/img/ams/icon/icon_detailed-search.svg); } &.icon-mylist::before { @include icons; width: 22px; height: 15px; - background-image: url(/img/icon/icon_mylist.svg); + background-image: url(/img/ams/icon/icon_mylist.svg); } &.icon-mylist-b::before { @include icons; width: 22px; height: 15px; - background-image: url(/img/icon/icon_mylist-blue.svg); + background-image: url(/img/ams/icon/icon_mylist-blue.svg); } &.icon-nord::before { @include icons; width: 14px; height: 16px; - background-image: url(/img/icon/icon_data.svg); + background-image: url(/img/ams/icon/icon_data.svg); } &.icon-norf::before { @include icons; width: 14px; height: 16px; - background-image: url(/img/icon/icon_file.svg); + background-image: url(/img/ams/icon/icon_file.svg); } &.icon-thesis::before { @include icons; width: 14px; height: 16px; - background-image: url(/img/icon/icon_thesis.svg); + background-image: url(/img/ams/icon/icon_thesis.svg); } &.icon-noro::before { @include icons; width: 14px; height: 16px; - background-image: url(/img/icon/icon_organization.svg); + background-image: url(/img/ams/icon/icon_organization.svg); } &.icon-news::before { @include icons; width: 18px; height: 18px; - background-image: url(/img/icon/icon_feed.svg); + background-image: url(/img/ams/icon/icon_feed.svg); } &.icon-info::before { @@ -182,7 +182,7 @@ vertical-align: sup; width: 20px; height: 20px; - background-image: url(/img/icon/icon_info.svg); + background-image: url(/img/ams/icon/icon_info.svg); } &.icon-publish::before { @@ -190,14 +190,14 @@ width: 13px; height: 13px; vertical-align: baseline; - background-image: url(/img/icon/icon_upload-date.svg); + background-image: url(/img/ams/icon/icon_upload-date.svg); } &.icon-update::before { @include icons; width: 13px; height: 13px; - background-image: url(/img/icon/icon_update-date.svg); + background-image: url(/img/ams/icon/icon_update-date.svg); } &.icon-rank::before { @@ -205,7 +205,7 @@ width: 21px; height: 15px; margin-right: 10px; - background-image: url(/img/icon/icon_ranking.svg); + background-image: url(/img/ams/icon/icon_ranking.svg); } &.icon-dl-rank::before { @@ -213,7 +213,7 @@ width: 20px; height: 20px; margin-right: 10px; - background-image: url(/img/icon/icon_dl-rank.svg); + background-image: url(/img/ams/icon/icon_dl-rank.svg); } &.icon-statistics::before { @@ -221,7 +221,7 @@ width: 17px; height: 17px; margin-right: 10px; - background-image: url(/img/icon/icon_statistics.svg); + background-image: url(/img/ams/icon/icon_statistics.svg); } &.icon-export::before { @@ -229,7 +229,7 @@ width: 22px; height: 22px; margin-right: 10px; - background-image: url(/img/icon/icon_export.svg); + background-image: url(/img/ams/icon/icon_export.svg); } &.icon-mail::before { @@ -237,7 +237,7 @@ width: 24px; height: 24px; margin-right: 10px; - background-image: url(/img/icon/icon_mail.svg); + background-image: url(/img/ams/icon/icon_mail.svg); } &.icon-external { @@ -246,7 +246,7 @@ @include icons; width: 10px; height: 10px; - background-image: url(/img/icon/icon_external.svg); + background-image: url(/img/ams/icon/icon_external.svg); } } @@ -255,7 +255,7 @@ width: 27px; height: 21px; margin-right: 10px; - background-image: url(/img/icon/icon_item.svg); + background-image: url(/img/ams/icon/icon_item.svg); } &.icon-index::before { @@ -264,10 +264,10 @@ height: 21px; vertical-align: bottom; margin-right: 10px; - background-image: url(/img/icon/icon_plus01.svg); + background-image: url(/img/ams/icon/icon_plus01.svg); .active & { - background-image: url(/img/icon/icon_minus.svg); + background-image: url(/img/ams/icon/icon_minus.svg); } } @@ -276,7 +276,7 @@ width: 24px; height: 24px; margin-left: 10px; - background-image: url(/img/icon/icon_remove.svg); + background-image: url(/img/ams/icon/icon_remove.svg); } &.icon-minus::before { @@ -284,7 +284,7 @@ width: 19px; height: 21px; margin-right: 10px; - background-image: url(/img/icon/icon_minus.svg); + background-image: url(/img/ams/icon/icon_minus.svg); vertical-align: bottom; } @@ -292,14 +292,14 @@ @include icons; width: 21px; height: 21px; - background-image: url(/img/icon/icon_search-results.svg); + background-image: url(/img/ams/icon/icon_search-results.svg); } &.icon-filter::before { @include icons; width: 22px; height: 15px; - background-image: url(/img/icon/icon_filter.svg); + background-image: url(/img/ams/icon/icon_filter.svg); } &.icon-table::before { @@ -307,7 +307,7 @@ width: 22px; height: 15px; margin-right: 3px; - background-image: url(/img/icon/icon_item-display.svg); + background-image: url(/img/ams/icon/icon_item-display.svg); } &.icon-display { @@ -322,31 +322,31 @@ &.display1 { &::before { - background-image: url(/img/icon/icon_summary_inactive.svg); + background-image: url(/img/ams/icon/icon_summary_inactive.svg); } &.active::before { - background-image: url(/img/icon/icon_summary_active.svg); + background-image: url(/img/ams/icon/icon_summary_active.svg); } } &.display2 { &::before { - background-image: url(/img/icon/icon_table_inactive.svg); + background-image: url(/img/ams/icon/icon_table_inactive.svg); } &.active::before { - background-image: url(/img/icon/icon_table_active.svg); + background-image: url(/img/ams/icon/icon_table_active.svg); } } &.display3 { &::before { - background-image: url(/img/icon/icon_block_inactive.svg); + background-image: url(/img/ams/icon/icon_block_inactive.svg); } &.active::before { - background-image: url(/img/icon/icon_block_active.svg); + background-image: url(/img/ams/icon/icon_block_active.svg); } } } @@ -355,7 +355,7 @@ &::before { @include icon-display; background-size: 26px 14px; - background-image: url(/img/icon/icon_sort01.svg); + background-image: url(/img/ams/icon/icon_sort01.svg); } &.desc::before { @@ -369,7 +369,7 @@ @include icons; width: 13px; height: 13px; - background-image: url(/img/icon/icon_download.svg); + background-image: url(/img/ams/icon/icon_download.svg); } } @@ -378,7 +378,7 @@ margin-right: 10px; width: 14px; height: 16px; - background-image: url(/img/icon/icon_add.svg); + background-image: url(/img/ams/icon/icon_add.svg); } &.icon-date::after { @@ -386,7 +386,7 @@ margin-right: 10px; width: 10px; height: 10px; - background-image: url(/img/icon/icon_calendar.svg); + background-image: url(/img/ams/icon/icon_calendar.svg); } &.icon-checkbox { @@ -435,19 +435,19 @@ } &.icon-group::before { - background-image: url(/img/icon/icon_group.svg); + background-image: url(/img/ams/icon/icon_group.svg); } &.icon-member::before { - background-image: url(/img/icon/icon_member.svg); + background-image: url(/img/ams/icon/icon_member.svg); } &.icon-private::before { - background-image: url(/img/icon/icon_private.svg); + background-image: url(/img/ams/icon/icon_private.svg); } &.icon-public::before { - background-image: url(/img/icon/icon_public.svg); + background-image: url(/img/ams/icon/icon_public.svg); } } @@ -460,25 +460,25 @@ &.icon-preview::before { width: 12px; height: 12px; - background-image: url(/img/icon/icon_preview.svg); + background-image: url(/img/ams/icon/icon_preview.svg); } &.icon-download::before { width: 12px; height: 12px; - background-image: url(/img/icon/icon_download-bk.svg); + background-image: url(/img/ams/icon/icon_download-bk.svg); } &.icon-info-bk::before { width: 12px; height: 12px; - background-image: url(/img/icon/icon_information-bk.svg); + background-image: url(/img/ams/icon/icon_information-bk.svg); } &.icon-approval::before { width: 12px; height: 12px; - background-image: url(/img/icon/icon_approval.svg); + background-image: url(/img/ams/icon/icon_approval.svg); } } @@ -487,7 +487,7 @@ @include icons; width: 14px; height: 7px; - background-image: url(/img/icon/icon_down-arrow.svg); + background-image: url(/img/ams/icon/icon_down-arrow.svg); vertical-align: middle; margin-right: 12px; } @@ -498,7 +498,7 @@ @include icons; width: 14px; height: 7px; - background-image: url(/img/icon/icon_up-arrow.svg); + background-image: url(/img/ams/icon/icon_up-arrow.svg); vertical-align: middle; margin-right: 12px; } @@ -975,7 +975,7 @@ dialog { color: $miby-black; font-weight: 500; cursor: pointer; - list-style-image: url(/img/icon/icon_plus01.svg); + list-style-image: url(/img/ams/icon/icon_plus01.svg); &::before { content: ''; @@ -983,7 +983,7 @@ dialog { left: 0; width: 19px; height: 21px; - background-image: url(/img/icon/icon_plus01.svg); + background-image: url(/img/ams/icon/icon_plus01.svg); background-position: center; background-size: 19px 21px; background-repeat: no-repeat; @@ -994,7 +994,7 @@ dialog { font-weight: 700; &::before { - background-image: url(/img/icon/icon_minus.svg); + background-image: url(/img/ams/icon/icon_minus.svg); } } } @@ -1019,7 +1019,7 @@ dialog { height: 25px; margin-right: 10px; margin-top: 1px; - background-image: url(/img/icon/icon_rhombus.svg); + background-image: url(/img/ams/icon/icon_rhombus.svg); background-position: center; background-size: 19px 21px; background-repeat: no-repeat; @@ -1083,7 +1083,7 @@ dialog { select { &#AddCondition { &:not([size]) { - background-image: url(/img/icon/icon_down-arrow.svg); + background-image: url(/img/ams/icon/icon_down-arrow.svg); background-size: 10px 7px; padding-right: 2rem; } @@ -1097,7 +1097,7 @@ select { margin-right: 10px; width: 10px; height: 10px; - background-image: url(/img/icon/icon_add.svg); + background-image: url(/img/ams/icon/icon_add.svg); } } } @@ -1179,7 +1179,7 @@ input[type='file'] { border-color: $miby-link-blue; background-color: $miby-link-blue; cursor: pointer; - background-image: url(/img/icon/icon_check.svg); + background-image: url(/img/ams/icon/icon_check.svg); background-repeat: no-repeat; background-size: 100%; vertical-align: text-top; diff --git a/nginx/ams/weko-frontend/assets/sass/styles.scss b/nginx/ams/weko-frontend/assets/sass/styles.scss index 0668053f9a..2cbea04804 100644 --- a/nginx/ams/weko-frontend/assets/sass/styles.scss +++ b/nginx/ams/weko-frontend/assets/sass/styles.scss @@ -96,7 +96,7 @@ right: 20px; width: 12px; height: 12px; - background-image: url(/img/icon/icon_close02.svg); + background-image: url(/img/ams/icon/icon_close02.svg); background-position: center; background-size: 12px; background-repeat: no-repeat; @@ -115,7 +115,7 @@ width: 20px; height: 20px; vertical-align: bottom; - background-image: url(/img/icon/icon_q.svg); + background-image: url(/img/ams/icon/icon_q.svg); } } } @@ -133,7 +133,7 @@ width: 20px; height: 20px; vertical-align: bottom; - background-image: url(/img/icon/icon_a.svg); + background-image: url(/img/ams/icon/icon_a.svg); } } @@ -182,3 +182,25 @@ } } } + +.text-divider { + display: flex; + align-items: center; + margin-top: 1rem; + margin-bottom: 1rem; + padding-left: 40px; + padding-right: 40px; +} +.text-divider::before, +.text-divider::after { + content: ''; + height: 1px; + background-color: #a3a3a3; + flex-grow: 1; +} +.text-divider::before { + margin-right: 1rem; +} +.text-divider::after { + margin-left: 1rem; +} diff --git a/nginx/ams/weko-frontend/components/common/Alert.vue b/nginx/ams/weko-frontend/components/common/Alert.vue index f68775dba7..f196eed28d 100644 --- a/nginx/ams/weko-frontend/components/common/Alert.vue +++ b/nginx/ams/weko-frontend/components/common/Alert.vue @@ -92,7 +92,12 @@ stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /> - {{ $t(message) + (code ? ' (' + code + ')' : '') }} + + {{ $t('message.error.oauthError', { time: transitionSecond }) }} + {{ loginPage }} + {{ code ? ' (' + code + ')' : '' }} + + {{ $t(message) + (code ? ' (' + code + ')' : '') }}
@@ -325,6 +325,7 @@ const titleSearchType = reactive({ name: 'exactMatch' } }); +const appConf = useAppConfig(); /* /////////////////////////////////// // function @@ -414,7 +415,7 @@ function search() { emits('clickSearch'); } else { sessionStorage.setItem('conditions', JSON.stringify(conditions)); - navigateTo('/search'); + navigateTo(`${appConf.amsPath ?? ''}/search`); } } @@ -592,6 +593,7 @@ try { $fetch(useAppConfig().wekoApi + '/records', { timeout: useRuntimeConfig().public.apiTimeout, method: 'GET', + credentials: 'omit', headers: { 'Cache-Control': 'no-store', Pragma: 'no-cache', @@ -610,6 +612,7 @@ try { $fetch(useAppConfig().wekoApi + '/records', { timeout: useRuntimeConfig().public.apiTimeout, method: 'GET', + credentials: 'omit', headers: { 'Cache-Control': 'no-store', Pragma: 'no-cache', @@ -643,6 +646,7 @@ try { await $fetch(useAppConfig().wekoApi + '/tree/index', { timeout: useRuntimeConfig().public.apiTimeout, method: 'GET', + credentials: 'omit', headers: { 'Cache-Control': 'no-store', Pragma: 'no-cache', diff --git a/nginx/ams/weko-frontend/components/common/modal/CreaterInfo.vue b/nginx/ams/weko-frontend/components/common/modal/CreaterInfo.vue index 90488fe568..f623380bf7 100644 --- a/nginx/ams/weko-frontend/components/common/modal/CreaterInfo.vue +++ b/nginx/ams/weko-frontend/components/common/modal/CreaterInfo.vue @@ -7,7 +7,7 @@
@@ -55,6 +55,7 @@ defineExpose({ /////////////////////////////////// */ const modalShowFlag = ref(false); +const appConf = useAppConfig(); /* /////////////////////////////////// // function diff --git a/nginx/ams/weko-frontend/components/common/modal/DetailSearch.vue b/nginx/ams/weko-frontend/components/common/modal/DetailSearch.vue index 7f805c4ed4..29756cd2e6 100644 --- a/nginx/ams/weko-frontend/components/common/modal/DetailSearch.vue +++ b/nginx/ams/weko-frontend/components/common/modal/DetailSearch.vue @@ -10,7 +10,7 @@
@@ -196,6 +196,7 @@ const conditions = reactive({ type: '0', keyword: '', detail: {}, detailData: {} const columnList = ref([]); const selected = ref(selectorDefault); let selector = JSON.parse(JSON.stringify(DetailColumn)); +const appConf = useAppConfig(); /* /////////////////////////////////// // function @@ -280,7 +281,7 @@ function search() { closeModal(); } else { sessionStorage.setItem('conditions', JSON.stringify(conditions)); - navigateTo('/search'); + navigateTo(`${appConf.amsPath ?? ''}/search`); closeModal(); } } @@ -358,6 +359,7 @@ try { await $fetch(useAppConfig().wekoApi + '/tree/index', { timeout: useRuntimeConfig().public.apiTimeout, method: 'GET', + credentials: 'omit', headers: { 'Cache-Control': 'no-store', Pragma: 'no-cache', diff --git a/nginx/ams/weko-frontend/components/contact/modal/ContactConfirm.vue b/nginx/ams/weko-frontend/components/contact/modal/ContactConfirm.vue index e41ee1c1fb..96b706df0d 100644 --- a/nginx/ams/weko-frontend/components/contact/modal/ContactConfirm.vue +++ b/nginx/ams/weko-frontend/components/contact/modal/ContactConfirm.vue @@ -10,7 +10,7 @@
@@ -192,7 +192,7 @@ function getCaptcha() { dirtyAnswer.value = false; answeResult.value = false; - $fetch(appConf.wekoApi + '/captcha/image', { + $fetch(`${appConf.amsApi ?? '/api'}/captcha/image`, { timeout: useRuntimeConfig().public.apiTimeout, method: 'GET', headers: { @@ -227,7 +227,7 @@ async function send() { locale.value = String(localStorage.getItem('locale')); const contactType = t(props.type); - await useFetch('/api/mail/send', { + await useFetch(`${appConf.amsApi ?? '/api'}/mail/send`, { method: 'POST', body: JSON.stringify({ key: captchaKey, diff --git a/nginx/ams/weko-frontend/components/detail/DownloadRank.vue b/nginx/ams/weko-frontend/components/detail/DownloadRank.vue index e359364acf..f03228eec2 100644 --- a/nginx/ams/weko-frontend/components/detail/DownloadRank.vue +++ b/nginx/ams/weko-frontend/components/detail/DownloadRank.vue @@ -43,6 +43,7 @@ diff --git a/nginx/ams/weko-frontend/components/detail/ViewsNumber.vue b/nginx/ams/weko-frontend/components/detail/ViewsNumber.vue index 53e7251e78..b7c7ac3dd4 100644 --- a/nginx/ams/weko-frontend/components/detail/ViewsNumber.vue +++ b/nginx/ams/weko-frontend/components/detail/ViewsNumber.vue @@ -30,6 +30,7 @@ diff --git a/nginx/ams/weko-frontend/pages/files.vue b/nginx/ams/weko-frontend/pages/ams/files.vue similarity index 87% rename from nginx/ams/weko-frontend/pages/files.vue rename to nginx/ams/weko-frontend/pages/ams/files.vue index 8178895a53..30106651af 100644 --- a/nginx/ams/weko-frontend/pages/files.vue +++ b/nginx/ams/weko-frontend/pages/ams/files.vue @@ -170,7 +170,7 @@ {{ $t('comment') }} - + - Download + Download {{ $t('download') }} ({{ selectedFiles.length }} {{ $t('items') }}) @@ -209,18 +209,14 @@ - + diff --git a/nginx/ams/weko-frontend/pages/logout.vue b/nginx/ams/weko-frontend/pages/ams/logout.vue similarity index 90% rename from nginx/ams/weko-frontend/pages/logout.vue rename to nginx/ams/weko-frontend/pages/ams/logout.vue index 4f2fbeb864..d97fa92976 100644 --- a/nginx/ams/weko-frontend/pages/logout.vue +++ b/nginx/ams/weko-frontend/pages/ams/logout.vue @@ -2,7 +2,7 @@
- AMS Logo + AMS Logo
- Logout + Logout {{ $t('logout') }}
@@ -49,13 +49,14 @@ /////////////////////////////////// */ const isLogout = ref(false); +const appConf = useAppConfig(); /* /////////////////////////////////// // function /////////////////////////////////// */ function logout() { - $fetch(useAppConfig().wekoApi + '/logout', { + $fetch(appConf.wekoApi + '/logout', { timeout: useRuntimeConfig().public.apiTimeout, method: 'POST', onResponse() { diff --git a/nginx/ams/weko-frontend/pages/preview.vue b/nginx/ams/weko-frontend/pages/ams/preview.vue similarity index 100% rename from nginx/ams/weko-frontend/pages/preview.vue rename to nginx/ams/weko-frontend/pages/ams/preview.vue diff --git a/nginx/ams/weko-frontend/pages/search/[id].vue b/nginx/ams/weko-frontend/pages/ams/search/[id].vue similarity index 76% rename from nginx/ams/weko-frontend/pages/search/[id].vue rename to nginx/ams/weko-frontend/pages/ams/search/[id].vue index d53bfbc6ef..d758860507 100644 --- a/nginx/ams/weko-frontend/pages/search/[id].vue +++ b/nginx/ams/weko-frontend/pages/ams/search/[id].vue @@ -6,16 +6,13 @@ @@ -109,16 +106,14 @@ - +