rajah 10 месяцев назад
Родитель
Сommit
c83ca74cc5

BIN
.gradle/8.11.1/checksums/checksums.lock


BIN
.gradle/8.11.1/executionHistory/executionHistory.bin


BIN
.gradle/8.11.1/executionHistory/executionHistory.lock


BIN
.gradle/8.11.1/fileHashes/fileHashes.bin


BIN
.gradle/8.11.1/fileHashes/fileHashes.lock


BIN
.gradle/buildOutputCleanup/buildOutputCleanup.lock


BIN
.gradle/file-system.probe


+ 9 - 0
build.gradle

@@ -24,12 +24,17 @@ dependencies {
   implementation 'io.hypersistence:hypersistence-utils-hibernate-63:3.9.0'
   implementation 'org.hibernate.validator:hibernate-validator:8.0.2.Final'
   implementation 'com.google.guava:guava:33.4.0-jre'
+  
   implementation 'com.twelvemonkeys.imageio:imageio-core:3.12.0'
   implementation 'net.coobird:thumbnailator:0.4.20'
 	implementation 'org.json:json:20250107'
 	implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
+	
 	implementation 'org.apache.httpcomponents.client5:httpclient5:5.4.3'
 	
+	implementation 'org.apache.pdfbox:pdfbox:3.0.4' 
+	implementation 'com.github.vandeseer:easytable:1.0.2'
+	
 	runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
   runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
   runtimeOnly 'org.postgresql:postgresql'
@@ -42,6 +47,10 @@ dependencies {
   testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
 }
 
+configurations.implementation {
+    
+  exclude group: 'commons-logging', module: 'commons-logging'
+}
 
 tasks.named('test') { useJUnitPlatform() }
 

+ 9 - 5
src/main/java/fr/triplea/demovote/dao/ProductionRepository.java

@@ -35,7 +35,8 @@ public interface ProductionRepository extends JpaRepository<Production, Integer>
               + "p.nom_archive, "
               + "p.vignette, "
               + "p.numero_version,"
-              + "0 AS numero_categorie "
+              + "0 AS numero_categorie, "
+              + "0 AS ordre_presentation "
               + "FROM vote.productions AS p "
               + "INNER JOIN vote.participants AS g ON p.numero_participant = g.numero_participant "
               + "WHERE p.flag_actif IS TRUE "
@@ -62,7 +63,8 @@ public interface ProductionRepository extends JpaRepository<Production, Integer>
               + "p.nom_archive, "
               + "p.vignette, "
               + "p.numero_version,"
-              + "0 AS numero_categorie "
+              + "0 AS numero_categorie, "
+              + "0 AS ordre_presentation "
               + "FROM vote.productions AS p "
               + "INNER JOIN vote.participants AS g ON p.numero_participant = g.numero_participant "
               + "WHERE p.numero_production = :numeroProduction "
@@ -86,13 +88,15 @@ public interface ProductionRepository extends JpaRepository<Production, Integer>
               + "p.nom_archive, "
               + "p.vignette, "
               + "p.numero_version,"
-              + "s.numero_categorie "
+              + "s.numero_categorie, "
+              + "((c.numero_ordre * 10000) + s.numero_ordre) AS ordre_presentation "
               + "FROM vote.productions AS p "
               + "INNER JOIN vote.participants AS g ON p.numero_participant = g.numero_participant "
               + "INNER JOIN vote.presentations AS s ON p.numero_production = s.numero_production "
+              + "INNER JOIN vote.categories AS c ON s.numero_categorie = c.numero_categorie "
               + "WHERE p.flag_actif IS TRUE "
-              + "ORDER BY p.titre ASC ")
-  List<ProductionShort> findLinkedWithoutArchive();
+              + "ORDER BY ordre_presentation ASC, p.titre ASC ")
+  List<ProductionShort> findLinkedWithoutArchive(); // TODO ordonner par ordre de présentation
 
   @NativeQuery("SELECT DISTINCT " 
       + "p.numero_production, "

+ 2 - 1
src/main/java/fr/triplea/demovote/dto/ProductionShort.java

@@ -21,7 +21,8 @@ public record ProductionShort
   String nomArchive,
   byte[] vignette,
   Integer numeroVersion,
-  Integer numeroCategorie
+  Integer numeroCategorie,
+  Integer ordrePresentation
 ) 
 { 
   public Production toProduction() 

+ 37 - 0
src/main/java/fr/triplea/demovote/model/Presentation.java

@@ -1,14 +1,22 @@
 package fr.triplea.demovote.model;
 
+import java.sql.Types;
+import java.util.Base64;
+
+import org.hibernate.annotations.JdbcTypeCode;
+import org.springframework.util.StringUtils;
+
 import jakarta.persistence.Column;
 import jakarta.persistence.Entity;
 import jakarta.persistence.GeneratedValue;
 import jakarta.persistence.GenerationType;
 import jakarta.persistence.Id;
 import jakarta.persistence.JoinColumn;
+import jakarta.persistence.Lob;
 import jakarta.persistence.ManyToOne;
 import jakarta.persistence.OneToOne;
 import jakarta.persistence.Table;
+import jakarta.persistence.Transient;
 
 @Entity(name = "vote.presentations")
 @Table(name = "presentations")
@@ -29,6 +37,13 @@ public class Presentation
   private Production production;
   
   private Integer numeroOrdre;
+  
+  @Column(name="media_mime", length = 128)
+  private String mediaMime = "application/octet-stream";
+
+  @Lob @JdbcTypeCode(Types.BINARY)
+  @Column(name="media_data")
+  private byte[] mediaData;
 
   private Integer nombrePoints;
 
@@ -50,6 +65,28 @@ public class Presentation
   public void setNumeroOrdre(int n) { this.numeroOrdre = Integer.valueOf(n); }
   public Integer getNumeroOrdre() { return this.numeroOrdre; }
   
+  public void setMediaMime(String str) { if (str != null) { this.mediaMime = StringUtils.truncate(str, 128); } }
+  public String getMediaMime() { return this.mediaMime; }
+
+  public void setMediaData(String a) 
+  { 
+    if (a.startsWith("data:") && a.contains(",")) { a = a.split(",")[1]; } 
+  
+    try { this.mediaData = Base64.getDecoder().decode(a); } catch(Exception e) { this.mediaData = null; }
+  }
+  @Transient
+  public void setMediaData(byte[] a) { this.mediaData = (a == null) ? null : a.clone(); }
+  public String getMediaData() 
+  { 
+    if (this.mediaData == null) { return ""; } 
+    
+    return "data:" + this.mediaMime + "," + Base64.getEncoder().encodeToString(this.mediaData); 
+  }
+  @Transient
+  public byte[] getMediaDataAsBinary() { return this.mediaData; }
+  @Transient
+  public long getMediaDataSize() { if (this.mediaData == null) { return 0; } return this.mediaData.length; }
+  
   public void setNombrePoints(int n) { this.nombrePoints = Integer.valueOf(n); }
   public Integer getNombrePoints() { return this.nombrePoints; }
   

+ 8 - 17
src/main/java/fr/triplea/demovote/model/Production.java

@@ -97,12 +97,11 @@ public class Production
 
   private String informationsPrivees;
 
-  @Column(length = 256)
+  @Column(name="nom_archive", length = 256)
   private String nomArchive;
 
-  @Lob @JdbcTypeCode(Types.BINARY)
-  @Column(name="archive")
-  private byte[] archive;
+  @Column(name="nom_local", length = 256)
+  private String nomLocal;
 
   @Lob @JdbcTypeCode(Types.BINARY)
   @Column(name="vignette")
@@ -209,20 +208,13 @@ public class Production
   public void setNomArchive(String str) { if (str != null) { this.nomArchive = StringUtils.truncate(str, 256); } }
   public String getNomArchive() { return this.nomArchive; }
   
-  public void setArchive(String a) 
-  { 
-    if (a.startsWith("data:") && a.contains(",")) { a = a.split(",")[1]; } 
-  
-    try { this.archive = Base64.getDecoder().decode(a); } catch(Exception e) { this.archive = null; }
-  }
-  @Transient
-  public void setArchive(byte[] a) { this.archive = (a == null) ? null : a.clone(); }
-  public String getArchive() { if (this.archive == null) { return ""; } return "data:application/zip;base64," + Base64.getEncoder().encodeToString(this.archive); }
-  @Transient
-  public byte[] getArchiveAsBinary() { return this.archive; }
-  
+  public void setNomLocal(String str) { if (str != null) { this.nomLocal = StringUtils.truncate(str, 256); } }
+  public String getNomLocal() { return this.nomLocal; }
+
   public void setVignette(String v) 
   { 
+    // TODO : vignette par défaut, selon le type
+    
     String[] s;
     
     if (v.startsWith("data:") && v.contains(",")) 
@@ -314,7 +306,6 @@ public class Production
            .append(", plateforme=").append(plateforme)
            .append(", commentaire=").append(commentaire)
            .append(", nomArchive=").append(nomArchive)
-           .append(", archive=").append("" + archive.length + " bytes")
            .append(", vignette=").append("" + vignette.length + " bytes")
            .append(", version=").append(numeroVersion)
            .append(", créé=").append(dateCreation)

+ 170 - 2
src/main/java/fr/triplea/demovote/web/controller/PresentationController.java

@@ -1,12 +1,22 @@
 package fr.triplea.demovote.web.controller;
 
 
+import java.awt.Color;
+import java.io.ByteArrayOutputStream;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
 
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.pdmodel.PDPage;
+import org.apache.pdfbox.pdmodel.common.PDRectangle;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.MessageSource;
+import org.springframework.core.io.ByteArrayResource;
+import org.springframework.core.io.Resource;
+import org.springframework.http.HttpHeaders;
 import org.springframework.http.ResponseEntity;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -15,6 +25,11 @@ import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.servlet.LocaleResolver;
+import org.vandeseer.easytable.OverflowOnSamePageRepeatableHeaderTableDrawer;
+import org.vandeseer.easytable.settings.HorizontalAlignment;
+import org.vandeseer.easytable.structure.Row;
+import org.vandeseer.easytable.structure.Table;
+import org.vandeseer.easytable.structure.cell.TextCell;
 
 import fr.triplea.demovote.dao.CategorieRepository;
 import fr.triplea.demovote.dao.PresentationRepository;
@@ -31,8 +46,10 @@ import jakarta.servlet.http.HttpServletRequest;
 @RequestMapping("/presentation")
 public class PresentationController 
 {
+  @SuppressWarnings("unused") 
+  private static final Logger LOG = LoggerFactory.getLogger(PresentationController.class);
 
-  // TODO version PDF imprimable pour MrBio (pour la répartition à la remise des lots pendant l'affichage des résultats)
+  // TODO préparer média à partir de l'archive uploadée, pour les présentation + flag "préparé" sur chaque production présentée
   // TODO version diaporama pour affichage sur écran de régie
   // TODO raccourci 'ouvrir / fermer / calculer' les votes
   
@@ -64,6 +81,157 @@ public class PresentationController
     return ret;  
   }
 
+  private final static String LETTRES = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+
+  @GetMapping(value = "/file")
+  @PreAuthorize("hasRole('ADMIN')")
+  public ResponseEntity<Resource> getPresentationsVersionPDF(HttpServletRequest request) 
+  { 
+    Locale locale = localeResolver.resolveLocale(request);
+
+    List<Categorie> categories = categorieRepository.findAll();
+    
+    List<ProductionShort> productions = productionRepository.findLinkedWithoutArchive();
+   
+    if ((categories != null) && (productions != null)) 
+    { 
+      ByteArrayOutputStream baos = new ByteArrayOutputStream();
+      
+      try 
+      {
+        PDDocument document = new PDDocument();
+        
+        document.isAllSecurityToBeRemoved();
+
+        float POINTS_PER_INCH = 72;
+        float POINTS_PER_MM = 1 / (10 * 2.54f) * POINTS_PER_INCH;
+
+        PDRectangle A4_paysage = new PDRectangle(297 * POINTS_PER_MM, 210 * POINTS_PER_MM);
+        
+        Table.TableBuilder tb = Table.builder().addColumnsOfWidth(20f, 100f, 100f, 100f, 105f, 190f, 190f).padding(4);
+
+        for (Categorie categorie: categories)
+        {
+          if (categorie.isAvailable())
+          {
+            tb.addRow(createTitleRow(categorie.getLibelle()));
+            tb.addRow(createHeaderRow(locale));
+
+            int nombre = 0;
+            
+            if ((productions.size() > 0)) 
+            { 
+              for (ProductionShort production: productions) 
+              { 
+                if (production.numeroCategorie() == categorie.getNumeroCategorie())
+                {
+                  nombre++;
+                  
+                  tb.addRow(createProductionRow(production, nombre));
+                }
+              } 
+            } 
+            
+            tb.addRow(createTitleRow(categorie.getLibelle() + " : " + nombre + " " + messageSource.getMessage("show.pdf.productions", null, locale)));
+            tb.addRow(createEmptyRow());
+           }
+        }
+
+        OverflowOnSamePageRepeatableHeaderTableDrawer.builder()
+          .table(tb.build())
+          .startX(15f)
+          .startY(A4_paysage.getUpperRightY() - 15f)
+          .lanesPerPage(1)
+          .numberOfRowsToRepeat(0)
+          .spaceInBetween(1)
+          .endY(15f) // note: if not set, table is drawn over the end of the page
+          .build()
+          .draw(() -> document, () -> new PDPage(A4_paysage), 100f);
+
+        document.save(baos);
+        document.close();
+        
+        baos.close();
+      } 
+      catch (Exception e) { LOG.error(e.toString()); }
+       
+      byte[] binaire = baos.toByteArray();
+
+      Resource r = new ByteArrayResource(binaire);
+      
+      return ResponseEntity
+              .ok()
+              .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"presentations.pdf\"")
+              .header(HttpHeaders.CONTENT_LENGTH, "" + binaire.length)
+              .header(HttpHeaders.CONTENT_TYPE, "application/pdf")
+              .body(r); 
+    }
+    
+    return ResponseEntity.notFound().build();
+  }
+  private Row createTitleRow(String str) 
+  {
+    return Row.builder()
+              .add(TextCell.builder().text(str).colSpan(7).fontSize(10).backgroundColor(Color.WHITE).horizontalAlignment(HorizontalAlignment.LEFT).borderWidth(0.1f).build())
+              .build();
+  }
+  private Row createEmptyRow() 
+  {
+    return Row.builder()
+              .add(TextCell.builder().text(" ").colSpan(7).fontSize(10).backgroundColor(Color.WHITE).horizontalAlignment(HorizontalAlignment.LEFT).borderWidth(0).build())
+              .build();
+  }
+  private Row createHeaderRow(Locale locale) 
+  {
+    return Row.builder()
+              .add(createHeaderCell(""))
+              .add(createHeaderCell(messageSource.getMessage("show.pdf.title", null, locale)))
+              .add(createHeaderCell(messageSource.getMessage("show.pdf.authors", null, locale)))
+              .add(createHeaderCell(messageSource.getMessage("show.pdf.groups", null, locale)))
+              .add(createHeaderCell(messageSource.getMessage("show.pdf.manager", null, locale)))
+              .add(createHeaderCell(messageSource.getMessage("show.pdf.comments", null, locale)))
+              .add(createHeaderCell(messageSource.getMessage("show.pdf.private", null, locale)))
+              .build();
+  }
+  private TextCell createHeaderCell(String str)
+  {
+    return TextCell.builder()
+                   .text(str)
+                   .backgroundColor(Color.GRAY)
+                   .textColor(Color.WHITE)
+                   .horizontalAlignment(HorizontalAlignment.LEFT)
+                   .borderWidth(0.1f)
+                   .fontSize(8)
+                   .build();
+  }
+  private Row createProductionRow(ProductionShort production, int nombre) 
+  {
+    return Row.builder()
+              .add(createCell("#" + (nombre < 27 ? LETTRES.charAt(nombre - 1) : "?")))
+              .add(createCell(production.titre()))
+              .add(createCell(production.auteurs()))
+              .add(createCell(production.groupes()))
+              .add(createCell(production.nomGestionnaire()))
+              .add(createCell(production.commentaire()))
+              .add(createCell(production.informationsPrivees()))
+              .build();
+  }
+  private TextCell createCell(String str)
+  {
+    return TextCell.builder()
+                   .text(str)
+                   .backgroundColor(Color.WHITE)
+                   .textColor(Color.BLACK)
+                   .horizontalAlignment(HorizontalAlignment.LEFT)
+                   .borderWidth(0.1f)
+                   .fontSize(8)
+                   .build();
+  }
+
+  
+  
+  
+  
   
   @GetMapping(value = "/list-linked/{id}")
   @PreAuthorize("hasRole('ADMIN')")
@@ -265,5 +433,5 @@ public class PresentationController
      
     return ResponseEntity.notFound().build(); 
   }
