Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions app/assets/stylesheets/dsfr-extensions.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,37 @@
/* DSFR scope les variantes de couleur des tags aux <a>/<button> uniquement.
On élargit aux tags non-cliquables (<p>, <span>) utilisés en informationnel. */
.fr-tag.fr-tag--brown-cafe-creme {
color: var(--text-action-high-brown-cafe-creme);
background-color: var(--background-action-low-brown-cafe-creme);
}

/* Liste ordonnée des étapes d'un cas d'usage : chip numéroté + nom de step,
un item par ligne. */
.dp-flow-steps {
list-style: none;
padding: 0;
margin: 0;
}
.dp-flow-steps > li {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.25rem 0;
}

/* Bloc de code (YAML, JSON, etc.). Reprend le style scopé existant
sur .markdown-content pre pour la cohérence visuelle. */
.dp-code-block {
background-color: var(--background-contrast-grey);
padding: 1rem;
overflow-x: auto;
border-radius: 4px;
margin-bottom: 1rem;
}
.dp-code-block:last-child {
margin-bottom: 0;
}

/* small fieldset elements are inline and have the same width */
.fr-fieldset__element.small {
flex-basis: 0px;
Expand Down
10 changes: 10 additions & 0 deletions app/controllers/instruction/abstract_catalogue_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class Instruction::AbstractCatalogueController < InstructionController
before_action :set_data_provider

private

def set_data_provider
@data_provider = DataProvider.friendly.find(params.expect(:provider_slug))
authorize [:instruction, @data_provider], :show?
end
end
21 changes: 21 additions & 0 deletions app/controllers/instruction/cas_usages_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
class Instruction::CasUsagesController < Instruction::AbstractCatalogueController
before_action :set_formulaire

def show
@cas_usage = @formulaire.available_forms.find { |form| form.uid == params[:uid] }
raise ActiveRecord::RecordNotFound unless @cas_usage

authorize [:instruction, @cas_usage], :show?
end

private

def set_formulaire
@formulaire = AuthorizationDefinition.find(params.expect(:formulaire_id))
raise ActiveRecord::RecordNotFound unless @formulaire.provider_slug == @data_provider.slug

authorize [:instruction, @formulaire], :show?
rescue StaticApplicationRecord::EntryNotFound
raise ActiveRecord::RecordNotFound
end
end
5 changes: 5 additions & 0 deletions app/controllers/instruction/data_providers_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class Instruction::DataProvidersController < InstructionController
def index
@data_providers = policy_scope([:instruction, DataProvider]).order(:name)
end
end
29 changes: 29 additions & 0 deletions app/controllers/instruction/formulaires_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
class Instruction::FormulairesController < Instruction::AbstractCatalogueController
def index
@formulaires = @data_provider.authorization_definitions
.select { |definition| permitted_definition?(definition) }
end

def show
@formulaire = AuthorizationDefinition.find(params.expect(:id))
raise ActiveRecord::RecordNotFound unless @formulaire.provider_slug == @data_provider.slug

authorize [:instruction, @formulaire], :show?

@cas_usages = @formulaire.available_forms
@demandes_counts = counts_by_form_uid(AuthorizationRequest)
@habilitations_counts = counts_by_form_uid(Authorization)
rescue StaticApplicationRecord::EntryNotFound
raise ActiveRecord::RecordNotFound
end

private

def permitted_definition?(definition)
Instruction::AuthorizationDefinitionPolicy.new(pundit_user, definition).show?
end

def counts_by_form_uid(model)
model.where(form_uid: @cas_usages.map(&:uid)).group(:form_uid).count
end
end
67 changes: 51 additions & 16 deletions app/models/authorization_request_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ class AuthorizationRequestForm < StaticApplicationRecord
:authorization_request_class,
:single_page_view,
:steps,
:static_blocks
:static_blocks,
:form_template

attr_writer :name,
:description,
Expand All @@ -28,40 +29,54 @@ def self.yaml_records
end

def self.db_records
return [] unless HabilitationType.table_exists?
return [] unless FormTemplate.table_exists?

HabilitationType.includes(:data_provider).filter_map do |record|
build_form_from_habilitation_type(record)
FormTemplate.includes(:habilitation_type).filter_map do |template|
build_form_from_template(template)
end
rescue ActiveRecord::NoDatabaseError, ActiveRecord::StatementInvalid
[]
end

def self.build_form_from_habilitation_type(record)
klass = authorization_request_class_for(record)
# rubocop:disable Metrics/AbcSize
def self.build_form_from_template(template)
klass = authorization_request_class_for(template.habilitation_type)
return unless klass

ht = template.habilitation_type
new(
uid: record.slug,
default: true,
introduction: record.form_introduction,
uid: template.slug,
default: template.default,
name: template.name.presence || ht.name,
description: template.description.presence || ht.description,
introduction: template.introduction.presence || ht.form_introduction,
public: template.public,
startable_by_applicant: template.startable_by_applicant,
use_case: template.use_case,
single_page_view: template.single_page_view,
service_provider: template.service_provider,
authorization_request_class: klass,
steps: record.ordered_steps.map { |name| { name: name } },
static_blocks: [],
service_provider: nil,
use_case: nil,
single_page_view: nil,
scopes_config: {},
steps: cascaded_steps(template, ht),
static_blocks: template.static_blocks.map(&:deep_symbolize_keys),
scopes_config: template.scopes_config.deep_symbolize_keys,
initialize_with: template.initialize_with.deep_symbolize_keys,
form_template: template,
)
end
# rubocop:enable Metrics/AbcSize

def self.cascaded_steps(template, habilitation_type)
template.steps.presence&.map(&:deep_symbolize_keys) ||
habilitation_type.ordered_steps.map { |step_name| { name: step_name } }
end

def self.authorization_request_class_for(record)
AuthorizationRequest.const_get(record.uid.classify)
rescue NameError
nil
end

private_class_method :yaml_records, :db_records, :build_form_from_habilitation_type, :authorization_request_class_for
private_class_method :yaml_records, :db_records, :build_form_from_template, :cascaded_steps, :authorization_request_class_for

# rubocop:disable Metrics/AbcSize
def self.build(uid, hash)
Expand Down Expand Up @@ -148,6 +163,26 @@ def public
value_or_default(@public, true)
end

CASCADED_FIELDS = {
name: :name,
description: :description,
introduction: :introduction,
steps: :steps,
}.freeze

def inherited?(field)
return false unless form_template

column = CASCADED_FIELDS[field]
return false unless column

form_template.read_attribute(column).blank?
end

def from_database?
form_template.present?
end

def prefilled?
@initialize_with.present?
end
Expand Down
68 changes: 68 additions & 0 deletions app/models/form_template.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
class FormTemplate < ApplicationRecord
extend FriendlyId

has_paper_trail

friendly_id :name, use: :slugged

belongs_to :habilitation_type

validates :slug, presence: true, uniqueness: true
validate :service_provider_must_exist, if: -> { service_provider_id.present? }
validate :slug_not_taken_by_yaml
validate :ht_keeps_at_least_one_default, on: :update
validate :only_one_default_per_habilitation_type, if: :default?
before_destroy :ensure_not_last_default

after_commit :reset_arf_cache, on: %i[create update destroy]

def service_provider
return nil if service_provider_id.blank?

ServiceProvider.find(service_provider_id)
rescue StaticApplicationRecord::EntryNotFound
nil
end

private

def reset_arf_cache
AuthorizationRequestForm.reset!
end

def service_provider_must_exist
return if ServiceProvider.exists?(id: service_provider_id)

errors.add(:service_provider_id, :inclusion)
end

def slug_not_taken_by_yaml
return if slug.blank?
return unless AuthorizationRequestFormConfigurations.instance.all.key?(slug.to_sym)

errors.add(:slug, :taken_by_yaml_form)
end

def other_defaults
habilitation_type.form_templates.where(default: true).where.not(id: id)
end

def ht_keeps_at_least_one_default
return unless default_was && !default

errors.add(:default, :last_default_form_template) unless other_defaults.exists?
end

def only_one_default_per_habilitation_type
errors.add(:default, :already_taken) if other_defaults.exists?
end

def ensure_not_last_default
return if destroyed_by_association
return unless default?
return if other_defaults.exists?

errors.add(:base, :last_default_form_template)
throw :abort
end
end
11 changes: 9 additions & 2 deletions app/models/habilitation_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class HabilitationType < ApplicationRecord
friendly_id :name, use: :slugged

belongs_to :data_provider
has_many :form_templates, dependent: :destroy

enum :kind, { api: 'api', service: 'service' }, validate: true

Expand All @@ -24,11 +25,11 @@ class HabilitationType < ApplicationRecord
validates :scopes, presence: true, if: :scopes_block_selected?
validate :validate_each_scope, if: :scopes_block_selected?

after_create :ensure_default_form_template!
before_destroy :ensure_no_authorization_requests
after_destroy :unregister_dynamic_class
after_destroy :reset_static_caches
after_save :register_dynamic_class
after_save :reset_static_caches
after_commit :reset_static_caches, on: %i[create update destroy]

def public
true
Expand Down Expand Up @@ -63,6 +64,12 @@ def ordered_steps
BLOCK_ORDER.select { |name| block_names.include?(name) }
end

def ensure_default_form_template!
return if form_templates.exists?(default: true)

form_templates.create!(slug: "#{slug}-default", default: true)
end

def self.preload_requests_counts!(habilitation_types)
types = habilitation_types.map(&:authorization_request_type)
counts = AuthorizationRequest.where(type: types).group(:type).count
Expand Down
13 changes: 13 additions & 0 deletions app/policies/instruction/authorization_definition_policy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class Instruction::AuthorizationDefinitionPolicy < ApplicationPolicy
def show?
user.reporter?(record.id)
end

class Scope < Scope
def resolve
return scope.all if user.admin?

scope.all.select { |definition| user.reporter?(definition.id) }
end
end
end
5 changes: 5 additions & 0 deletions app/policies/instruction/authorization_request_form_policy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class Instruction::AuthorizationRequestFormPolicy < ApplicationPolicy
def show?
user.reporter?(record.authorization_definition.id)
end
end
24 changes: 24 additions & 0 deletions app/policies/instruction/data_provider_policy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
class Instruction::DataProviderPolicy < ApplicationPolicy
# NOTE: iterates AuthorizationDefinition.all on every request — cheap while
# definitions are static records (~30-50). Revisit once they live in DB
# (DP-1719/1671 migration), where this becomes a hot-path N+1.
def show?
user.admin? ||
AuthorizationDefinition.all
.select { |ad| ad.provider_slug == record.slug }
.any? { |ad| user.reporter?(ad.id) }
end

class Scope < Scope
def resolve
return scope.all if user.admin?

accessible_slugs = AuthorizationDefinition.all
.select { |ad| user.reporter?(ad.id) }
.map(&:provider_slug)
.uniq

scope.where(slug: accessible_slugs)
end
end
end
5 changes: 5 additions & 0 deletions app/services/skip_links_implemented_checker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ class SkipLinksImplementedChecker
admin/bans#destroy
admin/bans#confirm_destroy

instruction/data_providers#index
instruction/formulaires#index
instruction/formulaires#show
instruction/cas_usages#show

developers/open_api#show
developers/oauth_applications#index
developers/tutorials#index
Expand Down
20 changes: 20 additions & 0 deletions app/views/instruction/cas_usages/_rendered_html_tabs.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<div class="fr-tabs">
<ul class="fr-tabs__list" role="tablist" aria-label="<%= t('instruction.cas_usages.show.html_tabs.aria_label') %>">
<li role="presentation">
<button type="button" id="<%= id_prefix %>-tab-rendered" class="fr-tabs__tab fr-icon-eye-line fr-tabs__tab--icon-left" tabindex="0" role="tab" aria-selected="true" aria-controls="<%= id_prefix %>-panel-rendered">
<%= t('instruction.cas_usages.show.html_tabs.rendered') %>
</button>
</li>
<li role="presentation">
<button type="button" id="<%= id_prefix %>-tab-html" class="fr-tabs__tab fr-icon-code-s-slash-line fr-tabs__tab--icon-left" tabindex="-1" role="tab" aria-selected="false" aria-controls="<%= id_prefix %>-panel-html">
<%= t('instruction.cas_usages.show.html_tabs.html') %>
</button>
</li>
</ul>
<div id="<%= id_prefix %>-panel-rendered" class="fr-tabs__panel fr-tabs__panel--selected" role="tabpanel" aria-labelledby="<%= id_prefix %>-tab-rendered" tabindex="0">
<%= rendered %>
</div>
<div id="<%= id_prefix %>-panel-html" class="fr-tabs__panel" role="tabpanel" aria-labelledby="<%= id_prefix %>-tab-html" tabindex="0">
<pre class="dp-code-block"><code><%= source %></code></pre>
</div>
</div>
Loading