Merge branch 'features/custom-docx-reports' of github.com:scinote-eln/scinote-web into features/custom-docx-reports

This commit is contained in:
Anton 2024-11-14 11:35:52 +01:00
commit 1f1ab08001
53 changed files with 601 additions and 291 deletions

View file

@ -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_the_curve', '~> 1.4', '>= 1.4.6'
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

@ -14,6 +14,16 @@ GIT
docile (>= 1.1.0)
rails (>= 4)
GIT
remote: https://github.com/scinote-eln/caracal.git
revision: 54c21353798569476a1eaa73b5fd3e275ac85419
branch: custom-docx-reports
specs:
caracal (1.4.2)
nokogiri (~> 1.6)
rubyzip (>= 2.3)
tilt (>= 1.4)
GIT
remote: https://github.com/scinote-eln/img2zpl
revision: 23d61cfc3e90ac4caa62dd08546fa0d7590a5140
@ -210,10 +220,6 @@ GEM
capybara-email (3.0.2)
capybara (>= 2.4, < 4.0)
mail
caracal_the_curve (1.4.6)
nokogiri (~> 1.6)
rubyzip (>= 1.1.0, < 3.0)
tilt (>= 1.4)
case_transform (0.2)
activesupport
caxlsx (4.0.0)
@ -790,7 +796,7 @@ DEPENDENCIES
canaid!
capybara
capybara-email
caracal_the_curve (~> 1.4, >= 1.4.6)
caracal!
caxlsx
cssbundling-rails
cucumber-rails

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] = {
@ -1046,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;
}
@ -1256,7 +1278,8 @@ function reportHandsonTableConverter() {
function reCheckContinueButton() {
if (dropdownSelector.getValues('#projectSelector').length > 0
&& dropdownSelector.getValues('#templateSelector').length > 0
&& dropdownSelector.getValues('#docxTemplateSelector').length > 0) {
&& (dropdownSelector.getValues('#docxTemplateSelector').length > 0
|| $('#docxTemplateSelector').closest('.hidden').length > 0)) {
$('.continue-button').attr('disabled', false);
} else {
$('.continue-button').attr('disabled', true);
@ -1279,6 +1302,12 @@ 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');
@ -1349,10 +1378,24 @@ function reportHandsonTableConverter() {
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
@ -1382,7 +1425,7 @@ function reportHandsonTableConverter() {
}
function loadDocxTemplate() {
let template = $('#docxTemplateSelector').val();
const template = dropdownSelector.getValues('#docxTemplateSelector');
let params = {
project_id: dropdownSelector.getValues('#projectSelector'),
template: template

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

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

@ -2,10 +2,9 @@
module Reports
class RepositoriesInputComponent < TemplateValueComponent
def initialize(report:, name:, label:, placeholder: nil, editing: true, displayed_field: :name)
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.accessible_by_teams(report.team).sort_by { |r| r.name.downcase }
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)

View file

@ -18,7 +18,8 @@ class ReportsController < ApplicationController
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
@ -44,6 +45,8 @@ class ReportsController < ApplicationController
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
@ -69,6 +72,7 @@ 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
@ -77,6 +81,8 @@ class ReportsController < ApplicationController
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
@ -102,6 +108,7 @@ 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
@ -363,6 +370,9 @@ 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 check_project_read_permissions
@ -430,6 +440,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

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

@ -119,4 +119,8 @@ module ReportsHelper
end
end
end
def custom_templates(templates)
templates.any? { |template, _| template != :scinote_template }
end
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)

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,7 +64,8 @@ class Report < ApplicationRecord
result_comments: true,
result_order: 'new',
activities: true,
repositories: []
repositories: [],
excluded_repository_columns: {}
}
}.freeze
@ -124,4 +127,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

@ -30,7 +30,7 @@ class Reports::Docx
@link_style = {}
@color = {}
@scinote_url = options[:scinote_url][0..-2]
@template = @settings[:docx_template] || 'scinote_template'
@template = @settings[:docx_template].presence || 'scinote_template'
extend "#{@template.camelize}Docx".constantize
end

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

@ -4,6 +4,7 @@ module Reports::Docx::DrawMyModule
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,10 +75,13 @@ module Reports::Docx::DrawMyModule
filter_steps_for_report(my_module.protocol.steps, @settings).order(:position).each do |step|
draw_step(step)
end
draw_results(my_module) unless without_results
@docx.p
unless without_results
draw_results(my_module)
@docx.p
end
subject.children.active.each do |child|
next if without_repositories && child.type_of == 'my_module_repository'

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

@ -1,19 +1,31 @@
# frozen_string_literal: true
module Reports::Docx::DrawResults
def draw_results(my_module)
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?
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]
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|

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

View file

@ -5,6 +5,7 @@ module Reports::Docx::DrawStepAsset
timestamp = asset.created_at
asset_url = Rails.application.routes.url_helpers.asset_download_url(asset)
color = @color
settings = @settings
@docx.p
begin
@ -22,9 +23,11 @@ module Reports::Docx::DrawStepAsset
link I18n.t('projects.reports.elements.download'), asset_url do
italic true
end
text ' '
text I18n.t('projects.reports.elements.step_asset.user_time',
timestamp: I18n.l(timestamp, format: :full)), color: color[:gray]
unless settings['exclude_timestamps']
text ' '
text I18n.t('projects.reports.elements.step_asset.user_time',
timestamp: I18n.l(timestamp, format: :full)), color: color[:gray]
end
end
end
end

View file

@ -4,6 +4,7 @@ module Reports::Docx::DrawStepChecklist
def draw_step_checklist(checklist)
team = @report_team
user = @user
settings = @settings
items = checklist.checklist_items
timestamp = checklist.created_at
@ -15,9 +16,11 @@ module Reports::Docx::DrawStepChecklist
team,
I18n.t('projects.reports.elements.step_checklist.checklist_name', name: checklist.name)
).text, italic: true
text ' '
text I18n.t('projects.reports.elements.step_checklist.user_time',
timestamp: I18n.l(timestamp, format: :full)), color: color[:gray]
unless settings['exclude_timestamps']
text ' '
text I18n.t('projects.reports.elements.step_checklist.user_time',
timestamp: I18n.l(timestamp, format: :full)), color: color[:gray]
end
end
if items.any?
@docx.ul do