-
+  
 }

+ 81 - 12
src/main/java/fr/triplea/demovote/web/controller/ProductionController.java

@@ -1,11 +1,18 @@
 package fr.triplea.demovote.web.controller;
 
 
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
 import java.util.ArrayList;
+import java.util.Base64;
 import java.util.List;
 import java.util.Locale;
+import java.util.UUID;
 import java.util.stream.Collectors;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.MessageSource;
 import org.springframework.core.io.ByteArrayResource;
@@ -43,6 +50,8 @@ import jakarta.servlet.http.HttpServletRequest;
 @RequestMapping("/production")
 public class ProductionController 
 {
+  @SuppressWarnings("unused") 
+  private static final Logger LOG = LoggerFactory.getLogger(ProductionController.class);
 
   @Autowired
   private ProductionRepository productionRepository;
@@ -56,7 +65,6 @@ public class ProductionController
   @Autowired
   private MessageSource messageSource;
 
-  // TODO : externaliser le stockage des fichiers
  
   @GetMapping(value = "/list")
   @PreAuthorize("hasRole('USER')")
@@ -90,11 +98,28 @@ public class ProductionController
       
       if ((numeroUser == 0) || (p.getNumeroGestionnaire() == numeroUser))
       {
-        Resource r = new ByteArrayResource(p.getArchiveAsBinary());
+        byte[] data = null;
+        
+        File f = new File("../uploads", p.getNomLocal());
+        
+        try 
+        {
+          data = new byte[(int) Math.min(f.length(), Integer.MAX_VALUE)]; //  limitation : 2 Go
+
+          FileInputStream fis = new FileInputStream(f);
+          
+          fis.read(data);
+          fis.close();
+        } 
+        catch (Exception e) { data = new byte[]{}; }
+        
+        
+        Resource r = new ByteArrayResource(data);
         
         return ResponseEntity
                 .ok()
                 .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + p.getNomArchive() + "\"")
+                .header(HttpHeaders.CONTENT_LENGTH, "" + data.length)
                 .header(HttpHeaders.CONTENT_TYPE, "application/zip")
                 .body(r); 
       }
