Merge pull request #8053 from scinote-eln/develop

November 2024 Release
This commit is contained in:
Martin Artnik 2024-11-20 11:46:40 +01:00 committed by GitHub
commit 4c57d97f77
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
165 changed files with 2205 additions and 795 deletions

View file

@ -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'

View file

@ -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)

View file

@ -1 +1 @@
1.37.0
1.38.0

View file

@ -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();

View file

@ -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();

View file

@ -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(`<input type="hidden"
form="${$input.attr('form')}"
name="repository_cells[${$input.data('col-id')}]"
value=""/>`);
}
$('#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'));

View file

@ -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(`<input type="hidden"
form="${$input.attr('form')}"
name="repository_cells[${$input.data('col-id')}]"
value=""/>`);
}
$('#deleteRepositoryAssetValueModal').modal('show');
});
}

View file

@ -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(`
<div class="table-wrapper w-full" style="overflow: auto;"></div>
`);
}
}
$('.rtf-view').toArray().forEach((rtf) => {
$(rtf).find('table').toArray().forEach((table) => {
if ($(table).parents('table').length === 0) {
$(table).css('float', 'none')
.wrapAll('<div class="table-wrapper w-full" style="overflow: auto"></div>');
}
});
});
}());

View file

@ -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) {

View file

@ -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')) {
$(`<div class="dropdown-select-all btn">${selectElement.data('select-all-button')}</div>`)
$(`<div class="dropdown-select-all">${selectElement.data('select-all-button')}</div>`)
.appendTo(dropdownContainer.find('.dropdown-container'))
.click(() => {
// For AJAX dropdown we will use only "Deselect All"

View file

@ -142,23 +142,14 @@ $.fn.initSubmitModal = function(modalID, modelName) {
* @returns {string} - HTML with tables wrapped.
*/
function wrapTables(htmlStringOrDomEl) {
if (typeof htmlStringOrDomEl === 'string') {
const container = $(`<span class="text-base">${htmlStringOrDomEl}</span>`);
container.find('table').toArray().forEach((table) => {
if ($(table).parent().hasClass('table-wrapper')) return;
$(table).css('float', 'none').wrapAll(`
<div class="table-wrapper" style="overflow: auto; width: 100%"></div>
`);
});
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('<div class="table-wrapper" style="overflow: auto; width: 100%"></div>');
const updatedHtml = $(htmlStringOrDomEl).html();
$(htmlStringOrDomEl).replaceWith(updatedHtml);
const htmlContent = `<span class="text-base">${htmlStringOrDomEl}</span>`;
const container = typeof htmlStringOrDomEl === 'string' ? $(htmlContent) : $(htmlStringOrDomEl);
container.find('table').toArray().forEach((table) => {
if ($(table).parents('table').length === 0) {
$(table).css('float', 'none')
.wrapAll('<div class="table-wrapper w-full" style="overflow: auto"></div>');
}
}
});
return container.prop('outerHTML');
}

View file

@ -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;

View file

@ -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;

View file

@ -175,6 +175,10 @@
top: 0;
width: 100%;
z-index: 5;
&:hover {
background: $color-concrete;
}
}
.dropdown-blank {

View file

@ -1362,6 +1362,10 @@ th.custom-field .modal-tooltiptext {
cursor: pointer;
}
.tooltip {
z-index: 9999;
}
.tooltip-open {
background-color: $color-concrete;
color: $color-black;

View file

@ -0,0 +1,11 @@
<% if @editing %>
<div class="sci-select-container">
<%= 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 %>
</div>
<% else %>
<% @project_members.where(id: @value).each do |member| %>
<%= member.public_send(@displayed_field) %>
<br>
<% end %>
<% end %>

View file

@ -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

View file

@ -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")

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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"
)

View file

@ -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

View file

@ -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? &&

View file

@ -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?

View file

@ -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',

View file

@ -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

View file

@ -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],

View file

@ -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

View file

@ -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?

View file

@ -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 = {

View file

@ -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

View file

@ -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'])

View file

@ -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

View file

@ -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 << "<span class='help-block'>"
res << flash[:recaptcha_error]

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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(`
<div class="table-wrapper" style="overflow: auto; width: ${container.width()}px"></div>
`);
if ($(table).parents('table').length === 0) {
$(table).css('float', 'none')
.wrapAll('<div class="table-wrapper w-full" style="overflow: auto"></div>');
}
});
}
};