View file

@ -8,8 +8,10 @@ module Reports::Docx::DrawStepComments
@docx.p
@docx.p I18n.t('projects.reports.elements.step_comments.name', step: step.name),
bold: true, size: Constants::REPORT_DOCX_STEP_ELEMENTS_TITLE_SIZE
comments.each do |comment|
comments.find_each.with_index do |comment, index|
comment_ts = comment.created_at
@docx.p unless index.zero?
@docx.p I18n.t('projects.reports.elements.step_comments.comment_prefix',
user: comment.user.full_name,
date: I18n.l(comment_ts, format: :full_date),
@ -17,7 +19,6 @@ module Reports::Docx::DrawStepComments
html = custom_auto_link(comment.message, team: @report_team)
Reports::HtmlToWordConverter.new(@docx, { scinote_url: @scinote_url,
link_style: @link_style }).html_to_word_converter(html)
@docx.p
end
end
end

View file

@ -4,6 +4,7 @@ module Reports::Docx::DrawStepTable
def draw_step_table(table, table_type)
color = @color
timestamp = table.created_at
settings = @settings
obj = self
table_data = JSON.parse(table.contents_utf_8)['data']
table_data = obj.add_headers_to_table(table_data, table_type == 'step_well_plates_table')
@ -38,9 +39,11 @@ module Reports::Docx::DrawStepTable
end
@docx.p do
text I18n.t("projects.reports.elements.#{table_type}.table_name", name: table.name), italic: true
text ' '
text I18n.t("projects.reports.elements.#{table_type}.user_time",
timestamp: I18n.l(timestamp, format: :full)), color: color[:gray]
unless settings['exclude_timestamps']
text ' '
text I18n.t("projects.reports.elements.#{table_type}.user_time",
timestamp: I18n.l(timestamp, format: :full)), color: color[:gray]
end
end
end
end

View file

@ -5,11 +5,18 @@ module Reports::Docx::DrawStepText
step_text = element.orderable
timestamp = element.created_at
color = @color
@docx.p do
text step_text.name.presence || '', italic: true
text ' '
text I18n.t('projects.reports.elements.result_text.user_time',
timestamp: I18n.l(timestamp, format: :full)), color: color[:gray]
settings = @settings
if step_text.name.present? || !settings['exclude_timestamps']
@docx.p do
text step_text.name.to_s, italic: true
text ' ' if step_text.name.present?
unless settings['exclude_timestamps']
text I18n.t('projects.reports.elements.result_text.user_time',
timestamp: I18n.l(timestamp, format: :full)), color: color[:gray]
end
end
end
if step_text.text.present?
html = custom_auto_link(step_text.text, team: @report_team)

View file

@ -4,15 +4,20 @@ module Reports::Docx::RepositoryHelper
include InputSanitizeHelper
include ActionView::Helpers::NumberHelper
def prepare_row_columns_for_docx(repository_data, my_module = nil, repository = nil)
return if repository_data[:headers].blank?
result = [repository_data[:headers]]
excluded_columns = repository_data[:excluded_columns]
repository_data[:rows].each do |record|
row = []
row.push(record.code)
row.push(escape_input(record.archived ? "#{record.name} [#{I18n.t('general.archived')}]" : record.name))
row.push(I18n.l(record.created_at, format: :full))
row.push(escape_input(record.created_by.full_name))
row.push(record.code) unless excluded_columns.include?(-1)
unless excluded_columns.include?(-2)
row.push(escape_input(record.archived ? "#{record.name} [#{I18n.t('general.archived')}]" : record.name))
end
row.push(I18n.l(record.created_at, format: :full)) unless excluded_columns.include?(-3)
row.push(escape_input(record.created_by.full_name)) unless excluded_columns.include?(-4)
cell_values = {}
custom_cells = record.repository_cells
@ -39,6 +44,8 @@ module Reports::Docx::RepositoryHelper
end
repository_data[:custom_columns].each do |column_id|
next if excluded_columns.include?(column_id)
value = cell_values[column_id]
row.push(value)
end

View file

@ -130,6 +130,13 @@ module Reports
row[:data].each do |cell|
docx_cell = Caracal::Core::Models::TableCellModel.new do |c|
cell.each do |content|
c.background content[:style][:background] if content.dig(:style, :background).present?
if content.dig(:style, :vertical_align).present? && content[:style][:vertical_align] != :middle
c.vertical_align content[:style][:vertical_align]
else
c.vertical_align :center
end
if content[:type] == 'p'
Reports::DocxRenderer.render_p_element(c, content, options.merge({ skip_br: true }))
elsif content[:type] == 'table'

View file

@ -208,7 +208,7 @@ module Reports
if style
style_keys.each do |key|
style_el = style.value.split(';').select { |i| (i.include? key) }[0]
style_el = style.value.split(';').find { |i| i.strip.start_with?(key) }
next unless style_el
value = style_el.split(':')[1].strip if style_el
@ -259,6 +259,29 @@ module Reports
}
end
def table_cell_styling(elem)
style = elem.attributes['style']
result = {}
style_keys = %w(background-color vertical-align background)
if style
style_keys.each do |key|
style_el = style.value.split(';').find { |i| (i.include? key) }
next unless style_el
value = style_el.split(':')[1].strip if style_el
case key
when 'background-color', 'background'
result[:background] = normalized_hex_color(value)
when 'vertical-align'
result[:vertical_align] = value.to_sym
end
end
end
result
end
def tiny_mce_table_element(table_element)
# array of elements
rows = table_element.css('tbody').first.children.map do |row|
@ -267,11 +290,13 @@ module Reports
cells = row.children.map do |cell|
next unless cell.name == 'td'
style = table_cell_styling(cell)
# Parse cell content
formated_cell = recursive_children(cell.children, [], true)
# Combine text elements to single paragraph
formated_cell = combine_docx_elements(formated_cell)
formated_cell.each { |element| element[:style] = style } if style.present?
formated_cell
end.reject(&:blank?)
{ type: 'tr', data: cells }

