Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,5 @@ app/migration/.hubee_config.yml
lib/suivi_dtnum/sources/*
lib/suivi_dtnum/__pycache__
venv/

/docs/shaping/*
3 changes: 1 addition & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ Always use `make` commands - they handle Docker setup including Chrome for tests
- Run all tests: **`make tests`**
- Run specific test: `make tests spec/path/to/file_spec.rb:LINE_NUMBER`
- Run E2E tests: `make e2e` or `make e2e features/path/to/file.feature:LINE_NUMBER`
- Run linter: `make lint`
- Fix linting issues: `make fix-lint`
- Run linter: `make fix-lint` (this fixes autocorrectable issues right away)
- JS linting: `make js-lint`

If a Docker command fails with missing dependencies (gems, packages, etc.), run `make build` to rebuild the image, then retry.
Expand Down
16 changes: 16 additions & 0 deletions app/assets/stylesheets/components/definition_card.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.data-provider-card-img {
display: flex;
align-items: center;
justify-content: center;
width: 4.2rem;
padding: .2em;
}

.data-provider-card-img img {
max-width: 4rem;
max-height: 4rem;
}

.definitions-search-count {
text-align: right;
}
4 changes: 4 additions & 0 deletions app/assets/stylesheets/components/wide_header.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.wide-header-logo {
max-height: 5rem;
max-width: 10rem;
}
7 changes: 7 additions & 0 deletions app/assets/stylesheets/dsfr-extensions.css
Original file line number Diff line number Diff line change
Expand Up @@ -308,3 +308,10 @@ fr-badge--grey {
.fr-table--align-middle th {
vertical-align: middle;
}

code.code-inline {
background: var(--background-contrast-grey);
color: var(--text-default-grey);
padding: 0.125rem 0.5rem;
border-radius: 0.25rem;
}
2 changes: 1 addition & 1 deletion app/components/application_component.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
class ApplicationComponent < ViewComponent::Base
include ApplicationHelper

delegate :policy, to: :helpers
delegate :policy, :policy_scope, to: :helpers
end
9 changes: 9 additions & 0 deletions app/components/instructor_menu_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@
</a>
</li>

<% if show_definitions %>
<li class="fr-nav__item">
<a class="fr-nav__link center" href="<%= instruction_authorization_definitions_path %>" target="_self">
<%= t('layouts.header.menu.instruction.formulaires') %>
</a>
</li>
<% end %>

<% if show_drafts %>
<li class="fr-nav__item">
<a class="fr-nav__link center" href="<%= instruction_instructor_draft_requests_path %>" target="_self">
Expand All @@ -27,3 +35,4 @@
</a>
</li>
<% end %>

7 changes: 4 additions & 3 deletions app/components/instructor_menu_component.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
class InstructorMenuComponent < ApplicationComponent
def initialize(show_drafts:, show_templates:, show_user_rights:)
def initialize(show_drafts:, show_templates:, show_user_rights:, show_definitions: false)
@show_drafts = show_drafts
@show_templates = show_templates
@show_user_rights = show_user_rights
@show_definitions = show_definitions
end

def render?
@show_drafts || @show_templates || @show_user_rights
@show_drafts || @show_templates || @show_user_rights || @show_definitions
end

attr_reader :show_drafts, :show_templates, :show_user_rights
attr_reader :show_drafts, :show_templates, :show_user_rights, :show_definitions
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<div class="fr-card fr-enlarge-link">
<div class="fr-card__body">
<div class="fr-card__content fr-pb-2w">
<div class="fr-grid-row fr-grid-row--gutters">
<% if provider.logo&.attached? %>
<div class="data-provider-card-img">
<%= image_tag provider.logo, alt: '', class: 'fr-mr-1w' %>
</div>
<% end %>
<div class="fr-col">
<h2 class="fr-card__title">
<%= link_to name_with_stage, '#' %>
</h2>
<p class="fr-card__desc"><%= provider.name %></p>
</div>
</div>
</div>
<div class="fr-card__footer">
<div class="fr-grid-row fr-grid-row--gutters">
<div class="fr-col-auto">
<span><%= validated_count %></span>
<span class="fr-badge fr-badge--sm fr-badge--success fr-badge--no-icon"><%= t('authorization_request.status.validated') %></span>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pourquoi ne pas utiliser le component Atoms::ColorBadgeComponent ici ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Par ce que nulle part dans le code on utilise Atom::ColorBadgeComponent pour les badges des status de demandes, et que c'est pas le scope de cette PR de refacto tout ça.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DsfrComponent::BadgeComponent gère les badge d'état.
C'est a appliquer ici.
ex <%= dsfr_badge(status: :success) { 'Validé' } %>

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nulle part dans le code on utilise dsfr_badge pour les badges de status de demandes, et c'est pas le scope de cette PR d'introduire ça.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Si tu veux, fait un ticket "utiliser dsfr_badge pour les badges de status de demandes", et on harmonise tout ça dans un followup. Mais je vais pas faire juste l'effort ici et laisser tout le reste de la codebase faire autrement.

</div>
<div class="fr-col-auto">
<span><%= submitted_count %></span>
<span class="fr-badge fr-badge--sm fr-badge--info fr-badge--no-icon"><%= t('authorization_request.status.submitted') %></span>
Comment thread
JeSuisUnCaillou marked this conversation as resolved.
</div>
</div>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class Molecules::Instruction::AuthorizationDefinition::CardComponent < ApplicationComponent
def initialize(authorization_definition:, validated_count:, submitted_count:)
@authorization_definition = authorization_definition
@validated_count = validated_count
@submitted_count = submitted_count
end

private

attr_reader :authorization_definition, :validated_count, :submitted_count

delegate :name_with_stage, :provider, to: :authorization_definition
end
27 changes: 27 additions & 0 deletions app/components/molecules/instruction/wide_header.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<div class="fr-background-alt--blue-france fr-py-4w fr-mb-4w">
<div class="fr-container">
<% if back_link %>
<div class="fr-text--sm fr-mb-2w fr-text-mention--grey">
<%= link_to back_link[:text],
back_link[:path],
class: 'fr-link fr-icon-arrow-left-line fr-link--icon-left' %>
</div>
<% end %>

<div class="fr-grid-row fr-grid-row--top fr-grid-row--gutters">
<div class="fr-col-auto">
<% if logo_asset&.attached? %>
<%= image_tag logo_asset, alt: title, class: 'wide-header-logo' %>
<% elsif dsfr_logo %>
<%= dsfr_pictogram(dsfr_logo) %>
<% end %>
</div>
<div class="fr-col">
<h1 class="fr-mb-1v">
<%= title %>
</h1>
<%= subtitle_content %>
</div>
</div>
</div>
</div>
14 changes: 14 additions & 0 deletions app/components/molecules/instruction/wide_header.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
class Molecules::Instruction::WideHeader < ApplicationComponent
renders_one :subtitle_content

def initialize(title:, logo_asset: nil, dsfr_logo: nil, back_link: nil)
@title = title
@logo_asset = logo_asset
@dsfr_logo = dsfr_logo
@back_link = back_link
end

private

attr_reader :title, :logo_asset, :dsfr_logo, :back_link
end
6 changes: 6 additions & 0 deletions app/controllers/authenticated_user_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@ class AuthenticatedUserController < ApplicationController

impersonates :user

helper_method :authorization_definitions_feature_enabled?

before_action :refresh_current_organization_insee_data

allow_unauthenticated_access only: :bypass_login

def authorization_definitions_feature_enabled?
current_user&.admin? || Rails.env.test?
end

def bypass_login
return if Rails.env.production?

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
class Instruction::AuthorizationDefinitionsController < Instruction::FormManagementController
def index
authorize %i[instruction authorization_definition], :index?
@authorization_definitions = accessible_definitions
@counts_by_definition = preload_counts(@authorization_definitions)
end

private

def accessible_definitions
AuthorizationDefinition.all
.select { |d| current_user.reporter?(d.id) }
.sort_by(&:name)
end

def preload_counts(definitions)
types = definitions.map { |d| d.authorization_request_class.to_s }
raw_counts = AuthorizationRequest
.where(type: types, state: %w[validated submitted])
.group(:type, :state)
.count

definitions.to_h do |d|
type = d.authorization_request_class.to_s
[d.id, {
validated: raw_counts[[type, 'validated']] || 0,
submitted: raw_counts[[type, 'submitted']] || 0
}]
end
end
end
7 changes: 7 additions & 0 deletions app/controllers/instruction/form_management_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class Instruction::FormManagementController < InstructionController
private

def layout_name
'wide_container'
Comment thread
JeSuisUnCaillou marked this conversation as resolved.
end
end
34 changes: 34 additions & 0 deletions app/javascript/controllers/search_list_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Controller } from '@hotwired/stimulus'

export default class extends Controller {
static targets = ['input', 'item', 'count', 'emptyMessage']

filter () {
const query = this._normalize(this.inputTarget.value)
let visibleCount = 0

this.itemTargets.forEach(item => {
const match = !query || item.dataset.searchText.includes(query)
item.classList.toggle('fr-hidden', !match)
if (match) visibleCount++
})

this._updateCount(visibleCount)

if (this.hasEmptyMessageTarget) {
this.emptyMessageTarget.classList.toggle('fr-hidden', visibleCount > 0)
}
}

_updateCount (count) {
if (!this.hasCountTarget) return

const templates = JSON.parse(this.countTarget.dataset.templates)
const key = count === 0 ? 'zero' : count === 1 ? 'one' : 'other'
this.countTarget.textContent = templates[key].replace('%{count}', count)
}

_normalize (text) {
return text.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().trim()
}
}
4 changes: 4 additions & 0 deletions app/models/authorization_definition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ def name_with_stage
end
end

def search_text
I18n.transliterate([name_with_stage, provider&.name].compact_blank.join(' ')).downcase
end

def feature?(name, default: true)
features.fetch(name.to_sym, default)
end
Expand Down
107 changes: 107 additions & 0 deletions app/models/concerns/user_roles.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
module UserRoles
extend ActiveSupport::Concern

def roles=(value)
super
@role_sets = nil
end

def roles_for(kind)
@role_sets ||= {}
@role_sets[kind] ||= RoleSet.new(roles, kind)
end

def instructor?(definition_id = nil)
roles_for(:instructor).covers?(definition_id)
end

def manager?(definition_id = nil)
roles_for(:manager).covers?(definition_id)
end

def reporter?(definition_id = nil)
return true if admin?

roles_for(:reporter).covers?(definition_id)
end

def fd_reporter?(provider_slug)
return true if admin?

roles_for(:reporter).provider_slugs.include?(provider_slug)
end

def developer?
roles_for(:developer).any?
end

def definition_ids_for(kind)
roles_for(kind).definition_ids
end

def managed_fd_slugs
roles.filter_map { |role_string|
parsed = ParsedRole.parse(role_string)
parsed.provider_slug if parsed.fd_level? && parsed.role == 'manager'
}.uniq
end

def manages_role?(role_string)
parsed = ParsedRole.parse(role_string)
return false if parsed.admin? || parsed.role.nil?

if parsed.fd_level?
managed_fd_slugs.include?(parsed.provider_slug)
else
definition_ids_for(:manager).include?(parsed.definition_id)
end
end

def managed_by?(other_user)
roles.any? { |role| other_user.manages_role?(role) }
end

def authorization_request_types_for(kind)
roles_for(kind).authorization_request_types
end

def grant_role(kind, definition_id)
fd = ParsedRole.resolve_provider_slug(definition_id)
raise ParsedRole::UnknownDefinitionError, "Unknown definition: #{definition_id}" unless fd

roles << "#{fd}:#{definition_id}:#{kind}"
roles.uniq!
@role_sets = nil
end

def grant_fd_role(kind, provider_slug)
roles << "#{provider_slug}:*:#{kind}"
roles.uniq!
@role_sets = nil
end

def grant_admin_role
roles << 'admin'
roles.uniq!
@role_sets = nil
end

def revoke_all_roles
self.roles = []
@role_sets = nil
end

def admin?
roles.include?('admin') ||
bug_bounty_users_within_staging_env?
end

def bug_bounty_users_within_staging_env?
Rails.env.staging? &&
/-ywhadmin@yopmail.com$/.match?(email)
end

def authorization_definition_roles_as(kind)
roles_for(kind).authorization_definitions
end
end
Loading