commit
8b9691cca1
16 changed files with 114 additions and 66 deletions
|
@ -4,8 +4,7 @@ class RechercheController < ApplicationController
|
||||||
PROJECTIONS = [
|
PROJECTIONS = [
|
||||||
{ "table" => 'procedure', "column" => 'libelle' },
|
{ "table" => 'procedure', "column" => 'libelle' },
|
||||||
{ "table" => 'user', "column" => 'email' },
|
{ "table" => 'user', "column" => 'email' },
|
||||||
{ "table" => 'procedure', "column" => 'procedure_id' },
|
{ "table" => 'procedure', "column" => 'procedure_id' }
|
||||||
{ "table" => 'dossier', "column" => 'hidden_by_administration_at' }
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
|
|
@ -31,7 +31,7 @@ class StatsController < ApplicationController
|
||||||
@procedures_in_the_last_4_months = last_four_months_serie(procedures, :published_at)
|
@procedures_in_the_last_4_months = last_four_months_serie(procedures, :published_at)
|
||||||
|
|
||||||
@dossiers_cumulative = stat.dossiers_cumulative
|
@dossiers_cumulative = stat.dossiers_cumulative
|
||||||
@dossiers_in_the_last_4_months = stat.dossiers_in_the_last_4_months
|
@dossiers_in_the_last_4_months = format_keys_as_months(stat.dossiers_in_the_last_4_months)
|
||||||
end
|
end
|
||||||
|
|
||||||
def download
|
def download
|
||||||
|
@ -136,11 +136,18 @@ class StatsController < ApplicationController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def format_keys_as_months(series)
|
||||||
|
series.transform_keys do |k|
|
||||||
|
date = k.is_a?(Date) ? k : (Date.parse(k) rescue k)
|
||||||
|
l(date, format: "%B %Y")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def last_four_months_serie(association, date_attribute)
|
def last_four_months_serie(association, date_attribute)
|
||||||
association
|
series = association
|
||||||
.group_by_month(date_attribute, last: 4, current: super_admin_signed_in?)
|
.group_by_month(date_attribute, last: 4, current: super_admin_signed_in?)
|
||||||
.count
|
.count
|
||||||
.transform_keys { |date| l(date, format: "%B %Y") }
|
format_keys_as_months(series)
|
||||||
end
|
end
|
||||||
|
|
||||||
def cumulative_month_serie(association, date_attribute)
|
def cumulative_month_serie(association, date_attribute)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
module StringToHtmlHelper
|
module StringToHtmlHelper
|
||||||
def string_to_html(str, wrapper_tag = 'p')
|
def string_to_html(str, wrapper_tag = 'p')
|
||||||
|
return nil if str.blank?
|
||||||
html_formatted = simple_format(str, {}, { wrapper_tag: wrapper_tag })
|
html_formatted = simple_format(str, {}, { wrapper_tag: wrapper_tag })
|
||||||
with_links = Anchored::Linker.auto_link(html_formatted, target: '_blank', rel: 'noopener')
|
with_links = Anchored::Linker.auto_link(html_formatted, target: '_blank', rel: 'noopener')
|
||||||
sanitize(with_links, attributes: ['target', 'rel', 'href'])
|
sanitize(with_links, attributes: ['target', 'rel', 'href'])
|
||||||
|
|
|
@ -80,11 +80,7 @@ class Stat < ApplicationRecord
|
||||||
association.group_by_month(date_attribute, last: 4, current: false).count
|
association.group_by_month(date_attribute, last: 4, current: false).count
|
||||||
end
|
end
|
||||||
|
|
||||||
month_serie(sum_hashes(*timeseries))
|
sum_hashes(*timeseries).sort.to_h
|
||||||
end
|
|
||||||
|
|
||||||
def month_serie(date_serie)
|
|
||||||
date_serie.keys.sort.each_with_object({}) { |date, h| h[I18n.l(date, format: "%B %Y")] = date_serie[date] }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def cumulative_month_serie(associations_with_date_attribute)
|
def cumulative_month_serie(associations_with_date_attribute)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
class DossierProjectionService
|
class DossierProjectionService
|
||||||
class DossierProjection < Struct.new(:dossier_id, :state, :archived, :hidden_by_user_at, :columns)
|
class DossierProjection < Struct.new(:dossier_id, :state, :archived, :hidden_by_user_at, :hidden_by_administration_at, :columns)
|
||||||
end
|
end
|
||||||
|
|
||||||
TABLE = 'table'
|
TABLE = 'table'
|
||||||
|
@ -21,8 +21,8 @@ class DossierProjectionService
|
||||||
state_field = { TABLE => 'self', COLUMN => 'state' }
|
state_field = { TABLE => 'self', COLUMN => 'state' }
|
||||||
archived_field = { TABLE => 'self', COLUMN => 'archived' }
|
archived_field = { TABLE => 'self', COLUMN => 'archived' }
|
||||||
hidden_by_user_at_field = { TABLE => 'self', COLUMN => 'hidden_by_user_at' }
|
hidden_by_user_at_field = { TABLE => 'self', COLUMN => 'hidden_by_user_at' }
|
||||||
|
hidden_by_administration_at_field = { TABLE => 'self', COLUMN => 'hidden_by_administration_at' }
|
||||||
([state_field, archived_field, hidden_by_user_at_field] + fields) # the view needs state and archived dossier attributes
|
([state_field, archived_field, hidden_by_user_at_field, hidden_by_administration_at_field] + fields) # the view needs state and archived dossier attributes
|
||||||
.each { |f| f[:id_value_h] = {} }
|
.each { |f| f[:id_value_h] = {} }
|
||||||
.group_by { |f| f[TABLE] } # one query per table
|
.group_by { |f| f[TABLE] } # one query per table
|
||||||
.each do |table, fields|
|
.each do |table, fields|
|
||||||
|
@ -46,7 +46,7 @@ class DossierProjectionService
|
||||||
.pluck(:id, *fields.map { |f| f[COLUMN].to_sym })
|
.pluck(:id, *fields.map { |f| f[COLUMN].to_sym })
|
||||||
.each do |id, *columns|
|
.each do |id, *columns|
|
||||||
fields.zip(columns).each do |field, value|
|
fields.zip(columns).each do |field, value|
|
||||||
if [state_field, archived_field, hidden_by_user_at_field].include?(field)
|
if [state_field, archived_field, hidden_by_user_at_field, hidden_by_administration_at_field].include?(field)
|
||||||
field[:id_value_h][id] = value
|
field[:id_value_h][id] = value
|
||||||
else
|
else
|
||||||
field[:id_value_h][id] = value&.strftime('%d/%m/%Y') # other fields are datetime
|
field[:id_value_h][id] = value&.strftime('%d/%m/%Y') # other fields are datetime
|
||||||
|
@ -100,6 +100,7 @@ class DossierProjectionService
|
||||||
state_field[:id_value_h][dossier_id],
|
state_field[:id_value_h][dossier_id],
|
||||||
archived_field[:id_value_h][dossier_id],
|
archived_field[:id_value_h][dossier_id],
|
||||||
hidden_by_user_at_field[:id_value_h][dossier_id],
|
hidden_by_user_at_field[:id_value_h][dossier_id],
|
||||||
|
hidden_by_administration_at_field[:id_value_h][dossier_id],
|
||||||
fields.map { |f| f[:id_value_h][dossier_id] }
|
fields.map { |f| f[:id_value_h][dossier_id] }
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
|
@ -28,8 +28,7 @@
|
||||||
state: dossier.state,
|
state: dossier.state,
|
||||||
archived: dossier.archived,
|
archived: dossier.archived,
|
||||||
dossier_is_followed: current_instructeur&.follow?(dossier),
|
dossier_is_followed: current_instructeur&.follow?(dossier),
|
||||||
close_to_expiration: dossier.close_to_expiration?,
|
close_to_expiration: dossier.close_to_expiration? }
|
||||||
recently_deleted: dossier.hidden_by_administration? }
|
|
||||||
|
|
||||||
|
|
||||||
.state-button
|
.state-button
|
||||||
|
|
|
@ -21,13 +21,7 @@
|
||||||
%span.icon.archive
|
%span.icon.archive
|
||||||
.dropdown-description
|
.dropdown-description
|
||||||
Archiver le dossier
|
Archiver le dossier
|
||||||
- if recently_deleted
|
|
||||||
%li.danger
|
|
||||||
= link_to restore_instructeur_dossier_path(procedure_id, dossier_id), method: :patch, data: { confirm: "Voulez vous vraiment restaurer le dossier #{dossier_id}" } do
|
|
||||||
%span.icon.reply
|
|
||||||
.dropdown-description
|
|
||||||
= t('views.instructeurs.dossiers.restore')
|
|
||||||
- else
|
|
||||||
%li.danger
|
%li.danger
|
||||||
= link_to supprimer_dossier_instructeur_dossier_path(procedure_id, dossier_id), method: :patch, data: { confirm: "Voulez vous vraiment supprimer le dossier #{dossier_id} ? Cette action est irréversible. \nNous vous suggérons de télécharger le dossier au format PDF au préalable." } do
|
= link_to supprimer_dossier_instructeur_dossier_path(procedure_id, dossier_id), method: :patch, data: { confirm: "Voulez vous vraiment supprimer le dossier #{dossier_id} ? Cette action est irréversible. \nNous vous suggérons de télécharger le dossier au format PDF au préalable." } do
|
||||||
%span.icon.delete
|
%span.icon.delete
|
||||||
|
|
|
@ -132,14 +132,19 @@
|
||||||
%td.status-col
|
%td.status-col
|
||||||
%a.cell-link{ href: path }= status_badge(p.state)
|
%a.cell-link{ href: path }= status_badge(p.state)
|
||||||
|
|
||||||
%td.action-col.follow-col= render partial: 'dossier_actions',
|
- if @statut == 'supprimes_recemment'
|
||||||
locals: { procedure_id: @procedure.id,
|
%td.action-col.follow-col
|
||||||
dossier_id: p.dossier_id,
|
= link_to restore_instructeur_dossier_path(@procedure, p.dossier_id), method: :patch, class: "button primary" do
|
||||||
state: p.state,
|
= t('views.instructeurs.dossiers.restore')
|
||||||
archived: p.archived,
|
|
||||||
dossier_is_followed: @followed_dossiers_id.include?(p.dossier_id),
|
- else
|
||||||
close_to_expiration: @statut == 'expirant',
|
%td.action-col.follow-col= render partial: 'dossier_actions',
|
||||||
recently_deleted: @statut == 'supprimes_recemment' }
|
locals: { procedure_id: @procedure.id,
|
||||||
|
dossier_id: p.dossier_id,
|
||||||
|
state: p.state,
|
||||||
|
archived: p.archived,
|
||||||
|
dossier_is_followed: @followed_dossiers_id.include?(p.dossier_id),
|
||||||
|
close_to_expiration: @statut == 'expirant' }
|
||||||
|
|
||||||
= pagination
|
= pagination
|
||||||
- else
|
- else
|
||||||
|
|
17
app/views/recherche/_hidden_dossier.html.haml
Normal file
17
app/views/recherche/_hidden_dossier.html.haml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
%td.folder-col
|
||||||
|
%p.cell-link
|
||||||
|
%span.icon.folder
|
||||||
|
|
||||||
|
%td.number-col
|
||||||
|
%p.cell-link= p.dossier_id
|
||||||
|
|
||||||
|
%td
|
||||||
|
%p.cell-link= procedure_libelle
|
||||||
|
|
||||||
|
%td
|
||||||
|
%p.cell-link
|
||||||
|
= user_email
|
||||||
|
= "- #{t('views.instructeurs.dossiers.deleted_by_administration')}" if p.hidden_by_administration_at.present?
|
||||||
|
|
||||||
|
%td.status-col
|
||||||
|
%p.cell-link= status_badge(p.state)
|
|
@ -20,13 +20,14 @@
|
||||||
%th.action-col.follow-col
|
%th.action-col.follow-col
|
||||||
%tbody
|
%tbody
|
||||||
- @projected_dossiers.each do |p|
|
- @projected_dossiers.each do |p|
|
||||||
- procedure_libelle, user_email, procedure_id, hidden_by_administration = p.columns
|
- procedure_libelle, user_email, procedure_id = p.columns
|
||||||
- instructeur_dossier = @instructeur_dossiers_ids.include?(p.dossier_id)
|
- instructeur_dossier = @instructeur_dossiers_ids.include?(p.dossier_id)
|
||||||
- expert_dossier = @dossier_avis_ids_h[p.dossier_id].present?
|
- expert_dossier = @dossier_avis_ids_h[p.dossier_id].present?
|
||||||
|
- hidden_by_administration = p.hidden_by_administration_at.present?
|
||||||
- instructeur_and_expert_dossier = instructeur_dossier && expert_dossier
|
- instructeur_and_expert_dossier = instructeur_dossier && expert_dossier
|
||||||
- path = instructeur_dossier ? instructeur_dossier_path(procedure_id, p.dossier_id) : expert_avis_path(procedure_id, @dossier_avis_ids_h[p.dossier_id])
|
- path = instructeur_dossier ? instructeur_dossier_path(procedure_id, p.dossier_id) : expert_avis_path(procedure_id, @dossier_avis_ids_h[p.dossier_id])
|
||||||
|
|
||||||
%tr
|
%tr{ class: [p.hidden_by_administration_at.present? && "file-hidden-by-user"] }
|
||||||
- if instructeur_and_expert_dossier
|
- if instructeur_and_expert_dossier
|
||||||
%td.folder-col.cell-link
|
%td.folder-col.cell-link
|
||||||
%span.icon.folder
|
%span.icon.folder
|
||||||
|
@ -39,7 +40,11 @@
|
||||||
%td.status-col
|
%td.status-col
|
||||||
.cell-link= status_badge(p.state)
|
.cell-link= status_badge(p.state)
|
||||||
|
|
||||||
|
- elsif hidden_by_administration
|
||||||
|
= render partial: "recherche/hidden_dossier", locals: {p: p, procedure_libelle: procedure_libelle, user_email: user_email}
|
||||||
|
|
||||||
- else
|
- else
|
||||||
|
|
||||||
%td.folder-col
|
%td.folder-col
|
||||||
%a.cell-link{ href: path }
|
%a.cell-link{ href: path }
|
||||||
%span.icon.folder
|
%span.icon.folder
|
||||||
|
@ -76,14 +81,19 @@
|
||||||
Donner mon avis
|
Donner mon avis
|
||||||
|
|
||||||
- elsif instructeur_dossier
|
- elsif instructeur_dossier
|
||||||
%td.action-col.follow-col= render partial: "instructeurs/procedures/dossier_actions",
|
- if hidden_by_administration
|
||||||
|
%td.action-col.follow-col
|
||||||
|
= link_to restore_instructeur_dossier_path(procedure_id, p.dossier_id), method: :patch, class: "button primary" do
|
||||||
|
= t('views.instructeurs.dossiers.restore')
|
||||||
|
|
||||||
|
- else
|
||||||
|
%td.action-col.follow-col= render partial: "instructeurs/procedures/dossier_actions",
|
||||||
locals: { procedure_id: procedure_id,
|
locals: { procedure_id: procedure_id,
|
||||||
dossier_id: p.dossier_id,
|
dossier_id: p.dossier_id,
|
||||||
state: p.state,
|
state: p.state,
|
||||||
archived: p.archived,
|
archived: p.archived,
|
||||||
dossier_is_followed: @followed_dossiers_id.include?(p.dossier_id),
|
dossier_is_followed: @followed_dossiers_id.include?(p.dossier_id),
|
||||||
close_to_expiration: nil,
|
close_to_expiration: nil }
|
||||||
recently_deleted: hidden_by_administration.blank? }
|
|
||||||
|
|
||||||
- else
|
- else
|
||||||
%td
|
%td
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
%p.attachment-error-title
|
%p.attachment-error-title
|
||||||
Une erreur s’est produite pendant l’envoi du fichier.
|
Une erreur s’est produite pendant l’envoi du fichier.
|
||||||
%p.attachment-error-description
|
%p.attachment-error-description
|
||||||
|
Une erreur inconnue s'est produite pendant l'envoi du fichier
|
||||||
= button_tag type: 'button', class: 'button attachment-error-retry', data: { 'input-target': ".attachment-input-#{attachment_id}" } do
|
= button_tag type: 'button', class: 'button attachment-error-retry', data: { 'input-target': ".attachment-input-#{attachment_id}" } do
|
||||||
%span.icon.retry
|
%span.icon.retry
|
||||||
Ré-essayer
|
Ré-essayer
|
||||||
|
|
|
@ -57,7 +57,7 @@
|
||||||
- when TypeDeChamp.type_champs.fetch(:number)
|
- when TypeDeChamp.type_champs.fetch(:number)
|
||||||
= number_with_html_delimiter(c.to_s)
|
= number_with_html_delimiter(c.to_s)
|
||||||
- else
|
- else
|
||||||
= format_text_value(c.to_s)
|
= format_text_value(c.to_s) unless c.blank?
|
||||||
|
|
||||||
- if c.type_champ != TypeDeChamp.type_champs.fetch(:header_section)
|
- if c.type_champ != TypeDeChamp.type_champs.fetch(:header_section)
|
||||||
%td.updated-at
|
%td.updated-at
|
||||||
|
|
|
@ -140,7 +140,8 @@ en:
|
||||||
archived_dossier: "This file will be kept for an additional month"
|
archived_dossier: "This file will be kept for an additional month"
|
||||||
delete_dossier: "Delete file"
|
delete_dossier: "Delete file"
|
||||||
deleted_by_user: "File deleted by user"
|
deleted_by_user: "File deleted by user"
|
||||||
restore: "Restore the file"
|
deleted_by_administration: "File deleted by administration"
|
||||||
|
restore: "Restore"
|
||||||
avis:
|
avis:
|
||||||
introduction_file_explaination: "File attached to the request for advice"
|
introduction_file_explaination: "File attached to the request for advice"
|
||||||
users:
|
users:
|
||||||
|
|
|
@ -137,7 +137,8 @@ fr:
|
||||||
archived_dossier: "Le dossier sera conservé 1 mois supplémentaire"
|
archived_dossier: "Le dossier sera conservé 1 mois supplémentaire"
|
||||||
delete_dossier: "Supprimer le dossier"
|
delete_dossier: "Supprimer le dossier"
|
||||||
deleted_by_user: "Dossier supprimé par l'usager"
|
deleted_by_user: "Dossier supprimé par l'usager"
|
||||||
restore: "Restaurer le dossier"
|
deleted_by_administration: "Dossier supprimé par l'administration"
|
||||||
|
restore: "Restaurer"
|
||||||
avis:
|
avis:
|
||||||
introduction_file_explaination: "Fichier joint à la demande d’avis"
|
introduction_file_explaination: "Fichier joint à la demande d’avis"
|
||||||
users:
|
users:
|
||||||
|
|
|
@ -28,7 +28,7 @@ RSpec.describe StringToHtmlHelper, type: :helper do
|
||||||
context "with empty decription" do
|
context "with empty decription" do
|
||||||
let(:description) { nil }
|
let(:description) { nil }
|
||||||
|
|
||||||
it { is_expected.to eq('<p></p>') }
|
it { is_expected.to eq nil }
|
||||||
end
|
end
|
||||||
|
|
||||||
context "with a bad script" do
|
context "with a bad script" do
|
||||||
|
|
|
@ -57,24 +57,30 @@ describe Stat do
|
||||||
create(:dossier, state: :en_construction, depose_at: i.months.ago)
|
create(:dossier, state: :en_construction, depose_at: i.months.ago)
|
||||||
create(:deleted_dossier, dossier_id: i + 100, state: :en_construction, deleted_at: i.month.ago)
|
create(:deleted_dossier, dossier_id: i + 100, state: :en_construction, deleted_at: i.month.ago)
|
||||||
end
|
end
|
||||||
rs = Stat.send(:cumulative_month_serie, [
|
s = Stat.new({
|
||||||
[Dossier.state_not_brouillon, :depose_at],
|
dossiers_cumulative:
|
||||||
[DeletedDossier.where.not(state: :brouillon), :deleted_at]
|
Stat.send(:cumulative_month_serie, [
|
||||||
|
[Dossier.state_not_brouillon, :depose_at],
|
||||||
|
[DeletedDossier.where.not(state: :brouillon), :deleted_at]
|
||||||
|
])
|
||||||
|
})
|
||||||
|
s.save!
|
||||||
|
s.reload
|
||||||
|
# Use `Hash#to_a` to also test the key ordering
|
||||||
|
expect(s.dossiers_cumulative.to_a).to eq([
|
||||||
|
[formatted_n_months_ago(12), 2],
|
||||||
|
[formatted_n_months_ago(11), 4],
|
||||||
|
[formatted_n_months_ago(10), 6],
|
||||||
|
[formatted_n_months_ago(9), 8],
|
||||||
|
[formatted_n_months_ago(8), 10],
|
||||||
|
[formatted_n_months_ago(7), 12],
|
||||||
|
[formatted_n_months_ago(6), 14],
|
||||||
|
[formatted_n_months_ago(5), 16],
|
||||||
|
[formatted_n_months_ago(4), 18],
|
||||||
|
[formatted_n_months_ago(3), 20],
|
||||||
|
[formatted_n_months_ago(2), 22],
|
||||||
|
[formatted_n_months_ago(1), 24]
|
||||||
])
|
])
|
||||||
expect(rs).to eq({
|
|
||||||
12 => 2,
|
|
||||||
11 => 4,
|
|
||||||
10 => 6,
|
|
||||||
9 => 8,
|
|
||||||
8 => 10,
|
|
||||||
7 => 12,
|
|
||||||
6 => 14,
|
|
||||||
5 => 16,
|
|
||||||
4 => 18,
|
|
||||||
3 => 20,
|
|
||||||
2 => 22,
|
|
||||||
1 => 24
|
|
||||||
}.transform_keys { |i| i.months.ago.beginning_of_month.to_date })
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -85,16 +91,22 @@ describe Stat do
|
||||||
create(:dossier, state: :en_construction, depose_at: i.months.ago)
|
create(:dossier, state: :en_construction, depose_at: i.months.ago)
|
||||||
create(:deleted_dossier, dossier_id: i + 100, state: :en_construction, deleted_at: i.month.ago)
|
create(:deleted_dossier, dossier_id: i + 100, state: :en_construction, deleted_at: i.month.ago)
|
||||||
end
|
end
|
||||||
rs = Stat.send(:last_four_months_serie, [
|
s = Stat.new({
|
||||||
[Dossier.state_not_brouillon, :depose_at],
|
dossiers_in_the_last_4_months:
|
||||||
[DeletedDossier.where.not(state: :brouillon), :deleted_at]
|
Stat.send(:last_four_months_serie, [
|
||||||
])
|
[Dossier.state_not_brouillon, :depose_at],
|
||||||
expect(rs).to eq({
|
[DeletedDossier.where.not(state: :brouillon), :deleted_at]
|
||||||
"juillet 2021" => 2,
|
])
|
||||||
"août 2021" => 2,
|
|
||||||
"septembre 2021" => 2,
|
|
||||||
"octobre 2021" => 2
|
|
||||||
})
|
})
|
||||||
|
s.save!
|
||||||
|
s.reload
|
||||||
|
# Use `Hash#to_a` to also test the key ordering
|
||||||
|
expect(s.dossiers_in_the_last_4_months.to_a).to eq([
|
||||||
|
['2021-07-01', 2],
|
||||||
|
['2021-08-01', 2],
|
||||||
|
['2021-09-01', 2],
|
||||||
|
['2021-10-01', 2]
|
||||||
|
])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -104,4 +116,8 @@ describe Stat do
|
||||||
expect(Stat.send(:sum_hashes, *[{ a: 1, b: 2, d: 5 }, { a: 2, b: 3, c: 5 }])).to eq({ a: 3, b: 5, c: 5, d: 5 })
|
expect(Stat.send(:sum_hashes, *[{ a: 1, b: 2, d: 5 }, { a: 2, b: 3, c: 5 }])).to eq({ a: 3, b: 5, c: 5, d: 5 })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def formatted_n_months_ago(i)
|
||||||
|
i.months.ago.beginning_of_month.to_date.to_s
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue