Manage membership

This commit is contained in:
julien committed 2026-11-03 21:38:38 +01:00
1 parent 491457c50e
commit a6ba3681b2
7 files changed
+413 -21

No files matched your search

+46 -9
View File
@@ -2,11 +2,11 @@
## À faire
- traduire le message Rodauth « Please login to continue » ;
- limiter le spam sur `/contact/send` ;
- développer les demandes d'adhésion et leur examen par le Noyau ;
- développer l'espace personnel des membres ;
- afficher les recettes et les dépenses sur la page d'accueil ;
- gérer les accès aux services selon la situation des membres.
- gérer les accès aux services selon la situation des membres ;
- traiter les demandes de démission et de suppression des comptes.
## Développement
@@ -85,6 +85,38 @@ Les personnes physiques et les personnes morales peuvent créer un compte.
La création du compte crée également une fiche dans `members`. L'adresse
électronique doit être vérifiée avant de pouvoir se connecter.
### Espaces utilisateurs
Après avoir vérifié son adresse électronique, chaque personne connectée
accède à son espace personnel :
```text
http://[::1]:9292/account
```
Cet espace permet notamment de consulter sa situation d'adhésion et
de formuler une demande d'adhésion à l'association.
Les membres du Noyau disposent également d'un accès à la gestion de
l'association :
```text
http://[::1]:9292/noyau
```
Cet espace permet notamment de consulter les demandes d'adhésion,
de les accepter ou de les refuser, et de consulter les comptes
de l'association.
Les demandes en attente sont accessibles à l'adresse :
```text
http://[::1]:9292/noyau/memberships
```
La consultation et la gestion de l'association sont réservées
aux membres du Noyau.
### Initialisation du Noyau
Pour ajouter une personne au Noyau, utiliser le script :
@@ -93,17 +125,22 @@ Pour ajouter une personne au Noyau, utiliser le script :
bundle exec ruby bin/add-core-member adresse@example.net
```
Le compte doit exister, son adresse électronique doit être vérifiée et
la fiche membre doit correspondre à une personne physique. Une adhésion
au statut `accepted` doit également déjà exister dans `memberships`.
Le compte doit exister, son adresse électronique doit être vérifiée
et la fiche membre doit correspondre à une personne physique.
Une adhésion au statut `accepted` doit également déjà exister
dans `memberships`.
Le script ne crée ni le compte ni l'adhésion. Il attribue uniquement
la qualité de membre du Noyau à une fiche membre qui remplit ces
conditions. Il doit être exécuté séparément pour chaque personne.
La gestion des demandes d'adhésion n'est pas encore disponible dans
l'interface. Une adhésion acceptée doit donc être enregistrée dans la
base avant l'utilisation de ce script.
Les demandes d'adhésion sont formulées depuis l'espace personnel
(`/account`) et examinées par le Noyau depuis l'espace de gestion
de l'association (`/noyau/memberships`).
Pour constituer le premier Noyau, les adhésions des personnes
désignées doivent être enregistrées comme acceptées avant
l'utilisation du script.
Les statuts prévoient un Noyau composé d'au moins trois personnes
physiques.
+149
View File
@@ -621,6 +621,77 @@ summary:focus-visible {
vertical-align: top;
}
/* Demandes d'adhésion
------------------------------------------------------------------------ */
.table-wrapper {
width: 100%;
margin-block: 1.5rem;
}
.memberships-table {
width: 100%;
table-layout: fixed;
font-size: 1rem;
}
.membership-col-identity {
width: 35%;
}
.membership-col-email {
width: 23%;
}
.membership-col-date {
width: 17%;
}
.membership-col-actions {
width: 25%;
}
.memberships-table th,
.memberships-table td {
padding: 10px 8px;
white-space: nowrap;
vertical-align: middle;
}
.memberships-table td strong,
.membership-kind {
display: inline;
}
.membership-kind {
margin-left: 0.25rem;
color: var(--color-link);
font-size: 0.85rem;
}
.membership-date {
font-size: 0.9rem;
}
.membership-actions {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: 0.5rem;
}
.membership-actions form {
flex: 0 0 auto;
margin: 0;
}
.membership-actions .button {
padding: 8px 10px;
font-size: 1rem;
line-height: 1.2;
white-space: nowrap;
}
/* Pied de page
------------------------------------------------------------------------ */
@@ -679,6 +750,68 @@ footer {
}
}
/* Adaptations pour les écrans intermédiaires et petits
------------------------------------------------------------------------ */
@media screen and (max-width: 1100px) {
.memberships-table,
.memberships-table tbody {
display: block;
}
.memberships-table thead {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
}
.memberships-table tbody {
display: grid;
gap: 1rem;
}
.memberships-table tr {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.5rem 1rem;
padding: 1rem;
border: 1px solid var(--color-shadow-light);
background: var(--color-surface-dark);
}
.memberships-table td {
display: block;
min-width: 0;
padding: 0.35rem 0;
border: 0;
white-space: normal;
overflow-wrap: anywhere;
}
.memberships-table td::before {
display: block;
margin-bottom: 0.25rem;
color: var(--color-highlight);
font-weight: 700;
content: attr(data-label);
}
.memberships-table td[data-label="Décision"] {
grid-column: 1 / -1;
}
.membership-kind {
font-size: 0.9rem;
}
.membership-actions {
flex-wrap: wrap;
}
}
/* Adaptations pour les petits écrans
------------------------------------------------------------------------ */
@@ -693,6 +826,22 @@ footer {
padding-bottom: 60px;
}
.memberships-table tr {
grid-template-columns: minmax(0, 1fr);
}
.memberships-table td[data-label="Décision"] {
grid-column: auto;
}
.membership-actions form {
flex: 1 1 auto;
}
.membership-actions .button {
width: 100%;
}
footer {
padding: 24px 20px;
}
+88 -3
View File
@@ -1,21 +1,106 @@
class Netig
private
# Enregistrement d'une décision d'adhésion
def record_membership_decision!(r, membership_id, status)
member_id = DB[:members]
.where(account_id: rodauth.session_value)
.get(:id)
unless member_id
r.halt [403, {}, ["Accès interdit."]]
end
message = case status
when "accepted"
"La demande d'adhésion a été acceptée."
when "rejected"
"La demande d'adhésion a été refusée."
end
updated = DB[:memberships]
.where(id: membership_id, status: "pending")
.update(
status: status,
decided_at: Sequel::CURRENT_TIMESTAMP,
recorded_by: member_id
)
if updated == 1
flash["notice"] = message
else
flash["error"] = "Cette demande n'est plus en attente ou n'existe pas."
end
r.redirect "/noyau/memberships"
end
hash_branch "noyau" do |r|
require_core_member!(r)
@page_title = "Noyau — NETig"
@page_description = "Espace de gestion de l'association NETig."
@page_title = "Gestion de l'association — NETig"
@page_description = "Gestion collective de l'association NETig par le Noyau."
@canonical_url = "https://netig.net/noyau"
# Accueil
r.is do
r.get do
view "noyau"
end
end
# Demandes d'adhésion
r.on "memberships" do
r.is do
r.get do
@page_title = "Demandes d'adhésion — NETig"
@page_description = "Examen des demandes d'adhésion à NETig."
@canonical_url = "https://netig.net/noyau/memberships"
@memberships = DB[:memberships]
.join(:members, id: :member_id)
.join(:accounts, id: Sequel[:members][:account_id])
.where(
Sequel[:memberships][:status] => "pending",
Sequel[:accounts][:status_id] => 2
)
.select_all(:memberships)
.select_append(
Sequel[:members][:kind].as(:member_kind),
Sequel[:members][:first_name].as(:member_first_name),
Sequel[:members][:last_name].as(:member_last_name),
Sequel[:members][:organization_name].as(:member_organization_name),
Sequel[:accounts][:email].as(:account_email)
)
.order(
Sequel.asc(Sequel[:memberships][:requested_at]),
Sequel.asc(Sequel[:memberships][:id])
)
.all
view "noyau/memberships"
end
end
# Décision sur une demande
r.on Integer do |membership_id|
r.post "accept" do
record_membership_decision!(r, membership_id, "accepted")
end
r.post "reject" do
record_membership_decision!(r, membership_id, "rejected")
end
end
end
# Gestion des comptes
r.on "members" do
r.is do
r.get do
@page_title = "Comptes — Noyau — NETig"
@page_title = "Comptes — NETig"
@page_description = "Consultation des comptes de l'association NETig."
@canonical_url = "https://netig.net/noyau/members"
@members = DB[:members]
.join(:accounts, id: :account_id)
+1 -1
View File
@@ -36,7 +36,7 @@
<% if @membership.nil? || @membership[:status] == "rejected" %>
<form action="/account/membership" method="post">
<%== rodauth.csrf_tag %>
<%== rodauth.csrf_tag("/account/membership") %>
<button class="button" type="submit">
Demander à adhérer
</button>
+6 -3
View File
@@ -49,17 +49,20 @@
Déconnexion
</button>
</form>
<% elsif request.path.start_with?("/noyau/") %>
<a class="topbar__account" href="/noyau">
Gestion de l'association ➜
</a>
<% else %>
<a class="topbar__account" href="/account">
Mon espace →
Mon espace ➜
</a>
<% end %>
<% elsif request.path != rodauth.login_path %>
<a class="topbar__account" href="<%= rodauth.login_path %>">
Mon espace →
Mon espace ➜
</a>
<% end %>
</nav>
</header>
+27 -5
View File
@@ -1,11 +1,33 @@
<section class="page-section">
<div class="content">
<h1>Espace du Noyau</h1>
<div class="content page-panel">
<h1>Gestion de l'association</h1>
<p>
<a class="button" href="/noyau/members">
Gérer les adhérents
</a>
Gestion collective de l'association par le Noyau.
</p>
<section aria-labelledby="membership-heading">
<h2 id="membership-heading">Adhésions</h2>
<p>
Consultez et examinez les demandes d'adhésion à NETig.
</p>
<a class="button" href="/noyau/memberships">
Demandes d'adhésion
</a>
</section>
<section aria-labelledby="members-heading">
<h2 id="members-heading">Comptes</h2>
<p>
Consultez les comptes et les informations des membres.
</p>
<a class="button" href="/noyau/members">
Consulter les comptes
</a>
</section>
</div>
</section>
+96
View File
@@ -0,0 +1,96 @@
<section class="page-section">
<div class="content page-panel">
<h1>Demandes d'adhésion</h1>
<% if flash["notice"] || flash["error"] %>
<div class="flash-container">
<% if notice = flash["notice"] %>
<p class="flash flash--notice" role="status"><%= notice %></p>
<% end %>
<% if error = flash["error"] %>
<p class="flash flash--error" role="alert"><%= error %></p>
<% end %>
</div>
<% end %>
<p>
Consultez les demandes d'adhésion en attente d'examen
par le Noyau.
</p>
<% if @memberships.empty? %>
<p>Aucune demande d'adhésion en attente.</p>
<% else %>
<div class="table-wrapper">
<table class="members-table memberships-table">
<colgroup>
<col class="membership-col-identity">
<col class="membership-col-email">
<col class="membership-col-date">
<col class="membership-col-actions">
</colgroup>
<thead>
<tr>
<th scope="col">Identité</th>
<th scope="col">Email</th>
<th scope="col">Demande</th>
<th scope="col">Décision</th>
</tr>
</thead>
<tbody>
<% @memberships.each do |membership| %>
<tr>
<td data-label="Identité">
<% if membership[:member_kind] == "individual" %>
<strong><%= "#{membership[:member_first_name]} #{membership[:member_last_name]}" %></strong>
<small class="membership-kind">· Personne physique</small>
<% else %>
<strong><%= membership[:member_organization_name] %></strong>
<small class="membership-kind">· Personne morale</small>
<% end %>
</td>
<td data-label="Email">
<%= membership[:account_email] %>
</td>
<td data-label="Demande">
<time class="membership-date">
<%= membership[:requested_at].strftime("%d/%m/%Y à %H:%M") %>
</time>
</td>
<td data-label="Décision">
<div class="membership-actions">
<form
method="post"
action="/noyau/memberships/<%= membership[:id] %>/accept"
>
<%== rodauth.csrf_tag("/noyau/memberships/#{membership[:id]}/accept") %>
<button class="button" type="submit">
Accepter
</button>
</form>
<form
method="post"
action="/noyau/memberships/<%= membership[:id] %>/reject"
>
<%== rodauth.csrf_tag("/noyau/memberships/#{membership[:id]}/reject") %>
<button class="button button--secondary" type="submit">
Refuser
</button>
</form>
</div>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<% end %>
</div>
</section>