diff --git a/sonar/config.py b/sonar/config.py index d12f2aaf..6066c94c 100644 --- a/sonar/config.py +++ b/sonar/config.py @@ -267,7 +267,19 @@ def _(x): 'route': '/deposits/<pid_value>/files/<filename>', 'view_imp': 'invenio_records_files.utils:file_download_ui', 'record_class': 'invenio_records_files.api:Record' - } + }, + 'org_previewer': { + 'pid_type': 'org', + 'route': '/organisations/<pid_value>/preview/<filename>', + 'view_imp': 'invenio_previewer.views:preview', + 'record_class': 'sonar.modules.organisations.api:OrganisationRecord' + }, + 'org_files': { + 'pid_type': 'org', + 'route': '/organisations/<pid_value>/files/<filename>', + 'view_imp': 'invenio_records_files.utils:file_download_ui', + 'record_class': 'invenio_records_files.api:Record' + }, } """Records UI for sonar.""" @@ -335,7 +347,8 @@ def _(x): ':json_v1'), }, list_route='/organisations/', - item_route='/organisations/<pid(org):pid_value>', + item_route='/organisations/<pid(org, record_class="sonar.modules.' + 'organisations.api:OrganisationRecord"):pid_value>', default_media_type='application/json', max_result_window=10000, search_factory_imp='sonar.modules.organisations.query:search_factory', @@ -505,7 +518,8 @@ def _(x): RECORDS_FILES_REST_ENDPOINTS = { 'RECORDS_REST_ENDPOINTS': { 'doc': '/files', - 'depo': '/files' + 'depo': '/files', + 'org': '/files' } } diff --git a/sonar/modules/organisations/api.py b/sonar/modules/organisations/api.py index bb6df636..43f47bd7 100644 --- a/sonar/modules/organisations/api.py +++ b/sonar/modules/organisations/api.py @@ -17,7 +17,6 @@ """Organisation Api.""" - from functools import partial from werkzeug.local import LocalProxy @@ -33,11 +32,8 @@ lambda: OrganisationRecord.get_organisation_by_user(current_user_record)) # provider -OrganisationProvider = type( - 'OrganisationProvider', - (Provider,), - dict(pid_type='org') -) +OrganisationProvider = type('OrganisationProvider', (Provider, ), + dict(pid_type='org')) # minter organisation_pid_minter = partial(id_minter, provider=OrganisationProvider) # fetcher @@ -62,6 +58,16 @@ class OrganisationRecord(SonarRecord): provider = OrganisationProvider schema = 'organisations/organisation-v1.0.0.json' + @classmethod + def create(cls, data, id_=None, dbcommit=False, with_bucket=True, + **kwargs): + """Create organisation record.""" + return super(OrganisationRecord, cls).create(data, + id_=id_, + dbcommit=dbcommit, + with_bucket=with_bucket, + **kwargs) + @classmethod def get_organisation_by_user(cls, user): """Return organisation associated with user. diff --git a/sonar/modules/organisations/jsonschemas/organisations/organisation-v1.0.0.json b/sonar/modules/organisations/jsonschemas/organisations/organisation-v1.0.0.json index d3361783..f659a7fd 100644 --- a/sonar/modules/organisations/jsonschemas/organisations/organisation-v1.0.0.json +++ b/sonar/modules/organisations/jsonschemas/organisations/organisation-v1.0.0.json @@ -34,6 +34,18 @@ "type": "string", "minLength": 1 }, + "description": { + "title": "Description", + "description": "HTML markup admitted.", + "type": "string", + "minLength": 1, + "form": { + "type": "textarea", + "templateOptions": { + "rows": 5 + } + } + }, "isShared": { "title": "Is shared", "description": "Organisation records can be accessed by a specific URL.", @@ -48,11 +60,67 @@ "form": { "hideExpression": "!field.model.isShared" } + }, + "_bucket": { + "title": "Bucket UUID", + "type": "string", + "minLength": 1 + }, + "_files": { + "title": "Files", + "description": "List of files attached to the record.", + "type": "array", + "items": { + "title": "File item", + "description": "Describes the information of a single file in the record.", + "additionalProperties": false, + "type": "object", + "properties": { + "bucket": { + "title": "Bucket UUID", + "type": "string", + "minLength": 1 + }, + "file_id": { + "title": "File UUID", + "type": "string", + "minLength": 1 + }, + "version_id": { + "title": "Version UUID", + "type": "string", + "minLength": 1 + }, + "key": { + "title": "Key", + "type": "string", + "minLength": 1 + }, + "checksum": { + "title": "Checksum", + "description": "MD5 checksum of the file.", + "type": "string", + "minLength": 1 + }, + "size": { + "title": "Size", + "description": "Size of the file in bytes.", + "type": "integer" + } + }, + "required": [ + "bucket", + "file_id", + "version_id", + "key" + ] + } } }, "propertiesOrder": [ "code", "name", + "description", "isShared", "isDedicated" ], diff --git a/sonar/modules/organisations/mappings/v6/organisations/organisation-v1.0.0.json b/sonar/modules/organisations/mappings/v6/organisations/organisation-v1.0.0.json index 14800d9a..fe9b2786 100644 --- a/sonar/modules/organisations/mappings/v6/organisations/organisation-v1.0.0.json +++ b/sonar/modules/organisations/mappings/v6/organisations/organisation-v1.0.0.json @@ -10,6 +10,9 @@ "pid": { "type": "keyword" }, + "description": { + "type": "text" + }, "code": { "type": "keyword" }, diff --git a/sonar/modules/organisations/marshmallow/json.py b/sonar/modules/organisations/marshmallow/json.py index d8f16f1c..3e719667 100644 --- a/sonar/modules/organisations/marshmallow/json.py +++ b/sonar/modules/organisations/marshmallow/json.py @@ -41,6 +41,7 @@ class OrganisationMetadataSchemaV1(StrictKeysMixin): pid = PersistentIdentifier() code = SanitizedUnicode(required=True) name = SanitizedUnicode(required=True) + description = SanitizedUnicode() isShared = fields.Boolean() isDedicated = fields.Boolean() # When loading, if $schema is not provided, it's retrieved by @@ -50,6 +51,8 @@ class OrganisationMetadataSchemaV1(StrictKeysMixin): data_key="$schema", deserialize=schema_from_organisation) permissions = fields.Dict(dump_only=True) + _files = fields.List(fields.Dict()) + _bucket = SanitizedUnicode() @pre_dump def add_permissions(self, item): diff --git a/sonar/theme/templates/sonar/frontpage.html b/sonar/theme/templates/sonar/frontpage.html index c1cda541..d8138b4b 100755 --- a/sonar/theme/templates/sonar/frontpage.html +++ b/sonar/theme/templates/sonar/frontpage.html @@ -24,7 +24,7 @@ <div class="container"> <div class="row justify-content-center"> <div class="col-8 col-xs-6 col-lg-4 py-3 py-sm-5"> - <a href="{{ url_for('documents.index', view=view_code) }}"> + <a href="{{ url_for('documents.index', view=view_code if g.get('organisation', {}).get('isDedicated') else config.SONAR_APP_DEFAULT_ORGANISATION) }}"> {%- if config.THEME_LOGO %} {% set logo_code = view_code if g.get('organisation', {}).get('isDedicated') else config.SONAR_APP_DEFAULT_ORGANISATION %} <img src="{{ url_for('static', filename='images/' ~ logo_code ~ '-logo.svg')}}" alt="{{_(config.THEME_SITENAME)}}" class="logo"/> @@ -36,7 +36,7 @@ </div> <div class="row justify-content-center"> <div class="col-sm-12 col-lg-8 text-right my-4"> - <form class="justify-content-end" action="{{ url_for('documents.search', view=view_code) }}" role="search"> + <form class="justify-content-end" action="{{ url_for('documents.search', view=view_code if g.get('organisation', {}).get('isDedicated') else config.SONAR_APP_DEFAULT_ORGANISATION) }}" role="search"> <div class="row"> <div class="col-12 col-sm-11"> <input class="form-control form-control-lg mr-2" type="search" placeholder="{{_('Search publications, authors, projects, ...')}}" aria-label="Search" name="q"> @@ -56,6 +56,7 @@ <div class="bg-secondary text-light text-center py-2"> <h6 class="m-0">{{ _('Software under development!') }}</h6> </div> +{% include 'sonar/partial/organisation.html' %} {% endblock %} {%- block body %} diff --git a/sonar/theme/templates/sonar/page.html b/sonar/theme/templates/sonar/page.html index 3ad9e836..c140981d 100755 --- a/sonar/theme/templates/sonar/page.html +++ b/sonar/theme/templates/sonar/page.html @@ -95,7 +95,9 @@ {%- block body_inner %} <header> {% include 'sonar/partial/navbar.html' %} - {%- block header %}{%- endblock header %} + {%- block header %} + {% include 'sonar/partial/organisation.html' %} + {%- endblock header %} </header> <div class="container my-5"> diff --git a/sonar/theme/templates/sonar/partial/navbar.html b/sonar/theme/templates/sonar/partial/navbar.html index 58a32e35..dfdc2d94 100644 --- a/sonar/theme/templates/sonar/partial/navbar.html +++ b/sonar/theme/templates/sonar/partial/navbar.html @@ -14,21 +14,6 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. #} - -{% if g.get('organisation') and not g.organisation['isDedicated'] %} - <div class="bg-secondary text-light p-1"> - <div class="container"> - <div class="row"> - <div class="col">{{ g.organisation.name }}</div> - <div class="col text-right"> - <a class="text-light" href="{{url_for('documents.index', view=config.SONAR_APP_DEFAULT_ORGANISATION)}}"> - {{_('Back to SONAR')}} - </a> - </div> - </div> - </div> - </div> -{% endif %} <nav class="navbar navbar-expand-lg navbar-dark bg-{{ 'header' if page == 'home' else 'organisation' }}"> <div class="container"> {%- if not current_user.is_authenticated %} @@ -58,7 +43,7 @@ class="d-inline-block align-top mr-3 my-4" alt=""> </a> {% if not admin %} - <form action="{{ url_for('documents.search', view=view_code) }}" + <form action="{{ url_for('documents.search', view=view_code if g.get('organisation', {}).get('isDedicated') else config.SONAR_APP_DEFAULT_ORGANISATION) }}" class="form-inline my-2 my-lg-0 ml-lg-5"> <input name="q" class="form-control mr-sm-2" type="search" placeholder="{{_('Search')}}" aria-label="{{_('Search')}}" value="{{ request.args.get('q', '') }}"> diff --git a/sonar/theme/templates/sonar/partial/organisation.html b/sonar/theme/templates/sonar/partial/organisation.html new file mode 100644 index 00000000..f0a7f5d0 --- /dev/null +++ b/sonar/theme/templates/sonar/partial/organisation.html @@ -0,0 +1,41 @@ +{% if g.get('organisation') and not g.organisation['isDedicated'] %} +<div class="bg-light py-4"> + <div class="container"> + <div class="row justify-content-center"> + {% set thumbnail = g.organisation | record_image_url %} + {% if thumbnail %} + <div class="col-6 col-lg-2"> + <img src="{{ thumbnail }}" alt="{{ g.organisation.name }}" class="img-fluid"> + </div> + {% endif %} + <div class="col-12 col-lg-{{ '10' if thumbnail else '12' }}"> + <h1 class="mb-2">{{ g.organisation.name }}</h1> + {% if g.organisation.get('description') %} + <p class="mb-3 text-justify">{{ g.organisation.description | nl2br | safe }}</p> + {% endif %} + <div class="row"> + <div class="col-12 col-lg-4"> + <form action="{{ url_for('documents.search', view=view_code) }}" role="search"> + <div class="input-group mb-2"> + <input type="text" name="q" value="{{ request.args.get('q', '') }}" class="form-control" + id="inlineFormInputGroup" placeholder="{{ _('Search') }}"> + <div class="input-group-append"> + <button type="submit" class="btn btn-primary mb-2"> + <i class="fa fa-search"></i> + </button> + </div> + </div> + </form> + </div> + <div class="col text-right"> + <a href="{{url_for('documents.index', view=config.SONAR_APP_DEFAULT_ORGANISATION)}}" + class="btn btn-outline-primary"> + {{_('Back to SONAR')}} + </a> + </div> + </div> + </div> + </div> + </div> +</div> +{% endif %} diff --git a/sonar/theme/views.py b/sonar/theme/views.py index 5f730d7d..96dc77fe 100644 --- a/sonar/theme/views.py +++ b/sonar/theme/views.py @@ -189,6 +189,36 @@ def schemas(record_type): abort(404) +@blueprint.app_template_filter() +def record_image_url(record, key=None): + """Get image URL for a record. + + :param files: Liste of files of the record. + :param key: The key of the file to be rendered, if no key, takes the first. + :returns: Image url corresponding to key, or the first one. + """ + if not record.get('_files'): + return None + + def image_url(file): + """Return image URL for a file. + + :param file: File to get the URL from. + :returns: URL of the file. + """ + return '/api/files/{bucket}/{key}'.format(key=file['key'], + bucket=file['bucket']) + + for file in record['_files']: + if re.match(r'^.*\.(jpe?g|png|gif|svg)$', + file['key'], + flags=re.IGNORECASE): + if not key or file['key'] == key: + return image_url(file) + + return None + + def prepare_schema(schema): """Prepare schema before sending it.""" # Recursively translate properties in schema diff --git a/tests/conftest.py b/tests/conftest.py index d33028ee..7c7e7e2e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -90,7 +90,7 @@ def app_config(app_config): @pytest.fixture -def make_organisation(app, db): +def make_organisation(app, db, bucket_location): """Factory for creating organisation.""" def _make_organisation(code): diff --git a/tests/ui/documents/dojson/rerodoc/test_rerodoc_model.py b/tests/ui/documents/dojson/rerodoc/test_rerodoc_model.py index 1aadbeb8..2822029e 100644 --- a/tests/ui/documents/dojson/rerodoc/test_rerodoc_model.py +++ b/tests/ui/documents/dojson/rerodoc/test_rerodoc_model.py @@ -26,7 +26,7 @@ from sonar.modules.documents.dojson.rerodoc.model import marc21tojson -def test_marc21_to_type_and_organisation(app): +def test_marc21_to_type_and_organisation(app, bucket_location): """Test type and organisation.""" # Type only diff --git a/tests/ui/documents/dojson/rerodoc/test_rerodoc_overdo.py b/tests/ui/documents/dojson/rerodoc/test_rerodoc_overdo.py index abf3deec..63f54258 100644 --- a/tests/ui/documents/dojson/rerodoc/test_rerodoc_overdo.py +++ b/tests/ui/documents/dojson/rerodoc/test_rerodoc_overdo.py @@ -23,7 +23,7 @@ from sonar.modules.organisations.api import OrganisationRecord -def test_create_organisation(app): +def test_create_organisation(app, bucket_location): """Test create organisation.""" Overdo.create_organisation('test') diff --git a/tests/ui/organisations/cli/test_organisations_cli_organisations.py b/tests/ui/organisations/cli/test_organisations_cli_organisations.py index abdf0b82..44e1745a 100644 --- a/tests/ui/organisations/cli/test_organisations_cli_organisations.py +++ b/tests/ui/organisations/cli/test_organisations_cli_organisations.py @@ -23,7 +23,7 @@ from sonar.modules.organisations.api import OrganisationRecord -def test_import_organisations(app, script_info): +def test_import_organisations(app, script_info, bucket_location): """Test import organisations.""" runner = CliRunner() diff --git a/tests/ui/test_views.py b/tests/ui/test_views.py index f80ce9da..0f892dbf 100644 --- a/tests/ui/test_views.py +++ b/tests/ui/test_views.py @@ -22,6 +22,8 @@ from invenio_accounts.testutils import login_user_via_session, \ login_user_via_view +from sonar.theme.views import record_image_url + def test_error(client): """Test error page""" @@ -184,3 +186,35 @@ def test_profile(client, user): # Wrong PID res = client.get(url_for('sonar.profile', pid='wrong')) assert res.status_code == 403 + + +def test_record_image_url(): + """Test getting record image url.""" + # No file key + assert not record_image_url({}) + + # No files + assert not record_image_url({'_files': []}) + + # No images + assert not record_image_url( + {'_files': [{ + 'bucket': '1234', + 'key': 'test.pdf' + }]}) + + record = { + '_files': [{ + 'bucket': '1234', + 'key': 'test.jpg' + }, { + 'bucket': '1234', + 'key': 'test2.jpg' + }] + } + + # Take the first file + assert record_image_url(record) == '/api/files/1234/test.jpg' + + # Take files corresponding to key + assert record_image_url(record, 'test2.jpg') == '/api/files/1234/test2.jpg'