Add report name and details to CSV files

This commit is contained in:
Gareth Rogers
2025-07-30 20:39:00 -04:00
committed by Maikel Linke
parent f5a9ec7fa9
commit 0a9eb173ea
12 changed files with 245 additions and 15 deletions

View File

@@ -22,14 +22,12 @@ module Admin
def show
@report = report_class.new(spree_current_user, params, render: false)
@rendering_options = rendering_options
show_report
end
def create
@report = report_class.new(spree_current_user, params, render: true)
update_rendering_options
render_in_background
end
@@ -61,7 +59,9 @@ module Admin
@blob = ReportBlob.create_for_upload_later!(report_filename)
ReportJob.perform_later(
report_class:, user: spree_current_user, params:,
report_class:,
user: spree_current_user,
params:,
format: report_format,
blob: @blob,
channel: ScopedChannel.for_id(params[:uuid]),

View File

@@ -84,6 +84,7 @@ module ReportsActions
else
params[:fields_to_show]
end,
display_metadata_rows: false,
display_summary_row: request.get?,
display_header_row: false
}
@@ -94,6 +95,7 @@ module ReportsActions
rendering_options.update(
options: {
fields_to_show: params[:fields_to_show],
display_metadata_rows: params[:display_metadata_rows].present?,
display_summary_row: params[:display_summary_row].present?,
display_header_row: params[:display_header_row].present?
}

View File

@@ -1,19 +1,23 @@
- if @report_subtypes.present? && @report_subtypes.count > 1
%input{type: 'hidden', name: 'report_subtype', value: @report_subtype}
.row.rendering-options{ "data-controller": "csv-select" }
.row.rendering-options{ "data-controller": "csv-select metadata-toggle" }
.alpha.two.columns
= label_tag :report_format, t(".generate_report")
.omega.fourteen.columns{ style: "margin-bottom: 1.5em;" }
= select_tag :report_format, grouped_options_for_select({ |
t('.formatted_data') => { t('.on_screen') => '', "PDF" => 'pdf', t('.spreadsheet') => 'xlsx' }, |
t('.raw_data') => { "CSV" => 'csv' }, |
}), { "data-csv-select-target": "reportType", "data-action": "csv-select#handleSelectChange" }
}), { "data-csv-select-target": "reportType", "data-metadata-toggle-target": "reportType", "data-action": "csv-select#handleSelectChange metadata-toggle#handleSelectChange" }
- if @report.header_option? || @report.summary_row_option?
- if @report.header_option? || @report.summary_row_option? || @report.metadata_option?
.row
.alpha.two.columns= label_tag nil, t(".display")
.omega.fourteen.columns
- if @report.metadata_option?
%span.inline-checkbox{ style: "margin-right: 1rem;" }
= check_box_tag :display_metadata_rows, true, @rendering_options.options[:display_metadata_rows], { "disabled": "true", "data-metadata-toggle-target": "checkbox" }
= label_tag :display_metadata_rows, t(".metadata_rows"), {"class": "disabled", "data-metadata-toggle-target": "label" }
- if @report.header_option?
%span.inline-checkbox{ style: "margin-right: 1rem;" }
= check_box_tag :display_header_row, true, @rendering_options.options[:display_header_row]

View File

