Browse Source

CLDC-3448 Generate validations documentation (#2438)

* Add openai gem

* Add validation table and model

* Add describe_lettings_validations task

* lint

* Add describe_soft_lettings_validations task

* Add describe_bu_lettings_validations task

* Add add_numeric_lettings_validations task

* Rename

* Add sales tasks for generating validations docs

* Add rails admin

* refactor

* Rename validation table

* Rename validation_methods variable (?)

* Move tests

* lint

* Add back fonts

* Add method_source gem

* Generate numeric validations for both log types
pull/2454/head v0.4.47
kosiakkatrina 7 months ago committed by GitHub
parent
commit
ab6c64ce76
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      Gemfile
  2. 48
      Gemfile.lock
  3. 2
      app/assets/stylesheets/rails_admin.scss
  4. 1
      app/javascript/rails_admin.js
  5. 2
      app/models/log_validation.rb
  6. 371
      app/services/documentation_generator.rb
  7. 1
      config/initializers/assets.rb
  8. 46
      config/initializers/rails_admin.rb
  9. 1
      config/routes.rb
  10. 20
      db/migrate/20240523153434_add_validations_table.rb
  11. 5
      db/migrate/20240529133005_rename_validations_table.rb
  12. 20
      db/schema.rb
  13. 90
      lib/tasks/generate_lettings_documentation.rake
  14. 89
      lib/tasks/generate_sales_documentation.rake
  15. 4
      package.json
  16. 38
      spec/lib/tasks/generate_lettings_documentation_spec.rb
  17. 38
      spec/lib/tasks/generate_sales_documentation_spec.rb
  18. 46
      spec/requests/rails_admin_controller_spec.rb
  19. 230
      spec/services/documentation_generator_spec.rb
  20. 3
      webpack.config.js
  21. 82
      yarn.lock

4
Gemfile

@ -62,6 +62,9 @@ gem "possessive"
# Strip whitespace from active record attributes
gem "auto_strip_attributes"
# Use sidekiq for background processing
gem "method_source", "~> 1.1"
gem "rails_admin", "~> 3.0"
gem "ruby-openai"
gem "sidekiq"
gem "sidekiq-cron"
gem "unread"
@ -105,4 +108,5 @@ group :test do
end
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "cssbundling-rails"
gem "tzinfo-data", platforms: %i[mingw mswin x64_mingw jruby]

48
Gemfile.lock

@ -51,6 +51,10 @@ GEM
globalid (>= 0.3.6)
activemodel (7.0.8.4)
activesupport (= 7.0.8.4)
activemodel-serializers-xml (1.0.2)
activemodel (> 5.x)
activesupport (> 5.x)
builder (~> 3.1)
activerecord (7.0.8.4)
activemodel (= 7.0.8.4)
activesupport (= 7.0.8.4)
@ -130,6 +134,8 @@ GEM
bigdecimal
rexml
crass (1.0.6)
cssbundling-rails (1.4.0)
railties (>= 6.0.0)
date (3.3.4)
devise (4.9.3)
bcrypt (~> 3.0)
@ -160,6 +166,7 @@ GEM
erubi (1.12.0)
et-orbi (1.2.7)
tzinfo
event_stream_parser (1.0.0)
excon (0.109.0)
factory_bot (6.4.6)
activesupport (>= 5.0.0)
@ -168,6 +175,12 @@ GEM
railties (>= 5.0.0)
faker (3.2.3)
i18n (>= 1.8.11, < 2)
faraday (2.9.0)
faraday-net_http (>= 2.0, < 3.2)
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (3.1.0)
net-http
ffi (1.16.3)
fugit (1.10.0)
et-orbi (~> 1, >= 1.2.7)
@ -199,6 +212,18 @@ GEM
addressable (>= 2.8)
jwt (2.8.0)
base64
kaminari (1.2.2)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.2)
kaminari-activerecord (= 1.2.2)
kaminari-core (= 1.2.2)
kaminari-actionview (1.2.2)
actionview
kaminari-core (= 1.2.2)
kaminari-activerecord (1.2.2)
activerecord
kaminari-core (= 1.2.2)
kaminari-core (1.2.2)
launchy (2.5.2)
addressable (~> 2.8)
listen (3.9.0)
@ -218,6 +243,10 @@ GEM
mini_mime (1.1.5)
minitest (5.23.1)
msgpack (1.7.2)
multipart-post (2.4.1)
nested_form (0.3.2)
net-http (0.4.1)
uri
net-imap (0.4.12)
date
net-protocol
@ -304,6 +333,12 @@ GEM
rails-html-sanitizer (1.6.0)
loofah (~> 2.21)
nokogiri (~> 1.14)
rails_admin (3.1.2)
activemodel-serializers-xml (>= 1.0)
kaminari (>= 0.14, < 2.0)
nested_form (~> 0.3)
rails (>= 6.0, < 8)
turbo-rails (~> 1.0)
railties (7.0.8.4)
actionpack (= 7.0.8.4)
activesupport (= 7.0.8.4)
@ -378,6 +413,10 @@ GEM
rubocop (~> 1.0)
rubocop-rspec (2.7.0)
rubocop (~> 1.19)
ruby-openai (7.0.1)
event_stream_parser (>= 0.3.0, < 2.0.0)
faraday (>= 1)
faraday-multipart (>= 1)
ruby-progressbar (1.13.0)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
@ -413,12 +452,17 @@ GEM
thor (1.3.1)
timecop (0.9.8)
timeout (0.4.1)
turbo-rails (1.5.0)
actionpack (>= 6.0.0)
activejob (>= 6.0.0)
railties (>= 6.0.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
uk_postcode (2.1.8)
unicode-display_width (2.5.0)
unread (0.13.1)
activerecord (>= 6.1)
uri (0.13.0)
view_component (3.10.0)
activesupport (>= 5.2.0, < 8.0)
concurrent-ruby (~> 1.0)
@ -461,6 +505,7 @@ DEPENDENCIES
capybara
capybara-lockstep
capybara-screenshot
cssbundling-rails
devise
devise_two_factor_authentication
dotenv-rails
@ -473,6 +518,7 @@ DEPENDENCIES
jsbundling-rails
json-schema
listen (~> 3.3)
method_source (~> 1.1)
notifications-ruby-client
overcommit (>= 0.37.0)
paper_trail
@ -489,12 +535,14 @@ DEPENDENCIES
rack-attack
rack-mini-profiler (~> 2.0)
rails (~> 7.0.8.3)
rails_admin (~> 3.0)
redis (~> 4.8)
roo
rspec-rails
rubocop-govuk (= 4.3.0)
rubocop-performance
rubocop-rails
ruby-openai
selenium-webdriver
sentry-rails
sentry-ruby

2
app/assets/stylesheets/rails_admin.scss

@ -0,0 +1,2 @@
$fa-font-path: ".";
@import "rails_admin/src/rails_admin/styles/base";

1
app/javascript/rails_admin.js

@ -0,0 +1 @@
import 'rails_admin/src/rails_admin/base'

2
app/models/log_validation.rb

@ -0,0 +1,2 @@
class LogValidation < ApplicationRecord
end

371
app/services/documentation_generator.rb

@ -0,0 +1,371 @@
class DocumentationGenerator
include Validations::Sales::SetupValidations
include Validations::Sales::HouseholdValidations
include Validations::Sales::PropertyValidations
include Validations::Sales::FinancialValidations
include Validations::Sales::SaleInformationValidations
include Validations::SharedValidations
include Validations::LocalAuthorityValidations
include Validations::SoftValidations
include Validations::Sales::SoftValidations
def describe_hard_validations(client, all_validation_methods, all_helper_methods, log_type)
form = FormHandler.instance.forms["current_#{log_type}"]
all_validation_methods.each do |meth|
if LogValidation.where(validation_name: meth.to_s, bulk_upload_specific: false, log_type:).exists?
Rails.logger.info("Validation #{meth} already exists")
next
end
validation_source = method(meth).source
helper_methods_source = all_helper_methods.map { |helper_method|
if validation_source.include?(helper_method.to_s)
method(helper_method).source
end
}.compact.join("\n")
response = describe_hard_validation(client, meth, validation_source, helper_methods_source, form)
next unless response
begin
result = JSON.parse(response.dig("choices", 0, "message", "tool_calls", 0, "function", "arguments"))
save_hard_validation(result, meth, form, log_type)
rescue StandardError => e
Rails.logger.error("Failed to save #{meth} for #{form.start_date.year}")
Rails.logger.error("Error #{e.message}")
end
end
end
def describe_bu_validations(client, form, row_parser_class, all_validation_methods, all_helper_methods, field_mapping_for_errors, log_type)
all_validation_methods.each do |meth|
if LogValidation.where(validation_name: meth.to_s, bulk_upload_specific: true, from: form.start_date, log_type:).exists?
Rails.logger.info("Validation #{meth} already exists for #{form.start_date.year}")
next
end
validation_source = row_parser_class.instance_method(meth).source
helper_methods_source = all_helper_methods.map { |helper_method|
if validation_source.include?(helper_method.to_s)
row_parser_class.instance_method(helper_method).source
end
}.compact.join("\n")
response = describe_hard_validation(client, meth, validation_source, helper_methods_source, form)
next unless response
begin
result = JSON.parse(response.dig("choices", 0, "message", "tool_calls", 0, "function", "arguments"))
save_bu_validation(result, meth, form, log_type, field_mapping_for_errors)
rescue StandardError => e
Rails.logger.error("Failed to save #{meth} for #{form.start_date.year}")
Rails.logger.error("Error #{e.message}")
end
end
end
def describe_soft_validations(client, all_validation_methods, all_helper_methods, log_type)
validation_descriptions = {}
all_validation_methods.each do |meth|
validation_source = method(meth).source
helper_methods_source = all_helper_methods.map { |helper_method|
if validation_source.include?(helper_method.to_s)
method(helper_method).source
end
}.compact.join("\n")
response = soft_validation_description(client, meth, validation_source, helper_methods_source)
next unless response
result = JSON.parse(response.dig("choices", 0, "message", "tool_calls", 0, "function", "arguments"))
validation_descriptions[meth.to_s] = result
end
current_form = FormHandler.instance.forms["current_#{log_type}"]
previous_form = FormHandler.instance.forms["previous_#{log_type}"]
[current_form, previous_form].each do |form|
interruption_screen_pages = form.pages.select { |page| page.questions.first.type == "interruption_screen" }
interruption_screen_pages_grouped_by_question = interruption_screen_pages.group_by { |page| page.questions.first.id }
interruption_screen_pages_grouped_by_question.each do |_question_id, pages|
pages.map do |page|
save_soft_validation(form, page, validation_descriptions, log_type)
end
end
end
end
private
def describe_hard_validation(client, meth, validation_source, helper_methods_source, form)
en_yml = File.read("./config/locales/en.yml")
begin
client.chat(
parameters: {
model: "gpt-3.5-turbo",
messages: [
{
role: "system",
content: "You write amazing documentation, as a senior technical writer. Your audience is non-technical team members. You have been asked to document the validations in a Rails application. The application collects social housing data for different collection years. There are validations on different fields, sometimes the validations depend on several fields.
Describe what given validation does, be very explicit about all the different validation cases (be specific about the years for which these validations would run, which values would be invalid or which values are required, look at private helper methods to understand what is being checked in more detail). Quote the error messages that would be added in each case exactly. Here is the translation file for validation messages: #{en_yml}.
You should create `create_documentation_for_given_validation` method. Call it once, and include the documentation for given validation.",
},
{
role: "user",
content: "Describe #{meth} validation in detail. Here is the content of the validation:
#{validation_source}
Look at these helper methods where needed to understand what is being checked in more detail: #{helper_methods_source}",
},
],
tools: [
{
type: "function",
function: {
name: "create_documentation_for_given_validation",
description: "Use this function to save the complete documentation, covering given validation in the provided code.",
parameters: {
type: :object,
properties: {
description: {
type: :string,
description: "A human-readbale description of the validation",
},
cases: {
type: :array,
description: "A list of cases that this validation triggers on, each with specific details",
items: {
type: :object,
description: "Information about a single case that triggers this validation",
properties: {
case_description: {
type: :string,
description: "A human-readable description of the case in which this validation triggers",
},
errors: {
type: :array,
description: "The error messages that would be added if this case triggers the validation",
items: {
type: :object,
description: "Information about a single error message for a specific field",
properties: {
error_message: {
type: :string,
description: "A single error message",
},
field: {
type: :string,
description: "The field that the error message would be added to.",
},
},
required: %w[error_message field],
},
},
from: {
type: :number,
description: "the year from which the validation starts. If this validation runs for logs with a startdate after a certain year, specify that year here, only if it is not specified in the validation method, leave this field blank",
},
to: {
type: :number,
description: "the year in which the validation ends. If this validation runs for logs with a startdate before a certain year, specify that year here, only if it is not specified in the validation method, leave this field blank",
},
validation_type: {
type: :string,
enum: %w[presence format minimum maximum range inclusion length other],
description: "The type of validation that is being performed. This should be one of the following: presence (validates that the question is answered), format (validates that the answer format is valid), minimum (validates that entered value is more than minimum allowed value), maximum (validates that entered value is less than maximum allowed value), range (values must be between two values), inclusion (validates that the values that are not allowed arent selected), length (validates the length of the answer), other",
},
other_validated_models: {
type: :string,
description: "Comma separated list of any other models (other than log) that were used in this validation. These are possible models (only add a value to this field if other validation models are one of these models): User, Organisation, Scheme, Location, Organisation_relationship, LaRentRange. Only leave this blank if no other models were used in this validation.",
},
},
required: %w[case_description errors validation_type other_validated_models],
},
},
},
required: %w[description cases],
},
},
},
],
tool_choice: { type: "function", function: { name: "create_documentation_for_given_validation" } },
},
)
rescue StandardError => e
Rails.logger.error("Failed to describe #{meth} for #{form.start_date.year}")
Rails.logger.error("Error #{e.message}")
sleep(15)
false
end
end
def soft_validation_description(client, meth, validation_source, helper_methods_source)
client.chat(
parameters: {
model: "gpt-3.5-turbo",
messages: [
{
role: "system",
content: "You write amazing documentation, as a senior technical writer. Your audience is non-technical team members. You have been asked to document the validations in a Rails application. The application collects social housing data for different collection years. There are validations on different fields, sometimes the validations depend on several fields.
You are given a method that contains a validation. Describe what given method does, be very explicit about all the different validation cases (be specific about the years for which these validations would run, which values would be invalid or which values are required, look at private helper methods to understand what is being checked in more detail).
You should create `create_documentation_for_given_validation` method. Call it once, and include the documentation for given validation.",
},
{
role: "user",
content: "Describe #{meth} validation in detail. Here is the content of the validation:
#{validation_source}
Look at these helper methods where needed to understand what is being checked in more detail: #{helper_methods_source}",
},
],
tools: [
{
type: "function",
function: {
name: "create_documentation_for_given_validation",
description: "Use this function to save the complete documentation, covering given validation in the provided code.",
parameters: {
type: :object,
properties: {
description: {
type: :string,
description: "A human-readbale description of the validation",
},
validation_type: {
type: :string,
enum: %w[presence format minimum maximum range inclusion length other],
description: "The type of validation that is being performed. This should be one of the following: presence (validates that the question is answered), format (validates that the answer format is valid), minimum (validates that entered value is more than minimum allowed value), maximum (validates that entered value is less than maximum allowed value), range (values must be between two values), inclusion (validates that the values that are not allowed arent selected), length (validates the length of the answer), other",
},
other_validated_models: {
type: :string,
description: "Comma separated list of any other models (other than log) that were used in this validation. These are possible models (only add a value to this field if other validation models are one of these models): User, Organisation, Scheme, Location, Organisation_relationship, LaRentRange. Only leave this blank if no other models were used in this validation.",
},
},
required: %w[description validation_type other_validated_models],
},
},
},
],
tool_choice: { type: "function", function: { name: "create_documentation_for_given_validation" } },
},
)
rescue StandardError => e
Rails.logger.error("Failed to describe #{meth}")
Rails.logger.error("Error #{e.message}")
sleep(15)
false
end
def save_hard_validation(result, meth, form, log_type)
result["cases"].each do |case_info|
case_info["errors"].each do |error|
LogValidation.create!(log_type:,
validation_name: meth.to_s,
description: result["description"],
field: error["field"],
error_message: error["error_message"],
case: case_info["case_description"],
section: form.get_question(error["field"], nil)&.subsection&.id,
from: case_info["from"] || "",
to: case_info["to"] || "",
validation_type: case_info["validation_type"],
hard_soft: "hard",
other_validated_models: case_info["other_validated_models"])
end
end
Rails.logger.info("******** described #{meth} ********")
end
def save_bu_validation(result, meth, form, log_type, field_mapping_for_errors)
result["cases"].each do |case_info|
case_info["errors"].each do |error|
error_fields = field_mapping_for_errors.select { |_key, values| values.include?(error["field"].to_sym) }.keys
error_fields = [error["field"]] if error_fields.empty?
error_fields.each do |error_field|
LogValidation.create!(log_type:,
validation_name: meth.to_s,
description: result["description"],
field: error_field,
error_message: error["error_message"],
case: case_info["case_description"],
section: form.get_question(error_field, nil)&.subsection&.id,
from: form.start_date,
to: form.start_date + 1.year,
validation_type: case_info["validation_type"],
hard_soft: "hard",
other_validated_models: case_info["other_validated_models"],
bulk_upload_specific: true)
end
end
end
Rails.logger.info("******** described #{meth} for #{form.start_date.year} ********")
end
def save_soft_validation(form, page, validation_descriptions, log_type)
subsection_pages = form.subsection_for_page(page).pages
page_index = subsection_pages.index(page)
page_the_validation_applied_to = subsection_pages[page_index - 1]
loop do
break unless page_the_validation_applied_to.questions.first.type == "interruption_screen"
page_index -= 1
page_the_validation_applied_to = subsection_pages[page_index - 1]
end
validation_depends_on_hash = page.depends_on.each_with_object({}) do |depends_on, result|
depends_on.each do |key, value|
if validation_descriptions.include?(key)
result[key] = value
end
end
end
if validation_depends_on_hash.empty?
Rails.logger.error("No validation description found for #{page.questions.first.id}")
return
end
if LogValidation.where(validation_name: validation_depends_on_hash.keys.first, field: page_the_validation_applied_to.questions.first.id, from: form.start_date, log_type:).exists?
Rails.logger.info("Validation #{validation_depends_on_hash.keys.first} already exists for #{page_the_validation_applied_to.questions.first.id} for start year #{form.start_date.year}")
return
end
result = validation_descriptions[validation_depends_on_hash.keys.first]
informative_text = page.informative_text
if informative_text.present? && !(informative_text.is_a? String)
informative_text = I18n.t(page.informative_text["translation"])
end
title_text = page.title_text
if title_text.present? && !(title_text.is_a? String)
title_text = I18n.t(page.title_text["translation"])
end
error_message = [title_text, informative_text, page.questions.first.hint_text].compact.join("\n")
case_info = page.depends_on.first.values.first ? "Provided values fulfill the description" : "Provided values do not fulfill the description"
LogValidation.create!(log_type:,
validation_name: validation_depends_on_hash.keys.first.to_s,
description: result["description"],
field: page_the_validation_applied_to.questions.first.id,
error_message:,
case: case_info,
section: form.get_question(page_the_validation_applied_to.questions.first.id, nil)&.subsection&.id,
from: form.start_date,
to: form.start_date + 1.year,
validation_type: result["validation_type"],
hard_soft: "soft",
other_validated_models: result["other_validated_models"])
Rails.logger.info("******** described #{validation_depends_on_hash.keys.first} for #{page_the_validation_applied_to.questions.first.id} ********")
end
end

1
config/initializers/assets.rb

@ -0,0 +1 @@
Rails.application.config.assets.paths << Rails.root.join("node_modules/@fortawesome/fontawesome-free/webfonts")

46
config/initializers/rails_admin.rb

@ -0,0 +1,46 @@
RailsAdmin.config do |config|
config.asset_source = :webpack
### Popular gems integration
## == Devise ==
config.authenticate_with do
warden.authenticate! scope: :user
end
config.current_user_method(&:current_user)
config.authorize_with do
redirect_to main_app.root_path unless current_user&.support?
end
## == CancanCan ==
# config.authorize_with :cancancan
## == Pundit ==
# config.authorize_with :pundit
## == PaperTrail ==
# config.audit_with :paper_trail, 'User', 'PaperTrail::Version' # PaperTrail >= 3.0.0
### More at https://github.com/railsadminteam/rails_admin/wiki/Base-configuration
## == Gravatar integration ==
## To disable Gravatar integration in Navigation Bar set to false
# config.show_gravatar = true
config.included_models = %w[LogValidation]
config.actions do
dashboard # mandatory
index # mandatory
new
export
bulk_delete
show
edit
delete
show_in_app
## With an audit adapter, you can add:
# history_index
# history_show
end
end

1
config/routes.rb

@ -1,5 +1,6 @@
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
Rails.application.routes.draw do
mount RailsAdmin::Engine => "/admin", as: "rails_admin"
mount_sidekiq = -> { mount Sidekiq::Web => "/sidekiq" }
authenticate(:user, :support?.to_proc, &mount_sidekiq)

20
db/migrate/20240523153434_add_validations_table.rb

@ -0,0 +1,20 @@
class AddValidationsTable < ActiveRecord::Migration[7.0]
def change
create_table :validations do |t|
t.column :log_type, :string
t.column :section, :string
t.column :validation_name, :string
t.column :description, :string
t.column :case, :string
t.column :field, :string
t.column :error_message, :string
t.column :from, :datetime
t.column :to, :datetime
t.column :validation_type, :string
t.column :hard_soft, :string
t.column :bulk_upload_specific, :boolean, default: false
t.column :other_validated_models, :string
t.timestamps
end
end
end

5
db/migrate/20240529133005_rename_validations_table.rb

@ -0,0 +1,5 @@
class RenameValidationsTable < ActiveRecord::Migration[7.0]
def change
rename_table :validations, :log_validations
end
end

20
db/schema.rb

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2024_04_08_102550) do
ActiveRecord::Schema[7.0].define(version: 2024_05_29_133005) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -381,6 +381,24 @@ ActiveRecord::Schema[7.0].define(version: 2024_04_08_102550) do
t.index ["scheme_id"], name: "index_locations_on_scheme_id"
end
create_table "log_validations", force: :cascade do |t|
t.string "log_type"
t.string "section"
t.string "validation_name"
t.string "description"
t.string "case"
t.string "field"
t.string "error_message"
t.datetime "from"
t.datetime "to"
t.string "validation_type"
t.string "hard_soft"
t.boolean "bulk_upload_specific", default: false
t.string "other_validated_models"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "logs_exports", force: :cascade do |t|
t.datetime "created_at", default: -> { "CURRENT_TIMESTAMP" }
t.datetime "started_at", null: false

