feat: add affiliation request save

This commit is contained in:
Thibaut Valentin 2024-03-06 15:17:20 +01:00
parent b0232cd7b7
commit 9615660101
9 changed files with 326 additions and 55 deletions

View File

@ -0,0 +1,43 @@
package fr.titionfire.ffsaf.data.model;
import io.quarkus.runtime.annotations.RegisterForReflection;
import jakarta.persistence.*;
import lombok.*;
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@RegisterForReflection
@Entity
@Table(name = "affiliation_request")
public class AffiliationRequestModel {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
String name;
String siren;
String RNA;
String address;
String president_lname;
String president_fname;
String president_email;
int president_lincence;
String tresorier_lname;
String tresorier_fname;
String tresorier_email;
int tresorier_lincence;
String secretaire_lname;
String secretaire_fname;
String secretaire_email;
int secretaire_lincence;
int saison;
}

View File

@ -0,0 +1,9 @@
package fr.titionfire.ffsaf.data.repository;
import fr.titionfire.ffsaf.data.model.AffiliationRequestModel;
import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class AffiliationRequestRepository implements PanacheRepositoryBase<AffiliationRequestModel, Long> {
}

View File

@ -0,0 +1,60 @@
package fr.titionfire.ffsaf.domain.service;
import fr.titionfire.ffsaf.data.model.AffiliationRequestModel;
import fr.titionfire.ffsaf.data.repository.AffiliationRequestRepository;
import fr.titionfire.ffsaf.data.repository.CombRepository;
import fr.titionfire.ffsaf.rest.from.AffiliationRequestForm;
import fr.titionfire.ffsaf.utils.Utils;
import io.quarkus.hibernate.reactive.panache.Panache;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.eclipse.microprofile.config.inject.ConfigProperty;
@WithSession
@ApplicationScoped
public class AffiliationService {
@Inject
CombRepository combRepository;
@Inject
AffiliationRequestRepository repository;
@ConfigProperty(name = "upload_dir")
String media;
public Uni<String> save(AffiliationRequestForm form) {
AffiliationRequestModel affModel = form.toModel();
affModel.setSaison(Utils.getSaison());
return Uni.createFrom().item(affModel)
.call(model -> ((model.getPresident_lincence() != 0) ? combRepository.find("licence",
model.getPresident_lincence()).count().invoke(count -> {
if (count == 0) {
throw new IllegalArgumentException("Licence président inconnue");
}
}) : Uni.createFrom().nullItem())
)
.call(model -> ((model.getTresorier_lincence() != 0) ? combRepository.find("licence",
model.getTresorier_lincence()).count().invoke(count -> {
if (count == 0) {
throw new IllegalArgumentException("Licence trésorier inconnue");
}
}) : Uni.createFrom().nullItem())
)
.call(model -> ((model.getSecretaire_lincence() != 0) ? combRepository.find("licence",
model.getSecretaire_lincence()).count().invoke(count -> {
if (count == 0) {
throw new IllegalArgumentException("Licence secrétaire inconnue");
}
}) : Uni.createFrom().nullItem())
).chain(model -> Panache.withTransaction(() -> repository.persist(model)))
.call(model -> Uni.createFrom().future(Utils.replacePhoto(model.getId(), form.getLogo(), media,
"aff_request/logo")))
.call(model -> Uni.createFrom().future(Utils.replacePhoto(model.getId(), form.getStatus(), media,
"aff_request/status")))
.map(__ -> "Ok");
}
}

View File

@ -0,0 +1,29 @@
package fr.titionfire.ffsaf.rest;
import fr.titionfire.ffsaf.rest.from.AffiliationRequestForm;
import io.smallrye.mutiny.Uni;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
@Path("api/affiliation")
public class AffiliationEndpoints {
@POST
@Path("save")
@Produces(MediaType.TEXT_PLAIN)
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Uni<String> saveAffRequest(AffiliationRequestForm form) {
System.out.println(form);
return Uni.createFrom().item("OK");
}
/*@POST
@Path("affiliation")
@Produces(MediaType.TEXT_PLAIN)
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Uni<String> saveAffRequest(AffiliationRequestForm form) {
System.out.println(form);
return service.save(form);
}*/
}

View File

