diff --git a/sonar/config.py b/sonar/config.py index 9845852e..428a8561 100644 --- a/sonar/config.py +++ b/sonar/config.py @@ -652,3 +652,7 @@ def _(x): # PREVIEW # ======= PREVIEWER_BASE_TEMPLATE = 'sonar/preview/base.html' +PREVIEWER_MAX_IMAGE_SIZE_BYTES = 5 * 1024 * 1024 +"""Maximum file size in bytes for image files.""" +PREVIEWER_MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024 +"""Maximum file size in bytes for JSON/XML files.""" diff --git a/sonar/modules/config.py b/sonar/modules/config.py index 6321b772..3e8f594d 100644 --- a/sonar/modules/config.py +++ b/sonar/modules/config.py @@ -66,3 +66,8 @@ 'user': ('sonar.modules.users.serializers.schemas.export:' 'ExportSchemaV1'), } + +SONAR_APP_FILE_PREVIEW_EXTENSIONS = [ + 'jpeg', 'jpg', 'gif', 'png', 'pdf', 'json', 'xml', 'csv', 'zip', 'md' +] +"""List of extensions for which files can be previewed.""" diff --git a/sonar/modules/documents/api.py b/sonar/modules/documents/api.py index 73291a30..ded09777 100644 --- a/sonar/modules/documents/api.py +++ b/sonar/modules/documents/api.py @@ -181,19 +181,25 @@ def sync_files(self, file, deleted=False): :param file: File object. :param deleted: Wether the given file has been deleted or not. """ - # For documents, a thumbnail and a fulltext file is generated. + # Synchronise files between bucket and record. + self.files.flush() + + # If file is not deleted, a thumbnail and a fulltext file is generated. if not deleted: self.create_fulltext_file(self.files[file.key]) self.create_thumbnail(self.files[file.key]) - if not self.files[file.key].get('type'): - self.files[file.key]['type'] = 'file' + # Default type is `file` + if not self.files[file.key].get('type'): + self.files[file.key]['type'] = 'file' - if not self.files[file.key].get('label'): - self.files[file.key]['label'] = file.key + # Default label is `file.key` + if not self.files[file.key].get('label'): + self.files[file.key]['label'] = file.key - if not self.files[file.key].get('order'): - self.files[file.key]['order'] = self.get_next_file_order() + # Order is calculated with other files + if not self.files[file.key].get('order'): + self.files[file.key]['order'] = self.get_next_file_order() super(DocumentRecord, self).sync_files(file, deleted) diff --git a/sonar/modules/documents/jsonschemas/documents/document-v1.0.0_src.json b/sonar/modules/documents/jsonschemas/documents/document-v1.0.0_src.json index 705342be..05cd8584 100644 --- a/sonar/modules/documents/jsonschemas/documents/document-v1.0.0_src.json +++ b/sonar/modules/documents/jsonschemas/documents/document-v1.0.0_src.json @@ -78,15 +78,16 @@ }, "order": { "title": "Position", - "description": "Position of the file, The lowest position means file is the main file.", + "description": "Position of the file, the first file in the list is the main file.", "type": "integer", "default": 1, "minimum": 1 }, "external_url": { - "title": "URL to file", + "title": "External URL", "type": "string", "minLength": 1, + "format": "uri", "pattern": "^https?://.*" }, "restricted": { @@ -101,6 +102,7 @@ "description": "Example: 2019-05-05", "type": "string", "minLength": 1, + "format": "date", "pattern": "^[0-9]{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$" } }, diff --git a/sonar/modules/documents/marshmallow/json.py b/sonar/modules/documents/marshmallow/json.py index 647d6688..0ab97501 100644 --- a/sonar/modules/documents/marshmallow/json.py +++ b/sonar/modules/documents/marshmallow/json.py @@ -26,13 +26,14 @@ from invenio_records_rest.schemas import Nested, StrictKeysMixin from invenio_records_rest.schemas.fields import GenFunction, \ PersistentIdentifier, SanitizedUnicode -from marshmallow import fields, pre_dump, pre_load +from marshmallow import EXCLUDE, fields, pre_dump, pre_load from sonar.modules.documents.api import DocumentRecord from sonar.modules.documents.permissions import DocumentPermission +from sonar.modules.documents.utils import has_external_urls_for_files, \ + populate_files_properties from sonar.modules.documents.views import contribution_text, \ - create_publication_statement, dissertation, is_file_restricted, \ - part_of_format + create_publication_statement, dissertation, part_of_format from sonar.modules.serializers import schema_from_context from sonar.modules.users.api import current_user_record @@ -43,6 +44,14 @@ class FileSchemaV1(StrictKeysMixin): """File schema.""" + class Meta: + """Meta for file schema.""" + + # Specifically exclude unknown fields, as in the new version of + # marshmallow, dump_only fields are treated as included. + # https://github.com/marshmallow-code/marshmallow/issues/875 + unknown = EXCLUDE + bucket = SanitizedUnicode() file_id = SanitizedUnicode() version_id = SanitizedUnicode() @@ -56,12 +65,8 @@ class FileSchemaV1(StrictKeysMixin): restricted = SanitizedUnicode() embargo_date = SanitizedUnicode() restriction = fields.Dict(dump_only=True) - - @pre_load - def remove_restriction(self, data, **kwargs): - """Remove restriction information before saving.""" - data.pop('restriction', None) - return data + links = fields.Dict(dump_only=True) + thumbnail = SanitizedUnicode(dump_only=True) class DocumentMetadataSchemaV1(StrictKeysMixin): @@ -103,8 +108,8 @@ class DocumentMetadataSchemaV1(StrictKeysMixin): permalink = SanitizedUnicode(dump_only=True) @pre_dump - def process_files(self, item, **kwargs): - """Add restrictions to file before dumping data. + def populate_files_properties(self, item, **kwargs): + """Add some customs properties to file before dumping it. :param item: Item object to process :returns: Modified item @@ -112,17 +117,11 @@ def process_files(self, item, **kwargs): if not item.get('_files'): return item - # Add restrictions - for key, file in enumerate(item['_files']): - if file.get('type') == 'file': - restricted = is_file_restricted(file, item) - - # Format date before serialization - if restricted.get('date'): - restricted['date'] = restricted['date'].strftime( - '%Y-%m-%d') + # Check if organisation record forces to point file to an external url + item['external_url'] = has_external_urls_for_files(item) - item['_files'][key]['restriction'] = restricted + # Add restriction, link and thumbnail to files + populate_files_properties(item) # Sort files to have the main file in first position item['_files'] = sorted(item['_files'], diff --git a/sonar/modules/documents/templates/documents/record.html b/sonar/modules/documents/templates/documents/record.html index 477a8a47..b1457f85 100644 --- a/sonar/modules/documents/templates/documents/record.html +++ b/sonar/modules/documents/templates/documents/record.html @@ -20,7 +20,6 @@ {% from 'sonar/macros/macro.html' import thumbnail %} {%- block body %} -{% set record = record.replace_refs() %} {% set title = record.title[0] | title_format(current_i18n.language) %} {% set files = record.get_files_list() %} @@ -29,8 +28,10 @@
{% if files and files | length > 0 %}
- {{ thumbnail('documents', record, files | first) }} + {{ thumbnail(record, files | first) }}
+ {% else %} + {{ title }} {% endif %} @@ -77,8 +78,8 @@