90
lib/tasks/generate_lettings_documentation.rake

@ -0,0 +1,90 @@
namespace :generate_lettings_documentation do
desc "Generate documentation for hard lettings validations"
task describe_lettings_validations: :environment do
client = OpenAI::Client.new(access_token: ENV["OPENAI_API_KEY"])
include Validations::SetupValidations
include Validations::HouseholdValidations
include Validations::PropertyValidations
include Validations::FinancialValidations
include Validations::TenancyValidations
include Validations::DateValidations
include Validations::LocalAuthorityValidations
all_validation_methods = public_methods.select { |method| method.starts_with?("validate_") }
all_methods = [Validations::SetupValidations,
Validations::HouseholdValidations,
Validations::PropertyValidations,
Validations::FinancialValidations,
Validations::TenancyValidations,
Validations::DateValidations,
Validations::LocalAuthorityValidations].map { |x| x.instance_methods + x.private_instance_methods }.flatten
all_helper_methods = all_methods - all_validation_methods
DocumentationGenerator.new.describe_hard_validations(client, all_validation_methods, all_helper_methods, "lettings")
end
desc "Generate documentation for soft lettings validations"
task describe_soft_lettings_validations: :environment do
include Validations::SoftValidations
client = OpenAI::Client.new(access_token: ENV["OPENAI_API_KEY"])
all_helper_methods = Validations::SoftValidations.private_instance_methods
all_validation_methods = Validations::SoftValidations.instance_methods
DocumentationGenerator.new.describe_soft_validations(client, all_validation_methods, all_helper_methods, "lettings")
end
desc "Generate documentation for hard bu lettings validations"
task describe_bu_lettings_validations: :environment do
client = OpenAI::Client.new(access_token: ENV["OPENAI_API_KEY"])
[[FormHandler.instance.forms[FormHandler.instance.form_name_from_start_year(2023, "lettings")], BulkUpload::Lettings::Year2023::RowParser],
[FormHandler.instance.forms[FormHandler.instance.form_name_from_start_year(2024, "lettings")], BulkUpload::Lettings::Year2024::RowParser]].each do |form, row_parser_class|
all_validation_methods = row_parser_class.private_instance_methods.select { |method| method.starts_with?("validate_") }
all_helper_methods = row_parser_class.private_instance_methods(false) + row_parser_class.instance_methods(false) - all_validation_methods
field_mapping_for_errors = row_parser_class.new.send("field_mapping_for_errors")
DocumentationGenerator.new.describe_bu_validations(client, form, row_parser_class, all_validation_methods, all_helper_methods, field_mapping_for_errors, "lettings")
end
end
desc "Generate documentation for lettings numeric validations"
task add_numeric_lettings_validations: :environment do
form = FormHandler.instance.forms["current_lettings"]
form.numeric_questions.each do |question|
next unless question.min || question.max
field = question.id
min = [question.prefix, question.min].join("") if question.min
max = [question.prefix, question.max].join("") if question.max
error_message = I18n.t("validations.numeric.above_min", field:, min:)
validation_name = "minimum"
validation_description = "Field value is lower than the minimum value"
if min && max
validation_name = "range"
error_message = I18n.t("validations.numeric.within_range", field:, min:, max:)
validation_description = "Field value is lower than the minimum value or higher than the maximum value"
end
if LogValidation.where(validation_name:, field:, log_type: "lettings").exists?
Rails.logger.info("Validation #{validation_name} already exists for #{field}")
next
end
LogValidation.create!(log_type: "lettings",
validation_name:,
description: validation_description,
field:,
error_message:,
case: validation_description,
section: form.get_question(field, nil)&.subsection&.id,
validation_type: validation_name,
hard_soft: "hard")
end
end
end

