diff --git a/modules/invenio-accounts/invenio_accounts/admin.py b/modules/invenio-accounts/invenio_accounts/admin.py index 039d82708e..2eae4f9ce9 100644 --- a/modules/invenio-accounts/invenio_accounts/admin.py +++ b/modules/invenio-accounts/invenio_accounts/admin.py @@ -15,16 +15,21 @@ from flask_admin.babel import lazy_gettext from flask_admin.contrib.sqla import ModelView from flask_admin.contrib.sqla.ajax import QueryAjaxModelLoader +from flask_admin.contrib.sqla.fields import QuerySelectMultipleField +from flask_admin.form import Select2Widget from flask_admin.form.fields import DateTimeField from flask_admin.model.fields import AjaxSelectMultipleField from flask_babelex import gettext as _ from flask_security import current_user from flask_security.recoverable import send_reset_password_instructions from flask_security.utils import hash_password +from invenio_communities.models import Community from invenio_db import db from passlib import pwd +from sqlalchemy import func from werkzeug.local import LocalProxy -from wtforms.fields import BooleanField +from collections import OrderedDict +from wtforms.fields import BooleanField, SelectMultipleField from wtforms.validators import DataRequired from weko_workflow.models import WorkFlow, WorkflowRole @@ -52,7 +57,7 @@ class UserView(ModelView): column_details_list = \ list_all - form_columns = ('email', 'password', 'active', 'roles', 'notification') + form_columns = ('email', 'password', 'active', 'notification') form_args = dict( email=dict(label='Email', validators=[DataRequired()]), @@ -79,6 +84,44 @@ class UserView(ModelView): 'last_login_ip': _('Last Login IP') } + def scaffold_form(self): + form_class = super(UserView, self).scaffold_form() + form_class.role = QuerySelectMultipleField( + 'Roles', + query_factory=lambda: Role.query.filter(~Role.name.like('%_groups_%')).all(), + get_label='name', + widget=Select2Widget(multiple=True) + ) + form_class.group = QuerySelectMultipleField( + 'Groups', + query_factory=lambda: Role.query.filter(Role.name.like('%_groups_%')).all(), + get_label='name', + widget=Select2Widget(multiple=True) + ) + + return form_class + + def _order_fields(self, form): + custom_order = ['email', 'password', 'active', 'role', 'group', 'notification'] + ordered_fields = OrderedDict() + for field_name in custom_order: + ordered_fields[field_name] = form._fields[field_name] + form._fields = ordered_fields + return form + + def create_form(self, obj=None): + form = super(UserView, self).create_form(obj) + return self._order_fields(form) + + def edit_form(self, obj=None): + form = super(UserView, self).edit_form(obj) + return self._order_fields(form) + + def on_form_prefill(self, form, id): + obj = self.get_one(id) + form.role.data = [role for role in obj.roles if '_groups_' not in role.name] + form.group.data = [role for role in obj.roles if '_groups_' in role.name] + def on_model_change(self, form, User, is_created): """Hash password when saving.""" if form.password.data is not None: @@ -86,11 +129,32 @@ def on_model_change(self, form, User, is_created): if pwd_ctx.identify(form.password.data) is None: User.password = hash_password(form.password.data) + roles = form.role.data + form.group.data + User.roles = roles + def after_model_change(self, form, User, is_created): """Send password instructions if desired.""" if is_created and form.notification.data is True: send_reset_password_instructions(User) + def get_query(self): + """Return a query for the model type.""" + if any(role.name in current_app.config['WEKO_PERMISSION_SUPER_ROLE_USER'] for role in current_user.roles): + return self.session.query(self.model) + else: + repositories = Community.get_repositories_by_user(current_user) + groups = [repository.group for repository in repositories] + return self.session.query(self.model).filter(self.model.roles.any(Role.id.in_([group.id for group in groups]))) + + def get_count_query(self): + """Return a the count query for the model type""" + if any(role.name in current_app.config['WEKO_PERMISSION_SUPER_ROLE_USER'] for role in current_user.roles): + return self.session.query(func.count('*')).select_from(self.model) + else: + repositories = Community.get_repositories_by_user(current_user) + groups = [repository.group for repository in repositories] + return self.session.query(func.count('*')).select_from(self.model).filter(self.model.roles.any(Role.id.in_([group.id for group in groups]))) + @action('inactivate', _('Inactivate'), _('Are you sure you want to inactivate selected users?')) @commit @@ -138,7 +202,12 @@ def action_activate(self, ids): _system_role = os.environ.get('INVENIO_ROLE_SYSTEM', 'System Administrator') - + _repo_role = os.environ.get('INVENIO_ROLE_REPOSITORY' + 'Repository Administrator') + _com_role = os.environ.get('INVENIO_ROLE_COMMUNITY', + 'Community Administrator') + _admin_roles = [_system_role, _repo_role, _com_role] + @property def can_create(self): """Check permission for creating.""" @@ -147,12 +216,12 @@ def can_create(self): @property def can_edit(self): """Check permission for Editing.""" - return self._system_role in [role.name for role in current_user.roles] + return any(role.name in self._admin_roles for role in current_user.roles) @property def can_delete(self): """Check permission for Deleting.""" - return self._system_role in [role.name for role in current_user.roles] + return any(role.name in self._admin_roles for role in current_user.roles) class RoleView(ModelView): diff --git a/modules/invenio-accounts/invenio_accounts/utils.py b/modules/invenio-accounts/invenio_accounts/utils.py index 0c2fbc182c..bca6b3c797 100644 --- a/modules/invenio-accounts/invenio_accounts/utils.py +++ b/modules/invenio-accounts/invenio_accounts/utils.py @@ -17,6 +17,7 @@ from jwt import DecodeError, ExpiredSignatureError, decode, encode from .errors import JWTDecodeError, JWTExpiredToken +from .models import User, userrole, Role def jwt_create_token(user_id=None, additional_data=None): @@ -85,3 +86,15 @@ def set_session_info(app, response, **extra): response.headers['X-Session-ID'] = session_id if current_user.is_authenticated: response.headers['X-User-ID'] = current_user.get_id() + + +def get_user_ids_by_role(role_id): + """Get user IDs by role ID. + + Args: + role_id (int): The ID of the role. + + Returns: + list: A list of user IDs associated with the given role. + """ + return [str(user.id) for user in User.query.join(userrole).join(Role).filter(Role.id == role_id).all()] diff --git a/modules/invenio-accounts/tests/conftest.py b/modules/invenio-accounts/tests/conftest.py index 2e847fc041..91f1f12f05 100644 --- a/modules/invenio-accounts/tests/conftest.py +++ b/modules/invenio-accounts/tests/conftest.py @@ -31,6 +31,7 @@ from simplekv.memory.redisstore import RedisStore from sqlalchemy_utils.functions import create_database, database_exists, \ drop_database +from weko_records_ui.config import WEKO_PERMISSION_SUPER_ROLE_USER from invenio_accounts import InvenioAccounts from invenio_accounts.admin import role_adminview, session_adminview, \ @@ -69,7 +70,8 @@ def _app_factory(config=None): TESTING=True, WTF_CSRF_ENABLED=False, ACCOUNTS_JWT_ALOGORITHM = 'HS256', - ACCOUNTS_JWT_SECRET_KEY = 'None' + ACCOUNTS_JWT_SECRET_KEY = 'None', + WEKO_PERMISSION_SUPER_ROLE_USER = WEKO_PERMISSION_SUPER_ROLE_USER, ) # Set key value session store to use Redis when running on TravisCI. diff --git a/modules/invenio-accounts/tests/test_admin.py b/modules/invenio-accounts/tests/test_admin.py index 8dba1f328e..8a6cb02806 100644 --- a/modules/invenio-accounts/tests/test_admin.py +++ b/modules/invenio-accounts/tests/test_admin.py @@ -7,7 +7,7 @@ # under the terms of the MIT License; see LICENSE file for more details. import pytest -from mock import patch +from mock import patch, MagicMock from flask import current_app, session, url_for from flask_admin import menu from flask_security import url_for_security @@ -17,9 +17,9 @@ from invenio_accounts import InvenioAccounts from invenio_accounts.cli import users_create -from invenio_accounts.models import SessionActivity +from invenio_accounts.models import SessionActivity, User, Role from invenio_accounts.testutils import create_test_user, login_user_via_view -from invenio_accounts.admin import SessionActivityView +from invenio_accounts.admin import SessionActivityView, UserView _datastore = LocalProxy( lambda: current_app.extensions['security'].datastore @@ -220,4 +220,66 @@ def test_admin_sessions_action_delete(app): view.action_delete([user1_sid]) sessions = SessionActivity.query.all() assert len(sessions) == 1 - \ No newline at end of file + + +# .tox/c1/bin/pytest --cov=invenio_accounts tests/test_admin.py::test_userview_get_query -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/invenio-accounts/.tox/c1/tmp +def test_userview_get_query(app, users): + """Test get_query for super role user.""" + view = UserView(User, db.session) + mock_repo = MagicMock(group=MagicMock(id=1)) + with app.test_request_context(): + with patch("flask_login.utils._get_user", return_value=users[2]['obj']): + query = view.get_query() + assert query is not None + assert query.count() == User.query.count() + + with patch("flask_login.utils._get_user", return_value=users[0]['obj']): + with patch("invenio_communities.models.Community.get_repositories_by_user", return_value=[mock_repo]): + db.session.add(users[0]['obj']) + db.session.commit() + query = view.get_query() + assert query is not None + assert query.count() == 0 + + +# .tox/c1/bin/pytest --cov=invenio_accounts tests/test_admin.py::test_userview_get_count_query -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/invenio-accounts/.tox/c1/tmp +def test_userview_get_count_query(app, users): + """Test get_count_query for super role user.""" + view = UserView(User, db.session) + mock_repo = MagicMock(group=MagicMock(id=1)) + with app.test_request_context(): + with patch("flask_login.utils._get_user", return_value=users[2]['obj']): + query = view.get_count_query() + assert query is not None + assert query.scalar() == User.query.count() + + with patch("flask_login.utils._get_user", return_value=users[0]['obj']): + with patch("invenio_communities.models.Community.get_repositories_by_user", return_value=[mock_repo]): + db.session.add(users[0]['obj']) + db.session.commit() + query = view.get_count_query() + assert query is not None + assert query.scalar() == 0 + +# .tox/c1/bin/pytest --cov=invenio_accounts tests/test_admin.py::test_userview_on_form_prefill -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/invenio-accounts/.tox/c1/tmp +def test_userview_on_form_prefill(app, users): + """Test on_form_prefill for super role user.""" + view = UserView(User, db.session) + form = view.create_form() + user = users[2]['obj'] + user.roles = [ + Role(name='role1'), + Role(name='role2_groups_1') + ] + view.get_one = MagicMock(return_value=user) + view.on_form_prefill(form, user.id) + assert form.data['active'] is False + assert form.role.data == [user.roles[0]] + assert form.group.data == [user.roles[1]] + +# .tox/c1/bin/pytest --cov=invenio_accounts tests/test_admin.py::test_userview_edit_form -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/invenio-accounts/.tox/c1/tmp +def test_userview_edit_form(app, users): + """Test edit_form for super role user.""" + view = UserView(User, db.session) + form = view.edit_form() + assert form.data['active'] is False \ No newline at end of file diff --git a/modules/invenio-accounts/tests/test_utils.py b/modules/invenio-accounts/tests/test_utils.py index 3c4ba867ff..0413d6b805 100644 --- a/modules/invenio-accounts/tests/test_utils.py +++ b/modules/invenio-accounts/tests/test_utils.py @@ -20,7 +20,7 @@ from invenio_accounts import testutils from invenio_accounts.errors import JWTDecodeError, JWTExpiredToken -from invenio_accounts.utils import jwt_create_token, jwt_decode_token +from invenio_accounts.utils import jwt_create_token, jwt_decode_token, get_user_ids_by_role def test_client_authenticated(app): @@ -172,3 +172,33 @@ def test_jwt_expired_token(app): # Random token with pytest.raises(JWTDecodeError): jwt_decode_token('Roadster SV') + + +def test_get_user_ids_by_role_returns_user_ids(app): + """Test get_user_ids_by_role.""" + with app.app_context(): + ds = app.extensions['invenio-accounts'].datastore + + user1 = ds.create_user(email='test1@test.org', active=True) + user2 = ds.create_user(email='test2@test.org', active=True) + role = ds.create_role(name='superuser', description='1234') + ds.add_role_to_user(user1, role) + ds.add_role_to_user(user2, role) + ds.commit() + + user_ids = get_user_ids_by_role(role.id) + assert str(user1.id) in user_ids + assert str(user2.id) in user_ids + assert len(user_ids) == 2 + + +@pytest.mark.parametrize("role_id, expected", [ + (999, []), + (None, []), + ('1', []), +]) +def test_get_user_ids_by_role_returns_empty_list(app, role_id, expected): + """Test get_user_ids_by_role.""" + with app.app_context(): + user_ids = get_user_ids_by_role(role_id) + assert user_ids == expected \ No newline at end of file diff --git a/modules/invenio-communities/invenio_communities/admin.py b/modules/invenio-communities/invenio_communities/admin.py index 13f3b282f4..514ad8637e 100644 --- a/modules/invenio-communities/invenio_communities/admin.py +++ b/modules/invenio-communities/invenio_communities/admin.py @@ -25,18 +25,32 @@ from __future__ import absolute_import, print_function +import os +import json import re +import sys +from flask import request, abort, jsonify, redirect, url_for, flash from flask.globals import current_app from flask_admin.contrib.sqla import ModelView +from flask_admin import expose from flask_login import current_user +from invenio_accounts.models import Role from invenio_db import db from sqlalchemy import func, or_ from weko_index_tree.models import Index from wtforms.validators import ValidationError +from wtforms import FileField, RadioField, StringField +from wtforms.utils import unset_value +from invenio_i18n.ext import current_i18n +from weko_gridlayout.services import WidgetDesignPageServices +from weko_handle.api import Handle +from weko_workflow.config import WEKO_SERVER_CNRI_HOST_LINK +from b2handle.clientcredentials import PIDClientCredentials +from wtforms.ext.sqlalchemy.fields import QuerySelectField from .models import Community, FeaturedCommunity, InclusionRequest -from .utils import get_user_role_ids +from .utils import get_user_role_ids, delete_empty def _(x): @@ -47,13 +61,12 @@ def _(x): class CommunityModelView(ModelView): """ModelView for the Community.""" - can_create = True can_edit = True - can_delete = False + can_delete = True can_view_details = True column_display_all_relations = True - form_columns = ('id', 'owner', 'index', 'title', 'description', 'page', - 'curation_policy', 'ranking', 'fixed_points') + form_columns = ('id', 'cnri', 'owner', 'index', 'group', 'title', 'description', 'page', + 'curation_policy', 'ranking', 'fixed_points','content_policy', 'thumbnail','login_menu_enabled') column_list = ( 'id', @@ -69,6 +82,438 @@ class CommunityModelView(ModelView): column_searchable_list = ('id', 'title', 'description') edit_template = "invenio_communities/admin/edit.html" + @expose('/new/', methods=['GET', 'POST']) + def create_view(self): + + def validate_input_id(id): + the_patterns = { + "ASCII_LETTER_PATTERN": "[a-zA-Z0-9_-]+$", + "FIRST_LETTER_PATTERN1": "^[a-zA-Z_-].*", + "FIRST_LETTER_PATTERN2": "^[-]+[0-9]+", + } + the_result = { + "ASCII_LETTER_PATTERN": "Don't use space or special " + "character except `-` and `_`.", + "FIRST_LETTER_PATTERN1": 'The first character cannot ' + 'be a number or special character. ' + 'It should be an ' + 'alphabet character, "-" or "_"', + "FIRST_LETTER_PATTERN2": "Cannot set negative number to ID.", + } + + m = re.match(the_patterns['FIRST_LETTER_PATTERN1'], id) + if m is None: + raise ValidationError(the_result['FIRST_LETTER_PATTERN1']) + m = re.match(the_patterns['FIRST_LETTER_PATTERN2'], id) + if m is not None: + raise ValidationError(the_result['FIRST_LETTER_PATTERN2']) + m = re.match(the_patterns['ASCII_LETTER_PATTERN'], id) + if m is None: + raise ValidationError(the_result['ASCII_LETTER_PATTERN']) + + form = self.create_form() + + if(request.method == 'POST'): + try: + pageaddFlag = True + form_data = request.form.to_dict() + model = Community() + validate_input_id(form_data['id']) + model.id = form_data['id'] + model.id_role = form_data['owner'] + model.root_node_id = form_data['index'] + model.title = form_data['title'] + model.description = form_data['description'] + model.page = form_data['page'] + model.curation_policy = form_data['curation_policy'] + model.ranking = form_data['ranking'] + model.fixed_points = form_data['fixed_points'] + model.content_policy = form_data['content_policy'] + if form_data['login_menu_enabled'] == 'True': + model.login_menu_enabled = True + else: + model.login_menu_enabled = False + if form_data.get('group') != "__None": + model.group_id = form_data.get('group') + the_result = { + "FILE_PATTERN": "Thumbnail file only 'jpeg', 'jpg', 'png' format.", + } + fp = request.files.get('thumbnail') + if '' != fp.filename: + directory = os.path.join( + current_app.instance_path, + current_app.config['WEKO_THEME_INSTANCE_DATA_DIR'], + 'c') + if not os.path.exists(directory): + os.makedirs(directory) + + ext = os.path.splitext(fp.filename)[1].lower() + allowed_extensions = {'.png', '.jpg', '.jpeg'} + if ext not in allowed_extensions: + raise ValidationError(the_result['FILE_PATTERN']) + filename = os.path.join( + directory, + model.id + '_' + fp.filename) + file_uri = '/data/' + 'c/' + model.id + '_' + fp.filename + fp.save(filename) + model.thumbnail_path = file_uri + + catalog_json = json.loads(form_data['catalog_data']) + flg, result_json = delete_empty(catalog_json['metainfo']) + if flg: + model.catalog_json = result_json['parentkey'] + else: + model.catalog_json = None + model.id_user = current_user.get_id() + model.cnri = None + + comm = Community.create( + community_id=model.id, + role_id=model.id_role, + id_user=model.id_user, + root_node_id=model.root_node_id, + group_id=model.group_id, + title=model.title, + description=model.description, + page=model.page, + curation_policy=model.curation_policy, + ranking=model.ranking, + fixed_points=model.fixed_points, + content_policy=model.content_policy, + login_menu_enabled=model.login_menu_enabled, + thumbnail_path=model.thumbnail_path, + catalog_json=model.catalog_json, + cnri=model.cnri + ) + db.session.commit() + + # get CNRI handle + if current_app.config.get('WEKO_HANDLE_ALLOW_REGISTER_CNRI'): + weko_handle = Handle() + url = request.url.split('/admin/')[0] + '/c/' + str(model.id) + credential = PIDClientCredentials.load_from_JSON( + current_app.config.get('WEKO_HANDLE_CREDS_JSON_PATH')) + hdl = credential.get_prefix() + '/c/' + str(model.id) + handle = weko_handle.register_handle(location=url, hdl=hdl) + if handle: + model_for_handle = self.get_one(model.id) + model_for_handle.cnri = WEKO_SERVER_CNRI_HOST_LINK + str(handle) + db.session.commit() + else: + current_app.logger.info('Cannot connect Handle server!') + + data = [ + { + "is_edit": False, + "page_id": 0, + "repository_id": model.id, + "title": "About", + "url": "/c/" + model.id + "/page/about", + "content": "", + "settings": "", + "multi_lang_data": {"en": "About"}, + "is_main_layout": False, + }, + { + "is_edit": False, + "page_id": 0, + "repository_id": model.id, + "title": "Editorial board", + "url": "/c/" + model.id + "/page/eb", + "content": "", + "settings": "", + "multi_lang_data": {"en": "Editorial board"}, + "is_main_layout": False, + }, + { + "is_edit": False, + "page_id": 0, + "repository_id": model.id, + "title": "OA Policy", + "url": "/c/" + model.id + "/page/oapolicy", + "content": "", + "settings": "", + "multi_lang_data": {"en": "OA Policy"}, + "is_main_layout": False, + } + ] + for page in data: + addPageResult = WidgetDesignPageServices.add_or_update_page(page) + if addPageResult['result'] == False: + current_app.logger.error(page['url'] + " page add failed.") + pageaddFlag = False + + if pageaddFlag == False: + return redirect(url_for('.index_view', pageaddFlag=pageaddFlag)) + else: + return redirect(url_for('.index_view')) + except Exception as e: + db.session.rollback() + return jsonify({"error": str(e)}), 400 + + else: + form.login_menu_enabled.data = 'False' + + return self.render( + "invenio_communities/admin/edit.html", + form=form, + jsonschema="/admin/community/jsonschema", + schemaform="/admin/community/schemaform", + pid=None, + record=None, + type = 'create', + + return_url=request.args.get('url'), + c_id=id, + ) + + @expose('/edit//', methods=['GET', 'POST']) + def edit_view(self, id): + model = self.get_one(id) + if model is None: + abort(404) + + form=super(CommunityModelView, self).edit_form(id) + form.id.data = model.id or '' + form.cnri.data = model.cnri or '' + form.title.data = model.title or '' + form.owner.data = model.owner or '' + form.index.data = model.index or '' + form.group.data = model.group or '' + form.description.data = model.description or '' + form.page.data = model.page or '' + form.curation_policy.data = model.curation_policy or '' + form.ranking.data = model.ranking or '0' + form.fixed_points.data = model.fixed_points or '0' + form.content_policy.data = model.content_policy or '' + if model.login_menu_enabled: + form.login_menu_enabled.data = 'True' + else: + form.login_menu_enabled.data = 'False' + + if(request.method == 'POST'): + form_data = request.form.to_dict() + try: + model.id_role = form_data['owner'] + model.root_node_id = form_data['index'] + model.title = form_data['title'] + model.description = form_data['description'] + model.page = form_data['page'] + model.curation_policy = form_data['curation_policy'] + model.ranking = form_data['ranking'] + model.fixed_points = form_data['fixed_points'] + model.content_policy = form_data['content_policy'] + if form_data['login_menu_enabled'] == 'True': + model.login_menu_enabled = True + else: + model.login_menu_enabled = False + if form_data.get('group') != "__None": + model.group_id = form_data.get('group') + the_result = { + "FILE_PATTERN": "Thumbnail file only 'jpeg', 'jpg', 'png' format.", + } + fp = request.files.get('thumbnail') + if '' != fp.filename: + directory = os.path.join( + current_app.instance_path, + current_app.config['WEKO_THEME_INSTANCE_DATA_DIR'], + 'c') + if not os.path.exists(directory): + os.makedirs(directory) + + ext = os.path.splitext(fp.filename)[1].lower() + allowed_extensions = {'.png', '.jpg', '.jpeg'} + if ext not in allowed_extensions: + raise ValidationError(the_result['FILE_PATTERN']) + filename = os.path.join( + directory, + model.id + '_' + fp.filename) + file_uri = '/data/' + 'c/' + model.id + '_' + fp.filename + if model.thumbnail_path: + currentfile = os.path.join( + current_app.instance_path, + current_app.config['WEKO_THEME_INSTANCE_DATA_DIR'], + 'c', + model.thumbnail_path[8:]) + try: + os.remove(currentfile) + current_app.logger.info(f"{currentfile} deleted.") + except Exception as e: + current_app.logger.error(f"file delete failed.: {e}") + fp.save(filename) + model.thumbnail_path = file_uri + + catalog_json = json.loads(form_data['catalog_data']) + flg, result_json = delete_empty(catalog_json['metainfo']) + if flg: + model.catalog_json = result_json['parentkey'] + else: + model.catalog_json = None + + # get CNRI handle + if current_app.config.get('WEKO_HANDLE_ALLOW_REGISTER_CNRI') and not model.cnri: + weko_handle = Handle() + credential = PIDClientCredentials.load_from_JSON( + current_app.config.get('WEKO_HANDLE_CREDS_JSON_PATH')) + url = request.url.split('/admin/')[0] + '/c/' + str(id) + hdl = credential.get_prefix() + '/c/' + str(id) + handle = weko_handle.register_handle(location=url, hdl=hdl) + if handle: + model.cnri = WEKO_SERVER_CNRI_HOST_LINK + str(handle) + else: + current_app.logger.info('Cannot connect Handle server!') + + db.session.commit() + return redirect(url_for('.index_view')) + except Exception as e: + db.session.rollback() + return jsonify({"error": str(e)}), 400 + + else: + # request method GET + record = {} + if model.catalog_json is not None: + record['parentkey'] = model.catalog_json + else: + record = None + return self.render( + "invenio_communities/admin/edit.html", + form=form, + jsonschema="/admin/community/jsonschema", + schemaform="/admin/community/schemaform", + pid=None, + record=record, + type = 'edit', + + return_url=request.args.get('url'), + c_id=id, + ) + + + @expose('/jsonschema', methods=['GET']) + def get_json_schema(self): + """Get json schema. + + :return: The json object. + """ + try: + json_schema = None + # cur_lang = current_i18n.language + + """Log error for output info of journal, level: ERROR, status code: 101, + content: Invalid setting file error""" + filepath = "schemas/jsonschema.json" + if (filepath + != filepath)\ + or (filepath + == "" + or (current_app.config[ + 'WEKO_INDEXTREE_JOURNAL_SCHEMA_JSON_FILE' + ] is None)): + current_app.logger.error( + '[{0}] Invalid setting file error'.format(101) + ) + + schema_file = os.path.join( + os.path.dirname(__file__), + filepath) + + json_schema = json.load(open(schema_file)) + if json_schema == {}: + return '{}' + + result = db.session.execute("SELECT schema FROM item_type_property WHERE id = 1057;").fetchone() + catalogschema = result[0] + tempschema = catalogschema["properties"] + keys_to_keep = [ 'catalog_contributors', 'catalog_identifiers', 'catalog_subjects', 'catalog_licenses', 'catalog_rights', 'catalog_access_rights'] + keys_to_remove = [k for k in tempschema.keys() if k not in keys_to_keep] + for key in keys_to_remove: + tempschema.pop(key) + + json_schema["properties"]["parentkey"]["items"]["properties"] = tempschema + + except BaseException: + current_app.logger.error( + "Unexpected error: {}".format(sys.exc_info())) + abort(500) + return jsonify(json_schema) + + @expose('/schemaform', methods=['GET']) + def get_schema_form(self): + """Get schema form. + + :return: The json object. + """ + try: + schema_form = None + cur_lang = current_i18n.language + + """Log error for output info of journal, level: ERROR, status code: 101, + content: Invalid setting file error.""" + + result = db.session.execute("SELECT forms FROM item_type_property WHERE id = 1057;").fetchone() + temp_schema_form = [result[0]] + + schema_form = temp_schema_form + + i = 1 + for elem in schema_form: + if 'title_i18n' in elem: + if cur_lang in elem['title_i18n']: + if len(elem['title_i18n'][cur_lang]) > 0: + elem['title'] = elem['title_i18n'][cur_lang] + if 'items' in elem: + for sub_elem in elem['items'][:]: + if sub_elem['key'] in ['parentkey[].catalog_contributors', 'parentkey[].catalog_identifiers', 'parentkey[].catalog_subjects', 'parentkey[].catalog_licenses', 'parentkey[].catalog_rights', 'parentkey[].catalog_access_rights']: + if 'title_i18n' in sub_elem: + if cur_lang in sub_elem['title_i18n']: + if len(sub_elem['title_i18n'] + [cur_lang]) > 0: + sub_elem['title'] = sub_elem['title_i18n'][ + cur_lang] + if 'items' in sub_elem: + for sub_sub_elem in sub_elem['items'][:]: + if 'title_i18n' in sub_sub_elem: + if cur_lang in sub_sub_elem['title_i18n']: + if len(sub_sub_elem['title_i18n'] + [cur_lang]) > 0: + sub_sub_elem['title'] = sub_sub_elem['title_i18n'][ + cur_lang] + if 'items' in sub_sub_elem: + for sub_sub_sub_elem in sub_sub_elem['items'][:]: + if 'title_i18n' in sub_sub_sub_elem: + if cur_lang in sub_sub_sub_elem['title_i18n']: + if len(sub_sub_sub_elem['title_i18n'] + [cur_lang]) > 0: + sub_sub_sub_elem['title'] = sub_sub_sub_elem['title_i18n'][ + cur_lang] + else: + elem['items'].remove(sub_elem) + + except BaseException: + current_app.logger.error( + "Unexpected error: {}".format(sys.exc_info())) + abort(500) + return jsonify(schema_form) + + def on_model_delete(self, model): + if model.cnri: + from .tasks import delete_handle + hdl = model.cnri.split(WEKO_SERVER_CNRI_HOST_LINK)[-1] + delete_handle.delay(hdl) + + if model.thumbnail_path: + currentfile = os.path.join( + current_app.instance_path, + current_app.config['WEKO_THEME_INSTANCE_DATA_DIR'], + 'c', + model.thumbnail_path[8:]) + try: + os.remove(currentfile) + current_app.logger.info(f"{currentfile} deleted.") + except Exception as e: + current_app.logger.error(f"file delete failed.: {e}") + def on_model_change(self, form, model, is_created): """Perform some actions before a model is created or updated. @@ -115,7 +560,16 @@ def _validate_input_id(self, field): form_args = { 'id': { 'validators': [_validate_input_id] - } + }, + 'group': { + 'allow_blank': False, + 'query_factory': lambda: db.session.query(Role).filter(Role.name.like("%_groups_%")).all(), + } + } + form_extra_fields = { + 'cnri': StringField(), + 'thumbnail': FileField(description='ファイルタイプ: JPG ,JPEG, PNG'), + 'login_menu_enabled': RadioField('login_menu_enabled', choices=[('False', 'Disabled'), ('True', 'Enabled')] ), } form_widget_args = { @@ -124,14 +578,18 @@ def _validate_input_id(self, field): 'maxlength': 100, } } + + @property + def can_create(self): + """Check permission for creating.""" + role_ids = get_user_role_ids() + return min(role_ids) <= \ + current_app.config['COMMUNITIES_LIMITED_ROLE_ACCESS_PERMIT'] def role_query_cond(self, role_ids): """Query conditions by role_id and user_id.""" if role_ids: - return or_( - Community.id_role.in_(role_ids), - Community.id_user == current_user.id - ) + return Community.group_id.in_(role_ids) def get_query(self): """Return a query for the model type. diff --git a/modules/invenio-communities/invenio_communities/alembic/1b352b00f1ed_add_columns.py b/modules/invenio-communities/invenio_communities/alembic/1b352b00f1ed_add_columns.py new file mode 100644 index 0000000000..b745c476da --- /dev/null +++ b/modules/invenio-communities/invenio_communities/alembic/1b352b00f1ed_add_columns.py @@ -0,0 +1,32 @@ +# +# This file is part of Invenio. +# Copyright (C) 2016-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. + +"""Add columns""" + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '1b352b00f1ed' +down_revision = 'd2d56dc5e385' +branch_labels = () +depends_on = None + + +def upgrade(): + """Upgrade database.""" + op.add_column('communities_community', sa.Column('content_policy', sa.Text(), nullable=True)) + op.add_column('communities_community', sa.Column('group_id', sa.Integer(), nullable=True)) + op.create_foreign_key(op.f('fk_communities_community_group_id_accounts_role'), 'communities_community', 'accounts_role', ['group_id'], ['id']) + + +def downgrade(): + """Downgrade database.""" + op.drop_constraint(op.f('fk_communities_community_group_id_accounts_role'), 'communities_community', type_='foreignkey') + op.drop_column('communities_community', 'group_id') + op.drop_column('communities_community', 'content_policy') diff --git a/modules/invenio-communities/invenio_communities/alembic/d2d56dc5e385_add_column.py b/modules/invenio-communities/invenio_communities/alembic/d2d56dc5e385_add_column.py new file mode 100644 index 0000000000..e5422a09be --- /dev/null +++ b/modules/invenio-communities/invenio_communities/alembic/d2d56dc5e385_add_column.py @@ -0,0 +1,34 @@ +# +# This file is part of Invenio. +# Copyright (C) 2016-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. + +"""add_column""" + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects.postgresql import JSONB + +# revision identifiers, used by Alembic. +revision = 'd2d56dc5e385' +down_revision = '2d9884d0e3fa' +branch_labels = () +depends_on = None + + +def upgrade(): + """Upgrade database.""" + op.add_column('communities_community', sa.Column('thumbnail_path', sa.Text(), nullable=True)) + op.add_column('communities_community', sa.Column('login_menu_enabled', sa.Boolean(), nullable=False, server_default=sa.sql.expression.false())) + op.add_column('communities_community', sa.Column('catalog_json', JSONB, nullable=True)) + op.add_column('communities_community', sa.Column('cnri', sa.Text(), nullable=True)) + + +def downgrade(): + """Downgrade database.""" + op.drop_column('communities_community', 'thumbnail_path') + op.drop_column('communities_community', 'login_menu_enabled') + op.drop_column('communities_community', 'catalog_json') + op.drop_column('communities_community', 'cnri') diff --git a/modules/invenio-communities/invenio_communities/config.py b/modules/invenio-communities/invenio_communities/config.py index 62a9ebcde9..400a00f426 100644 --- a/modules/invenio-communities/invenio_communities/config.py +++ b/modules/invenio-communities/invenio_communities/config.py @@ -164,3 +164,9 @@ COMMUNITIES_LIMITED_ROLE_ACCESS_PERMIT = 2 """Allowed Role's id higher than this number full access to list Indexes.""" + +COMMUNITIES_LIST_THUMBNAIL_WIDTH = 256 +"""community thumbnail width in community list.""" + +COMMUNITIES_LIST_THUMBNAIL_HEIGHT = 256 +"""community thumbnail height in community list.""" diff --git a/modules/invenio-communities/invenio_communities/models.py b/modules/invenio-communities/invenio_communities/models.py index c3dd98e55e..c4c768d320 100644 --- a/modules/invenio-communities/invenio_communities/models.py +++ b/modules/invenio-communities/invenio_communities/models.py @@ -45,7 +45,9 @@ InclusionRequestObsoleteError from .signals import inclusion_request_created from .utils import save_and_validate_logo - +from sqlalchemy.dialects import postgresql +from sqlalchemy_utils.types import JSONType +from sqlalchemy import cast, String class InclusionRequest(db.Model, Timestamp): """Association table for Community and Record models. @@ -221,6 +223,31 @@ class Community(db.Model, Timestamp): deleted_at = db.Column(db.DateTime, nullable=True, default=None) """Time at which the community was soft-deleted.""" + thumbnail_path = db.Column(db.Text, nullable=True, default='') + """thumbnail_path.""" + + login_menu_enabled = db.Column(db.Boolean, nullable=False, default=False) + """login_menu enabled or Disabled.""" + + catalog_json = db.Column( + db.JSON().with_variant( + postgresql.JSONB(none_as_null=True), + 'postgresql', + ).with_variant( + JSONType(), + 'sqlite', + ).with_variant( + JSONType(), + 'mysql', + ), + default=[], + nullable=True + ) + """catalog.""" + + cnri = db.Column(db.Text, nullable=True, default=None) + """thumbnail_path.""" + # root_node_id = db.Column(db.Text, nullable=False, default='') root_node_id = db.Column( @@ -228,8 +255,17 @@ class Community(db.Model, Timestamp): db.ForeignKey(Index.id), nullable=False ) - """Id of Root Node""" + + content_policy = db.Column(db.Text, nullable=True, default='') + """Community content policy.""" + + group_id = db.Column( + db.Integer, + db.ForeignKey(Role.id), + nullable=True + ) + """Group of the community.""" # # Relationships @@ -248,6 +284,9 @@ class Community(db.Model, Timestamp): backref='index', foreign_keys=[root_node_id]) """Relation to the owner (Index) of the community.""" + + group = db.relationship(Role, backref='group', + foreign_keys=[group_id]) def __repr__(self): """String representation of the community object.""" @@ -293,6 +332,15 @@ def get_by_user(cls, role_ids, with_deleted=False): return query.order_by(db.asc(Community.title)) + @classmethod + def get_by_root_node_id(cls, root_node_id, with_deleted=False): + """Get communities by root_node_id.""" + q = cls.query.filter_by(root_node_id=root_node_id) + if not with_deleted: + q = q.filter(cls.deleted_at.is_(None)) + + return q.order_by(db.asc(Community.title)).all() + @classmethod def filter_communities(cls, p, so, with_deleted=False): """Search for communities. @@ -314,6 +362,7 @@ def filter_communities(cls, p, so, with_deleted=False): cls.id.ilike('%' + p + '%'), cls.title.ilike('%' + p + '%'), cls.description.ilike('%' + p + '%'), + cast(cls.catalog_json, String).ilike('%' + p + '%'), )) if so in current_app.config['COMMUNITIES_SORTING_OPTIONS']: @@ -322,6 +371,12 @@ def filter_communities(cls, p, so, with_deleted=False): else: query = query.order_by(db.desc(cls.ranking)) return query + + @classmethod + def get_repositories_by_user(cls, user): + """Get repository ids for user.""" + role_ids = [role.id for role in user.roles] + return Community.query.filter(Community.group_id.in_(role_ids)).all() def add_record(self, record): """Add a record to the community. @@ -411,6 +466,14 @@ def undelete(self): else: self.deleted_at = None + def to_dict(self): + """Convert the Community object to a dictionary. + + Returns: + dict: Dictionary representation of the Community object. + """ + return {column.name: getattr(self, column.name) for column in self.__table__.columns} + @property def is_deleted(self): """Return whether given community is marked for deletion.""" diff --git a/modules/invenio-communities/invenio_communities/schemas/jsonschema.json b/modules/invenio-communities/invenio_communities/schemas/jsonschema.json new file mode 100644 index 0000000000..22bd3dd199 --- /dev/null +++ b/modules/invenio-communities/invenio_communities/schemas/jsonschema.json @@ -0,0 +1 @@ +{"type": "object", "$schema": "http://json-schema.org/draft-04/schema#", "description": "", "required": ["pubdate"], "properties": { "parentkey": {"type": "array", "title": "カタログ", "maxItems": 9999, "minItems": 1, "items": {"type": "object", "properties": {} }}}} diff --git a/modules/invenio-communities/invenio_communities/static/js/invenio_communities/app.js b/modules/invenio-communities/invenio_communities/static/js/invenio_communities/app.js new file mode 100644 index 0000000000..95115060bd --- /dev/null +++ b/modules/invenio-communities/invenio_communities/static/js/invenio_communities/app.js @@ -0,0 +1,323 @@ +(function (angular) { + // Bootstrap it! + angular.element(document).ready(function() { + angular.module('wekoRecords.controllers', []); + + function WekoRecordsCtrl($scope, $rootScope, $modal, InvenioRecordsAPI){ + $rootScope.$on('invenio.records.loading.stop', function (ev) { + setTimeout(function () { + let model = $rootScope.recordsVM.invenioRecordsModel; + CustomBSDatePicker.setDataFromFieldToModel(model, true); + }, 1000); + }); + $scope.saveData = function(){ + + const form1Data = new FormData(document.getElementsByClassName('admin-form form-horizontal')[0]); + + var metainfo = { 'metainfo': $rootScope.recordsVM.invenioRecordsModel }; + form2Data = new FormData(); + form2Data.append('catalog_data', JSON.stringify(metainfo)); + + for (const [key, value] of form2Data) { + form1Data.append(key, value); + } + + fetch('', { + method: 'POST', + body: form1Data, + }).then(response => { + if (response.ok) { + const url = new URL(response.url); + let params = url.searchParams; + if (params.get('pageaddFlag') == 'False') { + alert('The community was successfully saved, but there are additional pages that failed to be created.'); + window.location.href = url.pathname; + } + else { + alert('Successfully saved.'); + window.location.href = response.url; + } + } else { + return response.json().then(err => { throw new Error(err.error); }); + } + }).catch(error => { + alert("An error has occurred. : " + error.message); // エラーメッセージを表示 + }); + } + } + + // Inject depedencies + WekoRecordsCtrl.$inject = [ + '$scope', + '$rootScope', + '$modal', + 'InvenioRecordsAPI', + ]; + angular.module('wekoRecords.controllers') + .controller('WekoRecordsCtrl', WekoRecordsCtrl); + + var ModalInstanceCtrl = function($scope, $modalInstance, items) { + $scope.items = items; + $scope.searchKey = ''; + $scope.selected = { + item : $scope.items[0] + }; + $scope.ok = function() { + $modalInstance.close($scope.selected); + }; + $scope.cancel = function() { + $modalInstance.dismiss('cancel'); + }; + $scope.search = function() { + $scope.items.push($scope.searchKey); + } + }; + + angular.module('wekoRecords', [ + 'invenioRecords', + 'wekoRecords.controllers', + ]); + + angular.bootstrap( + document.getElementById('weko-records'), [ + 'wekoRecords', 'invenioRecords', 'schemaForm', 'mgcrea.ngStrap', + 'mgcrea.ngStrap.modal', 'pascalprecht.translate', 'ui.sortable', + 'ui.select', 'mgcrea.ngStrap.select', 'mgcrea.ngStrap.datepicker', + 'mgcrea.ngStrap.helpers.dateParser', 'mgcrea.ngStrap.tooltip', + 'invenioFiles' + ] + ); +}); +})(angular); + + +/** + * Custom bs-datepicker. + * Default bs-datepicker: just support one pattern for input. + * Custom bs-datepicker: support validate three pattern. + * Used way: + * templateUrl: /static/templates/weko_deposit/datepicker_multi_format.html + * customFormat: enter your pattern. + * if it none, pattern are yyyy-MM-dd, yyyy-MM, yyyy. +*/ +var Pattern = { + yyyy: '\\d{4}', + MM: '(((0)[1-9])|((1)[0-2]))', + dd: '([0-2][0-9]|(3)[0-1])', + sep: '(-)' +} +var Format = { + yyyyMMdd: '^(' + Pattern.yyyy + Pattern.sep + + Pattern.MM + Pattern.sep + Pattern.dd + ')$', + yyyyMM: '^(' + Pattern.yyyy + Pattern.sep + Pattern.MM + ')$', + yyyy: '^(' + Pattern.yyyy + ')$', +} +var CustomBSDatePicker = { + option: { + element: undefined, + defaultFormat: Format.yyyyMMdd + '|' + Format.yyyyMM + '|' + Format.yyyy, + cls: 'multi_date_format' + }, + /** + * Clear validate status for this element. + */ + init: function () { + let $element = $(CustomBSDatePicker.option.element); + let $this_parent = $element.parent().parent(); + $element.removeClass('ng-invalid ng-invalid-date ng-invalid-parse'); + $element.next().next().addClass('hide'); + $this_parent.removeClass('has-error'); + }, + /** + * Get format from defined user on form schema. + * If user don't defined, this pattern get default pattern. + * Default pattern: option.defaultFormat. + * @return {String} return pattern. + */ + getPattern: function () { + let def_pattern = CustomBSDatePicker.option.defaultFormat; + let $element = $(CustomBSDatePicker.option.element); + let pattern = $element.data('custom-format'); + return (pattern.length == 0) ? def_pattern : pattern; + }, + /** + * Check data input valid with defined pattern. + * @return {Boolean} return true if value matched + */ + isMatchRegex: function () { + let $element = $(CustomBSDatePicker.option.element); + let val = $element.val(); + let pattern = CustomBSDatePicker.getPattern(); + let reg = new RegExp(pattern); + return reg.test(val); + }, + /** + * Check input required. + * @return {Boolean} return true if input required + */ + isRequired: function () { + let $lement = $(CustomBSDatePicker.option.element); + let $this_parent = $lement.parent().parent(); + let label = $this_parent.find('label'); + return label.hasClass('field-required'); + }, + /** + * Get the number of days in any particular month + * @param {number} m The month (valid: 0-11) + * @param {number} y The year + * @return {number} The number of days in the month + */ + daysInMonth: function (m, y) { + switch (m) { + case 1: + return (y % 4 == 0 && y % 100) || y % 400 == 0 ? 29 : 28; + case 8: case 3: case 5: case 10: + return 30; + default: + return 31 + } + }, + /** + * Check if a date is valid + * @param {number} d The day + * @param {number} m The month + * @param {number} y The year + * @return {Boolean} Returns true if valid + */ + isValidDate: function (d, m, y) { + let month = parseInt(m, 10) - 1; + let checkMonth = month >= 0 && month < 12; + let checkDay = d > 0 && d <= CustomBSDatePicker.daysInMonth(month, y); + return checkMonth && checkDay; + }, + /** + * Check all validate for this. + * All validation valid => return true. + * @return {Boolean} Returns true if valid + */ + isValidate: function () { + let $element = $(CustomBSDatePicker.option.element); + let val = $element.val(); + if (val.length == 0) { + //Required input invalid. + if (CustomBSDatePicker.isRequired()) return false; + } else { + //Data input is not match with defined pattern. + if (!CustomBSDatePicker.isMatchRegex()) return false; + //Check day by month and year. + let arr = val.split('-'); + if (arr.length == 3 && !CustomBSDatePicker.isValidDate(arr[2], arr[1], arr[0])) return false; + } + return true; + }, + /** + * Check validate and apply css for this field. + */ + validate: function () { + let $element = $(CustomBSDatePicker.option.element); + let $this_parent = $element.parent().parent(); + if (!CustomBSDatePicker.isValidate()) { + $element.next().next().removeClass('hide'); + $this_parent.addClass('has-error'); + $element.addClass('ng-invalid'); + } else { + $element.removeClass('ng-invalid'); + $this_parent.removeClass('has-error'); + } + }, + /** + * This is mean function in order to validate. + * @param {[type]} element date field + */ + process: function (element) { + CustomBSDatePicker.option.element = element; + CustomBSDatePicker.init(); + CustomBSDatePicker.validate(); + }, + /** + * Init attribute of model object if them undefine. + * @param {[object]} model + * @param {[object]} element is date input control. + */ + initAttributeForModel: function (model, element) { + if($(element).val().length == 0) return; + let ng_model = $(element).attr('ng-model').replace(/']/g, ''); + let arr = ng_model.split("['"); + //Init attribute of model object if them undefine. + let str_code = ''; + $.each(arr, function (ind_01, val_02) { + str_code += (ind_01 == 0) ? val_02 : "['" + val_02 + "']"; + let chk_str_code = ''; + if (ind_01 != arr.length - 1) { + chk_str_code = "if(!" + str_code + ") " + str_code + "={};"; + } + eval(chk_str_code); + }); + }, + /** + * Excute this function before 'Save' and 'Next' processing + * Get data from fields in order to fill to model. + * @param {[object]} model + * @param {[Boolean]} reverse + */ + setDataFromFieldToModel: function (model, reverse) { + let cls = CustomBSDatePicker.option.cls; + let element_arr = $('.' + cls); + $.each(element_arr, function (ind, val) { + CustomBSDatePicker.initAttributeForModel(model, val); + if (reverse) { + //Fill data from model to fields + str_code = "$(val).val(" + $(val).attr('ng-model') + ")"; + try { + eval(str_code); + } catch (e) { + // If the date on model is undefined, we can safetly ignore it. + if (!e instanceof TypeError) { + throw e; + } + } + } else { + //Fill data from fields to model + str_code = 'if ($(val).val().length != 0) {' + $(val).attr('ng-model') + '=$(val).val()}'; + eval(str_code); + } + }); + }, + /** + * Get date fields name which invalid. + * @return {array} Returns name list. + */ + getInvalidFieldNameList: function () { + let cls = CustomBSDatePicker.option.cls; + let element_arr = $('.' + cls); + let result = []; + $.each(element_arr, function (ind, val) { + let $element = $(val); + let $parent = $element.parent().parent(); + if ($parent.hasClass('has-error')) { + let name = $element.attr('name'); + let label = $("label[for=" + name + "]").text().trim(); + result.push(label); + } + }); + return result; + }, + /** + * If input empty, this attribute delete. + * Fix bug: not enter data for date field. + */ + removeLastAttr: function(model){ + let cls = CustomBSDatePicker.option.cls; + let element_arr = $('.' + cls); + $.each(element_arr, function (ind, val) { + if($(val).val().length > 0){ + CustomBSDatePicker.initAttributeForModel(model, val); + let ng_model = $(val).attr('ng-model'); + let last_index = ng_model.lastIndexOf('['); + let previous_attr = ng_model.substring(0, last_index); + let str_code = "if("+ng_model+"==''){"+previous_attr+"={}}"; + eval(str_code); + } + }); + } +} diff --git a/modules/invenio-communities/invenio_communities/static/js/invenio_communities/communities_list.js b/modules/invenio-communities/invenio_communities/static/js/invenio_communities/communities_list.js new file mode 100644 index 0000000000..c9c7e28a1b --- /dev/null +++ b/modules/invenio-communities/invenio_communities/static/js/invenio_communities/communities_list.js @@ -0,0 +1,44 @@ +function printCatalogInfo(data) { + if (data == "None" || data == "[]") { + console.log("data is null"); + return; + } + console.log(data); + + string_contributors = "" + string_subjects = "" + const cleanedString = data.replaceAll("'", "\""); + jsonString = "\{\"catalog\":" + cleanedString + "\}"; + const json = JSON.parse(jsonString); + + console.log(json.catalog); + json.catalog.forEach(item => { + if ("catalog_subjects" in item) { + item.catalog_subjects.forEach(subject => { + string_subjects = subject.catalog_subject + ", "; + string_subjects += subject.catalog_subject_uri + ", "; + string_subjects += subject.catalog_subject_scheme + ", "; + string_subjects += subject.catalog_subject_language; + }); + document.write("

