diff --git a/Gemfile b/Gemfile index e28a8437a..8943e018c 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ # frozen_string_literal: true -source 'http://rubygems.org' +source 'https://rubygems.org' ruby '~> 3.2.2' @@ -13,7 +13,7 @@ gem 'pg', '~> 1.5' gem 'pg_search' # PostgreSQL full text search gem 'psych', '< 4.0' gem 'rails', '~> 7.0.8' -gem 'recaptcha', require: 'recaptcha/rails' +gem 'recaptcha' gem 'sanitize' gem 'sprockets-rails' gem 'view_component' @@ -47,8 +47,7 @@ gem 'aspector' # Aspect-oriented programming for Rails gem 'auto_strip_attributes', '~> 2.1' # Removes unnecessary whitespaces AR gem 'bcrypt', '~> 3.1.10' # gem 'caracal' -gem 'caracal', - git: 'https://github.com/scinote-eln/caracal.git', branch: 'rubyzip2' # Build docx report +gem 'caracal', git: 'https://github.com/scinote-eln/caracal.git', branch: 'custom-docx-reports' # Build docx report gem 'caxlsx' # Build XLSX files gem 'deface', '~> 1.9' gem 'down', '~> 5.0' diff --git a/Gemfile.lock b/Gemfile.lock index 82d0e20e7..07911f921 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -16,10 +16,10 @@ GIT GIT remote: https://github.com/scinote-eln/caracal.git - revision: f8e4c279adfee7801eb1024e1d6a18bb06c9c76a - branch: rubyzip2 + revision: 54c21353798569476a1eaa73b5fd3e275ac85419 + branch: custom-docx-reports specs: - caracal (1.4.1) + caracal (1.4.2) nokogiri (~> 1.6) rubyzip (>= 2.3) tilt (>= 1.4) @@ -50,49 +50,49 @@ GIT mime-types (>= 1.23) GEM - remote: http://rubygems.org/ + remote: https://rubygems.org/ specs: - actioncable (7.0.8.4) - actionpack (= 7.0.8.4) - activesupport (= 7.0.8.4) + actioncable (7.0.8.5) + actionpack (= 7.0.8.5) + activesupport (= 7.0.8.5) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.8.4) - actionpack (= 7.0.8.4) - activejob (= 7.0.8.4) - activerecord (= 7.0.8.4) - activestorage (= 7.0.8.4) - activesupport (= 7.0.8.4) + actionmailbox (7.0.8.5) + actionpack (= 7.0.8.5) + activejob (= 7.0.8.5) + activerecord (= 7.0.8.5) + activestorage (= 7.0.8.5) + activesupport (= 7.0.8.5) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.0.8.4) - actionpack (= 7.0.8.4) - actionview (= 7.0.8.4) - activejob (= 7.0.8.4) - activesupport (= 7.0.8.4) + actionmailer (7.0.8.5) + actionpack (= 7.0.8.5) + actionview (= 7.0.8.5) + activejob (= 7.0.8.5) + activesupport (= 7.0.8.5) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.0) - actionpack (7.0.8.4) - actionview (= 7.0.8.4) - activesupport (= 7.0.8.4) + actionpack (7.0.8.5) + actionview (= 7.0.8.5) + activesupport (= 7.0.8.5) rack (~> 2.0, >= 2.2.4) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.8.4) - actionpack (= 7.0.8.4) - activerecord (= 7.0.8.4) - activestorage (= 7.0.8.4) - activesupport (= 7.0.8.4) + actiontext (7.0.8.5) + actionpack (= 7.0.8.5) + activerecord (= 7.0.8.5) + activestorage (= 7.0.8.5) + activesupport (= 7.0.8.5) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.8.4) - activesupport (= 7.0.8.4) + actionview (7.0.8.5) + activesupport (= 7.0.8.5) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -102,14 +102,14 @@ GEM activemodel (>= 4.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - activejob (7.0.8.4) - activesupport (= 7.0.8.4) + activejob (7.0.8.5) + activesupport (= 7.0.8.5) globalid (>= 0.3.6) - activemodel (7.0.8.4) - activesupport (= 7.0.8.4) - activerecord (7.0.8.4) - activemodel (= 7.0.8.4) - activesupport (= 7.0.8.4) + activemodel (7.0.8.5) + activesupport (= 7.0.8.5) + activerecord (7.0.8.5) + activemodel (= 7.0.8.5) + activesupport (= 7.0.8.5) activerecord-import (1.4.1) activerecord (>= 4.2) activerecord-session_store (2.1.0) @@ -119,14 +119,14 @@ GEM multi_json (~> 1.11, >= 1.11.2) rack (>= 2.0.8, < 4) railties (>= 6.1) - activestorage (7.0.8.4) - actionpack (= 7.0.8.4) - activejob (= 7.0.8.4) - activerecord (= 7.0.8.4) - activesupport (= 7.0.8.4) + activestorage (7.0.8.5) + actionpack (= 7.0.8.5) + activejob (= 7.0.8.5) + activerecord (= 7.0.8.5) + activesupport (= 7.0.8.5) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (7.0.8.4) + activesupport (7.0.8.5) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -428,7 +428,7 @@ GEM net-imap net-pop net-smtp - marcel (1.0.2) + marcel (1.0.4) matrix (0.4.2) method_source (1.0.0) mime-types (3.4.1) @@ -447,20 +447,17 @@ GEM rails (>= 3.2.0) net-http (0.4.1) uri - net-imap (0.4.10) + net-imap (0.4.17) date net-protocol net-pop (0.1.2) net-protocol net-protocol (0.2.2) timeout - net-smtp (0.4.0.1) + net-smtp (0.5.0) net-protocol - newrelic_rpm (9.2.2) + newrelic_rpm (9.14.0) nio4r (2.7.3) - nokogiri (1.16.7) - mini_portile2 (~> 2.8.2) - racc (~> 1.4) nokogiri (1.16.7-arm64-darwin) racc (~> 1.4) nokogiri (1.16.7-x86_64-linux) @@ -547,7 +544,7 @@ GEM nio4r (~> 2.0) raabro (1.4.0) racc (1.8.1) - rack (2.2.9) + rack (2.2.10) rack-attack (6.6.1) rack (>= 1.0, < 3) rack-cors (2.0.2) @@ -564,20 +561,20 @@ GEM rack (~> 2.2, >= 2.2.4) rack-test (2.1.0) rack (>= 1.3) - rails (7.0.8.4) - actioncable (= 7.0.8.4) - actionmailbox (= 7.0.8.4) - actionmailer (= 7.0.8.4) - actionpack (= 7.0.8.4) - actiontext (= 7.0.8.4) - actionview (= 7.0.8.4) - activejob (= 7.0.8.4) - activemodel (= 7.0.8.4) - activerecord (= 7.0.8.4) - activestorage (= 7.0.8.4) - activesupport (= 7.0.8.4) + rails (7.0.8.5) + actioncable (= 7.0.8.5) + actionmailbox (= 7.0.8.5) + actionmailer (= 7.0.8.5) + actionpack (= 7.0.8.5) + actiontext (= 7.0.8.5) + actionview (= 7.0.8.5) + activejob (= 7.0.8.5) + activemodel (= 7.0.8.5) + activerecord (= 7.0.8.5) + activestorage (= 7.0.8.5) + activesupport (= 7.0.8.5) bundler (>= 1.15.0) - railties (= 7.0.8.4) + railties (= 7.0.8.5) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -598,9 +595,9 @@ GEM railties (> 3.1) rails_serve_static_assets (0.0.5) rails_stdout_logging (0.0.5) - railties (7.0.8.4) - actionpack (= 7.0.8.4) - activesupport (= 7.0.8.4) + railties (7.0.8.5) + actionpack (= 7.0.8.5) + activesupport (= 7.0.8.5) method_source rake (>= 12.2) thor (~> 1.0) @@ -611,12 +608,12 @@ GEM rb-inotify (0.10.1) ffi (~> 1.0) rdoc (6.3.4.1) - recaptcha (5.14.0) + recaptcha (5.17.0) regexp_parser (2.8.1) responders (3.1.1) actionpack (>= 5.2) railties (>= 5.2) - rexml (3.3.7) + rexml (3.3.9) rgl (0.6.3) pairing_heap (>= 0.3.0) rexml (~> 3.2, >= 3.2.4) @@ -715,14 +712,12 @@ GEM faraday-follow_redirects sys-uname (1.2.3) ffi (~> 1.1) - tailwindcss-rails (2.4.0) - railties (>= 6.0.0) tailwindcss-rails (2.4.0-arm64-darwin) railties (>= 6.0.0) tailwindcss-rails (2.4.0-x86_64-linux) railties (>= 6.0.0) thor (1.3.1) - tilt (2.2.0) + tilt (2.4.0) timecop (0.9.6) timeout (0.4.1) turbolinks (5.2.1) @@ -776,7 +771,6 @@ GEM PLATFORMS arm64-darwin - ruby x86_64-linux DEPENDENCIES @@ -810,7 +804,6 @@ DEPENDENCIES deface (~> 1.9) delayed_job_active_record devise (~> 4.8.1) - devise-async! devise_invitable discard doorkeeper (>= 4.6) diff --git a/VERSION b/VERSION index bf50e910e..ebeef2f2d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.37.0 +1.38.0 diff --git a/app/assets/javascripts/my_modules/protocols.js b/app/assets/javascripts/my_modules/protocols.js index 7d74d1e41..70dbc5593 100644 --- a/app/assets/javascripts/my_modules/protocols.js +++ b/app/assets/javascripts/my_modules/protocols.js @@ -29,7 +29,8 @@ function initEditMyModuleDescription() { }); setTimeout(function() { - TinyMCE.wrapTables(viewObject); + const notesContainerEl = document.getElementById('notes-container'); + window.wrapTables(notesContainerEl); }, 100); } @@ -327,16 +328,6 @@ function initAccessModal() { }); } -function initWrapTables() { - const viewMode = new URLSearchParams(window.location.search).get('view_mode'); - if (['archived', 'locked'].includes(viewMode)) { - setTimeout(() => { - const notesContainerEl = document.getElementById('notes-container'); - window.wrapTables(notesContainerEl); - }, 100); - } -} - /** * Initializes page */ @@ -348,7 +339,6 @@ function init() { initProtocolSectionOpenEvent(); initDetailsDropdown(); initAccessModal(); - initWrapTables(); } init(); diff --git a/app/assets/javascripts/reports/new.js b/app/assets/javascripts/reports/new.js index a7aba901e..bcc8ad5d3 100644 --- a/app/assets/javascripts/reports/new.js +++ b/app/assets/javascripts/reports/new.js @@ -968,6 +968,16 @@ function reportHandsonTableConverter() { } (function() { + function getSelectedRepositoryColumnValues(element, selectedAll = false) { + const values = []; + $(element).find('option').each((_, option) => { + if ($(option).attr('selected-value') || selectedAll) { + values.push(option.value); + } + }); + return values; + } + function getReportData() { var reportData = {}; @@ -982,7 +992,7 @@ function reportHandsonTableConverter() { // Template values reportData.template_values = {}; - $.each($('.report-template-values-container').find('.sci-input-field'), function(i, field) { + $.each($('.report-template-values-container').find('.sci-input-field').not('.report-template-value-dropdown'), (_, field) => { if (field.value.length === 0) return; reportData.template_values[field.name] = { @@ -1036,6 +1046,7 @@ function reportHandsonTableConverter() { // Settings reportData.report.settings.template = dropdownSelector.getValues('#templateSelector'); + reportData.report.settings.docx_template = dropdownSelector.getValues('#docxTemplateSelector'); reportData.report.settings.all_tasks = $('.project-contents-container .select-all-my-modules-checkbox') .prop('checked'); $.each($('.task-contents-container .content-element .protocol-setting'), function(i, e) { @@ -1045,12 +1056,24 @@ function reportHandsonTableConverter() { reportData.report.settings.task[e.value] = e.checked; }); reportData.report.settings.task.repositories = []; - $.each($('.task-contents-container .repositories-contents .repositories-setting:checked'), function(i, e) { - reportData.report.settings.task.repositories.push(parseInt(e.value, 10)); + reportData.report.settings.task.excluded_repository_columns = {}; + + $.each($('.task-contents-container .repositories-contents .repositories-setting:checked'), (_, e) => { + const value = parseInt(e.value, 10); + const $repositoryColumn = $(e).parent().siblings('.repository-columns')[0]; + const selectedValues = dropdownSelector.getValues($repositoryColumn); + const excludedValues = getSelectedRepositoryColumnValues($repositoryColumn, true) + .filter((item) => !selectedValues.includes(item)) + .map((el) => parseInt(el, 10)); + reportData.report.settings.task.repositories.push(value); + reportData.report.settings.task.excluded_repository_columns[value] = excludedValues; }); reportData.report.settings.task.result_order = dropdownSelector.getValues('#taskResultsOrder'); + reportData.report.settings.exclude_task_metadata = $('.exclude-task-metadata-setting')[0].checked; + reportData.report.settings.exclude_timestamps = $('.exclude-timestamps-setting')[0].checked; + return reportData; } @@ -1254,7 +1277,9 @@ function reportHandsonTableConverter() { function reCheckContinueButton() { if (dropdownSelector.getValues('#projectSelector').length > 0 - && dropdownSelector.getValues('#templateSelector').length > 0) { + && dropdownSelector.getValues('#templateSelector').length > 0 + && (dropdownSelector.getValues('#docxTemplateSelector').length > 0 + || $('#docxTemplateSelector').closest('.hidden').length > 0)) { $('.continue-button').attr('disabled', false); } else { $('.continue-button').attr('disabled', true); @@ -1276,9 +1301,18 @@ function reportHandsonTableConverter() { } if (dropdownSelector.getValues('#projectSelector').length > 0) { dropdownSelector.enableSelector('#templateSelector'); + dropdownSelector.enableSelector('#docxTemplateSelector'); + if ($('#templateSelector').data('defaultTemplate')) { + dropdownSelector.selectValues('#templateSelector', $('#templateSelector').data('defaultTemplate')); + } + if ($('#docxTemplateSelector').data('defaultTemplate')) { + dropdownSelector.selectValues('#docxTemplateSelector', $('#docxTemplateSelector').data('defaultTemplate')); + } } else { dropdownSelector.selectValues('#templateSelector', ''); dropdownSelector.disableSelector('#templateSelector'); + dropdownSelector.selectValues('#docxTemplateSelector', ''); + dropdownSelector.disableSelector('#docxTemplateSelector'); } reCheckContinueButton(); } @@ -1292,12 +1326,12 @@ function reportHandsonTableConverter() { disableSearch: true, onSelect: function() { if (dropdownSelector.getValues('#templateSelector').length === 0) { - $('.report-template-values-container').html('').addClass('hidden'); + $('.report-template-values-container.pdf').html('').addClass('hidden'); reCheckContinueButton(); return; } - let filledFieldsCount = $('.report-template-values-container') + let filledFieldsCount = $('.report-template-values-container.pdf') .find('input.sci-input-field, textarea.sci-input-field').filter(function() { return !!this.value; }).length; @@ -1311,13 +1345,57 @@ function reportHandsonTableConverter() { } }); + dropdownSelector.init('#docxTemplateSelector', { + singleSelect: true, + closeOnSelect: true, + noEmptyOption: true, + selectAppearance: 'simple', + disableSearch: true, + onSelect: function() { + if (dropdownSelector.getValues('#docxTemplateSelector').length === 0) { + $('.report-template-values-container.docx').html('').addClass('hidden'); + reCheckContinueButton(); + return; + } + + let filledFieldsCount = $('.report-template-values-container.docx') + .find('input.sci-input-field, textarea.sci-input-field').filter(function() { + return !!this.value; + }).length; + + if (filledFieldsCount === 0) { + loadDocxTemplate(); + } else { + $('#templateReportWarningModal').modal('show'); + } + reCheckContinueButton(); + } + }); + if (dropdownSelector.getValues('#templateSelector').length > 0) { loadTemplate(); } + if (dropdownSelector.getValues('#docxTemplateSelector').length > 0) { + loadDocxTemplate(); + } + + $('.repository-columns').each((_, element) => { + const elementId = `#${$(element).attr('id')}`; + const elements = getSelectedRepositoryColumnValues(elementId); + + dropdownSelector.init(elementId, { + selectAppearance: 'simple', + optionClass: 'checkbox-icon' + }); + + if (elements.length) { + dropdownSelector.selectValues(elementId, elements); + } + }); } function loadTemplate() { - let template = $('#templateSelector').val(); + const template = dropdownSelector.getValues('#templateSelector'); let params = { project_id: dropdownSelector.getValues('#projectSelector'), template: template @@ -1325,8 +1403,38 @@ function reportHandsonTableConverter() { $('#templateSelector').data('selected-template', template); $.get($('#templateSelector').data('valuesEditorPath'), params, function(result) { - $('.report-template-values-container').removeClass('hidden'); - $('.report-template-values-container').html(result.html); + $('.report-template-values-container.pdf').removeClass('hidden'); + $('.report-template-values-container.pdf').html(result.html); + + $('.section').each(function() { + var section = $(this); + var collapseButton = section.find('.sn-icon-down'); + var valuesContainer = section.find('.values-container'); + + if (valuesContainer.children().length === 0) { + collapseButton.hide(); + } + }); + + $('.report-template-value-dropdown').each(function() { + dropdownSelector.init($(this), { + noEmptyOption: true + }); + }); + }); + } + + function loadDocxTemplate() { + const template = dropdownSelector.getValues('#docxTemplateSelector'); + let params = { + project_id: dropdownSelector.getValues('#projectSelector'), + template: template + }; + + $('#docxTemplateSelector').data('selected-template', template); + $.get($('#docxTemplateSelector').data('valuesEditorPath'), params, function(result) { + $('.report-template-values-container.docx').removeClass('hidden'); + $('.report-template-values-container.docx').html(result.html); $('.section').each(function() { var section = $(this); @@ -1448,7 +1556,9 @@ function reportHandsonTableConverter() { .on('hide.bs.modal', function() { if (!$('#templateReportWarningModal').hasClass('skip-hide-event')) { let previousTemplate = $('#templateSelector').data('selected-template'); + let previousDocxTemplate = $('#docxTemplateSelector').data('selected-template'); dropdownSelector.selectValues('#templateSelector', previousTemplate); + dropdownSelector.selectValues('#docxTemplateSelector', previousDocxTemplate); } $('#templateReportWarningModal').removeClass('skip-hide-event'); }); @@ -1461,7 +1571,7 @@ function reportHandsonTableConverter() { $('#reportWizardEditWarning').modal('show'); $('.experiment-contents').sortable(); - + initNameContainerFocus(); initGenerateButton(); initReportWizard(); diff --git a/app/assets/javascripts/repositories/repository_datatable.js b/app/assets/javascripts/repositories/repository_datatable.js index 933bed39a..d030666ce 100644 --- a/app/assets/javascripts/repositories/repository_datatable.js +++ b/app/assets/javascripts/repositories/repository_datatable.js @@ -328,6 +328,40 @@ var RepositoryDatatable = (function(global) { }); } + function initDeleteAssetValueConfirmModal() { + $('#deleteRepositoryAssetValueModal').on('shown.bs.modal', function() { + let $fileBtn = $(this).data('cellFileBtn'); + let $input = $(this).data('cellInput'); + let $label = $(this).data('cellLabel'); + + $('#confirmAssetValueDelete').one('click', function() { + $fileBtn.addClass('new-file'); + $label.text(''); + $input.val(''); + $fileBtn.removeClass('error'); + + if (!$input.data('is-empty')) { // set hidden field for deletion only if original value has been set on rendering + $input + .prev('.file-hidden-field-container') + .html(``); + } + + $('#deleteRepositoryAssetValueModal').modal('hide'); + }); + }); + + $('#deleteRepositoryAssetValueModal').on('hidden.bs.modal', function() { + const $deleteRepositoryAssetValueModal = $('#deleteRepositoryAssetValueModal'); + + $deleteRepositoryAssetValueModal.data('cellFileBtn', null); + $deleteRepositoryAssetValueModal.data('cellInput', null); + $deleteRepositoryAssetValueModal.data('cellLabel', null); + }); + } + function initActiveRemindersFilter() { $(TABLE_WRAPPER_ID).find('#only_reminders').on('change', function() { var $activeRemindersFilter = $(this).closest('.active-reminders-filter'); @@ -804,6 +838,7 @@ var RepositoryDatatable = (function(global) { initSaveButton(); initCancelButton(); initBSTooltips(); + initDeleteAssetValueConfirmModal(); window.initRepositoryStateMenu(); DataTableHelpers.initLengthAppearance($(TABLE_ID).closest('.dataTables_wrapper')); diff --git a/app/assets/javascripts/repositories/row_editor.js b/app/assets/javascripts/repositories/row_editor.js index 490b6f63d..422b01acf 100644 --- a/app/assets/javascripts/repositories/row_editor.js +++ b/app/assets/javascripts/repositories/row_editor.js @@ -80,25 +80,17 @@ var RepositoryDatatableRowEditor = (function() { $fileBtn.removeClass('error'); }); - deleteButtons.on('click', function() { + const $deleteRepositoryAssetValueModal = $('#deleteRepositoryAssetValueModal'); let $fileBtn = $(this).parent(); let $input = $fileBtn.prev('input[type=file]'); let $label = $fileBtn.find('label'); - $fileBtn.addClass('new-file'); - $label.text(''); - $input.val(''); - $fileBtn.removeClass('error'); + $deleteRepositoryAssetValueModal.data('cellFileBtn', $fileBtn); + $deleteRepositoryAssetValueModal.data('cellInput', $input); + $deleteRepositoryAssetValueModal.data('cellLabel', $label); - if (!$input.data('is-empty')) { // set hidden field for deletion only if original value has been set on rendering - $input - .prev('.file-hidden-field-container') - .html(``); - } + $('#deleteRepositoryAssetValueModal').modal('show'); }); } diff --git a/app/assets/javascripts/shareable_links/handson_table_wraping.js b/app/assets/javascripts/shareable_links/handson_table_wraping.js index 084c6d1ae..68c91b104 100644 --- a/app/assets/javascripts/shareable_links/handson_table_wraping.js +++ b/app/assets/javascripts/shareable_links/handson_table_wraping.js @@ -1,17 +1,12 @@ /* global */ (function () { - const rtf = $('.rtf-view').toArray(); - for (let i = 0; i < rtf.length; i += 1) { - const container = $(rtf[i]).find('table').toArray(); - - for (let j = 0; j < container.length; j += 1) { - const table = $(container[j]); - if ($(table).parent().hasClass('table-wrapper')) return; - - $(table).wrap(` -
- `); - } - } + $('.rtf-view').toArray().forEach((rtf) => { + $(rtf).find('table').toArray().forEach((table) => { + if ($(table).parents('table').length === 0) { + $(table).css('float', 'none') + .wrapAll('
'); + } + }); + }); }()); diff --git a/app/assets/javascripts/sitewide/active_storage_previews.js b/app/assets/javascripts/sitewide/active_storage_previews.js index fe0ab846e..d1085dd33 100644 --- a/app/assets/javascripts/sitewide/active_storage_previews.js +++ b/app/assets/javascripts/sitewide/active_storage_previews.js @@ -22,8 +22,10 @@ var ActiveStoragePreviews = (function() { if (!$(img).parent().hasClass('processing')) $(img).parent().addClass('processing'); setTimeout(() => { - img.src = src; - img.retryCount += 1; + if (document.body.contains(img)) { + img.src = src; + img.retryCount += 1; + } }, RETRY_DELAY); }, showPreview: function(ev) { diff --git a/app/assets/javascripts/sitewide/dropdown_selector.js b/app/assets/javascripts/sitewide/dropdown_selector.js index b78704270..41902a504 100644 --- a/app/assets/javascripts/sitewide/dropdown_selector.js +++ b/app/assets/javascripts/sitewide/dropdown_selector.js @@ -353,7 +353,7 @@ var dropdownSelector = (function() { // If we setup Select All we draw it and add correspond logic if (selectElement.data('select-all-button')) { - $(``) + $(``) .appendTo(dropdownContainer.find('.dropdown-container')) .click(() => { // For AJAX dropdown we will use only "Deselect All" diff --git a/app/assets/javascripts/sitewide/utils.js b/app/assets/javascripts/sitewide/utils.js index d4ae79462..bfd02c99f 100644 --- a/app/assets/javascripts/sitewide/utils.js +++ b/app/assets/javascripts/sitewide/utils.js @@ -142,23 +142,14 @@ $.fn.initSubmitModal = function(modalID, modelName) { * @returns {string} - HTML with tables wrapped. */ function wrapTables(htmlStringOrDomEl) { - if (typeof htmlStringOrDomEl === 'string') { - const container = $(`${htmlStringOrDomEl}`); - container.find('table').toArray().forEach((table) => { - if ($(table).parent().hasClass('table-wrapper')) return; - $(table).css('float', 'none').wrapAll(` -
- `); - }); - return container.prop('outerHTML'); - } - // Check if the value is a DOM element - if (htmlStringOrDomEl instanceof Element) { - const tableElement = $(htmlStringOrDomEl).find('table'); - if (tableElement.length > 0) { - tableElement.wrap('
'); - const updatedHtml = $(htmlStringOrDomEl).html(); - $(htmlStringOrDomEl).replaceWith(updatedHtml); + const htmlContent = `${htmlStringOrDomEl}`; + const container = typeof htmlStringOrDomEl === 'string' ? $(htmlContent) : $(htmlStringOrDomEl); + + container.find('table').toArray().forEach((table) => { + if ($(table).parents('table').length === 0) { + $(table).css('float', 'none') + .wrapAll('
'); } - } + }); + return container.prop('outerHTML'); } diff --git a/app/assets/stylesheets/navigation/notifications.scss b/app/assets/stylesheets/navigation/notifications.scss index 688a94995..c7049d6ef 100644 --- a/app/assets/stylesheets/navigation/notifications.scss +++ b/app/assets/stylesheets/navigation/notifications.scss @@ -30,7 +30,7 @@ flex-direction: column; height: calc(100vh - 8rem); padding: 1.5rem; - width: 400px; + width: 600px; .sci--navigation--notificaitons-flyout-title { @include font-h2; diff --git a/app/assets/stylesheets/reports/new.scss b/app/assets/stylesheets/reports/new.scss index 0355c4064..8b50742d8 100644 --- a/app/assets/stylesheets/reports/new.scss +++ b/app/assets/stylesheets/reports/new.scss @@ -250,6 +250,25 @@ } } + // scss-lint:disable ImportantRule + .dropdown-selector-container { + .dropdown-container { + left: auto !important; + margin: auto !important; + position: absolute !important; + } + } + // scss-lint:enable ImportantRule + + .repositories-contents { + .dropdown-selector-container { + display: inline-flex; + flex-shrink: 0; + margin-left: auto; + width: 200px; + } + } + .project-selector-container { background: $color-white; box-shadow: $modal-shadow; diff --git a/app/assets/stylesheets/shared/dropdown_selector.scss b/app/assets/stylesheets/shared/dropdown_selector.scss index ef119838a..21c9ff9e8 100644 --- a/app/assets/stylesheets/shared/dropdown_selector.scss +++ b/app/assets/stylesheets/shared/dropdown_selector.scss @@ -175,6 +175,10 @@ top: 0; width: 100%; z-index: 5; + + &:hover { + background: $color-concrete; + } } .dropdown-blank { diff --git a/app/assets/stylesheets/themes/scinote.scss b/app/assets/stylesheets/themes/scinote.scss index 711677980..fd7f60180 100644 --- a/app/assets/stylesheets/themes/scinote.scss +++ b/app/assets/stylesheets/themes/scinote.scss @@ -1362,6 +1362,10 @@ th.custom-field .modal-tooltiptext { cursor: pointer; } +.tooltip { + z-index: 9999; +} + .tooltip-open { background-color: $color-concrete; color: $color-black; diff --git a/app/components/reports/repositories_input_component.html.erb b/app/components/reports/repositories_input_component.html.erb new file mode 100644 index 000000000..9906b3b2c --- /dev/null +++ b/app/components/reports/repositories_input_component.html.erb @@ -0,0 +1,11 @@ +<% if @editing %> +
+ <%= label_tag @name, @label %> + <%= select_tag @name, options_from_collection_for_select(@repositories, :id, :name, @value), placeholder: @placeholder, class: 'sci-input-field report-template-value-dropdown', data: { type: 'RepositoriesInputComponent' }, multiple: true %> +
+<% else %> + <% @project_members.where(id: @value).each do |member| %> + <%= member.public_send(@displayed_field) %> +
+ <% end %> +<% end %> diff --git a/app/components/reports/repositories_input_component.rb b/app/components/reports/repositories_input_component.rb new file mode 100644 index 000000000..f168cc396 --- /dev/null +++ b/app/components/reports/repositories_input_component.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Reports + class RepositoriesInputComponent < TemplateValueComponent + def initialize(report:, name:, label:, placeholder: nil, editing: true, displayed_field: :name, user: nil) + super(report: report, name: name, label: label, placeholder: placeholder, editing: editing) + live_repositories = Repository.viewable_by_user(user, report.team).sort_by { |r| r.name.downcase } + snapshots_of_deleted = RepositorySnapshot.left_outer_joins(:original_repository) + .where(team: report.team) + .where.not(original_repository: live_repositories) + .select('DISTINCT ON ("repositories"."parent_id") "repositories".*') + .sort_by { |r| r.name.downcase } + @repositories = live_repositories + snapshots_of_deleted + @displayed_field = displayed_field + end + end +end diff --git a/app/controllers/api/api_controller.rb b/app/controllers/api/api_controller.rb index 27141eded..3eb460f18 100644 --- a/app/controllers/api/api_controller.rb +++ b/app/controllers/api/api_controller.rb @@ -8,6 +8,8 @@ module Api before_action :authenticate_request!, except: %i(status health) + newrelic_ignore only: %i(health status) + rescue_from StandardError do |e| logger.error e.message logger.error e.backtrace.join("\n") diff --git a/app/controllers/api/v1/base_controller.rb b/app/controllers/api/v1/base_controller.rb index 72eb1780e..213e4bc56 100644 --- a/app/controllers/api/v1/base_controller.rb +++ b/app/controllers/api/v1/base_controller.rb @@ -180,6 +180,9 @@ module Api def load_inventory(key = :inventory_id) @inventory = @team.repositories.find(params.require(key)) + + @inventory.unlock! if @inventory.is_a?(SoftLockedRepository) + raise PermissionError.new(Repository, :read) unless can_read_repository?(@inventory) end diff --git a/app/controllers/api/v1/inventory_columns_controller.rb b/app/controllers/api/v1/inventory_columns_controller.rb index 320c2b82c..393e62736 100644 --- a/app/controllers/api/v1/inventory_columns_controller.rb +++ b/app/controllers/api/v1/inventory_columns_controller.rb @@ -8,8 +8,9 @@ module Api before_action only: %i(show update destroy) do load_inventory_column(:id) end - before_action :check_manage_permissions, only: %i(update destroy) before_action :check_create_permissions, only: %i(create) + before_action :check_manage_permissions, only: %i(update) + before_action :check_delete_permissions, only: %i(destroy) def index columns = timestamps_filter(@inventory.repository_columns).includes(:repository_list_items) @@ -61,6 +62,10 @@ module Api raise PermissionError.new(RepositoryColumn, :manage) unless can_manage_repository_column?(@inventory_column) end + def check_delete_permissions + raise PermissionError.new(RepositoryColumn, :delete) unless can_delete_repository_column?(@inventory_column) + end + def check_create_permissions raise PermissionError.new(RepositoryColumn, :create) unless can_create_repository_columns?(@inventory) end diff --git a/app/controllers/api/v1/results_controller.rb b/app/controllers/api/v1/results_controller.rb index c26c2ef02..4665f46a8 100644 --- a/app/controllers/api/v1/results_controller.rb +++ b/app/controllers/api/v1/results_controller.rb @@ -124,7 +124,7 @@ module Api Result.transaction do old_checksum = asset.file.blob.checksum if @form_multipart_upload - asset.file.attach(result_file_params[:file]) + asset.attach_file_version(result_file_params[:file]) else blob = create_blob_from_params asset.update!(file: blob) diff --git a/app/controllers/asset_sync_controller.rb b/app/controllers/asset_sync_controller.rb index 54c6e7440..968503cd7 100644 --- a/app/controllers/asset_sync_controller.rb +++ b/app/controllers/asset_sync_controller.rb @@ -16,7 +16,8 @@ class AssetSyncController < ApplicationController asset_sync_token = current_user.asset_sync_tokens.find_or_create_by(asset_id: params[:asset_id]) unless asset_sync_token.token_valid? - asset_sync_token = current_user.asset_sync_tokens.create(asset_id: params[:asset_id]) + asset_sync_token = + current_user.asset_sync_tokens.create(asset_id: params[:asset_id]) end render json: AssetSyncTokenSerializer.new(asset_sync_token).as_json @@ -27,34 +28,32 @@ class AssetSyncController < ApplicationController end def update - if @asset_sync_token.conflicts?(request.headers['VersionToken']) - ActiveRecord::Base.transaction do - conflict_response = AssetSyncTokenSerializer.new(conflicting_asset_copy_token).as_json - error_message = { message: I18n.t('assets.conflict_error', filename: @asset.file.filename) } - log_activity(:create) - render json: conflict_response.merge(error_message), status: :conflict - end - - return - end - - orig_file_size = @asset.file_size + asset_conflicts = @asset_sync_token.conflicts?(request.headers['VersionToken']) ActiveRecord::Base.transaction do @asset.update(last_modified_by: current_user) if wopi_file?(@asset) @asset.update_contents(request.body) else - @asset.file.attach(io: request.body, filename: @asset.file.filename) + @asset.attach_file_version(io: request.body, filename: @asset.file.filename) @asset.touch end - @asset.team.release_space(orig_file_size) @asset.post_process_file log_activity(:edit) end + if asset_conflicts + ActiveRecord::Base.transaction do + conflict_response = AssetSyncTokenSerializer.new(@asset_sync_token).as_json + error_message = { message: I18n.t('assets.conflict_error', filename: @asset.file.filename) } + render json: conflict_response.merge(error_message), status: :conflict + end + + return + end + render json: AssetSyncTokenSerializer.new(@asset_sync_token).as_json end @@ -94,7 +93,7 @@ class AssetSyncController < ApplicationController metadata: @asset.blob.metadata ) - new_asset.file.attach(blob) + new_asset.attach_file_version(blob) case @asset.parent when Step diff --git a/app/controllers/assets_controller.rb b/app/controllers/assets_controller.rb index 383964890..64000acb1 100644 --- a/app/controllers/assets_controller.rb +++ b/app/controllers/assets_controller.rb @@ -19,15 +19,14 @@ class AssetsController < ApplicationController before_action :load_vars, except: :create_wopi_file before_action :check_read_permission, except: %i(edit destroy duplicate create_wopi_file toggle_view_mode) before_action :check_manage_permission, only: %i(edit destroy duplicate rename toggle_view_mode) + before_action :check_restore_permission, only: :restore_version def file_preview - editable = can_manage_asset?(@asset) && (@asset.repository_asset_value.blank? || - !@asset.repository_cell.repository_row.repository.is_a?(SoftLockedRepository)) render json: { html: render_to_string( partial: 'shared/file_preview/content', locals: { asset: @asset, - can_edit: editable, + can_edit: can_manage_asset?(@asset), gallery: params[:gallery], preview: params[:preview] }, @@ -197,7 +196,7 @@ class AssetsController < ApplicationController return render_403 unless can_read_team?(@asset.team) @asset.last_modified_by = current_user - @asset.file.attach(io: params.require(:image), filename: orig_file_name) + @asset.attach_file_version(io: params.require(:image), filename: orig_file_name) @asset.save! create_edit_image_activity(@asset, current_user, :finish_editing) # release previous image space @@ -242,9 +241,9 @@ class AssetsController < ApplicationController # Asset validation asset = Asset.new(created_by: current_user, team: current_team) - asset.file.attach(io: StringIO.new, - filename: "#{params[:file_name]}.#{params[:file_type]}", - content_type: wopi_content_type(params[:file_type])) + asset.attach_file_version(io: StringIO.new, + filename: "#{params[:file_name]}.#{params[:file_type]}", + content_type: wopi_content_type(params[:file_type])) unless asset.valid?(:wopi_file_creation) render json: { @@ -362,7 +361,8 @@ class AssetsController < ApplicationController when Step, Result new_asset = @asset.duplicate( new_name: - "#{@asset.file.filename.base} (1).#{@asset.file.filename.extension}" + "#{@asset.file.filename.base} (1).#{@asset.file.filename.extension}", + created_by: current_user ) @asset.parent.assets << new_asset @@ -397,13 +397,70 @@ class AssetsController < ApplicationController render json: { checksum: @asset.file.blob.checksum } end + def versions + blobs = + [@asset.file.blob] + + @asset.previous_files.map(&:blob).sort_by { |b| -1 * b.metadata['version'].to_i }[0..(VersionedAttachments.enabled? ? -1 : 1)] + render( + json: ActiveModel::SerializableResource.new( + blobs, + each_serializer: ActiveStorage::BlobSerializer, + user: current_user + ).as_json.merge( + enabled: VersionedAttachments.enabled?, + enable_url: ENV.fetch('SCINOTE_FILE_VERSIONING_ENABLE_URL', nil), + disabled_disclaimer: VersionedAttachments.disabled_disclaimer + ) + ) + end + + def restore_version + render_403 unless VersionedAttachments.enabled? + + @asset.last_modified_by = current_user + + @asset.restore_file_version(params[:version].to_i) + @asset.restore_preview_image_version(params[:version].to_i) if @asset.preview_image.attached? + + message_items = { + version: params[:version].to_i, + file: @asset.file_name + } + + case @asset.parent + when Step + if @asset.parent.protocol.in_module? + message_items.merge!({ my_module: @assoc.protocol.my_module.id, step: @asset.parent.id }) + log_restore_activity(:task_step_restore_asset_version, @assoc.protocol, + @assoc.protocol.team, @assoc.my_module&.project, message_items) + else + message_items.merge!({ protocol: @assoc.protocol.id, step: @asset.parent.id }) + log_restore_activity(:protocol_step_restore_asset_version, @assoc.protocol, + @assoc.protocol.team, nil, message_items) + end + when Result + message_items.merge!({ result: @assoc.id, my_module: @assoc.my_module.id }) + log_restore_activity(:task_result_restore_asset_version, @assoc, + @assoc.my_module.team, @assoc.my_module.project, message_items) + when RepositoryCell + message_items.merge!({ repository_column: @assoc.repository_column.id, repository: @repository.id }) + log_restore_activity(:repository_column_restore_asset_version, @repository, + @repository.team, nil, message_items) + end + + @asset.save! + + render json: @asset.file.blob + end + private def load_vars @asset = Asset.find_by(id: params[:id]) return render_404 unless @asset - current_user.permission_team = @asset.team + # don't overwrite permission team if asset is in a repositoy, since then sharing rules may apply and depend on user's current team + current_user.permission_team = @asset.team unless @asset.repository_cell @assoc ||= @asset.step @assoc ||= @asset.result @@ -426,6 +483,10 @@ class AssetsController < ApplicationController render_403 and return unless can_manage_asset?(@asset) end + def check_restore_permission + render_403 and return unless can_restore_asset?(@asset) + end + def append_wd_params(url) exclude_params = %w(wdPreviousSession wdPreviousCorrelation) wd_params = params.as_json.select { |key, _value| key[/^wd.*/] && !(exclude_params.include? key) }.to_query @@ -472,4 +533,14 @@ class AssetsController < ApplicationController result: result.id }.merge(message_items)) end + + def log_restore_activity(type_of, subject, team, project = nil, message_items = {}) + Activities::CreateActivityService + .call(activity_type: type_of, + owner: current_user, + subject: subject, + team: team, + project: project, + message_items: message_items) + end end diff --git a/app/controllers/gene_sequence_assets_controller.rb b/app/controllers/gene_sequence_assets_controller.rb index a706a200f..69526e9a6 100644 --- a/app/controllers/gene_sequence_assets_controller.rb +++ b/app/controllers/gene_sequence_assets_controller.rb @@ -72,15 +72,12 @@ class GeneSequenceAssetsController < ApplicationController ensure_asset! - @asset.file.purge - @asset.preview_image.purge - - @asset.file.attach( + @asset.attach_file_version( io: StringIO.new(params[:sequence_data].to_json), filename: "#{params[:sequence_name]}.json" ) - @asset.preview_image.attach( + @asset.attach_preview_image_version( io: StringIO.new(Base64.decode64(params[:base64_image].split(',').last)), filename: "#{params[:sequence_name]}.png" ) diff --git a/app/controllers/global_activities_controller.rb b/app/controllers/global_activities_controller.rb index b531c5ac1..b84085994 100644 --- a/app/controllers/global_activities_controller.rb +++ b/app/controllers/global_activities_controller.rb @@ -151,7 +151,25 @@ class GlobalActivitiesController < ApplicationController end def activity_filter_params - params.permit(:name, filter: {}) + params.permit( + :name, + filter: [ + :to_date, + :from_date, + { types: [] }, + { subjects: { + 'Report' => [], + 'Project' => [], + 'MyModule' => [], + 'Protocol' => [], + 'Experiment' => [], + 'RepositoryRow' => [], + 'RepositoryBase' => [] + } }, + { users: [] }, + { teams: [] } + ] + ) end def activity_filters diff --git a/app/controllers/protocols_controller.rb b/app/controllers/protocols_controller.rb index b0ade7fb4..29d7368a6 100644 --- a/app/controllers/protocols_controller.rb +++ b/app/controllers/protocols_controller.rb @@ -18,11 +18,13 @@ class ProtocolsController < ApplicationController print versions_modal protocol_status_bar - linked_children - linked_children_datatable versions_list permissions ) + before_action :check_linked_protocol_view_permissions, only: %i( + linked_children + linked_children_datatable + ) before_action :switch_team_with_param, only: %i(index protocolsio_index) before_action :check_view_all_permissions, only: %i( index @@ -909,7 +911,6 @@ class ProtocolsController < ApplicationController end def set_inline_name_editing - return unless @protocol.initial_draft? return unless can_manage_protocol_draft_in_repository?(@protocol) @inline_editable_title_config = { @@ -934,6 +935,16 @@ class ProtocolsController < ApplicationController end def check_view_permissions + @protocol = Protocol.find_by(id: params[:id]) + current_team_switch(@protocol.team) if current_team != @protocol.team + unless @protocol.present? && + (can_read_protocol_in_module?(@protocol) || + can_read_protocol_in_repository?(@protocol)) + render_403 + end + end + + def check_linked_protocol_view_permissions @protocol = Protocol.find_by(id: params[:id]) current_team_switch(@protocol.team) if current_team != @protocol.team unless @protocol.present? && diff --git a/app/controllers/reports_controller.rb b/app/controllers/reports_controller.rb index 8e779bb0c..fab290fd7 100644 --- a/app/controllers/reports_controller.rb +++ b/app/controllers/reports_controller.rb @@ -7,18 +7,20 @@ class ReportsController < ApplicationController before_action :load_vars, only: %i(edit update document_preview generate_pdf generate_docx status save_pdf_to_inventory_modal save_pdf_to_inventory_item) before_action :load_vars_nested, only: %i(create edit update generate_pdf - generate_docx new_template_values project_contents) + generate_docx new_template_values new_docx_template_values project_contents) before_action :load_wizard_vars, only: %i(new edit) + before_action :load_repositories_vars, only: %i(new edit create update) before_action :load_available_repositories, only: %i(index save_pdf_to_inventory_modal available_repositories) before_action :check_project_read_permissions, only: %i(create edit update generate_pdf - generate_docx new_template_values project_contents) + generate_docx new_template_values new_docx_template_values project_contents) before_action :check_read_permissions, except: %i(index new create edit update destroy actions_toolbar generate_pdf - generate_docx new_template_values project_contents + generate_docx new_template_values project_contents new_docx_template_values available_repositories) before_action :check_create_permissions, only: %i(new create) before_action :check_manage_permissions, only: %i(edit update generate_pdf generate_docx) before_action :switch_team_with_param, only: :index - after_action :generate_pdf_report, only: %i(create update generate_pdf) + after_action :generate_pdf_report, only: %i(generate_pdf) + after_action :generate_report, only: %i(create update) # Index showing all reports of a single project def index @@ -37,12 +39,15 @@ class ReportsController < ApplicationController # Report grouped by modules def new @templates = Extends::REPORT_TEMPLATES + @docx_templates = Extends::DOCX_REPORT_TEMPLATES @report = current_team.reports.new end def new_template_values if Extends::REPORT_TEMPLATES.key?(params[:template]&.to_sym) template = params[:template] + @type = :pdf + @template_name = Extends::REPORT_TEMPLATES[params[:template].to_sym] else return render_404 end @@ -68,6 +73,43 @@ class ReportsController < ApplicationController else render json: { html: render_to_string(partial: 'reports/wizard/no_template_values', + locals: { type: @type, template: @template_name }, + formats: :html) + } + end + end + + def new_docx_template_values + if Extends::DOCX_REPORT_TEMPLATES.key?(params[:template]&.to_sym) + template = params[:template] + @type = :docx + @template_name = Extends::DOCX_REPORT_TEMPLATES[params[:template].to_sym] + else + return render_404 + end + + report = current_team.reports.where(project: @project).find_by(id: params[:report_id]) + if report.present? + return render_403 unless can_manage_report?(report) + else + return render_403 unless can_create_reports?(current_team) + + report = current_team.reports.new(project: @project) + end + + if lookup_context.any_templates?("reports/docx_templates/#{template}/edit") + render json: { + html: render_to_string( + template: "reports/docx_templates/#{template}/edit", + layout: 'reports/template_values_editor', + locals: { report: report }, + formats: :html + ) + } + else + render json: { + html: render_to_string(partial: 'reports/wizard/no_template_values', + locals: { type: @type, template: @template_name }, formats: :html) } end @@ -102,6 +144,7 @@ class ReportsController < ApplicationController def edit @edit = true @active_template = @report.settings[:template] + @active_docx_template = @report.settings[:docx_template].presence || 'scinote_template' @report.settings = Report::DEFAULT_SETTINGS if @report.settings.blank? @project_contents = { @@ -312,13 +355,7 @@ class ReportsController < ApplicationController def load_wizard_vars @templates = Extends::REPORT_TEMPLATES - live_repositories = Repository.viewable_by_user(current_user).sort_by { |r| r.name.downcase } - snapshots_of_deleted = RepositorySnapshot.left_outer_joins(:original_repository) - .where(team: current_team) - .where.not(original_repository: live_repositories) - .select('DISTINCT ON ("repositories"."parent_id") "repositories".*') - .sort_by { |r| r.name.downcase } - @repositories = live_repositories + snapshots_of_deleted + @docx_templates = Extends::DOCX_REPORT_TEMPLATES @visible_projects = current_team.projects .active .joins(experiments: :my_modules) @@ -327,6 +364,19 @@ class ReportsController < ApplicationController .merge(MyModule.active) .group(:id) .select(:id, :name) + @default_template = Extends::REPORT_TEMPLATES.keys.first.to_s if Extends::REPORT_TEMPLATES.one? + + @default_docx_template = Extends::DOCX_REPORT_TEMPLATES.keys.first.to_s if Extends::DOCX_REPORT_TEMPLATES.one? && custom_templates(Extends::DOCX_REPORT_TEMPLATES) + end + + def load_repositories_vars + live_repositories = Repository.viewable_by_user(current_user).sort_by { |r| r.name.downcase } + snapshots_of_deleted = RepositorySnapshot.left_outer_joins(:original_repository) + .where(team: current_team) + .where.not(original_repository: live_repositories) + .select('DISTINCT ON ("repositories"."parent_id") "repositories".*') + .sort_by { |r| r.name.downcase } + @repositories = live_repositories + snapshots_of_deleted end def check_project_read_permissions @@ -361,7 +411,7 @@ class ReportsController < ApplicationController def report_params params.require(:report) - .permit(:name, :description, :grouped_by, :report_contents, settings: {}) + .permit(:name, :description, :grouped_by, :report_contents, settings: permit_report_settings_structure(Report::DEFAULT_SETTINGS, @repositories)) end def search_params @@ -394,6 +444,26 @@ class ReportsController < ApplicationController Rails.logger.error e.message end + def generate_docx_report + return unless @report.persisted? + + @report.docx_processing! + log_activity(:generate_docx_report) + + ensure_report_template! + Reports::DocxJob.perform_later(@report.id, user_id: current_user.id, root_url: root_url) + rescue ActiveRecord::ActiveRecordError => e + Rails.logger.error e.message + end + + def generate_report + return unless @report.persisted? + + generate_pdf_report + + generate_docx_report if @report.settings['docx_template'].present? && custom_templates(Extends::DOCX_REPORT_TEMPLATES) + end + def ensure_report_template! return if @report.settings['template'].present? diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb index bddd1c863..e17486afc 100644 --- a/app/controllers/repositories_controller.rb +++ b/app/controllers/repositories_controller.rb @@ -495,7 +495,7 @@ class RepositoriesController < ApplicationController end def set_inline_name_editing - return unless can_manage_repository?(@repository) && !@repository.is_a?(SoftLockedRepository) + return unless can_manage_repository?(@repository) @inline_editable_title_config = { name: 'title', diff --git a/app/controllers/repository_columns_controller.rb b/app/controllers/repository_columns_controller.rb index 707a6beb8..73cefa87b 100644 --- a/app/controllers/repository_columns_controller.rb +++ b/app/controllers/repository_columns_controller.rb @@ -5,7 +5,8 @@ class RepositoryColumnsController < ApplicationController before_action :load_repository before_action :load_column, only: %i(edit update destroy_html destroy items) before_action :check_create_permissions, only: %i(new create) - before_action :check_manage_permissions, only: %i(edit update destroy_html destroy) + before_action :check_manage_permissions, only: %i(edit update) + before_action :check_delete_permissions, only: %i(destroy_html destroy) before_action :load_asset_type_columns, only: :available_asset_type_columns def index @@ -130,6 +131,10 @@ class RepositoryColumnsController < ApplicationController render_403 unless can_manage_repository_column?(@repository_column) end + def check_delete_permissions + render_403 unless can_delete_repository_column?(@repository_column) + end + def search_params params.permit(:q, :repository_id) end diff --git a/app/controllers/result_assets_controller.rb b/app/controllers/result_assets_controller.rb index e2c429260..400ae671e 100644 --- a/app/controllers/result_assets_controller.rb +++ b/app/controllers/result_assets_controller.rb @@ -125,7 +125,7 @@ class ResultAssetsController < ApplicationController ActiveRecord::Base.transaction do params[:results_files].each do |index, file| asset = Asset.create!(created_by: current_user, last_modified_by: current_user, team: current_team) - asset.file.attach(file[:signed_blob_id]) + asset.attach_file_version(file[:signed_blob_id]) result = Result.create!(user: current_user, my_module: @my_module, name: params[:results_names][index], diff --git a/app/controllers/results_controller.rb b/app/controllers/results_controller.rb index 075b2b669..3bc46e3d2 100644 --- a/app/controllers/results_controller.rb +++ b/app/controllers/results_controller.rb @@ -90,7 +90,7 @@ class ResultsController < ApplicationController team: @my_module.team, view_mode: @result.assets_view_mode ) - @asset.file.attach(params[:signed_blob_id]) + @asset.attach_file_version(params[:signed_blob_id]) @asset.post_process_file end diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index a86d04a82..afa8fd4e4 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -145,7 +145,10 @@ class SearchController < ApplicationController def quick results = if params[:filter].present? - object_quick_search(params[:filter].singularize) + class_name = params[:filter].singularize + return render_422(t('general.invalid_params')) unless Constants::QUICK_SEARCH_SEARCHABLE_OBJECTS.include?(class_name) + + object_quick_search(class_name) else Constants::QUICK_SEARCH_SEARCHABLE_OBJECTS.filter_map do |object| next if object == 'label_template' && !LabelTemplate.enabled? diff --git a/app/controllers/steps_controller.rb b/app/controllers/steps_controller.rb index 40f2845f5..ce97a4cc2 100644 --- a/app/controllers/steps_controller.rb +++ b/app/controllers/steps_controller.rb @@ -41,7 +41,7 @@ class StepsController < ApplicationController team: @protocol.team, view_mode: @step.assets_view_mode ) - @asset.file.attach(params[:signed_blob_id]) + @asset.attach_file_version(params[:signed_blob_id]) @asset.post_process_file default_message_items = { diff --git a/app/controllers/user_notifications_controller.rb b/app/controllers/user_notifications_controller.rb index ca05c3bbc..127d35289 100644 --- a/app/controllers/user_notifications_controller.rb +++ b/app/controllers/user_notifications_controller.rb @@ -5,11 +5,18 @@ class UserNotificationsController < ApplicationController def index page = (params.dig(:page, :number) || 1).to_i - notifications = load_notifications.page(page).per(Constants::INFINITE_SCROLL_LIMIT) + notifications = load_notifications + + case params[:tab] + when 'read' + notifications = notifications.where.not(read_at: nil) + when 'unread' + notifications = notifications.where(read_at: nil) + end + + notifications = notifications.page(page).per(Constants::INFINITE_SCROLL_LIMIT) render json: notifications, each_serializer: NotificationSerializer - - notifications.mark_as_read! end def unseen_counter @@ -18,6 +25,17 @@ class UserNotificationsController < ApplicationController } end + def mark_all_read + load_notifications.mark_as_read! + render json: { success: true } + end + + def toggle_read + notification = current_user.notifications.find(params[:id]) + notification.update(read_at: (params[:mark_as_read] ? DateTime.now : nil)) + render json: notification, serializer: NotificationSerializer + end + private def load_notifications @@ -25,5 +43,4 @@ class UserNotificationsController < ApplicationController .in_app .order(created_at: :desc) end - end diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index 74d7fc180..ad62346c3 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -114,10 +114,7 @@ module Users email: auth_hash['info']['email'], password: generate_user_password ) - if auth_hash['info']['picture_url'] - avatar = URI.open(auth_hash['info']['picture_url']) - @user.avatar.attach(io: avatar, filename: 'linkedin_avatar.jpg') - end + @user.avatar.attach(io: URI(auth_hash['info']['picture_url']).open, filename: 'linkedin_avatar.jpg') if auth_hash['info']['picture_url'] user_identity = UserIdentity.new(user: @user, provider: auth_hash['provider'], uid: auth_hash['uid']) diff --git a/app/controllers/wopi_controller.rb b/app/controllers/wopi_controller.rb index 6dbe4051e..a34b01d27 100644 --- a/app/controllers/wopi_controller.rb +++ b/app/controllers/wopi_controller.rb @@ -200,7 +200,6 @@ class WopiController < ActionController::Base if @asset.lock == lock logger.warn 'WOPI: replacing file' - @team.release_space(@asset.estimated_size) @asset.last_modified_by = @user @asset.update_contents(request.body) @asset.save @@ -220,7 +219,6 @@ class WopiController < ActionController::Base elsif !@asset.file_size.nil? && @asset.file_size.zero? logger.warn 'WOPI: initializing empty file' - @team.release_space(@asset.estimated_size) @asset.update_contents(request.body) @asset.last_modified_by = @user @asset.save diff --git a/app/helpers/form_tag_helper.rb b/app/helpers/form_tag_helper.rb index 71235c154..be9532ee7 100644 --- a/app/helpers/form_tag_helper.rb +++ b/app/helpers/form_tag_helper.rb @@ -9,7 +9,7 @@ module FormTagHelper res << label_tag(:recaptcha_label, I18n.t('users.registrations.new.captcha_description')) end - res << recaptcha_tags + res << recaptcha_tags(nonce: content_security_policy_nonce) if flash[:recaptcha_error] res << "" res << flash[:recaptcha_error] diff --git a/app/helpers/global_activities_helper.rb b/app/helpers/global_activities_helper.rb index af945712a..4d09aaee1 100644 --- a/app/helpers/global_activities_helper.rb +++ b/app/helpers/global_activities_helper.rb @@ -91,7 +91,7 @@ module GlobalActivitiesHelper end when Protocol if obj.my_module.nil? - path = protocols_path(team: obj.team.id) + path = protocol_path(obj) elsif obj.my_module.navigable? path = protocols_my_module_path(obj.my_module) else diff --git a/app/helpers/input_sanitize_helper.rb b/app/helpers/input_sanitize_helper.rb index 73c9eaca2..55c3ff38e 100644 --- a/app/helpers/input_sanitize_helper.rb +++ b/app/helpers/input_sanitize_helper.rb @@ -40,11 +40,12 @@ module InputSanitizeHelper preview_repository = options.fetch(:preview_repository, false) format_opt = wrapper_tag.merge(sanitize: false) base64_encoded_imgs = options.fetch(:base64_encoded_imgs, false) - text = simple_format(text, {}, format_opt) if simple_f # allow base64 images when sanitizing if base64_encoded_imgs is true sanitizer_config = Constants::INPUT_SANITIZE_CONFIG.deep_dup + text = sanitize_input(text, tags, sanitizer_config: sanitizer_config) + text = simple_format(text, {}, format_opt) if simple_f text = smart_annotation_parser(text, team, base64_encoded_imgs, preview_repository) if text.match?(SmartAnnotations::TagToHtml::ALL_REGEX) diff --git a/app/helpers/reports_helper.rb b/app/helpers/reports_helper.rb index 7dcc85929..7d7df14cc 100644 --- a/app/helpers/reports_helper.rb +++ b/app/helpers/reports_helper.rb @@ -106,4 +106,25 @@ module ReportsHelper experiment_element.experiment.description end end + + def permit_report_settings_structure(settings_definition, repositories) + settings_definition.each_with_object([]) do |(key, value), permitted| + permitted << if key == :excluded_repository_columns && repositories.present? + { key => repositories.each_with_object({}) { |repository, hash| hash[repository.id.to_s] = [] } } + else + case value + when Hash + { key => permit_report_settings_structure(value, repositories) } + when Array + { key => [] } + else + key + end + end + end + end + + def custom_templates(templates) + templates.any? { |template, _| template != :scinote_template } + end end diff --git a/app/helpers/repository_datatable_helper.rb b/app/helpers/repository_datatable_helper.rb index 2f64004d0..a1f4364d2 100644 --- a/app/helpers/repository_datatable_helper.rb +++ b/app/helpers/repository_datatable_helper.rb @@ -12,8 +12,7 @@ module RepositoryDatatableHelper repository_row_connections_enabled = Repository.repository_row_connections_enabled? reminders_enabled = Repository.reminders_enabled? stock_managable = has_stock_management && !options[:disable_stock_management] && - can_manage_repository_stock?(repository) && - !repository.is_a?(SoftLockedRepository) + can_manage_repository_stock?(repository) stock_consumption_permitted = has_stock_management && options[:include_stock_consumption] && options[:my_module] && stock_consumption_permitted?(repository, options[:my_module]) default_columns_method_name = "#{repository.class.name.underscore}_default_columns" @@ -31,7 +30,7 @@ module RepositoryDatatableHelper row['relationships_enabled'] = repository_row_connections_enabled row['hasActiveReminders'] = record.has_active_reminders if reminders_enabled - unless options[:view_mode] || repository.is_a?(SoftLockedRepository) + unless options[:view_mode] row['recordUpdateUrl'] = Rails.application.routes.url_helpers.repository_repository_row_path(repository, record) diff --git a/app/javascript/packs/tiny_mce.js b/app/javascript/packs/tiny_mce.js index 723d3ab0a..cc041eb4c 100644 --- a/app/javascript/packs/tiny_mce.js +++ b/app/javascript/packs/tiny_mce.js @@ -489,11 +489,10 @@ window.TinyMCE = (() => { }, wrapTables: (container) => { container.find('table').toArray().forEach((table) => { - if ($(table).parent().hasClass('table-wrapper')) return; - - $(table).css('float', 'none').wrapAll(` -
- `); + if ($(table).parents('table').length === 0) { + $(table).css('float', 'none') + .wrapAll('
'); + } }); } }; diff --git a/app/javascript/packs/vue/design_system/breadcrumbs.js b/app/javascript/packs/vue/design_system/breadcrumbs.js new file mode 100644 index 000000000..207ba469a --- /dev/null +++ b/app/javascript/packs/vue/design_system/breadcrumbs.js @@ -0,0 +1,22 @@ +import { createApp } from 'vue/dist/vue.esm-bundler.js'; +import Breadcrumbs from '../../../vue/shared/breadcrumbs.vue'; +import { mountWithTurbolinks } from '../helpers/turbolinks.js'; + +const app = createApp({ + computed: { + breadcrumbs() { + return [ + { name: 'Home', url: '/' }, + { name: 'Very very very long name ', url: '' }, + { name: 'Data', url: '' }, + { name: 'Very very very very very very very very very very long name ', url: '' }, + { name: 'Very very very very very very very long name ', url: '' }, + { name: 'Very very very very very long name ', url: '' }, + { name: 'Very very very very long name ', url: '' } + ]; + } + } +}); +app.component('Breadcrumbs', Breadcrumbs); +app.config.globalProperties.i18n = window.I18n; +mountWithTurbolinks(app, '#breadcrumbs'); diff --git a/app/javascript/vue/navigation/notifications/notification_item.vue b/app/javascript/vue/navigation/notifications/notification_item.vue index b187636f1..15aea8d55 100644 --- a/app/javascript/vue/navigation/notifications/notification_item.vue +++ b/app/javascript/vue/navigation/notifications/notification_item.vue @@ -1,41 +1,63 @@ ; diff --git a/app/javascript/vue/shared/content/attachments/attachment_actions.vue b/app/javascript/vue/shared/content/attachments/attachment_actions.vue index 71561f7c3..6147efa33 100644 --- a/app/javascript/vue/shared/content/attachments/attachment_actions.vue +++ b/app/javascript/vue/shared/content/attachments/attachment_actions.vue @@ -27,6 +27,7 @@ @attachment:changed="$emit('attachment:changed', $event)" @attachment:update="$emit('attachment:update', $event)" @menu-toggle="$emit('attachment:toggle_menu', $event)" + @attachment:versionRestored="$emit('attachment:versionRestored', $event)" :withBorder="withBorder" /> diff --git a/app/javascript/vue/shared/content/attachments/context_menu.vue b/app/javascript/vue/shared/content/attachments/context_menu.vue index bb17af66c..49135586b 100644 --- a/app/javascript/vue/shared/content/attachments/context_menu.vue +++ b/app/javascript/vue/shared/content/attachments/context_menu.vue @@ -34,6 +34,7 @@ @duplicate="duplicate" @viewMode="changeViewMode" @move="showMoveModal" + @fileVersionsModal="fileVersionsModal = true" @menu-toggle="$emit('menu-toggle', $event)" > @@ -55,6 +56,13 @@ :targets_url="attachment.attributes.urls.move_targets" @confirm="moveAttachment($event)" @cancel="closeMoveModal" /> + @@ -65,6 +73,7 @@ import deleteAttachmentModal from './delete_modal.vue'; import MoveAssetModal from '../modal/move.vue'; import MoveMixin from './mixins/move.js'; import MenuDropdown from '../../menu_dropdown.vue'; +import FileVersionsModal from '../../file_versions_modal.vue'; import axios from '../../../../packs/custom_axios.js'; export default { @@ -73,6 +82,7 @@ export default { RenameAttachmentModal, deleteAttachmentModal, MoveAssetModal, + FileVersionsModal, MenuDropdown }, mixins: [MoveMixin], @@ -91,7 +101,8 @@ export default { return { viewModeOptions: ['inline', 'thumbnail', 'list'], deleteModal: false, - renameModal: false + renameModal: false, + fileVersionsModal: false }; }, computed: { @@ -124,6 +135,12 @@ export default { data_e2e: 'e2e-BT-attachmentOptions-delete' }); } + if (this.attachment.attributes.urls.versions) { + menu.push({ + text: this.i18n.t('assets.context_menu.versions'), + emit: 'fileVersionsModal' + }); + } if (this.attachment.attributes.urls.toggle_view_mode) { this.viewModeOptions.forEach((viewMode, i) => { menu.push({ diff --git a/app/javascript/vue/shared/content/attachments/inline.vue b/app/javascript/vue/shared/content/attachments/inline.vue index 14788fc32..d1fec5fb7 100644 --- a/app/javascript/vue/shared/content/attachments/inline.vue +++ b/app/javascript/vue/shared/content/attachments/inline.vue @@ -38,6 +38,7 @@ @attachment:delete="deleteAttachment" @attachment:moved="attachmentMoved" @attachment:uploaded="reloadAttachments" + @attachment:versionRestored="reloadAttachments" @attachment:changed="$emit('attachment:changed', $event)" @attachment:update="$emit('attachment:update', $event)" @attachment:toggle_menu="toggleMenuDropdown" diff --git a/app/javascript/vue/shared/content/attachments/list.vue b/app/javascript/vue/shared/content/attachments/list.vue index d8cdc5ad6..2a2aeb03b 100644 --- a/app/javascript/vue/shared/content/attachments/list.vue +++ b/app/javascript/vue/shared/content/attachments/list.vue @@ -37,6 +37,7 @@ @attachment:delete="deleteAttachment" @attachment:moved="attachmentMoved" @attachment:uploaded="reloadAttachments" + @attachment:versionRestored="reloadAttachments" @attachment:changed="$emit('attachment:changed', $event)" @attachment:update="$emit('attachment:update', $event)" @attachment:toggle_menu="toggleMenuDropdown" diff --git a/app/javascript/vue/shared/content/attachments/open_locally_menu.vue b/app/javascript/vue/shared/content/attachments/open_locally_menu.vue index 8ea7048cc..add560936 100644 --- a/app/javascript/vue/shared/content/attachments/open_locally_menu.vue +++ b/app/javascript/vue/shared/content/attachments/open_locally_menu.vue @@ -1,31 +1,34 @@ @@ -55,14 +65,20 @@ import OpenLocallyMixin from './mixins/open_locally.js'; import MenuDropdown from '../../menu_dropdown.vue'; import UpdateVersionModal from '../modal/update_version_modal.vue'; +import FileVersionsModal from '../../file_versions_modal.vue'; export default { name: 'OpenLocallyMenu', mixins: [OpenLocallyMixin], - components: { MenuDropdown, UpdateVersionModal }, + components: { MenuDropdown, UpdateVersionModal, FileVersionsModal }, props: { attachment: { type: Object, required: true }, - disableLocalOpen: { type: Boolean, default: false } + canEdit: { type: Boolean, default: true } + }, + data() { + return { + fileVersionsModal: false + }; }, created() { this.fetchLocalAppInfo(); @@ -92,7 +108,7 @@ export default { }); } - if (this.canOpenLocally && !this.disableLocalOpen) { + if (this.canOpenLocally) { const text = this.localAppName ? this.i18n.t('attachments.open_locally_in', { application: this.localAppName }) : this.i18n.t('attachments.open_locally'); @@ -116,6 +132,13 @@ export default { methods: { openImageEditor() { document.getElementById('editImageButton').click(); + }, + refreshPreview() { + const filePreview = document.querySelector('.file-preview-container'); + + if (!filePreview) return; + + window.location.reload(); } } }; diff --git a/app/javascript/vue/shared/content/attachments/thumbnail.vue b/app/javascript/vue/shared/content/attachments/thumbnail.vue index ebdc25442..bc7ac5684 100644 --- a/app/javascript/vue/shared/content/attachments/thumbnail.vue +++ b/app/javascript/vue/shared/content/attachments/thumbnail.vue @@ -54,6 +54,7 @@ @attachment:delete="deleteAttachment" @attachment:moved="attachmentMoved" @attachment:uploaded="reloadAttachments" + @attachment:versionRestored="reloadAttachments" @attachment:changed="$emit('attachment:changed', $event)" @attachment:update="$emit('attachment:update', $event)" @attachment:toggle_menu="toggleMenu" diff --git a/app/javascript/vue/shared/file_versions_modal.vue b/app/javascript/vue/shared/file_versions_modal.vue new file mode 100644 index 000000000..6d4765066 --- /dev/null +++ b/app/javascript/vue/shared/file_versions_modal.vue @@ -0,0 +1,143 @@ + + + diff --git a/app/javascript/vue/storage_locations/modals/new_edit.vue b/app/javascript/vue/storage_locations/modals/new_edit.vue index b0a97df6f..16057a0ac 100644 --- a/app/javascript/vue/storage_locations/modals/new_edit.vue +++ b/app/javascript/vue/storage_locations/modals/new_edit.vue @@ -44,11 +44,11 @@ {{ i18n.t('storage_locations.index.edit_modal.grid') }} -
+
-
+
diff --git a/app/jobs/my_modules/due_date_reminder_job.rb b/app/jobs/my_modules/due_date_reminder_job.rb index 839f4d07f..9975a5436 100644 --- a/app/jobs/my_modules/due_date_reminder_job.rb +++ b/app/jobs/my_modules/due_date_reminder_job.rb @@ -3,6 +3,7 @@ module MyModules class DueDateReminderJob < ApplicationJob def perform + NewRelic::Agent.ignore_transaction my_modules = MyModule.uncomplete.approaching_due_dates my_modules.each do |task| diff --git a/app/jobs/notification_cleanup_job.rb b/app/jobs/notification_cleanup_job.rb index cea9fa32f..31c2380d4 100644 --- a/app/jobs/notification_cleanup_job.rb +++ b/app/jobs/notification_cleanup_job.rb @@ -2,6 +2,7 @@ class NotificationCleanupJob < ApplicationJob def perform + NewRelic::Agent.ignore_transaction Notification.where('created_at < ?', 3.months.ago).delete_all end end diff --git a/app/jobs/pdf_preview_job.rb b/app/jobs/pdf_preview_job.rb index 8f161d226..1a9a22cb8 100644 --- a/app/jobs/pdf_preview_job.rb +++ b/app/jobs/pdf_preview_job.rb @@ -16,39 +16,9 @@ class PdfPreviewJob < ApplicationJob def perform(asset_id) asset = Asset.find(asset_id) - blob = asset.file.blob - blob.open(tmpdir: tempdir) do |input| - work_dir = File.dirname(input.path) - preview_filename = "#{File.basename(input.path, '.*')}.pdf" - preview_file = File.join(work_dir, preview_filename) - Rails.logger.info "Starting preparing document preview for file #{blob.filename.sanitized}..." - ActiveRecord::Base.transaction do - success = system( - libreoffice_path, '--headless', '--invisible', '--convert-to', 'pdf', '--outdir', work_dir, input.path - ) - unless success && File.file?(preview_file) - raise StandardError, "There was an error generating PDF preview, blob id: #{blob.id}" - end - - ActiveRecord::Base.no_touching do - asset.file_pdf_preview.attach(io: File.open(preview_file), filename: "#{blob.filename.base}.pdf") - asset.update(pdf_preview_processing: false) - end - Rails.logger.info("Finished preparing PDF preview for file #{blob.filename.sanitized}.") - end - ensure - File.delete(preview_file) if File.file?(preview_file) - end - end - - private - - def tempdir - Rails.root.join('tmp') - end - - def libreoffice_path - ENV['LIBREOFFICE_PATH'] || 'soffice' + PdfPreviewService.new(asset.file.blob, asset.file_pdf_preview).generate! + ensure + asset.update(pdf_preview_processing: false) end end diff --git a/app/jobs/protocols/docx_import_job.rb b/app/jobs/protocols/docx_import_job.rb index 49a34bbc5..dccd1a0dd 100644 --- a/app/jobs/protocols/docx_import_job.rb +++ b/app/jobs/protocols/docx_import_job.rb @@ -117,7 +117,7 @@ module Protocols def create_step_asset_element!(step, step_element_json) asset = @team.assets.new(created_by: @user, last_modified_by: @user) # Decode the file bytes - asset.file.attach(io: StringIO.new(Base64.decode64(step_element_json['contents'])), filename: 'file.blob') + asset.attach_file_version(io: StringIO.new(Base64.decode64(step_element_json['contents'])), filename: 'file.blob') asset.save! step.step_assets.create!(asset: asset) asset.post_process_file diff --git a/app/jobs/reports/docx_job.rb b/app/jobs/reports/docx_job.rb index 51043ff37..1165653eb 100644 --- a/app/jobs/reports/docx_job.rb +++ b/app/jobs/reports/docx_job.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'caracal' + module Reports class DocxJob < ApplicationJob extend InputSanitizeHelper diff --git a/app/jobs/reports/docx_preview_job.rb b/app/jobs/reports/docx_preview_job.rb index 5134bc529..9c35c6160 100644 --- a/app/jobs/reports/docx_preview_job.rb +++ b/app/jobs/reports/docx_preview_job.rb @@ -15,39 +15,7 @@ module Reports def perform(report_id) report = Report.find(report_id) - blob = report.docx_file.blob - blob.open(tmpdir: tempdir) do |input| - work_dir = File.dirname(input.path) - preview_filename = "#{File.basename(input.path, '.*')}.pdf" - preview_file = File.join(work_dir, preview_filename) - Rails.logger.info "Starting preparing document preview for file #{blob.filename.sanitized}..." - - ActiveRecord::Base.transaction do - success = system( - libreoffice_path, '--headless', '--invisible', '--convert-to', 'pdf', '--outdir', work_dir, input.path - ) - unless success && File.file?(preview_file) - raise StandardError, "There was an error generating PDF preview, blob id: #{blob.id}" - end - - ActiveRecord::Base.no_touching do - report.docx_preview_file.attach(io: File.open(preview_file), filename: "#{blob.filename.base}.pdf") - end - Rails.logger.info("Finished preparing PDF preview for file #{blob.filename.sanitized}.") - end - ensure - File.delete(preview_file) if File.file?(preview_file) - end - end - - private - - def tempdir - Rails.root.join('tmp') - end - - def libreoffice_path - ENV['LIBREOFFICE_PATH'] || 'soffice' + PdfPreviewService.new(report.docx_file.blob, report.docx_preview_file).generate! end end end diff --git a/app/jobs/repository_item_date_reminder_job.rb b/app/jobs/repository_item_date_reminder_job.rb index 0ed56f118..555ca1e74 100644 --- a/app/jobs/repository_item_date_reminder_job.rb +++ b/app/jobs/repository_item_date_reminder_job.rb @@ -4,6 +4,7 @@ class RepositoryItemDateReminderJob < ApplicationJob queue_as :default def perform + NewRelic::Agent.ignore_transaction process_repository_values(RepositoryDateTimeValue, DateTime.current) process_repository_values(RepositoryDateValue, Date.current) end diff --git a/app/models/asset.rb b/app/models/asset.rb index 0c2601d82..6841a795a 100644 --- a/app/models/asset.rb +++ b/app/models/asset.rb @@ -3,11 +3,11 @@ class Asset < ApplicationRecord include SearchableModel include DatabaseHelper - include Encryptor include WopiUtil include ActiveStorageFileUtil include ActiveStorageConcerns include ActiveStorageHelper + include VersionedAttachments require 'tempfile' # Lock duration set to 30 minutes @@ -17,9 +17,9 @@ class Asset < ApplicationRecord enum view_mode: { thumbnail: 0, list: 1, inline: 2 } # ActiveStorage configuration - has_one_attached :file + has_one_versioned_attached :file + has_one_versioned_attached :preview_image has_one_attached :file_pdf_preview - has_one_attached :preview_image # Asset validation # This could cause some problems if you create empty asset and want to @@ -145,41 +145,67 @@ class Asset < ApplicationRecord file&.blob&.content_type end - def duplicate(new_name: nil) + def duplicate(new_name: nil, include_file_versions: false, created_by: nil) new_asset = dup file.filename = new_name if new_name + if created_by + new_asset.created_by = created_by + new_asset.last_modified_by = created_by + end + return unless new_asset.save - duplicate_file(new_asset) + duplicate_file(new_asset, new_name: new_name, include_file_versions: include_file_versions) + new_asset end - def duplicate_file(to_asset) + def duplicate_blob(blob, attach_method, metadata: nil) + new_blob = nil + + blob.open do |tmp_file| + new_blob = ActiveStorage::Blob.create_and_upload!( + io: tmp_file, + filename: blob.filename, + metadata: (metadata || blob.metadata) + ) + + attach_method.call(new_blob) + end + + new_blob + end + + def duplicate_file_versions(to_asset) + previous_files.map(&:blob).sort_by(&:created_at).each do |blob| + duplicate_blob(blob, to_asset.previous_files.method(:attach)) # .update_column(:created_at, blob.created_at) + end + end + + def duplicate_file(to_asset, new_name: nil, include_file_versions: false) return unless file.attached? raise ArgumentError, 'Destination asset should be persisted first!' unless to_asset.persisted? - file.blob.open do |tmp_file| - to_blob = ActiveStorage::Blob.create_and_upload!( - io: tmp_file, - filename: blob.filename, - metadata: blob.metadata - ) - to_asset.file.attach(to_blob) + metadata = file.blob.metadata.dup + + # set new name in metadata for OVE and MarvinJS files + if new_name && file.blob.metadata['asset_type'].in?(%w(gene_sequence marvinjs)) + new_metadata_name = File.basename(new_name, File.extname(new_name)) + metadata['name'] = new_metadata_name end - if preview_image.attached? - preview_image.blob.open do |tmp_preview_image| - to_blob = ActiveStorage::Blob.create_and_upload!( - io: tmp_preview_image, - filename: blob.filename, - metadata: blob.metadata - ) - to_asset.preview_image.attach(to_blob) - end + unless include_file_versions + metadata.delete('version') + metadata.delete('restored_from_version') + metadata['created_by_id'] = to_asset.created_by_id end + duplicate_file_versions(to_asset) if include_file_versions + duplicate_blob(blob, to_asset.method(:attach_file_version), metadata: metadata) + duplicate_blob(preview_image.blob, to_asset.method(:attach_preview_image_version)) if preview_image.attached? + to_asset.post_process_file end @@ -341,7 +367,7 @@ class Asset < ApplicationRecord end def update_contents(new_file) - file.attach(io: new_file, filename: file_name) + attach_file_version(io: new_file, filename: file_name) self.version = version.nil? ? 1 : version + 1 save end diff --git a/app/models/concerns/versioned_attachments.rb b/app/models/concerns/versioned_attachments.rb new file mode 100644 index 000000000..5ec8f501c --- /dev/null +++ b/app/models/concerns/versioned_attachments.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +module VersionedAttachments + extend ActiveSupport::Concern + + class_methods do + def has_one_versioned_attached(name) + has_one_attached name, dependent: :detach + has_many_attached "previous_#{name.to_s.pluralize}", dependent: :detach + + define_method :"attach_#{name}_version" do |*args, **options| + ActiveRecord::Base.transaction(requires_new: true) do + __send__(:"previous_#{name.to_s.pluralize}").attach(__send__(name).blob) if __send__(name).attached? + __send__(name).attach(*args, **options) + + new_blob = __send__(name).blob + new_blob.metadata['created_by_id'] ||= last_modified_by_id + + # set version of current latest file if previous versions exist + new_blob.save! and next unless __send__(:"previous_#{name.to_s.pluralize}").any? + + new_version = + (__send__(:"previous_#{name.to_s.pluralize}").last.blob.metadata['version'] || 1) + 1 + new_blob.metadata['version'] = new_version + new_blob.save! + end + end + + define_method :"restore_#{name}_version" do |version| + ActiveRecord::Base.transaction(requires_new: true) do + blob = __send__(:"previous_#{name.to_s.pluralize}").map(&:blob).find do |b| + (b.metadata['version'] || 1) == version + end + + blob.open do |tmp_file| + new_blob = ActiveStorage::Blob.create_and_upload!( + io: tmp_file, + filename: blob.filename, + metadata: blob.metadata.merge({ 'restored_from_version' => version, 'created_by_id' => last_modified_by_id }) + ) + + __send__(:"attach_#{name}_version", new_blob) + end + end + end + end + end + + module_function + + def enabled? + ApplicationSettings.instance.values['versioned_attachments_enabled'] + end + + def disabled_disclaimer + { + text: I18n.t('assets.file_versions_modal.disabled_disclaimer'), + button: I18n.t('assets.file_versions_modal.enable_button') + } + end +end diff --git a/app/models/linked_repository.rb b/app/models/linked_repository.rb index a6874ad91..5e1872924 100644 --- a/app/models/linked_repository.rb +++ b/app/models/linked_repository.rb @@ -24,9 +24,9 @@ class LinkedRepository < Repository 'repository_rows.created_at', 'users.full_name', 'repository_rows.updated_at', - 'last_modified_bies_repository_rows.full_name', + 'last_modified_by.full_name', 'repository_rows.archived_on', - 'archived_bies_repository_rows.full_name', + 'archived_by.full_name', 'repository_rows.external_id' ] end diff --git a/app/models/my_module.rb b/app/models/my_module.rb index 36b7578ff..5ded2c691 100644 --- a/app/models/my_module.rb +++ b/app/models/my_module.rb @@ -391,17 +391,17 @@ class MyModule < ApplicationRecord { data: data, headers: headers } end - def repository_docx_json(repository) - headers = [ - I18n.t('repositories.table.id'), - I18n.t('repositories.table.row_name'), - I18n.t('repositories.table.added_on'), - I18n.t('repositories.table.added_by') - ] + def repository_docx_json(repository, excluded_columns) + headers = Report.default_repository_columns.filter_map do |key, value| + value unless excluded_columns.include?(key.to_s.to_i) + end + custom_columns = [] return false unless repository repository.repository_columns.order(:id).each do |column| + next if excluded_columns.include?(column.id) + if column.data_type == 'RepositoryStockValue' if repository.has_stock_consumption? headers.push(I18n.t('repositories.table.row_consumption')) @@ -416,7 +416,7 @@ class MyModule < ApplicationRecord records = repository.assigned_rows(self) .select(:id, :name, :created_at, :created_by_id, :repository_id, :parent_id, :archived) - { headers: headers, rows: records, custom_columns: custom_columns } + { headers: headers, rows: records, custom_columns: custom_columns, excluded_columns: excluded_columns } end def deep_clone(current_user) @@ -565,6 +565,12 @@ class MyModule < ApplicationRecord yield + if status_changing_direction == :forward + my_module_status.my_module_status_consequences.each do |consequence| + consequence.before_forward_call(self) + end + end + if my_module_status.my_module_status_consequences.any?(&:runs_in_background?) MyModuleStatusConsequencesJob .perform_later(self, my_module_status.my_module_status_consequences.to_a, status_changing_direction) diff --git a/app/models/my_module_status_consequence.rb b/app/models/my_module_status_consequence.rb index b9b7b2f71..6abce4dc8 100644 --- a/app/models/my_module_status_consequence.rb +++ b/app/models/my_module_status_consequence.rb @@ -7,6 +7,8 @@ class MyModuleStatusConsequence < ApplicationRecord def backward(my_module); end + def before_forward_call(my_module); end + def runs_in_background? false end diff --git a/app/models/my_module_status_consequences/repository_snapshot.rb b/app/models/my_module_status_consequences/repository_snapshot.rb index 30d47ce73..e39f8efeb 100644 --- a/app/models/my_module_status_consequences/repository_snapshot.rb +++ b/app/models/my_module_status_consequences/repository_snapshot.rb @@ -6,9 +6,14 @@ module MyModuleStatusConsequences true end - def forward(my_module) + def before_forward_call(my_module) my_module.assigned_repositories.each do |repository| - repository_snapshot = ::RepositorySnapshot.create_preliminary!(repository, my_module) + ::RepositorySnapshot.create_preliminary!(repository, my_module) + end + end + + def forward(my_module) + my_module.repository_snapshots.where(status: :provisioning).find_each do |repository_snapshot| service = Repositories::SnapshotProvisioningService.call(repository_snapshot: repository_snapshot) unless service.succeed? diff --git a/app/models/protocol.rb b/app/models/protocol.rb index 46156dc67..e16f385b0 100644 --- a/app/models/protocol.rb +++ b/app/models/protocol.rb @@ -70,27 +70,10 @@ class Protocol < ApplicationRecord with_options if: :in_repository_published_version? do validates :parent, presence: true validate :parent_type_constraint - validate :versions_same_name_constraint end with_options if: :in_repository_draft? do # Only one draft can exist for each protocol validate :ensure_single_draft - validate :versions_same_name_constraint - end - with_options if: -> { in_repository? && !parent && !archived_changed?(from: false) } do |protocol| - # Active protocol must have unique name inside its team - protocol - .validates_uniqueness_of :name, case_sensitive: false, - scope: :team, - conditions: lambda { - where( - protocol_type: [ - Protocol.protocol_types[:in_repository_published_original], - Protocol.protocol_types[:in_repository_draft] - ], - parent_id: nil - ) - } end with_options if: -> { in_repository? && archived? && !previous_version } do |protocol| protocol.validates :archived_by, presence: true @@ -351,7 +334,9 @@ class Protocol < ApplicationRecord end # Deep-clone given array of assets - def self.deep_clone_assets(assets_to_clone) + # There is an issue with Delayed Job delayed methods, ruby 3, and keyword arguments (https://github.com/collectiveidea/delayed_job/issues/1134) + # so we're forced to use a normal argument with default value. + def self.deep_clone_assets(assets_to_clone, include_file_versions = false) ActiveRecord::Base.no_touching do assets_to_clone.each do |src_id, dest_id| src = Asset.find_by(id: src_id) @@ -360,12 +345,12 @@ class Protocol < ApplicationRecord next unless src.present? && dest.present? # Clone file - src.duplicate_file(dest) + src.duplicate_file(dest, include_file_versions: include_file_versions) end end end - def self.clone_contents(src, dest, current_user, clone_keywords, only_contents = false) + def self.clone_contents(src, dest, current_user, clone_keywords, only_contents: false, include_file_versions: false) dest.update(description: src.description, name: src.name) unless only_contents src.clone_tinymce_assets(dest, dest.team) @@ -382,7 +367,7 @@ class Protocol < ApplicationRecord # Copy steps src.steps.each do |step| - step.duplicate(dest, current_user, step_position: step.position) + step.duplicate(dest, current_user, step_position: step.position, include_file_versions: include_file_versions) end end @@ -615,7 +600,7 @@ class Protocol < ApplicationRecord return draft if draft.invalid? ActiveRecord::Base.no_touching do - draft = deep_clone(draft, current_user) + draft = deep_clone(draft, current_user, include_file_versions: true) end parent_protocol.user_assignments.each do |parent_user_assignment| @@ -760,7 +745,7 @@ class Protocol < ApplicationRecord end end - def deep_clone(clone, current_user) + def deep_clone(clone, current_user, include_file_versions: false) # Save cloned protocol first success = clone.save @@ -772,7 +757,7 @@ class Protocol < ApplicationRecord raise ActiveRecord::RecordNotSaved unless success - Protocol.clone_contents(self, clone, current_user, true, true) + Protocol.clone_contents(self, clone, current_user, true, only_contents: true, include_file_versions: include_file_versions) clone.reload clone @@ -794,12 +779,6 @@ class Protocol < ApplicationRecord end end - def versions_same_name_constraint - if parent.present? && !parent.name.eql?(name) - errors.add(:base, I18n.t('activerecord.errors.models.protocol.wrong_version_name')) - end - end - def version_number_constraint if Protocol.where(protocol_type: Protocol::REPOSITORY_TYPES) .where.not(id: id) diff --git a/app/models/report.rb b/app/models/report.rb index 0d1fbc97d..87d159e6a 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -43,6 +43,8 @@ class Report < ApplicationRecord DEFAULT_SETTINGS = { all_tasks: true, + exclude_task_metadata: false, + exclude_timestamps: false, task: { protocol: { description: true, @@ -62,8 +64,11 @@ class Report < ApplicationRecord result_comments: true, result_order: 'new', activities: true, - repositories: [] - } + repositories: [], + excluded_repository_columns: {} + }, + template: 'scinote_template', + docx_template: 'scinote_template' }.freeze def self.search( @@ -124,4 +129,13 @@ class Report < ApplicationRecord ReportActions::ReportContent.new(report, content, {}, current_user).save_with_content report end + + def self.default_repository_columns + { + '-1': I18n.t('repositories.table.id'), + '-2': I18n.t('repositories.table.row_name'), + '-3': I18n.t('repositories.table.added_on'), + '-4': I18n.t('repositories.table.added_by') + } + end end diff --git a/app/models/repository.rb b/app/models/repository.rb index 166dd6f89..d60d072c3 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -83,9 +83,9 @@ class Repository < RepositoryBase 'repository_rows.created_at', 'users.full_name', 'repository_rows.updated_at', - 'last_modified_bies_repository_rows.full_name', + 'last_modified_by.full_name', 'repository_rows.archived_on', - 'archived_bies_repository_rows.full_name' + 'archived_by.full_name' ] end diff --git a/app/models/repository_asset_value.rb b/app/models/repository_asset_value.rb index 5e9ba015f..9e4685aa6 100644 --- a/app/models/repository_asset_value.rb +++ b/app/models/repository_asset_value.rb @@ -60,9 +60,9 @@ class RepositoryAssetValue < ApplicationRecord def update_data!(new_data, user) if new_data.is_a?(String) # assume it's a signed_id_token - asset.file.attach(new_data) + asset.attach_file_version(new_data) elsif new_data[:file_data] - asset.file.attach(io: StringIO.new(Base64.decode64(new_data[:file_data])), filename: new_data[:file_name]) + asset.attach_file_version(io: StringIO.new(Base64.decode64(new_data[:file_data])), filename: new_data[:file_name]) end asset.file_pdf_preview.purge if asset.file_pdf_preview.attached? @@ -99,9 +99,9 @@ class RepositoryAssetValue < ApplicationRecord value.asset = Asset.create!(created_by: value.created_by, last_modified_by: value.created_by, team: team) if payload.is_a?(String) # assume it's a signed_id_token - value.asset.file.attach(payload) + value.asset.attach_file_version(payload) elsif payload[:file_data] - value.asset.file.attach(io: StringIO.new(Base64.decode64(payload[:file_data])), filename: payload[:file_name]) + value.asset.attach_file_version(io: StringIO.new(Base64.decode64(payload[:file_data])), filename: payload[:file_name]) end value.asset.post_process_file diff --git a/app/models/repository_cell.rb b/app/models/repository_cell.rb index 9afda532d..73a9f3bd8 100644 --- a/app/models/repository_cell.rb +++ b/app/models/repository_cell.rb @@ -77,9 +77,9 @@ class RepositoryCell < ApplicationRecord '"repository_date_time_values"."id" = "repository_cells"."value_id" AND ' \ '"repository_cells"."value_type" = \'RepositoryDateTimeValueBase\' ' \ 'AND repository_reminder_columns.metadata ->> \'reminder_value\' <> \'\' AND ' \ - '(repository_date_time_values.data - NOW()) <= ' \ - '(repository_reminder_columns.metadata ->> \'reminder_value\')::int * ' \ - '(repository_reminder_columns.metadata ->> \'reminder_unit\')::int * interval \'1 sec\'' + 'repository_date_time_values.data <= ' \ + '(NOW() AT TIME ZONE \'UTC\') + (repository_reminder_columns.metadata ->> \'reminder_value\')::int * ' \ + '(repository_reminder_columns.metadata ->> \'reminder_unit\')::int * \'1 SECOND\'::interval' ).joins( 'LEFT OUTER JOIN "hidden_repository_cell_reminders" ON ' \ '"repository_cells"."id" = "hidden_repository_cell_reminders"."repository_cell_id" AND ' \ diff --git a/app/models/soft_locked_repository.rb b/app/models/soft_locked_repository.rb index 93a51178d..43e44c73d 100644 --- a/app/models/soft_locked_repository.rb +++ b/app/models/soft_locked_repository.rb @@ -8,4 +8,12 @@ class SoftLockedRepository < Repository def shareable_write? false end + + def unlocked? + @unlocked == true + end + + def unlock! + @unlocked = true + end end diff --git a/app/models/step.rb b/app/models/step.rb index 4652e0c47..395dfbc55 100644 --- a/app/models/step.rb +++ b/app/models/step.rb @@ -117,7 +117,7 @@ class Step < ApplicationRecord step_texts.order(created_at: :asc).first end - def duplicate(protocol, user, step_position: nil, step_name: nil) + def duplicate(protocol, user, step_position: nil, step_name: nil, include_file_versions: false) ActiveRecord::Base.transaction do assets_to_clone = [] @@ -142,7 +142,7 @@ class Step < ApplicationRecord # "Shallow" Copy assets assets.each do |asset| new_asset = asset.dup - new_asset.save! + new_asset.update!(created_by: user, last_modified_by: user) new_step.assets << new_asset assets_to_clone << [asset.id, new_asset.id] end @@ -153,7 +153,7 @@ class Step < ApplicationRecord end # Call clone helper - Protocol.delay(queue: :assets).deep_clone_assets(assets_to_clone) + Protocol.delay(queue: :assets).deep_clone_assets(assets_to_clone, include_file_versions) new_step end diff --git a/app/models/team.rb b/app/models/team.rb index 311fc27bb..ac4da78fc 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -221,6 +221,7 @@ class Team < ApplicationRecord def create_default_label_templates ZebraLabelTemplate.default.update(team: self, default: true) + ZebraLabelTemplate.default_203dpi.update(team: self, default: false) FluicsLabelTemplate.default.update(team: self, default: true) end end diff --git a/app/models/zebra_label_template.rb b/app/models/zebra_label_template.rb index a3174b05a..619271a2a 100644 --- a/app/models/zebra_label_template.rb +++ b/app/models/zebra_label_template.rb @@ -11,4 +11,15 @@ class ZebraLabelTemplate < LabelTemplate density: 12 ) end + + def self.default_203dpi + ZebraLabelTemplate.new( + name: I18n.t('label_templates.default_zebra_name_203dpi'), + width_mm: 25.4, + height_mm: 12.7, + content: Extends::DEFAULT_LABEL_TEMPLATE_203DPI[:zpl], + unit: 0, + density: 12 + ) + end end diff --git a/app/permissions/asset.rb b/app/permissions/asset.rb index bd6acb444..8f576ca09 100644 --- a/app/permissions/asset.rb +++ b/app/permissions/asset.rb @@ -27,11 +27,16 @@ Canaid::Permissions.register_for(Asset) do if object.repository_column.repository.is_a?(RepositorySnapshot) false else + object.repository_row.active? && can_manage_repository_assets?(user, object.repository_column.repository) end end end + can :restore_asset do |user, asset| + VersionedAttachments.enabled? && can_manage_asset?(user, asset) + end + can :open_asset_locally do |_user, asset| ENV['ASSET_SYNC_URL'].present? end diff --git a/app/permissions/repository.rb b/app/permissions/repository.rb index 3a7283793..738358423 100644 --- a/app/permissions/repository.rb +++ b/app/permissions/repository.rb @@ -25,10 +25,12 @@ Canaid::Permissions.register_for(Repository) do create_repository_rows manage_repository_rows delete_repository_rows - create_repository_columns) + create_repository_columns + manage_repository_columns) .each do |perm| can perm do |_, repository| - repository.active? && repository.repository_snapshots.provisioning.none? + repository.active? && repository.repository_snapshots.provisioning.none? && + (!repository.is_a?(SoftLockedRepository) || repository.unlocked?) end end @@ -105,7 +107,7 @@ Canaid::Permissions.register_for(Repository) do end can :manage_repository_columns do |user, repository| - repository.repository_snapshots.provisioning.none? && can_create_repository_columns?(user, repository) + repository.permission_granted?(user, RepositoryPermissions::COLUMNS_UPDATE) end # repository: create/update/delete filters @@ -122,6 +124,10 @@ Canaid::Permissions.register_for(RepositoryColumn) do # repository: update/delete field # Tested in scope of RepositoryPermissions spec can :manage_repository_column do |user, repository_column| - repository_column.repository.repository_snapshots.provisioning.none? && can_create_repository_columns?(user, repository_column.repository) + repository_column.repository.repository_snapshots.provisioning.none? && repository_column.repository.permission_granted?(user, RepositoryPermissions::COLUMNS_UPDATE) + end + + can :delete_repository_column do |user, repository_column| + repository_column.repository.repository_snapshots.provisioning.none? && repository_column.repository.permission_granted?(user, RepositoryPermissions::COLUMNS_DELETE) end end diff --git a/app/serializers/active_storage/blob_serializer.rb b/app/serializers/active_storage/blob_serializer.rb new file mode 100644 index 000000000..ff3a1ed8d --- /dev/null +++ b/app/serializers/active_storage/blob_serializer.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module ActiveStorage + class BlobSerializer < ActiveModel::Serializer + include Rails.application.routes.url_helpers + + attributes :filename, :extension, :basename, :url, :created_at, :byte_size, :version, :restored_from_version, :created_by, :download_url + + def basename + object.filename.base + end + + def extension + object.filename.extension_without_delimiter + end + + def download_url + rails_blob_url(object, disposition: 'attachment') + end + + def version + object.metadata['version'] || 1 + end + + def restored_from_version + object.metadata['restored_from_version'] + end + + def created_at + return object.created_at unless @instance_options[:user] + + object.created_at.strftime("#{@instance_options[:user].date_format}, %H:%M") + end + + def created_by + User.select(:id, :full_name, :email).find_by( + id: object.metadata['created_by_id'] || object.attachments.first&.record&.created_by_id + ) + end + end +end diff --git a/app/serializers/asset_serializer.rb b/app/serializers/asset_serializer.rb index 9817f448a..d3408bf93 100644 --- a/app/serializers/asset_serializer.rb +++ b/app/serializers/asset_serializer.rb @@ -138,7 +138,8 @@ class AssetSerializer < ActiveModel::Serializer load_asset: load_asset_path(object), asset_file: asset_file_url_path(object), marvin_js: marvin_js_asset_path(object), - marvin_js_icon: image_path('icon_small/marvinjs.svg') + marvin_js_icon: image_path('icon_small/marvinjs.svg'), + versions: (asset_versions_path(object) if attached) } user = scope[:user] || @instance_options[:user] if can_manage_asset?(user, object) @@ -155,6 +156,7 @@ class AssetSerializer < ActiveModel::Serializer ) end + urls[:restore_version] = asset_restore_version_path(object) if can_restore_asset?(user, object) urls[:open_vector_editor_edit] = edit_gene_sequence_asset_path(object.id) if can_manage_asset?(user, object) if can_manage_asset?(user, object) && can_open_asset_locally?(user, object) diff --git a/app/serializers/notification_serializer.rb b/app/serializers/notification_serializer.rb index ebb8ef838..75244c379 100644 --- a/app/serializers/notification_serializer.rb +++ b/app/serializers/notification_serializer.rb @@ -4,7 +4,7 @@ class NotificationSerializer < ActiveModel::Serializer include Rails.application.routes.url_helpers include BreadcrumbsHelper - attributes :id, :title, :message, :created_at, :read_at, :type, :breadcrumbs, :checked, :today + attributes :id, :title, :message, :created_at, :read_at, :type, :breadcrumbs, :checked, :today, :toggle_read_url def title object.to_notification.title @@ -31,4 +31,7 @@ class NotificationSerializer < ActiveModel::Serializer object.read_at.present? end + def toggle_read_url + toggle_read_user_notification_path(object) + end end diff --git a/app/services/label_printers/fluics/sync_service.rb b/app/services/label_printers/fluics/sync_service.rb index 987a42c33..3510d3c48 100644 --- a/app/services/label_printers/fluics/sync_service.rb +++ b/app/services/label_printers/fluics/sync_service.rb @@ -9,6 +9,7 @@ module LabelPrinters end def sync_templates! + NewRelic::Agent.ignore_transaction LabelPrinter.fluics.each do |printer| api_client = ApiClient.new(printer.fluics_api_key) templates = api_client.list_templates diff --git a/app/services/marvin_js_service.rb b/app/services/marvin_js_service.rb index 8713763ef..8541ed490 100644 --- a/app/services/marvin_js_service.rb +++ b/app/services/marvin_js_service.rb @@ -22,7 +22,7 @@ class MarvinJsService asset = Asset.new(created_by: current_user, last_modified_by: current_user, team_id: current_team.id) - attach_file(asset.file, file, params) + attach_file_version(asset, file, params) asset.save! asset.post_process_file connect_asset(asset, params, current_user) @@ -40,21 +40,14 @@ class MarvinJsService file = generate_image(params) asset.update(last_modified_by: current_user) if asset.is_a?(Asset) - attach_file(attachment, file, params) - asset.post_process_file if asset.instance_of?(Asset) - asset - end - def update_file_name(new_name, asset_id, current_user, current_team) - asset = current_team.assets.find(asset_id) - prepared_name = prepare_name(new_name) - - ActiveRecord::Base.transaction do - asset.last_modified_by = current_user - asset.rename_file(prepared_name) - asset.save! + if params[:object_type] == 'TinyMceAsset' + attach_file(attachment, file, params) + else + attach_file_version(asset, file, params) end + asset.post_process_file if asset.instance_of?(Asset) asset end @@ -90,6 +83,19 @@ class MarvinJsService ) end + def attach_file_version(asset, file, params) + asset.attach_file_version( + io: file, + filename: "#{prepare_name(params[:name])}.jpg", + content_type: 'image/jpeg', + metadata: { + name: prepare_name(params[:name]), + description: params[:description], + asset_type: 'marvinjs' + } + ) + end + def prepare_name(sketch_name) if !sketch_name.blank? sketch_name diff --git a/app/services/pdf_preview_service.rb b/app/services/pdf_preview_service.rb new file mode 100644 index 000000000..cb12e2159 --- /dev/null +++ b/app/services/pdf_preview_service.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +class PdfPreviewService + def initialize(blob, attached) + @blob = blob + @attached = attached + end + + def generate! + preview_file = nil + + @blob.open(tmpdir: tempdir) do |input| + work_dir = File.dirname(input.path) + preview_filename = "#{File.basename(input.path, '.*')}.pdf" + preview_file = File.join(work_dir, preview_filename) + Rails.logger.info "Starting preparing document preview for file #{@blob.filename.sanitized}..." + + ActiveRecord::Base.transaction do + success = system( + 'timeout', + Constants::PREVIEW_TIMEOUT_SECONDS.to_s, + libreoffice_path, + '--headless', + '--invisible', + '--convert-to', + 'pdf', '--outdir', + work_dir, input.path + ) + unless success && File.file?(preview_file) + raise StandardError, "There was an error generating PDF preview, blob id: #{@blob.id}" + end + + ActiveRecord::Base.no_touching do + @attached.attach(io: File.open(preview_file), filename: "#{@blob.filename.base}.pdf") + end + Rails.logger.info("Finished preparing PDF preview for file #{@blob.filename.sanitized}.") + end + end + ensure + File.delete(preview_file) if File.file?(preview_file) + end + + private + + def tempdir + Rails.root.join('tmp') + end + + def libreoffice_path + ENV['LIBREOFFICE_PATH'] || 'soffice' + end +end diff --git a/app/services/report_actions/save_pdf_to_inventory_item.rb b/app/services/report_actions/save_pdf_to_inventory_item.rb index 88b1e1a90..01866820f 100644 --- a/app/services/report_actions/save_pdf_to_inventory_item.rb +++ b/app/services/report_actions/save_pdf_to_inventory_item.rb @@ -44,7 +44,7 @@ module ReportActions def create_new_asset asset = Asset.create(created_by: @user, last_modified_by: @user, team: @team) - asset.file.attach(@report.pdf_file.blob) + asset.attach_file_version(@report.pdf_file.blob) asset end diff --git a/app/services/reports/docx.rb b/app/services/reports/docx.rb index 7a0251371..72e6ce339 100644 --- a/app/services/reports/docx.rb +++ b/app/services/reports/docx.rb @@ -2,6 +2,10 @@ # rubocop:disable Style/ClassAndModuleChildren +Dir[Rails.root.join('app/views/reports/docx_templates/**/docx.rb')].each do |file| + require file +end + class Reports::Docx include ActionView::Helpers::TextHelper include ActionView::Helpers::UrlHelper @@ -26,15 +30,23 @@ class Reports::Docx @link_style = {} @color = {} @scinote_url = options[:scinote_url][0..-2] + @template = @settings[:docx_template].presence || 'scinote_template' + + extend "#{@template.camelize}Docx".constantize end def draw initial_document_load - @report.root_elements.each do |subject| - public_send("draw_#{subject.type_of}", subject) - end + prepare_docx + @docx end + + private + + def get_template_value(name) + @report.report_template_values.find_by(name: name)&.value + end end # rubocop:enable Style/ClassAndModuleChildren diff --git a/app/services/reports/docx/draw_experiment.rb b/app/services/reports/docx/draw_experiment.rb index e02d018a7..ebe3ae7ca 100644 --- a/app/services/reports/docx/draw_experiment.rb +++ b/app/services/reports/docx/draw_experiment.rb @@ -4,6 +4,7 @@ module Reports::Docx::DrawExperiment def draw_experiment(subject) color = @color link_style = @link_style + settings = @settings scinote_url = @scinote_url experiment = subject.experiment return unless can_read_experiment?(@user, experiment) @@ -14,12 +15,15 @@ module Reports::Docx::DrawExperiment link_style end - @docx.p do - text I18n.t('projects.reports.elements.experiment.user_time', - code: experiment.code, timestamp: I18n.l(experiment.created_at, format: :full)), color: color[:gray] - if experiment.archived? - text ' | ' - text I18n.t('search.index.archived'), color: color[:gray] + if !settings['exclude_timestamps'] || experiment.archived? + @docx.p do + unless settings['exclude_timestamps'] + text I18n.t('projects.reports.elements.experiment.user_time', + code: experiment.code, + timestamp: I18n.l(experiment.created_at, format: :full)), color: color[:gray] + text ' | ' if experiment.archived? + end + text I18n.t('search.index.archived'), color: color[:gray] if experiment.archived? end end html = custom_auto_link(experiment.description, team: @report_team) diff --git a/app/services/reports/docx/draw_my_module.rb b/app/services/reports/docx/draw_my_module.rb index ddfefd7df..bd859baab 100644 --- a/app/services/reports/docx/draw_my_module.rb +++ b/app/services/reports/docx/draw_my_module.rb @@ -1,9 +1,10 @@ # frozen_string_literal: true module Reports::Docx::DrawMyModule - def draw_my_module(subject) + def draw_my_module(subject, without_results: false, without_repositories: false) color = @color link_style = @link_style + settings = @settings scinote_url = @scinote_url my_module = subject.my_module tags = my_module.tags.order(:id) @@ -15,45 +16,50 @@ module Reports::Docx::DrawMyModule link_style end - @docx.p do - text I18n.t('projects.reports.elements.module.user_time', code: my_module.code, - timestamp: I18n.l(my_module.created_at, format: :full)), color: color[:gray] - if my_module.archived? - text ' | ' - text I18n.t('search.index.archived'), color: color[:gray] - end - end - - if my_module.started_on.present? + if my_module.archived? || !settings['exclude_timestamps'] @docx.p do - text I18n.t('projects.reports.elements.module.started_on', - started_on: I18n.l(my_module.started_on, format: :full)) + unless settings['exclude_timestamps'] + text I18n.t('projects.reports.elements.module.user_time', code: my_module.code, + timestamp: I18n.l(my_module.created_at, format: :full)), color: color[:gray] + text ' | ' if my_module.archived? + end + + text I18n.t('search.index.archived'), color: color[:gray] if my_module.archived? end end - if my_module.due_date.present? + unless settings['exclude_task_metadata'] + if my_module.started_on.present? + @docx.p do + text I18n.t('projects.reports.elements.module.started_on', + started_on: I18n.l(my_module.started_on, format: :full)) + end + end + + if my_module.due_date.present? + @docx.p do + text I18n.t('projects.reports.elements.module.due_date', + due_date: I18n.l(my_module.due_date, format: :full)) + end + end + + status = my_module.my_module_status @docx.p do - text I18n.t('projects.reports.elements.module.due_date', - due_date: I18n.l(my_module.due_date, format: :full)) + text I18n.t('projects.reports.elements.module.status') + text ' ' + text "[#{status.name}]", color: (status.light_color? ? '000000' : status.color.delete('#')) + if my_module.completed? + text " #{I18n.t('my_modules.states.completed')} #{I18n.l(my_module.completed_on, format: :full)}" + end end - end - status = my_module.my_module_status - @docx.p do - text I18n.t('projects.reports.elements.module.status') - text ' ' - text "[#{status.name}]", color: (status.light_color? ? '000000' : status.color.delete('#')) - if my_module.completed? - text " #{I18n.t('my_modules.states.completed')} #{I18n.l(my_module.completed_on, format: :full)}" - end - end - - if tags.present? - @docx.p do - text I18n.t('projects.reports.elements.module.tags_header') - tags.each do |tag| - text ' ' - text "[#{tag.name}]", color: tag.color.delete('#') + if tags.present? + @docx.p do + text I18n.t('projects.reports.elements.module.tags_header') + tags.each do |tag| + text ' ' + text "[#{tag.name}]", color: tag.color.delete('#') + end end end end @@ -69,31 +75,16 @@ module Reports::Docx::DrawMyModule filter_steps_for_report(my_module.protocol.steps, @settings).order(:position).each do |step| draw_step(step) end + @docx.p - if my_module.results.any? && (%w(file_results table_results text_results).any? { |k| @settings.dig('task', k) }) - @docx.h4 I18n.t('Results') - order_results_for_report(my_module.results, @settings.dig('task', 'result_order')).each do |result| - @docx.p do - text result.name.presence || I18n.t('projects.reports.unnamed'), italic: true - text " #{I18n.t('search.index.archived')} ", bold: true if result.archived? - text I18n.t('projects.reports.elements.result.user_time', - timestamp: I18n.l(result.created_at, format: :full), - user: result.user.full_name), color: color[:gray] - end - draw_result_asset(result, @settings) if @settings.dig('task', 'file_results') - result.result_orderable_elements.each do |element| - if @settings.dig('task', 'table_results') && element.orderable_type == 'ResultTable' - draw_result_table(element) - elsif @settings.dig('task', 'text_results') && element.orderable_type == 'ResultText' - draw_result_text(element) - end - end - draw_result_comments(result) if @settings.dig('task', 'result_comments') - end + unless without_results + draw_results(my_module) + @docx.p end - @docx.p subject.children.active.each do |child| + next if without_repositories && child.type_of == 'my_module_repository' + public_send("draw_#{child.type_of}", child) end diff --git a/app/services/reports/docx/draw_my_module_protocol.rb b/app/services/reports/docx/draw_my_module_protocol.rb index 2096569ff..0f3b56dbe 100644 --- a/app/services/reports/docx/draw_my_module_protocol.rb +++ b/app/services/reports/docx/draw_my_module_protocol.rb @@ -12,13 +12,14 @@ module Reports::Docx::DrawMyModuleProtocol end if @settings.dig('task', 'protocol', 'description') && protocol.description.present? - @docx.p I18n.t('projects.reports.elements.module.protocol.user_time', code: protocol.original_code, - timestamp: I18n.l(protocol.created_at, format: :full)), color: @color[:gray] + unless @settings['exclude_timestamps'] + @docx.p I18n.t('projects.reports.elements.module.protocol.user_time', code: protocol.original_code, + timestamp: I18n.l(protocol.created_at, format: :full)), color: @color[:gray] + end html = custom_auto_link(protocol.description, team: @report_team) Reports::HtmlToWordConverter.new(@docx, { scinote_url: @scinote_url, link_style: @link_style }).html_to_word_converter(html) @docx.p - @docx.p end end end diff --git a/app/services/reports/docx/draw_my_module_repository.rb b/app/services/reports/docx/draw_my_module_repository.rb index b552211d1..11f1ed0c8 100644 --- a/app/services/reports/docx/draw_my_module_repository.rb +++ b/app/services/reports/docx/draw_my_module_repository.rb @@ -5,11 +5,12 @@ module Reports::Docx::DrawMyModuleRepository my_module = subject.my_module repository = subject.repository repository = assigned_repository_or_snapshot(my_module, repository) + excluded_repository_columns = @settings.dig(:task, :excluded_repository_columns, repository.id.to_s) || {} return unless repository && can_read_experiment?(@user, my_module.experiment) && (repository.is_a?(RepositorySnapshot) || can_read_repository?(@user, repository)) - repository_data = my_module.repository_docx_json(repository) + repository_data = my_module.repository_docx_json(repository, excluded_repository_columns) return false unless repository_data[:rows].any? && can_read_repository?(@user, repository) @@ -19,7 +20,12 @@ module Reports::Docx::DrawMyModuleRepository @docx.p I18n.t('projects.reports.elements.module_repository.name', repository: repository.name, my_module: my_module.name), bold: true, size: Constants::REPORT_DOCX_STEP_ELEMENTS_TITLE_SIZE - @docx.table table, border_size: Constants::REPORT_DOCX_TABLE_BORDER_SIZE + + if table.present? + @docx.table table, border_size: Constants::REPORT_DOCX_TABLE_BORDER_SIZE + else + @docx.p I18n.t('projects.reports.elements.module_repository.no_columns'), italic: true + end @docx.p @docx.p diff --git a/app/services/reports/docx/draw_project_header.rb b/app/services/reports/docx/draw_project_header.rb index defa8e36e..c0bbffeef 100644 --- a/app/services/reports/docx/draw_project_header.rb +++ b/app/services/reports/docx/draw_project_header.rb @@ -15,10 +15,12 @@ module Reports::Docx::DrawProjectHeader link_style end - @docx.p do - text I18n.t('projects.reports.elements.project_header.user_time', code: project.code, - timestamp: I18n.l(project.created_at, format: :full)), color: color[:gray] - br + unless @settings['exclude_timestamps'] + @docx.p do + text I18n.t('projects.reports.elements.project_header.user_time', code: project.code, + timestamp: I18n.l(project.created_at, format: :full)), color: color[:gray] + br + end end end end diff --git a/app/services/reports/docx/draw_result_asset.rb b/app/services/reports/docx/draw_result_asset.rb index 030de2591..fc690977f 100644 --- a/app/services/reports/docx/draw_result_asset.rb +++ b/app/services/reports/docx/draw_result_asset.rb @@ -25,11 +25,9 @@ module Reports::Docx::DrawResultAsset end text " #{I18n.t('search.index.archived')} ", bold: true if result.archived? text ' ' + I18n.t('projects.reports.elements.result_asset.file_name', file: asset.file_name) - text ' ' + I18n.t('projects.reports.elements.result_asset.user_time', - user: result.user.full_name, timestamp: I18n.l(timestamp, format: :full)), color: color[:gray] - - if settings.dig(:task, :file_results_previews) && ActiveStorageFileUtil.previewable_document?(asset&.file&.blob) - text " #{I18n.t('projects.reports.elements.result_asset.full_preview_attached')}", color: color[:gray] + unless settings['exclude_timestamps'] + text ' ' + I18n.t('projects.reports.elements.result_asset.user_time', + user: result.user.full_name, timestamp: I18n.l(timestamp, format: :full)), color: color[:gray] end end diff --git a/app/services/reports/docx/draw_result_comments.rb b/app/services/reports/docx/draw_result_comments.rb index 3fc68ef1f..ebec9ad13 100644 --- a/app/services/reports/docx/draw_result_comments.rb +++ b/app/services/reports/docx/draw_result_comments.rb @@ -8,8 +8,10 @@ module Reports::Docx::DrawResultComments @docx.p @docx.p I18n.t('projects.reports.elements.result_comments.name', result: result.name), bold: true, size: Constants::REPORT_DOCX_STEP_ELEMENTS_TITLE_SIZE - comments.each do |comment| + comments.find_each.with_index do |comment, index| comment_ts = comment.created_at + + @docx.p unless index.zero? @docx.p I18n.t('projects.reports.elements.result_comments.comment_prefix', user: comment.user.full_name, date: I18n.l(comment_ts, format: :full_date), @@ -17,7 +19,6 @@ module Reports::Docx::DrawResultComments html = custom_auto_link(comment.message, team: @report_team) Reports::HtmlToWordConverter.new(@docx, { scinote_url: @scinote_url, link_style: @link_style }).html_to_word_converter(html) - @docx.p end end end diff --git a/app/services/reports/docx/draw_result_table.rb b/app/services/reports/docx/draw_result_table.rb index 661d41a46..f919cc8a4 100644 --- a/app/services/reports/docx/draw_result_table.rb +++ b/app/services/reports/docx/draw_result_table.rb @@ -5,6 +5,7 @@ module Reports::Docx::DrawResultTable result = element.result table = element.orderable.table timestamp = table.created_at + settings = @settings color = @color obj = self table_data = JSON.parse(table.contents_utf_8)['data'] @@ -39,9 +40,11 @@ module Reports::Docx::DrawResultTable end @docx.p do text I18n.t 'projects.reports.elements.result_table.table_name', name: table.name - text ' ' - text I18n.t('projects.reports.elements.result_table.user_time', - timestamp: I18n.l(timestamp, format: :full), user: result.user.full_name), color: color[:gray] + unless settings['exclude_timestamps'] + text ' ' + text I18n.t('projects.reports.elements.result_table.user_time', + timestamp: I18n.l(timestamp, format: :full), user: result.user.full_name), color: color[:gray] + end end end end diff --git a/app/services/reports/docx/draw_result_text.rb b/app/services/reports/docx/draw_result_text.rb index 2ab606a7e..baf9bc9f3 100644 --- a/app/services/reports/docx/draw_result_text.rb +++ b/app/services/reports/docx/draw_result_text.rb @@ -4,12 +4,18 @@ module Reports::Docx::DrawResultText def draw_result_text(element) result_text = element.orderable timestamp = element.created_at + settings = @settings color = @color - @docx.p do - text result_text.name.presence || '', italic: true - text ' ' - text I18n.t('projects.reports.elements.result_text.user_time', - timestamp: I18n.l(timestamp, format: :full)), color: color[:gray] + if result_text.name.present? || !settings['exclude_timestamps'] + @docx.p do + text result_text.name.to_s, italic: true + text ' ' if result_text.name.present? + + unless settings['exclude_timestamps'] + text I18n.t('projects.reports.elements.result_text.user_time', + timestamp: I18n.l(timestamp, format: :full)), color: color[:gray] + end + end end html = custom_auto_link(result_text.text, team: @report_team) Reports::HtmlToWordConverter.new(@docx, { scinote_url: @scinote_url, diff --git a/app/services/reports/docx/draw_results.rb b/app/services/reports/docx/draw_results.rb new file mode 100644 index 000000000..a708c262e --- /dev/null +++ b/app/services/reports/docx/draw_results.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Reports::Docx::DrawResults + def draw_results(my_module, with_my_module_name: false) + color = @color + settings = @settings + scinote_url = @scinote_url + link_style = @link_style + return unless can_read_my_module?(@user, my_module) + + if my_module.results.any? && (%w(file_results table_results text_results).any? { |k| @settings.dig('task', k) }) + if with_my_module_name + @docx.h3 do + link my_module.name, + scinote_url + Rails.application.routes.url_helpers.protocols_my_module_path(my_module), + link_style + end + end + @docx.h4 I18n.t('Results') + order_results_for_report(my_module.results, @settings.dig('task', 'result_order')).each do |result| + @docx.p do + text result.name.presence || I18n.t('projects.reports.unnamed'), italic: true + text " #{I18n.t('search.index.archived')} ", bold: true if result.archived? + unless settings['exclude_timestamps'] + text I18n.t('projects.reports.elements.result.user_time', + timestamp: I18n.l(result.created_at, format: :full), + user: result.user.full_name), color: color[:gray] + end + end + draw_result_asset(result, @settings) if @settings.dig('task', 'file_results') + result.result_orderable_elements.each do |element| + if @settings.dig('task', 'table_results') && element.orderable_type == 'ResultTable' + draw_result_table(element) + elsif @settings.dig('task', 'text_results') && element.orderable_type == 'ResultText' + draw_result_text(element) + end + end + draw_result_comments(result) if @settings.dig('task', 'result_comments') + end + end + end +end diff --git a/app/services/reports/docx/draw_step.rb b/app/services/reports/docx/draw_step.rb index c6cc90660..579a24c4e 100644 --- a/app/services/reports/docx/draw_step.rb +++ b/app/services/reports/docx/draw_step.rb @@ -6,22 +6,30 @@ module Reports::Docx::DrawStep step_type_str = step.completed ? 'completed' : 'uncompleted' user = (step.completed? && step.last_modified_by) || step.user timestamp = step.completed ? step.completed_on : step.created_at + settings = @settings @docx.p - @docx.h5( + @docx.h4( "#{I18n.t('projects.reports.elements.step.step_pos', pos: step.position_plus_one)} #{step.name}" ) - @docx.p do - if step.completed - text I18n.t('protocols.steps.completed'), color: color[:green], bold: true - else - text I18n.t('protocols.steps.uncompleted'), color: color[:gray], bold: true + + unless settings['exclude_task_metadata'] || settings['exclude_timestamps'] + @docx.p do + unless settings['exclude_task_metadata'] + if step.completed + text I18n.t('protocols.steps.completed'), color: color[:green], bold: true + else + text I18n.t('protocols.steps.uncompleted'), color: color[:gray], bold: true + end + end + unless settings['exclude_timestamps'] + text ' | ' unless settings['exclude_task_metadata'] + text I18n.t( + "projects.reports.elements.step.#{step_type_str}.user_time", + user: user.full_name, + timestamp: I18n.l(timestamp, format: :full) + ), color: color[:gray] + end end - text ' | ' - text I18n.t( - "projects.reports.elements.step.#{step_type_str}.user_time", - user: user.full_name, - timestamp: I18n.l(timestamp, format: :full) - ), color: color[:gray] end step.step_orderable_elements.order(:position).each do |element| @@ -41,9 +49,6 @@ module Reports::Docx::DrawStep end draw_step_comments(step) if @settings.dig('task', 'protocol', 'step_comments') - - @docx.p - @docx.p end def handle_step_table(table) diff --git a/app/services/reports/docx/draw_step_asset.rb b/app/services/reports/docx/draw_step_asset.rb index 8d83ca221..5b6569919 100644 --- a/app/services/reports/docx/draw_step_asset.rb +++ b/app/services/reports/docx/draw_step_asset.rb @@ -5,6 +5,7 @@ module Reports::Docx::DrawStepAsset timestamp = asset.created_at asset_url = Rails.application.routes.url_helpers.asset_download_url(asset) color = @color + settings = @settings @docx.p begin @@ -22,9 +23,11 @@ module Reports::Docx::DrawStepAsset link I18n.t('projects.reports.elements.download'), asset_url do italic true end - text ' ' - text I18n.t('projects.reports.elements.step_asset.user_time', - timestamp: I18n.l(timestamp, format: :full)), color: color[:gray] + unless settings['exclude_timestamps'] + text ' ' + text I18n.t('projects.reports.elements.step_asset.user_time', + timestamp: I18n.l(timestamp, format: :full)), color: color[:gray] + end end end end diff --git a/app/services/reports/docx/draw_step_checklist.rb b/app/services/reports/docx/draw_step_checklist.rb index e2662e60e..5c510c9f9 100644 --- a/app/services/reports/docx/draw_step_checklist.rb +++ b/app/services/reports/docx/draw_step_checklist.rb @@ -4,6 +4,7 @@ module Reports::Docx::DrawStepChecklist def draw_step_checklist(checklist) team = @report_team user = @user + settings = @settings items = checklist.checklist_items timestamp = checklist.created_at @@ -15,9 +16,11 @@ module Reports::Docx::DrawStepChecklist team, I18n.t('projects.reports.elements.step_checklist.checklist_name', name: checklist.name) ).text, italic: true - text ' ' - text I18n.t('projects.reports.elements.step_checklist.user_time', - timestamp: I18n.l(timestamp, format: :full)), color: color[:gray] + unless settings['exclude_timestamps'] + text ' ' + text I18n.t('projects.reports.elements.step_checklist.user_time', + timestamp: I18n.l(timestamp, format: :full)), color: color[:gray] + end end if items.any? @docx.ul do diff --git a/app/services/reports/docx/draw_step_comments.rb b/app/services/reports/docx/draw_step_comments.rb index 87d0abc5f..f9733eaeb 100644 --- a/app/services/reports/docx/draw_step_comments.rb +++ b/app/services/reports/docx/draw_step_comments.rb @@ -8,8 +8,10 @@ module Reports::Docx::DrawStepComments @docx.p @docx.p I18n.t('projects.reports.elements.step_comments.name', step: step.name), bold: true, size: Constants::REPORT_DOCX_STEP_ELEMENTS_TITLE_SIZE - comments.each do |comment| + comments.find_each.with_index do |comment, index| comment_ts = comment.created_at + + @docx.p unless index.zero? @docx.p I18n.t('projects.reports.elements.step_comments.comment_prefix', user: comment.user.full_name, date: I18n.l(comment_ts, format: :full_date), @@ -17,7 +19,6 @@ module Reports::Docx::DrawStepComments html = custom_auto_link(comment.message, team: @report_team) Reports::HtmlToWordConverter.new(@docx, { scinote_url: @scinote_url, link_style: @link_style }).html_to_word_converter(html) - @docx.p end end end diff --git a/app/services/reports/docx/draw_step_table.rb b/app/services/reports/docx/draw_step_table.rb index a0826741a..3565c200e 100644 --- a/app/services/reports/docx/draw_step_table.rb +++ b/app/services/reports/docx/draw_step_table.rb @@ -4,6 +4,7 @@ module Reports::Docx::DrawStepTable def draw_step_table(table, table_type) color = @color timestamp = table.created_at + settings = @settings obj = self table_data = JSON.parse(table.contents_utf_8)['data'] table_data = obj.add_headers_to_table(table_data, table_type == 'step_well_plates_table') @@ -38,9 +39,11 @@ module Reports::Docx::DrawStepTable end @docx.p do text I18n.t("projects.reports.elements.#{table_type}.table_name", name: table.name), italic: true - text ' ' - text I18n.t("projects.reports.elements.#{table_type}.user_time", - timestamp: I18n.l(timestamp, format: :full)), color: color[:gray] + unless settings['exclude_timestamps'] + text ' ' + text I18n.t("projects.reports.elements.#{table_type}.user_time", + timestamp: I18n.l(timestamp, format: :full)), color: color[:gray] + end end end end diff --git a/app/services/reports/docx/draw_step_text.rb b/app/services/reports/docx/draw_step_text.rb index 978386a94..5fc364639 100644 --- a/app/services/reports/docx/draw_step_text.rb +++ b/app/services/reports/docx/draw_step_text.rb @@ -5,11 +5,18 @@ module Reports::Docx::DrawStepText step_text = element.orderable timestamp = element.created_at color = @color - @docx.p do - text step_text.name.presence || '', italic: true - text ' ' - text I18n.t('projects.reports.elements.result_text.user_time', - timestamp: I18n.l(timestamp, format: :full)), color: color[:gray] + settings = @settings + + if step_text.name.present? || !settings['exclude_timestamps'] + @docx.p do + text step_text.name.to_s, italic: true + text ' ' if step_text.name.present? + + unless settings['exclude_timestamps'] + text I18n.t('projects.reports.elements.result_text.user_time', + timestamp: I18n.l(timestamp, format: :full)), color: color[:gray] + end + end end if step_text.text.present? html = custom_auto_link(step_text.text, team: @report_team) diff --git a/app/services/reports/docx/private_methods.rb b/app/services/reports/docx/private_methods.rb index 8e5db9780..1136bc635 100644 --- a/app/services/reports/docx/private_methods.rb +++ b/app/services/reports/docx/private_methods.rb @@ -16,16 +16,6 @@ module Reports::Docx::PrivateMethods bottom Constants::REPORT_DOCX_MARGIN_BOTTOM end - @docx.page_numbers true, align: :right - - insert_logo - - @docx.p do - text I18n.t('projects.reports.new.generate_PDF.generated_on', timestamp: I18n.l(Time.zone.now, format: :full)) - end - - @docx.hr - generate_html_styles end diff --git a/app/services/reports/docx/repository_helper.rb b/app/services/reports/docx/repository_helper.rb index ff25507ef..d8166ca9b 100644 --- a/app/services/reports/docx/repository_helper.rb +++ b/app/services/reports/docx/repository_helper.rb @@ -4,19 +4,25 @@ module Reports::Docx::RepositoryHelper include InputSanitizeHelper include ActionView::Helpers::NumberHelper - def prepare_row_columns_for_docx(repository_data, my_module, repository) + def prepare_row_columns_for_docx(repository_data, my_module = nil, repository = nil) + return if repository_data[:headers].blank? + result = [repository_data[:headers]] + excluded_columns = repository_data[:excluded_columns] + repository_data[:rows].each do |record| row = [] - row.push(record.code) - row.push(escape_input(record.archived ? "#{record.name} [#{I18n.t('general.archived')}]" : record.name)) - row.push(I18n.l(record.created_at, format: :full)) - row.push(escape_input(record.created_by.full_name)) + row.push(record.code) unless excluded_columns.include?(-1) + unless excluded_columns.include?(-2) + row.push(escape_input(record.archived ? "#{record.name} [#{I18n.t('general.archived')}]" : record.name)) + end + row.push(I18n.l(record.created_at, format: :full)) unless excluded_columns.include?(-3) + row.push(escape_input(record.created_by.full_name)) unless excluded_columns.include?(-4) cell_values = {} custom_cells = record.repository_cells custom_cells.each do |cell| - if cell.value.instance_of? RepositoryStockValue + if cell.value.instance_of?(RepositoryStockValue) && my_module if repository.is_a?(RepositorySnapshot) consumed_stock = record.repository_stock_consumption_cell&.value&.formatted || 0 cell_values[cell.repository_column_id] = consumed_stock @@ -38,6 +44,8 @@ module Reports::Docx::RepositoryHelper end repository_data[:custom_columns].each do |column_id| + next if excluded_columns.include?(column_id) + value = cell_values[column_id] row.push(value) end diff --git a/app/services/reports/docx_renderer.rb b/app/services/reports/docx_renderer.rb index 91872dc46..dd027bc4b 100644 --- a/app/services/reports/docx_renderer.rb +++ b/app/services/reports/docx_renderer.rb @@ -130,6 +130,13 @@ module Reports row[:data].each do |cell| docx_cell = Caracal::Core::Models::TableCellModel.new do |c| cell.each do |content| + c.background content[:style][:background] if content.dig(:style, :background).present? + if content.dig(:style, :vertical_align).present? && content[:style][:vertical_align] != :middle + c.vertical_align content[:style][:vertical_align] + else + c.vertical_align :center + end + if content[:type] == 'p' Reports::DocxRenderer.render_p_element(c, content, options.merge({ skip_br: true })) elsif content[:type] == 'table' diff --git a/app/services/reports/html_to_word_converter.rb b/app/services/reports/html_to_word_converter.rb index f4e312f8e..6d589a020 100644 --- a/app/services/reports/html_to_word_converter.rb +++ b/app/services/reports/html_to_word_converter.rb @@ -208,7 +208,7 @@ module Reports if style style_keys.each do |key| - style_el = style.value.split(';').select { |i| (i.include? key) }[0] + style_el = style.value.split(';').find { |i| i.strip.start_with?(key) } next unless style_el value = style_el.split(':')[1].strip if style_el @@ -259,6 +259,29 @@ module Reports } end + def table_cell_styling(elem) + style = elem.attributes['style'] + result = {} + style_keys = %w(background-color vertical-align background) + + if style + style_keys.each do |key| + style_el = style.value.split(';').find { |i| (i.include? key) } + next unless style_el + + value = style_el.split(':')[1].strip if style_el + + case key + when 'background-color', 'background' + result[:background] = normalized_hex_color(value) + when 'vertical-align' + result[:vertical_align] = value.to_sym + end + end + end + result + end + def tiny_mce_table_element(table_element) # array of elements rows = table_element.css('tbody').first.children.map do |row| @@ -267,11 +290,13 @@ module Reports cells = row.children.map do |cell| next unless cell.name == 'td' + style = table_cell_styling(cell) # Parse cell content formated_cell = recursive_children(cell.children, [], true) # Combine text elements to single paragraph formated_cell = combine_docx_elements(formated_cell) + formated_cell.each { |element| element[:style] = style } if style.present? formated_cell end.reject(&:blank?) { type: 'tr', data: cells } diff --git a/app/services/repository_actions/duplicate_cell.rb b/app/services/repository_actions/duplicate_cell.rb index e33cebaf6..4c370f7e7 100644 --- a/app/services/repository_actions/duplicate_cell.rb +++ b/app/services/repository_actions/duplicate_cell.rb @@ -25,7 +25,7 @@ module RepositoryActions private def repository_asset_value_extra_attributes(value) - new_asset = @cell.value.asset.duplicate + new_asset = @cell.value.asset.duplicate(created_by: @user) value.asset = new_asset end diff --git a/app/services/team_importer.rb b/app/services/team_importer.rb index e600b451e..e81c651c6 100644 --- a/app/services/team_importer.rb +++ b/app/services/team_importer.rb @@ -934,7 +934,7 @@ class TeamImporter user_id || find_user(asset.last_modified_by_id) asset.team = team asset.save! - asset.file.attach(io: file, filename: File.basename(file)) + asset.attach_file_version(io: file, filename: File.basename(file)) asset.post_process_file @asset_mappings[orig_asset_id] = asset.id @asset_counter += 1 diff --git a/app/services/templates_service.rb b/app/services/templates_service.rb index 8bdb18caf..75a6d0c12 100644 --- a/app/services/templates_service.rb +++ b/app/services/templates_service.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'newrelic_rpm' + class TemplatesService def initialize(base_dir = nil) @base_dir = base_dir ? base_dir : "#{Rails.root}/app/assets/templates" @@ -49,6 +51,7 @@ class TemplatesService end def update_all_templates + NewRelic::Agent.ignore_transaction processed_counter = 0 updated_counter = 0 Team.find_each do |team| diff --git a/app/services/toolbars/repositories_service.rb b/app/services/toolbars/repositories_service.rb index 34ac15a30..c66059b23 100644 --- a/app/services/toolbars/repositories_service.rb +++ b/app/services/toolbars/repositories_service.rb @@ -31,7 +31,7 @@ module Toolbars private def rename_action - return unless @single && can_manage_repository?(@repository) && !@repository.is_a?(SoftLockedRepository) + return unless @single && can_manage_repository?(@repository) { name: :update, @@ -67,7 +67,7 @@ module Toolbars def archive_action return unless @repositories.all? do |repository| - can_archive_repository?(repository) && !@repository.is_a?(SoftLockedRepository) + can_archive_repository?(repository) end { @@ -92,7 +92,7 @@ module Toolbars def restore_action return unless @repositories.all? do |repository| - can_archive_repository?(repository) && !repository.is_a?(SoftLockedRepository) + can_archive_repository?(repository) end { @@ -105,7 +105,7 @@ module Toolbars end def delete_action - return unless @single && can_delete_repository?(@repository) && !@repository.is_a?(SoftLockedRepository) + return unless @single && can_delete_repository?(@repository) { name: :delete, diff --git a/app/services/toolbars/repository_rows_service.rb b/app/services/toolbars/repository_rows_service.rb index cfbbdf84e..ad0924727 100644 --- a/app/services/toolbars/repository_rows_service.rb +++ b/app/services/toolbars/repository_rows_service.rb @@ -42,7 +42,7 @@ module Toolbars private def restore_action - return unless can_manage_repository_rows?(@repository) && !@repository.is_a?(SoftLockedRepository) + return unless can_manage_repository_rows?(@repository) return unless @repository_rows.all?(&:archived?) @@ -57,7 +57,7 @@ module Toolbars end def edit_action - return unless can_manage_repository_rows?(@repository) && !@repository.is_a?(SoftLockedRepository) + return unless can_manage_repository_rows?(@repository) return unless @repository_rows.all?(&:active?) @@ -87,7 +87,7 @@ module Toolbars end def duplicate_action - return unless can_create_repository_rows?(@repository) && !@repository.is_a?(SoftLockedRepository) + return unless can_create_repository_rows?(@repository) return unless @repository_rows.all?(&:active?) @@ -151,7 +151,7 @@ module Toolbars end def archive_action - return unless can_manage_repository_rows?(@repository) && !@repository.is_a?(SoftLockedRepository) + return unless can_manage_repository_rows?(@repository) return unless @repository_rows.all?(&:active?) @@ -166,7 +166,7 @@ module Toolbars end def delete_action - return unless can_delete_repository_rows?(@repository) && !@repository.is_a?(SoftLockedRepository) + return unless can_delete_repository_rows?(@repository) return unless @repository_rows.all?(&:archived?) diff --git a/app/utilities/encryptor.rb b/app/utilities/encryptor.rb deleted file mode 100644 index b18637391..000000000 --- a/app/utilities/encryptor.rb +++ /dev/null @@ -1,19 +0,0 @@ -module Encryptor - def decrypt(data) - return '' unless data.present? - cipher = build_cipher(:decrypt, 'f5awRubeTUd2E*8duxum') - cipher.update(Base64.urlsafe_decode64(data).unpack('m')[0]) + cipher.final - end - - def encrypt(data) - return '' unless data.present? - cipher = build_cipher(:encrypt, 'f5awRubeTUd2E*8duxum') - Base64.urlsafe_encode64([cipher.update(data) + cipher.final].pack('m')) - end - - def build_cipher(type, password) - cipher = OpenSSL::Cipher::Cipher.new('DES-EDE3-CBC').send(type) - cipher.pkcs5_keyivgen(password) - cipher - end -end \ No newline at end of file diff --git a/app/utilities/protocol_importers/attachments_builder.rb b/app/utilities/protocol_importers/attachments_builder.rb index df6788f1d..f0c50b5ea 100644 --- a/app/utilities/protocol_importers/attachments_builder.rb +++ b/app/utilities/protocol_importers/attachments_builder.rb @@ -7,7 +7,7 @@ module ProtocolImporters step_json[:attachments].map do |f| asset = Asset.new(created_by: user, last_modified_by: user, team: team) - asset.file.attach(io: URI.open(f[:url]), filename: f[:name]) + asset.attach_file_version(io: URI(f[:url]).open, filename: f[:name]) asset end end diff --git a/app/utilities/protocols_exporter_v2.rb b/app/utilities/protocols_exporter_v2.rb index a4e2ba3ea..e7ff623fa 100644 --- a/app/utilities/protocols_exporter_v2.rb +++ b/app/utilities/protocols_exporter_v2.rb @@ -132,7 +132,7 @@ module ProtocolsExporterV2 "\n" \ "#{asset.file_name}\n" \ "#{asset.content_type}\n" \ - "\n" \ + "\n" \ "\n" end end diff --git a/app/utilities/protocols_importer.rb b/app/utilities/protocols_importer.rb index a66b8fe02..4738c8eb9 100644 --- a/app/utilities/protocols_importer.rb +++ b/app/utilities/protocols_importer.rb @@ -132,7 +132,7 @@ class ProtocolsImporter ) # Decode the file bytes - asset.file.attach(io: StringIO.new(Base64.decode64(asset_json['bytes'])), + asset.attach_file_version(io: StringIO.new(Base64.decode64(asset_json['bytes'])), filename: asset_json['fileName'], content_type: asset_json['fileType'], metadata: JSON.parse(asset_json['fileMetadata'] || '{}')) diff --git a/app/utilities/protocols_importer_v2.rb b/app/utilities/protocols_importer_v2.rb index ba6e3b813..9d91f438c 100644 --- a/app/utilities/protocols_importer_v2.rb +++ b/app/utilities/protocols_importer_v2.rb @@ -109,7 +109,7 @@ class ProtocolsImporterV2 ) # Decode the file bytes - asset.file.attach(io: StringIO.new(Base64.decode64(asset_json['bytes'])), + asset.attach_file_version(io: StringIO.new(Base64.decode64(asset_json['bytes'])), filename: asset_json['fileName'], content_type: asset_json['fileType'], metadata: JSON.parse(asset_json['fileMetadata'] || '{}')) diff --git a/app/views/design_elements/_breadcrumbs.html.erb b/app/views/design_elements/_breadcrumbs.html.erb new file mode 100644 index 000000000..dc24c31d1 --- /dev/null +++ b/app/views/design_elements/_breadcrumbs.html.erb @@ -0,0 +1,8 @@ +
+

breadcrumbs

+ +
+ +<%= javascript_include_tag 'vue_design_system_breadcrumbs' %> diff --git a/app/views/design_elements/index.html.erb b/app/views/design_elements/index.html.erb index 523cfe07e..5f9585a19 100644 --- a/app/views/design_elements/index.html.erb +++ b/app/views/design_elements/index.html.erb @@ -14,6 +14,8 @@ <%= render partial: 'select' %> +<%= render partial: 'breadcrumbs' %> + <%= render partial: 'modals' %> <%= render partial: 'icons', locals: {icons_list: icons_list} %> diff --git a/app/views/global_activities/references/_protocol.html.erb b/app/views/global_activities/references/_protocol.html.erb index 1ff72f58b..930a249cf 100644 --- a/app/views/global_activities/references/_protocol.html.erb +++ b/app/views/global_activities/references/_protocol.html.erb @@ -9,7 +9,7 @@ <% if subject.in_repository? %>
- <%= route_to_other_team protocols_path(team: subject.team), + <%= route_to_other_team protocol_path(subject), team, subject.name&.truncate(Constants::NAME_TRUNCATION_LENGTH), title: subject.name %> diff --git a/app/views/layouts/reports/template_values_editor.html.erb b/app/views/layouts/reports/template_values_editor.html.erb index 1f9352e83..841b024ef 100644 --- a/app/views/layouts/reports/template_values_editor.html.erb +++ b/app/views/layouts/reports/template_values_editor.html.erb @@ -1,6 +1,10 @@

- <%= t('projects.reports.wizard.first_step.values_editor.title') %> + <% if @type == :pdf %> + <%= t('projects.reports.wizard.first_step.values_editor.title_pdf', template: @template_name) %> + <% else %> + <%= t('projects.reports.wizard.first_step.values_editor.title_docx', template: @template_name) %> + <% end %>

-
-

- - <%= t('projects.reports.wizard.first_step.values_editor.header') %> -

-
- <%= yield :header %> -
-
+<% + toc = yield :toc + header = yield :header + cover = yield :cover + footer = yield :footer +%> -
-

- - <%= t('projects.reports.wizard.first_step.values_editor.cover') %> -

-
- <%= yield :cover %> +<% if toc.present? %> +
+

+ + <%= t('projects.reports.wizard.first_step.values_editor.toc') %> +

+
+ <%= toc %> +
-
+<% end %> -
-

- - <%= t('projects.reports.wizard.first_step.values_editor.footer') %> -

-
- <%= yield :footer %> +<% if header.present? %> +
+

+ + <%= t('projects.reports.wizard.first_step.values_editor.header') %> +

+
+ <%= yield header %> +
-
+<% end %> + +<% if cover.present? %> +
+

+ + <%= t('projects.reports.wizard.first_step.values_editor.cover') %> +

+
+ <%= cover %> +
+
+<% end %> + +<% if footer.present? %> +
+

+ + <%= t('projects.reports.wizard.first_step.values_editor.footer') %> +

+
+ <%= footer %> +
+
+<% end %> diff --git a/app/views/reports/docx_templates/scinote_template/docx.rb b/app/views/reports/docx_templates/scinote_template/docx.rb new file mode 100644 index 000000000..5b5e05312 --- /dev/null +++ b/app/views/reports/docx_templates/scinote_template/docx.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module ScinoteTemplateDocx + def prepare_docx + @docx.page_numbers true, align: :right + + insert_logo + + @docx.p do + text I18n.t('projects.reports.new.generate_PDF.generated_on', timestamp: I18n.l(Time.zone.now, format: :full)) + end + + @docx.hr + + @report.root_elements.each do |subject| + public_send("draw_#{subject.type_of}", subject) + end + end +end diff --git a/app/views/reports/docx_templates/scinote_template/name.txt b/app/views/reports/docx_templates/scinote_template/name.txt new file mode 100644 index 000000000..9c7a580cc --- /dev/null +++ b/app/views/reports/docx_templates/scinote_template/name.txt @@ -0,0 +1 @@ +SciNote Template diff --git a/app/views/reports/elements/_experiment_element.html.erb b/app/views/reports/elements/_experiment_element.html.erb index 86804dc9a..0020d7ec7 100644 --- a/app/views/reports/elements/_experiment_element.html.erb +++ b/app/views/reports/elements/_experiment_element.html.erb @@ -9,9 +9,11 @@ <%= t('search.index.archived') %> <% end %> -
- <%= t('projects.reports.elements.experiment.user_time', code: experiment.code, timestamp: l(timestamp, format: :full)) %> -
+ <% unless @settings['exclude_timestamps'] %> +
+ <%= t('projects.reports.elements.experiment.user_time', code: experiment.code, timestamp: l(timestamp, format: :full)) %> +
+ <% end %> <% if experiment.description.present? %> <%= custom_auto_link(experiment.description, team: current_team, base64_encoded_imgs: export_all) %> <% end %> diff --git a/app/views/reports/elements/_my_module_element.html.erb b/app/views/reports/elements/_my_module_element.html.erb index 43d9fd665..b8778e7a7 100644 --- a/app/views/reports/elements/_my_module_element.html.erb +++ b/app/views/reports/elements/_my_module_element.html.erb @@ -9,45 +9,49 @@ <%= t('search.index.archived') %> <% end %> -
- <%= t('projects.reports.elements.module.user_time', code: my_module.code, timestamp: l(timestamp, format: :full)) %> -
-

- <% if my_module.started_on.present? %> - <%= t('projects.reports.elements.module.started_on', started_on: l(my_module.started_on, format: :full)) %> - <% end %> -

-

- <% if my_module.due_date.present? %> - <%= t('projects.reports.elements.module.due_date', due_date: l(my_module.due_date, format: :full)) %> - <% end %> -

-

- <% status = my_module.my_module_status %> - <%= t('projects.reports.elements.module.status') %> - - <%= status.name %> - - <% if my_module.completed? %> - - <%= t('my_modules.states.completed') %> - <%= l(my_module.completed_on, format: :full) %> - - <% end %> -

-
-
- <%= t('projects.reports.elements.module.tags_header') %> + <% unless @settings['exclude_timestamps'] %> +
+ <%= t('projects.reports.elements.module.user_time', code: my_module.code, timestamp: l(timestamp, format: :full)) %>
- <% if (tags = my_module.tags.order(:id)).present? %> - <% tags.each do |tag| %> -
- <%= tag.name %> -
+ <% end %> + <% unless @settings['exclude_task_metadata'] %> +

+ <% if my_module.started_on.present? %> + <%= t('projects.reports.elements.module.started_on', started_on: l(my_module.started_on, format: :full)) %> <% end %> - <% end %> -

+

+

+ <% if my_module.due_date.present? %> + <%= t('projects.reports.elements.module.due_date', due_date: l(my_module.due_date, format: :full)) %> + <% end %> +

+

+ <% status = my_module.my_module_status %> + <%= t('projects.reports.elements.module.status') %> + + <%= status.name %> + + <% if my_module.completed? %> + + <%= t('my_modules.states.completed') %> + <%= l(my_module.completed_on, format: :full) %> + + <% end %> +

+
+
+ <%= t('projects.reports.elements.module.tags_header') %> +
+ <% if (tags = my_module.tags.order(:id)).present? %> + <% tags.each do |tag| %> +
+ <%= tag.name %> +
+ <% end %> + <% end %> +
+ <% end %>
<% if my_module.description.present? %> @@ -90,9 +94,11 @@ <% if @settings.dig('task', 'file_results') %> <%= render partial: 'reports/elements/my_module_result_asset_element', locals: { result: result, report: report, export_all: export_all } %> -
- <%= t('projects.reports.elements.result.user_time', user: result.user.full_name, timestamp: l(result.created_at, format: :full)) %> -
+ <% unless @settings['exclude_timestamps'] %> +
+ <%= t('projects.reports.elements.result.user_time', user: result.user.full_name, timestamp: l(result.created_at, format: :full)) %> +
+ <% end %> <% end %>
diff --git a/app/views/reports/elements/_my_module_protocol_element.html.erb b/app/views/reports/elements/_my_module_protocol_element.html.erb index 9fd76c321..67f2c3adc 100644 --- a/app/views/reports/elements/_my_module_protocol_element.html.erb +++ b/app/views/reports/elements/_my_module_protocol_element.html.erb @@ -9,9 +9,11 @@ <%= t('projects.reports.elements.module.protocol.name') %> <% end %> -
- <%= t('projects.reports.elements.module.protocol.user_time', code: protocol.original_code, timestamp: l(protocol.created_at, format: :full)) %> -
+ <% unless @settings['exclude_timestamps'] %> +
+ <%= t('projects.reports.elements.module.protocol.user_time', code: protocol.original_code, timestamp: l(protocol.created_at, format: :full)) %> +
+ <% end %>
<% if @settings.dig('task', 'protocol', 'description') && protocol.description.present? %> <%= custom_auto_link(protocol.prepare_for_report(:description, export_all: export_all), diff --git a/app/views/reports/elements/_my_module_result_asset_element.html.erb b/app/views/reports/elements/_my_module_result_asset_element.html.erb index 65c9a3e43..f4cd9e072 100644 --- a/app/views/reports/elements/_my_module_result_asset_element.html.erb +++ b/app/views/reports/elements/_my_module_result_asset_element.html.erb @@ -24,9 +24,11 @@ <%= link_to t('projects.reports.elements.download'), asset_download_url(asset, disposition: 'attachment'), class: 'download-link', target: :_blank %> <% end %> -
+
- <%= t("projects.reports.elements.result_asset.user_time", user: result.user.full_name, timestamp: l(timestamp, format: :full)) %> + <% unless @settings['exclude_timestamps'] %> + <%= t("projects.reports.elements.result_asset.user_time", user: result.user.full_name, timestamp: l(timestamp, format: :full)) %> + <% end %> <% if report.settings.dig(:task, :file_results_previews) && ActiveStorageFileUtil.previewable_document?(asset&.file&.blob) %> <%= t('projects.reports.elements.result_asset.full_preview_attached') %> <% end %> diff --git a/app/views/reports/elements/_my_module_result_table_element.html.erb b/app/views/reports/elements/_my_module_result_table_element.html.erb index 2464735b8..dbcced513 100644 --- a/app/views/reports/elements/_my_module_result_table_element.html.erb +++ b/app/views/reports/elements/_my_module_result_table_element.html.erb @@ -21,9 +21,11 @@ <% end %> <% end %>
-
- <%= t('projects.reports.elements.result_table.user_time', timestamp: l(timestamp, format: :full)) %> -
+ <% unless @settings['exclude_timestamps'] %> +
+ <%= t('projects.reports.elements.result_table.user_time', timestamp: l(timestamp, format: :full)) %> +
+ <% end %>
diff --git a/app/views/reports/elements/_my_module_result_text_element.html.erb b/app/views/reports/elements/_my_module_result_text_element.html.erb index b3b96926f..0dff3eb67 100644 --- a/app/views/reports/elements/_my_module_result_text_element.html.erb +++ b/app/views/reports/elements/_my_module_result_text_element.html.erb @@ -11,9 +11,11 @@ <% end %>
-
- <%= t("projects.reports.elements.result_text.user_time", timestamp: l(timestamp, format: :full)) %> -
+ <% unless @settings['exclude_timestamps'] %> +
+ <%= t("projects.reports.elements.result_text.user_time", timestamp: l(timestamp, format: :full)) %> +
+ <% end %>
diff --git a/app/views/reports/elements/_my_module_step_element.html.erb b/app/views/reports/elements/_my_module_step_element.html.erb index f0b636cd2..fa330886d 100644 --- a/app/views/reports/elements/_my_module_step_element.html.erb +++ b/app/views/reports/elements/_my_module_step_element.html.erb @@ -8,11 +8,15 @@
<%= t('projects.reports.elements.step.step_pos', pos: (step.position_plus_one)) %> <%= step.name %> - <%= step_status_label(step) %> + <% unless @settings['exclude_task_metadata'] %> + <%= step_status_label(step) %> + <% end %>
-
- <%= t("projects.reports.elements.step.#{step_type_str}.user_time", user: user.full_name , timestamp: l(timestamp, format: :full)) %> -
+ <% unless @settings['exclude_timestamps'] %> +
+ <%= t("projects.reports.elements.step.#{step_type_str}.user_time", user: user.full_name , timestamp: l(timestamp, format: :full)) %> +
+ <% end %>
<% step.step_orderable_elements.order(:position).each do |e| %> diff --git a/app/views/reports/elements/_project_header_element.html.erb b/app/views/reports/elements/_project_header_element.html.erb index 54ad6a629..c6d3b9636 100644 --- a/app/views/reports/elements/_project_header_element.html.erb +++ b/app/views/reports/elements/_project_header_element.html.erb @@ -7,9 +7,11 @@ <%= t('search.index.archived') %> <% end %> -
- <%= t('projects.reports.elements.project_header.user_time', code: project.code, timestamp: l(project.created_at, format: :full)) %> -
+ <% unless @settings['exclude_timestamps'] %> +
+ <%= t('projects.reports.elements.project_header.user_time', code: project.code, timestamp: l(project.created_at, format: :full)) %> +
+ <% end %>
<% if defined?(children) %>
diff --git a/app/views/reports/elements/_step_asset_element.html.erb b/app/views/reports/elements/_step_asset_element.html.erb index 2842a7adf..04d8313e4 100644 --- a/app/views/reports/elements/_step_asset_element.html.erb +++ b/app/views/reports/elements/_step_asset_element.html.erb @@ -18,9 +18,11 @@ <% end %> -
-  <%= t('projects.reports.elements.step_asset.user_time', timestamp: l(timestamp, format: :full)) %> -
+ <% unless @settings['exclude_timestamps'] %> +
+  <%= t('projects.reports.elements.step_asset.user_time', timestamp: l(timestamp, format: :full)) %> +
+ <% end %>
<% if asset.previewable? && !asset.list? %> diff --git a/app/views/reports/elements/_step_checklist_element.html.erb b/app/views/reports/elements/_step_checklist_element.html.erb index f4d625d1a..02549a31b 100644 --- a/app/views/reports/elements/_step_checklist_element.html.erb +++ b/app/views/reports/elements/_step_checklist_element.html.erb @@ -9,9 +9,11 @@ team: current_team, base64_encoded_imgs: export_all) %>
-
- <%= t('projects.reports.elements.step_checklist.user_time', timestamp: l(timestamp, format: :full)) %> -
+ <% unless @settings['exclude_timestamps'] %> +
+ <%= t('projects.reports.elements.step_checklist.user_time', timestamp: l(timestamp, format: :full)) %> +
+ <% end %>
<% items.each do |item| %> diff --git a/app/views/reports/elements/_step_table_element.html.erb b/app/views/reports/elements/_step_table_element.html.erb index e3334f08c..813c91766 100644 --- a/app/views/reports/elements/_step_table_element.html.erb +++ b/app/views/reports/elements/_step_table_element.html.erb @@ -18,9 +18,11 @@ <% end %> <% end %>
-
- <%= t("projects.reports.elements.#{table_type}.user_time", timestamp: l(timestamp, format: :full)) %> -
+ <% unless @settings['exclude_timestamps'] %> +
+ <%= t("projects.reports.elements.#{table_type}.user_time", timestamp: l(timestamp, format: :full)) %> +
+ <% end %>
diff --git a/app/views/reports/elements/_step_text_element.html.erb b/app/views/reports/elements/_step_text_element.html.erb index 01c274f89..6e42a7487 100644 --- a/app/views/reports/elements/_step_text_element.html.erb +++ b/app/views/reports/elements/_step_text_element.html.erb @@ -8,9 +8,11 @@ <% end %>
-
- <%= t("projects.reports.elements.step_text.user_time", timestamp: l(timestamp, format: :full)) %> -
+ <% unless @settings['exclude_timestamps'] %> +
+ <%= t("projects.reports.elements.step_text.user_time", timestamp: l(timestamp, format: :full)) %> +
+ <% end %>
<% if step_text.text.present? %> diff --git a/app/views/reports/wizard/_first_step.html.erb b/app/views/reports/wizard/_first_step.html.erb index 5d9cda6fc..1fba29ada 100644 --- a/app/views/reports/wizard/_first_step.html.erb +++ b/app/views/reports/wizard/_first_step.html.erb @@ -1,5 +1,7 @@
<%= render partial: 'reports/wizard/project_template_selector', locals: {report: report} %>
-
"> +
"> +
+
">
diff --git a/app/views/reports/wizard/_no_template_values.html.erb b/app/views/reports/wizard/_no_template_values.html.erb index bed919b36..4d3f958da 100644 --- a/app/views/reports/wizard/_no_template_values.html.erb +++ b/app/views/reports/wizard/_no_template_values.html.erb @@ -1,5 +1,9 @@

- <%= t('projects.reports.wizard.first_step.values_editor.no_values_title') %> + <% if type == :pdf %> + <%= t('projects.reports.wizard.first_step.values_editor.no_values_title_pdf', template: ) %> + <% else %> + <%= t('projects.reports.wizard.first_step.values_editor.no_values_title_docx', template: ) %> + <% end %>

<%= t('projects.reports.wizard.first_step.values_editor.no_values_description') %> diff --git a/app/views/reports/wizard/_project_template_selector.html.erb b/app/views/reports/wizard/_project_template_selector.html.erb index f29b6e7d6..ce8e5c6fc 100644 --- a/app/views/reports/wizard/_project_template_selector.html.erb +++ b/app/views/reports/wizard/_project_template_selector.html.erb @@ -20,9 +20,23 @@ disable_on_load: report.settings[:template].blank? && report.new_record?, placeholder: t('projects.reports.wizard.first_step.select_template'), selected_template: report.settings[:template], + default_template: @default_template, values_editor_path: reports_new_template_values_path(report_id: report.id) } %>

+
+ <%= label_tag :docxTemplateSelector, t('projects.reports.wizard.first_step.select_docx_template') %> + <%= select_tag :docxTemplateSelector, + options_for_select(@docx_templates.invert, @active_docx_template), + prompt: t('projects.reports.wizard.first_step.select_docx_template'), + data: { + disable_on_load: report.settings[:docx_template].blank? && report.new_record?, + placeholder: t('projects.reports.wizard.first_step.select_docx_template'), + selected_template: report.settings[:docx_template], + default_template: @default_docx_template, + values_editor_path: reports_new_docx_template_values_path(report_id: report.id) + } %> +
diff --git a/app/views/reports/wizard/_third_step.html.erb b/app/views/reports/wizard/_third_step.html.erb index 642eb9db5..a1f8216f7 100644 --- a/app/views/reports/wizard/_third_step.html.erb +++ b/app/views/reports/wizard/_third_step.html.erb @@ -55,7 +55,10 @@
- <%= t("projects.reports.wizard.third_step.assigned_items_description") %> +
+

<%= t("projects.reports.wizard.third_step.assigned_items_description") %>

+

<%= t("projects.reports.wizard.third_step.assigned_items_repository_items_description_html") %>

+
  • @@ -68,25 +71,58 @@
      <% @repositories.each do |repository| %>
    • - - /> - - - <%= repository.name %> - <% if repository.archived? %> - - <%= t("projects.reports.wizard.third_step.archived") %> +
      + + /> + - <% elsif repository.is_a?(RepositorySnapshot) %> - - <%= t("projects.reports.wizard.third_step.deleted") %> - - <% end %> + <%= repository.name %> + <% if repository.archived? %> + + <%= t("projects.reports.wizard.third_step.archived") %> + + <% elsif repository.is_a?(RepositorySnapshot) %> + + <%= t("projects.reports.wizard.third_step.deleted") %> + + <% end %> + +
    • <% end %> @@ -184,7 +220,7 @@ <%= t("projects.reports.wizard.third_step.additional_content") %>
        -
      • +
      • /> @@ -194,6 +230,26 @@
      • +
      • +
        + + /> + + + <%= t("projects.reports.wizard.third_step.exclude_task_metadata") %> +
        +
        +
      • +
      • +
        + + /> + + + <%= t("projects.reports.wizard.third_step.exclude_timestamps") %> +
        +
        +
diff --git a/app/views/repositories/_delete_asset_value_modal.html.erb b/app/views/repositories/_delete_asset_value_modal.html.erb new file mode 100644 index 000000000..81ccb9356 --- /dev/null +++ b/app/views/repositories/_delete_asset_value_modal.html.erb @@ -0,0 +1,19 @@ + diff --git a/app/views/repositories/_repository_table.html.erb b/app/views/repositories/_repository_table.html.erb index 1dfd4fd57..4f542613c 100644 --- a/app/views/repositories/_repository_table.html.erb +++ b/app/views/repositories/_repository_table.html.erb @@ -48,7 +48,7 @@ <% if @repository.is_a?(LinkedRepository) %> <%= t('repositories.table.external_id') %> <% end %> - <% columns_editable = can_manage_repository_columns?(@repository) && !repository.is_a?(SoftLockedRepository) %> + <% columns_editable = can_manage_repository_columns?(@repository) %> <% repository.repository_columns.order(:id).each do |column| %> <%= t('repositories.index.snapshot_provisioning_in_progress') %>
<% end %> - <% if can_create_repository_rows?(@repository) && !@repository.is_a?(SoftLockedRepository) %> + <% if can_create_repository_rows?(@repository) %>