89
lib/tasks/generate_sales_documentation.rake

@ -0,0 +1,89 @@
namespace :generate_sales_documentation do
desc "Generate documentation for hard sales validations"
task describe_sales_validations: :environment do
client = OpenAI::Client.new(access_token: ENV["OPENAI_API_KEY"])
include Validations::Sales::SetupValidations
include Validations::Sales::HouseholdValidations
include Validations::Sales::PropertyValidations
include Validations::Sales::FinancialValidations
include Validations::Sales::SaleInformationValidations
include Validations::SharedValidations
include Validations::LocalAuthorityValidations
all_validation_methods = public_methods.select { |method| method.starts_with?("validate_") }
all_methods = [Validations::Sales::SetupValidations,
Validations::Sales::HouseholdValidations,
Validations::Sales::PropertyValidations,
Validations::Sales::FinancialValidations,
Validations::Sales::SaleInformationValidations,
Validations::SharedValidations,
Validations::LocalAuthorityValidations].map { |x| x.instance_methods + x.private_instance_methods }.flatten
all_helper_methods = all_methods - all_validation_methods
DocumentationGenerator.new.describe_hard_validations(client, all_validation_methods, all_helper_methods, "sales")
end
desc "Generate documentation for soft sales validations"
task describe_soft_sales_validations: :environment do
include Validations::SoftValidations
include Validations::Sales::SoftValidations
client = OpenAI::Client.new(access_token: ENV["OPENAI_API_KEY"])
all_helper_methods = Validations::SoftValidations.private_instance_methods + Validations::Sales::SoftValidations.private_instance_methods
all_validation_methods = Validations::SoftValidations.instance_methods + Validations::Sales::SoftValidations.instance_methods
DocumentationGenerator.new.describe_soft_validations(client, all_validation_methods, all_helper_methods, "sales")
end
desc "Generate documentation for hard bu sales validations"
task describe_bu_sales_validations: :environment do
client = OpenAI::Client.new(access_token: ENV["OPENAI_API_KEY"])
[[FormHandler.instance.forms[FormHandler.instance.form_name_from_start_year(2023, "sales")], BulkUpload::Sales::Year2023::RowParser],
[FormHandler.instance.forms[FormHandler.instance.form_name_from_start_year(2024, "sales")], BulkUpload::Sales::Year2024::RowParser]].each do |form, row_parser_class|
all_validation_methods = row_parser_class.private_instance_methods.select { |method| method.starts_with?("validate_") }
all_helper_methods = row_parser_class.private_instance_methods(false) + row_parser_class.instance_methods(false) - all_validation_methods
field_mapping_for_errors = row_parser_class.new.send("field_mapping_for_errors")
DocumentationGenerator.new.describe_bu_validations(client, form, row_parser_class, all_validation_methods, all_helper_methods, field_mapping_for_errors, "sales")
end
end
desc "Generate documentation for sales numeric validations"
task add_numeric_sales_validations: :environment do
form = FormHandler.instance.forms["current_sales"]
form.numeric_questions.each do |question|
next unless question.min || question.max
field = question.id
min = [question.prefix, question.min].join("") if question.min
max = [question.prefix, question.max].join("") if question.max
error_message = I18n.t("validations.numeric.above_min", field:, min:)
validation_name = "minimum"
validation_description = "Field value is lower than the minimum value"
if min && max
validation_name = "range"
error_message = I18n.t("validations.numeric.within_range", field:, min:, max:)
validation_description = "Field value is lower than the minimum value or higher than the maximum value"
end
if LogValidation.where(validation_name:, field:, log_type: "sales").exists?
Rails.logger.info("Validation #{validation_name} already exists for #{field}")
next
end
LogValidation.create!(log_type: "sales",
validation_name:,
description: validation_description,
field:,
error_message:,
case: validation_description,
section: form.get_question(field, nil)&.subsection&.id,
validation_type: validation_name,
hard_soft: "hard")
end
end
end