" + string_subjects + "

"); + } + if ("catalog_contributors" in item) { + item.catalog_contributors.forEach(contributor => { + if ("contributor_type" in contributor) { + string_contributors = contributor.contributor_type; + } + if ("contributor_names" in contributor) { + contributor.contributor_names.forEach(name => { + if ("contributor_name" in name) { + string_contributors += ", " + name.contributor_name; + } + if ("contributor_name_language" in name) { + string_contributors += ", " + name.contributor_name_language; + } + }); + } + document.write("

" + string_contributors + "

"); + }); + } + }); +} diff --git a/modules/invenio-communities/invenio_communities/static/scss/invenio_communities/extra_fields.css b/modules/invenio-communities/invenio_communities/static/scss/invenio_communities/extra_fields.css new file mode 100644 index 0000000000..4c48520aea --- /dev/null +++ b/modules/invenio-communities/invenio_communities/static/scss/invenio_communities/extra_fields.css @@ -0,0 +1,13 @@ +#login_menu_enabled { + border: none; + box-shadow: none; + display: flex; + list-style: none; + padding: 5px 0 0 0; + gap: 20px; +} + +#thumbnail { + border: none; + box-shadow: none; +} diff --git a/modules/invenio-communities/invenio_communities/tasks.py b/modules/invenio-communities/invenio_communities/tasks.py index e75186849b..e3693a84c8 100644 --- a/modules/invenio-communities/invenio_communities/tasks.py +++ b/modules/invenio-communities/invenio_communities/tasks.py @@ -29,9 +29,11 @@ from datetime import datetime from celery import shared_task +from flask import current_app from invenio_db import db from .models import Community, InclusionRequest +from weko_handle.api import Handle # @shared_task(ignore_result=True) @@ -53,3 +55,12 @@ def delete_expired_requests(): db.session.commit() except Exception as ex: db.session.rollback() + +@shared_task(ignore_result=True) +def delete_handle(hdl): + weko_handle = Handle() + handle = weko_handle.delete_handle(hdl) + if handle: + current_app.logger.info(hdl + ' handle deleted successfully.') + else: + current_app.logger.info( hdl + " handle delete failed.") diff --git a/modules/invenio-communities/invenio_communities/templates/invenio_communities/admin/edit.html b/modules/invenio-communities/invenio_communities/templates/invenio_communities/admin/edit.html index eff5dd9bb7..dadeaa0101 100644 --- a/modules/invenio-communities/invenio_communities/templates/invenio_communities/admin/edit.html +++ b/modules/invenio-communities/invenio_communities/templates/invenio_communities/admin/edit.html @@ -2,6 +2,19 @@ {% import 'admin/lib.html' as lib with context %} {% from 'admin/lib.html' import extra with context %} {# backward compatible #} +{%- block css %} + {% assets "invenio_deposit_css" %}{% endassets %} + {{ super() }} + +{%- endblock css %} + +{%- block javascript %} + {% assets "invenio_deposit_dependencies_js" %}{% endassets %} + {{ super() }} + {% assets "invenio_deposit_js" %}{% endassets %} + +{%- endblock javascript %} + {% block head %} {{ super() }} {{ lib.form_css() }} @@ -13,33 +26,114 @@
  • {{ _gettext('List') }}
  • - {%- if admin_view.can_create -%} -
  • - {{ _gettext('Create') }} -
  • + {% if type == 'edit' %} + {%- if admin_view.can_create -%} +
  • + {{ _gettext('Create') }} +
  • + {%- endif -%} + {% else %} +
  • + {{ _gettext('Create') }} +
  • {%- endif -%} -
  • - {{ _gettext('Edit') }} -
  • - {%- if admin_view.can_view_details -%} -
  • - {{ _gettext('Details') }} -
  • + {% if type == 'edit' %} +
  • + {{ _gettext('Edit') }} +
  • + {%- if admin_view.can_view_details -%} +
  • + {{ _gettext('Details') }} +
  • + {%- endif -%} {%- endif -%} {% endblock %} {% block edit_form %} - {{ lib.render_form(form, return_url, extra(), form_opts) }} + {% call lib.form_tag(None) %} + {{ lib.render_form_fields([form.id]) }} + {{ lib.render_form_fields([form.cnri]) }} + {{ lib.render_form_fields([form.owner]) }} + {{ lib.render_form_fields([form.index]) }} + {{ lib.render_form_fields([form.group]) }} + {{ lib.render_form_fields([form.title]) }} + {{ lib.render_form_fields([form.description]) }} + {{ lib.render_form_fields([form.page]) }} + {{ lib.render_form_fields([form.curation_policy]) }} + {{ lib.render_form_fields([form.ranking]) }} + {{ lib.render_form_fields([form.fixed_points]) }} + {{ lib.render_form_fields([form.content_policy]) }} + {{ lib.render_form_fields([form.login_menu_enabled]) }} + {{ lib.render_form_fields([form.thumbnail]) }} + {% endcall %} +
    +
    + + + + + +
    + + + +
    +
    + + + {{ _('Cancel') }} + +
    +
    +
    +
    +
    +
    {% endblock %} {% endblock %} {% block tail %} {{ super() }} {{ lib.form_js() }} - + {% if type == 'edit' %} + + {% else %} + + {% endif %} {% endblock %} diff --git a/modules/invenio-communities/invenio_communities/templates/invenio_communities/communities_list_body.html b/modules/invenio-communities/invenio_communities/templates/invenio_communities/communities_list_body.html index a31af4fe32..e4da1479c8 100644 --- a/modules/invenio-communities/invenio_communities/templates/invenio_communities/communities_list_body.html +++ b/modules/invenio-communities/invenio_communities/templates/invenio_communities/communities_list_body.html @@ -17,6 +17,9 @@ # Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, # MA 02111-1307, USA. #} +{%- block javascript %} + +{%- endblock javascript %}
    @@ -85,11 +88,21 @@

    {{ featured_community.title }}

    {% if obj %}
    -

    - {{ obj.title }} -

    -
    +
    + {% if obj.thumbnail_path is not none and obj.thumbnail_path != "" %} + + {% endif %} +

    + {{ obj.title }} +

    +
    +

    {{ obj.description|striptags|truncate }}

    +
    {% endif %} diff --git a/modules/invenio-communities/invenio_communities/templates/invenio_communities/community_base.html b/modules/invenio-communities/invenio_communities/templates/invenio_communities/community_base.html index d2685786d5..e9f1b77fdf 100644 --- a/modules/invenio-communities/invenio_communities/templates/invenio_communities/community_base.html +++ b/modules/invenio-communities/invenio_communities/templates/invenio_communities/community_base.html @@ -23,7 +23,7 @@ {% extends config.SEARCH_UI_BASE_TEMPLATE %} -{%- from "invenio_communities/macros.html" import community_header, community_footer %} +{%- from "invenio_communities/macros.html" import community_header, community_footer with context %} {% set title = community.title + " | " + config.THEME_SITENAME %} diff --git a/modules/invenio-communities/invenio_communities/templates/invenio_communities/content_policy.html b/modules/invenio-communities/invenio_communities/templates/invenio_communities/content_policy.html new file mode 100644 index 0000000000..0ac7bbc8ea --- /dev/null +++ b/modules/invenio-communities/invenio_communities/templates/invenio_communities/content_policy.html @@ -0,0 +1,76 @@ +{# + # This file is part of WEKO3. + # Copyright (C) 2017 National Institute of Informatics. + # + # WEKO3 is free software; you can redistribute it + # and/or modify it under the terms of the GNU General Public License as + # published by the Free Software Foundation; either version 2 of the + # License, or (at your option) any later version. + # + # WEKO3 is distributed in the hope that it will be + # useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + # General Public License for more details. + # + # You should have received a copy of the GNU General Public License + # along with WEKO3; if not, write to the + # Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, + # MA 02111-1307, USA. + #} + + {%- extends config.BASE_PAGE_TEMPLATE %} + + {%- from "invenio_communities/macros.html" import community_header with context %} + + {%- block css %} + {{ super() }} + {% assets "invenio_search_ui_search_css" %}{% endassets %} + {% assets "weko_search_ui_css" %}{% endassets %} + {%- endblock css %} + + {%- block javascript %} + {{ super() }} + {% assets "weko_theme_js_treeview" %}{% endassets %} + {% assets "weko_theme_js_top_page" %}{% endassets %} + {% assets "weko_theme_js_detail_search" %}{% endassets %} + {% assets "invenio_search_ui_search_js" %}{% endassets %} + {% assets "weko_search_ui_dependencies_js" %}{% endassets %} + {% assets "weko_search_ui_js" %}{% endassets %} + {% assets "weko_theme_js_widget" %}{% endassets %} + {%- endblock javascript %} + + {%- block page_body_tabs %} + {% from "weko_theme/macros/tabs_selector.html" import tabs_selector with context %} + {{ tabs_selector('content_policy',community_id) }} + {%- endblock page_body_tabs%} + + {% block page_body %} +
    + + {{ community_header(community, subtitle='') }} + + {%- from "weko_theme/macros/footer-community.html" import community_footer_widget %} + {{ community_footer_widget(render_widgets, community, link=False, subtitle='') }} +
    +
    + {% endblock page_body %} + + {%- block page_body_main %} +
    +
    +

    コンテンツポリシー

    +
    +
    +
    + {% if community.content_policy %} + {% for line in community.content_policy.split('\n') %} +

    {{ line|e }}

    + {% endfor %} + {% endif %} +
    +
    + {%- endblock page_body_main %} diff --git a/modules/invenio-communities/invenio_communities/templates/invenio_communities/curate.html b/modules/invenio-communities/invenio_communities/templates/invenio_communities/curate.html index fd3f7b0e4d..5c6e109ea3 100644 --- a/modules/invenio-communities/invenio_communities/templates/invenio_communities/curate.html +++ b/modules/invenio-communities/invenio_communities/templates/invenio_communities/curate.html @@ -23,7 +23,7 @@ {% extends config.SEARCH_UI_SEARCH_TEMPLATE %} -{%- from "invenio_communities/macros.html" import community_header, community_footer %} +{%- from "invenio_communities/macros.html" import community_header, community_footer with context %} diff --git a/modules/invenio-communities/invenio_communities/templates/invenio_communities/macros.html b/modules/invenio-communities/invenio_communities/templates/invenio_communities/macros.html index c0d530b0a4..e4f0989661 100644 --- a/modules/invenio-communities/invenio_communities/templates/invenio_communities/macros.html +++ b/modules/invenio-communities/invenio_communities/templates/invenio_communities/macros.html @@ -21,18 +21,38 @@ # as an Intergovernmental Organization or submit itself to any jurisdiction. #} {% macro community_header(community, link=True, subtitle=None) %} + diff --git a/modules/weko-search-ui/weko_search_ui/static/templates/weko_search_ui/itemlist.html b/modules/weko-search-ui/weko_search_ui/static/templates/weko_search_ui/itemlist.html index c73e2439a6..1d2664f4f7 100644 --- a/modules/weko-search-ui/weko_search_ui/static/templates/weko_search_ui/itemlist.html +++ b/modules/weko-search-ui/weko_search_ui/static/templates/weko_search_ui/itemlist.html @@ -18,11 +18,11 @@ alt="{{record.metadata._thumbnail.thumbnail_label}}" width="{{record.metadata._thumbnail.thumbnail_width}}"> - + {{ author.creatorNames.creatorName|escapeTitle }} {{ author.familyNames.familyName|escapeTitle }}, {{ author.givenNames.givenName|escapeTitle }} - + @@ -65,51 +65,78 @@
    {{ record | json }}
    --> -
    - -
    - -
    - - ● ● ● - +
    + +
    +
    +

    Description Type : {{ description.subitem_description_type|escapeTitle }}

    +

    Description : {{ description.subitem_description|escapeTitle }}

    +

    Language : {{ description.subitem_description_language|escapeTitle }}

    - -
    - -
    - - ● ● ● - + + + - -
    - -
    - - ● ● ● - + + +
    +
    +

    Relation Type : {{ reference.subitem_relation_type|escapeTitle }}

    +

    Identifier Type : {{ reference.subitem_relation_type_id.subitem_relation_type_select|escapeTitle }}

    +

    Identifier : {{ reference.subitem_relation_type_id.subitem_relation_type_id_text|escapeTitle }}

    +
    +

    Relation Name : {{ relation_name.subitem_relation_name_text|escapeTitle }}

    +

    Language : {{ relation_name.subitem_relation_name_language|escapeTitle }}

    +
    diff --git a/modules/weko-search-ui/weko_search_ui/static/templates/weko_search_ui/itemtablecontents.html b/modules/weko-search-ui/weko_search_ui/static/templates/weko_search_ui/itemtablecontents.html index 3311ade369..c13bfcb6fa 100644 --- a/modules/weko-search-ui/weko_search_ui/static/templates/weko_search_ui/itemtablecontents.html +++ b/modules/weko-search-ui/weko_search_ui/static/templates/weko_search_ui/itemtablecontents.html @@ -11,61 +11,168 @@ {{record.metadata.heading=vm.invenioSearchResults.hits.hits[$index-1].metadata.heading;""}} -
    -
    -
    - -
    -
    - + + {{date.subitem_date_issued_type}}/{{date.subitem_date_issued_datetime}}; + +
    + + + + + +

    + {{record.metadata.pageStart[0]}} - {{record.metadata.pageEnd[0]}} +

    +

    + {{record.metadata.pageStart[0]}} + {{record.metadata.pageEnd[0]}} +

    +
    + +

    +
    + + +
    - -
    -
    -
    {{ record | json }}
    -
    -
    - --> -
    + +
    + + +
    diff --git a/modules/weko-search-ui/weko_search_ui/templates/weko_search_ui/admin/item_management_display.html b/modules/weko-search-ui/weko_search_ui/templates/weko_search_ui/admin/item_management_display.html index 4af3c5eea0..75d0aa6ba0 100644 --- a/modules/weko-search-ui/weko_search_ui/templates/weko_search_ui/admin/item_management_display.html +++ b/modules/weko-search-ui/weko_search_ui/templates/weko_search_ui/admin/item_management_display.html @@ -20,7 +20,7 @@ {%- extends admin_base_template %} -{%- from "invenio_communities/macros.html" import community_header %} +{%- from "invenio_communities/macros.html" import community_header with context %} {%- block css %} {{ super() }} diff --git a/modules/weko-search-ui/weko_search_ui/templates/weko_search_ui/search.html b/modules/weko-search-ui/weko_search_ui/templates/weko_search_ui/search.html index aa521279eb..7a3702759c 100644 --- a/modules/weko-search-ui/weko_search_ui/templates/weko_search_ui/search.html +++ b/modules/weko-search-ui/weko_search_ui/templates/weko_search_ui/search.html @@ -20,7 +20,7 @@ {% extends config.WEKO_SEARCH_UI_BASE_PAGE_TEMPLATE %} -{%- from "invenio_communities/macros.html" import community_header %} +{%- from "invenio_communities/macros.html" import community_header with context %} {%- block css %} {{ super() }} diff --git a/modules/weko-search-ui/weko_search_ui/utils.py b/modules/weko-search-ui/weko_search_ui/utils.py index f49e263318..088d75010a 100644 --- a/modules/weko-search-ui/weko_search_ui/utils.py +++ b/modules/weko-search-ui/weko_search_ui/utils.py @@ -83,6 +83,7 @@ check_index_permissions, check_restrict_doi_with_indexes, ) +from weko_index_tree.models import Index from weko_indextree_journal.api import Journals from weko_records.api import FeedbackMailList, RequestMailList, ItemTypeNames, ItemTypes, Mapping from weko_records.models import ItemMetadata @@ -297,7 +298,8 @@ def get_journal_info(index_id=0): data = res[0] val = title.get(cur_lang) + "{0}{1}".format(": ", data) result.update({value["key"]: val}) - open_search_uri = request.host_url + journal.get("title_url") + index = Index.get_index_by_id(index_id) + open_search_uri = index.index_url result.update({"openSearchUrl": open_search_uri}) except BaseException: @@ -3590,8 +3592,28 @@ def _get_item_type_list(item_type_id): current_app.logger.error(ex) return item_types - def _get_export_data(export_path, item_types, retrys, fromid="", toid="", retry_info={}): + def _get_index_id_list(user_id): + """Get index id list.""" + from invenio_accounts.models import User + from invenio_communities.models import Community + from weko_index_tree.api import Indexes + + if not user_id: + return [] + user = User.query.get(user_id) + if any(role.name in current_app.config['WEKO_PERMISSION_SUPER_ROLE_USER'] for role in user.roles): + return None + else: + index_id_list = [] + repositories = Community.get_repositories_by_user(user) + for repository in repositories: + index = Indexes.get_child_list_recursive(repository.root_node_id) + index_id_list.extend(index) + return index_id_list + + def _get_export_data(export_path, item_types, retrys, fromid="", toid="", retry_info={}, user_id=None): try: + index_id_list = _get_index_id_list(user_id) for it in item_types.copy(): item_type_id = it[0] item_type_name = it[1] @@ -3666,10 +3688,16 @@ def _get_export_data(export_path, item_types, retrys, fromid="", toid="", retry_ if len(recids) == 0: item_types.remove(it) continue - - record_ids = [(recid.pid_value, recid.object_uuid) + + if index_id_list is None: + 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]] + else: + 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] + and any(index in recid.json['path'] for index in index_id_list)] if len(record_ids) == 0: item_types.remove(it) @@ -3750,7 +3778,7 @@ def _get_export_data(export_path, item_types, retrys, fromid="", toid="", retry_ db.session.rollback() sleep(5) result = _get_export_data( - export_path, item_types, retrys, fromid, toid, retry_info + export_path, item_types, retrys, fromid, toid, retry_info, user_id=user_id ) return result else: @@ -3791,7 +3819,7 @@ def _get_export_data(export_path, item_types, retrys, fromid="", toid="", retry_ result = None if not fromid or not toid or (fromid and toid and int(fromid) <= int(toid)): - result = _get_export_data(export_path, item_types, 0, fromid, toid) + result = _get_export_data(export_path, item_types, 0, fromid, toid, user_id=user_id) if result: # Create bag diff --git a/modules/weko-search-ui/weko_search_ui/views.py b/modules/weko-search-ui/weko_search_ui/views.py index 39e5bab0f6..0f21dc1570 100644 --- a/modules/weko-search-ui/weko_search_ui/views.py +++ b/modules/weko-search-ui/weko_search_ui/views.py @@ -413,20 +413,67 @@ def get_last_item_id(): """Get last item id.""" result = {"last_id": ""} try: - data = db.session.query( - func.max( - func.to_number( - PersistentIdentifier.pid_value, - current_app.config["WEKO_SEARCH_UI_TO_NUMBER_FORMAT"] + is_super = any(role.name in current_app.config['WEKO_PERMISSION_SUPER_ROLE_USER'] for role in current_user.roles) + + if is_super: + data = db.session.query( + func.max( + func.to_number( + PersistentIdentifier.pid_value, + current_app.config["WEKO_SEARCH_UI_TO_NUMBER_FORMAT"] + ) ) - ) - ).filter( - PersistentIdentifier.status == PIDStatus.REGISTERED, - PersistentIdentifier.pid_type == 'recid', - PersistentIdentifier.pid_value.notlike("%.%") - ).one_or_none() - if data[0]: - result["last_id"] = str(data[0]) + ).filter( + PersistentIdentifier.status == PIDStatus.REGISTERED, + PersistentIdentifier.pid_type == 'recid', + PersistentIdentifier.pid_value.notlike("%.%") + ).one_or_none() + if data[0]: + result["last_id"] = str(data[0]) + else: + from invenio_indexer.api import RecordIndexer + from invenio_communities.models import Community + index_id_list = [] + repositories = Community.get_repositories_by_user(current_user) + for repository in repositories: + index = Indexes.get_child_list_recursive(repository.root_node_id) + index_id_list.extend(index) + + index = current_app.config['SEARCH_UI_SEARCH_INDEX'] + query = { + "query": { + "bool": { + "filter": [ + { + "bool": { + "must_not": { + "regexp": { + "control_number": ".*\\..*" + } + } + } + }, + { + "terms": { + "path": index_id_list + } + } + ] + } + }, + "size": 1, + "_source": False, + "sort": [ + { + "control_number": { + "order": "desc" + } + } + ] + } + results = RecordIndexer().client.search(index=index, body=query) + if "hits" in results and "hits" in results["hits"] and results["hits"]["hits"]: + result["last_id"] = results["hits"]["hits"][0].get("sort", []) except Exception as ex: current_app.logger.error(ex) return jsonify(data=result), 200 diff --git a/modules/weko-theme/tests/conftest.py b/modules/weko-theme/tests/conftest.py index 340b49d8d9..d3fef8e224 100644 --- a/modules/weko-theme/tests/conftest.py +++ b/modules/weko-theme/tests/conftest.py @@ -597,7 +597,7 @@ def users(app, db): originalroleuser = create_test_user(email='originalroleuser@test.org') originalroleuser2 = create_test_user(email='originalroleuser2@test.org') noroleuser = create_test_user(email='noroleuser@test.org') - + role_count = Role.query.filter_by(name='System Administrator').count() if role_count != 1: sysadmin_role = ds.create_role(name='System Administrator') @@ -625,7 +625,7 @@ def users(app, db): ds.add_role_to_user(user, repoadmin_role) ds.add_role_to_user(user, contributor_role) ds.add_role_to_user(user, comadmin_role) - + # Assign access authorization with db.session.begin_nested(): action_users = [ @@ -732,7 +732,7 @@ def indices(app, db): db.session.add(testIndexThree) db.session.add(testIndexThreeChild) db.session.commit() - + return { 'index_dict': dict(testIndexThree), 'index_non_dict': testIndexThree, @@ -755,10 +755,10 @@ def esindex(app,db_records): search.client.indices.create("test-weko-items",body=mapping) search.client.indices.put_alias(index="test-weko-items", name="test-weko") # print(current_search_client.indices.get_alias()) - + for depid, recid, parent, doi, record, item in db_records: search.client.index(index='test-weko-item-v1.0.0', doc_type='item-v1.0.0', id=record.id, body=record,refresh='true') - + yield search @@ -800,11 +800,11 @@ def db_records(db, instance_path, users): 'parent': 0, 'value': 'IndexB', } - + with patch("flask_login.utils._get_user", return_value=users[2]["obj"]): Indexes.create(0, index_metadata) - + yield result @@ -824,7 +824,7 @@ def db_register(app, db): for data in action_datas: actions_db.append(Action(**data)) db.session.add_all(actions_db) - + actionstatus_datas = dict() with open('tests/data/action_status.json') as f: actionstatus_datas = json.load(f) @@ -833,7 +833,7 @@ def db_register(app, db): for data in actionstatus_datas: actionstatus_db.append(ActionStatus(**data)) db.session.add_all(actionstatus_db) - + index = Index( public_state=True ) @@ -857,7 +857,7 @@ def db_register(app, db): form={'type':'test form'}, render={'type':'test render'}, tag=1,version_id=1,is_deleted=False) - + flow_action1 = FlowAction(status='N', flow_id=flow_define.flow_id, action_id=1, @@ -904,7 +904,7 @@ def db_register(app, db): activity_confirm_term_of_use=True, title='test', shared_user_id=-1, extra_info={}, action_order=6) - + with db.session.begin_nested(): db.session.add(index) db.session.add(flow_define) @@ -915,7 +915,7 @@ def db_register(app, db): db.session.add(flow_action3) db.session.add(workflow) db.session.add(activity) - + # return {'flow_define':flow_define,'item_type_name':item_type_name,'item_type':item_type,'flow_action':flow_action,'workflow':workflow,'activity':activity} return { 'flow_define': flow_define, @@ -932,7 +932,7 @@ def db_register(app, db): @pytest.fixture() def db_register2(app, db): session_lifetime = SessionLifetime(lifetime=60,is_delete=False) - + with db.session.begin_nested(): db.session.add(session_lifetime) @@ -964,7 +964,7 @@ def records(db): db.session.add(rec1) db.session.add(rec2) db.session.add(rec3) - + search_query_result = json_data("data/search_result.json") return(search_query_result) @@ -1264,9 +1264,10 @@ def communities(app, db, user, indices): db.session.commit() comm0 = Community.create(community_id='comm1', role_id=r.id, + page=0, ranking=0, curation_policy='',fixed_points=0, thumbnail_path='',catalog_json=[], login_menu_enabled=False, id_user=user1.id, title='Title1', description='Description1', - root_node_id=33) + root_node_id=33 ) db.session.add(comm0) return comm0 diff --git a/modules/weko-theme/tests/test_utils.py b/modules/weko-theme/tests/test_utils.py index af410ae1a4..6ec6f88723 100644 --- a/modules/weko-theme/tests/test_utils.py +++ b/modules/weko-theme/tests/test_utils.py @@ -15,15 +15,15 @@ # def get_weko_contents(getargs): -def test_get_weko_contents(i18n_app, users, client_request_args, communities,redis_connect): +def test_get_weko_contents(i18n_app, users, client_request_args, communities,redis_connect, db): with patch("flask_login.utils._get_user", return_value=users[3]['obj']): assert get_weko_contents('comm1') # def get_community_id(getargs): -def test_get_community_id(i18n_app, users, client_request_args, communities): - with patch("flask_login.utils._get_user", return_value=users[3]['obj']): - assert get_community_id(request.args) +def test_get_community_id(i18n_app, communities, db): + assert get_community_id({'c': 'comm1'}) + assert get_community_id({'c': 'comm2'}) # def get_design_layout(repository_id): @@ -63,7 +63,7 @@ def dummy_response(data): i18n_app.config['WEKO_SEARCH_TYPE_DICT'] = {'INDEX': "WEKO_SEARCH_TYPE_DICT-INDEX"} i18n_app.config['COMMUNITIES_SORTING_OPTIONS'] = {'INDEX': "COMMUNITIES_SORTING_OPTIONS-INDEX"} test = MainScreenInitDisplaySetting() - + with patch('weko_theme.utils.SearchManagement.get', return_value=search_setting): with patch('invenio_search.RecordsSearch.execute', return_value=dummy_response('{"hits": {"hits": [{"_source": {"path": ["44"]}},{"_source": {"path": ["11"]}}]}}')): with patch('weko_theme.utils.get_journal_info', return_value="get_journal_info"): @@ -88,8 +88,8 @@ def dummy_response(data): with patch('weko_items_ui.utils.get_ranking', return_value="get_ranking"): search_setting.init_disp_setting["init_disp_screen_setting"] = "1" assert isinstance(test.get_init_display_setting(), dict) - + search_setting.init_disp_setting["init_disp_screen_setting"] = "2" assert isinstance(test.get_init_display_setting(), dict) - + assert isinstance(test.get_init_display_setting(), dict) diff --git a/modules/weko-theme/weko_theme/static/css/weko_theme/styles.bundle.css b/modules/weko-theme/weko_theme/static/css/weko_theme/styles.bundle.css index 006ac1f909..2c7f29259a 100644 --- a/modules/weko-theme/weko_theme/static/css/weko_theme/styles.bundle.css +++ b/modules/weko-theme/weko_theme/static/css/weko_theme/styles.bundle.css @@ -1 +1 @@ -.node-menu{position:relative;width:150px}.node-menu .node-menu-content{width:100%;padding:5px;position:absolute;border:1px solid #bdbdbd;border-radius:5%;box-shadow:0 0 5px #bdbdbd;background-color:#eee;color:#212121;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;z-index:999}.node-menu .node-menu-content li.node-menu-item{list-style:none;padding:3px}.node-menu .node-menu-content .node-menu-item:hover{border-radius:5%;opacity:unset;cursor:pointer;background-color:#bdbdbd;transition:background-color .2s ease-out}.node-menu .node-menu-content .node-menu-item .node-menu-item-icon{display:inline-block;width:16px}.node-menu .node-menu-content .node-menu-item .node-menu-item-icon.new-tag:before{content:'\25CF'}.node-menu .node-menu-content .node-menu-item .node-menu-item-icon.new-folder:before{content:'\25B6'}.node-menu .node-menu-content .node-menu-item .node-menu-item-icon.rename:before{content:'\270E'}.node-menu .node-menu-content .node-menu-item .node-menu-item-icon.remove:before{content:'\2716'}.node-menu .node-menu-content .node-menu-item .node-menu-item-value{margin-left:5px}tree-internal ul{padding:3px 0 3px 25px}tree-internal li{padding:0;margin:0;list-style:none}tree-internal .over-drop-target-sibling::before{content:"Drop here..."!important;color:#fff!important;display:block;border:4px solid #757575;transition:padding .2s ease-out;padding:5px;border-radius:5px}tree-internal .over-drop-target-sibling{border:0 solid transparent;transition:padding .2s ease-out;padding:0;border-radius:5%}tree-internal .over-drop-target{border:4px solid #757575;transition:padding .2s ease-out;padding:5px;border-radius:5%}tree-internal .tree{box-sizing:border-box;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif}tree-internal .tree li{list-style:none;cursor:default}tree-internal .tree li div{display:inline-block;box-sizing:border-box}tree-internal .tree .node-value{display:inline-block;color:#212121}tree-internal .tree .node-value:after{display:block;width:0;height:2px;background-color:#212121;content:'';transition:width .3s}tree-internal .tree .node-value:hover:after{width:100%}tree-internal .tree .node-left-menu{display:inline-block;height:100%;width:auto}tree-internal .tree .node-left-menu span:before{content:'\2026';color:#757575}tree-internal .tree .node-selected:after{width:100%}tree-internal .tree .folding{width:25px;display:inline-block;line-height:1px;padding:0 5px;font-weight:700}tree-internal .tree .folding.node-collapsed{cursor:pointer}tree-internal .tree .folding.node-collapsed:before{content:'\25B6';color:#757575}tree-internal .tree .folding.node-expanded{cursor:pointer}tree-internal .tree .folding.node-expanded:before{content:'\25BC';color:#757575}tree-internal .tree .folding.node-empty{color:#212121;text-align:center;font-size:.89em}tree-internal .tree .folding.node-empty:before{content:'\25B6';color:#757575}tree-internal .tree .folding.node-leaf{color:#212121;text-align:center;font-size:.89em}tree-internal .tree .folding.node-leaf:before{content:'\25CF';color:#757575}tree-internal ul.rootless{padding:0}tree-internal div.rootless{display:none!important}tree-internal .loading-children:after{content:' loading ...';color:#6a1b9a;font-style:italic;font-size:.9em;-webkit-animation-name:loading-children;animation-name:loading-children;-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}@-webkit-keyframes loading-children{0%{color:#f3e5f5}12.5%{color:#e1bee7}25%{color:#ce93d8}37.5%{color:#ba68c8}50%{color:#ab47bc}62.5%{color:#9c27b0}75%{color:#8e24aa}87.5%{color:#7b1fa2}100%{color:#6a1b9a}}@keyframes loading-children{0%{color:#f3e5f5}12.5%{color:#e1bee7}25%{color:#ce93d8}37.5%{color:#ba68c8}50%{color:#ab47bc}62.5%{color:#9c27b0}75%{color:#8e24aa}87.5%{color:#7b1fa2}100%{color:#6a1b9a}} \ No newline at end of file +.node-menu{position:relative;width:150px}.node-menu .node-menu-content{width:100%;padding:5px;position:absolute;border:1px solid #bdbdbd;border-radius:5%;box-shadow:0 0 5px #bdbdbd;background-color:#eee;color:#212121;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;z-index:999}.node-menu .node-menu-content li.node-menu-item{list-style:none;padding:3px}.node-menu .node-menu-content .node-menu-item:hover{border-radius:5%;opacity:unset;cursor:pointer;background-color:#bdbdbd;transition:background-color .2s ease-out}.node-menu .node-menu-content .node-menu-item .node-menu-item-icon{display:inline-block;width:16px}.node-menu .node-menu-content .node-menu-item .node-menu-item-icon.new-tag:before{content:'\25CF'}.node-menu .node-menu-content .node-menu-item .node-menu-item-icon.new-folder:before{content:'\25B6'}.node-menu .node-menu-content .node-menu-item .node-menu-item-icon.rename:before{content:'\270E'}.node-menu .node-menu-content .node-menu-item .node-menu-item-icon.remove:before{content:'\2716'}.node-menu .node-menu-content .node-menu-item .node-menu-item-value{margin-left:5px}tree-internal ul{padding:3px 0 3px 25px}tree-internal li{padding:0;margin:0;list-style:none}tree-internal .over-drop-target-sibling::before{content:"Drop here..."!important;color:#fff!important;display:block;border:4px solid #757575;transition:padding .2s ease-out;padding:5px;border-radius:5px}tree-internal .over-drop-target-sibling{border:0 solid transparent;transition:padding .2s ease-out;padding:0;border-radius:5%}tree-internal .over-drop-target{border:4px solid #757575;transition:padding .2s ease-out;padding:5px;border-radius:5%}tree-internal .tree{box-sizing:border-box;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif}tree-internal .tree li{list-style:none;cursor:default}tree-internal .tree li div{display:inline-block;box-sizing:border-box}tree-internal .tree .node-value{display:inline-block;color:#212121}tree-internal .tree .node-value:after{display:block;width:0;height:2px;background-color:#212121;content:'';transition:width .3s}tree-internal .tree .node-value:hover:after{width:100%}tree-internal .tree .node-left-menu{display:inline-block;height:100%;width:auto}tree-internal .tree .node-left-menu span:before{content:'\2026';color:#757575}tree-internal .tree .node-selected:after{width:100%}tree-internal .tree .folding{width:25px;display:inline-block;line-height:1px;padding:0 5px;font-weight:700}tree-internal .tree .folding.node-collapsed{cursor:pointer}tree-internal .tree .folding.node-collapsed:before{content:'\25B6';color:#757575}tree-internal .tree .folding.node-expanded{cursor:pointer}tree-internal .tree .folding.node-expanded:before{content:'\25BC';color:#757575}tree-internal .tree .folding.node-empty{color:#212121;text-align:center;font-size:.89em}tree-internal .tree .folding.node-empty:before{content:'\25B6';color:#757575}tree-internal .tree .folding.node-leaf{color:#212121;text-align:center;font-size:.89em}tree-internal .tree .folding.node-leaf:before{content:'\25CF';color:#757575}tree-internal ul.rootless{padding:0}tree-internal div.rootless{display:none!important}tree-internal .loading-children:after{content:' loading ...';color:#6a1b9a;font-style:italic;font-size:.9em;-webkit-animation-name:loading-children;animation-name:loading-children;-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}@-webkit-keyframes loading-children{0%{color:#f3e5f5}12.5%{color:#e1bee7}25%{color:#ce93d8}37.5%{color:#ba68c8}50%{color:#ab47bc}62.5%{color:#9c27b0}75%{color:#8e24aa}87.5%{color:#7b1fa2}100%{color:#6a1b9a}}@keyframes loading-children{0%{color:#f3e5f5}12.5%{color:#e1bee7}25%{color:#ce93d8}37.5%{color:#ba68c8}50%{color:#ab47bc}62.5%{color:#9c27b0}75%{color:#8e24aa}87.5%{color:#7b1fa2}100%{color:#6a1b9a}} diff --git a/modules/weko-theme/weko_theme/static/js/weko_theme/inline.bundle.js b/modules/weko-theme/weko_theme/static/js/weko_theme/inline.bundle.js index effa6aee75..c1fdcb6edc 100644 --- a/modules/weko-theme/weko_theme/static/js/weko_theme/inline.bundle.js +++ b/modules/weko-theme/weko_theme/static/js/weko_theme/inline.bundle.js @@ -1 +1 @@ -!function(e){function r(r){for(var n,l,f=r[0],i=r[1],p=r[2],c=0,s=[];c>>((3&t)<<3)&255;return o}}},"5NFG":function(e,t){},EVdn:function(e,t,n){var r;!function(t,n){"use strict";"object"==typeof e.exports?e.exports=t.document?n(t,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return n(e)}:n(t)}("undefined"!=typeof window?window:this,function(n,o){"use strict";var i=[],s=Object.getPrototypeOf,a=i.slice,u=i.flat?function(e){return i.flat.call(e)}:function(e){return i.concat.apply([],e)},l=i.push,c=i.indexOf,d={},p=d.toString,f=d.hasOwnProperty,h=f.toString,y=h.call(Object),g={},v=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},m=function(e){return null!=e&&e===e.window},b=n.document,w={type:!0,src:!0,nonce:!0,noModule:!0};function _(e,t,n){var r,o,i=(n=n||b).createElement("script");if(i.text=e,t)for(r in w)(o=t[r]||t.getAttribute&&t.getAttribute(r))&&i.setAttribute(r,o);n.head.appendChild(i).parentNode.removeChild(i)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?d[p.call(e)]||"object":typeof e}var C=/HTML$/i,E=function(e,t){return new E.fn.init(e,t)};function S(e){var t=!!e&&"length"in e&&e.length,n=x(e);return!v(e)&&!m(e)&&("array"===n||0===t||"number"==typeof t&&t>0&&t-1 in e)}function T(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}E.fn=E.prototype={jquery:"3.7.1",constructor:E,length:0,toArray:function(){return a.call(this)},get:function(e){return null==e?a.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=E.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return E.each(this,e)},map:function(e){return this.pushStack(E.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(a.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},even:function(){return this.pushStack(E.grep(this,function(e,t){return(t+1)%2}))},odd:function(){return this.pushStack(E.grep(this,function(e,t){return t%2}))},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n+~]|"+I+")"+I+"*"),B=new RegExp(I+"|>"),q=new RegExp(L),U=new RegExp("^"+M+"$"),z={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M+"|[*])"),ATTR:new RegExp("^"+j),PSEUDO:new RegExp("^"+L),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+I+"*(even|odd|(([+-]|)(\\d*)n|)"+I+"*(?:([+-]|)"+I+"*(\\d+)|))"+I+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+I+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+I+"*((?:-\\d)?\\d*)"+I+"*\\)|)(?=[^-]|$)","i")},$=/^(?:input|select|textarea|button)$/i,W=/^h\d$/i,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,G=/[+~]/,Q=new RegExp("\\\\[\\da-fA-F]{1,6}"+I+"?|\\\\([^\\r\\n\\f])","g"),X=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},Y=function(){ue()},J=pe(function(e){return!0===e.disabled&&T(e,"fieldset")},{dir:"parentNode",next:"legend"});try{y.apply(i=a.call(D.childNodes),D.childNodes)}catch(e){y={apply:function(e,t){P.apply(e,a.call(t))},call:function(e){P.apply(e,a.call(arguments,1))}}}function K(e,t,n,r){var o,i,s,a,l,c,f,h=t&&t.ownerDocument,m=t?t.nodeType:9;if(n=n||[],"string"!=typeof e||!e||1!==m&&9!==m&&11!==m)return n;if(!r&&(ue(t),t=t||u,d)){if(11!==m&&(l=Z.exec(e)))if(o=l[1]){if(9===m){if(!(s=t.getElementById(o)))return n;if(s.id===o)return y.call(n,s),n}else if(h&&(s=h.getElementById(o))&&K.contains(t,s)&&s.id===o)return y.call(n,s),n}else{if(l[2])return y.apply(n,t.getElementsByTagName(e)),n;if((o=l[3])&&t.getElementsByClassName)return y.apply(n,t.getElementsByClassName(o)),n}if(!(C[e+" "]||p&&p.test(e))){if(f=e,h=t,1===m&&(B.test(e)||H.test(e))){for((h=G.test(e)&&ae(t.parentNode)||t)==t&&g.scope||((a=t.getAttribute("id"))?a=E.escapeSelector(a):t.setAttribute("id",a=v)),i=(c=ce(e)).length;i--;)c[i]=(a?"#"+a:":scope")+" "+de(c[i]);f=c.join(",")}try{return y.apply(n,h.querySelectorAll(f)),n}catch(t){C(e,!0)}finally{a===v&&t.removeAttribute("id")}}}return me(e.replace(O,"$1"),t,n,r)}function ee(){var e=[];return function n(r,o){return e.push(r+" ")>t.cacheLength&&delete n[e.shift()],n[r+" "]=o}}function te(e){return e[v]=!0,e}function ne(e){var t=u.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function re(e){return function(t){return T(t,"input")&&t.type===e}}function oe(e){return function(t){return(T(t,"input")||T(t,"button"))&&t.type===e}}function ie(e){return function(t){return"form"in t?t.parentNode&&!1===t.disabled?"label"in t?"label"in t.parentNode?t.parentNode.disabled===e:t.disabled===e:t.isDisabled===e||t.isDisabled!==!e&&J(t)===e:t.disabled===e:"label"in t&&t.disabled===e}}function se(e){return te(function(t){return t=+t,te(function(n,r){for(var o,i=e([],n.length,t),s=i.length;s--;)n[o=i[s]]&&(n[o]=!(r[o]=n[o]))})})}function ae(e){return e&&void 0!==e.getElementsByTagName&&e}function ue(e){var n,r=e?e.ownerDocument||e:D;return r!=u&&9===r.nodeType&&r.documentElement?(l=(u=r).documentElement,d=!E.isXMLDoc(u),h=l.matches||l.webkitMatchesSelector||l.msMatchesSelector,l.msMatchesSelector&&D!=u&&(n=u.defaultView)&&n.top!==n&&n.addEventListener("unload",Y),g.getById=ne(function(e){return l.appendChild(e).id=E.expando,!u.getElementsByName||!u.getElementsByName(E.expando).length}),g.disconnectedMatch=ne(function(e){return h.call(e,"*")}),g.scope=ne(function(){return u.querySelectorAll(":scope")}),g.cssHas=ne(function(){try{return u.querySelector(":has(*,:jqfake)"),!1}catch(e){return!0}}),g.getById?(t.filter.ID=function(e){var t=e.replace(Q,X);return function(e){return e.getAttribute("id")===t}},t.find.ID=function(e,t){if(void 0!==t.getElementById&&d){var n=t.getElementById(e);return n?[n]:[]}}):(t.filter.ID=function(e){var t=e.replace(Q,X);return function(e){var n=void 0!==e.getAttributeNode&&e.getAttributeNode("id");return n&&n.value===t}},t.find.ID=function(e,t){if(void 0!==t.getElementById&&d){var n,r,o,i=t.getElementById(e);if(i){if((n=i.getAttributeNode("id"))&&n.value===e)return[i];for(o=t.getElementsByName(e),r=0;i=o[r++];)if((n=i.getAttributeNode("id"))&&n.value===e)return[i]}return[]}}),t.find.TAG=function(e,t){return void 0!==t.getElementsByTagName?t.getElementsByTagName(e):t.querySelectorAll(e)},t.find.CLASS=function(e,t){if(void 0!==t.getElementsByClassName&&d)return t.getElementsByClassName(e)},p=[],ne(function(e){var t;l.appendChild(e).innerHTML="",e.querySelectorAll("[selected]").length||p.push("\\["+I+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+v+"-]").length||p.push("~="),e.querySelectorAll("a#"+v+"+*").length||p.push(".#.+[+~]"),e.querySelectorAll(":checked").length||p.push(":checked"),(t=u.createElement("input")).setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),l.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&p.push(":enabled",":disabled"),(t=u.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||p.push("\\["+I+"*name"+I+"*="+I+"*(?:''|\"\")")}),g.cssHas||p.push(":has"),p=p.length&&new RegExp(p.join("|")),S=function(e,t){if(e===t)return s=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!g.sortDetached&&t.compareDocumentPosition(e)===n?e===u||e.ownerDocument==D&&K.contains(D,e)?-1:t===u||t.ownerDocument==D&&K.contains(D,t)?1:o?c.call(o,e)-c.call(o,t):0:4&n?-1:1)},u):u}for(e in K.matches=function(e,t){return K(e,null,null,t)},K.matchesSelector=function(e,t){if(ue(e),d&&!C[t+" "]&&(!p||!p.test(t)))try{var n=h.call(e,t);if(n||g.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){C(t,!0)}return K(t,u,null,[e]).length>0},K.contains=function(e,t){return(e.ownerDocument||e)!=u&&ue(e),E.contains(e,t)},K.attr=function(e,n){(e.ownerDocument||e)!=u&&ue(e);var r=t.attrHandle[n.toLowerCase()],o=r&&f.call(t.attrHandle,n.toLowerCase())?r(e,n,!d):void 0;return void 0!==o?o:e.getAttribute(n)},K.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},E.uniqueSort=function(e){var t,n=[],r=0,i=0;if(s=!g.sortStable,o=!g.sortStable&&a.call(e,0),N.call(e,S),s){for(;t=e[i++];)t===e[i]&&(r=n.push(i));for(;r--;)A.call(e,n[r],1)}return o=null,e},E.fn.uniqueSort=function(){return this.pushStack(E.uniqueSort(a.apply(this)))},(t=E.expr={cacheLength:50,createPseudo:te,match:z,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(Q,X),e[3]=(e[3]||e[4]||e[5]||"").replace(Q,X),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||K.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&K.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return z.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&q.test(n)&&(t=ce(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(Q,X).toLowerCase();return"*"===e?function(){return!0}:function(e){return T(e,t)}},CLASS:function(e){var t=w[e+" "];return t||(t=new RegExp("(^|"+I+")"+e+"("+I+"|$)"))&&w(e,function(e){return t.test("string"==typeof e.className&&e.className||void 0!==e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var o=K.attr(r,e);return null==o?"!="===t:!t||(o+="","="===t?o===n:"!="===t?o!==n:"^="===t?n&&0===o.indexOf(n):"*="===t?n&&o.indexOf(n)>-1:"$="===t?n&&o.slice(-n.length)===n:"~="===t?(" "+o.replace(V," ")+" ").indexOf(n)>-1:"|="===t&&(o===n||o.slice(0,n.length+1)===n+"-"))}},CHILD:function(e,t,n,r,o){var i="nth"!==e.slice(0,3),s="last"!==e.slice(-4),a="of-type"===t;return 1===r&&0===o?function(e){return!!e.parentNode}:function(t,n,u){var l,c,d,p,f,h=i!==s?"nextSibling":"previousSibling",y=t.parentNode,g=a&&t.nodeName.toLowerCase(),b=!u&&!a,w=!1;if(y){if(i){for(;h;){for(d=t;d=d[h];)if(a?T(d,g):1===d.nodeType)return!1;f=h="only"===e&&!f&&"nextSibling"}return!0}if(f=[s?y.firstChild:y.lastChild],s&&b){for(w=(p=(l=(c=y[v]||(y[v]={}))[e]||[])[0]===m&&l[1])&&l[2],d=p&&y.childNodes[p];d=++p&&d&&d[h]||(w=p=0)||f.pop();)if(1===d.nodeType&&++w&&d===t){c[e]=[m,p,w];break}}else if(b&&(w=p=(l=(c=t[v]||(t[v]={}))[e]||[])[0]===m&&l[1]),!1===w)for(;(d=++p&&d&&d[h]||(w=p=0)||f.pop())&&((a?!T(d,g):1!==d.nodeType)||!++w||(b&&((c=d[v]||(d[v]={}))[e]=[m,w]),d!==t)););return(w-=o)===r||w%r==0&&w/r>=0}}},PSEUDO:function(e,n){var r,o=t.pseudos[e]||t.setFilters[e.toLowerCase()]||K.error("unsupported pseudo: "+e);return o[v]?o(n):o.length>1?(r=[e,e,"",n],t.setFilters.hasOwnProperty(e.toLowerCase())?te(function(e,t){for(var r,i=o(e,n),s=i.length;s--;)e[r=c.call(e,i[s])]=!(t[r]=i[s])}):function(e){return o(e,0,r)}):o}},pseudos:{not:te(function(e){var t=[],n=[],r=ve(e.replace(O,"$1"));return r[v]?te(function(e,t,n,o){for(var i,s=r(e,null,o,[]),a=e.length;a--;)(i=s[a])&&(e[a]=!(t[a]=i))}):function(e,o,i){return t[0]=e,r(t,null,i,n),t[0]=null,!n.pop()}}),has:te(function(e){return function(t){return K(e,t).length>0}}),contains:te(function(e){return e=e.replace(Q,X),function(t){return(t.textContent||E.text(t)).indexOf(e)>-1}}),lang:te(function(e){return U.test(e||"")||K.error("unsupported lang: "+e),e=e.replace(Q,X).toLowerCase(),function(t){var n;do{if(n=d?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(n=n.toLowerCase())===e||0===n.indexOf(e+"-")}while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(e){var t=n.location&&n.location.hash;return t&&t.slice(1)===e.id},root:function(e){return e===l},focus:function(e){return e===function(){try{return u.activeElement}catch(e){}}()&&u.hasFocus()&&!!(e.type||e.href||~e.tabIndex)},enabled:ie(!1),disabled:ie(!0),checked:function(e){return T(e,"input")&&!!e.checked||T(e,"option")&&!!e.selected},selected:function(e){return!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!t.pseudos.empty(e)},header:function(e){return W.test(e.nodeName)},input:function(e){return $.test(e.nodeName)},button:function(e){return T(e,"input")&&"button"===e.type||T(e,"button")},text:function(e){var t;return T(e,"input")&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:se(function(){return[0]}),last:se(function(e,t){return[t-1]}),eq:se(function(e,t,n){return[n<0?n+t:n]}),even:se(function(e,t){for(var n=0;nt?t:n;--r>=0;)e.push(r);return e}),gt:se(function(e,t,n){for(var r=n<0?n+t:n;++r1?function(t,n,r){for(var o=e.length;o--;)if(!e[o](t,n,r))return!1;return!0}:e[0]}function he(e,t,n,r,o){for(var i,s=[],a=0,u=e.length,l=null!=t;a-1&&(i[l]=!(s[l]=p))}}else f=he(f===s?f.splice(v,f.length):f),o?o(null,s,f,u):y.apply(s,f)})}function ge(e){for(var n,o,i,s=e.length,a=t.relative[e[0].type],u=a||t.relative[" "],l=a?1:0,d=pe(function(e){return e===n},u,!0),p=pe(function(e){return c.call(n,e)>-1},u,!0),f=[function(e,t,o){var i=!a&&(o||t!=r)||((n=t).nodeType?d(e,t,o):p(e,t,o));return n=null,i}];l1&&fe(f),l>1&&de(e.slice(0,l-1).concat({value:" "===e[l-2].type?"*":""})).replace(O,"$1"),o,l0,i=e.length>0,s=function(s,a,l,c,p){var f,h,g,v=0,b="0",w=s&&[],_=[],x=r,C=s||i&&t.find.TAG("*",p),S=m+=null==x?1:Math.random()||.1,T=C.length;for(p&&(r=a==u||a||p);b!==T&&null!=(f=C[b]);b++){if(i&&f){for(h=0,a||f.ownerDocument==u||(ue(f),l=!d);g=e[h++];)if(g(f,a||u,l)){y.call(c,f);break}p&&(m=S)}o&&((f=!g&&f)&&v--,s&&w.push(f))}if(v+=b,o&&b!==v){for(h=0;g=n[h++];)g(w,_,a,l);if(s){if(v>0)for(;b--;)w[b]||_[b]||(_[b]=k.call(c));_=he(_)}y.apply(c,_),p&&!s&&_.length>0&&v+n.length>1&&E.uniqueSort(c)}return p&&(m=S,r=x),w};return o?te(s):s}(s,i))).selector=e}return a}function me(e,n,r,o){var i,s,a,u,l,c="function"==typeof e&&e,p=!o&&ce(e=c.selector||e);if(r=r||[],1===p.length){if((s=p[0]=p[0].slice(0)).length>2&&"ID"===(a=s[0]).type&&9===n.nodeType&&d&&t.relative[s[1].type]){if(!(n=(t.find.ID(a.matches[0].replace(Q,X),n)||[])[0]))return r;c&&(n=n.parentNode),e=e.slice(s.shift().value.length)}for(i=z.needsContext.test(e)?0:s.length;i--&&!t.relative[u=(a=s[i]).type];)if((l=t.find[u])&&(o=l(a.matches[0].replace(Q,X),G.test(s[0].type)&&ae(n.parentNode)||n))){if(s.splice(i,1),!(e=o.length&&de(s)))return y.apply(r,o),r;break}}return(c||ve(e,p))(o,n,!d,r,!n||G.test(e)&&ae(n.parentNode)||n),r}le.prototype=t.filters=t.pseudos,t.setFilters=new le,g.sortStable=v.split("").sort(S).join("")===v,ue(),g.sortDetached=ne(function(e){return 1&e.compareDocumentPosition(u.createElement("fieldset"))}),E.find=K,E.expr[":"]=E.expr.pseudos,E.unique=E.uniqueSort,K.compile=ve,K.select=me,K.setDocument=ue,K.tokenize=ce,K.escape=E.escapeSelector,K.getText=E.text,K.isXML=E.isXMLDoc,K.selectors=E.expr,K.support=E.support,K.uniqueSort=E.uniqueSort}();var j=function(e,t,n){for(var r=[],o=void 0!==n;(e=e[t])&&9!==e.nodeType;)if(1===e.nodeType){if(o&&E(e).is(n))break;r.push(e)}return r},L=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},V=E.expr.match.needsContext,F=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function H(e,t,n){return v(t)?E.grep(e,function(e,r){return!!t.call(e,r,e)!==n}):t.nodeType?E.grep(e,function(e){return e===t!==n}):"string"!=typeof t?E.grep(e,function(e){return c.call(t,e)>-1!==n}):E.filter(t,e,n)}E.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?E.find.matchesSelector(r,e)?[r]:[]:E.find.matches(e,E.grep(t,function(e){return 1===e.nodeType}))},E.fn.extend({find:function(e){var t,n,r=this.length,o=this;if("string"!=typeof e)return this.pushStack(E(e).filter(function(){for(t=0;t1?E.uniqueSort(n):n},filter:function(e){return this.pushStack(H(this,e||[],!1))},not:function(e){return this.pushStack(H(this,e||[],!0))},is:function(e){return!!H(this,"string"==typeof e&&V.test(e)?E(e):e||[],!1).length}});var B,q=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(E.fn.init=function(e,t,n){var r,o;if(!e)return this;if(n=n||B,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&e.length>=3?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(E.merge(this,E.parseHTML(r[1],(t=t instanceof E?t[0]:t)&&t.nodeType?t.ownerDocument||t:b,!0)),F.test(r[1])&&E.isPlainObject(t))for(r in t)v(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(o=b.getElementById(r[2]))&&(this[0]=o,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):v(e)?void 0!==n.ready?n.ready(e):e(E):E.makeArray(e,this)}).prototype=E.fn,B=E(b);var U=/^(?:parents|prev(?:Until|All))/,z={children:!0,contents:!0,next:!0,prev:!0};function $(e,t){for(;(e=e[t])&&1!==e.nodeType;);return e}E.fn.extend({has:function(e){var t=E(e,this),n=t.length;return this.filter(function(){for(var e=0;e-1:1===n.nodeType&&E.find.matchesSelector(n,e))){i.push(n);break}return this.pushStack(i.length>1?E.uniqueSort(i):i)},index:function(e){return e?"string"==typeof e?c.call(E(e),this[0]):c.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(E.uniqueSort(E.merge(this.get(),E(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),E.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return j(e,"parentNode")},parentsUntil:function(e,t,n){return j(e,"parentNode",n)},next:function(e){return $(e,"nextSibling")},prev:function(e){return $(e,"previousSibling")},nextAll:function(e){return j(e,"nextSibling")},prevAll:function(e){return j(e,"previousSibling")},nextUntil:function(e,t,n){return j(e,"nextSibling",n)},prevUntil:function(e,t,n){return j(e,"previousSibling",n)},siblings:function(e){return L((e.parentNode||{}).firstChild,e)},children:function(e){return L(e.firstChild)},contents:function(e){return null!=e.contentDocument&&s(e.contentDocument)?e.contentDocument:(T(e,"template")&&(e=e.content||e),E.merge([],e.childNodes))}},function(e,t){E.fn[e]=function(n,r){var o=E.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(o=E.filter(r,o)),this.length>1&&(z[e]||E.uniqueSort(o),U.test(e)&&o.reverse()),this.pushStack(o)}});var W=/[^\x20\t\r\n\f]+/g;function Z(e){return e}function G(e){throw e}function Q(e,t,n,r){var o;try{e&&v(o=e.promise)?o.call(e).done(t).fail(n):e&&v(o=e.then)?o.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n.apply(void 0,[e])}}E.Callbacks=function(e){e="string"==typeof e?function(e){var t={};return E.each(e.match(W)||[],function(e,n){t[n]=!0}),t}(e):E.extend({},e);var t,n,r,o,i=[],s=[],a=-1,u=function(){for(o=o||e.once,r=t=!0;s.length;a=-1)for(n=s.shift();++a-1;)i.splice(n,1),n<=a&&a--}),this},has:function(e){return e?E.inArray(e,i)>-1:i.length>0},empty:function(){return i&&(i=[]),this},disable:function(){return o=s=[],i=n="",this},disabled:function(){return!i},lock:function(){return o=s=[],n||t||(i=n=""),this},locked:function(){return!!o},fireWith:function(e,n){return o||(n=[e,(n=n||[]).slice?n.slice():n],s.push(n),t||u()),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!r}};return l},E.extend({Deferred:function(e){var t=[["notify","progress",E.Callbacks("memory"),E.Callbacks("memory"),2],["resolve","done",E.Callbacks("once memory"),E.Callbacks("once memory"),0,"resolved"],["reject","fail",E.Callbacks("once memory"),E.Callbacks("once memory"),1,"rejected"]],r="pending",o={state:function(){return r},always:function(){return i.done(arguments).fail(arguments),this},catch:function(e){return o.then(null,e)},pipe:function(){var e=arguments;return E.Deferred(function(n){E.each(t,function(t,r){var o=v(e[r[4]])&&e[r[4]];i[r[1]](function(){var e=o&&o.apply(this,arguments);e&&v(e.promise)?e.promise().progress(n.notify).done(n.resolve).fail(n.reject):n[r[0]+"With"](this,o?[e]:arguments)})}),e=null}).promise()},then:function(e,r,o){var i=0;function s(e,t,r,o){return function(){var a=this,u=arguments,l=function(){var n,l;if(!(e=i&&(r!==G&&(a=void 0,u=[n]),t.rejectWith(a,u))}};e?c():(E.Deferred.getErrorHook?c.error=E.Deferred.getErrorHook():E.Deferred.getStackHook&&(c.error=E.Deferred.getStackHook()),n.setTimeout(c))}}return E.Deferred(function(n){t[0][3].add(s(0,n,v(o)?o:Z,n.notifyWith)),t[1][3].add(s(0,n,v(e)?e:Z)),t[2][3].add(s(0,n,v(r)?r:G))}).promise()},promise:function(e){return null!=e?E.extend(e,o):o}},i={};return E.each(t,function(e,n){var s=n[2],a=n[5];o[n[1]]=s.add,a&&s.add(function(){r=a},t[3-e][2].disable,t[3-e][3].disable,t[0][2].lock,t[0][3].lock),s.add(n[3].fire),i[n[0]]=function(){return i[n[0]+"With"](this===i?void 0:this,arguments),this},i[n[0]+"With"]=s.fireWith}),o.promise(i),e&&e.call(i,i),i},when:function(e){var t=arguments.length,n=t,r=Array(n),o=a.call(arguments),i=E.Deferred(),s=function(e){return function(n){r[e]=this,o[e]=arguments.length>1?a.call(arguments):n,--t||i.resolveWith(r,o)}};if(t<=1&&(Q(e,i.done(s(n)).resolve,i.reject,!t),"pending"===i.state()||v(o[n]&&o[n].then)))return i.then();for(;n--;)Q(o[n],s(n),i.reject);return i.promise()}});var X=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;E.Deferred.exceptionHook=function(e,t){n.console&&n.console.warn&&e&&X.test(e.name)&&n.console.warn("jQuery.Deferred exception: "+e.message,e.stack,t)},E.readyException=function(e){n.setTimeout(function(){throw e})};var Y=E.Deferred();function J(){b.removeEventListener("DOMContentLoaded",J),n.removeEventListener("load",J),E.ready()}E.fn.ready=function(e){return Y.then(e).catch(function(e){E.readyException(e)}),this},E.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--E.readyWait:E.isReady)||(E.isReady=!0,!0!==e&&--E.readyWait>0||Y.resolveWith(b,[E]))}}),E.ready.then=Y.then,"complete"===b.readyState||"loading"!==b.readyState&&!b.documentElement.doScroll?n.setTimeout(E.ready):(b.addEventListener("DOMContentLoaded",J),n.addEventListener("load",J));var K=function(e,t,n,r,o,i,s){var a=0,u=e.length,l=null==n;if("object"===x(n))for(a in o=!0,n)K(e,t,a,n[a],!0,i,s);else if(void 0!==r&&(o=!0,v(r)||(s=!0),l&&(s?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(E(e),n)})),t))for(;a1,null,!0)},removeData:function(e){return this.each(function(){ae.remove(this,e)})}}),E.extend({queue:function(e,t,n){var r;if(e)return r=se.get(e,t=(t||"fx")+"queue"),n&&(!r||Array.isArray(n)?r=se.access(e,t,E.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){var n=E.queue(e,t=t||"fx"),r=n.length,o=n.shift(),i=E._queueHooks(e,t);"inprogress"===o&&(o=n.shift(),r--),o&&("fx"===t&&n.unshift("inprogress"),delete i.stop,o.call(e,function(){E.dequeue(e,t)},i)),!r&&i&&i.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return se.get(e,n)||se.access(e,n,{empty:E.Callbacks("once memory").add(function(){se.remove(e,[t+"queue",n])})})}}),E.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length\x20\t\r\n\f]*)/i,Te=/^$|^module$|\/(?:java|ecma)script/i;xe=b.createDocumentFragment().appendChild(b.createElement("div")),(Ce=b.createElement("input")).setAttribute("type","radio"),Ce.setAttribute("checked","checked"),Ce.setAttribute("name","t"),xe.appendChild(Ce),g.checkClone=xe.cloneNode(!0).cloneNode(!0).lastChild.checked,xe.innerHTML="",g.noCloneChecked=!!xe.cloneNode(!0).lastChild.defaultValue,xe.innerHTML="",g.option=!!xe.lastChild;var ke={thead:[1,"","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};function Ne(e,t){var n;return n=void 0!==e.getElementsByTagName?e.getElementsByTagName(t||"*"):void 0!==e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&T(e,t)?E.merge([e],n):n}function Ae(e,t){for(var n=0,r=e.length;n",""]);var Ie=/<|&#?\w+;/;function Oe(e,t,n,r,o){for(var i,s,a,u,l,c,d=t.createDocumentFragment(),p=[],f=0,h=e.length;f-1)o&&o.push(i);else if(l=ye(i),s=Ne(d.appendChild(i),"script"),l&&Ae(s),n)for(c=0;i=s[c++];)Te.test(i.type||"")&&n.push(i);return d}var Re=/^([^.]*)(?:\.(.+)|)/;function Me(){return!0}function De(){return!1}function Pe(e,t,n,r,o,i){var s,a;if("object"==typeof t){for(a in"string"!=typeof n&&(r=r||n,n=void 0),t)Pe(e,a,n,r,t[a],i);return e}if(null==r&&null==o?(o=n,r=n=void 0):null==o&&("string"==typeof n?(o=r,r=void 0):(o=r,r=n,n=void 0)),!1===o)o=De;else if(!o)return e;return 1===i&&(s=o,(o=function(e){return E().off(e),s.apply(this,arguments)}).guid=s.guid||(s.guid=E.guid++)),e.each(function(){E.event.add(this,t,o,r,n)})}function je(e,t,n){n?(se.set(e,t,!1),E.event.add(e,t,{namespace:!1,handler:function(e){var n,r=se.get(this,t);if(1&e.isTrigger&&this[t]){if(r)(E.event.special[t]||{}).delegateType&&e.stopPropagation();else if(r=a.call(arguments),se.set(this,t,r),this[t](),n=se.get(this,t),se.set(this,t,!1),r!==n)return e.stopImmediatePropagation(),e.preventDefault(),n}else r&&(se.set(this,t,E.event.trigger(r[0],r.slice(1),this)),e.stopPropagation(),e.isImmediatePropagationStopped=Me)}})):void 0===se.get(e,t)&&E.event.add(e,t,Me)}E.event={global:{},add:function(e,t,n,r,o){var i,s,a,u,l,c,d,p,f,h,y,g=se.get(e);if(oe(e))for(n.handler&&(n=(i=n).handler,o=i.selector),o&&E.find.matchesSelector(he,o),n.guid||(n.guid=E.guid++),(u=g.events)||(u=g.events=Object.create(null)),(s=g.handle)||(s=g.handle=function(t){return void 0!==E&&E.event.triggered!==t.type?E.event.dispatch.apply(e,arguments):void 0}),l=(t=(t||"").match(W)||[""]).length;l--;)f=y=(a=Re.exec(t[l])||[])[1],h=(a[2]||"").split(".").sort(),f&&(d=E.event.special[f]||{},d=E.event.special[f=(o?d.delegateType:d.bindType)||f]||{},c=E.extend({type:f,origType:y,data:r,handler:n,guid:n.guid,selector:o,needsContext:o&&E.expr.match.needsContext.test(o),namespace:h.join(".")},i),(p=u[f])||((p=u[f]=[]).delegateCount=0,d.setup&&!1!==d.setup.call(e,r,h,s)||e.addEventListener&&e.addEventListener(f,s)),d.add&&(d.add.call(e,c),c.handler.guid||(c.handler.guid=n.guid)),o?p.splice(p.delegateCount++,0,c):p.push(c),E.event.global[f]=!0)},remove:function(e,t,n,r,o){var i,s,a,u,l,c,d,p,f,h,y,g=se.hasData(e)&&se.get(e);if(g&&(u=g.events)){for(l=(t=(t||"").match(W)||[""]).length;l--;)if(f=y=(a=Re.exec(t[l])||[])[1],h=(a[2]||"").split(".").sort(),f){for(d=E.event.special[f]||{},p=u[f=(r?d.delegateType:d.bindType)||f]||[],a=a[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),s=i=p.length;i--;)c=p[i],!o&&y!==c.origType||n&&n.guid!==c.guid||a&&!a.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(i,1),c.selector&&p.delegateCount--,d.remove&&d.remove.call(e,c));s&&!p.length&&(d.teardown&&!1!==d.teardown.call(e,h,g.handle)||E.removeEvent(e,f,g.handle),delete u[f])}else for(f in u)E.event.remove(e,f+t[l],n,r,!0);E.isEmptyObject(u)&&se.remove(e,"handle events")}},dispatch:function(e){var t,n,r,o,i,s,a=new Array(arguments.length),u=E.event.fix(e),l=(se.get(this,"events")||Object.create(null))[u.type]||[],c=E.event.special[u.type]||{};for(a[0]=u,t=1;t=1))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(i=[],s={},n=0;n-1:E.find(o,this,null,[l]).length),s[o]&&i.push(r);i.length&&a.push({elem:l,handlers:i})}return l=this,u\s*$/g;function He(e,t){return T(e,"table")&&T(11!==t.nodeType?t:t.firstChild,"tr")&&E(e).children("tbody")[0]||e}function Be(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Ue(e,t){var n,r,o,i,s,a;if(1===t.nodeType){if(se.hasData(e)&&(a=se.get(e).events))for(o in se.remove(t,"handle events"),a)for(n=0,r=a[o].length;n1&&"string"==typeof h&&!g.checkClone&&Ve.test(h))return e.each(function(o){var i=e.eq(o);y&&(t[0]=h.call(this,o,i.html())),ze(i,t,n,r)});if(p&&(i=(o=Oe(t,e[0].ownerDocument,!1,e,r)).firstChild,1===o.childNodes.length&&(o=i),i||r)){for(a=(s=E.map(Ne(o,"script"),Be)).length;d0&&Ae(s,!d&&Ne(e,"script")),c},cleanData:function(e){for(var t,n,r,o=E.event.special,i=0;void 0!==(n=e[i]);i++)if(oe(n)){if(t=n[se.expando]){if(t.events)for(r in t.events)o[r]?E.event.remove(n,r):E.removeEvent(n,r,t.handle);n[se.expando]=void 0}n[ae.expando]&&(n[ae.expando]=void 0)}}}),E.fn.extend({detach:function(e){return $e(this,e,!0)},remove:function(e){return $e(this,e)},text:function(e){return K(this,function(e){return void 0===e?E.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)})},null,e,arguments.length)},append:function(){return ze(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||He(this,e).appendChild(e)})},prepend:function(){return ze(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=He(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return ze(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return ze(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(E.cleanData(Ne(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return E.clone(this,e,t)})},html:function(e){return K(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!Le.test(e)&&!ke[(Se.exec(e)||["",""])[1].toLowerCase()]){e=E.htmlPrefilter(e);try{for(;n=0&&(u+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-i-u-a-.5))||0),u+l}function ut(e,t,n){var r=Ge(e),o=(!g.boxSizingReliable()||n)&&"border-box"===E.css(e,"boxSizing",!1,r),i=o,s=Ye(e,t,r),a="offset"+t[0].toUpperCase()+t.slice(1);if(We.test(s)){if(!n)return s;s="auto"}return(!g.boxSizingReliable()&&o||!g.reliableTrDimensions()&&T(e,"tr")||"auto"===s||!parseFloat(s)&&"inline"===E.css(e,"display",!1,r))&&e.getClientRects().length&&(o="border-box"===E.css(e,"boxSizing",!1,r),(i=a in e)&&(s=e[a])),(s=parseFloat(s)||0)+at(e,t,n||(o?"border":"content"),i,r,s)+"px"}function lt(e,t,n,r,o){return new lt.prototype.init(e,t,n,r,o)}E.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Ye(e,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,aspectRatio:!0,borderImageSlice:!0,columnCount:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,gridArea:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnStart:!0,gridRow:!0,gridRowEnd:!0,gridRowStart:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,scale:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeMiterlimit:!0,strokeOpacity:!0},cssProps:{},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var o,i,s,a=re(t),u=Ze.test(t),l=e.style;if(u||(t=nt(a)),s=E.cssHooks[t]||E.cssHooks[a],void 0===n)return s&&"get"in s&&void 0!==(o=s.get(e,!1,r))?o:l[t];"string"==(i=typeof n)&&(o=pe.exec(n))&&o[1]&&(n=me(e,t,o),i="number"),null!=n&&n==n&&("number"!==i||u||(n+=o&&o[3]||(E.cssNumber[a]?"":"px")),g.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),s&&"set"in s&&void 0===(n=s.set(e,n,r))||(u?l.setProperty(t,n):l[t]=n))}},css:function(e,t,n,r){var o,i,s,a=re(t);return Ze.test(t)||(t=nt(a)),(s=E.cssHooks[t]||E.cssHooks[a])&&"get"in s&&(o=s.get(e,!0,n)),void 0===o&&(o=Ye(e,t,r)),"normal"===o&&t in it&&(o=it[t]),""===n||n?(i=parseFloat(o),!0===n||isFinite(i)?i||0:o):o}}),E.each(["height","width"],function(e,t){E.cssHooks[t]={get:function(e,n,r){if(n)return!rt.test(E.css(e,"display"))||e.getClientRects().length&&e.getBoundingClientRect().width?ut(e,t,r):Qe(e,ot,function(){return ut(e,t,r)})},set:function(e,n,r){var o,i=Ge(e),s=!g.scrollboxSize()&&"absolute"===i.position,a=(s||r)&&"border-box"===E.css(e,"boxSizing",!1,i),u=r?at(e,t,r,a,i):0;return a&&s&&(u-=Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-parseFloat(i[t])-at(e,t,"border",!1,i)-.5)),u&&(o=pe.exec(n))&&"px"!==(o[3]||"px")&&(e.style[t]=n,n=E.css(e,t)),st(0,n,u)}}}),E.cssHooks.marginLeft=Je(g.reliableMarginLeft,function(e,t){if(t)return(parseFloat(Ye(e,"marginLeft"))||e.getBoundingClientRect().left-Qe(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}))+"px"}),E.each({margin:"",padding:"",border:"Width"},function(e,t){E.cssHooks[e+t]={expand:function(n){for(var r=0,o={},i="string"==typeof n?n.split(" "):[n];r<4;r++)o[e+fe[r]+t]=i[r]||i[r-2]||i[0];return o}},"margin"!==e&&(E.cssHooks[e+t].set=st)}),E.fn.extend({css:function(e,t){return K(this,function(e,t,n){var r,o,i={},s=0;if(Array.isArray(t)){for(r=Ge(e),o=t.length;s1)}}),E.Tween=lt,(lt.prototype={constructor:lt,init:function(e,t,n,r,o,i){this.elem=e,this.prop=n,this.easing=o||E.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=i||(E.cssNumber[n]?"":"px")},cur:function(){var e=lt.propHooks[this.prop];return e&&e.get?e.get(this):lt.propHooks._default.get(this)},run:function(e){var t,n=lt.propHooks[this.prop];return this.pos=t=this.options.duration?E.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):lt.propHooks._default.set(this),this}}).init.prototype=lt.prototype,(lt.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=E.css(e.elem,e.prop,""))&&"auto"!==t?t:0},set:function(e){E.fx.step[e.prop]?E.fx.step[e.prop](e):1!==e.elem.nodeType||!E.cssHooks[e.prop]&&null==e.elem.style[nt(e.prop)]?e.elem[e.prop]=e.now:E.style(e.elem,e.prop,e.now+e.unit)}}}).scrollTop=lt.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},E.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},E.fx=lt.prototype.init,E.fx.step={};var ct,dt,pt=/^(?:toggle|show|hide)$/,ft=/queueHooks$/;function ht(){dt&&(!1===b.hidden&&n.requestAnimationFrame?n.requestAnimationFrame(ht):n.setTimeout(ht,E.fx.interval),E.fx.tick())}function yt(){return n.setTimeout(function(){ct=void 0}),ct=Date.now()}function gt(e,t){var n,r=0,o={height:e};for(t=t?1:0;r<4;r+=2-t)o["margin"+(n=fe[r])]=o["padding"+n]=e;return t&&(o.opacity=o.width=e),o}function vt(e,t,n){for(var r,o=(mt.tweeners[t]||[]).concat(mt.tweeners["*"]),i=0,s=o.length;i1)},removeAttr:function(e){return this.each(function(){E.removeAttr(this,e)})}}),E.extend({attr:function(e,t,n){var r,o,i=e.nodeType;if(3!==i&&8!==i&&2!==i)return void 0===e.getAttribute?E.prop(e,t,n):(1===i&&E.isXMLDoc(e)||(o=E.attrHooks[t.toLowerCase()]||(E.expr.match.bool.test(t)?bt:void 0)),void 0!==n?null===n?void E.removeAttr(e,t):o&&"set"in o&&void 0!==(r=o.set(e,n,t))?r:(e.setAttribute(t,n+""),n):o&&"get"in o&&null!==(r=o.get(e,t))?r:null==(r=E.find.attr(e,t))?void 0:r)},attrHooks:{type:{set:function(e,t){if(!g.radioValue&&"radio"===t&&T(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r=0,o=t&&t.match(W);if(o&&1===e.nodeType)for(;n=o[r++];)e.removeAttribute(n)}}),bt={set:function(e,t,n){return!1===t?E.removeAttr(e,n):e.setAttribute(n,n),n}},E.each(E.expr.match.bool.source.match(/\w+/g),function(e,t){var n=wt[t]||E.find.attr;wt[t]=function(e,t,r){var o,i,s=t.toLowerCase();return r||(i=wt[s],wt[s]=o,o=null!=n(e,t,r)?s:null,wt[s]=i),o}});var _t=/^(?:input|select|textarea|button)$/i,xt=/^(?:a|area)$/i;function Ct(e){return(e.match(W)||[]).join(" ")}function Et(e){return e.getAttribute&&e.getAttribute("class")||""}function St(e){return Array.isArray(e)?e:"string"==typeof e&&e.match(W)||[]}E.fn.extend({prop:function(e,t){return K(this,E.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[E.propFix[e]||e]})}}),E.extend({prop:function(e,t,n){var r,o,i=e.nodeType;if(3!==i&&8!==i&&2!==i)return 1===i&&E.isXMLDoc(e)||(o=E.propHooks[t=E.propFix[t]||t]),void 0!==n?o&&"set"in o&&void 0!==(r=o.set(e,n,t))?r:e[t]=n:o&&"get"in o&&null!==(r=o.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=E.find.attr(e,"tabindex");return t?parseInt(t,10):_t.test(e.nodeName)||xt.test(e.nodeName)&&e.href?0:-1}}},propFix:{for:"htmlFor",class:"className"}}),g.optSelected||(E.propHooks.selected={get:function(e){return null},set:function(e){}}),E.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){E.propFix[this.toLowerCase()]=this}),E.fn.extend({addClass:function(e){var t,n,r,o,i,s;return v(e)?this.each(function(t){E(this).addClass(e.call(this,t,Et(this)))}):(t=St(e)).length?this.each(function(){if(r=Et(this),n=1===this.nodeType&&" "+Ct(r)+" "){for(i=0;i-1;)n=n.replace(" "+o+" "," ");s=Ct(n),r!==s&&this.setAttribute("class",s)}}):this:this.attr("class","")},toggleClass:function(e,t){var n,r,o,i,s=typeof e,a="string"===s||Array.isArray(e);return v(e)?this.each(function(n){E(this).toggleClass(e.call(this,n,Et(this),t),t)}):"boolean"==typeof t&&a?t?this.addClass(e):this.removeClass(e):(n=St(e),this.each(function(){if(a)for(i=E(this),o=0;o-1)return!0;return!1}});var Tt=/\r/g;E.fn.extend({val:function(e){var t,n,r,o=this[0];return arguments.length?(r=v(e),this.each(function(n){var o;1===this.nodeType&&(null==(o=r?e.call(this,n,E(this).val()):e)?o="":"number"==typeof o?o+="":Array.isArray(o)&&(o=E.map(o,function(e){return null==e?"":e+""})),(t=E.valHooks[this.type]||E.valHooks[this.nodeName.toLowerCase()])&&"set"in t&&void 0!==t.set(this,o,"value")||(this.value=o))})):o?(t=E.valHooks[o.type]||E.valHooks[o.nodeName.toLowerCase()])&&"get"in t&&void 0!==(n=t.get(o,"value"))?n:"string"==typeof(n=o.value)?n.replace(Tt,""):null==n?"":n:void 0}}),E.extend({valHooks:{option:{get:function(e){var t=E.find.attr(e,"value");return null!=t?t:Ct(E.text(e))}},select:{get:function(e){var t,n,r,o=e.options,i=e.selectedIndex,s="select-one"===e.type,a=s?null:[],u=s?i+1:o.length;for(r=i<0?u:s?i:0;r-1)&&(n=!0);return n||(e.selectedIndex=-1),i}}}}),E.each(["radio","checkbox"],function(){E.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=E.inArray(E(e).val(),t)>-1}},g.checkOn||(E.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var kt=n.location,Nt={guid:Date.now()},At=/\?/;E.parseXML=function(e){var t,r;if(!e||"string"!=typeof e)return null;try{t=(new n.DOMParser).parseFromString(e,"text/xml")}catch(e){}return r=t&&t.getElementsByTagName("parsererror")[0],t&&!r||E.error("Invalid XML: "+(r?E.map(r.childNodes,function(e){return e.textContent}).join("\n"):e)),t};var It=/^(?:focusinfocus|focusoutblur)$/,Ot=function(e){e.stopPropagation()};E.extend(E.event,{trigger:function(e,t,r,o){var i,s,a,u,l,c,d,p,h=[r||b],y=f.call(e,"type")?e.type:e,g=f.call(e,"namespace")?e.namespace.split("."):[];if(s=p=a=r=r||b,3!==r.nodeType&&8!==r.nodeType&&!It.test(y+E.event.triggered)&&(y.indexOf(".")>-1&&(y=(g=y.split(".")).shift(),g.sort()),l=y.indexOf(":")<0&&"on"+y,(e=e[E.expando]?e:new E.Event(y,"object"==typeof e&&e)).isTrigger=o?2:3,e.namespace=g.join("."),e.rnamespace=e.namespace?new RegExp("(^|\\.)"+g.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,e.result=void 0,e.target||(e.target=r),t=null==t?[e]:E.makeArray(t,[e]),d=E.event.special[y]||{},o||!d.trigger||!1!==d.trigger.apply(r,t))){if(!o&&!d.noBubble&&!m(r)){for(It.test((u=d.delegateType||y)+y)||(s=s.parentNode);s;s=s.parentNode)h.push(s),a=s;a===(r.ownerDocument||b)&&h.push(a.defaultView||a.parentWindow||n)}for(i=0;(s=h[i++])&&!e.isPropagationStopped();)p=s,e.type=i>1?u:d.bindType||y,(c=(se.get(s,"events")||Object.create(null))[e.type]&&se.get(s,"handle"))&&c.apply(s,t),(c=l&&s[l])&&c.apply&&oe(s)&&(e.result=c.apply(s,t),!1===e.result&&e.preventDefault());return e.type=y,o||e.isDefaultPrevented()||d._default&&!1!==d._default.apply(h.pop(),t)||!oe(r)||l&&v(r[y])&&!m(r)&&((a=r[l])&&(r[l]=null),E.event.triggered=y,e.isPropagationStopped()&&p.addEventListener(y,Ot),r[y](),e.isPropagationStopped()&&p.removeEventListener(y,Ot),E.event.triggered=void 0,a&&(r[l]=a)),e.result}},simulate:function(e,t,n){var r=E.extend(new E.Event,n,{type:e,isSimulated:!0});E.event.trigger(r,null,t)}}),E.fn.extend({trigger:function(e,t){return this.each(function(){E.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return E.event.trigger(e,t,n,!0)}});var Rt=/\[\]$/,Mt=/\r?\n/g,Dt=/^(?:submit|button|image|reset|file)$/i,Pt=/^(?:input|select|textarea|keygen)/i;function jt(e,t,n,r){var o;if(Array.isArray(t))E.each(t,function(t,o){n||Rt.test(e)?r(e,o):jt(e+"["+("object"==typeof o&&null!=o?t:"")+"]",o,n,r)});else if(n||"object"!==x(t))r(e,t);else for(o in t)jt(e+"["+o+"]",t[o],n,r)}E.param=function(e,t){var n,r=[],o=function(e,t){var n=v(t)?t():t;r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(null==n?"":n)};if(null==e)return"";if(Array.isArray(e)||e.jquery&&!E.isPlainObject(e))E.each(e,function(){o(this.name,this.value)});else for(n in e)jt(n,e[n],t,o);return r.join("&")},E.fn.extend({serialize:function(){return E.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=E.prop(this,"elements");return e?E.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!E(this).is(":disabled")&&Pt.test(this.nodeName)&&!Dt.test(e)&&(this.checked||!Ee.test(e))}).map(function(e,t){var n=E(this).val();return null==n?null:Array.isArray(n)?E.map(n,function(e){return{name:t.name,value:e.replace(Mt,"\r\n")}}):{name:t.name,value:n.replace(Mt,"\r\n")}}).get()}});var Lt=/%20/g,Vt=/#.*$/,Ft=/([?&])_=[^&]*/,Ht=/^(.*?):[ \t]*([^\r\n]*)$/gm,Bt=/^(?:GET|HEAD)$/,qt=/^\/\//,Ut={},zt={},$t="*/".concat("*"),Wt=b.createElement("a");function Zt(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,o=0,i=t.toLowerCase().match(W)||[];if(v(n))for(;r=i[o++];)"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function Gt(e,t,n,r){var o={},i=e===zt;function s(a){var u;return o[a]=!0,E.each(e[a]||[],function(e,a){var l=a(t,n,r);return"string"!=typeof l||i||o[l]?i?!(u=l):void 0:(t.dataTypes.unshift(l),s(l),!1)}),u}return s(t.dataTypes[0])||!o["*"]&&s("*")}function Qt(e,t){var n,r,o=E.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((o[n]?e:r||(r={}))[n]=t[n]);return r&&E.extend(!0,e,r),e}Wt.href=kt.href,E.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:kt.href,type:"GET",isLocal:/^(?:about|app|app-storage|.+-extension|file|res|widget):$/.test(kt.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":$t,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":E.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?Qt(Qt(e,E.ajaxSettings),t):Qt(E.ajaxSettings,e)},ajaxPrefilter:Zt(Ut),ajaxTransport:Zt(zt),ajax:function(e,t){"object"==typeof e&&(t=e,e=void 0);var r,o,i,s,a,u,l,c,d,p,f=E.ajaxSetup({},t=t||{}),h=f.context||f,y=f.context&&(h.nodeType||h.jquery)?E(h):E.event,g=E.Deferred(),v=E.Callbacks("once memory"),m=f.statusCode||{},w={},_={},x="canceled",C={readyState:0,getResponseHeader:function(e){var t;if(l){if(!s)for(s={};t=Ht.exec(i);)s[t[1].toLowerCase()+" "]=(s[t[1].toLowerCase()+" "]||[]).concat(t[2]);t=s[e.toLowerCase()+" "]}return null==t?null:t.join(", ")},getAllResponseHeaders:function(){return l?i:null},setRequestHeader:function(e,t){return null==l&&(e=_[e.toLowerCase()]=_[e.toLowerCase()]||e,w[e]=t),this},overrideMimeType:function(e){return null==l&&(f.mimeType=e),this},statusCode:function(e){var t;if(e)if(l)C.always(e[C.status]);else for(t in e)m[t]=[m[t],e[t]];return this},abort:function(e){var t=e||x;return r&&r.abort(t),S(0,t),this}};if(g.promise(C),f.url=((e||f.url||kt.href)+"").replace(qt,kt.protocol+"//"),f.type=t.method||t.type||f.method||f.type,f.dataTypes=(f.dataType||"*").toLowerCase().match(W)||[""],null==f.crossDomain){u=b.createElement("a");try{u.href=f.url,u.href=u.href,f.crossDomain=Wt.protocol+"//"+Wt.host!=u.protocol+"//"+u.host}catch(e){f.crossDomain=!0}}if(f.data&&f.processData&&"string"!=typeof f.data&&(f.data=E.param(f.data,f.traditional)),Gt(Ut,f,t,C),l)return C;for(d in(c=E.event&&f.global)&&0==E.active++&&E.event.trigger("ajaxStart"),f.type=f.type.toUpperCase(),f.hasContent=!Bt.test(f.type),o=f.url.replace(Vt,""),f.hasContent?f.data&&f.processData&&0===(f.contentType||"").indexOf("application/x-www-form-urlencoded")&&(f.data=f.data.replace(Lt,"+")):(p=f.url.slice(o.length),f.data&&(f.processData||"string"==typeof f.data)&&(o+=(At.test(o)?"&":"?")+f.data,delete f.data),!1===f.cache&&(o=o.replace(Ft,"$1"),p=(At.test(o)?"&":"?")+"_="+Nt.guid+++p),f.url=o+p),f.ifModified&&(E.lastModified[o]&&C.setRequestHeader("If-Modified-Since",E.lastModified[o]),E.etag[o]&&C.setRequestHeader("If-None-Match",E.etag[o])),(f.data&&f.hasContent&&!1!==f.contentType||t.contentType)&&C.setRequestHeader("Content-Type",f.contentType),C.setRequestHeader("Accept",f.dataTypes[0]&&f.accepts[f.dataTypes[0]]?f.accepts[f.dataTypes[0]]+("*"!==f.dataTypes[0]?", "+$t+"; q=0.01":""):f.accepts["*"]),f.headers)C.setRequestHeader(d,f.headers[d]);if(f.beforeSend&&(!1===f.beforeSend.call(h,C,f)||l))return C.abort();if(x="abort",v.add(f.complete),C.done(f.success),C.fail(f.error),r=Gt(zt,f,t,C)){if(C.readyState=1,c&&y.trigger("ajaxSend",[C,f]),l)return C;f.async&&f.timeout>0&&(a=n.setTimeout(function(){C.abort("timeout")},f.timeout));try{l=!1,r.send(w,S)}catch(e){if(l)throw e;S(-1,e)}}else S(-1,"No Transport");function S(e,t,s,u){var d,p,b,w,_,x=t;l||(l=!0,a&&n.clearTimeout(a),r=void 0,i=u||"",C.readyState=e>0?4:0,d=e>=200&&e<300||304===e,s&&(w=function(e,t,n){for(var r,o,i,s,a=e.contents,u=e.dataTypes;"*"===u[0];)u.shift(),void 0===r&&(r=e.mimeType||t.getResponseHeader("Content-Type"));if(r)for(o in a)if(a[o]&&a[o].test(r)){u.unshift(o);break}if(u[0]in n)i=u[0];else{for(o in n){if(!u[0]||e.converters[o+" "+u[0]]){i=o;break}s||(s=o)}i=i||s}if(i)return i!==u[0]&&u.unshift(i),n[i]}(f,C,s)),!d&&E.inArray("script",f.dataTypes)>-1&&E.inArray("json",f.dataTypes)<0&&(f.converters["text script"]=function(){}),w=function(e,t,n,r){var o,i,s,a,u,l={},c=e.dataTypes.slice();if(c[1])for(s in e.converters)l[s.toLowerCase()]=e.converters[s];for(i=c.shift();i;)if(e.responseFields[i]&&(n[e.responseFields[i]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=i,i=c.shift())if("*"===i)i=u;else if("*"!==u&&u!==i){if(!(s=l[u+" "+i]||l["* "+i]))for(o in l)if((a=o.split(" "))[1]===i&&(s=l[u+" "+a[0]]||l["* "+a[0]])){!0===s?s=l[o]:!0!==l[o]&&(i=a[0],c.unshift(a[1]));break}if(!0!==s)if(s&&e.throws)t=s(t);else try{t=s(t)}catch(e){return{state:"parsererror",error:s?e:"No conversion from "+u+" to "+i}}}return{state:"success",data:t}}(f,w,C,d),d?(f.ifModified&&((_=C.getResponseHeader("Last-Modified"))&&(E.lastModified[o]=_),(_=C.getResponseHeader("etag"))&&(E.etag[o]=_)),204===e||"HEAD"===f.type?x="nocontent":304===e?x="notmodified":(x=w.state,p=w.data,d=!(b=w.error))):(b=x,!e&&x||(x="error",e<0&&(e=0))),C.status=e,C.statusText=(t||x)+"",d?g.resolveWith(h,[p,x,C]):g.rejectWith(h,[C,x,b]),C.statusCode(m),m=void 0,c&&y.trigger(d?"ajaxSuccess":"ajaxError",[C,f,d?p:b]),v.fireWith(h,[C,x]),c&&(y.trigger("ajaxComplete",[C,f]),--E.active||E.event.trigger("ajaxStop")))}return C},getJSON:function(e,t,n){return E.get(e,t,n,"json")},getScript:function(e,t){return E.get(e,void 0,t,"script")}}),E.each(["get","post"],function(e,t){E[t]=function(e,n,r,o){return v(n)&&(o=o||r,r=n,n=void 0),E.ajax(E.extend({url:e,type:t,dataType:o,data:n,success:r},E.isPlainObject(e)&&e))}}),E.ajaxPrefilter(function(e){var t;for(t in e.headers)"content-type"===t.toLowerCase()&&(e.contentType=e.headers[t]||"")}),E._evalUrl=function(e,t,n){return E.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,converters:{"text script":function(){}},dataFilter:function(e){E.globalEval(e,t,n)}})},E.fn.extend({wrapAll:function(e){var t;return this[0]&&(v(e)&&(e=e.call(this[0])),t=E(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){for(var e=this;e.firstElementChild;)e=e.firstElementChild;return e}).append(this)),this},wrapInner:function(e){return v(e)?this.each(function(t){E(this).wrapInner(e.call(this,t))}):this.each(function(){var t=E(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=v(e);return this.each(function(n){E(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(e){return this.parent(e).not("body").each(function(){E(this).replaceWith(this.childNodes)}),this}}),E.expr.pseudos.hidden=function(e){return!E.expr.pseudos.visible(e)},E.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},E.ajaxSettings.xhr=function(){try{return new n.XMLHttpRequest}catch(e){}};var Xt={0:200,1223:204},Yt=E.ajaxSettings.xhr();g.cors=!!Yt&&"withCredentials"in Yt,g.ajax=Yt=!!Yt,E.ajaxTransport(function(e){var t,r;if(g.cors||Yt&&!e.crossDomain)return{send:function(o,i){var s,a=e.xhr();if(a.open(e.type,e.url,e.async,e.username,e.password),e.xhrFields)for(s in e.xhrFields)a[s]=e.xhrFields[s];for(s in e.mimeType&&a.overrideMimeType&&a.overrideMimeType(e.mimeType),e.crossDomain||o["X-Requested-With"]||(o["X-Requested-With"]="XMLHttpRequest"),o)a.setRequestHeader(s,o[s]);t=function(e){return function(){t&&(t=r=a.onload=a.onerror=a.onabort=a.ontimeout=a.onreadystatechange=null,"abort"===e?a.abort():"error"===e?"number"!=typeof a.status?i(0,"error"):i(a.status,a.statusText):i(Xt[a.status]||a.status,a.statusText,"text"!==(a.responseType||"text")||"string"!=typeof a.responseText?{binary:a.response}:{text:a.responseText},a.getAllResponseHeaders()))}},a.onload=t(),r=a.onerror=a.ontimeout=t("error"),void 0!==a.onabort?a.onabort=r:a.onreadystatechange=function(){4===a.readyState&&n.setTimeout(function(){t&&r()})},t=t("abort");try{a.send(e.hasContent&&e.data||null)}catch(e){if(t)throw e}},abort:function(){t&&t()}}}),E.ajaxPrefilter(function(e){e.crossDomain&&(e.contents.script=!1)}),E.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return E.globalEval(e),e}}}),E.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),E.ajaxTransport("script",function(e){var t,n;if(e.crossDomain||e.scriptAttrs)return{send:function(r,o){t=E(" diff --git a/modules/weko-theme/weko_theme/templates/weko_theme/frontpage.html b/modules/weko-theme/weko_theme/templates/weko_theme/frontpage.html index c7cfe6bd74..316958d65a 100644 --- a/modules/weko-theme/weko_theme/templates/weko_theme/frontpage.html +++ b/modules/weko-theme/weko_theme/templates/weko_theme/frontpage.html @@ -20,7 +20,7 @@ {%- extends config.BASE_PAGE_TEMPLATE %} -{%- from "invenio_communities/macros.html" import community_header %} +{%- from "invenio_communities/macros.html" import community_header with context %} {%- block css %} {{ super() }} diff --git a/modules/weko-theme/weko_theme/templates/weko_theme/macros/footer-community.html b/modules/weko-theme/weko_theme/templates/weko_theme/macros/footer-community.html index 7d39ffba09..be7efae500 100644 --- a/modules/weko-theme/weko_theme/templates/weko_theme/macros/footer-community.html +++ b/modules/weko-theme/weko_theme/templates/weko_theme/macros/footer-community.html @@ -18,7 +18,7 @@ # MA 02111-1307, USA. #} -{%- from "invenio_communities/macros.html" import community_footer %} +{%- from "invenio_communities/macros.html" import community_footer with context %} {% macro community_footer_widget(render_widgets, community, link, subtitle) %} {%- if render_widgets %} @@ -29,4 +29,4 @@
    {{ community_footer(community, link, subtitle) }}
    -{% endmacro %} \ No newline at end of file +{% endmacro %} diff --git a/modules/weko-theme/weko_theme/templates/weko_theme/macros/tabs_selector.html b/modules/weko-theme/weko_theme/templates/weko_theme/macros/tabs_selector.html index 2c40cfb5e6..10b7ad0846 100644 --- a/modules/weko-theme/weko_theme/templates/weko_theme/macros/tabs_selector.html +++ b/modules/weko-theme/weko_theme/templates/weko_theme/macros/tabs_selector.html @@ -20,7 +20,7 @@ {% macro tabs_selector(tab_value='top',community_id='') %} {%- if community_id %} -
  • {{ _('Top') }}
  • +
  • {{ _('Top') }}
  • {%- if current_user.is_authenticated and current_user.roles %}
  • {{ _('WorkFlow') }}
  • {%- endif %} @@ -34,4 +34,7 @@ {% endif %} {%- endif %} + {%- if community_id %} +
  • コンテンツポリシー
  • + {% endif %} {% endmacro %} diff --git a/modules/weko-theme/weko_theme/utils.py b/modules/weko-theme/weko_theme/utils.py index 9ff8be78c8..6ab2e383b8 100644 --- a/modules/weko-theme/weko_theme/utils.py +++ b/modules/weko-theme/weko_theme/utils.py @@ -90,7 +90,7 @@ def get_weko_contents(getargs): ctx.update({ "display_community": display_community }) - + return dict( community_id=community_id, detail_condition=detail_condition, @@ -105,9 +105,9 @@ def get_community_id(getargs): # TODO: Use this to refactor """Get the community data for specific args.""" ctx = {'community': None} community_id = "" - if 'community' in getargs: + if 'c' in getargs: from weko_workflow.api import GetCommunity - comm = GetCommunity.get_community_by_id(getargs.get('community')) + comm = GetCommunity.get_community_by_id(getargs.get('c')) ctx = {'community': comm} if comm is not None: community_id = comm.id @@ -126,7 +126,7 @@ def get_design_layout(repository_id): Returns: _type_: page, render_widgets - """ + """ if not repository_id: return None, False @@ -271,7 +271,7 @@ def __index_search_result(cls, init_disp_index, init_disp_index_disp_method, display_format = current_index.display_format if display_format == '2': display_number = 100 - + if not init_disp_index: # In case is not found the index # set main screen initial display to the default diff --git a/modules/weko-workflow/tests/conftest.py b/modules/weko-workflow/tests/conftest.py index a5ae91b692..1c3e8bec27 100644 --- a/modules/weko-workflow/tests/conftest.py +++ b/modules/weko-workflow/tests/conftest.py @@ -753,7 +753,8 @@ def users(app, db): comm = Community.create(community_id="comm01", role_id=sysadmin_role.id, id_user=sysadmin.id, title="test community", description=("this is test community"), - root_node_id=index.id) + root_node_id=index.id, + group_id=comadmin_role.id) db.session.commit() return [ {'email': contributor.email, 'id': contributor.id, 'obj': contributor}, diff --git a/modules/weko-workflow/tests/test_admin.py b/modules/weko-workflow/tests/test_admin.py index a06e9add15..ad4c509cd0 100644 --- a/modules/weko-workflow/tests/test_admin.py +++ b/modules/weko-workflow/tests/test_admin.py @@ -67,6 +67,26 @@ def test_flow_detail_acl(self,client,workflow,db_register2,users,users_index,sta with patch("flask.templating._render", return_value=""): res = client.get(url) assert res.status_code == status_code + + # def flow_detail(self, flow_id='0'): + # .tox/c1/bin/pytest --cov=weko_workflow tests/test_admin.py::TestFlowSettingView::test_flow_detail_return_repositories -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-workflow/.tox/c1/tmp + def test_flow_detail_return_repositories(self,client,workflow,users): + flow_define = workflow['flow'] + url = '/admin/flowsetting/{}'.format(flow_define.flow_id) + login(client=client, email=users[2]['email']) + with patch("flask.templating._render", return_value="") as mock_render: + res = client.get(url) + assert res.status_code == 200 + args, kwargs = mock_render.call_args + assert len(args[1]["repositories"]) == 2 + + url = '/admin/flowsetting/{}'.format(0) + login(client=client, email=users[3]['email']) + with patch("flask.templating._render", return_value="") as mock_render: + res = client.get(url) + assert res.status_code == 200 + args, kwargs = mock_render.call_args + assert len(args[1]["repositories"]) == 1 # def flow_detail(self, flow_id='0'): # def new_flow(self, flow_id='0'): @@ -107,6 +127,7 @@ def test_new_flow(self,app,client,action_data,users): url = '/admin/flowsetting/{}'.format(0) q = FlowDefine.query.all() assert len(q) == 0 + with patch("flask.templating._render", return_value=""): res = client.post(url) assert res.status_code == 500 @@ -122,7 +143,7 @@ def test_new_flow(self,app,client,action_data,users): q = FlowDefine.query.all() assert len(q) == 0 - data = {"flow_name": "test1"} + data = {"flow_name": "test1", "repository_id": "Root Index"} login(client=client, email=users[1]['email']) url = '/admin/flowsetting/{}'.format(0) with patch("flask.templating._render", return_value=""): @@ -131,7 +152,7 @@ def test_new_flow(self,app,client,action_data,users): q = FlowDefine.query.all() assert len(q) == 1 - data = {"flow_name": "test1"} + data = {"flow_name": "test1", "repository_id": "Root Index"} login(client=client, email=users[1]['email']) url = '/admin/flowsetting/{}'.format(0) with patch("flask.templating._render", return_value=""): @@ -141,7 +162,7 @@ def test_new_flow(self,app,client,action_data,users): assert len(q) == 1 flow_id = q[0].flow_id - data = {"flow_name": "test2"} + data = {"flow_name": "test2", "repository_id": "Root Index"} login(client=client, email=users[1]['email']) url = '/admin/flowsetting/{}'.format(flow_id) with patch("flask.templating._render", return_value=""): @@ -159,7 +180,7 @@ def test_new_flow(self,app,client,action_data,users): q = FlowDefine.query.first() assert q.flow_name == 'test2' - data = {"flow_name": "test1"} + data = {"flow_name": "test1", "repository_id": "Root Index"} login(client=client, email=users[1]['email']) url = '/admin/flowsetting/{}'.format(0) with patch("flask.templating._render", return_value=""): @@ -168,7 +189,7 @@ def test_new_flow(self,app,client,action_data,users): q = FlowDefine.query.all() assert len(q) == 2 - data = {"flow_name": "test1"} + data = {"flow_name": "test1", "repository_id": "Root Index"} login(client=client, email=users[1]['email']) url = '/admin/flowsetting/{}'.format(flow_id) with patch("flask.templating._render", return_value=""): @@ -304,7 +325,7 @@ def test_index_acl_guest(self,client,db_register2): (0, 403), (1, 200), (2, 200), - (3, 403), + (3, 200), (4, 403), (5, 403), (6, 200), @@ -363,6 +384,21 @@ def test_workflow_detail_acl(self,app ,client,db_register,workflow_open_restrict res = client.get(url) assert res.status_code == status_code + # .tox/c1/bin/pytest --cov=weko_workflow tests/test_admin.py::TestWorkFlowSettingView::test_workflow_detail_return_repositories -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-workflow/.tox/c1/tmp + def test_workflow_detail_return_repositories(self,app ,client, users, workflow, mocker): + login(client=client, email=users[2]['email']) + url = url_for('workflowsetting.workflow_detail',workflow_id='0') + mock_render =mocker.patch("flask.templating._render", return_value=make_response()) + res = client.get(url) + args, kwargs = mock_render.call_args + assert len(args[1]["repositories"]) == 2 + + login(client=client, email=users[3]['email']) + url = url_for('workflowsetting.workflow_detail',workflow_id='0') + mock_render =mocker.patch("flask.templating._render", return_value=make_response()) + res = client.get(url) + args, kwargs = mock_render.call_args + assert len(args[1]["repositories"]) == 1 # def update_workflow(self, workflow_id='0'): diff --git a/modules/weko-workflow/tests/test_api.py b/modules/weko-workflow/tests/test_api.py index 33891e6c16..45f5a51b6e 100644 --- a/modules/weko-workflow/tests/test_api.py +++ b/modules/weko-workflow/tests/test_api.py @@ -1,4 +1,91 @@ +import uuid + from flask_login.utils import login_user +import pytest +from unittest.mock import patch + +from weko_workflow.api import Flow, WorkActivity, WorkFlow, GetCommunity + +# .tox/c1/bin/pytest --cov=weko_workflow tests/test_api.py::test_Flow_create_flow -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-workflow/.tox/c1/tmp +def test_Flow_create_flow(app, client, users, db, action_data): + with app.test_request_context(): + _flow = Flow() + + flow = _flow.create_flow({'flow_name': 'create_flow_test_root', 'repository_id': 'Root Index'}) + assert flow.flow_name == 'create_flow_test_root' + assert flow.repository_id == 'Root Index' + + flow = _flow.create_flow({'flow_name': 'create_flow_test_1', 'repository_id': 'comm01'}) + assert flow.flow_name == 'create_flow_test_1' + assert flow.repository_id == 'comm01' + + with pytest.raises(ValueError, match='Flow name cannot be empty.'): + _flow.create_flow({'flow_name': '', 'repository_id': 'com1'}) + + with pytest.raises(ValueError, match='Repository cannot be empty.'): + _flow.create_flow({'flow_name': 'test_flow'}) + + with pytest.raises(ValueError, match='Flow name is already in use.'): + _flow.create_flow({'flow_name': 'create_flow_test_root', 'repository_id': 'Root Index'}) + + with pytest.raises(ValueError, match='Repository is not found.'): + _flow.create_flow({'flow_name': 'test_flow', 'repository_id': '999'}) + +# .tox/c1/bin/pytest --cov=weko_workflow tests/test_api.py::test_Flow_upt_flow -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-workflow/.tox/c1/tmp +def test_Flow_upt_flow(app, client, users, db, action_data): + with app.test_request_context(): + _flow = Flow() + flow = _flow.create_flow({'flow_name': 'upt_flow_test', 'repository_id': 'Root Index'}) + + flow = _flow.upt_flow(flow.flow_id, {'flow_name': 'upt_flow_test_1', 'repository_id': 'Root Index'}) + assert flow.flow_name == 'upt_flow_test_1' + assert flow.repository_id == 'Root Index' + + flow = _flow.upt_flow(flow.flow_id, {'flow_name': 'upt_flow_test_1', 'repository_id': 'comm01'}) + assert flow.flow_name == 'upt_flow_test_1' + assert flow.repository_id == 'comm01' + + with pytest.raises(ValueError, match='Flow name cannot be empty.'): + _flow.upt_flow(flow.flow_id, {'flow_name': '', 'repository_id': 'Root Index'}) + + with pytest.raises(ValueError, match='Repository cannot be empty.'): + _flow.upt_flow(flow.flow_id, {'flow_name': 'upt_flow_test_1', 'repository_id': ''}) + + flow1 = _flow.create_flow({'flow_name': 'upt_flow_test', 'repository_id': 'comm01'}) + db.session.add(flow) + db.session.add(flow1) + db.session.commit() + with pytest.raises(ValueError, match='Flow name is already in use.'): + _flow.upt_flow(flow.flow_id, {'flow_name': 'upt_flow_test', 'repository_id': 'Root Index'}) + + with pytest.raises(ValueError, match='Repository is not found.'): + _flow.upt_flow(flow.flow_id, {'flow_name': 'test_flow', 'repository_id': '999'}) + +# .tox/c1/bin/pytest --cov=weko_workflow tests/test_api.py::test_Flow_get_flow_list -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-workflow/.tox/c1/tmp +def test_Flow_get_flow_list(app, client, users, db, action_data): + with app.test_request_context(): + _flow = Flow() + flow = _flow.create_flow({'flow_name': 'get_flow_list_test', 'repository_id': 'Root Index'}) + db.session.add(flow) + db.session.commit() + + login_user(users[2]["obj"]) + res = _flow.get_flow_list() + assert len(res) == 1 + assert res[0] == flow + + login_user(users[3]["obj"]) + res = _flow.get_flow_list() + assert len(res) == 0 + + flow_com = _flow.create_flow({'flow_name': 'flow_comm01', 'repository_id': 'comm01'}) + db.session.add(flow_com) + db.session.commit() + + res = _flow.get_flow_list() + assert len(res) == 1 + assert res[0] == flow_com + from weko_workflow.api import Flow, WorkActivity, _WorkFlow,WorkFlow @@ -7,7 +94,7 @@ def test_Flow_action(app, client, users, db, action_data): with app.test_request_context(): login_user(users[2]["obj"]) _flow = Flow() - flow = _flow.create_flow({'flow_name': 'create_flow_test'}) + flow = _flow.create_flow({'flow_name': 'create_flow_test', 'repository_id': 'Root Index'}) assert flow.flow_name == 'create_flow_test' _flow_data = [ @@ -83,6 +170,51 @@ def test_Flow_get_flow_action_list(db,workflow): assert res[5].action_order == 6 assert res[6].action_order == 7 +# .tox/c1/bin/pytest --cov=weko_workflow tests/test_api.py::test_WorkFlow_upt_workflow -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-workflow/.tox/c1/tmp +def test_WorkFlow_upt_workflow(app, db, workflow): + w = workflow["workflow"] + _workflow = WorkFlow() + data = dict(flows_id=w.flows_id, + flows_name='test workflow01', + itemtype_id=1, + flow_id=1, + index_tree_id=None, + open_restricted=False, + location_id=None, + is_gakuninrdm=False, + repository_id='Root Index') + + res = _workflow.upt_workflow(data) + for key in data: + assert getattr(res, key) == data[key] + + res = _workflow.upt_workflow({'flows_id': uuid.uuid4()}) + assert res is None + + with pytest.raises(AssertionError): + _workflow.upt_workflow(None) + +# .tox/c1/bin/pytest --cov=weko_workflow tests/test_api.py::test_WorkFlow_get_workflow_list -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-workflow/.tox/c1/tmp +def test_WorkFlow_get_workflow_list(app, db, workflow, users): + w = workflow["workflow"] + _workflow = WorkFlow() + res = _workflow.get_workflow_list() + assert len(res) == 1 + + user = users[2]["obj"] + res = _workflow.get_workflow_list(user=user) + assert len(res) == 1 + + user = users[3]["obj"] + res = _workflow.get_workflow_list(user=user) + assert len(res) == 0 + + w.repository_id = "comm01" + db.session.commit() + user = users[3]["obj"] + res = _workflow.get_workflow_list(user=user) + assert len(res) == 1 + # .tox/c1/bin/pytest --cov=weko_workflow tests/test_api.py::test_WorkActivity_filter_by_date -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-workflow/.tox/c1/tmp def test_WorkActivity_filter_by_date(app, db): query = db.session.query() @@ -135,6 +267,11 @@ def test_WorkActivity_get_corresponding_usage_activities(app, db_register): assert usage_application_list == {'activity_data_type': {}, 'activity_ids': []} assert output_report_list == {'activity_data_type': {}, 'activity_ids': []} +# .tox/c1/bin/pytest --cov=weko_workflow tests/test_api.py::test_GetCommunity_get_community_by_root_node_id -vv -s --cov-branch --cov-report=term --basetemp=/code/modules/weko-workflow/.tox/c1/tmp +def test_GetCommunity_get_community_by_root_node_id(db): + communities = GetCommunity.get_community_by_root_node_id(1738541618993) + assert communities is not None + # .tox/c1/bin/pytest --cov=weko_workflow tests/test_api.py::test_get_deleted_workflow_list -vv -s --cov-branch --cov-report=term --cov-report=html --basetemp=/code/modules/weko-workflow/.tox/c1/tmp def test_get_deleted_workflow_list(app,db,workflow): res = WorkFlow().get_deleted_workflow_list() diff --git a/modules/weko-workflow/weko_workflow/admin.py b/modules/weko-workflow/weko_workflow/admin.py index d6efef7635..4fa8b4e1cf 100644 --- a/modules/weko-workflow/weko_workflow/admin.py +++ b/modules/weko-workflow/weko_workflow/admin.py @@ -28,6 +28,7 @@ from flask_login import current_user from flask_babelex import gettext as _ from invenio_accounts.models import Role, User +from invenio_communities.models import Community from invenio_db import db from invenio_files_rest.models import Location from invenio_i18n.ext import current_i18n @@ -64,6 +65,11 @@ def flow_detail(self, flow_id='0'): """ users = User.query.filter_by(active=True).all() roles = Role.query.all() + if set(role.name for role in current_user.roles) & \ + set(current_app.config['WEKO_PERMISSION_SUPER_ROLE_USER']): + repositories = [{"id": "Root Index"}] + Community.query.all() + else: + repositories = Community.get_repositories_by_user(current_user) actions = self.get_actions() if '0' == flow_id: flow = None @@ -75,7 +81,8 @@ def flow_detail(self, flow_id='0'): users=users, roles=roles, actions=None, - action_list=actions + action_list=actions, + repositories=repositories ) UUID_PATTERN = re.compile(r'^[\da-f]{8}-([\da-f]{4}-){3}[\da-f]{12}$', re.IGNORECASE) @@ -97,7 +104,8 @@ def flow_detail(self, flow_id='0'): roles=roles, actions=flow.flow_actions, action_list=actions, - specifed_properties=specified_properties + specifed_properties=specified_properties, + repositories=repositories ) @staticmethod @@ -280,7 +288,7 @@ def index(self): :return: """ workflow = WorkFlow() - workflows = workflow.get_workflow_list() + workflows = workflow.get_workflow_list(user=current_user) role = Role.query.all() for wf in workflows: index_tree = Index().get_index_by_id(wf.index_tree_id) @@ -324,6 +332,11 @@ def workflow_detail(self, workflow_id='0'): display_label = self.get_language_workflows("display") hide_label = self.get_language_workflows("hide") display_hide = self.get_language_workflows("display_hide") + if set(role.name for role in current_user.roles) & \ + set(current_app.config['WEKO_PERMISSION_SUPER_ROLE_USER']): + repositories = [{"id": "Root Index"}] + Community.query.all() + else: + repositories = Community.get_repositories_by_user(current_user) # the workflow that open_restricted is true can update by system administrator only is_sysadmin = False @@ -347,6 +360,7 @@ def workflow_detail(self, workflow_id='0'): hide_label=hide_label, display_hide_label=display_hide, is_sysadmin=is_sysadmin, + repositories=repositories ) """Update the workflow info""" @@ -377,7 +391,9 @@ def workflow_detail(self, workflow_id='0'): display_label=display_label, hide_label=hide_label, display_hide_label=display_hide, - is_sysadmin=is_sysadmin + is_sysadmin=is_sysadmin, + repositories=repositories + ) @expose('/', methods=['POST', 'PUT']) @@ -395,7 +411,8 @@ def update_workflow(self, workflow_id='0'): index_tree_id=json_data.get('index_id'), location_id=json_data.get('location_id'), open_restricted=json_data.get('open_restricted', False), - is_gakuninrdm=json_data.get('is_gakuninrdm') + is_gakuninrdm=json_data.get('is_gakuninrdm'), + repository_id=json_data.get('repository_id', None), ) workflow = WorkFlow() try: @@ -404,6 +421,8 @@ def update_workflow(self, workflow_id='0'): form_workflow.update( flows_id=uuid.uuid4() ) + if form_workflow['repository_id'] == None: + form_workflow.pop('repository_id') workflow.create_workflow(form_workflow) workflow_detail = workflow.get_workflow_by_flows_id( form_workflow.get('flows_id')) diff --git a/modules/weko-workflow/weko_workflow/alembic/f312b8c2839a_add_columns.py b/modules/weko-workflow/weko_workflow/alembic/f312b8c2839a_add_columns.py new file mode 100644 index 0000000000..7014cd3f82 --- /dev/null +++ b/modules/weko-workflow/weko_workflow/alembic/f312b8c2839a_add_columns.py @@ -0,0 +1,30 @@ +# +# This file is part of Invenio. +# Copyright (C) 2016-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. + +"""Add columns""" + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'f312b8c2839a' +down_revision = '2084f52e00b7' +branch_labels = () +depends_on = None + + +def upgrade(): + """Upgrade database.""" + op.add_column('workflow_flow_define', sa.Column('repository_id', sa.String(length=100), nullable=False, server_default='Root Index')) + op.add_column('workflow_workflow', sa.Column('repository_id', sa.String(length=100), nullable=False, server_default='Root Index')) + + +def downgrade(): + """Downgrade database.""" + op.drop_column('workflow_workflow', 'repository_id') + op.drop_column('workflow_flow_define', 'repository_id') diff --git a/modules/weko-workflow/weko_workflow/api.py b/modules/weko-workflow/weko_workflow/api.py index a82a5cabe5..d838c18a1e 100644 --- a/modules/weko-workflow/weko_workflow/api.py +++ b/modules/weko-workflow/weko_workflow/api.py @@ -30,6 +30,7 @@ from flask import abort, current_app, request, session, url_for from flask_login import current_user from invenio_accounts.models import Role, User, userrole +from invenio_communities.models import Community from invenio_db import db from invenio_pidstore.models import PersistentIdentifier, PIDStatus from sqlalchemy import and_, asc, desc, func, or_ @@ -72,6 +73,10 @@ def create_flow(self, flow): if not flow_name: raise ValueError('Flow name cannot be empty.') + repository_id = flow.get('repository_id') + if not repository_id: + raise ValueError('Repository cannot be empty.') + with db.session.no_autoflush: cur_names = map( lambda flow: flow.flow_name, @@ -80,14 +85,21 @@ def create_flow(self, flow): if flow_name in cur_names: raise ValueError('Flow name is already in use.') + if repository_id != "Root Index": + repository = Community.query.filter_by(id=repository_id).one_or_none() + if not repository: + raise ValueError('Repository is not found.') + action_start = _Action.query.filter_by( action_endpoint='begin_action').one_or_none() action_end = _Action.query.filter_by( action_endpoint='end_action').one_or_none() + _flow = _Flow( flow_id=uuid.uuid4(), flow_name=flow_name, - flow_user=current_user.get_id() + flow_user=current_user.get_id(), + repository_id=repository_id, ) _flowaction_start = _FlowAction( flow_id=_flow.flow_id, @@ -124,6 +136,10 @@ def upt_flow(self, flow_id, flow): if not flow_name: raise ValueError('Flow name cannot be empty.') + repository_id = flow.get('repository_id') + if not repository_id: + raise ValueError('Repository cannot be empty.') + with db.session.begin_nested(): # Get all names but the one being updated cur_names = map( @@ -133,12 +149,18 @@ def upt_flow(self, flow_id, flow): ) if flow_name in cur_names: raise ValueError('Flow name is already in use.') + + if repository_id != "Root Index": + repository = Community.query.filter_by(id=repository_id).one_or_none() + if not repository: + raise ValueError('Repository is not found.') _flow = _Flow.query.filter_by( flow_id=flow_id).one_or_none() if _flow: _flow.flow_name = flow_name - _flow.flow_user = current_user.get_id() + _flow.flow_user = current_user.get_id(), + _flow.repository_id = repository_id db.session.merge(_flow) db.session.commit() return _flow @@ -153,8 +175,13 @@ def get_flow_list(self): :return: """ with db.session.no_autoflush: - query = _Flow.query.filter_by( - is_deleted=False).order_by(asc(_Flow.flow_id)) + query = _Flow.query.filter_by(is_deleted=False) + if any(role.name in current_app.config['WEKO_PERMISSION_SUPER_ROLE_USER'] for role in current_user.roles): + query = query.order_by(asc(_Flow.flow_id)) + else: + role_ids = [role.id for role in current_user.roles] + repository_ids = [community.id for community in Community.query.filter(Community.group_id.in_(role_ids)).all()] + query = query.filter(_Flow.repository_id.in_(repository_ids)).order_by(asc(_Flow.flow_id)) return query.all() def get_flow_detail(self, flow_id): @@ -412,6 +439,7 @@ def upt_workflow(self, workflow): _workflow.open_restricted = workflow.get('open_restricted') _workflow.location_id = workflow.get('location_id') _workflow.is_gakuninrdm = workflow.get('is_gakuninrdm') + _workflow.repository_id = workflow.get('repository_id') if workflow.get('repository_id') else _workflow.repository_id db.session.merge(_workflow) db.session.commit() return _workflow @@ -420,14 +448,21 @@ def upt_workflow(self, workflow): current_app.logger.exception(str(ex)) return None - def get_workflow_list(self): + def get_workflow_list(self, user=None): """Get workflow list info. :return: """ with db.session.no_autoflush: - query = _WorkFlow.query.filter_by( - is_deleted=False).order_by(asc(_WorkFlow.flows_id)) + query = _WorkFlow.query.filter_by(is_deleted=False) + if not user: + return query.order_by(asc(_WorkFlow.flows_id)).all() + if any(role.name in current_app.config['WEKO_PERMISSION_SUPER_ROLE_USER'] for role in user.roles): + query = query.order_by(asc(_WorkFlow.flows_id)) + else: + role_ids = [role.id for role in user.roles] + repository_ids = [community.id for community in Community.query.filter(Community.group_id.in_(role_ids)).all()] + query = query.filter(_WorkFlow.repository_id.in_(repository_ids)).order_by(asc(_WorkFlow.flows_id)) return query.all() def get_deleted_workflow_list(self): @@ -2826,3 +2861,10 @@ def get_community_by_id(cls, community_id): from invenio_communities.models import Community c = Community.get(community_id) return c + + @classmethod + def get_community_by_root_node_id(cls, root_node_id): + """Get Community by ID.""" + from invenio_communities.models import Community + c = Community.get_by_root_node_id(root_node_id) + return c diff --git a/modules/weko-workflow/weko_workflow/models.py b/modules/weko-workflow/weko_workflow/models.py index 07adcd944f..55f89080e7 100644 --- a/modules/weko-workflow/weko_workflow/models.py +++ b/modules/weko-workflow/weko_workflow/models.py @@ -501,6 +501,9 @@ class FlowDefine(db.Model, TimestampMixin): is_deleted = db.Column(db.Boolean(name='is_deleted'), nullable=False, default=False) """flow define delete flag.""" + + repository_id = db.Column(db.String(100), nullable=False, default="Root Index") + """the repository id of flow.""" class FlowAction(db.Model, TimestampMixin): @@ -673,6 +676,9 @@ class WorkFlow(db.Model, TimestampMixin): is_gakuninrdm = db.Column(db.Boolean(name='is_gakuninrdm'), nullable=False, default=False) """GakuninRDM flag.""" + + repository_id = db.Column(db.String(100), nullable=False, default="Root Index") + """the repository id of workflow.""" class Activity(db.Model, TimestampMixin): diff --git a/modules/weko-workflow/weko_workflow/static/js/weko_workflow/admin/flow_detail.js b/modules/weko-workflow/weko_workflow/static/js/weko_workflow/admin/flow_detail.js index 8a7d8d5b1d..233dc1e772 100644 --- a/modules/weko-workflow/weko_workflow/static/js/weko_workflow/admin/flow_detail.js +++ b/modules/weko-workflow/weko_workflow/static/js/weko_workflow/admin/flow_detail.js @@ -146,6 +146,7 @@ $(document).ready(function () { }); $('#btn-new-flow').on('click', function () { let flow_name = $('#txt_flow_name').val(); + let repository_id = $('#txt_repo_id').val(); if (flow_name.length == 0) { $('#div_flow_name').addClass('has-error'); $('#txt_flow_name').focus(); @@ -156,7 +157,10 @@ $(document).ready(function () { method: 'POST', async: true, contentType: 'application/json', - data: JSON.stringify({ 'flow_name': flow_name }), + data: JSON.stringify({ + 'flow_name': flow_name, + 'repository_id': repository_id + }), success: function (data, status) { if (data.code == 0) { document.location.href = data.data.redirect; @@ -175,6 +179,7 @@ $(document).ready(function () { }); $('#btn-upt-flow').on('click', function () { let flow_name = $('#txt_flow_name').val(); + let repository_id = $('#txt_repo_id').val(); if (flow_name.length == 0) { $('#div_flow_name').addClass('has-error'); $('#txt_flow_name').focus(); @@ -185,7 +190,10 @@ $(document).ready(function () { method: 'POST', async: true, contentType: 'application/json', - data: JSON.stringify({ 'flow_name': flow_name }), + data: JSON.stringify({ + 'flow_name': flow_name, + 'repository_id': repository_id + }), success: function (data, status) { document.querySelectorAll('#inputModal').forEach(element => { element.innerHTML = data.msg diff --git a/modules/weko-workflow/weko_workflow/static/js/weko_workflow/admin/workflow_detail.js b/modules/weko-workflow/weko_workflow/static/js/weko_workflow/admin/workflow_detail.js index ecc8ef7188..48f8cb4551 100644 --- a/modules/weko-workflow/weko_workflow/static/js/weko_workflow/admin/workflow_detail.js +++ b/modules/weko-workflow/weko_workflow/static/js/weko_workflow/admin/workflow_detail.js @@ -67,6 +67,7 @@ $("#btn_create").on("click", function () { id: $("#_id").val(), flows_name: $("#txt_workflow_name").val(), itemtype_id: $("#txt_itemtype").val(), + repository_id: $("#txt_repo_id").val(), flow_id: $("#txt_flow_name").val(), list_hide: list_hide, open_restricted: $('#restricted_access_flag')?.is(":checked"), diff --git a/modules/weko-workflow/weko_workflow/templates/weko_workflow/activity_detail.html b/modules/weko-workflow/weko_workflow/templates/weko_workflow/activity_detail.html index 25eef9b3a1..89cc74bffa 100644 --- a/modules/weko-workflow/weko_workflow/templates/weko_workflow/activity_detail.html +++ b/modules/weko-workflow/weko_workflow/templates/weko_workflow/activity_detail.html @@ -19,7 +19,7 @@ #} {%- extends config.WEKO_WORKFLOW_BASE_TEMPLATE %} -{%- from "invenio_communities/macros.html" import community_header %} +{%- from "invenio_communities/macros.html" import community_header with context %} {%- block css %} {{ super() }} diff --git a/modules/weko-workflow/weko_workflow/templates/weko_workflow/activity_list.html b/modules/weko-workflow/weko_workflow/templates/weko_workflow/activity_list.html index a812c9232e..ef420de6e5 100644 --- a/modules/weko-workflow/weko_workflow/templates/weko_workflow/activity_list.html +++ b/modules/weko-workflow/weko_workflow/templates/weko_workflow/activity_list.html @@ -19,7 +19,7 @@ #} {%- extends config.WEKO_WORKFLOW_BASE_TEMPLATE %} -{%- from "invenio_communities/macros.html" import community_header %} +{%- from "invenio_communities/macros.html" import community_header with context %} {%- block css %} {{ super() }} @@ -154,7 +154,7 @@ {% set flag_1=True %} {%- for role in current_user.roles %} {%- if role in activitylog_roles and flag_1 == True %} - +
    diff --git a/modules/weko-workflow/weko_workflow/templates/weko_workflow/admin/flow_detail.html b/modules/weko-workflow/weko_workflow/templates/weko_workflow/admin/flow_detail.html index 3ab7cbf621..58398b1478 100644 --- a/modules/weko-workflow/weko_workflow/templates/weko_workflow/admin/flow_detail.html +++ b/modules/weko-workflow/weko_workflow/templates/weko_workflow/admin/flow_detail.html @@ -47,6 +47,20 @@ placeholder="{{_('Ener the Flow name')}}" value="{{flow.flow_name if flow}}">
    +
    + +
    + {%- if repositories %} + + {%-else %} + + {%- endif %} +
    +
    diff --git a/modules/weko-workflow/weko_workflow/templates/weko_workflow/admin/workflow_detail.html b/modules/weko-workflow/weko_workflow/templates/weko_workflow/admin/workflow_detail.html index 206e673cbb..0f4e8acb31 100644 --- a/modules/weko-workflow/weko_workflow/templates/weko_workflow/admin/workflow_detail.html +++ b/modules/weko-workflow/weko_workflow/templates/weko_workflow/admin/workflow_detail.html @@ -87,6 +87,22 @@
    +
    +
    + +
    + +
    +
    +
    {%- if is_sysadmin %}
    diff --git a/modules/weko-workflow/weko_workflow/templates/weko_workflow/item_login_success.html b/modules/weko-workflow/weko_workflow/templates/weko_workflow/item_login_success.html index 9600cb81d2..2052bd2098 100644 --- a/modules/weko-workflow/weko_workflow/templates/weko_workflow/item_login_success.html +++ b/modules/weko-workflow/weko_workflow/templates/weko_workflow/item_login_success.html @@ -19,7 +19,7 @@ #} {%- extends config.WEKO_WORKFLOW_BASE_TEMPLATE %} -{%- from "invenio_communities/macros.html" import community_header %} +{%- from "invenio_communities/macros.html" import community_header with context %} {%- block css %} {{ super() }} diff --git a/modules/weko-workflow/weko_workflow/templates/weko_workflow/workflow_list.html b/modules/weko-workflow/weko_workflow/templates/weko_workflow/workflow_list.html index b71d24cc6e..f871afe290 100644 --- a/modules/weko-workflow/weko_workflow/templates/weko_workflow/workflow_list.html +++ b/modules/weko-workflow/weko_workflow/templates/weko_workflow/workflow_list.html @@ -19,7 +19,7 @@ #} {%- extends config.WEKO_WORKFLOW_BASE_TEMPLATE %} -{%- from "invenio_communities/macros.html" import community_header %} +{%- from "invenio_communities/macros.html" import community_header with context %} {%- block css %} {{ super() }} diff --git a/scripts/demo/defaultworkflow.sql b/scripts/demo/defaultworkflow.sql index 867552df2d..16f659d3f6 100644 --- a/scripts/demo/defaultworkflow.sql +++ b/scripts/demo/defaultworkflow.sql @@ -21,7 +21,7 @@ SET row_security = off; -- Data for Name: workflow_flow_define; Type: TABLE DATA; Schema: public; Owner: invenio -- -INSERT INTO public.workflow_flow_define (status, created, updated, id, flow_id, flow_name, flow_user, flow_status, is_deleted) VALUES ('N', '2024-06-12 21:30:19.564693', '2024-06-12 21:31:01.443099', 1, '95b9a88f-3318-4da4-8949-7345b9396e87', 'Registration Flow', 1, 'A', false); +INSERT INTO public.workflow_flow_define (status, created, updated, id, flow_id, flow_name, flow_user, flow_status, is_deleted, repository_id) VALUES ('N', '2024-06-12 21:30:19.564693', '2024-06-12 21:31:01.443099', 1, '95b9a88f-3318-4da4-8949-7345b9396e87', 'Registration Flow', 1, 'A', false, 'Root Index'); -- @@ -40,8 +40,8 @@ INSERT INTO public.workflow_flow_action (status, created, updated, id, flow_id, -- Data for Name: workflow_workflow; Type: TABLE DATA; Schema: public; Owner: invenio -- -INSERT INTO public.workflow_workflow (status, created, updated, id, flows_id, flows_name, itemtype_id, index_tree_id, flow_id, is_deleted, open_restricted, location_id, is_gakuninrdm) VALUES ('N', '2024-06-12 21:33:29.550958', '2024-06-12 21:33:29.550985', 1, '4bb9d036-fc1e-4eab-a7de-cffed26a2bb4', 'デフォルトアイテムタイプ(フル)', 30002, NULL, 1, false, false, NULL, false); -INSERT INTO public.workflow_workflow (status, created, updated, id, flows_id, flows_name, itemtype_id, index_tree_id, flow_id, is_deleted, open_restricted, location_id, is_gakuninrdm) VALUES ('N', '2024-06-12 21:33:55.106678', '2024-06-12 21:33:55.106704', 2, 'bbbb1d7f-2e3a-4bb2-945c-d71b221cb068', 'デフォルトアイテムタイプ(シンプル)', 30001, NULL, 1, false, false, NULL, false); +INSERT INTO public.workflow_workflow (status, created, updated, id, flows_id, flows_name, itemtype_id, index_tree_id, flow_id, is_deleted, open_restricted, location_id, is_gakuninrdm, repository_id) VALUES ('N', '2024-06-12 21:33:29.550958', '2024-06-12 21:33:29.550985', 1, '4bb9d036-fc1e-4eab-a7de-cffed26a2bb4', 'デフォルトアイテムタイプ(フル)', 30002, NULL, 1, false, false, NULL, false, 'Root Index'); +INSERT INTO public.workflow_workflow (status, created, updated, id, flows_id, flows_name, itemtype_id, index_tree_id, flow_id, is_deleted, open_restricted, location_id, is_gakuninrdm, repository_id) VALUES ('N', '2024-06-12 21:33:55.106678', '2024-06-12 21:33:55.106704', 2, 'bbbb1d7f-2e3a-4bb2-945c-d71b221cb068', 'デフォルトアイテムタイプ(シンプル)', 30001, NULL, 1, false, false, NULL, false, 'Root Index'); -- diff --git a/scripts/populate-instance.sh b/scripts/populate-instance.sh index dd4d1d7da9..ced052bd52 100755 --- a/scripts/populate-instance.sh +++ b/scripts/populate-instance.sh @@ -294,7 +294,8 @@ ${INVENIO_WEB_INSTANCE} access \ ${INVENIO_WEB_INSTANCE} access \ allow "stats-api-access" \ - role "${INVENIO_ROLE_REPOSITORY}" + role "${INVENIO_ROLE_REPOSITORY}" \ + role "${INVENIO_ROLE_COMMUNITY}" \ ${INVENIO_WEB_INSTANCE} access \ allow "read-style-action" \ @@ -429,7 +430,7 @@ ${INVENIO_WEB_INSTANCE} admin_settings create_settings \ "{'threshold_rate': 80, 'cycle': 'weekly', 'day': 0}" ${INVENIO_WEB_INSTANCE} admin_settings create_settings \ 3 "site_license_mail_settings" \ - "{'auto_send_flag': False}" + "{'Root Index': {'auto_send_flag': False}}" ${INVENIO_WEB_INSTANCE} admin_settings create_settings \ 4 "default_properties_settings" \ "{'show_flag': True}"