diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index b07c3177bd..e50b7a716f 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -70,6 +70,7 @@ jobs: - weko-schema-ui - weko-search-ui - weko-sitemap + - weko-swordserver - weko-theme - weko-user-profiles - weko-workflow diff --git a/docker-compose.yml b/docker-compose.yml index bbe4e643ab..f222a69341 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -212,6 +212,7 @@ services: - INVENIO_DB_POOL_CLASS=NullPool - GOOGLE_TRACKING_ID_SYSTEM=UA-86504114-1 - GOOGLE_TRACKING_ID_USER= + - TMPDIR=/var/tmp volumes: - weko3_data:/var/tmp - static_data:/home/invenio/.virtualenvs/invenio/var/instance/static diff --git a/docker-compose2.yml b/docker-compose2.yml index bbe4e643ab..f222a69341 100644 --- a/docker-compose2.yml +++ b/docker-compose2.yml @@ -212,6 +212,7 @@ services: - INVENIO_DB_POOL_CLASS=NullPool - GOOGLE_TRACKING_ID_SYSTEM=UA-86504114-1 - GOOGLE_TRACKING_ID_USER= + - TMPDIR=/var/tmp volumes: - weko3_data:/var/tmp - static_data:/home/invenio/.virtualenvs/invenio/var/instance/static diff --git a/modules/weko-index-tree/weko_index_tree/config.py b/modules/weko-index-tree/weko_index_tree/config.py index f66db4b91a..8b869cc979 100644 --- a/modules/weko-index-tree/weko_index_tree/config.py +++ b/modules/weko-index-tree/weko_index_tree/config.py @@ -92,3 +92,6 @@ WEKO_INDEX_TREE_INDEX_LOCK_KEY_PREFIX = "lock_index_" """Index lock key prefix.""" + +WEKO_INDEX_TREE_PUBLIC_DEFAULT_TIMEZONE = 'Asia/Tokyo' +"""Default time zone for index publication date.""" \ No newline at end of file diff --git a/modules/weko-search-ui/tests/conftest.py b/modules/weko-search-ui/tests/conftest.py index a8b5b2b97d..3d42317fd6 100644 --- a/modules/weko-search-ui/tests/conftest.py +++ b/modules/weko-search-ui/tests/conftest.py @@ -73,7 +73,7 @@ DEPOSIT_REST_ENDPOINTS, ) from invenio_files_rest import InvenioFilesREST -from invenio_files_rest.models import Bucket, Location, ObjectVersion +from invenio_files_rest.models import Bucket, FileInstance, Location, ObjectVersion from invenio_files_rest.permissions import ( bucket_listmultiparts_all, bucket_read_all, @@ -137,9 +137,9 @@ from weko_index_tree.models import Index, IndexStyle from weko_items_ui.config import WEKO_ITEMS_UI_FILE_SISE_PREVIEW_LIMIT, WEKO_ITEMS_UI_MS_MIME_TYPE from weko_records import WekoRecords -from weko_records.api import ItemsMetadata, ItemTypes, Mapping +from weko_records.api import ItemsMetadata, ItemTypes, Mapping, ItemTypeNames from weko_records.config import WEKO_ITEMTYPE_EXCLUDED_KEYS -from weko_records.models import ItemType, ItemTypeMapping, ItemTypeName +from weko_records.models import ItemType, ItemTypeMapping, ItemTypeName, ItemMetadata from weko_records_ui.config import ( EMAIL_DISPLAY_FLG, WEKO_PERMISSION_ROLE_COMMUNITY, @@ -657,7 +657,10 @@ def base_app(instance_path, search_class, request): WEKO_INDEX_TREE_API="/api/tree/index/", WEKO_SEARCH_UI_TO_NUMBER_FORMAT="99999999999999.99", WEKO_SEARCH_UI_BASE_TEMPLATE=WEKO_SEARCH_UI_BASE_TEMPLATE, - WEKO_SEARCH_KEYWORDS_DICT=WEKO_SEARCH_KEYWORDS_DICT + WEKO_SEARCH_KEYWORDS_DICT=WEKO_SEARCH_KEYWORDS_DICT, + WEKO_ITEMS_UI_INDEX_PATH_SPLIT = '///', + WEKO_SEARCH_UI_BULK_EXPORT_RETRY = 5, + WEKO_SEARCH_UI_BULK_EXPORT_LIMIT = 100 ) app_.url_map.converters["pid"] = PIDConverter app_.config["RECORDS_REST_ENDPOINTS"]["recid"]["search_class"] = search_class @@ -1697,6 +1700,23 @@ def __init__(self, **kwargs): # db.session.add(file) # db.session.commit() +@pytest.fixture +def create_file_instance(db): + file_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "data", + "sample_file", + "sample_file.txt", + ) + + file = FileInstance( + id="deadbeef-65bd-4d9b-93e2-ec88cc59aec5", uri=file_path, size=4, updated=None + ) + + db.session.add(file) + db.session.commit() + return file_path + @pytest.yield_fixture() def es(app): @@ -3894,7 +3914,7 @@ def make_itemtype(app,db): def factory(id,datas): result = dict() item_type_name = ItemTypeName( - id=id, name=datas["name"],has_site_license=True,is_active=True + name=datas["name"],has_site_license=True,is_active=True ) item_type_schema=dict() with open(datas["schema"],"r") as f: @@ -3907,10 +3927,12 @@ def factory(id,datas): with open(datas["render"], "r") as f: item_type_render = json.load(f) + with db.session.begin_nested(): + db.session.add(item_type_name) + item_type = ItemType( - id=id, - name_id=id, + name_id=item_type_name.id, harvesting_type=True, schema=item_type_schema, form=item_type_form, @@ -3928,7 +3950,6 @@ def factory(id,datas): db.session.add(item_type_mapping) result["item_type_mapping"] = item_type_mapping with db.session.begin_nested(): - db.session.add(item_type_name) db.session.add(item_type) db.session.commit() @@ -3936,4 +3957,21 @@ def factory(id,datas): result["item_type"] = item_type return result - return factory \ No newline at end of file + return factory + + +@pytest.fixture() +def create_export_all_data(db): + indexer = WekoIndexer() + indexer.get_es_index() + filepath = "tests/data/helloworld.pdf" + filename = "helloworld.pdf" + mimetype = "application/pdf" + uuid_list = db.session.query(PersistentIdentifier.object_uuid).distinct(PersistentIdentifier.object_uuid).all() + uuid_list = [uuid[0] for uuid in uuid_list] + item_meta_data_list = ItemMetadata.query.filter(ItemMetadata.id.in_(uuid_list)).all() + for meta in item_meta_data_list: + meta.item_type_id = 1 + db.session.merge(meta) + for i in range(1000, 1110): + make_record(db, indexer, i, filepath, filename, mimetype, '') diff --git a/modules/weko-search-ui/tests/test_admin.py b/modules/weko-search-ui/tests/test_admin.py index 9fdab6afe3..eab5b7bcc9 100644 --- a/modules/weko-search-ui/tests/test_admin.py +++ b/modules/weko-search-ui/tests/test_admin.py @@ -276,10 +276,95 @@ def test_ItemBulkExport_index(i18n_app, users, client_request_args, db_records2, # def export_all(self): ~ GETS STUCK -# def test_ItemBulkExport_export_all(i18n_app, users, client_request_args, db_records2): -# with patch("flask_login.utils._get_user", return_value=users[3]['obj']): -# test = ItemBulkExport() -# assert test.export_all() +# .tox/c1/bin/pytest --cov=weko_search_ui tests/test_admin.py::test_ItemBulkExport_export_all -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-search-ui/.tox/c1/tmp +def test_ItemBulkExport_export_all(users, client, redis_connect, mocker): + url = url_for('items/bulk-export.export_all') + start_time_str = '2024/05/01 12:55:36' + with patch("flask_login.utils._get_user", return_value=users[3]['obj']): + mocker.patch("weko_search_ui.admin.check_celery_is_run",return_value=True) + mocker.patch("weko_search_ui.admin.check_session_lifetime",return_value=True) + with patch("weko_search_ui.admin.get_export_status", + return_value=(True, '', '', '', 'STARTED', start_time_str, '')): + res = client.post(url) + assert json.loads(res.data) == {'data': { + 'celery_is_run': True, + 'is_lifetime': True, + 'error_message': '', + 'export_run_msg': '', + 'export_status': True, + 'finish_time': '', + 'start_time': start_time_str, + 'status': 'STARTED', + 'uri_status': False + }} + + task = MagicMock() + task.task_id = 1 + mocker.patch('weko_search_ui.tasks.export_all_task.apply_async', + return_value=task) + file_json = { + 'start_time': start_time_str, + 'finish_time': '', + 'export_path': '', + 'cancel_flg': False, + 'write_file_status': { + '1': 'started' + } + } + cache_key = current_app.config["WEKO_ADMIN_CACHE_PREFIX"].format( + name='KEY_EXPORT_ALL', + user_id=current_user.get_id() + ) + cache_uri_key = current_app.config["WEKO_ADMIN_CACHE_PREFIX"].format( + name='URI_EXPORT_ALL', + user_id=current_user.get_id() + ) + cache_msg_key = current_app.config["WEKO_ADMIN_CACHE_PREFIX"].format( + name='MSG_EXPORT_ALL', + user_id=current_user.get_id() + ) + run_msg_key = current_app.config["WEKO_ADMIN_CACHE_PREFIX"].format( + name='RUN_MSG_EXPORT_ALL', + user_id=current_user.get_id() + ) + file_cache_key = current_app.config["WEKO_ADMIN_CACHE_PREFIX"].format( + name='RUN_MSG_EXPORT_ALL_FILE_CREATE', + user_id=current_user.get_id() + ) + datastore = redis_connect + datastore.delete(cache_key) + datastore.put(cache_uri_key, 'test_uri'.encode('utf-8'), ttl_secs=30) + datastore.put(cache_msg_key, ''.encode('utf-8'), ttl_secs=30) + datastore.put(run_msg_key, ''.encode('utf-8'), ttl_secs=30) + datastore.put(file_cache_key, json.dumps(file_json).encode('utf-8'), ttl_secs=30) + mocker.patch("weko_search_ui.utils.AsyncResult",side_effect=MockAsyncResult) + res = client.post(url) + assert json.loads(res.data) == {'data': { + 'celery_is_run': True, + 'is_lifetime': True, + 'error_message': '', + 'export_run_msg': '', + 'export_status': True, + 'finish_time': '', + 'start_time': start_time_str, + 'status': 'STARTED', + 'uri_status': True + }} + + with patch("weko_search_ui.admin.get_export_status", + return_value=(False, '', '', '', 'STARTED', start_time_str, '')): + res = client.post(url) + assert json.loads(res.data) == {'data': { + 'celery_is_run': True, + 'is_lifetime': True, + 'error_message': '', + 'export_run_msg': '', + 'export_status': False, + 'finish_time': '', + 'start_time': start_time_str, + 'status': 'STARTED', + 'uri_status': False + }} # def check_export_status(self): ~ GETS STUCK # def test_ItemBulkExport_check_export_status(i18n_app, users, client_request_args, db_records2): @@ -312,35 +397,84 @@ class TestItemBulkExport: # .tox/c1/bin/pytest --cov=weko_search_ui tests/test_admin.py::TestItemBulkExport::test_check_export_status -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-search-ui/.tox/c1/tmp def test_check_export_status(self,app,client,users, redis_connect,mocker): - mocker.patch("weko_search_ui.utils.AsyncResult",side_effect=MockAsyncResult) mocker.patch("weko_search_ui.admin.check_celery_is_run",return_value=True) - with patch("flask_login.utils._get_user", return_value=users[3]["obj"]): - cache_key = app.config["WEKO_ADMIN_CACHE_PREFIX"].format( - name="KEY_EXPORT_ALL", user_id=current_user.get_id() - ) - datastore = redis_connect - datastore.put(cache_key, "SUCCESS_task".encode("utf-8"), ttl_secs=30) - + mocker.patch("weko_search_ui.admin.check_session_lifetime",return_value=True) + start_time_str = '2024/05/01 12:55:36' + with patch("flask_login.utils._get_user", return_value=users[3]["obj"]): url = url_for("items/bulk-export.check_export_status") - - res = client.get(url) - assert json.loads(res.data) == {'data': { - 'celery_is_run': True, - 'error_message': None, - 'export_run_msg': None, - 'export_status': False, - 'status': 'SUCCESS', - 'uri_status': False}} + with patch('weko_search_ui.admin.get_export_status', + return_value=(True, '', '', '', 'STARTED', start_time_str, '')): + res = client.get(url) + assert json.loads(res.data) == {'data': { + 'celery_is_run': True, + 'is_lifetime': True, + 'error_message': '', + 'export_run_msg': '', + 'export_status': True, + 'finish_time': '', + 'start_time': start_time_str, + 'status': 'STARTED', + 'uri_status': False + }} + + with patch('weko_search_ui.admin.get_export_status', + return_value=(True, 'test_uri', '', '', 'STARTED', start_time_str, '')): + res = client.get(url) + assert json.loads(res.data) == {'data': { + 'celery_is_run': True, + 'is_lifetime': True, + 'error_message': '', + 'export_run_msg': '', + 'export_status': True, + 'finish_time': '', + 'start_time': start_time_str, + 'status': 'STARTED', + 'uri_status': True + }} # .tox/c1/bin/pytest --cov=weko_search_ui tests/test_admin.py::TestItemBulkExport::test_cancel_export -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-search-ui/.tox/c1/tmp def test_cancel_export(self, app, client, users, redis_connect, mocker): url = url_for("items/bulk-export.cancel_export") with patch("flask_login.utils._get_user", return_value=users[3]["obj"]): mocker.patch("weko_search_ui.admin.cancel_export_all",return_value=True) - mocker.patch("weko_search_ui.admin.get_export_status",return_value=(False,"","","","REVOKED",)) + mocker.patch("weko_search_ui.admin.get_export_status",return_value=(False,"","","","REVOKED","","",)) res = client.get(url) assert json.loads(res.data) == {"data":{"cancel_status":True,"export_status":False,"status":"REVOKED"}} +# .tox/c1/bin/pytest --cov=weko_search_ui tests/test_admin.py::TestItemBulkExport::test_download -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-search-ui/.tox/c1/tmp + def test_download(self, client, users, mocker, create_file_instance): + url = url_for('items/bulk-export.download') + with patch('flask_login.utils._get_user', return_value=users[3]['obj']): + file_path = create_file_instance + # export_status is False, download_uri is not None + with patch('weko_search_ui.admin.get_export_status', + return_value=(False, file_path, '', '', '', '', '')): + res = client.get(url) + assert res.headers['Content-Disposition'].split('; ')[1].replace('filename=', '') == 'export-all.zip' + assert res.headers['Content-Type'] == 'application/octet-stream' + assert res.status_code == 200 + # export_status is False, download_uri is None + with patch('weko_search_ui.admin.get_export_status', + return_value=(False, None, '', '', '', '', '')): + res = client.get(url) + assert 'Content-Disposition' not in res.headers + assert res.headers['Content-Type'] != 'application/octet-stream' + assert res.status_code == 200 + # export_status is True, download_uri is not None + with patch('weko_search_ui.admin.get_export_status', + return_value=(True, file_path, '', '', '', '', '')): + res = client.get(url) + assert 'Content-Disposition' not in res.headers + assert res.headers['Content-Type'] != 'application/octet-stream' + assert res.status_code == 200 + # export_stauts is True, download_uri is None + with patch('weko_search_ui.admin.get_export_status', + return_value=(True, None, '', '', '', '', '')): + res = client.get(url) + assert 'Content-Disposition' not in res.headers + assert res.headers['Content-Type'] != 'application/octet-strean' + assert res.status_code == 200 + def compare_csv(data1, data2): def _str2csv(data): f = io.StringIO() diff --git a/modules/weko-search-ui/tests/test_tasks.py b/modules/weko-search-ui/tests/test_tasks.py index e8db271c53..758edab074 100644 --- a/modules/weko-search-ui/tests/test_tasks.py +++ b/modules/weko-search-ui/tests/test_tasks.py @@ -2,6 +2,7 @@ import os import json import pytest +import unittest from flask import current_app, make_response, request from mock import patch, MagicMock, Mock from flask_login import current_user @@ -13,10 +14,12 @@ import_item, remove_temp_dir_task, export_all_task, + write_files_task, delete_exported_task, is_import_running, check_celery_is_run, - delete_task_id_cache + delete_task_id_cache, + check_session_lifetime ) # def check_import_items_task(file_path, is_change_identifier: bool, host_url, lang="en"): @@ -87,17 +90,121 @@ def state(self): assert redis_connect.redis.exists(cache_key) == False -# def export_all_task(root_url, user_id, data): +# def export_all_task(root_url, user_id, data, start_time): +# .tox/c1/bin/pytest --cov=weko_search_ui tests/test_tasks.py::test_export_all_task -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-search-ui/.tox/c1/tmp def test_export_all_task(i18n_app, users): - with patch("weko_search_ui.utils.export_all", return_value="/"): - with patch("weko_admin.utils.reset_redis_cache", return_value=""): - with patch("weko_search_ui.tasks.delete_exported_task", return_value=""): - # Doesn't return a value - assert not export_all_task( - root_url="/", - user_id=users[3]['obj'].id, - data={} - ) + try: + with patch("weko_search_ui.tasks.export_all"): + export_all_task('/', users[3]['obj'].id, {}, '2024/05/02 13:24:51') + except: + assert False + +# def write_files_task(export_path, pickle_file_name, user_id) +# .tox/c1/bin/pytest --cov=weko_search_ui tests/test_tasks.py::test_write_files_task -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-search-ui/.tox/c1/tmp +def test_write_files_task(redis_connect, users, mocker): + class MockOpen: + def __init__(self, path, encoding=None): + self.path = path + self.encoding = encoding + def __enter__(self): + return self + def __exit__(self, exc_type, exc_value, traceback): + pass + + start_time_str = '2024/05/12 04:33:51' + def create_file_json(cancel_flg): + return { + 'start_time': start_time_str, + 'finish_time': '', + 'export_path': '', + 'cancel_flg': cancel_flg, + 'write_file_status': { + '1': 'started' + } + } + + def create_pickle_data(name): + return { + 'item_type_id': '', + 'name': name, + 'root_url': '', + 'jsonschema': 'items/jsonschema/', + 'keys': [], + 'labels': [], + 'recids': [], + 'data': {} + } + + def mock_open(path, encoding=None): + return MockOpen(path, encoding) + + with patch('flask_login.utils._get_user', return_value=users[3]['obj']): + mocker.patch('builtins.open', side_effect=mock_open) + mocker.patch('weko_search_ui.tasks.os.remove') + msg_key = current_app.config['WEKO_ADMIN_CACHE_PREFIX'].format( + name='MSG_EXPORT_ALL', + user_id=current_user.get_id() + ) + file_cache_key = current_app.config["WEKO_ADMIN_CACHE_PREFIX"].format( + name='RUN_MSG_EXPORT_ALL_FILE_CREATE', + user_id=current_user.get_id() + ) + datastore = redis_connect + # cancel_flg is False, result of write_files is True, export_file's name includes 'part' + file_json = create_file_json(False) + file_json['write_file_status']['2'] = 'waiting' + datastore.put(file_cache_key, json.dumps(file_json).encode('utf-8'), ttl_secs=30) + with patch('weko_search_ui.tasks.write_files', return_value=True), \ + patch('weko_search_ui.tasks.pickle.load', return_value=create_pickle_data('test.part1')): + write_files_task('export_path', 'test_path/test_file.pickle', current_user.get_id()) + result_data = datastore.get(file_cache_key).decode('utf-8') + assert json.loads(result_data) == { + 'start_time': start_time_str, + 'finish_time': '', + 'export_path': '', + 'cancel_flg': False, + 'write_file_status': { + '.1': 'finished', + '1': 'started', + '2': 'waiting' + } + } + + # cancel_flg is False, result of write_files is False, export_file's name doesn't include 'part' + datastore.delete(file_cache_key) + datastore.put(file_cache_key, json.dumps(create_file_json(False)).encode('utf-8'), ttl_secs=30) + with patch('weko_search_ui.tasks.write_files', return_value=False), \ + patch('weko_search_ui.tasks.pickle.load', return_value=create_pickle_data('test')): + write_files_task('export_path', 'test_path/test_file.pickle', current_user.get_id()) + result_data = datastore.get(file_cache_key).decode('utf-8') + assert json.loads(result_data) == { + 'start_time': start_time_str, + 'finish_time': '', + 'export_path': '', + 'cancel_flg': True, + 'write_file_status': { + '.1': 'error', + '1': 'started', + } + } + assert datastore.get(msg_key).decode('utf-8') == 'Export failed.' + + # cancel_flg is True, export_file's name doesn't include 'part' + datastore.delete(file_cache_key) + datastore.put(file_cache_key, json.dumps(create_file_json(True)).encode('utf-8'), ttl_secs=30) + with patch('weko_search_ui.tasks.pickle.load', return_value=create_pickle_data('test')): + write_files_task('export_path', 'test_path/test_file.pickle', current_user.get_id()) + result_data = datastore.get(file_cache_key).decode('utf-8') + assert json.loads(result_data) == { + 'start_time': start_time_str, + 'finish_time': '', + 'export_path': '', + 'cancel_flg': True, + 'write_file_status': { + '.1': 'canceled', + '1': 'started' + } + } # def delete_exported_task(uri, cache_key): @@ -105,8 +212,15 @@ def test_export_all_task(i18n_app, users): def test_delete_exported_task(i18n_app, db, users, file_instance_mock, redis_connect): from invenio_files_rest.models import FileInstance, Location # uri = file_instance_mock + def clear_test_data(): + Location.query.delete() + db.session.commit() + + clear_test_data() + cache_key = "test_cache_key" task_key = "test_task_key" + export_path = "test_export_path" file_uri = "test_location%test.txt" datastore = redis_connect @@ -117,22 +231,35 @@ def test_delete_exported_task(i18n_app, db, users, file_instance_mock, redis_con db.session.add(location) db.session.commit() with patch("flask_login.utils._get_user", return_value=users[3]['obj']): - # exist cache_key - file_instance = FileInstance(uri=file_uri) - db.session.add(file_instance) - db.session.commit() - delete_exported_task(file_uri,cache_key,task_key) - assert redis_connect.redis.exists(task_key) == False - assert redis_connect.redis.exists(cache_key) == False - - # not exist cache_key - file_instance = FileInstance(uri=file_uri) - db.session.add(file_instance) - db.session.commit() - delete_exported_task(file_uri,cache_key,task_key) - assert redis_connect.redis.exists(task_key) == False - assert redis_connect.redis.exists(cache_key) == False - + with patch("weko_search_ui.utils.delete_exported", return_value=True): + with patch("shutil.rmtree") as mock_rmtree: # shutil.rmtreeをモック + # exist cache_key + file_instance = FileInstance(uri=file_uri) + db.session.add(file_instance) + db.session.commit() + delete_exported_task(file_uri, cache_key, task_key, export_path) + assert redis_connect.redis.exists(task_key) == False + assert redis_connect.redis.exists(cache_key) == False + mock_rmtree.assert_called_once_with(export_path) # rmtreeが呼ばれたことを確認 + + # not exist cache_key + file_instance = FileInstance(uri=file_uri) + db.session.add(file_instance) + db.session.commit() + delete_exported_task(file_uri, cache_key, task_key, export_path) + assert redis_connect.redis.exists(task_key) == False + assert redis_connect.redis.exists(cache_key) == False + + # raise Exception + with patch("shutil.rmtree") as mock_rmtree: + with patch("weko_search_ui.utils.delete_exported", side_effect=Exception("Test exception")): + with patch("weko_search_ui.utils.FileInstance.get_by_uri", side_effect=Exception("Test exception")): + file_instance = FileInstance(uri=file_uri) + db.session.add(file_instance) + db.session.commit() + delete_exported_task(file_uri, cache_key, task_key, export_path) + assert redis_connect.redis.exists(task_key) == False + assert redis_connect.redis.exists(cache_key) == False # def is_import_running(): @@ -144,8 +271,6 @@ def test_is_import_running(i18n_app): assert is_import_running()==None - - # def check_celery_is_run(): # .tox/c1/bin/pytest --cov=weko_search_ui tests/test_tasks.py::test_check_celery_is_run -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-search-ui/.tox/c1/tmp def test_check_celery_is_run(i18n_app): @@ -154,3 +279,18 @@ def test_check_celery_is_run(i18n_app): with patch("celery.task.control.inspect.ping",return_value={}): assert check_celery_is_run()==False + + +class TestCheckSessionLifetime(unittest.TestCase): + @patch('weko_search_ui.tasks.get_lifetime') + def test_check_session_lifetime(self, mock_get_lifetime): + """Test check_session_lifetime function with mocked get_lifetime.""" + # Test when session lifetime is greater than or equal to one day + mock_get_lifetime.return_value = 86400 + self.assertTrue(check_session_lifetime()) + mock_get_lifetime.return_value = 86401 + self.assertTrue(check_session_lifetime()) + + # Test when session lifetime is less than one day + mock_get_lifetime.return_value = 86399 + self.assertFalse(check_session_lifetime()) \ No newline at end of file diff --git a/modules/weko-search-ui/tests/test_utils.py b/modules/weko-search-ui/tests/test_utils.py index e8ce42f03d..7be5e66934 100644 --- a/modules/weko-search-ui/tests/test_utils.py +++ b/modules/weko-search-ui/tests/test_utils.py @@ -6,19 +6,26 @@ import unittest from datetime import datetime import uuid +import weko_search_ui import pytest from flask import current_app, make_response, request from flask_babelex import Babel from flask_login import current_user +from sqlalchemy import func as _func +from sqlalchemy.exc import SQLAlchemyError +from invenio_files_rest.models import FileInstance from invenio_i18n.babel import set_locale from invenio_records.api import Record -from mock import MagicMock, Mock, patch -from invenio_pidstore.models import PersistentIdentifier, PIDStatus +from invenio_records.models import RecordMetadata +from mock import MagicMock, Mock, patch, mock_open +from invenio_pidstore.models import PersistentIdentifier, PIDStatus, Redirect +from invenio_db import db as iv_db from invenio_pidrelations.models import PIDRelation from weko_admin.config import WEKO_ADMIN_MANAGEMENT_OPTIONS -from weko_deposit.api import WekoDeposit, WekoIndexer +from weko_deposit.api import WekoDeposit, WekoIndexer, WekoRecord as d_wekorecord from weko_records.api import ItemsMetadata, WekoRecord +from weko_records.models import ItemMetadata from weko_search_ui import WekoSearchUI from weko_search_ui.config import ( @@ -46,6 +53,7 @@ delete_exported, delete_records, export_all, + get_retry_info, get_change_identifier_mode_content, get_content_workflow, get_current_language, @@ -117,6 +125,7 @@ update_publish_status, validation_date_property, validation_file_open_date, + write_files, combine_aggs ) @@ -174,6 +183,14 @@ def __init__(self): def can(self): return True + +def clear_test_data(): + Redirect.query.delete() + iv_db.session.commit() + + PersistentIdentifier.query.delete() + iv_db.session.commit() + # def get_tree_items(index_tree_id): ERROR ~ AttributeError: '_AppCtxGlobals' object has no attribute 'identity' # .tox/c1/bin/pytest --cov=weko_search_ui tests/test_utils.py::test_get_tree_items -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-search-ui/.tox/c1/tmp def test_get_tree_items(i18n_app, indices, users, mocker): @@ -2278,30 +2295,202 @@ def test_handle_check_duplication_item_id(i18n_app): # def export_all(root_url, user_id, data): *** not yet done -def test_export_all(db_activity, i18n_app, users, item_type, db_records2): - root_url = "/" - user_id = users[3]["obj"].id - data = {"item_type_id": "1", "item_id_range": "1"} - data2 = {"item_type_id": "-1", "item_id_range": "1-9"} - data3 = {"item_type_id": -1, "item_id_range": "1"} +# .tox/c1/bin/pytest --cov=weko_search_ui tests/test_utils.py::test_export_all -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-search-ui/.tox/c1/tmp +def test_export_all(db_activity, i18n_app, users, item_type, db_records2, redis_connect, db, create_export_all_data, mocker): + clear_test_data() + + # Delete existing data to avoid IntegrityError + db.session.query(PersistentIdentifier).delete() + db.session.commit() + + with patch("flask_login.utils._get_user", return_value=users[3]['obj']): + with patch("weko_search_ui.utils.os.getenv", return_value="/tmp/bulk_export"): + with patch("weko_search_ui.utils.os.makedirs") as mock_makedirs: + mock_makedirs.return_value = None # モックの戻り値を設定 + root_url = "/" + user_id = users[3]["obj"].id + data = {"item_type_id": "1", "item_id_range": "1"} + data2 = {"item_type_id": "-1", "item_id_range": "1-9"} + start_time_str = '2024/05/21 23:44:12' + msg_key = i18n_app.config["WEKO_ADMIN_CACHE_PREFIX"].format( + name="MSG_EXPORT_ALL", user_id=current_user.get_id() + ) + uri_key = i18n_app.config["WEKO_ADMIN_CACHE_PREFIX"].format( + name="URI_EXPORT_ALL", user_id=current_user.get_id() + ) + datastore = redis_connect + + task = MagicMock() + task.task_id = 1 + mocker.patch("weko_search_ui.tasks.write_files_task", return_value=task) + mocker.patch('builtins.open', side_effect=unittest.mock.mock_open()) + mocker.patch("weko_search_ui.utils.pickle.dump") + + # datastore.put(uri_key, "testuri".encode('utf-8')) + datastore.delete(uri_key) + export_all(root_url, user_id, data, start_time_str) + msg = datastore.get(msg_key) + assert msg.decode() == "" + + with patch("weko_search_ui.utils.delete_exported", return_value=None): + datastore.put(uri_key, 'test_uri'.encode('utf-8')) + export_all(root_url, user_id, data2, start_time_str) + msg = datastore.get(msg_key) + assert msg.decode() == "" + + datastore.delete(uri_key) + + data_no_range = {"item_type_id": "1", "item_id_range": ""} + export_all(root_url, user_id, data_no_range, start_time_str) + + recid_data_1 = [ + { + "pid_value": "1", + "object_uuid": "uuid1", + "json": {"publish_status": "public"} + }, + { + "pid_value": "2", + "object_uuid": "uuid2", + "json": {"publish_status": "private"} + } + ] + + recid_data_2 = [ + { + "pid_value": "1", + "object_uuid": "uuid1", + "json": {"publish_status": "public"} + }, + { + "pid_value": "2", + "object_uuid": "uuid2", + "json": {"publish_status": "private"} + } + ] + + with patch("weko_search_ui.utils.get_all_record_id", return_value=recid_data_1): + export_all(root_url, user_id, data, start_time_str) + + data_err = {"item_type_id": "1", "item_id_range": "10-1"} + export_all(root_url, user_id, data_err, start_time_str) + msg = datastore.get(msg_key) + assert msg.decode() == "Export failed. Please check item id range." + + with patch("weko_search_ui.utils.get_record_ids", return_value={}): + export_all(root_url, user_id, data, start_time_str) + + with patch("builtins.open", side_effect=SQLAlchemyError("Test SQLAlchemyError")): + export_all(root_url, user_id, data, start_time_str) + + with patch("weko_search_ui.tasks.write_files_task.apply_async", return_value=None): + with patch("weko_search_ui.utils.WekoRecord.get_record_by_uuid", side_effect=SQLAlchemyError("test_error")): + export_all(root_url, user_id, data, start_time_str) + + # recidsをモックして、record_idsが空になるように設定 + recids = [ + MagicMock(pid_value=str(uuid.uuid4()), object_uuid="uuid1", json=None), # json属性が存在しない + MagicMock(pid_value=str(uuid.uuid4()), object_uuid="uuid2", json={"publish_status": "draft"}), # publish_statusがPUBLICまたはPRIVATEでない + MagicMock(pid_value=str(uuid.uuid4()), object_uuid="uuid3", json={}) # json属性にpublish_statusが含まれていない + ] + + with patch("weko_search_ui.utils.db.session.query", return_value=recids): + export_all(root_url, user_id, data, start_time_str) + + with patch("weko_search_ui.utils.math.ceil", side_effect=Exception("test_error")): + export_all(root_url, user_id, data, start_time_str) + + # raise Exception in _get_item_type_list + with patch("weko_search_ui.utils.ItemTypes.get_by_id", side_effect=Exception("test_error")): + export_all(root_url, user_id, data, start_time_str) + + # # raise Exception in _get_export_data + with patch("weko_search_ui.tasks.write_files_task", side_effect=Exception("test_error")): + export_all(root_url, user_id, data_no_range, start_time_str) + + +def test_get_retry_info(): + # Test case 1: When item_type_id is included in retry_info + item_type_id = "1" + retry_info = { + "1": { + "counter": 5, + "part": 2, + "max": "10" + } + } + fromid = "1" + + counter, file_part, from_pid = get_retry_info(item_type_id, retry_info, fromid) + + assert counter == 5 + assert file_part == 2 + assert from_pid == "10" + + # Test case 2: When item_type_id is not included in retry_info + item_type_id = "2" + retry_info = {} + fromid = "1" + + counter, file_part, from_pid = get_retry_info(item_type_id, retry_info, fromid) + + assert counter == 0 + assert file_part == 1 + assert from_pid == "1" + + # Test case 3: When fromid is empty + item_type_id = "2" + retry_info = {} + fromid = "" + + counter, file_part, from_pid = get_retry_info(item_type_id, retry_info, fromid) - assert not export_all(root_url, user_id, data) - assert not export_all(root_url, user_id, data2) - assert not export_all(root_url, user_id, data3) + assert counter == 0 + assert file_part == 1 + assert from_pid == "1" # def delete_exported(uri, cache_key): def test_delete_exported(i18n_app, file_instance_mock): - file_path = os.path.join( - os.path.dirname(os.path.abspath(__file__)), - "data", - "sample_file", - "sample_file.txt", - ) + file_path = '/code/modules/weko-search-ui/tests/data/sample_file/sample_file.txt' + + mock_file_instance = FileInstance(uri=file_path) + + with patch("invenio_files_rest.models.FileInstance.get_by_uri", return_value=mock_file_instance): + with patch("invenio_files_rest.models.FileInstance.delete", return_value=None): + # Doesn't return any value + assert not delete_exported(file_path, "key") - with patch("invenio_files_rest.models.FileInstance.delete", return_value=None): - # Doesn't return any value - assert not delete_exported(file_path, "key") + +# def write_files(item_datas, export_path, user_id, retrys): +# .tox/c1/bin/pytest --cov=weko_search_ui tests/test_utils.py::test_write_files -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-search-ui/.tox/c1/tmp +def test_write_files(db_activity, i18n_app, users, redis_connect, mocker, item_type, db_records2): + import pytz + with patch("flask_login.utils._get_user", return_value=users[3]['obj']): + # result is True + record = d_wekorecord.get_record_by_pid(1) + item_datas = { + "item_type_id": "1", + "name": "test_item_type", + "recids": ["1"], + "root_url":"https://localhost/", + "jsonschema":'items/jsonschema/1', + "data": { + "1": record + } + } + + mocker.patch("weko_search_ui.utils.os.makedirs") + mocker.patch('builtins.open', side_effect=unittest.mock.mock_open()) + now = datetime.now() + mocker_datetime = mocker.patch('weko_search_ui.utils.datetime') + mocker_datetime.now.return_value = now + with patch('weko_search_ui.utils.pytz.timezone', return_value=pytz.UTC): + assert write_files(item_datas, "tests/data/write_files", current_user.get_id(), 0) + + # result is False + with patch("weko_items_ui.utils.make_stats_file_with_permission", side_effect=SQLAlchemyError("test_error")): + assert not write_files(item_datas, "tests/data/write_files", current_user.get_id(), 0) # def cancel_export_all(): @@ -2311,24 +2500,51 @@ def test_cancel_export_all(i18n_app, users, redis_connect, mocker): cache_key = i18n_app.config["WEKO_ADMIN_CACHE_PREFIX"].format( name="KEY_EXPORT_ALL", user_id=current_user.get_id() ) + file_cache_key = i18n_app.config["WEKO_ADMIN_CACHE_PREFIX"].format( + name="RUN_MSG_EXPORT_ALL_FILE_CREATE", user_id=current_user.get_id() + ) + file_json = { + 'start_time': '2024/05/21 14:23:46', + 'finish_time': '', + 'export_path': '', + 'cancel_flg': False, + 'write_file_status': { + '1': 'started' + } + } + file_result_json = { + 'start_time': '2024/05/21 14:23:46', + 'finish_time': '', + 'export_path': '', + 'cancel_flg': True, + 'write_file_status': { + '1': 'started' + } + } datastore = redis_connect datastore.put(cache_key, "test_task_key".encode("utf-8"), ttl_secs=30) # export_status is True - with patch("weko_search_ui.utils.get_export_status", return_value=(True,None,None,None,None)): + with patch("weko_search_ui.utils.get_export_status", return_value=(True,None,None,None,None,None,None)): + datastore.put(file_cache_key, json.dumps(file_json).encode('utf-8'), ttl_secs=30) mock_revoke = mocker.patch("weko_search_ui.utils.revoke") mock_delete_id = mocker.patch("weko_search_ui.utils.delete_task_id_cache.apply_async") result = cancel_export_all() assert result == True + ds_file_json = datastore.get(file_cache_key).decode('utf-8') + assert json.loads(ds_file_json) == file_result_json mock_revoke.assert_called_with("test_task_key",terminate=True) mock_delete_id.assert_called_with(args=("test_task_key","admin_cache_KEY_EXPORT_ALL_5"),countdown=60) # export_status is False - with patch("weko_search_ui.utils.get_export_status", return_value=(False,None,None,None,None)): + with patch("weko_search_ui.utils.get_export_status", return_value=(False,None,None,None,None,None,None)): + datastore.put(file_cache_key, json.dumps(file_json).encode('utf-8'), ttl_secs=30) mock_revoke = mocker.patch("weko_search_ui.utils.revoke") mock_delete_id = mocker.patch("weko_search_ui.utils.delete_task_id_cache.apply_async") result = cancel_export_all() assert result == True + ds_file_json = datastore.get(file_cache_key).decode('utf-8') + assert json.loads(ds_file_json) == file_json mock_revoke.assert_not_called() mock_delete_id.assert_not_called() @@ -2340,7 +2556,8 @@ def test_cancel_export_all(i18n_app, users, redis_connect, mocker): # def get_export_status(): # .tox/c1/bin/pytest --cov=weko_search_ui tests/test_utils.py::test_get_export_status -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-search-ui/.tox/c1/tmp -def test_get_export_status(i18n_app, users, redis_connect,mocker): +def test_get_export_status(i18n_app, users, redis_connect,mocker, location): + import pytz class MockAsyncResult: def __init__(self,task_id): self.task_id=task_id @@ -2351,6 +2568,47 @@ def successful(self): return self.state == "SUCCESS" def failed(self): return self.state == "FAILED" + + start_time_str = '2024/05/21 14:23:46' + def create_file_json(status): + return { + 'start_time': start_time_str, + 'finish_time': '', + 'export_path': '', + 'cancel_flg': False, + 'write_file_status': { + '1': status + } + } + + def create_file_cancel_json(status): + return { + 'start_time': start_time_str, + 'finish_time': '', + 'export_path': '', + 'cancel_flg': True, + 'write_file_status': { + '1': status + } + } + + def create_not_status_file_json(): + return { + 'start_time': start_time_str, + 'finish_time': '', + 'export_path': '', + 'cancel_flg': False, + 'write_file_status': {} + } + + def create_not_param_file_json(): + return { + 'start_time': start_time_str, + 'finish_time': '', + 'export_path': '', + 'cancel_flg': False + } + mocker.patch("weko_search_ui.utils.AsyncResult",side_effect=MockAsyncResult) with patch("flask_login.utils._get_user", return_value=users[3]["obj"]): cache_key = i18n_app.config["WEKO_ADMIN_CACHE_PREFIX"].format( @@ -2365,31 +2623,125 @@ def failed(self): run_msg = current_app.config["WEKO_ADMIN_CACHE_PREFIX"].format( name="RUN_MSG_EXPORT_ALL", user_id=current_user.get_id() ) + file_msg = current_app.config["WEKO_ADMIN_CACHE_PREFIX"].format( + name='RUN_MSG_EXPORT_ALL_FILE_CREATE', user_id=current_user.get_id() + ) datastore = redis_connect datastore.put(cache_uri, "test_uri".encode("utf-8"), ttl_secs=30) datastore.put(cache_msg, "test_msg".encode("utf-8"), ttl_secs=30) datastore.put(run_msg, "test_run_msg".encode("utf-8"), ttl_secs=30) - # task is success, failed, revoked - datastore.put(cache_key, "SUCCESS_task".encode("utf-8"), ttl_secs=30) - result=get_export_status() - assert result == (False, "test_uri", "test_msg", "test_run_msg", "SUCCESS") - - # task is not success, failed, revoked + # not exist task_id datastore.delete(cache_key) + result=get_export_status() + assert result == (False, "test_uri", "test_msg", "test_run_msg", "", "", "") + + # task is not success, failed, revoked (write_file_data is not exist) datastore.put(cache_key, "PENDING_task".encode("utf-8"), ttl_secs=30) + datastore.delete(file_msg) + datastore.put(file_msg, json.dumps({}).encode('utf-8'), ttl_secs=30) result=get_export_status() - assert result == (True, "test_uri", "test_msg", "test_run_msg", "PENDING") - - # not exist task_id + assert result == (False, "test_uri", "test_msg", "test_run_msg", "", "", "") + + # task is not success, failed, revoked (write_file_status is not exist) + datastore.put(cache_key, "PENDING_task".encode("utf-8"), ttl_secs=30) + datastore.put(file_msg, json.dumps(create_not_param_file_json()).encode('utf-8'), ttl_secs=30) + result=get_export_status() + assert result == (True, "test_uri", "test_msg", "test_run_msg", "", start_time_str, "") + + # task is not success, failed, revoked (write_file_status is started) + datastore.put(cache_key, "PENDING_task".encode("utf-8"), ttl_secs=30) + datastore.put(file_msg, json.dumps(create_file_json('started')).encode('utf-8'), ttl_secs=30) + result=get_export_status() + assert result == (True, "test_uri", "test_msg", "test_run_msg", "STARTED", start_time_str, "") + + # task is not success, failed, revoked (cancel_flg is True) + datastore.put(cache_key, "PENDING_task".encode("utf-8"), ttl_secs=30) + datastore.put(file_msg, json.dumps(create_file_cancel_json('started')).encode('utf-8'), ttl_secs=30) + result=get_export_status() + assert result == (True, "test_uri", "test_msg", "test_run_msg", "REVOKED", start_time_str, "") + + # task is success, failed, revoked (write_file_status is finished) + mocker.patch("os.path.isdir", return_value=False) datastore.delete(cache_key) + datastore.put(cache_key, "SUCCESS_task".encode("utf-8"), ttl_secs=30) + datastore.delete(file_msg) + file_json = create_file_json('finished') + file_json['export_path'] = 'tests/data/import' + datastore.put(file_msg, json.dumps(file_json).encode('utf-8'), ttl_secs=30) + now = datetime.now() + mocker_datetime = mocker.patch('weko_search_ui.utils.datetime') + mocker_datetime.now.return_value = now + mocker.patch('weko_search_ui.utils.bagit.make_bag') + mocker.patch('weko_search_ui.utils.shutil.make_archive') + mocker.patch("builtins.open", mock_open(read_data=b"data")) + mocker.patch("weko_search_ui.utils.FileInstance.create", return_value=MagicMock(uri="test_uri")) + mocker.patch("weko_search_ui.utils.Location.get_default", return_value=MagicMock(uri="test_location")) + task = MagicMock() + task.task_id = 1 + mocker.patch("weko_search_ui.tasks.delete_exported_task", return_value=task) + with patch('weko_search_ui.utils.pytz.timezone', return_value=pytz.UTC): + result = get_export_status() + uri = datastore.get(cache_uri) + assert result == (False, uri.decode(), "test_msg", "test_run_msg", "SUCCESS", start_time_str, now.strftime('%Y/%m/%d %H:%M:%S')) + + # os.path.isdir is True + with patch("weko_search_ui.utils.os.path.isdir", return_value=True): + datastore.delete(file_msg) + file_json = create_file_json('finished') + file_json['export_path'] = 'tests/data/import' + mocker.patch('weko_search_ui.utils.bagit.make_bag') + mocker.patch('weko_search_ui.utils.shutil.make_archive') + mocker.patch("builtins.open", mock_open(read_data=b"data")) + mocker.patch("weko_search_ui.utils.FileInstance.create", return_value=MagicMock(uri="test_uri")) + mocker.patch("weko_search_ui.utils.Location.get_default", return_value=MagicMock(uri="test_location")) + datastore.put(file_msg, json.dumps(file_json).encode('utf-8'), ttl_secs=30) + datastore.delete(run_msg) + datastore.put(run_msg, "test_run_msg".encode("utf-8"), ttl_secs=30) + result=get_export_status() + assert result == (False, uri.decode(), "test_msg", "test_run_msg", "SUCCESS", start_time_str, "") + + # task is success, failed, revoked (write_file_status is not value) + datastore.put(cache_key, "PENDING_task".encode("utf-8"), ttl_secs=30) + file_json = create_not_status_file_json() + file_json['export_path'] = 'tests/data/import' + datastore.put(file_msg, json.dumps(file_json).encode('utf-8'), ttl_secs=30) + now = datetime.now() + mocker_datetime = mocker.patch('weko_search_ui.utils.datetime') + mocker_datetime.now.return_value = now + mocker.patch('weko_search_ui.utils.bagit.make_bag') + mocker.patch('weko_search_ui.utils.shutil.make_archive') + task = MagicMock() + task.task_id = 1 + mocker.patch("weko_search_ui.tasks.delete_exported_task", return_value=task) result=get_export_status() - assert result == (False, "test_uri", "test_msg", "test_run_msg", "") + uri = datastore.get(cache_uri) + assert result == (True, uri.decode(), "test_msg", "test_run_msg", "SUCCESS", start_time_str, "") # raise Exception with patch("weko_search_ui.utils.AsyncResult",side_effect=Exception("test_error")): + datastore.delete(cache_uri) + datastore.put(cache_uri, "test_uri".encode("utf-8"), ttl_secs=30) + datastore.delete(cache_msg) + datastore.put(cache_msg, "test_msg".encode("utf-8"), ttl_secs=30) + datastore.delete(run_msg) + datastore.put(run_msg, "test_run_msg".encode("utf-8"), ttl_secs=30) result=get_export_status() - assert result == (False, "test_uri", "test_msg", "test_run_msg", "") + assert result == (False, "test_uri", "test_msg", "test_run_msg", "", "", "") + + # write_file_status is canceled + with patch("weko_search_ui.utils.AsyncResult",return_value=MockAsyncResult("REVOKED_task")): + datastore.delete(file_msg) + datastore.put(file_msg, json.dumps(create_file_json('canceled')).encode('utf-8'), ttl_secs=30) + result = get_export_status() + assert result == (False, "test_uri", "test_msg", "test_run_msg", "REVOKED", start_time_str, "") + + # write_file_status is errorwith patch("weko_search_ui.utils.AsyncResult",return_value=MockAsyncResult("FAILED_task")): + with patch("weko_search_ui.utils.AsyncResult",return_value=MockAsyncResult("FAILED_task")): + datastore.delete(file_msg) + datastore.put(file_msg, json.dumps(create_file_json('error')).encode('utf-8'), ttl_secs=30) + result = get_export_status() + assert result == (False, "test_uri", "test_msg", "test_run_msg", "", start_time_str, "") # def handle_check_item_is_locked(item): diff --git a/modules/weko-search-ui/weko_search_ui/admin.py b/modules/weko-search-ui/weko_search_ui/admin.py index bc7fb60120..c315edae5d 100644 --- a/modules/weko-search-ui/weko_search_ui/admin.py +++ b/modules/weko-search-ui/weko_search_ui/admin.py @@ -64,6 +64,7 @@ ) from .tasks import ( check_celery_is_run, + check_session_lifetime, check_import_items_task, export_all_task, import_item, @@ -756,40 +757,53 @@ def export_all(self): name=_task_config, user_id=user_id ) - export_status, download_uri, message, run_message, _ = get_export_status() - timezone = str(current_app.config["STATS_WEKO_DEFAULT_TIMEZONE"]()) + export_status, download_uri, message, run_message, \ + _, _, _ = get_export_status() + start_time = datetime.now().strftime('%Y/%m/%d %H:%M:%S') if not export_status: - export_task = export_all_task.apply_async(args=(request.url_root, user_id, data, timezone)) + export_task = export_all_task.apply_async( + args=(request.url_root, user_id, data, start_time) + ) reset_redis_cache(_cache_key, str(export_task.task_id)) # return Response(status=200) - check = check_celery_is_run() - export_status, download_uri, message, run_message, status = get_export_status() + check_celery = check_celery_is_run() + check_life_time = check_session_lifetime() + export_status, download_uri, message, run_message, \ + status, start_time, finish_time = get_export_status() return jsonify( data={ "export_status": export_status, "uri_status": True if download_uri else False, - "celery_is_run": check, + "celery_is_run": check_celery, + "is_lifetime": check_life_time, "error_message": message, "export_run_msg": run_message, - "status": status + "status": status, + "start_time": start_time, + "finish_time": finish_time } ) @expose("/check_export_status", methods=["GET"]) def check_export_status(self): """Check export status.""" - check = check_celery_is_run() - export_status, download_uri, message, run_message, status = get_export_status() + check_celery = check_celery_is_run() + check_life_time = check_session_lifetime() + export_status, download_uri, message, run_message, \ + status, start_time, finish_time = get_export_status() return jsonify( data={ "export_status": export_status, "uri_status": True if download_uri else False, - "celery_is_run": check, + "celery_is_run": check_celery, + "is_lifetime": check_life_time, "error_message": message, "export_run_msg": run_message, - "status": status + "status": status, + "start_time": start_time, + "finish_time": finish_time } ) @@ -797,7 +811,7 @@ def check_export_status(self): def cancel_export(self): """Check export status.""" result = cancel_export_all() - export_status, _, _, _, status = get_export_status() + export_status, _, _, _, status, _, _ = get_export_status() return jsonify(data={"cancel_status": result, "export_status":export_status, "status":status}) @expose("/download", methods=["GET"]) @@ -806,7 +820,8 @@ def download(self): path: it was load from FileInstance """ - export_status, download_uri, message, run_message, _ = get_export_status() + export_status, download_uri, message, run_message, \ + _, _, _ = get_export_status() if not export_status and download_uri is not None: file_instance = FileInstance.get_by_uri(download_uri) return file_instance.send_file( diff --git a/modules/weko-search-ui/weko_search_ui/config.py b/modules/weko-search-ui/weko_search_ui/config.py index 7bb69397b5..16d62c1842 100644 --- a/modules/weko-search-ui/weko_search_ui/config.py +++ b/modules/weko-search-ui/weko_search_ui/config.py @@ -708,7 +708,10 @@ WEKO_SEARCH_UI_BULK_EXPORT_RUN_MSG = "RUN_MSG_EXPORT_ALL" """Bulk export running message.""" -WEKO_SEARCH_UI_BULK_EXPORT_EXPIRED_TIME = 3 +WEKO_SEARCH_UI_BULK_EXPORT_FILE_CREATE_RUN_MSG = "RUN_MSG_EXPORT_ALL_FILE_CREATE" +"""Bulk export file create running message.""" + +WEKO_SEARCH_UI_BULK_EXPORT_EXPIRED_TIME = 1440 """Template for the Admin Bulk Export page.""" WEKO_SEARCH_UI_BULK_EXPORT_TASKID_EXPIRED_TIME = 1 diff --git a/modules/weko-search-ui/weko_search_ui/static/js/weko_search_ui/export.js b/modules/weko-search-ui/weko_search_ui/static/js/weko_search_ui/export.js index e8d013db77..96e04ba9a1 100644 --- a/modules/weko-search-ui/weko_search_ui/static/js/weko_search_ui/export.js +++ b/modules/weko-search-ui/weko_search_ui/static/js/weko_search_ui/export.js @@ -5,12 +5,15 @@ const item_id_label = document.getElementById("item_id").value; const export_item_label = document.getElementById("export_item").value; const download_url_label = document.getElementById("download_url").value; const status_label = document.getElementById("status").value; +const start_time_label = document.getElementById('start_time').value; +const finish_time_label = document.getElementById('finish_time').value; const export_label = document.getElementById("export").value; const export_messaage = document.getElementById("export_messaage").value; const cancel_messaage = document.getElementById("cancel_messaage").value; const run_label = document.getElementById("run").value; const cancel_label = document.getElementById("cancel").value; const celery_not_run = document.getElementById("celery_not_run").value; +const lifetime_not_one_day = document.getElementById("lifetime_not_one_day").value; const error_get_lstItemType = document.getElementById("error_get_lstItemType").value; const error_get_lastItemId = document.getElementById("error_get_lastItemId").value; @@ -60,6 +63,8 @@ class ExportComponent extends React.Component { isDisableExport: false, isDisableCancel: true, taskStatus: "", + startTime: "", + finishTime: "", isExport: false, confirmMessage: "", last_item_id: "", @@ -209,7 +214,8 @@ class ExportComponent extends React.Component { me.setState({ isDisableExport: response.data.export_status, isDisableCancel: !response.data.export_status, - taskStatus: response.data.status + taskStatus: response.data.status, + startTime: response.data.start_time }); }, error: function () { @@ -242,9 +248,11 @@ class ExportComponent extends React.Component { esportRunMessage: response.data.export_run_msg, exportStatus: response.data.export_status, uriStatus: response.data.uri_status, - isDisableExport: response.data.export_status || !response.data.celery_is_run, + isDisableExport: response.data.export_status || !response.data.celery_is_run || !response.data.is_lifetime, isDisableCancel: !response.data.export_status, - taskStatus: response.data.status + taskStatus: response.data.status, + startTime: response.data.start_time, + finishTime: response.data.finish_time }); if (!response.data.celery_is_run) { $('#errors').append( @@ -252,6 +260,12 @@ class ExportComponent extends React.Component { '' + celery_not_run + ''); } + if (!response.data.is_lifetime) { + $('#errors').append( + '
' + + '' + lifetime_not_one_day + '
'); + } if (response.data.error_message) { if(response.data.error_message.length>0){ $('#errors').append( @@ -283,6 +297,8 @@ class ExportComponent extends React.Component { isDisableExport, isDisableCancel, taskStatus, + startTime, + finishTime, esportRunMessage, exportStatus, uriStatus, @@ -344,6 +360,16 @@ class ExportComponent extends React.Component { +
+
+ +
+
+
+
+ +
+
diff --git a/modules/weko-search-ui/weko_search_ui/tasks.py b/modules/weko-search-ui/weko_search_ui/tasks.py index 66133d4dad..fc9a941267 100644 --- a/modules/weko-search-ui/weko_search_ui/tasks.py +++ b/modules/weko-search-ui/weko_search_ui/tasks.py @@ -19,6 +19,10 @@ # MA 02111-1307, USA. """WEKO3 module docstring.""" +import gc +import json +import os +import pickle import shutil from datetime import datetime, timedelta @@ -27,7 +31,7 @@ from celery.task.control import inspect from flask import current_app from weko_admin.api import TempDirInfo -from weko_admin.utils import get_redis_cache +from weko_admin.utils import get_redis_cache, reset_redis_cache from weko_redis.redis import RedisConnection from invenio_db import db @@ -35,6 +39,7 @@ check_import_items, delete_exported, export_all, + write_files, get_lifetime, import_items_to_system, ) @@ -104,37 +109,66 @@ def delete_task_id_cache(task_id, cache_key): datastore.delete(cache_key) @shared_task -def export_all_task(root_url, user_id, data, timezone): +def export_all_task(root_url, user_id, data, start_time): """Export all items.""" - from weko_admin.utils import reset_redis_cache + export_all(root_url, user_id, data, start_time) - _task_config = current_app.config["WEKO_SEARCH_UI_BULK_EXPORT_URI"] - _expired_time = current_app.config["WEKO_SEARCH_UI_BULK_EXPORT_EXPIRED_TIME"] - _cache_key = current_app.config["WEKO_ADMIN_CACHE_PREFIX"].format( - name=_task_config, +@shared_task +def write_files_task(export_path, pickle_file_name , user_id): + """Write files for export. + + Args: + export_path (str): path of files where csv/tsv export to. + pickle_file_name (str): pickle file's name + user_id (int): a user who processed file output. + """ + _msg_config = current_app.config["WEKO_SEARCH_UI_BULK_EXPORT_MSG"] + _msg_key = current_app.config["WEKO_ADMIN_CACHE_PREFIX"].format( + name=_msg_config, user_id=user_id ) - _task_key_config = current_app.config["WEKO_SEARCH_UI_BULK_EXPORT_TASK"] - _task_key = current_app.config["WEKO_ADMIN_CACHE_PREFIX"].format( - name=_task_key_config, + _file_create_config = \ + current_app.config["WEKO_SEARCH_UI_BULK_EXPORT_FILE_CREATE_RUN_MSG"] + _file_create_key = current_app.config["WEKO_ADMIN_CACHE_PREFIX"].format( + name=_file_create_config, user_id=user_id ) - uri = export_all(root_url, user_id, data, timezone) - reset_redis_cache(_cache_key, uri) - delete_exported_task.apply_async( - args=( - uri, - _cache_key, - _task_key - ), - countdown=int(_expired_time) * 60, - ) + def _update_redis_status(json_data, file_name, status,item_type_id): + "Update status in redis cache." + part_name = os.path.splitext(file_name)[1] + part_index = part_name.find('part') + part_number = part_name[part_index + 4:] if part_index != -1 else 1 + json_data['write_file_status'][item_type_id + '.' + str(part_number)] = status + reset_redis_cache(_file_create_key, json.dumps(json_data)) + del part_name, part_index, part_number + + with open(pickle_file_name, 'rb') as f: + import_datas = pickle.load(f) + json_data = json.loads(get_redis_cache(_file_create_key)) + if not json_data['cancel_flg']: + _update_redis_status(json_data, import_datas['name'], 'started',import_datas['item_type_id']) + with open(pickle_file_name, 'rb') as f: + import_datas = pickle.load(f) + result = write_files(import_datas, export_path, user_id, 0) + json_data = json.loads(get_redis_cache(_file_create_key)) + if result: + _update_redis_status(json_data, import_datas['name'], 'finished',import_datas['item_type_id']) + else: + reset_redis_cache(_msg_key, "Export failed.") + json_data['cancel_flg'] = True + _update_redis_status(json_data, import_datas['name'], 'error',import_datas['item_type_id']) + else: + _update_redis_status(json_data, import_datas['name'], 'canceled',import_datas['item_type_id']) + del import_datas,json_data + gc.collect() + os.remove(pickle_file_name) @shared_task -def delete_exported_task(uri, cache_key, task_key): +def delete_exported_task(uri, cache_key, task_key, export_path): """Delete expired exported file.""" + shutil.rmtree(export_path) redis_connection = RedisConnection() datastore = redis_connection.connection(db=current_app.config['CACHE_REDIS_DB'], kv = True) if datastore.redis.exists(cache_key): @@ -172,3 +206,8 @@ def check_celery_is_run(): return False else: return True + +def check_session_lifetime(): + """Check session lifetime.""" + lifetime = get_lifetime() + return True if lifetime >= 86400 else False diff --git a/modules/weko-search-ui/weko_search_ui/templates/weko_search_ui/admin/export.html b/modules/weko-search-ui/weko_search_ui/templates/weko_search_ui/admin/export.html index afe6361251..cd6b5bc448 100644 --- a/modules/weko-search-ui/weko_search_ui/templates/weko_search_ui/admin/export.html +++ b/modules/weko-search-ui/weko_search_ui/templates/weko_search_ui/admin/export.html @@ -44,6 +44,8 @@ + + @@ -51,6 +53,7 @@ + {%- endblock body %} 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 8303bfaa2b..01f9f5f84b 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 6bb1a601ae..eaa121bed6 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 @@ -828,3 +828,9 @@ msgstr "" #~ msgid "Execute" #~ msgstr "" +msgid "Export Start Time" +msgstr "" + +msgid "Export Finish Time" +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 ded7877096..e87f50f884 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 db13d8b017..1d76bf1717 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 @@ -834,3 +834,10 @@ msgstr "ロールの権限が足りずこのインデックスにアイテム登 #~ msgid "Execute" #~ msgstr "実行" + +msgid "Export Start Time" +msgstr "" + +msgid "Export Finish Time" +msgstr "" + diff --git a/modules/weko-search-ui/weko_search_ui/utils.py b/modules/weko-search-ui/weko_search_ui/utils.py index 64d0fd164f..23d58be220 100644 --- a/modules/weko-search-ui/weko_search_ui/utils.py +++ b/modules/weko-search-ui/weko_search_ui/utils.py @@ -22,16 +22,19 @@ import csv import json +import math import os import re import shutil import sys import pytz +import bagit import tempfile import traceback import uuid import zipfile import chardet +import gc from collections import Callable, OrderedDict from datetime import datetime from functools import partial, reduce, wraps @@ -41,7 +44,6 @@ from time import sleep import pickle -import bagit import redis from redis import sentinel from celery.result import AsyncResult @@ -125,9 +127,9 @@ WEKO_IMPORT_VALIDATE_MESSAGE, WEKO_REPO_USER, WEKO_SEARCH_TYPE_DICT, - WEKO_SEARCH_UI_BULK_EXPORT_LIMIT, WEKO_SEARCH_UI_BULK_EXPORT_MSG, WEKO_SEARCH_UI_BULK_EXPORT_RUN_MSG, + WEKO_SEARCH_UI_BULK_EXPORT_FILE_CREATE_RUN_MSG, WEKO_SEARCH_UI_BULK_EXPORT_TASK, WEKO_SEARCH_UI_BULK_EXPORT_URI, WEKO_SYS_USER, @@ -3278,15 +3280,18 @@ def handle_check_duplication_item_id(ids: list): return list(set(result)) -def export_all(root_url, user_id, data, timezone): - """Gather all the item data and export and return as a JSON or BIBTEX. +def export_all(root_url, user_id, data, start_time): + """Prepare to gather all the item data and export and return as a JSON or BIBTEX. Parameter - path is the path if file temparory - post_data is the data items - :return: JSON, BIBTEX + root_url (str): this system's root url. + user_id (int): a user who processed file output. + data (json): export processing's status data. + start_time (str): processing start time. """ - from weko_items_ui.utils import make_stats_file_with_permission, package_export_file + from weko_search_ui.tasks import write_files_task + + current_app.logger.info("Bulk export all start at {}.".format(start_time)) _cache_prefix = current_app.config["WEKO_ADMIN_CACHE_PREFIX"] _msg_config = current_app.config["WEKO_SEARCH_UI_BULK_EXPORT_MSG"] @@ -3299,47 +3304,17 @@ def export_all(root_url, user_id, data, timezone): name=_run_msg_config, user_id=user_id ) - _file_format = current_app.config.get('WEKO_ADMIN_OUTPUT_FORMAT', 'tsv').lower() + _file_create_config = \ + current_app.config["WEKO_SEARCH_UI_BULK_EXPORT_FILE_CREATE_RUN_MSG"] + _file_create_key = _cache_prefix.format( + name=_file_create_config, + user_id=user_id + ) def _itemtype_name(name): """Check a list of allowed characters in filenames.""" return re.sub(r'[\/:*"<>|\s]', "_", name) - def _write_files(item_datas, export_path): - """Write TSV/CSV data to files. - - @param item_datas: - @param export_path: - @param list_item_role: - @return: - """ - permissions = dict( - permission_show_hide=lambda a: True, - check_created_id=lambda a: True, - hide_meta_data_for_role=lambda a: True, - current_language=lambda: True, - ) - headers, records = make_stats_file_with_permission( - item_datas["item_type_id"], - item_datas["recids"], - item_datas["data"], - permissions, - export_path - ) - keys, labels, is_systems, options = headers - item_datas["recids"].sort() - item_datas["keys"] = keys - item_datas["labels"] = labels - item_datas["is_systems"] = is_systems - item_datas["options"] = options - item_datas["data"] = records - item_type_data = item_datas - - file_full_path = "{}/{}.{}".format(export_path, item_type_data.get("name"), _file_format) - with open(file_full_path, "w", encoding="utf-8-sig") as file: - file_output = package_export_file(item_type_data) - file.write(file_output.getvalue()) - def _get_item_type_list(item_type_id): """Get item type list.""" item_types = [] @@ -3360,106 +3335,68 @@ def _get_item_type_list(item_type_id): def _get_export_data(export_path, item_types, retrys, fromid="", toid="", retry_info={}): try: + write_file_json = { + 'start_time': start_time, + 'finish_time': '', + 'export_path': export_path, + 'cancel_flg': False, + 'write_file_status': {} + } + reset_redis_cache( + _file_create_key, + json.dumps(write_file_json) + ) for it in item_types.copy(): item_type_id = it[0] item_type_name = it[1] item_datas = {} - if item_type_id in retry_info: - counter = retry_info[item_type_id]["counter"] - file_part = retry_info[item_type_id]["part"] - from_pid = retry_info[item_type_id]["max"] - else: - counter = 0 - file_part = 1 - from_pid = fromid if fromid else "1" + pickle_file_name = '' + counter, file_part, from_pid = get_retry_info( + item_type_id, retry_info, fromid) current_app.logger.info( - "Start processing item type {}({}).".format( + "Start bulk export of item type {}({}).".format( item_type_name, item_type_id ) ) - # get all record id - if toid: - recids = db.session.query( - PersistentIdentifier.pid_value, - PersistentIdentifier.object_uuid, - RecordMetadata.json - ).join( - ItemMetadata, - PersistentIdentifier.object_uuid == ItemMetadata.id, - ).join( - RecordMetadata, - PersistentIdentifier.object_uuid == RecordMetadata.id, - ).filter( - PersistentIdentifier.pid_type == "recid", - PersistentIdentifier.status == PIDStatus.REGISTERED, - PersistentIdentifier.pid_value.notlike("%.%"), - _func.to_number( - PersistentIdentifier.pid_value, - current_app.config["WEKO_SEARCH_UI_TO_NUMBER_FORMAT"] - ) >= from_pid, - _func.to_number( - PersistentIdentifier.pid_value, - current_app.config["WEKO_SEARCH_UI_TO_NUMBER_FORMAT"] - ) <= toid, - ItemMetadata.item_type_id == item_type_id - ).order_by(_func.to_number( - PersistentIdentifier.pid_value, - current_app.config["WEKO_SEARCH_UI_TO_NUMBER_FORMAT"] - )).all() - else: - recids = db.session.query( - PersistentIdentifier.pid_value, - PersistentIdentifier.object_uuid, - RecordMetadata.json - ).join( - ItemMetadata, - PersistentIdentifier.object_uuid == ItemMetadata.id, - ).join( - RecordMetadata, - PersistentIdentifier.object_uuid == RecordMetadata.id, - ).filter( - PersistentIdentifier.pid_type == "recid", - PersistentIdentifier.status == PIDStatus.REGISTERED, - PersistentIdentifier.pid_value.notlike("%.%"), - _func.to_number( - PersistentIdentifier.pid_value, - current_app.config["WEKO_SEARCH_UI_TO_NUMBER_FORMAT"] - ) >= from_pid, - ItemMetadata.item_type_id == item_type_id - ).order_by(_func.to_number( - PersistentIdentifier.pid_value, - current_app.config["WEKO_SEARCH_UI_TO_NUMBER_FORMAT"] - )).all() - - if len(recids) == 0: + + recids = get_all_record_id(toid, from_pid, item_type_id) + current_app.logger.info("{}({}) get recids completed:{}".format(item_type_name, item_type_id, recids.count())) + if not recids: item_types.remove(it) continue + record_ids = get_record_ids(recids) - record_ids = [(recid.pid_value, recid.object_uuid) - for recid in recids if 'publish_status' in recid.json - and recid.json['publish_status'] in [PublishStatus.PUBLIC.value, PublishStatus.PRIVATE.value]] + # recidsを削除 + del recids + gc.collect() + + file_count = math.ceil(len(record_ids) / current_app.config["WEKO_SEARCH_UI_BULK_EXPORT_LIMIT"]) + write_file_json = json.loads(get_redis_cache(_file_create_key)) + for i in range(file_count): + write_file_json['write_file_status'][item_type_id + '.' + str(i + 1)] = 'waiting' + reset_redis_cache( + _file_create_key, + json.dumps(write_file_json) + ) if len(record_ids) == 0: item_types.remove(it) continue for recid, uuid in record_ids: - if counter % WEKO_SEARCH_UI_BULK_EXPORT_LIMIT == 0 and item_datas: + if counter % current_app.config["WEKO_SEARCH_UI_BULK_EXPORT_LIMIT"] == 0 and item_datas: # Create export info file item_datas["name"] = "{}.part{}".format( item_datas["name"], file_part ) - _write_files(item_datas, export_path) - reset_redis_cache( - _run_msg_key, - "The latest {} file was created on {}.".format( - _file_format, - datetime.now(pytz.timezone(timezone)).strftime("%Y/%m/%d %H:%M:%S")) - + " Number of retries: {} times.".format(retrys) - ) - current_app.logger.info( - "{}.{} has been created.".format(item_datas["name"], _file_format) + pickle_file_name = "{}.{}.part{}.pickle".format( + user_id, item_type_id, file_part ) + with open(pickle_file_name, 'wb') as f: + pickle.dump(item_datas, f) + del item_datas + gc.collect() + write_files_task.apply_async(args=(export_path, pickle_file_name, user_id,)) item_datas = {} file_part += 1 retry_info[item_type_id] = { @@ -3481,28 +3418,31 @@ def _get_export_data(export_path, item_types, retrys, fromid="", toid="", retry_ "recids": [], "data": {}, } + pickle_file_name = "{}.{}.pickle".format(user_id,item_type_id) item_datas["recids"].append(recid) item_datas["data"][recid] = record counter += 1 + del record + gc.collect() if file_part != 1: item_datas["name"] = "{}.part{}".format( item_datas["name"], file_part ) + pickle_file_name = "{}.{}.part{}.pickle".format( + user_id, item_type_id,file_part + ) + + with open(pickle_file_name, 'wb') as f: + pickle.dump(item_datas, f) + + del item_datas + gc.collect() + # Create export info file - _write_files(item_datas, export_path) - reset_redis_cache( - _run_msg_key, - "The latest {} file was created on {}.".format( - _file_format, - datetime.now(pytz.timezone(timezone)).strftime("%Y/%m/%d %H:%M:%S")) - + " Number of retries: {} times.".format(retrys) - ) + write_files_task.apply_async(args=(export_path, pickle_file_name, user_id,)) item_types.remove(it) - current_app.logger.info( - "{}.{} has been created.".format(item_datas["name"], _file_format) - ) current_app.logger.info( "Processed {} items of item type {}.".format( counter, item_type_name @@ -3526,9 +3466,10 @@ def _get_export_data(export_path, item_types, retrys, fromid="", toid="", retry_ reset_redis_cache(_msg_key, "") reset_redis_cache(_run_msg_key, "") - temp_path = tempfile.TemporaryDirectory( - prefix=current_app.config["WEKO_ITEMS_UI_EXPORT_TMP_PREFIX"] - ) + reset_redis_cache(_file_create_key, json.dumps({})) + + temp_path = os.getenv('TMPDIR') + os.makedirs(temp_path, exist_ok=True) try: # Delete old file _task_config = current_app.config["WEKO_SEARCH_UI_BULK_EXPORT_URI"] @@ -3540,7 +3481,7 @@ def _get_export_data(export_path, item_types, retrys, fromid="", toid="", retry_ if prev_uri: delete_exported(prev_uri, _uri_key) - export_path = temp_path.name + "/" + datetime.utcnow().strftime("%Y%m%d%H%M%S") + export_path = temp_path + "/" + datetime.utcnow().strftime("%Y%m%d%H%M%S%f") os.makedirs(export_path, exist_ok=True) item_type_id = data.get('item_type_id', "-1") @@ -3562,25 +3503,211 @@ def _get_export_data(export_path, item_types, retrys, fromid="", toid="", retry_ result = _get_export_data(export_path, item_types, 0, fromid, toid) if result: - # Create bag - bagit.make_bag(export_path) - shutil.make_archive(export_path, "zip", export_path) - with open(export_path + ".zip", "rb") as file: - src = FileInstance.create() - src.set_contents(file, default_location=Location.get_default().uri) db.session.commit() else: + json_data = json.loads(get_redis_cache(_file_create_key)) + json_data['cancel_flg'] = True + reset_redis_cache(_file_create_key, json.dumps(json_data)) reset_redis_cache(_msg_key, "Export failed.") else: reset_redis_cache(_msg_key, "Export failed. Please check item id range.") - reset_redis_cache(_run_msg_key, "") - return src.uri if result and src else "" except Exception as ex: db.session.rollback() current_app.logger.error(ex) reset_redis_cache(_msg_key, "Export failed.") reset_redis_cache(_run_msg_key, "") - return "" + +def get_all_record_id(toid, from_pid, item_type_id): + """Get all record id. + + Args: + toid (str): The ending ID. + from_pid (str): The starting ID. + item_type_id (str): The item type ID. + + Returns: + list: List of record IDs. + """ + + # get all record id + if toid: + recids = db.session.query( + PersistentIdentifier.pid_value, + PersistentIdentifier.object_uuid, + RecordMetadata.json + ).join( + ItemMetadata, + PersistentIdentifier.object_uuid == ItemMetadata.id, + ).join( + RecordMetadata, + PersistentIdentifier.object_uuid == RecordMetadata.id, + ).filter( + PersistentIdentifier.pid_type == "recid", + PersistentIdentifier.status == PIDStatus.REGISTERED, + PersistentIdentifier.pid_value.notlike("%.%"), + _func.to_number( + PersistentIdentifier.pid_value, + current_app.config["WEKO_SEARCH_UI_TO_NUMBER_FORMAT"] + ) >= from_pid, + _func.to_number( + PersistentIdentifier.pid_value, + current_app.config["WEKO_SEARCH_UI_TO_NUMBER_FORMAT"] + ) <= toid, + ItemMetadata.item_type_id == item_type_id + ).order_by(_func.to_number( + PersistentIdentifier.pid_value, + current_app.config["WEKO_SEARCH_UI_TO_NUMBER_FORMAT"] + )).yield_per(500) + else: + recids = db.session.query( + PersistentIdentifier.pid_value, + PersistentIdentifier.object_uuid, + RecordMetadata.json + ).join( + ItemMetadata, + PersistentIdentifier.object_uuid == ItemMetadata.id, + ).join( + RecordMetadata, + PersistentIdentifier.object_uuid == RecordMetadata.id, + ).filter( + PersistentIdentifier.pid_type == "recid", + PersistentIdentifier.status == PIDStatus.REGISTERED, + PersistentIdentifier.pid_value.notlike("%.%"), + _func.to_number( + PersistentIdentifier.pid_value, + current_app.config["WEKO_SEARCH_UI_TO_NUMBER_FORMAT"] + ) >= from_pid, + ItemMetadata.item_type_id == item_type_id + ).order_by(_func.to_number( + PersistentIdentifier.pid_value, + current_app.config["WEKO_SEARCH_UI_TO_NUMBER_FORMAT"] + )).yield_per(500) + + return recids + + +def get_retry_info(item_type_id, retry_info, fromid): + """Get retry information for item type. + + Args: + item_type_id (str): The item type ID. + retry_info (dict): The retry information dictionary. + fromid (str): The starting ID. + + Returns: + tuple: A tuple containing counter, file_part, and from_pid. + """ + if item_type_id in retry_info: + counter = retry_info[item_type_id]["counter"] + file_part = retry_info[item_type_id]["part"] + from_pid = retry_info[item_type_id]["max"] + else: + counter = 0 + file_part = 1 + from_pid = fromid if fromid else "1" + return counter, file_part, from_pid + + +def get_record_ids(recids): + """Get record ids. + + Args: + recids (list): List of record ids. + + Returns: + list: List of record ids. + """ + record_ids = [(recid.pid_value, recid.object_uuid) + for recid in recids if recid.json and 'publish_status' in recid.json \ + and recid.json['publish_status'] in [PublishStatus.PUBLIC.value, PublishStatus.PRIVATE.value]] + return record_ids + + +def write_files(item_datas, export_path, user_id, retrys): + """Write TSV/CSV data to files. + Args: + item_datas (json): data for file output + export_path (str): file creation destination + user_id (int): performing user id + retrys (int): retry time + + Returns: + bool: task is success or failure. + """ + from weko_items_ui.utils import make_stats_file_with_permission, \ + package_export_file + _cache_prefix = current_app.config["WEKO_ADMIN_CACHE_PREFIX"] + _run_msg_config = current_app.config["WEKO_SEARCH_UI_BULK_EXPORT_RUN_MSG"] + _run_msg_key = _cache_prefix.format( + name=_run_msg_config, + user_id=user_id + ) + _timezone = current_app.config.get("WEKO_INDEX_TREE_PUBLIC_DEFAULT_TIMEZONE") + _file_format = current_app.config.get('WEKO_ADMIN_OUTPUT_FORMAT', 'tsv').lower() + + try: + permissions = dict( + permission_show_hide=lambda a: True, + check_created_id=lambda a: True, + hide_meta_data_for_role=lambda a: True, + current_language=lambda: True, + ) + headers, records = make_stats_file_with_permission( + item_datas["item_type_id"], + item_datas["recids"], + item_datas["data"], + permissions, + export_path + ) + keys, labels, is_systems, options = headers + item_datas["recids"].sort() + item_datas["keys"] = keys + item_datas["labels"] = labels + item_datas["is_systems"] = is_systems + item_datas["options"] = options + item_datas["data"] = records + item_type_data = item_datas + + os.makedirs(export_path, exist_ok=True) + + file_full_path = "{}/{}.{}".format( + export_path, + item_type_data.get("name"), + _file_format + ) + with open(file_full_path, "w", encoding="utf-8-sig") as file: + file_output = package_export_file(item_type_data) + file.write(file_output.getvalue()) + del file_output,item_type_data + gc.collect() + reset_redis_cache( + _run_msg_key, + "The latest {} file was created on {}.".format( + _file_format, + datetime.now(pytz.timezone(_timezone)).strftime("%Y/%m/%d %H:%M:%S")) + + " Number of retries: {} times.".format(retrys) + ) + current_app.logger.info( + "{}.{} has been created.".format(item_datas["name"], _file_format) + ) + db.session.commit() + del item_datas, headers, records, keys, labels, is_systems, options,permissions + gc.collect() + return True + except SQLAlchemyError as ex: + current_app.logger.error(ex) + _num_retry = current_app.config["WEKO_SEARCH_UI_BULK_EXPORT_RETRY"] + if retrys < _num_retry: + retrys += 1 + current_app.logger.info("retry count: {}".format(retrys)) + db.session.rollback() + sleep(5) + result = write_files( + item_datas, export_path, user_id, retrys + ) + return result + else: + return False def delete_exported(uri, cache_key): @@ -3606,10 +3733,14 @@ def cancel_export_all(): name=WEKO_SEARCH_UI_BULK_EXPORT_TASK, user_id=current_user.get_id() ) + _file_create_key = current_app.config["WEKO_ADMIN_CACHE_PREFIX"].format( + name=WEKO_SEARCH_UI_BULK_EXPORT_FILE_CREATE_RUN_MSG, + user_id=current_user.get_id() + ) _expired_time=current_app.config["WEKO_SEARCH_UI_BULK_EXPORT_TASKID_EXPIRED_TIME"] try: task_id = get_redis_cache(cache_key) - export_status, _, _, _, _ = get_export_status() + export_status, _, _, _, _, _, _ = get_export_status() if export_status: revoke(task_id, terminate=True) @@ -3620,6 +3751,9 @@ def cancel_export_all(): ), countdown=int(_expired_time) * 60 ) + json_data = json.loads(get_redis_cache(_file_create_key)) + json_data['cancel_flg'] = True + reset_redis_cache(_file_create_key, json.dumps(json_data)) return True except Exception as ex: current_app.logger.error(ex) @@ -3632,6 +3766,8 @@ def get_export_status(): Return: True: Otthers False: Success / Failed / Revoked """ + from weko_search_ui.tasks import delete_exported_task + cache_key = current_app.config["WEKO_ADMIN_CACHE_PREFIX"].format( name=WEKO_SEARCH_UI_BULK_EXPORT_TASK, user_id=current_user.get_id() @@ -3648,27 +3784,91 @@ def get_export_status(): name=WEKO_SEARCH_UI_BULK_EXPORT_RUN_MSG, user_id=current_user.get_id() ) + file_msg = current_app.config["WEKO_ADMIN_CACHE_PREFIX"].format( + name=WEKO_SEARCH_UI_BULK_EXPORT_FILE_CREATE_RUN_MSG, + user_id=current_user.get_id() + ) + _expired_time = current_app.config["WEKO_SEARCH_UI_BULK_EXPORT_EXPIRED_TIME"] + + def _check_write_file_info(json): + status = json.get('write_file_status','before') + cancel_flg = json.get('cancel_flg', False) + if status == 'before': + return 'BEFORE' + elif status and ('waiting' not in status.values()) and ('started' not in status.values()): + if 'error' in status.values(): + return '' + elif 'canceled' in status.values(): + return 'REVOKED' + else: + return 'SUCCESS' + elif cancel_flg: + return 'REVOKED' + elif not status: + return 'SUCCESS' + else: + return 'STARTED' + export_status = False download_uri = None message = None run_message = "" status = "" + start_time = "" + finish_time = "" try: task_id = get_redis_cache(cache_key) download_uri = get_redis_cache(cache_uri) message = get_redis_cache(cache_msg) run_message = get_redis_cache(run_msg) + write_file_info = get_redis_cache(file_msg) if task_id: - task = AsyncResult(task_id) - status_cond = task.successful() or task.failed() or task.state == "REVOKED" - status = task.state - export_status = True if not status_cond else False + write_file_data = json.loads(write_file_info) + if write_file_data: + write_file_status = _check_write_file_info(write_file_data) + task = AsyncResult(task_id) + status_cond = (task.successful() or task.failed() or task.state == "REVOKED") \ + and write_file_status != 'STARTED' + if not write_file_status == 'BEFORE': + status = write_file_status + export_status = True if not status_cond else False + start_time = write_file_data.get("start_time") + finish_time = write_file_data.get("finish_time") + if status_cond and write_file_status == 'SUCCESS': + export_path = write_file_data['export_path'] + is_dir = not os.path.isdir(os.path.join(export_path, 'data')) + if is_dir: + bagit.make_bag(export_path) + shutil.make_archive(export_path, "zip", export_path) + with open(export_path + ".zip", "rb") as file: + src = FileInstance.create() + src.set_contents(file, default_location=Location.get_default().uri) + db.session.commit() + download_uri = src.uri + _timezone = current_app.config.get("WEKO_INDEX_TREE_PUBLIC_DEFAULT_TIMEZONE") + finish_time = datetime.now(pytz.timezone(_timezone)).strftime('%Y/%m/%d %H:%M:%S') + write_file_data = json.loads(get_redis_cache(file_msg)) + write_file_data["finish_time"] = finish_time + current_app.logger.info("Bulk export all finished at {}.".format(finish_time)) + reset_redis_cache(file_msg, json.dumps(write_file_data)) + reset_redis_cache(cache_uri, download_uri) + reset_redis_cache(run_msg, "") + delete_exported_task.apply_async( + args=( + download_uri, + cache_uri, + cache_key, + export_path + ), + countdown=int(_expired_time) * 60, + ) + os.remove(export_path + ".zip") except Exception as ex: current_app.logger.error(ex) export_status = False - return export_status, download_uri, message, run_message, status - + return export_status, download_uri, message, run_message, \ + status, start_time, finish_time def handle_check_item_is_locked(item): """Check an item is being edit or deleted.