4
package.json

@ -24,6 +24,7 @@
"html5shiv": "^3.7.3",
"intersection-observer": "^0.12.0",
"mini-css-extract-plugin": "^2.6.0",
"rails_admin": "3.1.2",
"regenerator-runtime": "^0.13.9",
"sass": "^1.49.9",
"sass-loader": "^12.6.0",
@ -60,6 +61,7 @@
"extends": "stylelint-config-gds/scss"
},
"scripts": {
"build": "webpack --config webpack.config.js"
"build": "webpack --config webpack.config.js",
"build:css": "sass ./app/assets/stylesheets/rails_admin.scss:./app/assets/builds/rails_admin.css --no-source-map --load-path=node_modules"
}
}

38
spec/lib/tasks/generate_lettings_documentation_spec.rb

@ -0,0 +1,38 @@
require "rails_helper"
require "rake"
RSpec.describe "generate_lettings_documentation" do
describe ":add_numeric_lettings_validations", type: :task do
subject(:task) { Rake::Task["generate_lettings_documentation:add_numeric_lettings_validations"] }
before do
Rake.application.rake_require("tasks/generate_lettings_documentation")
Rake::Task.define_task(:environment)
task.reenable
end
context "when the rake task is run" do
it "creates new validation documentation records" do
expect { task.invoke }.to change(LogValidation, :count)
expect(LogValidation.where(validation_name: "minimum").count).to be_positive
expect(LogValidation.where(validation_name: "range").count).to be_positive
any_min_validation = LogValidation.where(validation_name: "minimum").first
expect(any_min_validation.description).to include("Field value is lower than the minimum value")
expect(any_min_validation.field).not_to be_empty
expect(any_min_validation.error_message).to include("must be at least")
expect(any_min_validation.case).to include("Field value is lower than the minimum value")
expect(any_min_validation.from).to be_nil
expect(any_min_validation.to).to be_nil
expect(any_min_validation.validation_type).to eq("minimum")
expect(any_min_validation.hard_soft).to eq("hard")
expect(any_min_validation.other_validated_models).to be_nil
expect(any_min_validation.log_type).to eq("lettings")
end
it "skips if the validation already exists in the database" do
task.invoke
expect { task.invoke }.not_to change(LogValidation, :count)
end
end
end
end