View file

@ -1,6 +1,10 @@
<div class="template-editor-header">
<h1 class="title">
<%= t('projects.reports.wizard.first_step.values_editor.title') %>
<% if @type == :pdf %>
<%= t('projects.reports.wizard.first_step.values_editor.title_pdf', template: @template_name) %>
<% else %>
<%= t('projects.reports.wizard.first_step.values_editor.title_docx', template: @template_name) %>
<% end %>
</h1>
<div class="collapse-buttons sci-btn-group pull-right">
<button class="btn btn-light collapse-all">

View file

@ -1,57 +0,0 @@
<% content_for :toc do %>
<h3 class="mb-4">Chapter 1</h3>
<div class="pl-6 flex flex-col gap-2">
<%= render Reports::CheckboxInputComponent.new(report: report, name: :custom_docx_sub_chapter_1, label: 'Sub chapter 1') %>
<%= render Reports::CheckboxInputComponent.new(report: report, name: :custom_docx_sub_chapter_2, label: 'Sub chapter 2') %>
</div>
<h3 class="mb-4">Chapter 2</h3>
<div class="pl-6 flex flex-col gap-2">
<div>
<%= render Reports::CheckboxInputComponent.new(report: report, name: :custom_docx_sub_chapter_3, label: 'Sub chapter 3 with inventory') %>
<div class="pl-6 mb-4">
<%= render Reports::RepositoriesInputComponent.new(report: report, name: 'custom_docx_sub_chapter_3_inventories[]', label: 'Inventories') %>
</div>
</div>
<div>
<%= render Reports::CheckboxInputComponent.new(report: report, name: :custom_docx_sub_chapter_4, label: 'Sub chapter 4 with inventory') %>
<div class="pl-6 mb-4">
<%= render Reports::RepositoriesInputComponent.new(report: report, name: 'custom_docx_sub_chapter_4_inventories[]', label: 'Inventories') %>
</div>
</div>
<%= render Reports::CheckboxInputComponent.new(report: report, name: :custom_docx_sub_chapter_5, label: 'Sub chapter 5') %>
<%= render Reports::CheckboxInputComponent.new(report: report, name: :custom_docx_sub_chapter_6, label: 'Sub chapter 6') %>
<%= render Reports::CheckboxInputComponent.new(report: report, name: :custom_docx_sub_chapter_7, label: 'Sub chapter 7') %>
</div>
<h3 class="mb-4">Chapter 3</h3>
<div class="pl-6 flex flex-col gap-2">
<%= render Reports::CheckboxInputComponent.new(report: report, name: :custom_docx_sub_chapter_8, label: 'Sub chapter 8 with task') %>
<%= render Reports::CheckboxInputComponent.new(report: report, name: :custom_docx_sub_chapter_9, label: 'Sub chapter 9') %>
<%= render Reports::CheckboxInputComponent.new(report: report, name: :custom_docx_sub_chapter_10, label: 'Sub chapter 10') %>
<%= render Reports::CheckboxInputComponent.new(report: report, name: :custom_docx_sub_chapter_11, label: 'Sub chapter 11') %>
<%= render Reports::CheckboxInputComponent.new(report: report, name: :custom_docx_sub_chapter_12, label: 'Sub chapter 12') %>
<div class="pl-6 flex flex-col gap-2">
<%= render Reports::CheckboxInputComponent.new(report: report, name: :custom_docx_sub_sub_chapter_1, label: 'Sub sub chapter 1') %>
<%= render Reports::CheckboxInputComponent.new(report: report, name: :custom_docx_sub_sub_chapter_2, label: 'Sub sub chapter 2') %>
</div>
</div>
<h3 class="mb-4">Chapter 4</h3>
<div class="pl-6 flex flex-col gap-2">
<%= render Reports::CheckboxInputComponent.new(report: report, name: :custom_docx_sub_chapter_13, label: 'Sub chapter 13 with results') %>
<%= render Reports::CheckboxInputComponent.new(report: report, name: :custom_docx_sub_chapter_14, label: 'Sub chapter 14') %>
<%= render Reports::CheckboxInputComponent.new(report: report, name: :custom_docx_sub_chapter_15, label: 'Sub chapter 15') %>
<%= render Reports::CheckboxInputComponent.new(report: report, name: :custom_docx_sub_chapter_16, label: 'Sub chapter 16') %>
<%= render Reports::CheckboxInputComponent.new(report: report, name: :custom_docx_sub_chapter_17, label: 'Sub chapter 17') %>
</div>
<h3 class="mb-4">Chapter 5</h3>
<% end %>
<% content_for :cover do %>
<%= render Reports::TextInputComponent.new(report: report, name: :custom_docx_report_name, label: 'Report name') %>
<%= render Reports::TextInputComponent.new(report: report, name: :custom_docx_report_number, label: 'Report number') %>
<%= render Reports::ProjectMembersInputComponent.new(report: report, name: 'custom_docx_author[]', label: 'Author') %>
<%= render Reports::TextInputComponent.new(report: report, name: :custom_docx_author_role, label: 'Author Role') %>
<%= render Reports::ProjectMembersInputComponent.new(report: report, name: 'custom_docx_reviewer[]', label: 'Reviewer') %>
<%= render Reports::TextInputComponent.new(report: report, name: :custom_docx_reviewer_role, label: 'Reviewer Role') %>
<% end %>