@@ -4,9 +4,9 @@ export default class extends Controller {
static targets = ["reportType", "checkbox", "label"];
handleSelectChange() {
this.reportTypeTarget.value == "csv"
? this.disableField()
: this.enableField();
this.reportTypeTarget.value == "csv" ?
this.disableField():
this.enableField();
}
disableField() {

View File

@@ -0,0 +1,31 @@
import { Controller } from "stimulus";
export default class extends Controller {
static targets = ["reportType", "checkbox", "label"];
handleSelectChange() {
this.reportTypeTarget.value == "csv" ?
this.enableField():
this.disableField();
}
disableField() {
if (this.hasCheckboxTarget) {
this.checkboxTarget.checked = false;
this.checkboxTarget.disabled = true;
}
if (this.hasLabelTarget) {
this.labelTarget.classList.add("disabled");
}
}
enableField() {
if (this.hasCheckboxTarget) {
this.checkboxTarget.checked = true;
this.checkboxTarget.disabled = false;
}
if (this.hasLabelTarget) {
this.labelTarget.classList.remove("disabled");
}
}
}

View File

@@ -1798,6 +1798,8 @@ en:
not_visible: "%{enterprise} is not visible and so cannot be found on the map or in searches"
reports:
none: none
metadata:
report_title: Report Title
deprecated: "This report is deprecated and will be removed in a future release."
hidden_field: "< Hidden >"
unitsize: UNITSIZE
@@ -1900,6 +1902,7 @@ en:
display: Display
summary_row: Summary Row
header_row: Header Row
metadata_rows: Metadata Rows
raw_data: Raw Data
formatted_data: Formatted Data
packing:
@@ -2514,12 +2517,14 @@ en:
email_confirmed: "Thank you for confirming your email address."
email_confirmation_activate_account: "Before we can activate your new account, we need to confirm your email address."
email_confirmation_greeting: "Hi, %{contact}!"
email_confirmation_profile_created: "A profile for %{name} has been successfully created!
To activate your Profile we need to confirm this email address."
email_confirmation_profile_created: >
A profile for %{name} has been successfully created!
To activate your Profile we need to confirm this email address.
email_confirmation_click_link: "Please click the link below to confirm your email and to continue setting up your profile."
email_confirmation_link_label: "Confirm this email address »"
email_confirmation_help_html: "After confirming your email you can access your administration account for this enterprise.
See the %{link} to find out more about %{sitename}'s features and to start using your profile or online store."
email_confirmation_help_html: >
After confirming your email you can access your administration account for this enterprise.
See the %{link} to find out more about %{sitename}'s features and to start using your profile or online store."
email_confirmation_notice_unexpected: "You received this message because you signed up on %{sitename}, or were invited to sign up by someone you probably know. If you don't understand why you are receiving this email, please write to %{contact}."
email_social: "Connect with Us:"
email_contact: "Email us:"

View File

@@ -0,0 +1,78 @@
# frozen_string_literal: true
module Reporting
class ReportMetadataBuilder
attr_reader :report, :current_user
def initialize(report, current_user = nil)
@report = report
@current_user = current_user
end
def rows
rows = []
rows.concat(title_rows)
rows.concat(date_range_rows)
rows.concat(printed_rows)
rows.concat(other_filter_rows)
rows << [] if rows.any? # spacer only if something was added
rows
end
private
DATE_FROM_KEYS = %i[completed_at_gt created_at_gt updated_at_gt].freeze
DATE_TO_KEYS = %i[completed_at_lt created_at_lt updated_at_lt].freeze
def title_rows
type = params[:report_type]
sub = params[:report_subtype]
return [] if type.blank?
label = I18n.t("admin.reports.metadata.report_title", default: "Report Title")
type_name = I18n.t("admin.reports.#{type}.name",
default: I18n.t("admin.reports.#{type}",
default: type.to_s.tr('_', ' ').titleize))
sub_name = sub.present? ? sub.to_s.tr('_', ' ').titleize : nil
title = [type_name, sub_name].compact.join(' - ')
[[label, title]]
end
def date_range_rows
q = indifferent_ransack
from = first_present(q, DATE_FROM_KEYS)
to = first_present(q, DATE_TO_KEYS)
return [] unless from || to
label = I18n.t("date_range", default: "Date Range")
[[label, [from, to].compact.join(' - ')]] # en dash
end
def first_present(hash, keys)
keys.map { |k| hash[k] }.find(&:present?)
end
def indifferent_ransack
(report.ransack_params || {}).with_indifferent_access
end
def printed_rows
[[I18n.t("printed", default: "Printed"), Time.now.utc.strftime('%F %T %Z')]]
end
def other_filter_rows
q = indifferent_ransack.except(*DATE_FROM_KEYS, *DATE_TO_KEYS)
q.each_with_object([]) do |(k, v), rows|
next if v.blank?
rows << [k.to_s.humanize, v.is_a?(Array) ? v.join(', ') : v.to_s]
end
end
def params
(report.params || {}).with_indifferent_access
end
end
end

View File

@@ -1,5 +1,6 @@
# frozen_string_literal: true
require 'csv'
require 'spreadsheet_architect'
module Reporting
@@ -24,6 +25,10 @@ module Reporting
@report.params[:report_format].in?([nil, '', 'pdf'])
end
def display_metadata_rows?
@report.params[:display_metadata_rows].present? && raw_render?
end
def display_header_row?
@report.params[:display_header_row].present? && !raw_render?
end
@@ -33,13 +38,22 @@ module Reporting
end
def table_headers
@report.table_headers || []
base = @report.table_headers || []
return base unless display_metadata_rows?
[*metadata_headers, base]
end
def table_rows
@report.table_rows || []
end
def metadata_headers
return [] unless display_metadata_rows?
Reporting::ReportMetadataBuilder.new(@report, @report.try(:user)).rows
end
def as_json(_context_controller = nil)
@report.rows.map(&:to_h).as_json
end

View File

@@ -10,6 +10,10 @@ module Reporting
@formatted_rules ||= @report.rules.map { |rule| format_rule(rule) }
end
def metadata_option?
true
end
def header_option?
formatted_rules.find { |rule| rule[:header].present? }
end

View File

@@ -13,7 +13,7 @@ module Reporting
delegate :available_headers, :table_headers, :fields_to_hide, :fields_to_show,
to: :headers_builder
delegate :formatted_rules, :header_option?, :summary_row_option?, to: :ruler
delegate :formatted_rules, :header_option?, :summary_row_option?, :metadata_option?, to: :ruler
def initialize(user, params = {}, render: false)
unless render

View File

@@ -0,0 +1,48 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Reporting::ReportMetadataBuilder do
let(:from_key) { described_class::DATE_FROM_KEYS.first }
let(:to_key) { described_class::DATE_TO_KEYS.first }
let(:params) do
{ report_type: :order_cycle_customer_totals, report_subtype: 'by_distributor' }
end
let(:ransack_params) do
{
from_key => '2025-01-01',
to_key => '2025-01-31',
:status_in => %w[paid shipped],
:hub_id_eq => '42'
}
end
let(:report) { instance_double('Report', params:, ransack_params:) }
subject(:builder) { described_class.new(report, nil) }
it 'assembles rows with title, date range, printed, other filters, and spacer' do
travel_to(Time.zone.parse('2025-06-13 10:20:30 UTC')) do
rows = builder.rows
# Title
expect(rows).to include(['Report Title', 'Order Cycle Customer Totals - By Distributor'])
# Date range
expect(rows).to include(['Date Range', '2025-01-01 - 2025-01-31'])
# Printed timestamp
printed = rows.find { |r| r.first == 'Printed' }
expect(printed).to eq(['Printed', '2025-06-13 10:20:30 UTC'])
# Other filters (humanized keys)
expect(rows).to include(['Status in', 'paid, shipped'])
expect(rows).to include(['Hub id eq', '42'])
# Spacer
expect(rows.last).to eq([])
end
end
end

View File

@@ -34,4 +34,48 @@ RSpec.describe Reporting::ReportRenderer do
expect { subject.render_as("give_me_everything") }.to raise_error
end
end
# metadata headers
describe '#metadata_headers' do
let(:user) { create(:user) }
let(:from_key) { Reporting::ReportMetadataBuilder::DATE_FROM_KEYS.first }
let(:to_key) { Reporting::ReportMetadataBuilder::DATE_TO_KEYS.first }
let(:meta_report) do
instance_double(
'MetaReport',
rows: data,
params: {
display_metadata_rows: true,
report_type: :order_cycle_customer_totals,
report_subtype: 'by_distributor',
report_format: 'csv'
},
ransack_params: {
from_key => '2025-01-01',
to_key => '2025-01-31',
:status_in => %w[paid shipped]
},
user:
)
end
let(:renderer) { described_class.new(meta_report) }
it 'builds rows via ReportMetadataBuilder when display_metadata_rows?
is true and report_format is csv' do
rows = renderer.metadata_headers
labels = rows.map(&:first)
expect(labels).to include('Report Title')
expect(labels).to include('Date Range')
expect(labels).to include('Printed')
values = rows.map(&:second)
expect(values).to include('Order Cycle Customer Totals - By Distributor')
expect(values).to include('2025-01-01 - 2025-01-31')
expect(values).to include(Time.now.utc.strftime('%F %T %Z'))
end
end
end