38
spec/lib/tasks/generate_sales_documentation_spec.rb

@ -0,0 +1,38 @@
require "rails_helper"
require "rake"
RSpec.describe "generate_sales_documentation" do
describe ":add_numeric_sales_validations", type: :task do
subject(:task) { Rake::Task["generate_sales_documentation:add_numeric_sales_validations"] }
before do
Rake.application.rake_require("tasks/generate_sales_documentation")
Rake::Task.define_task(:environment)
task.reenable
end
context "when the rake task is run" do
it "creates new validation documentation records" do
expect { task.invoke }.to change(LogValidation, :count)
expect(LogValidation.where(validation_name: "minimum").count).to be_positive
expect(LogValidation.where(validation_name: "range").count).to be_positive
any_min_validation = LogValidation.where(validation_name: "minimum").first
expect(any_min_validation.description).to include("Field value is lower than the minimum value")
expect(any_min_validation.field).not_to be_empty
expect(any_min_validation.error_message).to include("must be at least")
expect(any_min_validation.case).to include("Field value is lower than the minimum value")
expect(any_min_validation.from).to be_nil
expect(any_min_validation.to).to be_nil
expect(any_min_validation.validation_type).to eq("minimum")
expect(any_min_validation.hard_soft).to eq("hard")
expect(any_min_validation.other_validated_models).to be_nil
expect(any_min_validation.log_type).to eq("sales")
end
it "skips if the validation already exists in the database" do
task.invoke
expect { task.invoke }.not_to change(LogValidation, :count)
end
end
end
end