View file

@ -1 +0,0 @@
Custom Template

View file

@ -9,9 +9,11 @@
<span class="label label-warning"><%= t('search.index.archived') %></span>
<% end %>
</h3>
<div class="user-time">
<%= t('projects.reports.elements.experiment.user_time', code: experiment.code, timestamp: l(timestamp, format: :full)) %>
</div>
<% unless @settings['exclude_timestamps'] %>
<div class="user-time">
<%= t('projects.reports.elements.experiment.user_time', code: experiment.code, timestamp: l(timestamp, format: :full)) %>
</div>
<% end %>
<% if experiment.description.present? %>
<%= custom_auto_link(experiment.description, team: current_team, base64_encoded_imgs: export_all) %>
<% end %>

View file

@ -9,45 +9,49 @@
<span class="label label-warning"><%= t('search.index.archived') %></span>
<% end %>
</h4>
<div class="user-time">
<%= t('projects.reports.elements.module.user_time', code: my_module.code, timestamp: l(timestamp, format: :full)) %>
</div>
<p class="module-start-date">
<% if my_module.started_on.present? %>
<%= t('projects.reports.elements.module.started_on', started_on: l(my_module.started_on, format: :full)) %>
<% end %>
</p>
<p class="module-due-date">
<% if my_module.due_date.present? %>
<%= t('projects.reports.elements.module.due_date', due_date: l(my_module.due_date, format: :full)) %>
<% end %>
</p>
<p class="module-status">
<% status = my_module.my_module_status %>
<%= t('projects.reports.elements.module.status') %>
<span class="status-block" style="background: <%= status.color %>;
<%= 'color: #000000; border: 1px solid #D0D5DD;' if status.light_color? %>">
<%= status.name %>
</span>
<% if my_module.completed? %>
<span style="margin-left: 10px;">
<%= t('my_modules.states.completed') %>
<%= l(my_module.completed_on, format: :full) %>
</span>
<% end %>
</p>
<div class="row module-tags">
<div class="pull-left">
<%= t('projects.reports.elements.module.tags_header') %>
<% unless @settings['exclude_timestamps'] %>
<div class="user-time">
<%= t('projects.reports.elements.module.user_time', code: my_module.code, timestamp: l(timestamp, format: :full)) %>
</div>
<% if (tags = my_module.tags.order(:id)).present? %>
<% tags.each do |tag| %>
<div class="pull-left module-tag" style="background-color: <%= tag.color %>;">
<%= tag.name %>
</div>
<% end %>
<% unless @settings['exclude_task_metadata'] %>
<p class="module-start-date">
<% if my_module.started_on.present? %>
<%= t('projects.reports.elements.module.started_on', started_on: l(my_module.started_on, format: :full)) %>
<% end %>
<% end %>
</div>
</p>
<p class="module-due-date">
<% if my_module.due_date.present? %>
<%= t('projects.reports.elements.module.due_date', due_date: l(my_module.due_date, format: :full)) %>
<% end %>
</p>
<p class="module-status">
<% status = my_module.my_module_status %>
<%= t('projects.reports.elements.module.status') %>
<span class="status-block" style="background: <%= status.color %>;
<%= 'color: #000000; border: 1px solid #D0D5DD;' if status.light_color? %>">
<%= status.name %>
</span>
<% if my_module.completed? %>
<span style="margin-left: 10px;">
<%= t('my_modules.states.completed') %>
<%= l(my_module.completed_on, format: :full) %>
</span>
<% end %>
</p>
<div class="row module-tags">
<div class="pull-left">
<%= t('projects.reports.elements.module.tags_header') %>
</div>
<% if (tags = my_module.tags.order(:id)).present? %>
<% tags.each do |tag| %>
<div class="pull-left module-tag" style="background-color: <%= tag.color %>;">
<%= tag.name %>
</div>
<% end %>
<% end %>
</div>
<% end %>
<div class="row">
<div class="col-xs-12">
<% if my_module.description.present? %>
@ -90,9 +94,11 @@
<% if @settings.dig('task', 'file_results') %>
<%= render partial: 'reports/elements/my_module_result_asset_element', locals: { result: result, report: report, export_all: export_all } %>
<div class="user-time">
<%= t('projects.reports.elements.result.user_time', user: result.user.full_name, timestamp: l(result.created_at, format: :full)) %>
</div>
<% unless @settings['exclude_timestamps'] %>
<div class="user-time">
<%= t('projects.reports.elements.result.user_time', user: result.user.full_name, timestamp: l(result.created_at, format: :full)) %>
</div>
<% end %>
<% end %>
<div class="report-element-children">