{{ title }}

{% if record.contribution | length > 3 %} {{ _('Show more') }}… {% endif %} - {% endif %}
+ {% endif %} {% for provision_activity in record.provisionActivity %} @@ -248,7 +249,9 @@
{{ _('Other files') }}
{% for file in files %} {% if loop.index > 1 %} - {{ thumbnail('documents', record, file, 'col-lg-2 mb-4') }} +
+ {{ thumbnail(record, file) }} +
{% endif %} {% endfor %}
diff --git a/sonar/modules/documents/utils.py b/sonar/modules/documents/utils.py index d663ed92..89683553 100644 --- a/sonar/modules/documents/utils.py +++ b/sonar/modules/documents/utils.py @@ -19,7 +19,14 @@ from __future__ import absolute_import, print_function -from sonar.modules.utils import format_date, remove_trailing_punctuation +import re +from datetime import datetime + +from flask import current_app, g, request + +from sonar.modules.api import SonarRecord +from sonar.modules.utils import change_filename_extension, format_date, \ + remove_trailing_punctuation def publication_statement_text(provision_activity): @@ -63,3 +70,164 @@ def publication_statement_text(provision_activity): value = remove_trailing_punctuation(value) statement_with_language[key] = value return statement_with_language + + +def get_current_organisation_code(): + """Return current organisation by globals or query parameter.""" + # Organisation is present in query parameters, useful for API calls. + organisation = request.args.get('view') + if organisation: + return organisation + + # Organisation stored in globals + if g.get('organisation', {}).get('code'): + return g.organisation['code'] + + # Default organisation + return current_app.config.get('SONAR_APP_DEFAULT_ORGANISATION') + + +def get_file_links(file, record): + """Return link data to preview and/or download the file. + + :param file: File record. + :param record: Record. + :returns: Dict containing the URL, the download URL and the type of link. + """ + links = {'external': None, 'preview': None, 'download': None} + + # File is restricted, no link. + if file['restriction']['restricted']: + return links + + # File has an external url + if record['external_url'] and file.get('external_url'): + links['external'] = file['external_url'] + return links + + match = re.search(r'\.(.*)$', file['key']) + if not match: + return links + + links['download'] = '/documents/{pid}/files/{key}'.format( + pid=record['pid'], key=file['key']) + + if not match.group(1) in current_app.config.get( + 'SONAR_APP_FILE_PREVIEW_EXTENSIONS', []): + return links + + links['preview'] = '/documents/{pid}/preview/{key}'.format( + pid=record['pid'], key=file['key']) + return links + + +def get_file_restriction(file, record): + """Check if current file can be displayed. + + :param file: File dict + :param record: Current record + :returns object containing result and possibly embargo date + """ + + def is_restricted_by_scope(file): + """File is restricted by scope (internal, rero or organisation). + + :param file: File object. + """ + # File is restricted by internal IPs + if file['restricted'] == 'internal': + return request.remote_addr not in current_app.config.get( + 'SONAR_APP_INTERNAL_IPS') + + # File is restricted by organisation + organisation = get_current_organisation_code() + + # We are in global organisation, so restriction is active + if organisation == current_app.config.get( + 'SONAR_APP_DEFAULT_ORGANISATION'): + return True + + # No organisation in record, restriction is active + if not record.get('organisation', {}).get('pid'): + return True + + # Record organisation is different from current organisation + if organisation != record['organisation']['pid']: + return True + + return False + + restricted = {'restricted': False, 'date': None} + + try: + embargo_date = datetime.strptime(file.get('embargo_date'), '%Y-%m-%d') + except Exception: + embargo_date = None + + # Store embargo date if greater than now + if embargo_date and embargo_date > datetime.now(): + restricted['restricted'] = True + restricted['date'] = embargo_date.strftime('%d/%m/%Y') + + # File is restricted by organisation + if file.get('restricted'): + restricted['restricted'] = is_restricted_by_scope(file) + + if not restricted['restricted']: + restricted['date'] = None + + return restricted + + +def has_external_urls_for_files(record): + """Check if files point to external website. + + :param record: Current record. + :returns: True if record's organisation is configured to point files to an + external URL. + """ + if not record.get('organisation', {}): + return False + + organisation_pid = SonarRecord.get_pid_by_ref_link( + record['organisation']['$ref']) if record['organisation'].get( + '$ref') else record['organisation']['pid'] + + return organisation_pid in current_app.config.get( + 'SONAR_DOCUMENTS_ORGANISATIONS_EXTERNAL_FILES') + + +def get_thumbnail(file, record): + """Get thumbnail from file. + + If file is restricted, a restricted image is returned. If no thumbnail + found, a default image is returned. + + :param file: Dict of file from which thumbnail will be returned. + :param record: Record object. + :returns: URL to thumbnail file. + """ + if file['restriction']['restricted']: + return 'static/images/restricted.png' + + key = change_filename_extension(file['key'], 'jpg') + + matches = [file for file in record['_files'] if file['key'] == key] + + if not matches: + return 'static/images/no-image.png' + + return '/documents/{pid}/files/{key}'.format(pid=record['pid'], key=key) + + +def populate_files_properties(record): + """Add restriction, link and thumbnail to file. + + :param record: Record object + :param file: File dict + """ + for file in record['_files']: + if file.get('type') == 'file': + file['restriction'] = get_file_restriction(file, record) + file['thumbnail'] = get_thumbnail(file, record) + file['links'] = get_file_links(file, record) diff --git a/sonar/modules/documents/views.py b/sonar/modules/documents/views.py index 860d2d87..96f6cc01 100644 --- a/sonar/modules/documents/views.py +++ b/sonar/modules/documents/views.py @@ -19,15 +19,15 @@ from __future__ import absolute_import, print_function -from datetime import datetime - from flask import Blueprint, abort, current_app, g, render_template, request from flask_babelex import gettext as _ from invenio_i18n.ext import current_i18n from sonar.modules.documents.api import DocumentRecord +from sonar.modules.documents.utils import has_external_urls_for_files, \ + populate_files_properties from sonar.modules.organisations.api import OrganisationRecord -from sonar.modules.utils import change_filename_extension, format_date +from sonar.modules.utils import format_date from .utils import publication_statement_text @@ -86,6 +86,14 @@ def detail(pid_value, view='global'): if not record: abort(404) + # Add restriction, link and thumbnail to files + if record.get('_files'): + # Check if organisation's record forces to point file to an external + # url + record['external_url'] = has_external_urls_for_files(record) + + populate_files_properties(record) + return render_template('documents/record.html', pid=pid_value, record=record) @@ -147,46 +155,6 @@ def file_size(size): return str(round(size / (1024 * 1024), 2)) + 'Mb' -@blueprint.app_template_filter() -def file_title(file): - """Return file label or key if label not exists. - - :param file: File to get title from. - """ - if file.get('label'): - return file['label'] - - return file['key'] - - -@blueprint.app_template_filter() -def thumbnail(file, files): - """Get thumbnail from file. - - :param file: Dict of file from which thumbnail will be returned. - :param files: Liste of files of the record. - """ - key = change_filename_extension(file['key'], 'jpg') - - matches = [file for file in files if file['key'] == key] - - if not matches: - return None - - return matches[0] - - -@blueprint.app_template_filter() -def has_external_urls_for_files(record): - """Check if files point to external website. - - :param record: Current record. - """ - return record.get('organisation', {}).get( - 'pid') and record['organisation']['pid'] in current_app.config.get( - 'SONAR_DOCUMENTS_ORGANISATIONS_EXTERNAL_FILES') - - @blueprint.app_template_filter() def part_of_format(part_of): """Format partOf property for display. @@ -214,65 +182,6 @@ def part_of_format(part_of): return ', '.join(items) -@blueprint.app_template_filter() -def is_file_restricted(file, record): - """Check if current file can be displayed. - - :param file: File dict - :param record: Current record - :returns object containing result and possibly embargo date - """ - - def is_restricted_by_scope(file): - """File is restricted by scope (internal, rero or organisation). - - :param file: File object. - """ - # File is restricted by internal IPs - if file['restricted'] == 'internal': - return request.remote_addr not in current_app.config.get( - 'SONAR_APP_INTERNAL_IPS') - - # File is restricted by organisation - organisation = get_current_organisation_code() - - # We are in global organisation, so restriction is active - if organisation == current_app.config.get( - 'SONAR_APP_DEFAULT_ORGANISATION'): - return True - - # No organisation in record, restriction is active - if not record.get('organisation', {}).get('pid'): - return True - - # Record organisation is different from current organisation - if organisation != record['organisation']['pid']: - return True - - return False - - restricted = {'restricted': False, 'date': None} - - try: - embargo_date = datetime.strptime(file.get('embargo_date'), '%Y-%m-%d') - except Exception: - embargo_date = None - - # Store embargo date if greater than now - if embargo_date and embargo_date > datetime.now(): - restricted['restricted'] = True - restricted['date'] = embargo_date - - # File is restricted by organisation - if file.get('restricted'): - restricted['restricted'] = is_restricted_by_scope(file) - - if not restricted['restricted']: - restricted['date'] = None - - return restricted - - @blueprint.app_template_filter() def contributors(record): """Get ordered list of contributors.""" @@ -411,18 +320,3 @@ def get_preferred_languages(force_language=None): preferred_languages.insert(0, force_language) return list(dict.fromkeys(preferred_languages)) - - -def get_current_organisation_code(): - """Return current organisation by globals or query parameter.""" - # Organisation is present in query parameters, useful for API calls. - organisation = request.args.get('view') - if organisation: - return organisation - - # Organisation stored in globals - if g.get('organisation', {}).get('code'): - return g.organisation['code'] - - # Default organisation - return current_app.config.get('SONAR_APP_DEFAULT_ORGANISATION') diff --git a/sonar/modules/utils.py b/sonar/modules/utils.py index 7d8ac8f0..5cc68585 100644 --- a/sonar/modules/utils.py +++ b/sonar/modules/utils.py @@ -52,20 +52,28 @@ def create_thumbnail_from_file(file_path, mimetype): img.format = 'jpg' img.background_color = Color('white') img.alpha_channel = 'remove' - img.resize(200, 300) + img.border('#dee2e6', 3, 3) + img.transform(resize='200x') return img.make_blob() def change_filename_extension(filename, extension): - """Return filename with the given extension.""" - matches = re.search(r'(.*)\..*$', filename) + """Return filename with the given extension. + + Additionally, the original extension is appended to the filename, to avoid + conflict with other files having the same name (without extension). + """ + matches = re.search(r'(.*)\.(.*)$', filename) if matches is None: raise Exception( '{filename} is not a valid filename'.format(filename=filename)) - return matches.group(1) + '.' + extension + return '{name}-{source_extension}.{extension}'.format( + name=matches.group(1), + source_extension=matches.group(2), + extension=extension) def send_email(recipients, subject, template, ctx=None, html=True, lang='en'): diff --git a/sonar/theme/static/images/no-image.png b/sonar/theme/static/images/no-image.png new file mode 100644 index 00000000..5ccf45b8 Binary files /dev/null and b/sonar/theme/static/images/no-image.png differ diff --git a/sonar/theme/static/images/restricted.png b/sonar/theme/static/images/restricted.png index bfc12f16..fc9d7d69 100644 Binary files a/sonar/theme/static/images/restricted.png and b/sonar/theme/static/images/restricted.png differ diff --git a/sonar/theme/templates/sonar/macros/macro.html b/sonar/theme/templates/sonar/macros/macro.html index e13a1534..455dd613 100644 --- a/sonar/theme/templates/sonar/macros/macro.html +++ b/sonar/theme/templates/sonar/macros/macro.html @@ -15,44 +15,51 @@ along with this program. If not, see . #} -{% macro thumbnail(resource_type, record, file, class = '') %} -{% set thumbnail = file | thumbnail(record._files) %} -{% if thumbnail %} -{% set file_title = file | file_title %} -{% set externalUrl = record | has_external_urls_for_files %} -
- {% set restricted = file | is_file_restricted(record) %} - {% if not restricted.restricted %} - {% if externalUrl and file.external_url %} - - {{ file_title }} - +{% macro thumbnail(record, file) %} +
+ {% if file.links.external %} + {{ thumbnail_image(file.thumbnail, file.label, file.links.external, '_blank') }} + {% elif file.links.preview %} + {{ thumbnail_image(file.thumbnail, file.label, file.links.preview, '_self', 'previewLink') }} + {% elif file.links.download %} + {{ thumbnail_image(file.thumbnail, file.label, file.links.download + '?download', '_self') }} {% else %} - - {{ file_title }} - - {% endif %} -