46
spec/requests/rails_admin_controller_spec.rb

@ -0,0 +1,46 @@
require "rails_helper"
RSpec.describe "RailsAdmin", type: :request do
let(:user) { create(:user) }
let(:support_user) { create(:user, :support) }
let(:page) { Capybara::Node::Simple.new(response.body) }
before do
allow(support_user).to receive(:need_two_factor_authentication?).and_return(false)
end
describe "GET /admin" do
context "when the user is not signed in" do
it "routes user to the sign in page" do
get rails_admin_path
follow_redirect!
expect(path).to eq("/account/sign-in")
expect(page).to have_content("Sign in to your account")
end
end
context "when the user is signed in as a non support user" do
before do
sign_in user
end
it "routes user to the home page" do
get rails_admin_path
follow_redirect!
expect(path).to eq("/")
expect(page).to have_content("Welcome back")
end
end
context "when the user is signed in as a support user" do
before do
sign_in support_user
end
it "routes user to the admin page" do
get rails_admin_path
expect(page).to have_content("Site Administration")
end
end
end
end

230
spec/services/documentation_generator_spec.rb

@ -0,0 +1,230 @@
require "rails_helper"
describe DocumentationGenerator do
let(:client) { instance_double(OpenAI::Client) }
let(:response) do
{ "choices" => [{ "message" => { "tool_calls" => [{ "function" => { "arguments" =>
"{\n \"description\": \"Validates the format.\",\n \"cases\": [\n {\n \"case_description\": \"Previous postcode is known and current postcode is blank\",\n \"errors\": [\n {\n \"error_message\": \"Enter a valid postcode\",\n \"field\": \"ppostcode_full\"\n }\n ],\n \"validation_type\": \"format\",\n \"other_validated_models\": \"User\" }]\n}" } }] } }] }
end
let(:all_validation_methods) { %w[validate_numeric_min_max] }
let(:all_helper_methods) { [] }
let(:log_type) { "lettings" }
before do
allow(client).to receive(:chat).and_return(response)
end
describe ":describe_hard_validations" do
context "when the service is run with lettings type" do
let(:log_type) { "lettings" }
it "creates new validation documentation records" do
expect(Rails.logger).to receive(:info).with(/described/).at_least(:once)
expect { described_class.new.describe_hard_validations(client, all_validation_methods, all_helper_methods, log_type) }.to change(LogValidation, :count)
expect(LogValidation.where(validation_name: "validate_numeric_min_max").count).to eq(1)
any_validation = LogValidation.first
expect(any_validation.description).to eq("Validates the format.")
expect(any_validation.field).to eq("ppostcode_full")
expect(any_validation.error_message).to eq("Enter a valid postcode")
expect(any_validation.case).to eq("Previous postcode is known and current postcode is blank")
expect(any_validation.from).to be_nil
expect(any_validation.to).to be_nil
expect(any_validation.validation_type).to eq("format")
expect(any_validation.hard_soft).to eq("hard")
expect(any_validation.other_validated_models).to eq("User")
expect(any_validation.log_type).to eq("lettings")
end
it "calls the client" do
expect(client).to receive(:chat)
described_class.new.describe_hard_validations(client, all_validation_methods, all_helper_methods, log_type)
end
it "skips if the validation already exists in the database" do
described_class.new.describe_hard_validations(client, all_validation_methods, all_helper_methods, log_type)
expect { described_class.new.describe_hard_validations(client, all_validation_methods, all_helper_methods, log_type) }.not_to change(LogValidation, :count)
end
context "when the response is not a JSON" do
let(:response) { "not a JSON" }
it "raises an error" do
expect(Rails.logger).to receive(:error).with(/Failed to save/).at_least(:once)
expect(Rails.logger).to receive(:error).with(/Error/).at_least(:once)
described_class.new.describe_hard_validations(client, all_validation_methods, all_helper_methods, log_type)
end
end
context "when the response does not have expected fields" do
let(:response) { { "choices" => [{ "message" => { "tool_calls" => [{ "function" => { "arguments" => "{}" } }] } }] } }
it "raises an error" do
expect(Rails.logger).to receive(:error).with(/Failed to save/).at_least(:once)
expect(Rails.logger).to receive(:error).with(/Error/).at_least(:once)
described_class.new.describe_hard_validations(client, all_validation_methods, all_helper_methods, log_type)
end
end
end
context "when the service is run with sales type" do
let(:log_type) { "sales" }
it "creates new validation documentation records" do
expect(Rails.logger).to receive(:info).with(/described/).at_least(:once)
expect { described_class.new.describe_hard_validations(client, all_validation_methods, all_helper_methods, log_type) }.to change(LogValidation, :count)
expect(LogValidation.where(validation_name: "validate_numeric_min_max").count).to eq(1)
any_validation = LogValidation.first
expect(any_validation.description).to eq("Validates the format.")
expect(any_validation.field).to eq("ppostcode_full")
expect(any_validation.error_message).to eq("Enter a valid postcode")
expect(any_validation.case).to eq("Previous postcode is known and current postcode is blank")
expect(any_validation.from).to be_nil
expect(any_validation.to).to be_nil
expect(any_validation.validation_type).to eq("format")
expect(any_validation.hard_soft).to eq("hard")
expect(any_validation.other_validated_models).to eq("User")
expect(any_validation.log_type).to eq("sales")
end
end
end
describe ":describe_soft_validations" do
let(:all_validation_methods) { ["rent_in_soft_min_range?"] }
let(:response) do
{ "choices" => [{ "message" => { "tool_calls" => [{ "function" => { "arguments" =>
"{\n \"description\": \"Validates the format.\",\n \"validation_type\": \"format\",\n \"other_validated_models\": \"User\"}" } }] } }] }
end
context "when the service is run for lettings" do
let(:log_type) { "lettings" }
it "creates new validation documentation records" do
expect { described_class.new.describe_soft_validations(client, all_validation_methods, all_helper_methods, log_type) }.to change(LogValidation, :count)
expect(LogValidation.where(validation_name: "rent_in_soft_min_range?").count).to be_positive
any_validation = LogValidation.first
expect(any_validation.description).to eq("Validates the format.")
expect(any_validation.field).not_to be_empty
expect(any_validation.error_message).not_to be_empty
expect(any_validation.case).to eq("Provided values fulfill the description")
expect(any_validation.from).not_to be_nil
expect(any_validation.to).not_to be_nil
expect(any_validation.validation_type).to eq("format")
expect(any_validation.hard_soft).to eq("soft")
expect(any_validation.other_validated_models).to eq("User")
expect(any_validation.log_type).to eq("lettings")
end
it "calls the client" do
expect(client).to receive(:chat)
described_class.new.describe_soft_validations(client, all_validation_methods, all_helper_methods, log_type)
end
it "skips if the validation already exists in the database" do
described_class.new.describe_soft_validations(client, all_validation_methods, all_helper_methods, log_type)
expect { described_class.new.describe_soft_validations(client, all_validation_methods, all_helper_methods, log_type) }.not_to change(LogValidation, :count)
end
end
context "when the service is run for sales" do
let(:log_type) { "sales" }
let(:all_validation_methods) { ["income2_under_soft_min?"] }
it "creates new validation documentation records" do
expect { described_class.new.describe_soft_validations(client, all_validation_methods, all_helper_methods, log_type) }.to change(LogValidation, :count)
expect(LogValidation.where(validation_name: "income2_under_soft_min?").count).to be_positive
any_validation = LogValidation.first
expect(any_validation.description).to eq("Validates the format.")
expect(any_validation.field).not_to be_empty
expect(any_validation.error_message).not_to be_empty
expect(any_validation.case).to eq("Provided values fulfill the description")
expect(any_validation.from).not_to be_nil
expect(any_validation.to).not_to be_nil
expect(any_validation.validation_type).to eq("format")
expect(any_validation.hard_soft).to eq("soft")
expect(any_validation.other_validated_models).to eq("User")
expect(any_validation.log_type).to eq("sales")
end
end
end
describe ":describe_bu_validations", type: :task do
let(:all_validation_methods) { %w[validate_owning_org_data_given] }
let(:field_mapping_for_errors) { row_parser_class.new.send("field_mapping_for_errors") }
context "when the service is run for lettings" do
let(:log_type) { "lettings" }
let(:form) { FormHandler.instance.forms[FormHandler.instance.form_name_from_start_year(2023, "lettings")] }
let(:row_parser_class) { BulkUpload::Lettings::Year2023::RowParser }
it "creates new validation documentation records" do
expect(Rails.logger).to receive(:info).with(/described/).at_least(:once)
expect { described_class.new.describe_bu_validations(client, form, row_parser_class, all_validation_methods, all_helper_methods, field_mapping_for_errors, log_type) }.to change(LogValidation, :count)
expect(LogValidation.where(validation_name: "validate_owning_org_data_given").count).to eq(1)
any_validation = LogValidation.first
expect(any_validation.description).to eq("Validates the format.")
expect(any_validation.field).to eq("ppostcode_full")
expect(any_validation.error_message).to eq("Enter a valid postcode")
expect(any_validation.case).to eq("Previous postcode is known and current postcode is blank")
expect(any_validation.from).not_to be_nil
expect(any_validation.to).not_to be_nil
expect(any_validation.validation_type).to eq("format")
expect(any_validation.hard_soft).to eq("hard")
expect(any_validation.other_validated_models).to eq("User")
expect(any_validation.log_type).to eq("lettings")
end
it "calls the client" do
expect(client).to receive(:chat)
described_class.new.describe_bu_validations(client, form, row_parser_class, all_validation_methods, all_helper_methods, field_mapping_for_errors, log_type)
end
it "skips if the validation already exists in the database" do
described_class.new.describe_bu_validations(client, form, row_parser_class, all_validation_methods, all_helper_methods, field_mapping_for_errors, log_type)
expect { described_class.new.describe_bu_validations(client, form, row_parser_class, all_validation_methods, all_helper_methods, field_mapping_for_errors, log_type) }.not_to change(LogValidation, :count)
end
context "when the response is not a JSON" do
let(:response) { "not a JSON" }
it "raises an error" do
expect(Rails.logger).to receive(:error).with(/Failed to save/).at_least(:once)
expect(Rails.logger).to receive(:error).with(/Error/).at_least(:once)
described_class.new.describe_bu_validations(client, form, row_parser_class, all_validation_methods, all_helper_methods, field_mapping_for_errors, log_type)
end
end
context "when the response does not have expected fields" do
let(:response) { { "choices" => [{ "message" => { "tool_calls" => [{ "function" => { "arguments" => "{}" } }] } }] } }
it "raises an error" do
expect(Rails.logger).to receive(:error).with(/Failed to save/).at_least(:once)
expect(Rails.logger).to receive(:error).with(/Error/).at_least(:once)
described_class.new.describe_bu_validations(client, form, row_parser_class, all_validation_methods, all_helper_methods, field_mapping_for_errors, log_type)
end
end
end
context "when the service is run for sales" do
let(:log_type) { "sales" }
let(:form) { FormHandler.instance.forms[FormHandler.instance.form_name_from_start_year(2023, "sales")] }
let(:row_parser_class) { BulkUpload::Sales::Year2023::RowParser }
it "creates new validation documentation records" do
expect(Rails.logger).to receive(:info).with(/described/).at_least(:once)
expect { described_class.new.describe_bu_validations(client, form, row_parser_class, all_validation_methods, all_helper_methods, field_mapping_for_errors, log_type) }.to change(LogValidation, :count)
expect(LogValidation.where(validation_name: "validate_owning_org_data_given").count).to eq(1)
any_validation = LogValidation.first
expect(any_validation.description).to eq("Validates the format.")
expect(any_validation.field).to eq("ppostcode_full")
expect(any_validation.error_message).to eq("Enter a valid postcode")
expect(any_validation.case).to eq("Previous postcode is known and current postcode is blank")
expect(any_validation.from).not_to be_nil
expect(any_validation.to).not_to be_nil
expect(any_validation.validation_type).to eq("format")
expect(any_validation.hard_soft).to eq("hard")
expect(any_validation.other_validated_models).to eq("User")
expect(any_validation.log_type).to eq("sales")
end
end
end
end