View file

@ -9,9 +9,11 @@
<%= t('projects.reports.elements.module.protocol.name') %>
<% end %>
</h4>
<div class="user-time">
<%= t('projects.reports.elements.module.protocol.user_time', code: protocol.original_code, timestamp: l(protocol.created_at, format: :full)) %>
</div>
<% unless @settings['exclude_timestamps'] %>
<div class="user-time">
<%= t('projects.reports.elements.module.protocol.user_time', code: protocol.original_code, timestamp: l(protocol.created_at, format: :full)) %>
</div>
<% end %>
<div class="row module-protocol-description">
<% if @settings.dig('task', 'protocol', 'description') && protocol.description.present? %>
<%= custom_auto_link(protocol.prepare_for_report(:description, export_all: export_all),

View file

@ -24,9 +24,11 @@
<%= link_to t('projects.reports.elements.download'), asset_download_url(asset, disposition: 'attachment'), class: 'download-link', target: :_blank %>
</em>
<% end %>
</div>
</div>
<div class="user-time">
<%= t("projects.reports.elements.result_asset.user_time", user: result.user.full_name, timestamp: l(timestamp, format: :full)) %>
<% unless @settings['exclude_timestamps'] %>
<%= t("projects.reports.elements.result_asset.user_time", user: result.user.full_name, timestamp: l(timestamp, format: :full)) %>
<% end %>
<% if report.settings.dig(:task, :file_results_previews) && ActiveStorageFileUtil.previewable_document?(asset&.file&.blob) %>
<%= t('projects.reports.elements.result_asset.full_preview_attached') %>
<% end %>

View file

@ -21,9 +21,11 @@
<% end %>
<% end %>
</div>
<div class="user-time">
<%= t('projects.reports.elements.result_table.user_time', timestamp: l(timestamp, format: :full)) %>
</div>
<% unless @settings['exclude_timestamps'] %>
<div class="user-time">
<%= t('projects.reports.elements.result_table.user_time', timestamp: l(timestamp, format: :full)) %>
</div>
<% end %>
</div>
<div class="report-element-body">
<input type="hidden" class="hot-table-contents" value="<%= table.contents_utf_8.gsub(/\</, '&lt;').gsub(/\>/, '&gt;') %>" />

View file

@ -11,9 +11,11 @@
</em>
<% end %>
</div>
<div class="user-time">
<%= t("projects.reports.elements.result_text.user_time", timestamp: l(timestamp, format: :full)) %>
</div>
<% unless @settings['exclude_timestamps'] %>
<div class="user-time">
<%= t("projects.reports.elements.result_text.user_time", timestamp: l(timestamp, format: :full)) %>
</div>
<% end %>
</div>
<div class="report-element-body">
<div class="row">

View file

@ -8,11 +8,15 @@
<div class="report-element-body">
<h5 class="step-name">
<b><%= t('projects.reports.elements.step.step_pos', pos: (step.position_plus_one)) %></b>&nbsp;<%= step.name %>
<%= step_status_label(step) %>
<% unless @settings['exclude_task_metadata'] %>
<%= step_status_label(step) %>
<% end %>
</h5>
<div class="user-time">
<%= t("projects.reports.elements.step.#{step_type_str}.user_time", user: user.full_name , timestamp: l(timestamp, format: :full)) %>
</div>
<% unless @settings['exclude_timestamps'] %>
<div class="user-time">
<%= t("projects.reports.elements.step.#{step_type_str}.user_time", user: user.full_name , timestamp: l(timestamp, format: :full)) %>
</div>
<% end %>
</div>
<div class="report-element-children">
<% step.step_orderable_elements.order(:position).each do |e| %>

View file

@ -7,9 +7,11 @@
<span class="label label-warning"><%= t('search.index.archived') %></span>
<% end %>
</h2>
<div class="user-time">
<%= t('projects.reports.elements.project_header.user_time', code: project.code, timestamp: l(project.created_at, format: :full)) %>
</div>
<% unless @settings['exclude_timestamps'] %>
<div class="user-time">
<%= t('projects.reports.elements.project_header.user_time', code: project.code, timestamp: l(project.created_at, format: :full)) %>
</div>
<% end %>
</div>
<% if defined?(children) %>
<div class="report-element-children">

View file

@ -18,9 +18,11 @@
</em>
<% end %>
</span>
<div class="user-time">
&nbsp;<%= t('projects.reports.elements.step_asset.user_time', timestamp: l(timestamp, format: :full)) %>
</div>
<% unless @settings['exclude_timestamps'] %>
<div class="user-time">
&nbsp;<%= t('projects.reports.elements.step_asset.user_time', timestamp: l(timestamp, format: :full)) %>
</div>
<% end %>
</div>
<div class="report-element-body">
<% if asset.previewable? && !asset.list? %>

View file

@ -9,9 +9,11 @@
team: current_team,
base64_encoded_imgs: export_all) %>
</div>
<div class="user-time">
<%= t('projects.reports.elements.step_checklist.user_time', timestamp: l(timestamp, format: :full)) %>
</div>
<% unless @settings['exclude_timestamps'] %>
<div class="user-time">
<%= t('projects.reports.elements.step_checklist.user_time', timestamp: l(timestamp, format: :full)) %>
</div>
<% end %>
</div>
<div class="report-element-body">
<% items.each do |item| %>