{{ file_title }}

- {% if not externalUrl or not file.external_url %} -

- - -

- {% endif %} - {% else %} - {{ file.key }} -

{{ file_title }}

- {% if restricted.date %} - - {{ _('Public access from') }} {{ restricted.date.strftime('%d/%m/%Y') }} - + {{ thumbnail_image(file.thumbnail, file.label) }} {% endif %} +
{{ file.label }}
+ {% if file.restriction and file.restriction.date %} +
+ + {{ _('Public access from') }}
{{ file.restriction.date }} +
+
{% endif %} +
+ {% if file.links.external %} + + + + {% endif %} + {% if file.links.preview %} + + + + {% endif %} + {% if file.links.download %} + + + + {% endif %} +
-{% endif %} +{% endmacro %} + +{% macro thumbnail_image(image, label, link=None, target='_self', class='') %} +{% if link %} + + {{ label }} + +{% else%} +{{ label }} +{% endif%} {% endmacro %} diff --git a/tests/api/test_api_simple_flow.py b/tests/api/test_api_simple_flow.py index 83f84438..7bad8a12 100644 --- a/tests/api/test_api_simple_flow.py +++ b/tests/api/test_api_simple_flow.py @@ -56,5 +56,5 @@ def test_add_files_restrictions(client, document_with_file, superuser): assert res.status_code == 200 assert res.json['metadata']['_files'][0]['restriction'] == { 'restricted': True, - 'date': '2021-01-01' + 'date': '01/01/2021' } diff --git a/tests/ui/documents/test_documents_receivers.py b/tests/ui/documents/test_documents_receivers.py index 6d7bcfb2..1c0f896d 100644 --- a/tests/ui/documents/test_documents_receivers.py +++ b/tests/ui/documents/test_documents_receivers.py @@ -42,7 +42,7 @@ def test_populate_fulltext_field(app, db, document, pdf_file): # Successful file add document.add_file(content, 'test1.pdf', type='file') assert document.files['test1.pdf'] - assert document.files['test1.txt'] + assert document.files['test1-pdf.txt'] db.session.commit() diff --git a/tests/ui/documents/test_documents_utils.py b/tests/ui/documents/test_documents_utils.py index 39836b96..ea69e355 100644 --- a/tests/ui/documents/test_documents_utils.py +++ b/tests/ui/documents/test_documents_utils.py @@ -17,7 +17,10 @@ """Test documents utils.""" +from flask import g + from sonar.modules.documents import utils +from sonar.modules.documents.views import store_organisation def test_publication_statement_text(): @@ -59,3 +62,204 @@ def test_publication_statement_text(): 'type': 'bf:Publication', 'startDate': '1990-12-31' }) == '31.12.1990' + + +def test_get_file_restriction(app, organisation): + """Test if a file is restricted by embargo date and/or organisation.""" + g.pop('organisation', None) + + store_organisation() + + record = {'organisation': {'pid': 'org'}} + + # No restriction and no embargo date + assert utils.get_file_restriction({}, {}) == { + 'date': None, + 'restricted': False + } + + # Restricted by internal, but IP is allowed + with app.test_request_context(environ_base={'REMOTE_ADDR': '127.0.0.1'}): + assert utils.get_file_restriction({'restricted': 'internal'}, {}) == { + 'date': None, + 'restricted': False + } + + # Restricted by internal, but IP is not allowed + with app.test_request_context(environ_base={'REMOTE_ADDR': '10.1.2.3'}): + assert utils.get_file_restriction({'restricted': 'internal'}, {}) == { + 'date': None, + 'restricted': True + } + + # Restricted by organisation and organisation is global + assert utils.get_file_restriction({'restricted': 'organisation'}, + record) == { + 'date': None, + 'restricted': True + } + + # Restricted by organisation and current organisation match + with app.test_request_context() as req: + req.request.view_args['view'] = 'org' + store_organisation() + + assert utils.get_file_restriction({'restricted': 'organisation'}, + record) == { + 'date': None, + 'restricted': False + } + + # Restricted by organisation and record don't have organisation + assert utils.get_file_restriction({'restricted': 'organisation'}, {}) == { + 'date': None, + 'restricted': True + } + + # Restricted by organisation and organisation don't match + assert utils.get_file_restriction({'restricted': 'organisation'}, + {'organisation': { + 'pid': 'some-org' + }}) == { + 'date': None, + 'restricted': True + } + + # Restricted by embargo date only, but embargo date is in the past + assert utils.get_file_restriction({'embargo_date': '2020-01-01'}, {}) == { + 'date': None, + 'restricted': False + } + + # Restricted by embargo date only and embargo date is in the future + with app.test_request_context(environ_base={'REMOTE_ADDR': '10.1.2.3'}): + assert utils.get_file_restriction({'embargo_date': '2021-01-01'}, + {}) == { + 'date': '01/01/2021', + 'restricted': True + } + + # Restricted by embargo date and organisation + g.pop('organisation', None) + store_organisation() + with app.test_request_context(environ_base={'REMOTE_ADDR': '10.1.2.3'}): + assert utils.get_file_restriction( + { + 'embargo_date': '2021-01-01', + 'restricted': 'organisation' + }, record) == { + 'restricted': True, + 'date': '01/01/2021' + } + + # Restricted by embargo date but internal IP gives access + with app.test_request_context(environ_base={'REMOTE_ADDR': '127.0.0.1'}): + assert utils.get_file_restriction( + { + 'embargo_date': '2021-01-01', + 'restricted': 'internal' + }, {}) == { + 'date': None, + 'restricted': False + } + + +def test_get_current_organisation_code(app, organisation): + """Test get current organisation.""" + # No globals and no args + assert utils.get_current_organisation_code() == 'global' + + # Default globals and no args + store_organisation() + assert utils.get_current_organisation_code() == 'global' + + # Organisation globals and no args + with app.test_request_context() as req: + req.request.view_args['view'] = 'org' + store_organisation() + assert utils.get_current_organisation_code() == 'org' + + # Args is global + with app.test_request_context() as req: + req.request.args = {'view': 'global'} + assert utils.get_current_organisation_code() == 'global' + + # Args has organisation view + with app.test_request_context() as req: + req.request.args = {'view': 'unisi'} + assert utils.get_current_organisation_code() == 'unisi' + + +def test_get_file_links(app): + """Test getting links for a file.""" + document = {'pid': 1, 'external_url': True} + file = {'key': 'test.pdf', 'restriction': {'restricted': True}} + + # File is restricted + assert utils.get_file_links(file, document) == { + 'download': None, + 'external': None, + 'preview': None + } + + # File as an external URL + file['restriction']['restricted'] = False + file['external_url'] = 'https://some.url' + assert utils.get_file_links(file, document) == { + 'download': None, + 'external': 'https://some.url', + 'preview': None + } + + # File key has no extension, no preview possible + file['key'] = 'test' + file['external_url'] = None + assert utils.get_file_links(file, document) == { + 'download': None, + 'external': None, + 'preview': None + } + + # Preview not possible + file['key'] = 'test.unknown' + file['external_url'] = None + assert utils.get_file_links(file, document) == { + 'download': '/documents/1/files/test.unknown', + 'external': None, + 'preview': None + } + + # Preview OK + file['key'] = 'test.pdf' + assert utils.get_file_links(file, document) == { + 'download': '/documents/1/files/test.pdf', + 'external': None, + 'preview': '/documents/1/preview/test.pdf' + } + + +def test_get_thumbnail(): + """Test get the thumbnail for a file.""" + document = { + 'pid': 1, + 'external_url': True, + '_files': [{ + 'key': 'test.pdf' + }, { + 'key': 'test-pdf.jpg' + }] + } + file = {'key': 'test.pdf', 'restriction': {'restricted': True}} + + # File is restricted + assert utils.get_thumbnail(file, + document) == 'static/images/restricted.png' + + # Thumbnail is returned + file['restriction']['restricted'] = False + assert utils.get_thumbnail(file, + document) == '/documents/1/files/test-pdf.jpg' + + # Thumbnail not found + document['_files'][1]['key'] = 'some-name.jpg' + assert utils.get_thumbnail(file, document) == 'static/images/no-image.png' diff --git a/tests/ui/documents/test_documents_views.py b/tests/ui/documents/test_documents_views.py index 63d3ae03..895def6a 100644 --- a/tests/ui/documents/test_documents_views.py +++ b/tests/ui/documents/test_documents_views.py @@ -17,8 +17,6 @@ """Test documents views.""" -import datetime - import pytest from flask import g, url_for @@ -71,10 +69,11 @@ def test_search(app, client): resource_type='documents')).status_code == 200 -def test_detail(app, client, document): +def test_detail(app, client, document_with_file): """Test document detail page.""" - assert client.get(url_for('documents.detail', - pid_value=document['pid'])).status_code == 200 + assert client.get( + url_for('documents.detail', + pid_value=document_with_file['pid'])).status_code == 200 assert client.get(url_for('documents.detail', pid_value='not-existing')).status_code == 404 @@ -166,96 +165,6 @@ def test_file_size(app): assert views.file_size(2889638) == '2.76Mb' -def test_file_title(): - """Test getting the caption of a file.""" - assert views.file_title({ - 'bucket': - '18126249-6eb7-4fa5-b0b5-f8599e3c2bf0', - 'checksum': - 'md5:e73223fe5c54532ebfd3e101cb6527ce', - 'file_id': - 'd953c07c-5e7f-4636-bc81-3627f947c494', - 'key': - '1_2004ECO001.pdf', - 'label': - 'Texte intégral', - 'order': - 1, - 'size': - 2889638, - 'type': - 'file', - 'version_id': - '1c9705d3-8a9c-489e-adaf-d7d741b205ba' - }) == 'Texte intégral' - - assert views.file_title({ - 'bucket': - '18126249-6eb7-4fa5-b0b5-f8599e3c2bf0', - 'checksum': - 'md5:e73223fe5c54532ebfd3e101cb6527ce', - 'file_id': - 'd953c07c-5e7f-4636-bc81-3627f947c494', - 'key': - '1_2004ECO001.pdf', - 'order': - 1, - 'size': - 2889638, - 'type': - 'file', - 'version_id': - '1c9705d3-8a9c-489e-adaf-d7d741b205ba' - }) == '1_2004ECO001.pdf' - - -def test_thumbnail(): - """Test getting the thumbnail of a file.""" - files = [{ - 'bucket': '18126249-6eb7-4fa5-b0b5-f8599e3c2bf0', - 'checksum': 'md5:e73223fe5c54532ebfd3e101cb6527ce', - 'file_id': 'd953c07c-5e7f-4636-bc81-3627f947c494', - 'key': '1_2004ECO001.pdf', - 'label': 'Texte intégral', - 'order': 1, - 'size': 2889638, - 'type': 'file', - 'version_id': '1c9705d3-8a9c-489e-adaf-d7d741b205ba' - }, { - 'bucket': '18126249-6eb7-4fa5-b0b5-f8599e3c2bf0', - 'checksum': 'md5:519bd9463e54f681d73861e7e17e2050', - 'file_id': '722264a3-b6d4-459c-a88d-2aca6fe34a0e', - 'key': '1_2004ECO001.jpg', - 'size': 160659, - 'type': 'thumbnail', - 'version_id': '1194512e-7c99-42e1-b320-a15ccb2ac946' - }] - - assert views.thumbnail(files[0], files) == { - 'bucket': '18126249-6eb7-4fa5-b0b5-f8599e3c2bf0', - 'checksum': 'md5:519bd9463e54f681d73861e7e17e2050', - 'file_id': '722264a3-b6d4-459c-a88d-2aca6fe34a0e', - 'key': '1_2004ECO001.jpg', - 'size': 160659, - 'type': 'thumbnail', - 'version_id': '1194512e-7c99-42e1-b320-a15ccb2ac946' - } - - files = [{ - 'bucket': '18126249-6eb7-4fa5-b0b5-f8599e3c2bf0', - 'checksum': 'md5:e73223fe5c54532ebfd3e101cb6527ce', - 'file_id': 'd953c07c-5e7f-4636-bc81-3627f947c494', - 'key': '1_2004ECO001.pdf', - 'label': 'Texte intégral', - 'order': 1, - 'size': 2889638, - 'type': 'file', - 'version_id': '1c9705d3-8a9c-489e-adaf-d7d741b205ba' - }] - - assert not views.thumbnail(files[0], files) - - def test_has_external_urls_for_files(app): """Test if record has to point files to external URL or not.""" assert views.has_external_urls_for_files({ @@ -297,133 +206,6 @@ def test_part_of_format(): }) == '2015' -def test_is_file_restricted(app, organisation): - """Test if a file is restricted by embargo date and/or organisation.""" - g.pop('organisation', None) - - views.store_organisation() - - record = {'organisation': {'pid': 'org'}} - - # No restriction and no embargo date - assert views.is_file_restricted({}, {}) == { - 'date': None, - 'restricted': False - } - - # Restricted by internal, but IP is allowed - with app.test_request_context(environ_base={'REMOTE_ADDR': '127.0.0.1'}): - assert views.is_file_restricted({'restricted': 'internal'}, {}) == { - 'date': None, - 'restricted': False - } - - # Restricted by internal, but IP is not allowed - with app.test_request_context(environ_base={'REMOTE_ADDR': '10.1.2.3'}): - assert views.is_file_restricted({'restricted': 'internal'}, {}) == { - 'date': None, - 'restricted': True - } - - # Restricted by organisation and organisation is global - assert views.is_file_restricted({'restricted': 'organisation'}, - record) == { - 'date': None, - 'restricted': True - } - - # Restricted by organisation and current organisation match - with app.test_request_context() as req: - req.request.view_args['view'] = 'org' - views.store_organisation() - - assert views.is_file_restricted({'restricted': 'organisation'}, - record) == { - 'date': None, - 'restricted': False - } - - # Restricted by organisation and record don't have organisation - assert views.is_file_restricted({'restricted': 'organisation'}, {}) == { - 'date': None, - 'restricted': True - } - - # Restricted by organisation and organisation don't match - assert views.is_file_restricted({'restricted': 'organisation'}, - {'organisation': { - 'pid': 'some-org' - }}) == { - 'date': None, - 'restricted': True - } - - # Restricted by embargo date only, but embargo date is in the past - assert views.is_file_restricted({'embargo_date': '2020-01-01'}, {}) == { - 'date': None, - 'restricted': False - } - - # Restricted by embargo date only and embargo date is in the future - with app.test_request_context(environ_base={'REMOTE_ADDR': '10.1.2.3'}): - assert views.is_file_restricted({'embargo_date': '2021-01-01'}, - {}) == { - 'date': datetime.datetime( - 2021, 1, 1, 0, 0), - 'restricted': True - } - - # Restricted by embargo date and organisation - g.pop('organisation', None) - views.store_organisation() - with app.test_request_context(environ_base={'REMOTE_ADDR': '10.1.2.3'}): - assert views.is_file_restricted( - { - 'embargo_date': '2021-01-01', - 'restricted': 'organisation' - }, record) == { - 'restricted': True, - 'date': datetime.datetime(2021, 1, 1, 0, 0) - } - - # Restricted by embargo date but internal IP gives access - with app.test_request_context(environ_base={'REMOTE_ADDR': '127.0.0.1'}): - assert views.is_file_restricted( - { - 'embargo_date': '2021-01-01', - 'restricted': 'internal' - }, {}) == { - 'date': None, - 'restricted': False - } - - -def test_get_current_organisation_code(app, organisation): - """Test get current organisation.""" - # No globals and no args - assert views.get_current_organisation_code() == 'global' - - # Default globals and no args - views.store_organisation() - assert views.get_current_organisation_code() == 'global' - - # Organisation globals and no args - with app.test_request_context() as req: - req.request.view_args['view'] = 'org' - views.store_organisation() - assert views.get_current_organisation_code() == 'org' - - # Args is global - with app.test_request_context() as req: - req.request.args = {'view': 'global'} - assert views.get_current_organisation_code() == 'global' - - # Args has organisation view - with app.test_request_context() as req: - req.request.args = {'view': 'usi'} - assert views.get_current_organisation_code() == 'usi' - - def test_abstracts(app): """Test getting ordered abstracts.""" # Abstracts are ordered, english first. diff --git a/tests/ui/test_api.py b/tests/ui/test_api.py index 00195b0c..471bb7d2 100644 --- a/tests/ui/test_api.py +++ b/tests/ui/test_api.py @@ -156,7 +156,7 @@ def test_add_file(mock_extract, app, pdf_file, document): # Successful file add document.add_file(content, 'test1.pdf') assert document.files['test1.pdf'] - assert document.files['test1.txt'] + assert document.files['test1-pdf.txt'] # Test already existing files document.add_file(content, 'test1.pdf') @@ -172,7 +172,7 @@ def test_add_file(mock_extract, app, pdf_file, document): app.config['SONAR_DOCUMENTS_EXTRACT_FULLTEXT_ON_IMPORT'] = False document.add_file(content, 'test4.pdf') assert document.files['test4.pdf'] - assert 'test4.txt' not in document.files + assert 'test4-pdf.txt' not in document.files # Test exception when extracting fulltext app.config['SONAR_DOCUMENTS_EXTRACT_FULLTEXT_ON_IMPORT'] = True @@ -183,7 +183,7 @@ def exception_side_effect(data): mock_extract.side_effect = exception_side_effect document.add_file(content, 'test5.pdf') assert document.files['test5.pdf'] - assert 'test5.txt' not in document.files + assert 'test5-pdf.txt' not in document.files def test_get_main_file(document_with_file): @@ -216,10 +216,10 @@ def test_create_thumbnail(document, pdf_file): # Successful thumbail creation document.create_thumbnail(document.files['test.pdf']) assert len(document.files) == 2 - assert document.files['test.jpg'] + assert document.files['test-pdf.jpg'] document.files['test.pdf'].remove() - document.files['test.jpg'].remove() + document.files['test-pdf.jpg'].remove() # Failed creation of thumbnail document.files['test.txt'] = BytesIO(b'Hello, World') diff --git a/tests/ui/test_utils.py b/tests/ui/test_utils.py index 671d3333..a266629b 100644 --- a/tests/ui/test_utils.py +++ b/tests/ui/test_utils.py @@ -34,7 +34,7 @@ def test_change_filename_extension(app): change_filename_extension('test', 'txt') assert str(e.value) == 'test is not a valid filename' - assert change_filename_extension('test.pdf', 'txt') == 'test.txt' + assert change_filename_extension('test.pdf', 'txt') == 'test-pdf.txt' def test_create_thumbnail_from_file():