View file

@ -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');

View file

@ -1,41 +1,63 @@
<template>
<div class="sci-navigation--notificaitons-flyout-notification">
<div class="sci-navigation--notificaitons-flyout-notification-date">
{{ notification.attributes.created_at }}
</div>
<div class="sci-navigation--notificaitons-flyout-notification-title"
v-html="notification.attributes.title"
:data-seen="notification.attributes.checked"></div>
<div v-html="notification.attributes.message" class="sci-navigation--notificaitons-flyout-notification-message"></div>
<div v-if="notification.attributes.breadcrumbs" class="flex items-center flex-wrap gap-0.5">
<template v-for="(breadcrumb, index) in notification.attributes.breadcrumbs" :key="index">
<div class="flex items-center gap-0.5">
<i v-if="index > 0" class="sn-icon sn-icon-right"></i>
<a :href="breadcrumb.url" :title="breadcrumb.name" class="truncate max-w-[20ch] inline-block">{{ breadcrumb.name }}</a>
<div class="sci-navigation--notificaitons-flyout-notification hover:bg-sn-super-light-grey !px-2 !-mx-2">
<div class="flex item-center">
<a :href="lastBreadcrumbUrl" @click="toggleRead(true); closeFlyout()" class="hover:no-underline text-black hover:text-black grow">
<div class="sci-navigation--notificaitons-flyout-notification-date">
{{ notification.attributes.created_at }}
</div>
</template>
</a>
<div class="ml-auto cursor-pointer" @click="toggleRead()">
<div v-if="!notification.attributes.checked" class="w-2.5 h-2.5 bg-sn-coral rounded-full cursor-pointer"></div>
<div v-else class="w-2.5 h-2.5 border-2 border-sn-grey rounded-full border-solid cursor-pointer hover:border-sn-coral"></div>
</div>
</div>
<a :href="lastBreadcrumbUrl" @click="toggleRead(true); closeFlyout()" class="hover:no-underline text-black hover:text-black">
<div class="sci-navigation--notificaitons-flyout-notification-title"
v-html="notification.attributes.title"
:data-seen="notification.attributes.checked"></div>
<div v-html="notification.attributes.message" class="sci-navigation--notificaitons-flyout-notification-message"></div>
</a>
<div v-if="notification.attributes.breadcrumbs" class="flex items-center flex-wrap gap-0.5">
<Breadcrumbs :breadcrumbs="notification.attributes.breadcrumbs" />
</div>
</div>
</template>
<script>
import axios from '../../../packs/custom_axios.js';
import Breadcrumbs from '../../shared/breadcrumbs.vue';
export default {
name: 'NotificationItem',
props: {
notification: Object
},
components: {
Breadcrumbs
},
computed: {
icon() {
switch (this.notification.attributes.type_of) {
case 'deliver':
return 'fas fa-truck';
case 'assignment':
return 'fas fa-list-alt';
case 'recent_changes':
return 'fas fa-list-alt';
case 'deliver_error':
return 'sn-icon sn-icon-alert-warning';
lastBreadcrumbUrl() {
if (!this.notification.attributes.breadcrumbs) {
return null;
}
return this.notification.attributes.breadcrumbs[this.notification.attributes.breadcrumbs.length - 1]?.url;
}
},
methods: {
closeFlyout() {
this.$emit('close');
},
toggleRead(check = false) {
const params = {};
if (!this.notification.attributes.checked || check) {
params.mark_as_read = true;
}
axios.post(this.notification.attributes.toggle_read_url, params)
.then((response) => {
const notification = response.data.data;
this.$emit('updateNotification', notification);
});
}
}
};

View file

@ -6,17 +6,47 @@
{{ i18n.t('nav.settings') }}
</a>
</div>
<hr>
<div class="flex items-center">
<div
@click="changeTab('all')"
class="px-4 py-2 text-sn-grey cursor-pointer border-solid border-0 border-b-4 border-transparent"
:class="{'!text-sn-black border-sn-blue': activeTab === 'all'}"
>
{{ i18n.t('nav.notifications.all') }}
</div>
<div
@click="changeTab('unread')"
class="px-4 py-2 text-sn-grey cursor-pointer border-solid border-0 border-b-4 border-transparent"
:class="{'!text-sn-black border-sn-blue': activeTab === 'unread'}"
>
{{ i18n.t('nav.notifications.unread') }}
</div>
<div
@click="changeTab('read')"
class="px-4 py-2 text-sn-grey cursor-pointer border-solid border-0 border-b-4 border-transparent"
:class="{'!text-sn-black border-sn-blue': activeTab === 'read'}"
>
{{ i18n.t('nav.notifications.read') }}
</div>
<div v-if="activeTab !== 'read'" class="py-2 ml-auto cursor-pointer" @click="markAllNotificationsAsRead">
{{ i18n.t('nav.notifications.read_all') }}
</div>
</div>
<hr class="!mt-0.5">
<perfect-scrollbar @wheel="preventPropagation" ref="scrollContainer" class="sci--navigation--notificaitons-flyout-notifications">
<div class="sci-navigation--notificaitons-flyout-subtitle" v-if="todayNotifications.length">
{{ i18n.t('nav.notifications.today') }}
</div>
<NotificationItem v-for="notification in todayNotifications" :key="notification.type_of + '-' + notification.id"
@updateNotification="updateNotification"
@close="$emit('close')"
:notification="notification" />
<div class="sci-navigation--notificaitons-flyout-subtitle" v-if="olderNotifications.length">
{{ i18n.t('nav.notifications.older') }}
</div>
<NotificationItem v-for="notification in olderNotifications" :key="notification.type_of + '-' + notification.id"
@updateNotification="updateNotification"
@close="$emit('close')"
:notification="notification" />
<div class="next-page-loader">
<img src="/images/medium/loading.svg" v-if="loadingPage" />
@ -37,6 +67,7 @@ export default {
},
props: {
notificationsUrl: String,
markAllNotificationsUrl: String,
unseenNotificationsCount: Number,
preferencesUrl: String
},
@ -45,11 +76,14 @@ export default {
notifications: [],
nextPageUrl: null,
scrollBar: null,
loadingPage: false
activeTab: 'all',
loadingPage: false,
firstPageUrl: null
};
},
created() {
this.nextPageUrl = this.notificationsUrl;
this.firstPageUrl = this.notificationsUrl;
this.loadNotifications();
},
mounted() {
@ -76,6 +110,27 @@ export default {
}
},
methods: {
changeTab(tab) {
this.activeTab = tab;
this.notifications = [];
this.nextPageUrl = this.firstPageUrl;
this.loadNotifications();
},
markAllNotificationsAsRead() {
axios.post(this.markAllNotificationsUrl)
.then(() => {
this.notifications = this.notifications.map((n) => {
n.attributes.checked = true;
return n;
});
this.$emit('update:unseenNotificationsCount');
});
},
updateNotification(notification) {
const index = this.notifications.findIndex((n) => n.id === notification.id);
this.notifications.splice(index, 1, notification);
this.$emit('update:unseenNotificationsCount');
},
preventPropagation(e) {
e.stopPropagation();
e.preventDefault();
@ -85,7 +140,7 @@ export default {
this.loadingPage = true;
axios.get(this.nextPageUrl)
axios.get(this.nextPageUrl, { params: { tab: this.activeTab } })
.then((response) => {
this.notifications = this.notifications.concat(response.data.data);
this.nextPageUrl = response.data.links.next;

View file

@ -45,8 +45,9 @@
<NotificationsFlyout
:preferencesUrl="user.preferences_url"
:notificationsUrl="notificationsUrl"
:markAllNotificationsUrl="markAllNotificationsUrl"
:unseenNotificationsCount="unseenNotificationsCount"
@update:unseenNotificationsCount="checkUnseenNotifications()"
@update:unseenNotificationsCount="checkUnseenNotifications(false)"
@close="$refs.notificationDropdown.$refs.field.click();"/>
</template>
</GeneralDropdown>
@ -92,6 +93,7 @@ export default {
props: {
url: String,
notificationsUrl: String,
markAllNotificationsUrl: String,
unseenNotificationsUrl: String,
quickSearchUrl: String,
teamsUrl: String,
@ -119,7 +121,7 @@ export default {
$(document).on('turbolinks:load', () => {
this.notificationsOpened = false;
this.checkUnseenNotifications();
this.checkUnseenNotifications(false);
this.refreshCurrentTeam();
this.hideSearch = !!document.getElementById('GlobalSearch');
this.globalSearchKey += 1;
@ -177,11 +179,13 @@ export default {
searchValue(e) {
window.open(`${this.searchUrl}?q=${e.target.value}`, '_self');
},
checkUnseenNotifications() {
checkUnseenNotifications(repeat = true) {
clearTimeout(this.unseenNotificationsTimeout);
$.get(this.unseenNotificationsUrl, (result) => {
this.unseenNotificationsCount = result.unseen;
this.unseenNotificationsTimeout = setTimeout(this.checkUnseenNotifications, 30000);
if (repeat) {
this.unseenNotificationsTimeout = setTimeout(this.checkUnseenNotifications, 30000);
}
});
},
refreshCurrentTeam() {

View file

@ -26,9 +26,9 @@
data-id="true" data-status="asset-present" :data-preview-url=this?.preview_url :href=this?.url>
{{ file_name }}
</a>
<tooltip-preview v-if="tooltipShowing && medium_preview_url" :id="id" :url="url" :file_name="file_name"
<TooltipPreview v-if="tooltipShowing && medium_preview_url" :id="id" :url="url" :file_name="file_name"
:preview_url="preview_url" :icon_html="icon_html" :medium_preview_url="medium_preview_url">
</tooltip-preview>
</TooltipPreview>
</div>
</div>
</div>
@ -44,17 +44,26 @@
{{ error }}
</div>
<input type="file" ref="fileInput" @change="handleFileChange" style="display: none" />
<Teleport to="body">
<ConfirmationModal
:title="i18n.t('repositories.modal_delete_asset_value.title')"
:description="i18n.t('repositories.modal_delete_asset_value.notice_html')"
confirmClass="btn btn-danger"
:confirmText="i18n.t('repositories.modal_delete_asset_value.delete')"
ref="deleteRepositoryAssetValueModal"
></ConfirmationModal>
</Teleport>
</div>
</template>
<script>
import TooltipPreview from './TooltipPreview.vue';
import ConfirmationModal from '../../shared/confirmation_modal.vue';
export default {
name: 'RepositoryAssetvalue',
components: {
'tooltip-preview': TooltipPreview
},
components: { TooltipPreview, ConfirmationModal },
data() {
return {
tooltipShowing: false,
@ -103,10 +112,12 @@ export default {
this.uploadFiles(event.target.files[0]);
}
},
clearFile() {
this.$refs.fileInput.value = '';
this.error = '';
this.updateCell(null);
async clearFile() {
if (await this.$refs.deleteRepositoryAssetValueModal.show()) {
this.$refs.fileInput.value = '';
this.error = '';
this.updateCell(null);
}
},
uploadFiles(file) {
this.uploading = true;

View file

@ -134,7 +134,7 @@
<ContentToolbar
v-if="orderedElements.length > 2 && insertMenu.length > 0"
:insertMenu="insertMenu"
@create:table="(...args) => this.createElement('table', ...args)"
@create:table="(args) => args ? this.createElement('table', ...args) : this.createElement('table')"
@create:text="createElement('text')"
@create:file="openLoadFromComputer"
@create:wopi_file="openWopiFileModal"

View file

@ -0,0 +1,93 @@
<template>
<div ref="container" class="w-full flex items-center flex-wrap gap-0.5">
<template v-if="!allVisible">
<div class="flex items-center gap-0.5">
<a :href="breadcrumbs[0].url" :title="breadcrumbs[0].name" class="max-w-[200px]">
<StringWithEllipsis class="w-full" :text="breadcrumbs[0].name"></StringWithEllipsis>
</a>
</div>
<div class="flex items-center gap-0.5">
<i class="sn-icon sn-icon-right text-sn-grey"></i>
<GeneralDropdown>
<template v-slot:field>
<a>...</a>
</template>
<template v-slot:flyout>
<div class="max-w-[600px]">
<div v-for="(breadcrumb, index) in hiddenBreadcrumbs" :key="index" class="p-2 hover:bg-sn-super-light-grey cursor-pointer">
<a :href="breadcrumb.url" :title="breadcrumb.name" class="max-w-[200px] hover:no-underline">
<StringWithEllipsis class="w-full" :text="breadcrumb.name"></StringWithEllipsis>
</a>
</div>
</div>
</template>
</GeneralDropdown>
<i class="sn-icon sn-icon-right text-sn-grey"></i>
</div>
</template>
<div v-for="(breadcrumb, index) in visibleBreadcrumbs" :key="index" class="flex items-center gap-0.5">
<i v-if="index > 0" class="sn-icon sn-icon-right text-sn-grey"></i>
<a :href="breadcrumb.url" :title="breadcrumb.name" class="max-w-[200px]">
<StringWithEllipsis class="w-full" :text="breadcrumb.name"></StringWithEllipsis>
</a>
</div>
</div>
</template>
<script>
import StringWithEllipsis from './string_with_ellipsis.vue';
import GeneralDropdown from './general_dropdown.vue';
export default {
name: 'Breadcrumbs',
props: {
breadcrumbs: {
type: Array,
required: true
}
},
components: {
StringWithEllipsis,
GeneralDropdown
},
data() {
return {
containerSize: 0,
breadcrumbSize: 200
};
},
computed: {
breadcrumbsVisibleCount() {
const size = Math.floor(this.containerSize / this.breadcrumbSize);
if (size < 2) {
return 2;
}
return size;
},
allVisible() {
return this.breadcrumbs.length <= this.breadcrumbsVisibleCount;
},
hiddenBreadcrumbs() {
return this.breadcrumbs.slice(1, this.breadcrumbs.length - this.breadcrumbsVisibleCount + 1);
},
visibleBreadcrumbs() {
if (this.allVisible) {
return this.breadcrumbs;
}
return this.breadcrumbs.slice(this.breadcrumbs.length - this.breadcrumbsVisibleCount + 1);
}
},
mounted() {
this.setContainerSize();
window.addEventListener('resize', this.setContainerSize);
},
beforeDestroy() {
window.removeEventListener('resize', this.setContainerSize);
},
methods: {
setContainerSize() {
this.containerSize = this.$refs.container.offsetWidth;
}
}
};
</script>;

View file

@ -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"
/>
</div>

View file

@ -34,6 +34,7 @@
@duplicate="duplicate"
@viewMode="changeViewMode"
@move="showMoveModal"
@fileVersionsModal="fileVersionsModal = true"
@menu-toggle="$emit('menu-toggle', $event)"
></MenuDropdown>
<Teleport to="body">
@ -55,6 +56,13 @@
:targets_url="attachment.attributes.urls.move_targets"
@confirm="moveAttachment($event)" @cancel="closeMoveModal"
/>
<FileVersionsModal
v-if="fileVersionsModal"
:versionsUrl="attachment.attributes.urls.versions"
:restoreVersionUrl="attachment.attributes.urls.restore_version"
@close="fileVersionsModal = false"
@fileVersionRestored="$emit('attachment:versionRestored', $event)"
/>
</Teleport>
</div>
</template>
@ -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({

View file

@ -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"

View file

@ -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"

View file

@ -1,31 +1,34 @@
<template>
<div class="sn-open-locally-menu" @mouseenter="fetchLocalAppInfo">
<div v-if="(!canOpenLocally || disableLocalOpen) && (attachment.attributes.wopi && attachment.attributes.urls.edit_asset)">
<a :href="editWopiSupported ? attachment.attributes.urls.edit_asset : null" target="_blank"
class="block whitespace-nowrap rounded px-3 py-2.5 hover:!text-sn-blue hover:no-underline cursor-pointer hover:bg-sn-super-light-grey"
:class="{ 'disabled': !editWopiSupported }"
:title="editWopiSupported ? null : attachment.attributes.wopi_context.title"
style="pointer-events: all"
>
{{ attachment.attributes.wopi_context.button_text }}
</a>
</div>
<div v-else-if="!usesWebIntegration">
<MenuDropdown
v-if="this.menu.length > 1"
class="ml-auto"
:listItems="this.menu"
:btnClasses="`btn btn-light icon-btn`"
:position="'right'"
:btnText="i18n.t('attachments.open_in')"
:caret="true"
@open-locally="openLocally"
@open-image-editor="openImageEditor"
></MenuDropdown>
<a v-else-if="menu.length === 1" class="btn btn-light" :href="menu[0].url" :target="menu[0].url_target" @click="this[this.menu[0].emit]()">
{{ menu[0].text }}
<div class="sn-open-locally-menu flex" @mouseenter="fetchLocalAppInfo">
<template v-if="canEdit">
<div v-if="(!canOpenLocally) && (attachment.attributes.wopi && attachment.attributes.urls.edit_asset)">
<a :href="editWopiSupported ? attachment.attributes.urls.edit_asset : null" target="_blank"
class="btn btn-light"
:class="{ 'disabled': !editWopiSupported }"
:title="editWopiSupported ? null : attachment.attributes.wopi_context.title"
style="pointer-events: all"
>
{{ attachment.attributes.wopi_context.button_text }}
</a>
</div>
</div>
<div v-else-if="!usesWebIntegration">
<MenuDropdown
v-if="this.menu.length > 1"
class="ml-auto"
:listItems="this.menu"
:btnClasses="`btn btn-light icon-btn`"
:position="'right'"
:btnText="i18n.t('attachments.open_in')"
:caret="true"
@open-locally="openLocally"
@open-image-editor="openImageEditor"
></MenuDropdown>
<a v-else-if="menu.length === 1" class="btn btn-light" :href="menu[0].url" :target="menu[0].url_target" @click="this[this.menu[0].emit]()">
{{ menu[0].text }}
</a>
</div>
</template>
<a @click="fileVersionsModal = true" class="btn btn-light"><i class="sn-icon sn-icon-history-search"></i>{{ i18n.t('assets.context_menu.versions') }}</a>
<Teleport to="body">
<NoPredefinedAppModal
@ -47,6 +50,13 @@
v-if="showUpdateVersionModal"
@close="showUpdateVersionModal = false"
/>
<FileVersionsModal
v-if="fileVersionsModal"
:versionsUrl="attachment.attributes.urls.versions"
:restoreVersionUrl="attachment.attributes.urls.restore_version"
@close="fileVersionsModal = false"
@fileVersionRestored="refreshPreview"
/>
</Teleport>
</div>
</template>
@ -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();
}
}
};

View file

@ -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"

View file

@ -0,0 +1,143 @@
<template>
<div ref="modal" @keydown.esc="close" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button @click="close" type="button" class="close" data-dismiss="modal" aria-label="Close"><i class="sn-icon sn-icon-close"></i></button>
<h4 class="modal-title">
{{ i18n.t("assets.file_versions_modal.title") }}
</h4>
</div>
<div class="modal-body">
<div class="relative" v-if="fileVersions">
<div v-for="(fileVersion, index) in fileVersions" :key="fileVersion.id">
<div class="flex w-full border border-sn-light-grey rounded mb-1.5 p-1.5 items-center">
<div class="basis-full">
<div class="mb-1.5">
<span v-if="fileVersion.attributes.version === 1" class="bg-sn-science-blue text-sn-white me-2 px-2 py-0.5 rounded">
{{ i18n.t("assets.file_versions_modal.original_file") }}
</span>
<span v-else class="bg-sn-grey-300 me-2 px-2 py-0.5 rounded">v{{ fileVersion.attributes.version }}</span>
<a :href="fileVersion.attributes.url" target="_blank" class="align-text-bottom">
<span :class="{
'max-w-80': !fileVersion.attributes.restored_from_version && fileVersion.attributes.version !== 1,
'max-w-64': !!fileVersion.attributes.restored_from_version || fileVersion.attributes.version === 1
}"
class="text-ellipsis overflow-hidden text-nowrap inline-block align-middle"
>{{ fileVersion.attributes.basename }}.</span><span class="inline-block align-middle">{{ fileVersion.attributes.extension }}</span>
</a>
<small class="inline-block ml-1" v-if="fileVersion.attributes.restored_from_version">
({{ i18n.t("assets.file_versions_modal.restored_from_version", { version: fileVersion.attributes.restored_from_version }) }})
</small>
</div>
<div class="flex text-xs text-sn-grey justify-start">
<div class="mr-3">{{ fileVersion.attributes.created_at }}</div>
<div v-if="fileVersion.attributes.created_by" class="mr-3 text-nowrap text-ellipsis overflow-hidden max-w-52">
{{ fileVersion.attributes.created_by.full_name }}
</div>
<div>{{ i18n.t("assets.file_versions_modal.size") }}: {{ Math.round(fileVersion.attributes.byte_size/1024) }}KB</div>
</div>
</div>
<div class="basis-1/4 flex justify-end">
<a class="btn btn-icon p-0 px-2 hover:bg-sn-light-grey"
v-if="enabled || index === 0"
:href="fileVersion.attributes.download_url"
target="_blank"
data-render-tooltip="true"
:title="i18n.t('assets.file_versions_modal.download')"
>
<i class="sn-icon sn-icon-export"></i>
</a>
<a v-if="restoreVersionUrl && index !== 0"
@click="restoreVersion(fileVersion.attributes.version)"
data-render-tooltip="true"
:title="i18n.t('assets.file_versions_modal.restore')"
class="btn btn-icon p-0 px-2 hover:bg-sn-light-grey"
>
<i class="sn-icon sn-icon-restore"></i>
</a>
</div>
</div>
</div>
<div v-if="!enabled" class="absolute bottom-0 w-full h-[150px] bg-gradient-to-t from-white to-transparent"></div>
</div>
<div v-else class="sci-loader"></div>
<div v-if="fileVersions && !enabled" class="bg-sn-super-light-blue p-4 rounded flex items-start">
<div class="mr-2">
<i class="sn-icon sn-icon-upgrade"></i>
</div>
<div>
<h3 class="mt-1 mb-2">{{ i18n.t('assets.file_versions_modal.title') }}</h3>
{{ disabledDisclaimer.text }}
</div>
</div>
</div>
<div class="modal-footer">
<button type='button' class='btn btn-secondary' @click="close">
{{ i18n.t('general.cancel') }}
</button>
<a v-if="fileVersions && !enabled" :href="enableUrl" class='btn btn-primary' target="_blank">
{{ disabledDisclaimer.button }}
</a>
</div>
</div>
</div>
</div>
</template>
<script>
import modalMixin from './modal_mixin';
import axios from '../../packs/custom_axios';
export default {
name: 'FileVersionsModal',
props: {
versionsUrl: {
type: String,
required: true
},
restoreVersionUrl: {
type: String
}
},
mixins: [modalMixin],
data() {
return {
fileVersions: null,
enabled: null,
enableUrl: null,
disabledDisclaimer: null
};
},
created() {
this.loadVersions();
},
beforeUnmount() {
document.querySelectorAll('[data-render-tooltip]').forEach((e) => {
window.destroyTooltip(e);
});
},
methods: {
loadVersions() {
axios.get(this.versionsUrl).then((response) => {
this.fileVersions = response.data.data;
this.enabled = response.data.enabled;
this.enableUrl = response.data.enable_url;
this.disabledDisclaimer = response.data.disabled_disclaimer;
this.$nextTick(() => {
document.querySelectorAll('[data-render-tooltip]').forEach((e) => {
window.initTooltip(e);
});
});
});
},
restoreVersion(version) {
axios.post(this.restoreVersionUrl, { version: version }).then(() => {
this.loadVersions();
this.$emit('fileVersionRestored');
});
}
}
};
</script>

View file

@ -44,11 +44,11 @@
<span class="sci-radio-label"></span>
</div>
<span>{{ i18n.t('storage_locations.index.edit_modal.grid') }}</span>
<div class="sci-input-container-v2 !w-28">
<div class="sci-input-container-v2 !w-28" v-if="object.metadata.dimensions">
<input type="number" :disabled="!canChangeGrid" v-model="object.metadata.dimensions[0]" min="1" max="24">
</div>
<i class="sn-icon sn-icon-close-small"></i>
<div class="sci-input-container-v2 !w-28">
<div class="sci-input-container-v2 !w-28" v-if="object.metadata.dimensions">
<input type="number" :disabled="!canChangeGrid" v-model="object.metadata.dimensions[1]" min="1" max="24">
</div>
</div>

View file

@ -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|

View file

@ -2,6 +2,7 @@
class NotificationCleanupJob < ApplicationJob
def perform
NewRelic::Agent.ignore_transaction
Notification.where('created_at < ?', 3.months.ago).delete_all
end
end

View file

@ -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

View file

@ -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

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
require 'caracal'
module Reports
class DocxJob < ApplicationJob
extend InputSanitizeHelper

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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?

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 ' \

View file

@ -8,4 +8,12 @@ class SoftLockedRepository < Repository
def shareable_write?
false
end
def unlocked?
@unlocked == true
end
def unlock!
@unlocked = true
end
end

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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)

Some files were not shown because too many files have changed in this diff Show more