View file

@ -18,9 +18,11 @@
<% end %>
<% end %>
</div>
<div class="user-time">
<%= t("projects.reports.elements.#{table_type}.user_time", timestamp: l(timestamp, format: :full)) %>
</div>
<% unless @settings['exclude_timestamps'] %>
<div class="user-time">
<%= t("projects.reports.elements.#{table_type}.user_time", timestamp: l(timestamp, format: :full)) %>
</div>
<% end %>
</div>
<div class="report-element-body">
<input type="hidden" class="hot-table-contents" value="<%= table.contents_utf_8.gsub(/\</, '&lt;').gsub(/\>/, '&gt;') %>" />

View file

@ -8,9 +8,11 @@
</em>
<% end %>
</div>
<div class="user-time">
<%= t("projects.reports.elements.step_text.user_time", timestamp: l(timestamp, format: :full)) %>
</div>
<% unless @settings['exclude_timestamps'] %>
<div class="user-time">
<%= t("projects.reports.elements.step_text.user_time", timestamp: l(timestamp, format: :full)) %>
</div>
<% end %>
</div>
<div class="report-element-body">
<% if step_text.text.present? %>

View file

@ -1,5 +1,9 @@
<h1>
<%= t('projects.reports.wizard.first_step.values_editor.no_values_title') %>
<% if type == :pdf %>
<%= t('projects.reports.wizard.first_step.values_editor.no_values_title_pdf', template: ) %>
<% else %>
<%= t('projects.reports.wizard.first_step.values_editor.no_values_title_docx', template: ) %>
<% end %>
</h1>
<h3>
<%= t('projects.reports.wizard.first_step.values_editor.no_values_description') %>

View file

