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
|
## À faire
|
||||||
|
|
||||||
|
- traduire le message Rodauth « Please login to continue » ;
|
||||||
- limiter le spam sur `/contact/send` ;
|
- 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 ;
|
- 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
|
## 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
|
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.
|
é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
|
### Initialisation du Noyau
|
||||||
|
|
||||||
Pour ajouter une personne au Noyau, utiliser le script :
|
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
|
bundle exec ruby bin/add-core-member adresse@example.net
|
||||||
```
|
```
|
||||||
|
|
||||||
Le compte doit exister, son adresse électronique doit être vérifiée et
|
Le compte doit exister, son adresse électronique doit être vérifiée
|
||||||
la fiche membre doit correspondre à une personne physique. Une adhésion
|
et la fiche membre doit correspondre à une personne physique.
|
||||||
au statut `accepted` doit également déjà exister dans `memberships`.
|
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
|
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
|
la qualité de membre du Noyau à une fiche membre qui remplit ces
|
||||||
conditions. Il doit être exécuté séparément pour chaque personne.
|
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
|
Les demandes d'adhésion sont formulées depuis l'espace personnel
|
||||||
l'interface. Une adhésion acceptée doit donc être enregistrée dans la
|
(`/account`) et examinées par le Noyau depuis l'espace de gestion
|
||||||
base avant l'utilisation de ce script.
|
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
|
Les statuts prévoient un Noyau composé d'au moins trois personnes
|
||||||
physiques.
|
physiques.
|
||||||
|
|||||||
@@ -621,6 +621,77 @@ summary:focus-visible {
|
|||||||
vertical-align: top;
|
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
|
/* 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
|
/* Adaptations pour les petits écrans
|
||||||
------------------------------------------------------------------------ */
|
------------------------------------------------------------------------ */
|
||||||
|
|
||||||
@@ -693,6 +826,22 @@ footer {
|
|||||||
padding-bottom: 60px;
|
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 {
|
footer {
|
||||||
padding: 24px 20px;
|
padding: 24px 20px;
|
||||||
}
|
}
|
||||||
|
|||||||
+88
-3
@@ -1,21 +1,106 @@
|
|||||||
class Netig
|
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|
|
hash_branch "noyau" do |r|
|
||||||
require_core_member!(r)
|
require_core_member!(r)
|
||||||
|
|
||||||
@page_title = "Noyau — NETig"
|
@page_title = "Gestion de l'association — NETig"
|
||||||
@page_description = "Espace de gestion de l'association NETig."
|
@page_description = "Gestion collective de l'association NETig par le Noyau."
|
||||||
@canonical_url = "https://netig.net/noyau"
|
@canonical_url = "https://netig.net/noyau"
|
||||||
|
|
||||||
|
# Accueil
|
||||||
r.is do
|
r.is do
|
||||||
r.get do
|
r.get do
|
||||||
view "noyau"
|
view "noyau"
|
||||||
end
|
end
|
||||||
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.on "members" do
|
||||||
r.is do
|
r.is do
|
||||||
r.get 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]
|
@members = DB[:members]
|
||||||
.join(:accounts, id: :account_id)
|
.join(:accounts, id: :account_id)
|
||||||
|
|||||||
+1
-1
@@ -36,7 +36,7 @@
|
|||||||
|
|
||||||
<% if @membership.nil? || @membership[:status] == "rejected" %>
|
<% if @membership.nil? || @membership[:status] == "rejected" %>
|
||||||
<form action="/account/membership" method="post">
|
<form action="/account/membership" method="post">
|
||||||
<%== rodauth.csrf_tag %>
|
<%== rodauth.csrf_tag("/account/membership") %>
|
||||||
<button class="button" type="submit">
|
<button class="button" type="submit">
|
||||||
Demander à adhérer
|
Demander à adhérer
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
+6
-3
@@ -49,17 +49,20 @@
|
|||||||
Déconnexion
|
Déconnexion
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
<% elsif request.path.start_with?("/noyau/") %>
|
||||||
|
<a class="topbar__account" href="/noyau">
|
||||||
|
Gestion de l'association ➜
|
||||||
|
</a>
|
||||||
<% else %>
|
<% else %>
|
||||||
<a class="topbar__account" href="/account">
|
<a class="topbar__account" href="/account">
|
||||||
Mon espace →
|
Mon espace ➜
|
||||||
</a>
|
</a>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% elsif request.path != rodauth.login_path %>
|
<% elsif request.path != rodauth.login_path %>
|
||||||
<a class="topbar__account" href="<%= rodauth.login_path %>">
|
<a class="topbar__account" href="<%= rodauth.login_path %>">
|
||||||
Mon espace →
|
Mon espace ➜
|
||||||
</a>
|
</a>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
+27
-5
@@ -1,11 +1,33 @@
|
|||||||
<section class="page-section">
|
<section class="page-section">
|
||||||
<div class="content">
|
<div class="content page-panel">
|
||||||
<h1>Espace du Noyau</h1>
|
<h1>Gestion de l'association</h1>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a class="button" href="/noyau/members">
|
Gestion collective de l'association par le Noyau.
|
||||||
Gérer les adhérents
|
|
||||||
</a>
|
|
||||||
</p>
|
</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>
|
</div>
|
||||||
</section>
|
</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