diff --git a/CHANGELOG.md b/CHANGELOG.md index 484daed35..e09837164 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog Fab-manager +- Fix a bug: unable to export statistics - Fix a bug: soft destroyed machines and spaces are still reported in the OpenAPI ## v5.5.5 2022 November 22 diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 60ef47237..92f888aaa 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -47,7 +47,7 @@ module ApplicationHelper message = MessageFormat.new(I18n.t(scope_key_by_partial(key)), I18n.locale.to_s) text = message.format(interpolations) if html_safe_translation_key?(key) - text.html_safe + text.html_safe # rubocop:disable Rails/OutputSafety else text end @@ -65,23 +65,6 @@ module ApplicationHelper amount / 100.00 end - ## - # Retrieve an item in the given array of items - # by default, the "id" is expected to match the given parameter but - # this can be overridden by passing a third parameter to specify the - # property to match - ## - def get_item(array, id, key = nil) - array.each do |i| - if key.nil? - return i if i.id == id - elsif i[key] == id - return i - end - end - nil - end - ## # Apply a correction for a future DateTime due to change in Daylight Saving Time (DST) period # @param reference {ActiveSupport::TimeWithZone} @@ -98,6 +81,7 @@ module ApplicationHelper private ## inspired by gems/actionview-4.2.5/lib/action_view/helpers/translation_helper.rb + # rubocop:disable Rails/HelperInstanceVariable def scope_key_by_partial(key) if key.to_s.first == '.' raise "Cannot use t(#{key.inspect}) shortcut because path is not available" unless @virtual_path @@ -107,6 +91,7 @@ module ApplicationHelper key end end + # rubocop:enable Rails/HelperInstanceVariable def html_safe_translation_key?(key) key.to_s =~ /(\b|_|\.)html$/ diff --git a/app/helpers/excel_helper.rb b/app/helpers/excel_helper.rb new file mode 100644 index 000000000..81653b6d2 --- /dev/null +++ b/app/helpers/excel_helper.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +# Helpers for excel exports, for use with AXSLX gem +module ExcelHelper + # format the given source data for an Excell cell + def format_xlsx_cell(source_data, data, styles, types, date_format: nil, source_data_type: '') + case source_data_type + when 'date' + data.push Date.strptime(source_data, '%Y-%m-%d') + styles.push date_format + types.push :date + when 'list' + data.push source_data.map { |e| e['name'] }.join(', ') + styles.push nil + types.push :string + when 'number' + data.push source_data + styles.push nil + types.push :float + else + data.push source_data + styles.push nil + types.push :string + end + end + + # build a new excel line for a statistic export + def statistics_line(hit, user, type, subtype, date_format) + data = [ + Date.strptime(hit['_source']['date'], '%Y-%m-%d'), + user&.profile&.full_name || t('export.deleted_user'), + user&.email || '', + user&.profile&.phone || '', + t("export.#{hit['_source']['gender']}"), + hit['_source']['age'], + subtype.nil? ? '' : subtype.label + ] + styles = [date_format, nil, nil, nil, nil, nil, nil] + types = %i[date string string string string integer string] + # do not proceed with the 'stat' field if the type is declared as 'simple' + unless type.simple + data.push hit['_source']['stat'] + styles.push nil + types.push :string + end + + [data, styles, types] + end + + # append a cell containing the CA amount + def add_ca_cell(index, hit, data, styles, types) + return unless index.ca + + data.push hit['_source']['ca'] + styles.push nil + types.push :float + end + + ## + # Retrieve an item in the given array of items + # by default, the "id" is expected to match the given parameter but + # this can be overridden by passing a third parameter to specify the + # property to match + ## + def get_item(array, id, key = nil) + array.each do |i| + if key.nil? + return i if i.id == id + elsif i[key] == id + return i + end + end + nil + end +end diff --git a/app/services/export_service.rb b/app/services/export_service.rb index a77d07c57..51e826a97 100644 --- a/app/services/export_service.rb +++ b/app/services/export_service.rb @@ -27,9 +27,9 @@ class ExportService def query_last_export(category, export_type, query = nil, key = nil, extension = nil) export = Export.where(category: category, export_type: export_type) - export.where(query: query) unless query.nil? - export.where(key: key) unless key.nil? - export.where(extension: extension) unless extension.nil? + export = export.where(query: query) unless query.nil? + export = export.where(key: key) unless key.nil? + export = export.where(extension: extension) unless extension.nil? export end diff --git a/app/services/statistics/builders/reservations_builder_service.rb b/app/services/statistics/builders/reservations_builder_service.rb index 57a1d7a3a..6c6bea826 100644 --- a/app/services/statistics/builders/reservations_builder_service.rb +++ b/app/services/statistics/builders/reservations_builder_service.rb @@ -21,16 +21,34 @@ class Statistics::Builders::ReservationsBuilderService stat[:stat] = (type == 'booking' ? 1 : r[:nb_hours]) stat["#{category}Id".to_sym] = r["#{category}_id".to_sym] - if category == 'event' - stat[:eventDate] = r[:event_date] - stat[:eventTheme] = r[:event_theme] - stat[:ageRange] = r[:age_range] - end - + stat = add_custom_attributes(category, stat, r) stat.save end end end end + + def add_custom_attributes(category, stat, reservation_data) + stat = add_event_attributes(category, stat, reservation_data) + add_training_attributes(category, stat, reservation_data) + end + + def add_event_attributes(category, stat, reservation_data) + return stat unless category == 'event' + + stat[:eventDate] = reservation_data[:event_date] + stat[:eventTheme] = reservation_data[:event_theme] + stat[:ageRange] = reservation_data[:age_range] + + stat + end + + def add_training_attributes(category, stat, reservation_data) + return stat unless category == 'training' + + stat[:trainingDate] = reservation_data[:training_date] + + stat + end end end diff --git a/app/services/statistics_export_service.rb b/app/services/statistics_export_service.rb index 43422b664..06aeeb21d 100644 --- a/app/services/statistics_export_service.rb +++ b/app/services/statistics_export_service.rb @@ -6,12 +6,11 @@ require 'action_view' require 'active_record' # require any helpers -require './app/helpers/application_helper' +require './app/helpers/excel_helper' +# Export statistics (from elasticsearch) to an excel file class StatisticsExportService - def export_global(export) - # query all stats with range arguments query = MultiJson.load(export.query) @@ -30,18 +29,19 @@ class StatisticsExportService ActionController::Base.prepend_view_path './app/views/' # place data in view_assigns - view_assigns = {results: @results, users: @users, indices: @indices} + view_assigns = { results: @results, users: @users, indices: @indices } av = ActionView::Base.new(ActionController::Base.view_paths, view_assigns) av.class_eval do # include any needed helpers (for the view) - include ApplicationHelper + include ExcelHelper end content = av.render template: 'exports/statistics_global.xlsx.axlsx' # write content to file - File.open(export.file, 'w+b') { |f| f.write content } + File.binwrite(export.file, content) end + # rubocop:disable Style/DocumentDynamicEvalDefinition %w[account event machine project subscription training space].each do |path| class_eval %{ def export_#{path}(export) @@ -71,14 +71,14 @@ class StatisticsExportService av = ActionView::Base.new(ActionController::Base.view_paths, view_assigns) av.class_eval do # include any needed helpers (for the view) - include ApplicationHelper + include ExcelHelper end content = av.render template: 'exports/statistics_current.xlsx.axlsx' # write content to file - File.open(export.file,"w+b") { |f| f.write content } + File.binwrite(export.file, content) end }, __FILE__, __LINE__ - 35 end - + # rubocop:enable Style/DocumentDynamicEvalDefinition end diff --git a/app/views/exports/statistics_current.xlsx.axlsx b/app/views/exports/statistics_current.xlsx.axlsx index 76c676854..e4b8062c4 100644 --- a/app/views/exports/statistics_current.xlsx.axlsx +++ b/app/views/exports/statistics_current.xlsx.axlsx @@ -1,79 +1,46 @@ +# frozen_string_literal: true + wb = xlsx_package.workbook -bold = wb.styles.add_style :b => true -header = wb.styles.add_style :b => true, :bg_color => Stylesheet.primary.upcase.gsub('#', 'FF'), :fg_color => 'FFFFFFFF' -date = wb.styles.add_style :format_code => Rails.application.secrets.excel_date_format +bold = wb.styles.add_style b: true +header = wb.styles.add_style b: true, bg_color: Stylesheet.primary.upcase.gsub('#', 'FF'), fg_color: 'FFFFFFFF' +date = wb.styles.add_style format_code: Rails.application.secrets.excel_date_format wb.add_worksheet(name: @index.label) do |sheet| ## heading stats for the current page - sheet.add_row [t('export.entries'), @results['hits']['total']], :style => [bold, nil], :types => [:string, :integer] + sheet.add_row [t('export.entries'), @results['hits']['total']], style: [bold, nil], types: %i[string integer] if @index.ca - sheet.add_row [t('export.revenue'), @results['aggregations']['total_ca']['value']], :style => [bold, nil], :types => [:string, :float] + sheet.add_row [t('export.revenue'), @results['aggregations']['total_ca']['value']], style: [bold, nil], types: %i[string float] end - sheet.add_row [t('export.average_age'), @results['aggregations']['average_age']['value']], :style => [bold, nil], :types => [:string, :float] + sheet.add_row [t('export.average_age'), @results['aggregations']['average_age']['value']], style: [bold, nil], types: %i[string float] unless @type.simple - sheet.add_row ["#{t('export.total')} #{@type.label}", @results['aggregations']['total_stat']['value']], :style => [bold, nil], :types => [:string, :integer] + sheet.add_row ["#{t('export.total')} #{@type.label}", @results['aggregations']['total_stat']['value']], + style: [bold, nil], + types: %i[string integer] end sheet.add_row [] ## data table # heading labels - columns = [t('export.date'), t('export.user'), t('export.email'), t('export.phone'), t('export.gender'), t('export.age'), t('export.type')] + columns = [t('export.date'), t('export.user'), t('export.email'), t('export.phone'), t('export.gender'), t('export.age'), + t('export.type')] columns.push @type.label unless @type.simple @fields.each do |f| columns.push f.label end columns.push t('export.revenue') if @index.ca - sheet.add_row columns, :style => header + sheet.add_row columns, style: header # data rows @results['hits']['hits'].each do |hit| user = get_item(@users, hit['_source']['userId']) subtype = get_item(@subtypes, hit['_source']['subType'], 'key') - data = [ - Date::strptime(hit['_source']['date'],'%Y-%m-%d'), - (user ? user.profile.full_name : "ID #{hit['_source']['userId']}"), - (user ? user.email : ''), - (user ? user.profile.phone : ''), - t("export.#{hit['_source']['gender']}"), - hit['_source']['age'], - subtype.nil? ? "" : subtype.label - ] - styles = [date, nil, nil, nil, nil, nil, nil] - types = [:date, :string, :string, :string, :string, :integer, :string] - unless @type.simple - data.push hit['_source']['stat'] - styles.push nil - types.push :string - end + data, styles, types = statistics_line(hit, user, @type, subtype, date) @fields.each do |f| - field_data = hit['_source'][f.key] - case f.data_type - when 'date' - data.push Date::strptime(field_data, '%Y-%m-%d') - styles.push date - types.push :date - when 'list' - data.push field_data.map{|e| e['name'] }.join(', ') - styles.push nil - types.push :string - when 'number' - data.push field_data - styles.push nil - types.push :float - else - data.push field_data - styles.push nil - types.push :string - end - - end - if @index.ca - data.push hit['_source']['ca'] - styles.push nil - types.push :float + format_xlsx_cell(hit['_source'][f.key], data, styles, types, source_data_type: f.data_type, date_format: date) end + add_ca_cell(@index, hit, data, styles, types) - sheet.add_row data, :style => styles, :types => types + sheet.add_row data, style: styles, types: types end -end \ No newline at end of file +end diff --git a/app/views/exports/statistics_global.xlsx.axlsx b/app/views/exports/statistics_global.xlsx.axlsx index f2953ff66..2b9a723d5 100644 --- a/app/views/exports/statistics_global.xlsx.axlsx +++ b/app/views/exports/statistics_global.xlsx.axlsx @@ -1,84 +1,48 @@ +# frozen_string_literal: true + wb = xlsx_package.workbook -header = wb.styles.add_style :b => true, :bg_color => Stylesheet.primary.upcase.gsub('#', 'FF'), :fg_color => 'FFFFFFFF' -date = wb.styles.add_style :format_code => Rails.application.secrets.excel_date_format +header = wb.styles.add_style b: true, bg_color: Stylesheet.primary.upcase.gsub('#', 'FF'), fg_color: 'FFFFFFFF' +date = wb.styles.add_style format_code: Rails.application.secrets.excel_date_format @indices.each do |index| - if index.table - index.statistic_types.each do |type| - # see https://msdn.microsoft.com/fr-fr/library/c6bdca6y(v=vs.90).aspx for unauthorized character list - sheet_name = "#{index.label} - #{type.label}".gsub(/[*|\\:"<>?\/]/,'').truncate(31) - wb.add_worksheet(name: sheet_name) do |sheet| + next unless index.table - ## data table - # heading labels - columns = [t('export.date'), t('export.user'), t('export.email'), t('export.phone'), t('export.gender'), t('export.age'), t('export.type')] - columns.push type.label unless type.simple + index.statistic_types.each do |type| + # see https://msdn.microsoft.com/fr-fr/library/c6bdca6y(v=vs.90).aspx for unauthorized character list + sheet_name = "#{index.label} - #{type.label}".gsub(%r{[*|\\:"<>?/]}, '').truncate(31) + wb.add_worksheet(name: sheet_name) do |sheet| + ## data table + # heading labels + columns = [t('export.date'), t('export.user'), t('export.email'), t('export.phone'), t('export.gender'), t('export.age'), + t('export.type')] + columns.push type.label unless type.simple + index.statistic_fields.each do |f| + columns.push f.label + end + columns.push t('export.revenue') if index.ca + sheet.add_row columns, style: header + + # data rows + @results['hits']['hits'].each do |hit| + # check that the current result is for the given index and type + next unless hit['_type'] == index.es_type_key && hit['_source']['type'] == type.key + + # get matching objects + user = get_item(@users, hit['_source']['userId']) + subtype = get_item(type.statistic_sub_types, hit['_source']['subType'], 'key') + # start to fill data and associated styles and data-types + data, styles, types = statistics_line(hit, user, type, subtype, date) + # proceed additional fields index.statistic_fields.each do |f| - columns.push f.label + format_xlsx_cell(hit['_source'][f.key], data, styles, types, source_data_type: f.data_type, date_format: date) end - columns.push t('export.revenue') if index.ca - sheet.add_row columns, :style => header + # proceed the 'ca' field if requested + add_ca_cell(index, hit, data, styles, types) - # data rows - @results['hits']['hits'].each do |hit| - # check that the current result is for the given index and type - if hit['_type'] == index.es_type_key and hit['_source']['type'] == type.key - # get matching objects - user = get_item(@users, hit['_source']['userId']) - subtype = get_item(type.statistic_sub_types, hit['_source']['subType'], 'key') - # start to fill data and associated styles and data-types - data = [ - Date::strptime(hit['_source']['date'],'%Y-%m-%d'), - (user ? user.profile.full_name : "ID #{hit['_source']['userId']}"), - (user ? user.email : ''), - (user ? user.profile.phone : ''), - t("export.#{hit['_source']['gender']}"), - hit['_source']['age'], - subtype.nil? ? "" : subtype.label - ] - styles = [date, nil, nil, nil, nil, nil, nil] - types = [:date, :string, :string, :string, :string, :integer, :string] - # do not proceed with the 'stat' field if the type is declared as 'simple' - unless type.simple - data.push hit['_source']['stat'] - styles.push nil - types.push :string - end - # proceed additional fields - index.statistic_fields.each do |f| - field_data = hit['_source'][f.key] - case f.data_type - when 'date' - data.push Date::strptime(field_data, '%Y-%m-%d') - styles.push date - types.push :date - when 'list' - data.push field_data.map{|e| e['name'] }.join(', ') - styles.push nil - types.push :string - when 'number' - data.push field_data - styles.push nil - types.push :float - else - data.push field_data - styles.push nil - types.push :string - end - - end - # proceed the 'ca' field if requested - if index.ca - data.push hit['_source']['ca'] - styles.push nil - types.push :float - end - # finally, add the data row to the workbook's sheet - sheet.add_row data, :style => styles, :types => types - end - end + # finally, add the data row to the workbook's sheet + sheet.add_row data, style: styles, types: types end end end -end \ No newline at end of file +end diff --git a/config/locales/en.yml b/config/locales/en.yml index 18f76841f..5dcc9c76f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -474,6 +474,7 @@ en: type: "Type" male: "Man" female: "Woman" + deleted_user: "Deleted user" #initial price's category for events, created to replace the old "reduced amount" property price_category: reduced_fare: "Reduced fare" diff --git a/doc/postgresql_readme.md b/doc/postgresql_readme.md index 36a1087dc..8de0d458e 100644 --- a/doc/postgresql_readme.md +++ b/doc/postgresql_readme.md @@ -19,8 +19,8 @@ cd /apps/fabmanager/ docker-compose exec postgres bash cd /var/lib/postgresql/data/ DB=$(psql -U postgres -c \\l | grep production | awk '{print $1}') -pg_dump -U postgres "$DB" > "$DB_$(date -I).sql" -tar cvzf "fabmanager_database_dump_$(date -I).tar.gz" "$DB_$(date -I).sql" +pg_dump -U postgres "$DB" > "${DB}_$(date -I).sql" +tar cvzf "fabmanager_database_dump_$(date -I).tar.gz" "${DB}_$(date -I).sql" ``` If you're connected to your server thought SSH, you can download the resulting dump file using the following: @@ -30,14 +30,15 @@ scp root@remote.server.fab:/apps/fabmanager/postgresql/fabmanager_database_dump_ Restore the dump with the following: ```bash +DUMP=$(tar -tvf "fabmanager_database_dump_$(date -I).tar.gz" | awk '{print $6}') tar xvf fabmanager_database_dump_$(date -I).tar.gz -sudo cp fabmanager_production_$(date -I).sql /apps/fabmanager/postgresql/ +sudo cp "$DUMP" /apps/fabmanager/postgresql/ cd /apps/fabmanager/ docker-compose down docker-compose up -d postgres docker-compose exec postgres dropdb -U postgres fabmanager_production docker-compose exec postgres createdb -U postgres fabmanager_production -docker-compose exec postgres psql -U postgres -d fabmanager_production -f /var/lib/postgresql/data/fabmanager_production_$(date -I).sql +docker-compose exec postgres psql -U postgres -d fabmanager_production -f "/var/lib/postgresql/data/${DUMP}" docker-compose up -d ```