Manage membership
This commit is contained in:
1 parent
491457c50e
commit
a6ba3681b2
7 files changed
+413
-21
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user