3
webpack.config.js

@ -13,7 +13,8 @@ module.exports = {
entry: {
application: [
'./app/frontend/application.js'
]
],
rails_admin: ['./app/javascript/rails_admin.js']
},
module: {
rules: [

82
yarn.lock

@ -950,6 +950,13 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.16.7":
version "7.24.6"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.6.tgz#5b76eb89ad45e2e4a0a8db54c456251469a3358e"
integrity sha512-Ja18XcETdEl5mzzACGd+DKgaGJzPTCow7EglgwTmHdwokzDFYh/MHua6lU6DV/hjF2IaOJ4oX2nqnjG7RElKOw==
dependencies:
regenerator-runtime "^0.14.0"
"@babel/template@^7.18.10":
version "7.18.10"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71"
@ -1042,11 +1049,29 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
"@fortawesome/fontawesome-free@>=5.15.0 <7.0.0":
version "6.5.2"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.5.2.tgz#310fe90cb5a8dee9698833171b98e7835404293d"
integrity sha512-hRILoInAx8GNT5IMkrtIt9blOdrqHOnPBH+k70aWUAqPZPgopb9G5EQJFpaBx/S8zp2fC+mPW349Bziuk1o28Q==
"@hotwired/stimulus@^3.0.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@hotwired/stimulus/-/stimulus-3.1.0.tgz#20215251e5afe6e0a3787285181ba1bfc9097df0"
integrity sha512-iDMHUhiEJ1xFeicyHcZQQgBzhtk5mPR0QZO3L6wtqzMsJEk2TKECuCQTGKjm+KJTHVY0dKq1dOOAWvODjpd2Mg==
"@hotwired/turbo-rails@^7.1.0":
version "7.3.0"
resolved "https://registry.yarnpkg.com/@hotwired/turbo-rails/-/turbo-rails-7.3.0.tgz#422c21752509f3edcd6c7b2725bbe9e157815f51"
integrity sha512-fvhO64vp/a2UVQ3jue9WTc2JisMv9XilIC7ViZmXAREVwiQ2S4UC7Go8f9A1j4Xu7DBI6SbFdqILk5ImqVoqyA==
dependencies:
"@hotwired/turbo" "^7.3.0"
"@rails/actioncable" "^7.0"
"@hotwired/turbo@^7.3.0":
version "7.3.0"
resolved "https://registry.yarnpkg.com/@hotwired/turbo/-/turbo-7.3.0.tgz#2226000fff1aabda9fd9587474565c9929dbf15d"
integrity sha512-Dcu+NaSvHLT7EjrDrkEmH4qET2ZJZ5IcCWmNXxNQTBwlnE5tBZfN6WxZ842n5cHV52DH/AKNirbPBtcEXDLW4g==
"@humanwhocodes/config-array@^0.10.4":
version "0.10.5"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.10.5.tgz#bb679745224745fff1e9a41961c1d45a49f81c04"
@ -1158,6 +1183,21 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@popperjs/core@^2.11.0":
version "2.11.8"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
"@rails/actioncable@^7.0":
version "7.1.3"
resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-7.1.3.tgz#4db480347775aeecd4dde2405659eef74a458881"
integrity sha512-ojNvnoZtPN0pYvVFtlO7dyEN9Oml1B6IDM+whGKVak69MMYW99lC2NOWXWeE3bmwEydbP/nn6ERcpfjHVjYQjA==
"@rails/ujs@^6.1.4-1":
version "6.1.7"
resolved "https://registry.yarnpkg.com/@rails/ujs/-/ujs-6.1.7.tgz#b09dc5b2105dd267e8374c47e4490240451dc7f6"
integrity sha512-0e7WQ4LE/+LEfW2zfAw9ppsB6A8RmxbdAUPAF++UT80epY+7emuQDkKXmaK0a9lp6An50RvzezI0cIQjp1A58w==
"@socket.io/component-emitter@~3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553"
@ -1789,6 +1829,11 @@ body-parser@1.20.2, body-parser@^1.20.2:
type-is "~1.6.18"
unpipe "1.0.0"
bootstrap@^5.1.3:
version "5.3.3"
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.3.3.tgz#de35e1a765c897ac940021900fcbb831602bac38"
integrity sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
@ -3020,6 +3065,11 @@ flat-cache@^3.0.4:
flatted "^3.1.0"
rimraf "^3.0.2"
flatpickr@^4.6.9:
version "4.6.13"
resolved "https://registry.yarnpkg.com/flatpickr/-/flatpickr-4.6.13.tgz#8a029548187fd6e0d670908471e43abe9ad18d94"
integrity sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw==
flatted@^3.1.0:
version "3.2.7"
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787"
@ -3719,6 +3769,18 @@ jest-worker@^27.4.5:
merge-stream "^2.0.0"
supports-color "^8.0.0"
jquery-ui@^1.12.1:
version "1.13.3"
resolved "https://registry.yarnpkg.com/jquery-ui/-/jquery-ui-1.13.3.tgz#d9f5292b2857fa1f2fdbbe8f2e66081664eb9bc5"
integrity sha512-D2YJfswSJRh/B8M/zCowDpNFfwsDmtfnMPwjJTyvl+CBqzpYwQ+gFYIbUUlzijy/Qvoy30H1YhoSui4MNYpRwA==
dependencies:
jquery ">=1.8.0 <4.0.0"
"jquery@>=1.8.0 <4.0.0", jquery@^3.6.0:
version "3.7.1"
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.7.1.tgz#083ef98927c9a6a74d05a6af02806566d16274de"
integrity sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==
js-sdsl@^4.1.4:
version "4.1.4"
resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.1.4.tgz#78793c90f80e8430b7d8dc94515b6c77d98a26a6"
@ -4629,6 +4691,21 @@ quick-lru@^5.1.1:
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932"
integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==
rails_admin@3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/rails_admin/-/rails_admin-3.1.2.tgz#00d6d85b7a00c89c69b5dbf5f1f4620702626504"
integrity sha512-uIQHN27lBvlav6s5ppmOtVxKN8GIxyhHuDFc9ZbvWgFknR4zgG4/xEUGzKzQ9R34AEsfZ/t8cZbvtvgj+aXp4A==
dependencies:
"@babel/runtime" "^7.16.7"
"@fortawesome/fontawesome-free" ">=5.15.0 <7.0.0"
"@hotwired/turbo-rails" "^7.1.0"
"@popperjs/core" "^2.11.0"
"@rails/ujs" "^6.1.4-1"
bootstrap "^5.1.3"
flatpickr "^4.6.9"
jquery "^3.6.0"
jquery-ui "^1.12.1"
random-bytes@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b"
@ -4751,6 +4828,11 @@ regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.9:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==
regenerator-runtime@^0.14.0:
version "0.14.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f"
integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==
regenerator-transform@^0.15.0:
version "0.15.0"
resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.0.tgz#cbd9ead5d77fae1a48d957cf889ad0586adb6537"

Loading…
Cancel
Save