@@ -169,10 +194,28 @@ public class ProductionController
       fresh.setPlateforme(production.plateforme());
       fresh.setCommentaire(production.commentaire());
       fresh.setInformationsPrivees(production.informationsPrivees());
-
       fresh.setParticipant(participant);
-      fresh.setNomArchive(production.nomArchive());
-      fresh.setArchive(production.archive());
+      
+      try 
+      { 
+        String nomLocal = UUID.nameUUIDFromBytes(production.nomArchive().getBytes()).toString() + ".zip";
+        
+        String donnees = production.archive();
+        
+        if (donnees.startsWith("data:") && donnees.contains(",")) { donnees = donnees.split(",")[1]; } 
+
+        File f = new File("../uploads", nomLocal);
+        
+        FileOutputStream fos = new FileOutputStream(f);
+
+        fos.write(Base64.getDecoder().decode(donnees));
+        fos.close();
+        
+        fresh.setNomArchive(production.nomArchive());
+        fresh.setNomLocal(nomLocal);
+      } 
+      catch(Exception e) { LOG.error(e.toString()); fresh.setNomArchive(null); fresh.setNomLocal(null); }
+      
       fresh.setVignette(production.vignette());
       fresh.setNumeroVersion(production.numeroVersion());
       
@@ -263,14 +306,40 @@ public class ProductionController
             {
               if (!(production.nomArchive().isBlank()))
               {
-                found.setNomArchive(production.nomArchive());
-                found.setArchive(production.archive());
-                found.setNumeroVersion(found.getNumeroVersion() + 1);
-                
-                productionRepository.save(found);
-           
                 MessagesTransfer mt = new MessagesTransfer();
-                mt.setInformation(messageSource.getMessage("production.file.updated", null, locale));
+               
+                try 
+                { 
+                  String nomLocal = UUID.nameUUIDFromBytes(production.nomArchive().getBytes()).toString() + ".zip";
+                  
+                  String donnees = production.archive();
+                  
+                  if (donnees.startsWith("data:") && donnees.contains(",")) { donnees = donnees.split(",")[1]; } 
+
+                  File f = new File("../uploads", nomLocal);
+                  
+                  FileOutputStream fos = new FileOutputStream(f);
+
+                  fos.write(Base64.getDecoder().decode(donnees));
+                  fos.close();
+                  
+                  found.setNomArchive(production.nomArchive());
+                  found.setNomLocal(nomLocal);
+                  found.setNumeroVersion(found.getNumeroVersion() + 1);
+                  
+                  mt.setInformation(messageSource.getMessage("production.file.updated", null, locale));
+                } 
+                catch(Exception e) 
+                { 
+                  LOG.error(e.toString()); 
+                  
+                  found.setNomArchive(null); 
+                  found.setNomLocal(null); 
+                  
+                  mt.setErreur(e.toString());
+                }
+
+                productionRepository.save(found);
 
                 return ResponseEntity.ok(mt);
               }

+ 8 - 0
src/main/resources/langs/messages_en.properties

@@ -33,3 +33,11 @@ account.password.old.missing=The old password is missing.
 account.password.new.missing=The new password is missing.
 account.password.old.failed=The old password is not the one registered in the database.
 account.password.changed=The new password was correctly registered in the database.
+
+show.pdf.title=Title
+show.pdf.authors=Author(s)
+show.pdf.groups=Group(s)
+show.pdf.manager=Manager
+show.pdf.comments=Comments
+show.pdf.private=Private informations
+show.pdf.productions=prod(s)

+ 8 - 0
src/main/resources/langs/messages_fr.properties

@@ -33,3 +33,11 @@ account.password.old.missing=L'ancien mot de passe est manquant.
 account.password.new.missing=Le nouveau mot de passe est manquant.
 account.password.old.failed=L'ancien mot de passe ne correspond pas à celui enregistré.
 account.password.changed=Le nouveau mot de passe est bien enregistré.
+
+show.pdf.title=Titre
+show.pdf.authors=Auteur(s)
+show.pdf.groups=Groupe(s)
+show.pdf.manager=Gestionnaire
+show.pdf.comments=Commentaires
+show.pdf.private=Informations privées
+show.pdf.productions=production(s)