@ -20,10 +20,11 @@
disable_on_load: report.settings[:template].blank? && report.new_record?,
placeholder: t('projects.reports.wizard.first_step.select_template'),
selected_template: report.settings[:template],
default_template: @default_template,
values_editor_path: reports_new_template_values_path(report_id: report.id)
} %>
</div>
<div class='template-selector'>
<div class='template-selector <%= "hidden" unless custom_templates(Extends::DOCX_REPORT_TEMPLATES) %>'>
<%= label_tag :docxTemplateSelector, t('projects.reports.wizard.first_step.select_docx_template') %>
<%= select_tag :docxTemplateSelector,
options_for_select(@docx_templates.invert, @active_docx_template),
@ -32,6 +33,7 @@
disable_on_load: report.settings[:docx_template].blank? && report.new_record?,
placeholder: t('projects.reports.wizard.first_step.select_docx_template'),
selected_template: report.settings[:docx_template],
default_template: @default_docx_template,
values_editor_path: reports_new_docx_template_values_path(report_id: report.id)
} %>
</div>

View file

@ -55,7 +55,10 @@
</span><br>
<span class="repositories-items-description">
<i class="sn-icon sn-icon-info"></i>
<span><%= t("projects.reports.wizard.third_step.assigned_items_description") %></span>
<div class="flex-col font-normal">
<p><%= t("projects.reports.wizard.third_step.assigned_items_description") %></p>
<p><%= t("projects.reports.wizard.third_step.assigned_items_repository_items_description_html") %></p>
</div>
</span>
<ul class="collapse in" id="repositoriesContents">
<li>
@ -68,25 +71,58 @@
<ul class="repositories-contents">
<% @repositories.each do |repository| %>
<li>
<span class="sci-checkbox-container">
<input type="checkbox"
class="sci-checkbox repositories-setting"
value="<%= repository.is_a?(RepositorySnapshot) ? repository.parent_id : repository.id %>"
<%= 'checked' if report.new_record? ||
@project_contents[:repositories].include?(repository.id) ||
(repository.is_a?(Repository) && repository.repository_snapshots.exists?(id: @project_contents[:repositories])) %> />
<span class="sci-checkbox-label"></span>
</span>
<%= repository.name %>
<% if repository.archived? %>
<span class="archived">
<%= t("projects.reports.wizard.third_step.archived") %>
<div class="flex items-center">
<span class="sci-checkbox-container">
<input type="checkbox"
class="sci-checkbox repositories-setting"
value="<%= repository.is_a?(RepositorySnapshot) ? repository.parent_id : repository.id %>"
<%= 'checked' if report.new_record? ||
@project_contents[:repositories].include?(repository.id) ||
(repository.is_a?(Repository) && repository.repository_snapshots.exists?(id: @project_contents[:repositories])) ||
(repository.is_a?(RepositorySnapshot) && @project_contents[:repositories].include?(repository.parent_id)) %> />
<span class="sci-checkbox-label"></span>
</span>
<% elsif repository.is_a?(RepositorySnapshot) %>
<span class="deleted">
<%= t("projects.reports.wizard.third_step.deleted") %>
</span>
<% end %>
<%= repository.name %>
<% if repository.archived? %>
<span class="archived">
<%= t("projects.reports.wizard.third_step.archived") %>
</span>
<% elsif repository.is_a?(RepositorySnapshot) %>
<span class="deleted">
<%= t("projects.reports.wizard.third_step.deleted") %>
</span>
<% end %>
<select class="repository-columns"
id=<%= "repository-#{repository.id}" %>
data-combine-tags="true"
data-placeholder="<%= t("projects.reports.wizard.third_step.repository_column.placeholder") %>"
data-select-multiple-all-selected="<%= t("projects.reports.wizard.third_step.repository_column.all_selected") %>"
data-select-multiple-name="<%= t("projects.reports.wizard.third_step.repository_column.selected") %>"
data-select-all-button="<%= t("projects.reports.wizard.third_step.repository_column.select_deselect_all") %>"
multiple
>
<% Report.default_repository_columns.each do |key, value| %>
<option value="<%= key %>"
selected-value="<%= report.settings.dig(:task,
:excluded_repository_columns,
(repository.is_a?(RepositorySnapshot) ? repository.parent_id : repository.id).to_s)
&.include?(key.to_s.to_i) ? '' : 'selected' %>"
>
<%= value %>
</option>
<% end %>
<% repository.repository_columns.find_each do |repository_column| %>
<option value="<%= repository_column.id %>"
selected-value="<%= report.settings.dig(:task,
:excluded_repository_columns,
(repository.is_a?(RepositorySnapshot) ? repository.parent_id : repository.id).to_s)
&.include?(repository_column.id.to_i) ? '' : 'selected' %>"
>
<%= repository_column.name %>
</option>
<% end %>
</select>
</div>
<div class="divider"></div>
</li>
<% end %>
@ -184,7 +220,7 @@
<%= t("projects.reports.wizard.third_step.additional_content") %>
</span>
<ul class="additional-contents collapse in" id="additionalContents">
<li>
<li class="additional-content-task-activity">
<div class="select-all-container">
<span class="sci-checkbox-container">
<input type="checkbox" class="sci-checkbox task-setting" value="activities" <%= 'checked' if report.settings.dig(:task, :activities) %>/>
@ -194,6 +230,26 @@
<div class="divider"></div>
</div>
</li>
<li>
<div class="select-all-container">
<span class="sci-checkbox-container">
<input type="checkbox" class="sci-checkbox exclude-task-metadata-setting" value="exclude_task_metadata" <%= 'checked' if report.settings.dig(:exclude_task_metadata) %>/>
<span class="sci-checkbox-label"></span>
</span>
<%= t("projects.reports.wizard.third_step.exclude_task_metadata") %>
<div class="divider"></div>
</div>
</li>
<li>
<div class="select-all-container">
<span class="sci-checkbox-container">
<input type="checkbox" class="sci-checkbox exclude-timestamps-setting" value="exclude_timestamps" <%= 'checked' if report.settings.dig(:exclude_timestamps) %>/>
<span class="sci-checkbox-label"></span>
</span>
<%= t("projects.reports.wizard.third_step.exclude_timestamps") %>
<div class="divider"></div>
</div>
</li>
</ul>
</ul>
</div>