@ -1,28 +1,53 @@
package fr.titionfire.ffsaf.rest;
import fr.titionfire.ffsaf.domain.service.AffiliationService;
import fr.titionfire.ffsaf.rest.client.SirenService;
import fr.titionfire.ffsaf.rest.data.UniteLegaleRoot;
import fr.titionfire.ffsaf.rest.from.AffiliationRequestForm;
import io.smallrye.mutiny.Uni;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jodd.net.MimeTypes;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import java.io.*;
import java.net.URLConnection;
import java.nio.file.Files;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
@Path("api/asso")
public class AssoEndpoints {
@RestClient
SirenService sirenService;
@Inject
AffiliationService service;
@ConfigProperty(name = "upload_dir")
String media;
@GET
@Path("siren/{siren}")
@Produces(MediaType.APPLICATION_JSON)
public Uni<UniteLegaleRoot> getInfoSiren(@PathParam("siren") String siren) {
return sirenService.get_unite(siren).onFailure().transform(throwable -> {
if (throwable instanceof WebApplicationException exception){
if (throwable instanceof WebApplicationException exception) {
if (exception.getResponse().getStatus() == 400)
return new BadRequestException("Not found");
}
return throwable;
});
}
@POST
@Path("affiliation")
@Produces(MediaType.TEXT_PLAIN)
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Uni<String> saveAffRequest(AffiliationRequestForm form) {
return service.save(form);
}
}

View File

@ -8,6 +8,7 @@ import fr.titionfire.ffsaf.rest.from.FullMemberForm;
import fr.titionfire.ffsaf.utils.GroupeUtils;
import fr.titionfire.ffsaf.utils.PageResult;
import fr.titionfire.ffsaf.utils.Pair;
import fr.titionfire.ffsaf.utils.Utils;
import io.quarkus.oidc.IdToken;
import io.quarkus.security.Authenticated;
import io.quarkus.security.identity.SecurityIdentity;
@ -58,8 +59,10 @@ public class CombEndpoints {
@Path("/find/admin")
@RolesAllowed({"federation_admin"})
@Produces(MediaType.APPLICATION_JSON)
public Uni<PageResult<SimpleMembre>> getFindAdmin(@QueryParam("limit") Integer limit, @QueryParam("page") Integer page,
@QueryParam("search") String search, @QueryParam("club") String club) {
public Uni<PageResult<SimpleMembre>> getFindAdmin(@QueryParam("limit") Integer limit,
@QueryParam("page") Integer page,
@QueryParam("search") String search,
@QueryParam("club") String club) {
if (limit == null)
limit = 50;
if (page == null || page < 1)
@ -71,7 +74,8 @@ public class CombEndpoints {
@Path("/find/club")
@RolesAllowed({"club_president", "club_secretaire", "club_respo_intra"})
@Produces(MediaType.APPLICATION_JSON)
public Uni<PageResult<SimpleMembre>> getFindClub(@QueryParam("limit") Integer limit, @QueryParam("page") Integer page,
public Uni<PageResult<SimpleMembre>> getFindClub(@QueryParam("limit") Integer limit,
@QueryParam("page") Integer page,
@QueryParam("search") String search) {
if (limit == null)
limit = 50;
@ -99,7 +103,8 @@ public class CombEndpoints {
if (!out.equals("OK")) throw new InternalError("Fail to update data: " + out);
})).chain(() -> {
if (input.getPhoto_data().length > 0)
return Uni.createFrom().future(replacePhoto(id, input.getPhoto_data())).invoke(Unchecked.consumer(out -> {
return Uni.createFrom().future(Utils.replacePhoto(id, input.getPhoto_data(), media, "ppMembre"
)).invoke(Unchecked.consumer(out -> {
if (!out.equals("OK")) throw new InternalError("Fail to get MimeType " + out);
}));
else
@ -117,7 +122,8 @@ public class CombEndpoints {
if (id == null) throw new InternalError("Fail to creat member data");
})).call(id -> {
if (input.getPhoto_data().length > 0)
return Uni.createFrom().future(replacePhoto(id, input.getPhoto_data()));
return Uni.createFrom().future(Utils.replacePhoto(id, input.getPhoto_data(), media, "ppMembre"
));
else
return Uni.createFrom().nullItem();
});
@ -134,7 +140,8 @@ public class CombEndpoints {
if (!out.equals("OK")) throw new InternalError("Fail to update data: " + out);
})).chain(() -> {
if (input.getPhoto_data().length > 0)
return Uni.createFrom().future(replacePhoto(id, input.getPhoto_data())).invoke(Unchecked.consumer(out -> {
return Uni.createFrom().future(Utils.replacePhoto(id, input.getPhoto_data(), media, "ppMembre"
)).invoke(Unchecked.consumer(out -> {
if (!out.equals("OK")) throw new InternalError("Fail to get MimeType " + out);
}));
else
@ -153,38 +160,13 @@ public class CombEndpoints {
if (id == null) throw new InternalError("Fail to creat member data");
})).call(id -> {
if (input.getPhoto_data().length > 0)
return Uni.createFrom().future(replacePhoto(id, input.getPhoto_data()));
return Uni.createFrom().future(Utils.replacePhoto(id, input.getPhoto_data(), media, "ppMembre"
));
else
return Uni.createFrom().nullItem();
});
}
private Future<String> replacePhoto(long id, byte[] input) {
return CompletableFuture.supplyAsync(() -> {
try (InputStream is = new BufferedInputStream(new ByteArrayInputStream(input))) {
String mimeType = URLConnection.guessContentTypeFromStream(is);
String[] detectedExtensions = MimeTypes.findExtensionsByMimeTypes(mimeType, false);
if (detectedExtensions.length == 0)
throw new IOException("Fail to detect file extension for MIME type " + mimeType);
FilenameFilter filter = (directory, filename) -> filename.startsWith(String.valueOf(id));
File[] files = new File(media, "ppMembre").listFiles(filter);
if (files != null) {
for (File file : files) {
//noinspection ResultOfMethodCallIgnored
file.delete();
}
}
String extension = "." + detectedExtensions[0];
Files.write(new File(media, "ppMembre/" + id + extension).toPath(), input);
return "OK";
} catch (IOException e) {
return e.getMessage();
}
});
}
@GET
@Path("{id}/photo")
@RolesAllowed({"federation_admin", "club_president", "club_secretaire", "club_respo_intra"})

View File

@ -1,5 +1,6 @@
package fr.titionfire.ffsaf.rest.from;
import fr.titionfire.ffsaf.data.model.AffiliationRequestModel;
import jakarta.ws.rs.FormParam;
import jakarta.ws.rs.core.MediaType;
import lombok.Getter;
@ -28,4 +29,59 @@ public class AffiliationRequestForm {
@FormParam("logo")
@PartType(MediaType.APPLICATION_OCTET_STREAM)
private byte[] logo = new byte[0];
@FormParam("president-nom")
private String president_lname = null;
@FormParam("president-prenom")
private String president_fname = null;
@FormParam("president-mail")
private String president_email = null;
@FormParam("president-licence")
private String president_lincence = null;
@FormParam("tresorier-nom")
private String tresorier_lname = null;
@FormParam("tresorier-prenom")
private String tresorier_fname = null;
@FormParam("tresorier-mail")
private String tresorier_email = null;
@FormParam("tresorier-licence")
private String tresorier_lincence = null;
@FormParam("secretaire-nom")
private String secretaire_lname = null;
@FormParam("secretaire-prenom")
private String secretaire_fname = null;
@FormParam("secretaire-mail")
private String secretaire_email = null;
@FormParam("secretaire-licence")
private String secretaire_lincence = null;
public AffiliationRequestModel toModel() {
AffiliationRequestModel model = new AffiliationRequestModel();
model.setName(this.getName());
model.setSiren(this.getSiren());
model.setRNA(this.getRna());
model.setAddress(this.getAdresse());
model.setPresident_lname(this.getPresident_lname());
model.setPresident_fname(this.getPresident_fname());
model.setPresident_email(this.getPresident_email());
model.setPresident_lincence((this.getPresident_lincence() == null || this.getPresident_lincence().isBlank())
? 0 : Integer.parseInt(this.getPresident_lincence()));
model.setTresorier_lname(this.getTresorier_lname());
model.setTresorier_fname(this.getTresorier_fname());
model.setTresorier_email(this.getTresorier_email());
model.setTresorier_lincence((this.getPresident_lincence() == null || this.getPresident_lincence().isBlank())
? 0 : Integer.parseInt(this.getTresorier_lincence()));
model.setSecretaire_lname(this.getSecretaire_lname());
model.setSecretaire_fname(this.getSecretaire_fname());
model.setSecretaire_email(this.getSecretaire_email());
model.setSecretaire_lincence((this.getPresident_lincence() == null || this.getPresident_lincence().isBlank())
? 0 : Integer.parseInt(this.getSecretaire_lincence()));
return model;
}
}

View File

@ -1,7 +1,14 @@
package fr.titionfire.ffsaf.utils;
import jodd.net.MimeTypes;
import java.io.*;
import java.net.URLConnection;
import java.nio.file.Files;
import java.util.Calendar;
import java.util.Date;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
public class Utils {
@ -19,4 +26,35 @@ public class Utils {
return calendar.get(Calendar.YEAR) - 1;
}
}
public static Future<String> replacePhoto(long id, byte[] input, String media, String dir) {
return CompletableFuture.supplyAsync(() -> {
try (InputStream is = new BufferedInputStream(new ByteArrayInputStream(input))) {
String mimeType = URLConnection.guessContentTypeFromStream(is);
String[] detectedExtensions = MimeTypes.findExtensionsByMimeTypes(mimeType, false);
if (detectedExtensions.length == 0)
throw new IOException("Fail to detect file extension for MIME type " + mimeType);
File dirFile = new File(media, dir);
if (!dirFile.exists())
if (dirFile.mkdirs())
throw new IOException("Fail to create directory " + dir);
FilenameFilter filter = (directory, filename) -> filename.startsWith(String.valueOf(id));
File[] files = dirFile.listFiles(filter);
if (files != null) {
for (File file : files) {
//noinspection ResultOfMethodCallIgnored
file.delete();
}
}
String extension = "." + detectedExtensions[0];
Files.write(new File(dirFile, id + extension).toPath(), input);
return "OK";
} catch (IOException e) {
return e.getMessage();
}
});
}
}

View File

@ -31,28 +31,35 @@ export function DemandeAff() {
event.preventDefault()
const formData = new FormData(event.target)
toast.promise(
apiAxios.post(`asso/affiliation`, formData),
apiAxios.post(`asso/affiliation`, formData, { headers: {'Accept': '*/*'}}),
{
pending: "Enregistrement de la demande d'affiliation en cours",
success: "Demande d'affiliation enregistrée avec succès 🎉",
error: "Échec de la demande d'affiliation 😕"
}
).then(_ => {
navigate("/affiliation/ok")
// navigate("/affiliation/ok")
})
}
return <div>
<h1>Demande d'affiliation</h1>
<p>L'affiliation est annuelle et valable pour une saison sportive : du 1er septembre au 31 août de lannée suivante.</p>
<p>L'affiliation est annuelle et valable pour une saison sportive : du 1er septembre au 31 août de lannée
suivante.</p>
Pour saffilier, une association sportive doit réunir les conditions suivantes :
<ul>
<li>Avoir son siège social en France ou Principauté de Monaco</li>
<li>Être constituée conformément au chapitre 1er du titre II du livre 1er du Code du Sport</li>
<li>Poursuivre un objet social entrant dans la définition de larticle 1 des statuts de la Fédération</li>
<li>Disposer de statuts compatibles avec les principes dorganisation et de fonctionnement de la Fédération</li>
<li>Assurer en son sein la liberté dopinion et le respect des droits de la défense, et sinterdire toute discrimination</li>
<li>Respecter les règles dencadrement, dhygiène et de sécurité établies par les règlements de la Fédération</li>
<li>Disposer de statuts compatibles avec les principes dorganisation et de fonctionnement de la
Fédération
</li>
<li>Assurer en son sein la liberté dopinion et le respect des droits de la défense, et sinterdire toute
discrimination
</li>
<li>Respecter les règles dencadrement, dhygiène et de sécurité établies par les règlements de la
Fédération
</li>
</ul>
<div className="card mb-4">
@ -68,16 +75,20 @@ export function DemandeAff() {
<MembreInfo role="secretaire"/>
<div className="mb-3">
<p>Après validation de votre demande, vous recevrez un login et mot de passe provisoire pour accéder à votre espace FFSAF</p>
<p>Après validation de votre demande, vous recevrez un login et mot de passe provisoire pour
accéder à votre espace FFSAF</p>
Notez que pour finaliser votre affiliation, il vous faudra :
<ul>
<li>Disposer dau moins trois membres licenciés, dont le président, le trésorier et le secrétaire</li>
<li>Disposer dau moins trois membres licenciés, dont le président, le trésorier et le
secrétaire
</li>
<li>S'être acquitté des cotisations prévues par les règlements fédéraux</li>
</ul>
</div>
<div className="row">
<div className="d-grid gap-2 d-md-flex justify-content-md-center">
<button type="submit" className="btn btn-primary">Confirmer ma demande d'affiliation</button>
<button type="submit" className="btn btn-primary">Confirmer ma demande d'affiliation
</button>
</div>
</div>
</div>
@ -113,7 +124,8 @@ function AssoInfo() {
return <>
<div className="input-group mb-3">
<span className="input-group-text" id="basic-addon1">Nom de l'association*</span>
<input type="text" className="form-control" placeholder="Nom de l'association" name="name" aria-label="Nom de l'association"
<input type="text" className="form-control" placeholder="Nom de l'association" name="name"
aria-label="Nom de l'association"
aria-describedby="basic-addon1" required/>
</div>
@ -121,24 +133,29 @@ function AssoInfo() {
<span className="input-group-text">N° SIREN*</span>
<input type="number" className="form-control" placeholder="siren" name="siren" required value={siren}
onChange={e => setSiren(e.target.value)}/>
<button className="btn btn-outline-secondary" type="button" id="button-addon2" onClick={fetchSiren}>Rechercher</button>
<button className="btn btn-outline-secondary" type="button" id="button-addon2"
onClick={fetchSiren}>Rechercher
</button>
</div>
<div className="input-group mb-3">
<span className="input-group-text" id="basic-addon1">Dénomination</span>
<input type="text" className="form-control" placeholder="Appuyer sur rechercher pour compléter" aria-label="Dénomination"
<input type="text" className="form-control" placeholder="Appuyer sur rechercher pour compléter"
aria-label="Dénomination"
aria-describedby="basic-addon1" disabled value={denomination} readOnly/>
</div>
<div className="input-group mb-3">
<span className="input-group-text" id="basic-addon1">RNA</span>
<input type="text" className="form-control" placeholder="RNA" aria-label="RNA" aria-describedby="basic-addon1"
<input type="text" className="form-control" placeholder="RNA" aria-label="RNA"
aria-describedby="basic-addon1"
disabled={!rnaEnable} name="rna" value={rna} onChange={e => setRna(e.target.value)}/>
</div>
<div className="input-group mb-3">
<span className="input-group-text" id="basic-addon1">Adresse*</span>
<input type="text" className="form-control" placeholder="Adresse" aria-label="Adresse" aria-describedby="basic-addon1"
<input type="text" className="form-control" placeholder="Adresse" aria-label="Adresse"
aria-describedby="basic-addon1"
required value={adresse} name="adresse" onChange={e => setAdresse(e.target.value)}/>
</div>
@ -149,7 +166,8 @@ function AssoInfo() {
<div className="input-group mb-3">
<label className="input-group-text" htmlFor="logo">Logo*</label>
<input type="file" className="form-control" id="logo" name="logo" accept=".jpg,.jpeg,.gif,.png,.svg" required/>
<input type="file" className="form-control" id="logo" name="logo" accept=".jpg,.jpeg,.gif,.png,.svg"
required/>
</div>
</>;
}
@ -159,19 +177,29 @@ function MembreInfo({role}) {
<div className="col-sm-3">
<div className="form-floating">
<input type="text" className="form-control" id="floatingInput" placeholder="Nom" name={role + "-nom"}/>
<label htmlFor="floatingInput">Nom*</label>
<label htmlFor="floatingInput">Nom</label>
</div>
</div>
<div className="col-sm-3">
<div className="form-floating">
<input type="text" className="form-control" id="floatingInput" placeholder="Prénom" name={role + "-prenom"}/>
<label htmlFor="floatingInput">Prénom*</label>
<input type="text" className="form-control" id="floatingInput" placeholder="Prénom"
name={role + "-prenom"}/>
<label htmlFor="floatingInput">Prénom</label>
</div>
</div>
<div className="col-sm-5">
<div className="form-floating">
<input type="email" className="form-control" id="floatingInput" placeholder="name@example.com" name={role + "-mail"}/>
<label htmlFor="floatingInput">Email*</label>
<input type="email" className="form-control" id="floatingInput" placeholder="name@example.com"
name={role + "-mail"}/>
<label htmlFor="floatingInput">Email</label>
</div>
</div>
<div className="col-sm-3">
<div>OU</div>
<div className="form-floating">
<input type="number" className="form-control" id="floatingInput" placeholder="N° Licence"
name={role + "-licence"}/>
<label htmlFor="floatingInput">N° Licence</label>
</div>
</div>
</div>
@ -181,7 +209,8 @@ export function DemandeAffOk() {
return (
<div>
<h1 className="text-green-800 text-4xl">Demande d'affiliation envoyée avec succès</h1>
<p>Une fois votre demande validée, vous recevrez un login et mot de passe provisoire pour accéder à votre espace FFSAF</p>
<p>Une fois votre demande validée, vous recevrez un login et mot de passe provisoire pour accéder à votre
espace FFSAF</p>
</div>
);
}