42
bin/load_report_templates Executable file
View file

@ -0,0 +1,42 @@
#!/usr/bin/env ruby
require 'httparty'
require 'zip'
template_zip_url_string = ENV.fetch('REPORT_TEMPLATES_ZIP_URL', nil)
return if template_zip_url_string.nil? || template_zip_url_string.empty?
template_zip_url = URI.parse(template_zip_url_string)
contents = case template_zip_url.scheme
when 'https'
HTTParty.get(template_zip_url).body
when 's3'
system("AWS_PAGER=\"\" aws s3api get-object --bucket #{template_zip_url.host} --key #{template_zip_url.path[1..]} #{ENV.fetch('APP_HOME', '.')}/app/views/reports/report_templates.zip")
File.read("#{ENV.fetch('APP_HOME', '.')}/app/views/reports/report_templates.zip")
end
puts "Loaded report templates zip from #{template_zip_url_string}"
root_folder = nil
Zip::File.open_buffer(StringIO.new(contents)) do |zip|
puts 'Extracting report templates...'
zip.each do |entry|
# set root zip folder
root_folder = entry.name and next if entry.name.count('/') == 1
# create path while omitting root zip folder
path = Pathname.new("#{ENV.fetch('APP_HOME', '.')}/app/views/reports/#{entry.name.sub(root_folder, '')}")
path.dirname.mkpath
# don't try and write file if entry is a folder
next if entry.name.end_with?('/')
path.open('wb') do |f|
f.write(entry.get_input_stream.read)
end
puts "Extracted #{path}"
end
end

View file

@ -40,7 +40,6 @@ development:
# The password associated with the postgres role (username).
# password: mysecretpassword
# Connect on a TCP socket. Omitted by default since the client uses a
# domain socket that doesn't need configuration. Windows does not have
# domain sockets, so uncomment these lines.

View file

@ -815,13 +815,15 @@ en:
report_description: "Report description (optional)"
report_description_placeholder: "In this report you can see..."
values_editor:
title: "Enter template data"
title_pdf: "PDF %{template}"
title_docx: "DOCX %{template}"
description: "This template requires you to fill out additional information about this project. This is the only place you will be able to do so."
header: "Header"
cover: "Title page"
footer: "Footer"
toc: "Table of contents"
no_values_title: "No additional data required"
no_values_title_pdf: "PDF %{template}: No additional data required"
no_values_title_docx: "DOCX %{template}: No additional data required"
no_values_description: "SciNote template doesnt need any additional input for it to be successfully generated."
second_step:
select_project_content: "Select and reorder experiments and tasks"
@ -845,6 +847,7 @@ en:
step_comments: "Step comments"
assigned_items: "Assigned items"
assigned_items_description: "Inventories selected below will only contain the items that you assigned to the tasks directly."
assigned_items_repository_items_description_html: "You can customize inventory columns <strong>only for the docx report.</strong> The stock management column by default reflects item consumption."
include_all_assigned_iitems: "Include all assigned items from the following inventories"
results: "Results"
all_results: "Include all results elements"
@ -863,8 +866,15 @@ en:
results_comments: "Result comments"
additional_content: "Additional content"
task_activity: "Include task activity"
exclude_task_metadata: "Exclude task metadata"
exclude_timestamps: "Exclude timestamps"
archived: "[archived]"
deleted: "[deleted]"
repository_column:
select_deselect_all: 'Select/deselect all'
placeholder: 'Select columns'
all_selected: 'All columns selected'
selected: 'columns selected'
new:
report_name_placeholder: "Name your report"
@ -967,6 +977,7 @@ en:
name: "%{repository} of task %{my_module}"
table_name: "%{name}"
no_items: "No items"
no_columns: "No columns selected"
result:
user_time: "Added on %{timestamp} by %{user}."
result_asset: