#145 : merge de la branch dev.

This commit is contained in:
Loic Masi
2026-04-03 09:46:13 +02:00
51 changed files with 1118 additions and 315 deletions

View File

@@ -0,0 +1,26 @@
name: Deploiement API Prod Docker
on:
push:
branches:
- dev
jobs:
deploy:
name: Build et Déploiement
runs-on: ubuntu-latest
steps:
- name: 📥 Récupération du code source
uses: actions/checkout@v4
- name: 🔐 Injection des variables d'environnement
run: |
echo "PGSQL_CONNECTION=${{ secrets.PGSQL_CONNECTION }}" > .env
- name: 🐳 Redémarrage Docker
run: |
echo "🚀 Démarrage du déploiement Docker sur api-prod..."
docker compose down
docker compose up -d --build
echo "✅ Déploiement terminé !"

View File

@@ -74,7 +74,6 @@ jobs:
run: | run: |
chmod +x scripts/test-endpoints.sh chmod +x scripts/test-endpoints.sh
bash scripts/test-endpoints.sh http://localhost:5038 1000 2>&1 | tee /tmp/webzine_endpoint_output.txt bash scripts/test-endpoints.sh http://localhost:5038 1000 2>&1 | tee /tmp/webzine_endpoint_output.txt
EXIT_CODE=${PIPESTATUS[0]}
FAIL_COUNT=$(grep -cE "^\[ÉCHEC\]" /tmp/webzine_endpoint_output.txt 2>/dev/null || echo 0) FAIL_COUNT=$(grep -cE "^\[ÉCHEC\]" /tmp/webzine_endpoint_output.txt 2>/dev/null || echo 0)
SLOW_COUNT=$(grep -cE "^\[LENT\]" /tmp/webzine_endpoint_output.txt 2>/dev/null || echo 0) SLOW_COUNT=$(grep -cE "^\[LENT\]" /tmp/webzine_endpoint_output.txt 2>/dev/null || echo 0)
@@ -137,10 +136,4 @@ jobs:
-H "Authorization: token $GITEA_TOKEN" \ -H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d "$(jq -n --arg body "$BODY" '{body: $body}')" \ -d "$(jq -n --arg body "$BODY" '{body: $body}')" \
"$GITEA_SERVER_URL/api/v1/repos/$REPO/issues/$PR_NUMBER/comments" "$GITEA_SERVER_URL/api/v1/repos/$REPO/issues/$PR_NUMBER/comments"
- name: Fail job if performance issues detected
if: steps.perf_test.outputs.failed > 0 || steps.perf_test.outputs.slow > 0
run: |
echo "❌ Job failed due to performance issues"
exit 1

View File

@@ -1,9 +1,9 @@
namespace Webzine.WebApplication.Areas.Administration.ViewModels; namespace Webzine.Business.Contracts.Dto;
/// <summary> /// <summary>
/// ViewModel pour le tableau de bord de l'administration du webzine. /// DTO pour le tableau de bord de l'administration du webzine.
/// </summary> /// </summary>
public class DashboardViewModel public class DashboardDTO
{ {
/// <summary> /// <summary>
/// Définit le nombre total d'artistes chroniqués dans le webzine. /// Définit le nombre total d'artistes chroniqués dans le webzine.

View File

@@ -0,0 +1,57 @@
namespace Webzine.Business.Contracts.Dto;
/// <summary>
/// Dto transportant les données métier d'un titre saisi en administration.
/// </summary>
public class TitreAdminDTO
{
/// <summary>
/// Identifiant du titre (0 lors d'une création).
/// </summary>
public int Id { get; set; }
/// <summary>
/// Identifiant de l'artiste sélectionné.
/// </summary>
public int IdArtiste { get; set; }
/// <summary>
/// Libellé du titre.
/// </summary>
public string Libelle { get; set; } = string.Empty;
/// <summary>
/// Nom de l'album.
/// </summary>
public string Album { get; set; } = string.Empty;
/// <summary>
/// Texte de la chronique.
/// </summary>
public string Chronique { get; set; } = string.Empty;
/// <summary>
/// Date de sortie du titre.
/// </summary>
public DateTime DateSortie { get; set; }
/// <summary>
/// Durée en secondes.
/// </summary>
public int Duree { get; set; }
/// <summary>
/// URL de la jaquette.
/// </summary>
public string UrlJaquette { get; set; } = string.Empty;
/// <summary>
/// URL d'écoute.
/// </summary>
public string? UrlEcoute { get; set; }
/// <summary>
/// Identifiants des styles sélectionnés.
/// </summary>
public List<int> Styles { get; set; } = new ();
}

View File

@@ -0,0 +1,16 @@
namespace Webzine.Business.Contracts;
using Webzine.Business.Contracts.Dto;
/// <summary>
/// Service responsable du calcul des statistiques affichées sur le tableau de bord d'administration.
/// Agrège les données provenant de plusieurs repositories pour produire un résumé cohérent.
/// </summary>
public interface IDashboardService
{
/// <summary>
/// Calcule et retourne toutes les statistiques du tableau de bord en une seule passe.
/// </summary>
/// <returns>Un <see cref="DashboardDTO"/> contenant les agrégats calculés.</returns>
DashboardDTO GetDashboardData();
}

View File

@@ -0,0 +1,22 @@
namespace Webzine.Business.Contracts;
using Webzine.Business.Contracts.Dto;
/// <summary>
/// Service responsable des opérations d'administration sur les titres.
/// Orchestre la résolution des dépendances (artiste, styles) et la persistance.
/// </summary>
public interface ITitreAdminService
{
/// <summary>
/// Crée un nouveau titre à partir des données du formulaire d'administration.
/// </summary>
/// <param name="commande">Les données saisies dans le formulaire de création.</param>
void CreerTitre(TitreAdminDTO commande);
/// <summary>
/// Met à jour un titre existant à partir des données du formulaire d'administration.
/// </summary>
/// <param name="commande">Les données saisies dans le formulaire de modification.</param>
void ModifierTitre(TitreAdminDTO commande);
}

View File

@@ -0,0 +1,70 @@
namespace Webzine.Business;
using Webzine.Business.Contracts;
using Webzine.Business.Contracts.Dto;
using Webzine.Entity;
using Webzine.Repository.Contracts;
/// <summary>
/// Implémentation de <see cref="IDashboardService"/>.
/// Orchestre plusieurs appels aux repositories pour produire les statistiques du tableau de bord.
/// </summary>
public class DashboardService : IDashboardService
{
private readonly IArtisteRepository artisteRepository;
private readonly ITitreRepository titreRepository;
private readonly IStyleRepository styleRepository;
/// <summary>
/// Initializes a new instance of the <see cref="DashboardService"/> class.
/// </summary>
/// <param name="artisteRepository">Repository des artistes.</param>
/// <param name="titreRepository">Repository des titres.</param>
/// <param name="styleRepository">Repository des styles.</param>
public DashboardService(
IArtisteRepository artisteRepository,
ITitreRepository titreRepository,
IStyleRepository styleRepository)
{
this.artisteRepository = artisteRepository;
this.titreRepository = titreRepository;
this.styleRepository = styleRepository;
}
/// <inheritdoc/>
public DashboardDTO GetDashboardData()
{
IEnumerable<Titre> titres = this.titreRepository.FindAll();
Artiste? artisteLePlusChronique = titres
.GroupBy(t => t.Artiste)
.OrderByDescending(g => g.Count())
.FirstOrDefault()
?.Key;
Artiste? albumLePlusChronique = titres
.GroupBy(t => (t.Artiste, t.Album))
.GroupBy(g => g.Key.Artiste)
.OrderByDescending(g => g.Count())
.FirstOrDefault()
?.Key;
Titre? musiqueLaPlusJouee = titres
.OrderByDescending(t => t.NbLectures)
.FirstOrDefault();
return new DashboardDTO
{
NombreArtistes = this.artisteRepository.Count(),
ArtisteLePlusChronique = artisteLePlusChronique.Nom,
AlbumLePlusChronique = albumLePlusChronique.Nom,
NombreBiographies = this.artisteRepository.Count(a => !string.IsNullOrEmpty(a.Biographie)),
IdMusiqueLaPlusJouee = musiqueLaPlusJouee.IdTitre,
MusiqueLaPlusJouee = musiqueLaPlusJouee.Libelle,
NombreTitres = this.titreRepository.Count(),
NombreGenres = this.styleRepository.Count(),
NombreLectures = titres.Sum(t => t.NbLectures),
NombreLikes = titres.Sum(t => t.NbLikes),
};
}
}

View File

@@ -0,0 +1,124 @@
namespace Webzine.Business;
using Microsoft.Extensions.Logging;
using Webzine.Business.Contracts;
using Webzine.Business.Contracts.Dto;
using Webzine.Entity;
using Webzine.Repository.Contracts;
/// <summary>
/// Implémentation de <see cref="ITitreAdminService"/>.
/// Orchestre la résolution des styles, la construction de l'entité
/// et la délégation au repository.
/// </summary>
public class TitreAdminService : ITitreAdminService
{
private readonly ITitreRepository titreRepository;
private readonly IArtisteRepository artisteRepository;
private readonly IStyleRepository styleRepository;
private readonly ILogger<TitreAdminService> logger;
/// <summary>
/// Initializes a new instance of the <see cref="TitreAdminService"/> class.
/// </summary>
/// <param name="titreRepository">Repository des titres.</param>
/// <param name="artisteRepository">Repository des artistes.</param>
/// <param name="styleRepository">Repository des styles.</param>
/// <param name="logger">Service de journalisation.</param>
public TitreAdminService(
ITitreRepository titreRepository,
IArtisteRepository artisteRepository,
IStyleRepository styleRepository,
ILogger<TitreAdminService> logger)
{
this.titreRepository = titreRepository;
this.artisteRepository = artisteRepository;
this.styleRepository = styleRepository;
this.logger = logger;
}
/// <inheritdoc/>
public void CreerTitre(TitreAdminDTO commande)
{
this.logger.LogInformation(
"Création d'un nouveau titre '{Libelle}' pour l'artiste ID {IdArtiste}.",
commande.Libelle,
commande.IdArtiste);
Artiste artiste = this.artisteRepository.Find(commande.IdArtiste);
List<Style> styles = this.ResoudreStyles(commande.Styles);
var titre = new Titre
{
IdArtiste = artiste.IdArtiste,
Artiste = artiste,
Libelle = commande.Libelle,
Album = commande.Album,
Chronique = commande.Chronique,
DateCreation = DateTime.UtcNow,
DateSortie = commande.DateSortie,
Duree = commande.Duree,
UrlJaquette = commande.UrlJaquette,
UrlEcoute = commande.UrlEcoute ?? string.Empty,
Styles = styles,
Commentaires = new List<Commentaire>(),
};
this.titreRepository.Add(titre);
this.logger.LogInformation("Titre '{Libelle}' créé avec succès (ID {IdTitre}).", titre.Libelle, titre.IdTitre);
}
/// <inheritdoc/>
public void ModifierTitre(TitreAdminDTO commande)
{
this.logger.LogInformation("Modification du titre ID {Id} ('{Libelle}').", commande.Id, commande.Libelle);
List<Style> styles = this.ResoudreStyles(commande.Styles);
// On charge le titre existant pour ne pas écraser NbLectures / NbLikes
Titre existant = this.titreRepository.Find(commande.Id);
existant.IdArtiste = commande.IdArtiste;
existant.Libelle = commande.Libelle;
existant.Album = commande.Album;
existant.Chronique = commande.Chronique;
existant.DateSortie = commande.DateSortie;
existant.Duree = commande.Duree;
existant.UrlJaquette = commande.UrlJaquette;
existant.UrlEcoute = commande.UrlEcoute ?? string.Empty;
existant.Styles.Clear();
foreach (var style in styles)
{
existant.Styles.Add(style);
}
this.titreRepository.Update(existant);
this.logger.LogInformation("Titre ID {Id} modifié avec succès.", commande.Id);
}
/// <summary>
/// Résout les entités <see cref="Style"/> à partir d'une liste d'identifiants.
/// Les identifiants introuvables sont ignorés avec un avertissement.
/// </summary>
/// <param name="styleIds">Identifiants des styles sélectionnés.</param>
/// <returns>Liste des styles résolus.</returns>
private List<Style> ResoudreStyles(List<int> styleIds)
{
var styles = new List<Style>();
foreach (int id in styleIds)
{
Style style = this.styleRepository.Find(id);
styles.Add(style);
}
this.logger.LogDebug("{NbResolus}/{NbDemandes} styles résolus avec succès.", styles.Count, styleIds.Count);
return styles;
}
}

View File

@@ -23,7 +23,10 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Webzine.Business.Contracts\Webzine.Business.Contracts.csproj" />
<ProjectReference Include="..\Webzine.Entity\Webzine.Entity.csproj" /> <ProjectReference Include="..\Webzine.Entity\Webzine.Entity.csproj" />
<ProjectReference Include="..\Webzine.Repository.Contracts\Webzine.Repository.Contracts.csproj" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-preview.1.25080.5" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -6,6 +6,11 @@
COMMIT_MSG=$(cat "$1") COMMIT_MSG=$(cat "$1")
# Skip validation for rebase or CI commits
if echo "$COMMIT_MSG" | grep -qiE "(Rebase|rebase|CI|merge|Merge)"; then
exit 0
fi
if [ ${#COMMIT_MSG} -le 10 ]; then if [ ${#COMMIT_MSG} -le 10 ]; then
echo "❌ Erreur : Le message doit faire plus de 10 caractères." echo "❌ Erreur : Le message doit faire plus de 10 caractères."
exit 1 exit 1

View File

@@ -3,7 +3,7 @@ namespace Webzine.Repository.Contracts
using Webzine.Entity; using Webzine.Entity;
/// <summary> /// <summary>
/// Défini une interface <see cref="IArtisteRepository"/> pour gérer les opérations de base de données liées aux artistes. /// Défini une interface <see cref="IArtisteRepository"/> pour gérer les opérations des artistes dans la source de données.
/// </summary> /// </summary>
public interface IArtisteRepository public interface IArtisteRepository
{ {
@@ -23,7 +23,7 @@ namespace Webzine.Repository.Contracts
/// Récupère un artiste par son identifiant unique. Si aucun artiste n'est trouvé, retourne null. /// Récupère un artiste par son identifiant unique. Si aucun artiste n'est trouvé, retourne null.
/// </summary> /// </summary>
/// <param name="id">L'identifiant de l'artiste.</param> /// <param name="id">L'identifiant de l'artiste.</param>
/// <returns></returns> /// <returns>L'artiste trouvé ou null.</returns>
Artiste Find(int id); Artiste Find(int id);
/// <summary> /// <summary>
@@ -51,5 +51,18 @@ namespace Webzine.Repository.Contracts
/// <param name="nom">Nom de l'artiste.</param> /// <param name="nom">Nom de l'artiste.</param>
/// <returns>IEnumarble.<Artiste> qui contient la chaine de caractere.</returns> /// <returns>IEnumarble.<Artiste> qui contient la chaine de caractere.</returns>
IEnumerable<Artiste> Search(string nom); IEnumerable<Artiste> Search(string nom);
/// <summary>
/// Récupère le nombre total d'artistes dans la collection.
/// </summary>
/// <returns>Le nombre total d'artistes.</returns>
int Count();
/// <summary>
/// Récupère le nombre d'artistes correspondant au prédicat fourni.
/// </summary>
/// <param name="predicate">Le prédicat de filtrage.</param>
/// <returns>Le nombre d'artistes correspondants.</returns>
int Count(Func<Artiste, bool> predicate);
} }
} }

View File

@@ -2,14 +2,43 @@ namespace Webzine.Repository.Contracts
{ {
using Webzine.Entity; using Webzine.Entity;
/// <summary>
/// Interface de repository pour les commentaires.
/// </summary>
public interface ICommentaireRepository public interface ICommentaireRepository
{ {
/// <summary>
/// Ajoute un commentaire à la source de données.
/// </summary>
/// <param name="commentaire">Commentaire à ajouter.</param>
void Add(Commentaire commentaire); void Add(Commentaire commentaire);
/// <summary>
/// Supprime un commentaire de la source de données.
/// </summary>
/// <param name="commentaire">Commentaire à supprimer.</param>
void Delete(Commentaire commentaire); void Delete(Commentaire commentaire);
/// <summary>
/// Trouve un commentaire par son ID.
/// </summary>
/// <param name="id">ID du commentaire à trouver.</param>
/// <returns>Le commentaire trouvé, ou null si non trouvé.</returns>
Commentaire Find(int id); Commentaire Find(int id);
/// <summary>
/// Retourne tous les commentaires de la source de données.
/// </summary>
/// <returns>Une collection de commentaires.</returns>
IEnumerable<Commentaire> FindAll(); IEnumerable<Commentaire> FindAll();
/// <summary>
/// Retourne une collection de commentaires paginée à partir de la source de données.
/// </summary>
/// <param name="offset">Le nombre de commentaires à ignorer avant de commencer à
/// récupérer les commentaires.</param>
/// <param name="limit">Le nombre maximum de commentaires à récupérer.</param>
/// <returns>Une collection de commentaires paginée.</returns>
IEnumerable<Commentaire> FindCommentaires(int offset, int limit);
} }
} }

View File

@@ -37,5 +37,11 @@ namespace Webzine.Repository.Contracts
/// </summary> /// </summary>
/// <param name="style">L'objet style à mettre à jour.</param /// <param name="style">L'objet style à mettre à jour.</param
void Update(Style style); void Update(Style style);
/// <summary>
/// Récupère le nombre total de styles dans la liste des styles.
/// </summary>
/// <returns>Le nombre total de styles présents dans la liste.</returns>
int Count();
} }
} }

View File

@@ -13,7 +13,6 @@ namespace Webzine.Repository
/// <summary> /// <summary>
/// Initialise une classe <see cref="DbArtisteRepository"/> qui implémente l'interface <see cref="IArtisteRepository"/> pour gérer les opérations de base de données liées aux artistes. /// Initialise une classe <see cref="DbArtisteRepository"/> qui implémente l'interface <see cref="IArtisteRepository"/> pour gérer les opérations de base de données liées aux artistes.
/// Utilise <see cref="IArtisteRepository"/> en injection de dépendances.
/// </summary> /// </summary>
public class DbArtisteRepository : IArtisteRepository public class DbArtisteRepository : IArtisteRepository
{ {
@@ -23,8 +22,8 @@ namespace Webzine.Repository
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DbArtisteRepository"/> class. /// Initializes a new instance of the <see cref="DbArtisteRepository"/> class.
/// </summary> /// </summary>
/// <param name="context">Le contexte de base de données à utiliser pour accéder aux entités et effectuer des opérations de /// <param name="context">Le contexte de base de données à utiliser pour accéder aux entités et effectuer des opérations de persistance.</param>
/// persistance. Ne peut pas être null.</param> /// <param name="logger">Le service de journalisation.</param>
public DbArtisteRepository(WebzineDbContext context, ILogger<LocalArtisteRepository> logger) public DbArtisteRepository(WebzineDbContext context, ILogger<LocalArtisteRepository> logger)
{ {
this.logger = logger; this.logger = logger;
@@ -56,11 +55,6 @@ namespace Webzine.Repository
{ {
try try
{ {
if (artiste == null)
{
throw new ArgumentNullException(nameof(artiste), "L'artiste à supprimer ne peut pas être null.");
}
this.context.Artistes.Remove(artiste); this.context.Artistes.Remove(artiste);
this.context.SaveChanges(); this.context.SaveChanges();
this.logger.LogDebug("L'artiste {IdArtiste} a bien été supprimé", artiste.IdArtiste); this.logger.LogDebug("L'artiste {IdArtiste} a bien été supprimé", artiste.IdArtiste);
@@ -84,7 +78,7 @@ namespace Webzine.Repository
{ {
Artiste artiste = this.context.Artistes Artiste artiste = this.context.Artistes
.Include(a => a.Titres) .Include(a => a.Titres)
.FirstOrDefault(a => a.IdArtiste == id); .SingleOrDefault(a => a.IdArtiste == id);
return artiste; return artiste;
} }
catch (Exception ex) catch (Exception ex)
@@ -118,27 +112,33 @@ namespace Webzine.Repository
try try
{ {
// .AsNoTracking() rend la requête beaucoup plus rapide pour de la lecture // .AsNoTracking() rend la requête beaucoup plus rapide pour de la lecture
var artistes = this.context.Artistes.AsNoTracking().Include(t => t.Titres).ToList(); // Pas besoin de faire un ToList() ici, car on retourne un IEnumerable<Artiste> et EF Core gère l'exécution différée de la requête.
this.logger.LogDebug("{Count} artistes récupérés de la base.", artistes.Count); var artistes = this.context.Artistes
.AsNoTracking()
.Include(t => t.Titres);
this.logger.LogDebug("La liste d'artistes a été récupérée de la base.");
return artistes; return artistes;
} }
catch (Exception ex) catch (Exception ex)
{ {
this.logger.LogError(ex, "Erreur lors de la récupération de tous les artistes."); this.logger.LogError(ex, "Erreur lors de la récupération de tous les artistes.");
return Enumerable.Empty<Artiste>(); // Retourne une liste vide au lieu de faire crash l'UI throw;
} }
} }
/// <inheritdoc/> /// <inheritdoc/>
public void Update(Artiste artiste) public void Update(Artiste artiste)
{ {
if (artiste == null)
{
throw new ArgumentNullException(nameof(artiste));
}
try try
{ {
Artiste existingArtiste = this.Find(artiste.IdArtiste); // Vérifie que l'artiste existe avant de tenter de le mettre à jour
if (existingArtiste == null)
{
this.logger.LogWarning("L'artiste {Id} n'a pas été trouvé pour l'update.", artiste.IdArtiste);
throw new InvalidOperationException($"L'artiste avec l'ID {artiste.IdArtiste} n'a pas été trouvé pour la mise à jour.");
}
this.context.Artistes.Update(artiste); this.context.Artistes.Update(artiste);
this.context.SaveChanges(); this.context.SaveChanges();
this.logger.LogDebug("Artiste {Id} ({Nom}) mis à jour avec succès.", artiste.IdArtiste, artiste.Nom); this.logger.LogDebug("Artiste {Id} ({Nom}) mis à jour avec succès.", artiste.IdArtiste, artiste.Nom);
@@ -167,8 +167,7 @@ namespace Webzine.Repository
var artiste = this.context.Artistes var artiste = this.context.Artistes
.Where(a => a.Nom.ToLower().Contains(mot.ToLower())) .Where(a => a.Nom.ToLower().Contains(mot.ToLower()))
.Include(t => t.Titres) .Include(t => t.Titres)
.AsNoTracking() .AsNoTracking();
.ToList();
return artiste; return artiste;
} }
catch (Exception ex) catch (Exception ex)
@@ -176,5 +175,37 @@ namespace Webzine.Repository
throw new Exception("Erreur lors de la recherche d'artiste {error}", ex); throw new Exception("Erreur lors de la recherche d'artiste {error}", ex);
} }
} }
/// <inheritdoc/>
public int Count()
{
try
{
int count = Enumerable.Count(this.context.Artistes);
this.logger.LogDebug("Nombre total d'artistes dans la base: {Count}", count);
return count;
}
catch (Exception ex)
{
this.logger.LogError(ex, "Erreur lors du comptage des artistes.");
throw;
}
}
/// <inheritdoc/>
public int Count(Func<Artiste, bool> predicate)
{
try
{
int count = this.context.Artistes.Count(predicate);
this.logger.LogDebug("Nombre d'artistes (avec prédicat): {Count}", count);
return count;
}
catch (Exception ex)
{
this.logger.LogError(ex, "Erreur lors du comptage des artistes avec prédicat.");
throw;
}
}
} }
} }

View File

@@ -16,10 +16,9 @@ public class DbCommentaireRepository : ICommentaireRepository
private readonly WebzineDbContext context; private readonly WebzineDbContext context;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DbCommentaireRepository"/> class.
/// Initialisation de <see cref="DbCommentaireRepository"/>. /// Initialisation de <see cref="DbCommentaireRepository"/>.
/// </summary> /// </summary>
/// <param name="logger">Le service de journalisation injecté pour suivre les opérations du repository.</param> /// <param name="logger">Le service de journalisation.</param>
/// <param name="context">Le contexte de base de données injecté.</param> /// <param name="context">Le contexte de base de données injecté.</param>
public DbCommentaireRepository(ILogger<DbCommentaireRepository> logger, WebzineDbContext context) public DbCommentaireRepository(ILogger<DbCommentaireRepository> logger, WebzineDbContext context)
{ {
@@ -33,7 +32,6 @@ public class DbCommentaireRepository : ICommentaireRepository
{ {
try try
{ {
this.logger.LogDebug("Ajout d'un nouveau commentaire de l'auteur : {Auteur}", commentaire.Auteur);
this.context.Commentaires.Add(commentaire); this.context.Commentaires.Add(commentaire);
this.context.SaveChanges(); this.context.SaveChanges();
this.logger.LogDebug("Commentaire ajouté avec l'id : {Id}", commentaire.IdCommentaire); this.logger.LogDebug("Commentaire ajouté avec l'id : {Id}", commentaire.IdCommentaire);
@@ -55,11 +53,6 @@ public class DbCommentaireRepository : ICommentaireRepository
{ {
try try
{ {
if (commentaire == null)
{
throw new ArgumentNullException(nameof(commentaire), "Le commentaire à supprimer ne peut pas être null.");
}
this.context.Commentaires.Remove(commentaire); this.context.Commentaires.Remove(commentaire);
this.context.SaveChanges(); this.context.SaveChanges();
this.logger.LogDebug("Le commentaire {IdCommentaire} a bien été supprimé", commentaire.IdCommentaire); this.logger.LogDebug("Le commentaire {IdCommentaire} a bien été supprimé", commentaire.IdCommentaire);
@@ -76,14 +69,6 @@ public class DbCommentaireRepository : ICommentaireRepository
} }
} }
/// <inheritdoc/>
public int Count()
{
var count = this.context.Commentaires.Count();
this.logger.LogDebug("Compte total des commentaires : {Count}", count);
return count;
}
/// <inheritdoc/> /// <inheritdoc/>
public Commentaire Find(int idCommentaire) public Commentaire Find(int idCommentaire)
{ {
@@ -92,50 +77,41 @@ public class DbCommentaireRepository : ICommentaireRepository
// On inclut le titre car il est souvent affiché avec le commentaire // On inclut le titre car il est souvent affiché avec le commentaire
return this.context.Commentaires return this.context.Commentaires
.Include(c => c.Titre) .Include(c => c.Titre)
.FirstOrDefault(c => c.IdCommentaire == idCommentaire); .SingleOrDefault(c => c.IdCommentaire == idCommentaire);
} }
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerable<Commentaire> FindAll() public IEnumerable<Commentaire> FindAll()
{ {
this.logger.LogDebug("Récupération de tous les commentaires");
var commentaires = this.context.Commentaires var commentaires = this.context.Commentaires
.AsNoTracking()
.Include(c => c.Titre) .Include(c => c.Titre)
.OrderByDescending(c => c.DateCreation) .OrderByDescending(c => c.DateCreation);
.ToList();
this.logger.LogDebug("Nombre de commentaires trouvés : {Count}", commentaires.Count); this.logger.LogDebug("La liste de commentaires a été récupérée.");
return commentaires; return commentaires;
} }
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerable<Commentaire> FindCommentaires(int offset, int limit) public IEnumerable<Commentaire> FindCommentaires(int offset, int limit)
{ {
this.logger.LogDebug("Recherche paginée des commentaires (offset : {Offset}, limit : {Limit})", offset, limit); try
{
this.logger.LogDebug("Recherche paginée des commentaires (offset : {Offset}, limit : {Limit})", offset, limit);
var commentaires = this.context.Commentaires var commentaires = this.context.Commentaires
.Include(c => c.Titre) .AsNoTracking()
.OrderByDescending(c => c.DateCreation) .Include(c => c.Titre)
.Skip(offset) .OrderByDescending(c => c.DateCreation)
.Take(limit) .Skip(offset)
.ToList(); .Take(limit);
this.logger.LogDebug("{Count} commentaires trouvés pour cette page", commentaires.Count); return commentaires;
return commentaires; }
} catch (Exception ex)
{
/// <inheritdoc/> this.logger.LogError(ex, "Erreur lors de la pagination des commentaires (offset : {Offset}, limit : {Limit})", offset, limit);
public IEnumerable<Commentaire> FindByIdTitre(int idTitre) throw new Exception("Une erreur est survenue lors de la pagination des commentaires.", ex);
{ }
this.logger.LogDebug("Recherche des commentaires pour le titre ID : {IdTitre}", idTitre);
var commentaires = this.context.Commentaires
.Where(c => c.Titre.IdTitre == idTitre)
.OrderByDescending(c => c.DateCreation)
.ToList();
this.logger.LogDebug($"{commentaires.Count} commentaires trouvés pour l'ID de titre : {idTitre}");
return commentaires;
} }
} }

View File

@@ -8,10 +8,17 @@ namespace Webzine.Repository
using Webzine.Entity; using Webzine.Entity;
using Webzine.Entity.Fixtures; using Webzine.Entity.Fixtures;
/// <summary>
/// Classe de repository pour les entités de la base de données.
/// </summary>
public class DbEntityRepository public class DbEntityRepository
{ {
private readonly WebzineDbContext context; private readonly WebzineDbContext context;
/// <summary>
/// Constructeur de DbEntityRepository.
/// </summary>
/// <param name="context">DB context.</param>
public DbEntityRepository(WebzineDbContext context) public DbEntityRepository(WebzineDbContext context)
{ {
this.context = context; this.context = context;

View File

@@ -93,27 +93,18 @@ public class DbStyleRepository : IStyleRepository
{ {
this.logger.LogDebug("Recherche du style avec l'ID: {Id}", id); this.logger.LogDebug("Recherche du style avec l'ID: {Id}", id);
if (id <= 0)
{
this.logger.LogWarning("Tentative de recherche d'un style avec un Id invalide: {Id}", id);
return new Style();
}
this.logger.LogDebug("Préparation de la requête avec inclusion des titres");
var style = this.context.Styles var style = this.context.Styles
.AsNoTracking()
.Include(s => s.Titres) .Include(s => s.Titres)
.FirstOrDefault(s => s.IdStyle == id); .SingleOrDefault(s => s.IdStyle == id);
if (style == null) if (style == null)
{ {
this.logger.LogWarning("Style avec l'ID {Id} non trouvé", id); this.logger.LogWarning("Style avec l'ID {Id} non trouvé", id);
style = new Style(); return null;
}
else
{
this.logger.LogDebug("Style trouvé: {Libelle}", style.Libelle);
} }
this.logger.LogDebug("Style trouvé: {Libelle}", style.Libelle);
return style; return style;
} }
catch (Exception ex) catch (Exception ex)
@@ -132,10 +123,10 @@ public class DbStyleRepository : IStyleRepository
this.logger.LogDebug("Tri des styles par libellé"); this.logger.LogDebug("Tri des styles par libellé");
var styles = this.context.Styles var styles = this.context.Styles
.OrderBy(s => s.Libelle) .AsNoTracking()
.ToList(); .OrderBy(s => s.Libelle);
this.logger.LogDebug("{Count} styles récupérés", styles.Count); this.logger.LogDebug("La liste de styles a été récupérée.");
return styles; return styles;
} }
catch (Exception ex) catch (Exception ex)
@@ -151,18 +142,15 @@ public class DbStyleRepository : IStyleRepository
try try
{ {
this.logger.LogInformation("Mise à jour du style avec l'ID: {IdStyle}", style.IdStyle); this.logger.LogInformation("Mise à jour du style avec l'ID: {IdStyle}", style.IdStyle);
this.logger.LogDebug("Recherche du style en base de données"); Style existingStyle = this.Find(style.IdStyle); // Vérifie que le style existe avant de tenter de le mettre à jour
var existingStyle = this.context.Styles.Find(style.IdStyle);
if (existingStyle == null) if (existingStyle == null)
{ {
this.logger.LogWarning("Style avec l'ID {IdStyle} non trouvé pour la mise à jour", style.IdStyle); this.logger.LogWarning("Style avec l'ID {IdStyle} non trouvé pour l'update.", style.IdStyle);
throw new InvalidOperationException($"Style avec l'ID {style.IdStyle} non trouvé."); throw new InvalidOperationException($"Style avec l'ID {style.IdStyle} non trouvé pour la mise à jour.");
} }
// Update properties this.context.Styles.Update(style);
this.logger.LogDebug("Style trouvé, mise à jour des propriétés");
existingStyle.Libelle = style.Libelle;
this.context.SaveChanges(); this.context.SaveChanges();
this.logger.LogDebug("Style mis à jour avec succès: {IdStyle}", style.IdStyle); this.logger.LogDebug("Style mis à jour avec succès: {IdStyle}", style.IdStyle);
@@ -178,4 +166,20 @@ public class DbStyleRepository : IStyleRepository
throw; throw;
} }
} }
/// <inheritdoc/>
public int Count()
{
try
{
int count = Enumerable.Count(this.context.Styles);
this.logger.LogDebug("Nombre total de styles: {Count}", count);
return count;
}
catch (Exception ex)
{
this.logger.LogError(ex, "Erreur lors du comptage des styles");
throw;
}
}
} }

View File

@@ -192,16 +192,13 @@ public class DbTitreRepository : ITitreRepository
try try
{ {
this.logger.LogInformation("Mise à jour du titre avec l'ID: {IdTitre}", titre.IdTitre); this.logger.LogInformation("Mise à jour du titre avec l'ID: {IdTitre}", titre.IdTitre);
this.logger.LogDebug("Début de la mise à jour du titre en base de données");
var existingTitre = this.context.Titres.Find(titre.IdTitre); Titre existingTitre = this.Find(titre.IdTitre);
if (existingTitre != null) if (existingTitre != null)
{ {
this.logger.LogDebug("Titre trouvé, mise à jour des propriétés");
this.context.Entry(existingTitre).CurrentValues.SetValues(titre); this.context.Entry(existingTitre).CurrentValues.SetValues(titre);
// Handle many-to-many relationships // Relation many-to-many
this.logger.LogDebug("Mise à jour des relations many-to-many (Styles)");
this.context.Entry(existingTitre).Collection(t => t.Styles).Load(); this.context.Entry(existingTitre).Collection(t => t.Styles).Load();
existingTitre.Styles.Clear(); existingTitre.Styles.Clear();
foreach (var style in titre.Styles) foreach (var style in titre.Styles)

View File

@@ -10,22 +10,21 @@ namespace Webzine.Repository
using Webzine.Repository.Contracts; using Webzine.Repository.Contracts;
/// <summary> /// <summary>
/// Initialise une classe <see cref="LocalArtisteRepository"/> qui implémente l'interface <see cref="IArtisteRepository"/> pour gérer les opérations de base de données liées aux artistes. /// Initialise une classe <see cref="LocalArtisteRepository"/> qui implémente l'interface <see cref="IArtisteRepository"/>.
/// Utilise <see cref="IArtisteRepository"/> en injection de dépendances. /// Gère les opérations liées aux artistes en utilisant une source de données locale (en mémoire).
/// </summary> /// </summary>
public class LocalArtisteRepository : IArtisteRepository public class LocalArtisteRepository : IArtisteRepository
{ {
private readonly ILogger<LocalArtisteRepository> logger; private readonly ILogger<LocalArtisteRepository> logger;
// private readonly List<Artiste> artistes;
private readonly InMemoryDataStore dataStore; private readonly InMemoryDataStore dataStore;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="LocalArtisteRepository"/> class. /// Initializes a new instance of the <see cref="LocalArtisteRepository"/> class.
/// Est liéee à une liste d'artistes en local et utilise un logger pour enregistrer les opérations effectuées sur les artistes. /// Est liéee à une liste d'artistes en local et utilise un logger pour enregistrer les opérations effectuées sur les artistes.
/// </summary> /// </summary>
/// <param name="artistes">La liste des artistes à initialiser. Ne peut pas être null.</param>
/// <param name="logger">Le logger à utiliser pour enregistrer les messages de journalisation. Ne peut pas être null.</param> /// <param name="logger">Le logger à utiliser pour enregistrer les messages de journalisation. Ne peut pas être null.</param>
/// <param name="dataStore">Le magasin de données en mémoire.</param>
public LocalArtisteRepository(InMemoryDataStore dataStore, ILogger<LocalArtisteRepository> logger) public LocalArtisteRepository(InMemoryDataStore dataStore, ILogger<LocalArtisteRepository> logger)
{ {
this.logger = logger; this.logger = logger;
@@ -37,25 +36,19 @@ namespace Webzine.Repository
/// <inheritdoc/> /// <inheritdoc/>
public void Add(Artiste artiste) public void Add(Artiste artiste)
{ {
throw new NotSupportedException("Mode Local"); this.dataStore.Artistes.Add(artiste);
} }
/// <inheritdoc/> /// <inheritdoc/>
public void Delete(Artiste artiste) public void Delete(Artiste artiste)
{ {
throw new NotSupportedException("Mode Local"); this.dataStore.Artistes.Remove(artiste);
} }
/// <inheritdoc/> /// <inheritdoc/>
public Artiste Find(int id) public Artiste Find(int id)
{ {
var artiste = this.dataStore.Artistes.First(a => a.IdArtiste == id); return this.dataStore.Artistes.SingleOrDefault(a => a.IdArtiste == id);
if (artiste == null)
{
return new Artiste();
}
return artiste;
} }
/// <inheritdoc/> /// <inheritdoc/>
@@ -74,7 +67,6 @@ namespace Webzine.Repository
} }
/// <inheritdoc/> /// <inheritdoc/>
/// La liste retournée est une copie de la liste interne, donc elle ne peut être nulle.
public IEnumerable<Artiste> FindAll() public IEnumerable<Artiste> FindAll()
{ {
return this.dataStore.Artistes; return this.dataStore.Artistes;
@@ -83,7 +75,16 @@ namespace Webzine.Repository
/// <inheritdoc/> /// <inheritdoc/>
public void Update(Artiste artiste) public void Update(Artiste artiste)
{ {
throw new NotSupportedException("Mode Local"); Artiste existingArtiste = this.Find(artiste.IdArtiste);
if (existingArtiste == null)
{
this.logger.LogWarning("L'artiste {Id} n'a pas été trouvé pour l'update.", artiste.IdArtiste);
return;
}
existingArtiste.Nom = artiste.Nom;
existingArtiste.Biographie = artiste.Biographie;
existingArtiste.Titres = artiste.Titres;
} }
/// <inheritdoc/> /// <inheritdoc/>
@@ -93,5 +94,17 @@ namespace Webzine.Repository
.Where(a => a.Nom.ToLower().Contains(mot.ToLower())) .Where(a => a.Nom.ToLower().Contains(mot.ToLower()))
.ToList(); .ToList();
} }
/// <inheritdoc/>
public int Count()
{
return this.dataStore.Artistes.Count;
}
/// <inheritdoc/>
public int Count(Func<Artiste, bool> predicate)
{
return this.dataStore.Artistes.Count(predicate);
}
} }
} }

View File

@@ -4,7 +4,6 @@
namespace Webzine.Repository namespace Webzine.Repository
{ {
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@@ -15,7 +14,6 @@ namespace Webzine.Repository
/// <summary> /// <summary>
/// Initialise une classe <see cref="LocalCommentaireRepository"/> qui implémente l'interface <see cref="ICommentaireRepository"/> pour gérer les opérations liées aux commentaires. /// Initialise une classe <see cref="LocalCommentaireRepository"/> qui implémente l'interface <see cref="ICommentaireRepository"/> pour gérer les opérations liées aux commentaires.
/// Utilise <see cref="ICommentaireRepository"/> en injection de dépendances.
/// </summary> /// </summary>
public class LocalCommentaireRepository : ICommentaireRepository public class LocalCommentaireRepository : ICommentaireRepository
{ {
@@ -23,9 +21,8 @@ namespace Webzine.Repository
private readonly InMemoryDataStore dataStore; private readonly InMemoryDataStore dataStore;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="LocalCommentaireRepository"/> class.
/// Initialise une nouvelle instance du <see cref="LocalCommentaireRepository"/> . /// Initialise une nouvelle instance du <see cref="LocalCommentaireRepository"/> .
/// Est liée à un magasin de données en mémoire et utilise un logger pour enregistrer les opérations. /// Gère les opérations liées aux commentaires en utilisant une source de données locale (en mémoire).
/// </summary> /// </summary>
/// <param name="dataStore">Le magasin de données en mémoire. Ne peut pas être null.</param> /// <param name="dataStore">Le magasin de données en mémoire. Ne peut pas être null.</param>
/// <param name="logger">Le logger à utiliser pour enregistrer les messages de journalisation. Ne peut pas être null.</param> /// <param name="logger">Le logger à utiliser pour enregistrer les messages de journalisation. Ne peut pas être null.</param>
@@ -38,31 +35,19 @@ namespace Webzine.Repository
/// <inheritdoc/> /// <inheritdoc/>
public void Add(Commentaire commentaire) public void Add(Commentaire commentaire)
{ {
throw new NotSupportedException("Mode Local"); this.dataStore.Commentaires.Add(commentaire);
} }
/// <inheritdoc/> /// <inheritdoc/>
public void Delete(Commentaire commentaire) public void Delete(Commentaire commentaire)
{ {
throw new NotSupportedException("Mode Local"); this.dataStore.Commentaires.Remove(commentaire);
}
/// <inheritdoc/>
public int Count()
{
return this.dataStore.Commentaires.Count;
} }
/// <inheritdoc/> /// <inheritdoc/>
public Commentaire Find(int idCommentaire) public Commentaire Find(int idCommentaire)
{ {
var commentaire = this.dataStore.Commentaires.FirstOrDefault(c => c.IdCommentaire == idCommentaire); return this.dataStore.Commentaires.SingleOrDefault(c => c.IdCommentaire == idCommentaire);
if (commentaire == null)
{
return new Commentaire();
}
return commentaire;
} }
/// <inheritdoc/> /// <inheritdoc/>
@@ -76,25 +61,10 @@ namespace Webzine.Repository
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerable<Commentaire> FindCommentaires(int offset, int limit) public IEnumerable<Commentaire> FindCommentaires(int offset, int limit)
{ {
if (offset < 0 || limit <= 0)
{
return Enumerable.Empty<Commentaire>();
}
return this.dataStore.Commentaires return this.dataStore.Commentaires
.OrderByDescending(c => c.DateCreation) .OrderByDescending(c => c.DateCreation)
.Skip(offset) .Skip(offset)
.Take(limit) .Take(limit);
.ToList();
}
/// <inheritdoc/>
public IEnumerable<Commentaire> FindByIdTitre(int idTitre)
{
return this.dataStore.Commentaires
.Where(c => c.Titre != null && c.Titre.IdTitre == idTitre)
.OrderByDescending(c => c.DateCreation)
.ToList();
} }
} }
} }

View File

@@ -17,9 +17,10 @@ public class LocalStyleRepository : IStyleRepository
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="LocalStyleRepository"/> class. /// Initializes a new instance of the <see cref="LocalStyleRepository"/> class.
/// Gère les opérations liées aux styles en utilisant une source de données locale (en mémoire).
/// </summary> /// </summary>
/// <param name="logger">Le service de journalisation injecté pour suivre les opérations du repository.</param> /// <param name="logger">Le service de journalisation injecté pour suivre les opérations du repository.</param>
/// <param name="styles">La liste de styles à utiliser comme source de données pour le repository.</param> /// <param name="dataStore">Les données en mémoire.</param>
public LocalStyleRepository(ILogger<LocalStyleRepository> logger, InMemoryDataStore dataStore) public LocalStyleRepository(ILogger<LocalStyleRepository> logger, InMemoryDataStore dataStore)
{ {
this.logger = logger; this.logger = logger;
@@ -30,19 +31,19 @@ public class LocalStyleRepository : IStyleRepository
/// <inheritdoc/> /// <inheritdoc/>
public void Add(Style style) public void Add(Style style)
{ {
throw new NotSupportedException("Mode local"); this.dataStore.Styles.Add(style);
} }
/// <inheritdoc/> /// <inheritdoc/>
public void Delete(Style style) public void Delete(Style style)
{ {
throw new NotSupportedException("Mode local"); this.dataStore.Styles.Remove(style);
} }
/// <inheritdoc/> /// <inheritdoc/>
public Style Find(int id) public Style Find(int id)
{ {
return this.dataStore.Styles.Find(s => s.IdStyle == id); return this.dataStore.Styles.SingleOrDefault(s => s.IdStyle == id);
} }
/// <inheritdoc/> /// <inheritdoc/>
@@ -54,6 +55,20 @@ public class LocalStyleRepository : IStyleRepository
/// <inheritdoc/> /// <inheritdoc/>
public void Update(Style style) public void Update(Style style)
{ {
throw new NotSupportedException("Mode local"); Style existingStyle = this.Find(style.IdStyle);
if (existingStyle == null)
{
this.logger.LogWarning("Style with id {IdStyle} not found for update.", style.IdStyle);
return;
}
existingStyle.Libelle = style.Libelle;
existingStyle.Titres = style.Titres;
}
/// <inheritdoc/>
public int Count()
{
return this.dataStore.Styles.Count;
} }
} }

View File

@@ -28,20 +28,20 @@ public class LocalTitreRepository : ITitreRepository
/// <inheritdoc/> /// <inheritdoc/>
public void Add(Titre titre) public void Add(Titre titre)
{ {
throw new NotSupportedException("Mode local"); this.dataStore.Titres.Add(titre);
} }
/// <inheritdoc/> /// <inheritdoc/>
public int Count() public int Count()
{ {
var count = this.dataStore.Titres.Count(); // On appelle directement LINQ count pour ne pas confondre avec la méthode Count() de l'interface ITitreRepository
return count; return Enumerable.Count(this.dataStore.Titres);
} }
/// <inheritdoc/> /// <inheritdoc/>
public void Delete(Titre titre) public void Delete(Titre titre)
{ {
throw new NotSupportedException("Mode Local"); this.dataStore.Titres.Remove(titre);
} }
/// <inheritdoc/> /// <inheritdoc/>
@@ -56,27 +56,47 @@ public class LocalTitreRepository : ITitreRepository
/// <inheritdoc/> /// <inheritdoc/>
public void IncrementNbLectures(Titre titre) public void IncrementNbLectures(Titre titre)
{ {
titre.NbLectures++; var stored = this.dataStore.Titres.FirstOrDefault(t => t.IdTitre == titre.IdTitre);
if (stored == null)
{
this.logger.LogWarning("Titre avec l'ID {Id} non trouvé pour incrémenter le nombre de lectures.", titre.IdTitre);
return;
}
stored.NbLectures++;
} }
/// <inheritdoc/> /// <inheritdoc/>
public void IncrementNbLikes(Titre titre) public void IncrementNbLikes(Titre titre)
{ {
titre.NbLikes++; var stored = this.dataStore.Titres.FirstOrDefault(t => t.IdTitre == titre.IdTitre);
if (stored == null)
{
this.logger.LogWarning("Titre avec l'ID {Id} non trouvé pour incrémenter le nombre de likes.", titre.IdTitre);
return;
}
stored.NbLikes++;
} }
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerable<Titre> Search(string mot) public IEnumerable<Titre> Search(string mot)
{ {
if (string.IsNullOrWhiteSpace(mot))
{
return Enumerable.Empty<Titre>();
}
return this.dataStore.Titres return this.dataStore.Titres
.Where(t => t.Libelle != null && t.Libelle.Contains(mot)); .Where(t => t.Libelle.ToLower().Contains(mot.ToLower()))
.ToList();
} }
/// <inheritdoc/> /// <inheritdoc/>
public Titre Find(int idTitre) public Titre Find(int idTitre)
{ {
return this.dataStore.Titres return this.dataStore.Titres
.First(t => t.IdTitre == idTitre); .SingleOrDefault(t => t.IdTitre == idTitre);
} }
/// <inheritdoc/> /// <inheritdoc/>
@@ -95,6 +115,20 @@ public class LocalTitreRepository : ITitreRepository
/// <inheritdoc/> /// <inheritdoc/>
public void Update(Titre titre) public void Update(Titre titre)
{ {
throw new NotSupportedException("Mode local"); // On trouve le titre stocké pour mettre à jour ses propriétés avec la méthode Find du repository
// pour éviter la duplication de code.
Titre existingTitre = this.Find(titre.IdTitre);
if (existingTitre == null)
{
this.logger.LogWarning("Titre avec l'ID {Id} non trouvé pour mise à jour.", titre.IdTitre);
return;
}
existingTitre.Libelle = titre.Libelle;
existingTitre.DateCreation = titre.DateCreation;
existingTitre.NbLectures = titre.NbLectures;
existingTitre.NbLikes = titre.NbLikes;
existingTitre.IdArtiste = titre.IdArtiste;
existingTitre.Styles = titre.Styles;
} }
} }

View File

@@ -31,17 +31,14 @@ public class ArtisteController : Controller
} }
/// <summary> /// <summary>
/// Affiche la liste des artistes. Pour l'instant, les artistes sont générés à partir de noms prédéfinis via la méthode SeedArtisteByName de la classe ArtisteFactory. /// Affiche la liste des artistes.
/// Chaque artiste est ensuite ajouté à une liste d'artistes qui est passée à la vue.
/// </summary> /// </summary>
/// <returns>Redirection.</returns> /// <returns>Redirection.</returns>
public IActionResult Index() public IActionResult Index()
{ {
IEnumerable<Artiste> artistes = this.artisteRepository.FindAll(); IEnumerable<Artiste> artistes = this.artisteRepository.FindAll().OrderBy(t => t.Nom);
var artistes_ordre = artistes.OrderBy(t => t.Nom).ToList(); return this.View(artistes);
return this.View(artistes_ordre);
} }
/// <summary> /// <summary>
@@ -61,14 +58,6 @@ public class ArtisteController : Controller
[HttpPost] [HttpPost]
public IActionResult Create(ArtisteCreateViewModel model) public IActionResult Create(ArtisteCreateViewModel model)
{ {
// vérifier si les données sont corrects.
if (!this.ModelState.IsValid)
{
// Passer model en paramètre afin que
// l'utilisateur conserve sa saissie.
return this.View(model);
}
// Créer un objet Artiste avecc les paramètres. // Créer un objet Artiste avecc les paramètres.
var artiste = new Artiste var artiste = new Artiste
{ {
@@ -115,11 +104,6 @@ public class ArtisteController : Controller
Biographie = model.Biographie, Biographie = model.Biographie,
}; };
if (!this.ModelState.IsValid)
{
return this.View(artiste);
}
this.artisteRepository.Update(artiste); this.artisteRepository.Update(artiste);
return this.RedirectToAction("Index"); return this.RedirectToAction("Index");
@@ -163,7 +147,6 @@ public class ArtisteController : Controller
this.artisteRepository.Delete(artiste); this.artisteRepository.Delete(artiste);
} }
// 3. Redirect back to the list (or wherever you want them to go after)
return this.RedirectToAction("Index"); return this.RedirectToAction("Index");
} }
} }

View File

@@ -5,6 +5,9 @@ namespace Webzine.WebApplication.Areas.Administration.Controllers
using Webzine.Repository.Contracts; using Webzine.Repository.Contracts;
using Webzine.WebApplication.Areas.Administration.ViewModels.Commentaire; using Webzine.WebApplication.Areas.Administration.ViewModels.Commentaire;
/// <summary>
/// Contrôleur pour la gestion des commentaires dans l'administration du webzine. Ce contrôleur permet d'afficher la liste des commentaires, de supprimer un commentaire spécifique et de gérer les interactions liées aux commentaires dans l'interface d'administration.
/// </summary>
[Area("Administration")] [Area("Administration")]
public class CommentaireController : Controller public class CommentaireController : Controller
{ {

View File

@@ -2,29 +2,28 @@ namespace Webzine.WebApplication.Areas.Administration.Controllers;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Webzine.Repository.Contracts; using Webzine.Business.Contracts;
using Webzine.WebApplication.Areas.Administration.ViewModels; using Webzine.Business.Contracts.Dto;
/// <summary>
/// Contrôleur pour gérer le tableau de bord de l'administration.
/// </summary>
[Area("Administration")] [Area("Administration")]
public class DashboardController : Controller public class DashboardController : Controller
{ {
private readonly ILogger<DashboardController> logger; private readonly ILogger<DashboardController> logger;
private readonly IStyleRepository styleRepository; private readonly IDashboardService dashboardService;
private readonly IArtisteRepository artisteRepository;
private readonly ITitreRepository titreRepository;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DashboardController"/> class. /// Initializes a new instance of the <see cref="DashboardController"/> class.
/// Initialise une nouvelle instance de la classe <see cref="DashboardController"/>. /// Initialise une nouvelle instance de la classe <see cref="DashboardController"/>.
/// </summary> /// </summary>
/// <param name="logger">Service de journalisation injecté.</param> /// <param name="logger">Service de journalisation injecté.</param>
/// <param name="styleRepository">Repository des styles injecté.</param> /// <param name="dashboardService">Service de calcul des statistiques du tableau de bord.</param>
public DashboardController(ILogger<DashboardController> logger, IStyleRepository styleRepository, IArtisteRepository artisteRepository, ITitreRepository titreRepository) public DashboardController(ILogger<DashboardController> logger, IDashboardService dashboardService)
{ {
this.logger = logger; this.logger = logger;
this.styleRepository = styleRepository; this.dashboardService = dashboardService;
this.artisteRepository = artisteRepository;
this.titreRepository = titreRepository;
this.logger.LogInformation("Initialisation du contrôleur TitreController."); this.logger.LogInformation("Initialisation du contrôleur TitreController.");
} }
@@ -35,42 +34,8 @@ public class DashboardController : Controller
/// <returns>La vue Index du tableau de bord.</returns> /// <returns>La vue Index du tableau de bord.</returns>
public IActionResult Index() public IActionResult Index()
{ {
var artisteLePlusChronique = this.titreRepository.FindAll() DashboardDTO data = this.dashboardService.GetDashboardData();
.GroupBy(t => t.Artiste)
.OrderByDescending(g => g.Count())
.First();
var albumLePlusChronique = this.titreRepository.FindAll() return this.View(data);
.GroupBy(t => t.Artiste)
.OrderByDescending(g => g.Select(t => t.Album).Distinct().Count())
.First();
var musiqueLaPlusJouee = this.titreRepository.FindAll()
.OrderByDescending(t => t.NbLectures)
.First();
var model = new DashboardViewModel
{
NombreArtistes = this.artisteRepository.FindAll().Count(),
ArtisteLePlusChronique = artisteLePlusChronique.Key.Nom,
AlbumLePlusChronique = albumLePlusChronique.Key.Nom,
NombreBiographies = this.artisteRepository.FindAll().Count(a => !string.IsNullOrEmpty(a.Biographie)),
IdMusiqueLaPlusJouee = musiqueLaPlusJouee.IdTitre,
MusiqueLaPlusJouee = musiqueLaPlusJouee.Libelle,
NombreTitres = this.titreRepository.Count(),
NombreGenres = this.styleRepository.FindAll().Count(),
NombreLectures = this.titreRepository.FindAll().Sum(t => t.NbLectures),
NombreLikes = this.titreRepository.FindAll().Sum(t => t.NbLikes),
};
return this.View(model);
} }
} }

View File

@@ -63,11 +63,6 @@ public class StyleController : Controller
[HttpPost] [HttpPost]
public IActionResult Create(StyleCreateViewModel model) public IActionResult Create(StyleCreateViewModel model)
{ {
if (!this.ModelState.IsValid)
{
return this.View(model);
}
var style = new Style var style = new Style
{ {
Libelle = model.Libelle, Libelle = model.Libelle,
@@ -124,7 +119,6 @@ public class StyleController : Controller
/// </summary> /// </summary>
/// <param name="id">L'identifiant du style a editer.</param> /// <param name="id">L'identifiant du style a editer.</param>
/// <returns>La vue d'edition ou une redirection vers l'index si le style n'existe pas.</returns> /// <returns>La vue d'edition ou une redirection vers l'index si le style n'existe pas.</returns>
[HttpGet]
public IActionResult Edit(int id) public IActionResult Edit(int id)
{ {
var style = this.styleRepository.Find(id); var style = this.styleRepository.Find(id);
@@ -151,11 +145,6 @@ public class StyleController : Controller
[HttpPost] [HttpPost]
public IActionResult Edit(StyleEditViewModel model) public IActionResult Edit(StyleEditViewModel model)
{ {
if (!this.ModelState.IsValid)
{
return this.View(model);
}
var style = this.styleRepository.Find(model.IdStyle); var style = this.styleRepository.Find(model.IdStyle);
if (style == null) if (style == null)
{ {

View File

@@ -1,8 +1,11 @@
namespace Webzine.WebApplication.Areas.Administration.Controllers; namespace Webzine.WebApplication.Areas.Administration.Controllers;
using Business.Contracts;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Rendering;
using Webzine.Business.Contracts.Dto;
using Webzine.Entity; using Webzine.Entity;
using Webzine.Repository.Contracts; using Webzine.Repository.Contracts;
using Webzine.WebApplication.Areas.Administration.ViewModels.Titre; using Webzine.WebApplication.Areas.Administration.ViewModels.Titre;
@@ -17,6 +20,7 @@ public class TitreController : Controller
private readonly ITitreRepository titreRepository; private readonly ITitreRepository titreRepository;
private readonly IArtisteRepository artisteRepository; private readonly IArtisteRepository artisteRepository;
private readonly IStyleRepository styleRepository; private readonly IStyleRepository styleRepository;
private readonly ITitreAdminService titreAdminService;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="TitreController"/> class. /// Initializes a new instance of the <see cref="TitreController"/> class.
@@ -26,12 +30,14 @@ public class TitreController : Controller
/// <param name="titreRepository">Repository des titres injecté pour accéder aux données des titres.</param> /// <param name="titreRepository">Repository des titres injecté pour accéder aux données des titres.</param>
/// <param name="artisteRepository">Repository des artistes injecté pour accéder aux données des artistes, nécessaires pour les associations avec les titres.</param> /// <param name="artisteRepository">Repository des artistes injecté pour accéder aux données des artistes, nécessaires pour les associations avec les titres.</param>
/// <param name="styleRepository">Repository des styles injecté pour accéder aux données des styles, nécessaires pour les associations avec les titres.</param> /// <param name="styleRepository">Repository des styles injecté pour accéder aux données des styles, nécessaires pour les associations avec les titres.</param>
public TitreController(ILogger<TitreController> logger, ITitreRepository titreRepository, IArtisteRepository artisteRepository, IStyleRepository styleRepository) /// <param name="titreAdminService">Service Titre Administration injecté gérant Edit et Crée.</param>
public TitreController(ILogger<TitreController> logger, ITitreRepository titreRepository, IArtisteRepository artisteRepository, IStyleRepository styleRepository, ITitreAdminService titreAdminService)
{ {
this.logger = logger; this.logger = logger;
this.titreRepository = titreRepository; this.titreRepository = titreRepository;
this.artisteRepository = artisteRepository; this.artisteRepository = artisteRepository;
this.styleRepository = styleRepository; this.styleRepository = styleRepository;
this.titreAdminService = titreAdminService;
} }
/// <summary> /// <summary>
@@ -81,6 +87,18 @@ public class TitreController : Controller
return this.View(model); return this.View(model);
} }
/// <summary>
/// Traite la soumission du formulaire de création d'un titre.
/// </summary>
/// <param name="model">Données saisies dans le formulaire.</param>
/// <returns>Redirection vers Index en cas de succès, réaffichage du formulaire sinon.</returns>
[HttpPost]
public IActionResult Create(TitreAdminDTO model)
{
this.titreAdminService.CreerTitre(model);
return this.RedirectToAction("Index");
}
/// <summary> /// <summary>
/// Affiche le formulaire de modification d'un titre existant dans la vue Edit, en préremplissant les champs avec les données du titre sélectionné. Les listes déroulantes pour les artistes et les styles sont également remplies pour permettre à l'utilisateur de modifier ces associations. /// Affiche le formulaire de modification d'un titre existant dans la vue Edit, en préremplissant les champs avec les données du titre sélectionné. Les listes déroulantes pour les artistes et les styles sont également remplies pour permettre à l'utilisateur de modifier ces associations.
/// </summary> /// </summary>
@@ -121,6 +139,18 @@ public class TitreController : Controller
return this.View(model); return this.View(model);
} }
/// <summary>
/// Traite la soumission du formulaire de modification d'un titre.
/// </summary>
/// <param name="model">Données saisies dans le formulaire.</param>
/// <returns>Redirection vers Index en cas de succès, réaffichage du formulaire sinon.</returns>
[HttpPost]
public IActionResult Edit(TitreAdminDTO model)
{
this.titreAdminService.ModifierTitre(model);
return this.RedirectToAction("Index");
}
/// <summary> /// <summary>
/// Affiche la vue de confirmation de suppression d'un titre, en récupérant les détails du titre à supprimer à partir de l'identifiant fourni. Le ViewModel contient les informations essentielles du titre, telles que le libellé et le nom de l'artiste, pour permettre à l'utilisateur de confirmer la suppression. /// Affiche la vue de confirmation de suppression d'un titre, en récupérant les détails du titre à supprimer à partir de l'identifiant fourni. Le ViewModel contient les informations essentielles du titre, telles que le libellé et le nom de l'artiste, pour permettre à l'utilisateur de confirmer la suppression.
/// </summary> /// </summary>
@@ -154,11 +184,18 @@ public class TitreController : Controller
public IActionResult Delete(AdminTitreDelete model) public IActionResult Delete(AdminTitreDelete model)
{ {
var titre = this.titreRepository.Find(model.Id); var titre = this.titreRepository.Find(model.Id);
if (!this.ModelState.IsValid)
{
return this.View(model);
}
if (titre != null) if (titre != null)
{ {
this.titreRepository.Delete(titre); this.titreRepository.Delete(titre);
return this.RedirectToAction("Index");
} }
return this.RedirectToAction("Index"); return this.View(model);
} }
} }

View File

@@ -25,7 +25,7 @@
{ {
<tr class="align-middle"> <tr class="align-middle">
<td> <td>
<a asp-action="Details" asp-controller="Titre" asp-route-id="@commentaire.Titre.IdTitre"> <a asp-controller="Titre" asp-action="Index" asp-route-id="@commentaire.Titre.IdTitre">
@commentaire.Titre.Libelle @commentaire.Titre.Libelle
</a> </a>
</td> </td>

View File

@@ -1,4 +1,5 @@
@model Webzine.WebApplication.Areas.Administration.ViewModels.DashboardViewModel @using Webzine.Business.Contracts.Dto
@model DashboardDTO
<h1 class="mb-4">Tableau de bord</h1> <h1 class="mb-4">Tableau de bord</h1>
@@ -94,7 +95,7 @@
<div class="col-md-4"> <div class="col-md-4">
<a asp-area="" <a asp-area=""
asp-controller="Titre" asp-controller="Titre"
asp-action="Details" asp-action="Index"
asp-route-id="@Model.IdMusiqueLaPlusJouee"> asp-route-id="@Model.IdMusiqueLaPlusJouee">
<div class="ratio ratio-4x3"> <div class="ratio ratio-4x3">

View File

@@ -0,0 +1,13 @@
namespace Webzine.WebApplication.Configuration;
/// <summary>
/// Options de seuil pour la détection des opérations EF Core lentes.
/// </summary>
public class EfPerformanceOptions
{
/// <summary>
/// Obtient ou définit le seuil en millisecondes au-delà duquel une commande SQL est journalisée.
/// Valeur par défaut : 200 ms.
/// </summary>
public int SeuilMs { get; set; } = 200;
}

View File

@@ -21,6 +21,7 @@
/// </summary> /// </summary>
/// <param name="logger">Service de journalisation injecté pour enregistrer les événements et les erreurs.</param> /// <param name="logger">Service de journalisation injecté pour enregistrer les événements et les erreurs.</param>
/// <param name="configuration">Service d'injection de configuration pour accéder aux paramètres de l'application.</param> /// <param name="configuration">Service d'injection de configuration pour accéder aux paramètres de l'application.</param>
/// <param name="titreRepository"></param>
public AccueilController( public AccueilController(
ILogger<AccueilController> logger, ILogger<AccueilController> logger,
IConfiguration configuration, IConfiguration configuration,

View File

@@ -2,12 +2,14 @@ namespace Webzine.WebApplication.Controllers;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
/// <summary>
/// Controller de version de l'API.
/// </summary>
public class ApiController : ControllerBase public class ApiController : ControllerBase
{ {
private readonly ILogger<ApiController> logger; private readonly ILogger<ApiController> logger;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ApiController"/> class.
/// Initialise une nouvelle instance de la classe <see cref="ApiController"/>. /// Initialise une nouvelle instance de la classe <see cref="ApiController"/>.
/// </summary> /// </summary>
/// <param name="logger">Service de journalisation injecté pour enregistrer les événements et les erreurs.</param> /// <param name="logger">Service de journalisation injecté pour enregistrer les événements et les erreurs.</param>
@@ -21,7 +23,6 @@ public class ApiController : ControllerBase
/// Endpoint de test pour vérifier que l'API fonctionne correctement. Retourne un objet JSON contenant le nom et la version de l'application. /// Endpoint de test pour vérifier que l'API fonctionne correctement. Retourne un objet JSON contenant le nom et la version de l'application.
/// </summary> /// </summary>
/// <returns>Un objet JSON avec les propriétés "nom" et "version".</returns> /// <returns>Un objet JSON avec les propriétés "nom" et "version".</returns>
[HttpGet]
public IActionResult Version() public IActionResult Version()
{ {
this.logger.LogInformation("Get Version was called"); this.logger.LogInformation("Get Version was called");

View File

@@ -5,6 +5,9 @@
using Webzine.Repository.Contracts; using Webzine.Repository.Contracts;
using Webzine.WebApplication.ViewModels.Artiste; using Webzine.WebApplication.ViewModels.Artiste;
/// <summary>
/// Contrôleur pour la gestion des artistes dans l'administration du webzine. Ce contrôleur gère les opérations de création, modification, suppression et affichage des artistes dans l'interface d'administration du webzine. Chaque action du contrôleur prépare un ViewModel spécifique pour la vue correspondante, permettant ainsi une séparation claire entre la logique métier et la présentation des données.
/// </summary>
public class ArtisteController : Controller public class ArtisteController : Controller
{ {
// Injection du logger via le constructeur // Injection du logger via le constructeur
@@ -12,10 +15,10 @@
private readonly IArtisteRepository artisteRepository; private readonly IArtisteRepository artisteRepository;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ArtisteController"/> class.
/// Initialise une nouvelle instance du <see cref="ArtisteController"/>. avec un service de journalisation injecté. /// Initialise une nouvelle instance du <see cref="ArtisteController"/>. avec un service de journalisation injecté.
/// </summary> /// </summary>
/// <param name="logger">Service de journalisation injecté pour enregistrer les événements et les erreurs.</param> /// <param name="logger">Service de journalisation injecté pour enregistrer les événements et les erreurs.</param>
/// <param name="artisteRepository">Repository pour accéder aux données des artistes, injecté pour permettre les opérations de création, modification, suppression et affichage des artistes.</param>
public ArtisteController( public ArtisteController(
ILogger<ArtisteController> logger, ILogger<ArtisteController> logger,
IArtisteRepository artisteRepository) IArtisteRepository artisteRepository)
@@ -26,11 +29,10 @@
} }
/// <summary> /// <summary>
/// Prend en paramètre le nom de l'artiste (ex: "fatal-bazooka"), utilise la factory pour trouver l'artiste correspondant, et affiche sa page dédiée. /// Affiche la liste des artistes.
/// </summary> /// </summary>
/// <param name="nom">Le nom de l'artiste à rechercher, formaté en kebab-case (ex: "fatal-bazooka").</param> /// <param name="nom">Le nom de l'artiste à rechercher, formaté en kebab-case (ex: "fatal-bazooka").</param>
/// <returns>La vue de l'artiste avec son ViewModel, ou une redirection vers l'accueil si le nom est vide, ou une erreur 404 si l'artiste n'est pas trouvé.</returns> /// <returns>La vue de l'artiste avec son ViewModel, ou une redirection vers l'accueil si le nom est vide, ou une erreur 404 si l'artiste n'est pas trouvé.</returns>
[HttpGet("/artiste/{nom}")]
public IActionResult Index(string nom) public IActionResult Index(string nom)
{ {
this.logger.LogInformation("Tentative d'accès à l'artiste avec le nom : {NomArtiste}", nom); this.logger.LogInformation("Tentative d'accès à l'artiste avec le nom : {NomArtiste}", nom);
@@ -41,14 +43,12 @@
return this.RedirectToAction("Index", "Accueil"); return this.RedirectToAction("Index", "Accueil");
} }
// On transforme "fatal-bazooka" en "Fatal Bazooka" pour la factory // On transforme "fatal-bazooka" en "Fatal Bazooka"
string nomPropre = System.Globalization.CultureInfo.CurrentCulture.TextInfo string nomPropre = System.Globalization.CultureInfo.CurrentCulture.TextInfo
.ToTitleCase(nom.Replace("-", " ")); .ToTitleCase(nom.Replace("-", " "));
// On appelle la factory pour obtenir l'artiste unique
var artiste = this.artisteRepository.FindByName(nomPropre); var artiste = this.artisteRepository.FindByName(nomPropre);
// Check if artiste was found
if (artiste == null) if (artiste == null)
{ {
this.logger.LogWarning("Artiste non trouvé avec le nom : {NomArtiste}", nomPropre); this.logger.LogWarning("Artiste non trouvé avec le nom : {NomArtiste}", nomPropre);

View File

@@ -9,12 +9,21 @@ namespace Webzine.WebApplication.Controllers
using Webzine.Repository.Contracts; using Webzine.Repository.Contracts;
using Webzine.WebApplication.ViewModels.Recherche; using Webzine.WebApplication.ViewModels.Recherche;
/// <summary>
/// Controller de la page de recherche d'artistes et de titres.
/// </summary>
public class RechercheController : Controller public class RechercheController : Controller
{ {
private readonly ILogger<RechercheController> logger; private readonly ILogger<RechercheController> logger;
private readonly ITitreRepository titreRepository; private readonly ITitreRepository titreRepository;
private readonly IArtisteRepository artisteRepository; private readonly IArtisteRepository artisteRepository;
/// <summary>
/// Constructeur du controller de la page de recherche d'artistes et de titres.
/// </summary>
/// <param name="logger">Le logger pour enregistrer les événements.</param>
/// <param name="titreRepository">Le repository pour gérer les opérations sur les titres.</param>
/// <param name="artisteRepository">Le repository pour gérer les opérations sur les artistes.</param>
public RechercheController( public RechercheController(
ILogger<RechercheController> logger, ILogger<RechercheController> logger,
ITitreRepository titreRepository, ITitreRepository titreRepository,
@@ -29,7 +38,7 @@ namespace Webzine.WebApplication.Controllers
/// Affichage de la page Recherche depuis le header de l'app. /// Affichage de la page Recherche depuis le header de l'app.
/// </summary> /// </summary>
/// <param name="mot">Nom d'artiste ou de titre.</param> /// <param name="mot">Nom d'artiste ou de titre.</param>
/// <returns>Page de recherche avec les résultats.</returns> /// <returns>Page de recherche avec les r<EFBFBD>sultats.</returns>
public IActionResult Index(string mot) public IActionResult Index(string mot)
{ {
// Logger la recherche. // Logger la recherche.
@@ -41,7 +50,7 @@ namespace Webzine.WebApplication.Controllers
// Recherche des artistes. // Recherche des artistes.
var artistes = this.artisteRepository.Search(mot); var artistes = this.artisteRepository.Search(mot);
// Paramètres a retourner à la vue. // Param<EFBFBD>tres a retourner <EFBFBD> la vue.
var vm = new RechercheIndexViewModel var vm = new RechercheIndexViewModel
{ {
Mot = mot, Mot = mot,

View File

@@ -15,7 +15,6 @@ namespace Webzine.WebApplication.Controllers
/// affichage des details, filtrage par style, /// affichage des details, filtrage par style,
/// ajout de likes, commentaires et recherche. /// ajout de likes, commentaires et recherche.
/// </summary> /// </summary>
[Route("titre")]
public class TitreController : Controller public class TitreController : Controller
{ {
private readonly ILogger<TitreController> logger; private readonly ILogger<TitreController> logger;
@@ -40,8 +39,7 @@ namespace Webzine.WebApplication.Controllers
/// </summary> /// </summary>
/// <param name="id">Identifiant du titre.</param> /// <param name="id">Identifiant du titre.</param>
/// <returns>Vue des details ou 404 si introuvable.</returns> /// <returns>Vue des details ou 404 si introuvable.</returns>
[HttpGet("{id}")] public IActionResult Index(int id)
public IActionResult Details(int id)
{ {
this.logger.LogInformation("Demande d'affichage du detail pour le titre ID {Id}.", id); this.logger.LogInformation("Demande d'affichage du detail pour le titre ID {Id}.", id);
@@ -53,6 +51,8 @@ namespace Webzine.WebApplication.Controllers
return this.RedirectToAction("Index"); return this.RedirectToAction("Index");
} }
this.titreRepository.IncrementNbLectures(titre);
var vm = new TitreDetail var vm = new TitreDetail
{ {
Details = new TitreContent Details = new TitreContent
@@ -83,7 +83,6 @@ namespace Webzine.WebApplication.Controllers
/// </summary> /// </summary>
/// <param name="style">Nom du style musical.</param> /// <param name="style">Nom du style musical.</param>
/// <returns>Vue contenant la liste filtree.</returns> /// <returns>Vue contenant la liste filtree.</returns>
[HttpGet("style/{style}")]
public IActionResult Style(string style) public IActionResult Style(string style)
{ {
this.logger.LogInformation("Recherche des titres pour le style : {Style}.", style); this.logger.LogInformation("Recherche des titres pour le style : {Style}.", style);
@@ -104,7 +103,7 @@ namespace Webzine.WebApplication.Controllers
/// </summary> /// </summary>
/// <param name="model">Modele contenant l'identifiant du titre.</param> /// <param name="model">Modele contenant l'identifiant du titre.</param>
/// <returns>Redirection vers la page detail.</returns> /// <returns>Redirection vers la page detail.</returns>
[HttpPost("like")] [HttpPost]
public IActionResult Like(TitreLike model) public IActionResult Like(TitreLike model)
{ {
this.logger.LogInformation("Ajout d'un like pour le titre ID {Id}.", model.IdTitre); this.logger.LogInformation("Ajout d'un like pour le titre ID {Id}.", model.IdTitre);
@@ -114,12 +113,13 @@ namespace Webzine.WebApplication.Controllers
if (titre == null) if (titre == null)
{ {
this.logger.LogWarning("Impossible d'ajouter un like. Titre ID {Id} introuvable.", model.IdTitre); this.logger.LogWarning("Impossible d'ajouter un like. Titre ID {Id} introuvable.", model.IdTitre);
return this.RedirectToAction("Index"); }
else
{
this.titreRepository.IncrementNbLikes(titre);
} }
titre.NbLikes++; return this.RedirectToAction("Index", new { id = model.IdTitre });
return this.RedirectToAction("Details", new { id = model.IdTitre });
} }
/// <summary> /// <summary>
@@ -127,15 +127,9 @@ namespace Webzine.WebApplication.Controllers
/// </summary> /// </summary>
/// <param name="model">Donnees du commentaire.</param> /// <param name="model">Donnees du commentaire.</param>
/// <returns>Redirection vers la page detail.</returns> /// <returns>Redirection vers la page detail.</returns>
[HttpPost("comment")] [HttpPost]
public IActionResult Comment(TitreComment model) public IActionResult Comment(TitreComment model)
{ {
if (!this.ModelState.IsValid)
{
this.logger.LogWarning("Echec de validation du modele de commentaire pour le titre ID {Id}.", model.IdTitre);
return this.RedirectToAction("Details", new { id = model.IdTitre });
}
var titre = this.titreRepository.Find(model.IdTitre); var titre = this.titreRepository.Find(model.IdTitre);
if (titre == null) if (titre == null)
@@ -156,7 +150,7 @@ namespace Webzine.WebApplication.Controllers
this.logger.LogInformation("Commentaire ajoute avec succes au titre ID {Id}.", model.IdTitre); this.logger.LogInformation("Commentaire ajoute avec succes au titre ID {Id}.", model.IdTitre);
return this.RedirectToAction("Details", new { id = model.IdTitre }); return this.RedirectToAction("Index", new { id = model.IdTitre });
} }
private static TitreStyleItem MapTitreItem(Titre titre) private static TitreStyleItem MapTitreItem(Titre titre)

View File

@@ -7,6 +7,37 @@ public static class RouteConfiguration
/// </summary> /// </summary>
public static void MapCustomRoutes(this IEndpointRouteBuilder endpoints) public static void MapCustomRoutes(this IEndpointRouteBuilder endpoints)
{ {
// ----------- TITRE -----------
endpoints.MapControllerRoute(
name: "TitreStyle",
pattern: "titres/style/{style}",
defaults: new { controller = "Titre", action = "Style" });
endpoints.MapControllerRoute(
name: "TitreIndex",
pattern: "titre/{id}",
defaults: new { controller = "Titre", action = "Index" });
endpoints.MapControllerRoute(
name: "ArtisteIndex",
pattern: "artiste/{nom}",
defaults: new { controller = "Artiste", action = "Index" });
// ----------- ADMIN -----------
var adminRoutes = new Dictionary<string, string>
{
{ "artistes", "Artiste" }, { "commentaires", "Commentaire" }, { "styles", "Style" }, { "titres", "Titre" },
};
foreach (var route in adminRoutes)
{
endpoints.MapControllerRoute(
name: $"Admin{route.Value}Index",
pattern: $"administration/{route.Key}",
defaults: new { area = "Administration", controller = route.Value, action = "Index" });
}
// --- AUTRE PROUTES ---
endpoints.MapControllerRoute( endpoints.MapControllerRoute(
name: "areas", name: "areas",
pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}"); pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");

View File

@@ -0,0 +1,42 @@
namespace Webzine.WebApplication.Filters;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
/// <summary>
/// Filtre d'exception global qui intercepte toute exception non gérée et la journalise automatiquement.
/// </summary>
public class GlobalExceptionFilter : IExceptionFilter
{
private readonly ILogger<GlobalExceptionFilter> logger;
/// <summary>
/// Initializes a new instance of the <see cref="GlobalExceptionFilter"/> class.
/// </summary>
/// <param name="logger">Service de journalisation injecté.</param>
public GlobalExceptionFilter(ILogger<GlobalExceptionFilter> logger)
{
this.logger = logger;
}
/// <inheritdoc/>
public void OnException(ExceptionContext context)
{
this.logger.LogError(
context.Exception,
"Erreur non gérée dans {Action} : {Message}",
context.ActionDescriptor.DisplayName,
context.Exception.Message);
context.Result = new ObjectResult(new
{
erreur = "Une erreur inattendue est survenue.",
detail = context.Exception.Message,
})
{
StatusCode = StatusCodes.Status500InternalServerError,
};
context.ExceptionHandled = true;
}
}

View File

@@ -0,0 +1,73 @@
namespace Webzine.WebApplication.Filters;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
/// <summary>
/// Filtre d'action qui valide automatiquement le ModelState avant l'exécution du contrôleur.
/// Mesure également le temps d'exécution de chaque action (niveau Trace).
/// </summary>
public class ValidationActionFilter : IActionFilter
{
private readonly ILogger<ValidationActionFilter> logger;
/// <summary>
/// Initializes a new instance of the <see cref="ValidationActionFilter"/> class.
/// </summary>
/// <param name="logger">Service de journalisation injecté.</param>
public ValidationActionFilter(ILogger<ValidationActionFilter> logger)
{
this.logger = logger;
}
/// <inheritdoc/>
public void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
var erreurs = context.ModelState
.Where(e => e.Value?.Errors.Count > 0)
.Select(e => $"{e.Key}: {string.Join(", ", e.Value!.Errors.Select(err => err.ErrorMessage))}")
.ToList();
this.logger.LogWarning(
"Validation échouée pour {Action} : {Erreurs}",
context.ActionDescriptor.DisplayName,
string.Join(" | ", erreurs));
string actionName = context.RouteData.Values["action"]?.ToString() ?? string.Empty;
// cas spécial: titre details
if (actionName.Equals("Index", StringComparison.OrdinalIgnoreCase))
{
context.Result = new RedirectResult("/");
return;
}
// Récupère le modèle soumis (premier argument de l'action, s'il existe)
object? model = context.ActionArguments.Values.FirstOrDefault();
if (context.Controller is Controller controller)
{
context.Result = new ViewResult
{
ViewName = actionName,
ViewData = new Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary(
controller.ViewData)
{
Model = model,
},
};
}
else
{
context.Result = new BadRequestObjectResult(context.ModelState);
}
}
}
/// <inheritdoc/>
public void OnActionExecuted(ActionExecutedContext context)
{
}
}

View File

@@ -0,0 +1,146 @@
namespace Webzine.WebApplication.Interceptors;
using System.Data.Common;
using System.Diagnostics;
using Configuration;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Options;
/// <summary>
/// Intercepteur EF Core qui journalise uniquement les commandes SQL dépassant le seuil configuré.
/// Remonte la pile d'appels pour identifier la méthode repository (<c>Webzine.Repository.*</c>) à l'origine de la requête.
/// </summary>
/// <remarks>
/// <para>
/// <b>Références :</b>
/// <list type="bullet">
/// <item>
/// EF Core interceptors (doc officielle) :
/// <see href="https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors"/>
/// </item>
/// <item>
/// <see cref="DbCommandInterceptor"/> API :
/// <see href="https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.diagnostics.dbcommandinterceptor"/>
/// </item>
/// <item>
/// Exemple de slow-query interceptor (SO) :
/// <see href="https://medium.com/@sudipdevdev/how-to-detect-and-log-slow-queries-in-entity-framework-core-e2ab71024849"/>
/// </item>
/// <item>
/// <see cref="System.Diagnostics.StackTrace"/> pour remonter l'appelant :
/// <see href="https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.stacktrace"/>
/// </item>
/// <item>
/// Enregistrement via <c>AddInterceptors</c> :
/// <see href="https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors#registering-interceptors"/>
/// </item>
/// </list>
/// </para>
/// </remarks>
public class EfSlowQueryInterceptor : DbCommandInterceptor
{
private readonly ILogger<EfSlowQueryInterceptor> logger;
private readonly int seuilMs;
/// <summary>
/// Initializes a new instance of the <see cref="EfSlowQueryInterceptor"/> class.
/// </summary>
/// <param name="logger">Le service de journalisation injecté pour suivre les opérations de l'intercepteur.</param>
/// <param name="options">Les options de performance EF injectées pour récupérer le seuil de lenteur configuré.</param>
public EfSlowQueryInterceptor(ILogger<EfSlowQueryInterceptor> logger, IOptions<EfPerformanceOptions> options)
{
this.logger = logger;
this.seuilMs = options.Value.SeuilMs;
this.logger.LogDebug("[EfSlowQueryInterceptor] Constructeur appelé — seuil : {SeuilMs} ms.", this.seuilMs);
}
/// <inheritdoc/>
public override DbDataReader ReaderExecuted(DbCommand command, CommandExecutedEventData eventData, DbDataReader result)
{
this.JournaliserSiLent(eventData.Duration);
return result;
}
/// <inheritdoc/>
public override ValueTask<DbDataReader> ReaderExecutedAsync(DbCommand command, CommandExecutedEventData eventData, DbDataReader result, CancellationToken cancellationToken = default)
{
this.JournaliserSiLent(eventData.Duration);
return ValueTask.FromResult(result);
}
/// <inheritdoc/>
public override int NonQueryExecuted(DbCommand command, CommandExecutedEventData eventData, int result)
{
this.JournaliserSiLent(eventData.Duration);
return result;
}
/// <inheritdoc/>
public override ValueTask<int> NonQueryExecutedAsync(DbCommand command, CommandExecutedEventData eventData, int result, CancellationToken cancellationToken = default)
{
this.JournaliserSiLent(eventData.Duration);
return ValueTask.FromResult(result);
}
/// <inheritdoc/>
public override object? ScalarExecuted(DbCommand command, CommandExecutedEventData eventData, object? result)
{
this.JournaliserSiLent(eventData.Duration);
return result;
}
/// <inheritdoc/>
public override ValueTask<object?> ScalarExecutedAsync(DbCommand command, CommandExecutedEventData eventData, object? result, CancellationToken cancellationToken = default)
{
this.JournaliserSiLent(eventData.Duration);
return ValueTask.FromResult(result);
}
/// <summary>
/// Remonte la pile d'appels pour trouver la première méthode dans <c>Webzine.Repository</c>.
/// Toutes les requêtes EF Core du projet transitent par ce namespace, ce qui garantit
/// un résultat pertinent sans parcourir l'intégralité de la stack.
/// </summary>
/// <returns>Chaîne <c>Classe.Méthode</c> ou <c>"inconnu"</c> si rien trouvé.</returns>
/// <remarks>
/// <see cref="StackTrace"/> est instancié uniquement quand le seuil est dépassé,
/// ce qui évite tout impact sur le chemin nominal.
/// Ref : <see href="https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.stacktrace"/>.
/// </remarks>
private static string TrouverAppelantRepository()
{
// skipFrames: 1 pour sauter TrouverAppelantRepository elle-même
// fNeedFileInfo: false — on ne veut pas les numéros de ligne (coût supplémentaire inutile)
var frames = new StackTrace(skipFrames: 1, fNeedFileInfo: false).GetFrames();
foreach (var frame in frames)
{
var methode = frame.GetMethod();
if (methode?.DeclaringType?.Namespace?.StartsWith("Webzine.Repository", StringComparison.Ordinal) == true)
{
return $"{methode.DeclaringType.Name}.{methode.Name}";
}
}
return "inconnu";
}
private void JournaliserSiLent(TimeSpan duree)
{
if (duree.TotalMilliseconds > this.seuilMs)
{
var appelant = TrouverAppelantRepository();
this.logger.LogWarning(
"[EfSlowQueryInterceptor] Opération EF Core lente détectée — durée réelle : {DureeMs} ms — seuil : {SeuilMs} ms — dépassement : +{Depassement} ms.{NewLine}Appelant : {Appelant}",
duree.TotalMilliseconds.ToString("F2"),
this.seuilMs,
(duree.TotalMilliseconds - this.seuilMs).ToString("F2"),
Environment.NewLine,
appelant);
}
}
}

View File

@@ -0,0 +1,63 @@
namespace Webzine.WebApplication.Middlewares
{
using System.Diagnostics;
public class LogTempsExecutionMiddleware
{
/// <summary>
/// log à chaque requete http.
/// </summary>
// _next représente le maillon suivant dans la chaîne (le prochain middleware ou le contrôleur)
private readonly RequestDelegate next;
private readonly ILogger<LogTempsExecutionMiddleware> logger;
// Le constructeur récupère "_next" et le Logger
public LogTempsExecutionMiddleware(RequestDelegate next, ILogger<LogTempsExecutionMiddleware> logger)
{
this.next = next;
this.logger = logger;
}
// méthode appelée à chaque requête HTTP
/// <summary>
/// Middleware chargé de journaliser le cycle de vie d'une requête HTTP (entrée, exécution, sortie et temps de réponse).
/// </summary>
/// <param name="context">Le contexte HTTP encapsulant toutes les informations de la requête et de la réponse.</param>
/// <returns>Une tâche (<see cref="Task"/>) représentant l'opération asynchrone.</returns>
public async Task InvokeAsync(HttpContext context)
{
// (Avant le contrôleur)
var chronometre = Stopwatch.StartNew(); // lance le chrono
// --- IN ---
var methode = context.Request.Method;
var endpoint = context.Request.Path;
var traceId = context.TraceIdentifier; // Identifiant unique généré par .NET
this.logger.LogInformation("[IN] TraceId: {traceId} | Méthode: {methode} | Endpoint: {endpoint}", traceId, methode, endpoint);
await this.next(context);
// (Après le contrôleur)
chronometre.Stop(); // arrête le chrono
var tempsEcoule = chronometre.ElapsedMilliseconds;
var httpCode = context.Response.StatusCode; // exemple: 200, 404, 500
// --- OUT ---
if (httpCode >= 500)
{
this.logger.LogError("[OUT] TraceId: {traceId} | HTTP {httpCode} | Temps: {tempsEcoule} ms | Endpoint: {endpoint}", traceId, httpCode, tempsEcoule, endpoint);
}
else if (httpCode >= 400)
{
this.logger.LogWarning("[OUT] TraceId: {traceId} | HTTP {httpCode} | Temps: {tempsEcoule} ms | Endpoint: {endpoint}", traceId, httpCode, tempsEcoule, endpoint);
}
else
{
this.logger.LogInformation("[OUT] TraceId: {traceId} | HTTP {httpCode} | Temps: {tempsEcoule} ms | Endpoint: {endpoint}", traceId, httpCode, tempsEcoule, endpoint);
}
}
}
}

View File

@@ -6,6 +6,8 @@ using Microsoft.EntityFrameworkCore;
using NLog; using NLog;
using NLog.Web; using NLog.Web;
using Webzine.Business;
using Webzine.Business.Contracts;
using Webzine.Business.Seeders; using Webzine.Business.Seeders;
using Webzine.EntitiesContext; using Webzine.EntitiesContext;
using Webzine.Entity; using Webzine.Entity;
@@ -14,6 +16,8 @@ using Webzine.Repository;
using Webzine.Repository.Contracts; using Webzine.Repository.Contracts;
using Webzine.WebApplication.Configuration; using Webzine.WebApplication.Configuration;
using Webzine.WebApplication.Extensions; using Webzine.WebApplication.Extensions;
using Webzine.WebApplication.Filters;
using Webzine.WebApplication.Interceptors;
// Initiation du logger NLog pour la classe courante afin de pouvoir l'utiliser pour logger des messages d'information, d'erreur, etc avant la construction de l'application. // Initiation du logger NLog pour la classe courante afin de pouvoir l'utiliser pour logger des messages d'information, d'erreur, etc avant la construction de l'application.
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger(); var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
@@ -23,6 +27,14 @@ try
{ {
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
// Ajoute les services necessaires pour permettre l'utilisation des
// controllers avec des vues.
builder.Services.AddControllersWithViews(options =>
{
options.Filters.Add<ValidationActionFilter>();
options.Filters.Add<GlobalExceptionFilter>();
})
// Ajoute les services necessaires pour permettre l'utilisation des controllers avec des vues. // Ajoute les services necessaires pour permettre l'utilisation des controllers avec des vues.
builder.Services.AddControllersWithViews() builder.Services.AddControllersWithViews()
@@ -37,6 +49,13 @@ try
builder.Services.Configure<SpotifySeederOptions>(builder.Configuration.GetSection("SpotifySeeder")); builder.Services.Configure<SpotifySeederOptions>(builder.Configuration.GetSection("SpotifySeeder"));
builder.Services.AddHttpClient<SeedDataSpotify>(); builder.Services.AddHttpClient<SeedDataSpotify>();
builder.Services.Configure<EfPerformanceOptions>(options =>
{
options.SeuilMs = builder.Configuration.GetValue<int>("EfPerformance:SeuilMs");
});
builder.Services.AddSingleton<EfSlowQueryInterceptor>();
// En fonction de la configuration, utilise soit les repositories basés sur une base de données, soit les repositories basés sur des listes locales.
// En fonction de la configuration, utilise soit les repositories bases sur une base de donnees, soit les repositories bases sur des listes locales. // En fonction de la configuration, utilise soit les repositories bases sur une base de donnees, soit les repositories bases sur des listes locales.
var repositoryType = builder.Configuration.GetValue<RepositoryType>("Repository"); var repositoryType = builder.Configuration.GetValue<RepositoryType>("Repository");
var seederType = builder.Configuration.GetValue<SeederType>("Seeder"); var seederType = builder.Configuration.GetValue<SeederType>("Seeder");
@@ -46,13 +65,21 @@ try
{ {
if (builder.Environment.IsProduction()) if (builder.Environment.IsProduction())
{ {
builder.Services.AddDbContext<WebzineDbContext>(options => builder.Services.AddDbContext<WebzineDbContext>((serviceProvider, options) =>
options.UseNpgsql(builder.Configuration.GetConnectionString("PostGreSQLConnection"))); {
options
.UseNpgsql(builder.Configuration.GetConnectionString("PostGreSQLConnection"))
.AddInterceptors(serviceProvider.GetRequiredService<EfSlowQueryInterceptor>());
});
} }
else else
{ {
builder.Services.AddDbContext<WebzineDbContext>(options => builder.Services.AddDbContext<WebzineDbContext>((serviceProvider, options) =>
options.UseSqlite(builder.Configuration.GetConnectionString("SqliteConnection"))); {
options
.UseSqlite(builder.Configuration.GetConnectionString("SqliteConnection"))
.AddInterceptors(serviceProvider.GetRequiredService<EfSlowQueryInterceptor>());
});
} }
builder.Services.AddScoped<DbEntityRepository>(); builder.Services.AddScoped<DbEntityRepository>();
@@ -70,17 +97,27 @@ try
builder.Services.AddSingleton<InMemoryDataStore>(); builder.Services.AddSingleton<InMemoryDataStore>();
} }
builder.Services.AddScoped<IDashboardService, DashboardService>();
builder.Services.AddScoped<ITitreAdminService, TitreAdminService>();
builder.Services.AddScoped<ValidationActionFilter>();
builder.Services.AddScoped<GlobalExceptionFilter>();
// https://learn.microsoft.com/fr-fr/aspnet/core/performance/response-compression?view=aspnetcore-10.0#configuration // https://learn.microsoft.com/fr-fr/aspnet/core/performance/response-compression?view=aspnetcore-10.0#configuration
// Ajoute le service de compression des reponses HTTP pour reduire la taille des donnees envoyees au client et ameliorer les performances de l'application. // Ajoute le service de compression des reponses HTTP pour reduire la taille des donnees envoyees au client et ameliorer les performances de l'application.
builder.Services.AddResponseCompression(); builder.Services.AddResponseCompression();
var app = builder.Build(); var app = builder.Build();
app.UseMiddleware<Webzine.WebApplication.Middlewares.LogTempsExecutionMiddleware>();
if (repositoryType == RepositoryType.Db) if (repositoryType == RepositoryType.Db)
{ {
using (var scope = app.Services.CreateScope()) using (var scope = app.Services.CreateScope())
{ {
var db = scope.ServiceProvider.GetRequiredService<WebzineDbContext>(); var db = scope.ServiceProvider.GetRequiredService<WebzineDbContext>();
db.Database.EnsureCreated();
if (shouldSeed) if (shouldSeed)
{ {
db.Database.EnsureDeleted(); db.Database.EnsureDeleted();

View File

@@ -29,7 +29,7 @@
@titre.Artiste.Nom @titre.Artiste.Nom
</a> </a>
- -
<a asp-action="Details" <a asp-action="Index"
asp-controller="Titre" asp-controller="Titre"
asp-route-id="@titre.IdTitre"> asp-route-id="@titre.IdTitre">
@titre.Libelle @titre.Libelle
@@ -43,7 +43,7 @@
<!-- Footer --> <!-- Footer -->
<div class="d-flex flex-wrap align-items-center gap-3"> <div class="d-flex flex-wrap align-items-center gap-3">
<a asp-action="Details" <a asp-action="Index"
asp-controller="Titre" asp-controller="Titre"
asp-route-id="@titre.IdTitre" asp-route-id="@titre.IdTitre"
class="btn btn-primary btn-sm"> class="btn btn-primary btn-sm">
@@ -90,7 +90,7 @@
<img class="card-img-top" src="@titre.UrlJaquette" alt="@titre.Album" loading="lazy" /> <img class="card-img-top" src="@titre.UrlJaquette" alt="@titre.Album" loading="lazy" />
<div class="card-body"> <div class="card-body">
<a asp-controller="Titre" asp-action="Details" asp-route-id="@titre.IdTitre" class="card-link"> <a asp-controller="Titre" asp-action="Index" asp-route-id="@titre.IdTitre" class="card-link">
@titre.Libelle @titre.Libelle
</a> </a>
<br /> <br />

View File

@@ -56,7 +56,7 @@ else
<td class="text-secondary font-monospace">@dureeFormatee</td> <td class="text-secondary font-monospace">@dureeFormatee</td>
<td> <td>
<a asp-controller="Titre" <a asp-controller="Titre"
asp-action="Details" asp-action="Index"
asp-route-id="@titre.IdTitre" asp-route-id="@titre.IdTitre"
class="text-primary"> class="text-primary">
@titre.Libelle @titre.Libelle

View File

@@ -15,7 +15,7 @@
@if (!Model.Artistes.Any()) @if (!Model.Artistes.Any())
{ {
<div class="alert alert-info"> <div class="alert alert-info">
<p>Aucun artiste n'a été trouvé.</p> <p>Aucun artiste n'a été trouvé.</p>
</div> </div>
} }
@@ -35,7 +35,7 @@
@if (!Model.Titres.Any()) @if (!Model.Titres.Any())
{ {
<div class="alert alert-info"> <div class="alert alert-info">
<p>Aucun titre n'a été trouvé.</p> <p>Aucun titre n'a été trouvé.</p>
</div> </div>
} }
@@ -43,7 +43,7 @@
{ {
<div class="d-flex align-items-start my-3"> <div class="d-flex align-items-start my-3">
<a asp-controller="Titre" <a asp-controller="Titre"
asp-action="Details" asp-action="Index"
asp-route-id="@titre.IdTitre" asp-route-id="@titre.IdTitre"
class="me-3 text-black"> class="me-3 text-black">
<img src="@titre.UrlJaquette" alt="@titre.Libelle" width="70" height="70" class="object-fit-cover" loading="lazy" /> <img src="@titre.UrlJaquette" alt="@titre.Libelle" width="70" height="70" class="object-fit-cover" loading="lazy" />
@@ -58,7 +58,7 @@
</a> </a>
- -
<a asp-controller="Titre" <a asp-controller="Titre"
asp-action="Details" asp-action="Index"
asp-route-id="@titre.IdTitre"> asp-route-id="@titre.IdTitre">
@titre.Libelle @titre.Libelle
</a> </a>

View File

@@ -60,13 +60,14 @@
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<form asp-action="Like" method="post"> <form asp-action="Like" method="post">
<input type="hidden" name="IdTitre" value="@Model.Details.IdTitre" /> <input type="hidden" name="IdTitre" value="@Model.Details.IdTitre"/>
<button type="submit" class="btn btn-outline-primary btn-sm"> <button type="submit" class="btn btn-outline-primary btn-sm">
<i class="fa fa-thumbs-up me-1"></i> Like <i class="fa fa-thumbs-up me-1"></i> Like
</button> </button>
</form> </form>
<a asp-area="Administration" asp-controller="Titre" asp-action="Edit" asp-route-id="@Model.Details.IdTitre" class="btn text-primary btn-sm"> <a asp-area="Administration" asp-controller="Titre" asp-action="Edit"
asp-route-id="@Model.Details.IdTitre" class="btn text-primary btn-sm">
<i class="fa fa-pen-to-square me-1"></i> Editer <i class="fa fa-pen-to-square me-1"></i> Editer
</a> </a>
@@ -88,7 +89,7 @@
class="img-fluid rounded shadow" class="img-fluid rounded shadow"
alt="Jaquette" alt="Jaquette"
loading="lazy" loading="lazy"
fetchpriority="high" /> fetchpriority="high"/>
</div> </div>
</div> </div>
@@ -135,7 +136,7 @@
<input name="Auteur" <input name="Auteur"
class="form-control input-full" class="form-control input-full"
placeholder="Votre nom" placeholder="Votre nom"
required /> required/>
</div> </div>
</div> </div>
@@ -145,10 +146,10 @@
</label> </label>
<div class="col"> <div class="col">
<textarea name="Contenu" <textarea name="Contenu"
rows="3" rows="3"
class="form-control input-full" class="form-control input-full"
placeholder="Votre commentaire..." placeholder="Votre commentaire..."
required></textarea> required></textarea>
</div> </div>
</div> </div>
@@ -170,7 +171,7 @@
<h4 class="mb-4">Commentaires</h4> <h4 class="mb-4">Commentaires</h4>
@if (Model.Details.Commentaires != null && Model.Details.Commentaires.Any()) @if (Model.Details.Commentaires.Any())
{ {
foreach (var comment in Model.Details.Commentaires.OrderByDescending(c => c.DateCreation)) foreach (var comment in Model.Details.Commentaires.OrderByDescending(c => c.DateCreation))
{ {
@@ -183,7 +184,7 @@
width="50" width="50"
height="50" height="50"
class="rounded-circle me-3 shadow-sm" class="rounded-circle me-3 shadow-sm"
alt="avatar" /> alt="avatar"/>
<div> <div>
<strong>@comment.Auteur</strong>, <strong>@comment.Auteur</strong>,

View File

@@ -26,7 +26,7 @@
<div class="d-flex align-items-start my-3"> <div class="d-flex align-items-start my-3">
<!-- Image --> <!-- Image -->
<a asp-action="Details" <a asp-action="Index"
asp-route-id="@titre.IdTitre" asp-route-id="@titre.IdTitre"
class="me-3 text-black"> class="me-3 text-black">
<img src="@titre.UrlJaquette" alt="@titre.Libelle" width="70px" height="70px" class="object-fit-cover" loading="lazy"/> <img src="@titre.UrlJaquette" alt="@titre.Libelle" width="70px" height="70px" class="object-fit-cover" loading="lazy"/>
@@ -41,7 +41,7 @@
@titre.ArtisteNom @titre.ArtisteNom
</a> </a>
- -
<a asp-action="Details" <a asp-action="Index"
asp-route-id="@titre.IdTitre"> asp-route-id="@titre.IdTitre">
@titre.Libelle @titre.Libelle
</a> </a>

View File

@@ -34,6 +34,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Webzine.Business.Contracts\Webzine.Business.Contracts.csproj" />
<ProjectReference Include="..\Webzine.Business\Webzine.Business.csproj" /> <ProjectReference Include="..\Webzine.Business\Webzine.Business.csproj" />
<ProjectReference Include="..\Webzine.EntitiesContext\Webzine.EntitiesContext.csproj" /> <ProjectReference Include="..\Webzine.EntitiesContext\Webzine.EntitiesContext.csproj" />
<ProjectReference Include="..\Webzine.Entity\Webzine.Entity.csproj" /> <ProjectReference Include="..\Webzine.Entity\Webzine.Entity.csproj" />

View File

@@ -23,5 +23,8 @@
"TracksPerAlbum": 40, "TracksPerAlbum": 40,
"MaxCommentsPerTrack": 3 "MaxCommentsPerTrack": 3
}, },
"AllowedHosts": "*" "AllowedHosts": "*",
"EfPerformance": {
"SeuilMs": 10
}
} }

View File

@@ -28,7 +28,7 @@
<rules> <rules>
<!-- Vos logs d'application en Debug+ --> <!-- Vos logs d'application en Debug+ -->
<logger name="Webzine.WebApplication.*" minlevel="Debug" writeTo="allfile,ownfile-web,console" /> <logger name="Webzine.WebApplication.*" minlevel="Info" writeTo="allfile,ownfile-web,console" />
<!-- Logs Microsoft en Warning+ sauf Hosting.Lifetime --> <!-- Logs Microsoft en Warning+ sauf Hosting.Lifetime -->
<logger name="Microsoft.*" minlevel="Warn" writeTo="allfile" final="true" /> <logger name="Microsoft.*" minlevel="Warn" writeTo="allfile" final="true" />

View File

@@ -3,5 +3,7 @@
image: webzine.webapplication image: webzine.webapplication
build: build:
context: . context: .
dockerfile: Webzine.WebApplication/Dockerfile ports:
- "8080:8080"
environment:
- ConnectionStrings__PostGreSQLConnection=${PGSQL_CONNECTION}

View File

@@ -131,7 +131,7 @@ info "── Titre Par style ───────────────
STYLES=("Rock" "Pop" "Rap" "Jazz" "Metal" "Electronic" "Hip-Hop" "Soul" "Funk") STYLES=("Rock" "Pop" "Rap" "Jazz" "Metal" "Electronic" "Hip-Hop" "Soul" "Funk")
for STYLE in "${STYLES[@]}"; do for STYLE in "${STYLES[@]}"; do
ENCODE=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$STYLE'))" 2>/dev/null || echo "$STYLE") ENCODE=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$STYLE'))" 2>/dev/null || echo "$STYLE")
verifier_endpoint GET "$BASE_URL/titre/style/$ENCODE" "GET /titre/style/$STYLE" verifier_endpoint GET "$BASE_URL/titres/style/$ENCODE" "GET /titres/style/$STYLE"
done done
log "" log ""
@@ -160,26 +160,26 @@ verifier_endpoint GET "$BASE_URL/Administration/Dashboard" "GET /A
log "" log ""
info "── Administration Artiste ──────────────────────────────" info "── Administration Artiste ──────────────────────────────"
verifier_endpoint GET "$BASE_URL/Administration/Artiste" "GET /Administration/Artiste (liste)" verifier_endpoint GET "$BASE_URL/Administration/Artistes" "GET /Administration/Artistes (liste)"
verifier_endpoint GET "$BASE_URL/Administration/Artiste/Create" "GET /Administration/Artiste/Create" verifier_endpoint GET "$BASE_URL/Administration/Artiste/Create" "GET /Administration/Artiste/Create"
verifier_endpoint GET "$BASE_URL/Administration/Artiste/Edit/1" "GET /Administration/Artiste/Edit/1" verifier_endpoint GET "$BASE_URL/Administration/Artiste/Edit/1" "GET /Administration/Artiste/Edit/1"
verifier_endpoint GET "$BASE_URL/Administration/Artiste/Delete/1" "GET /Administration/Artiste/Delete/1" verifier_endpoint GET "$BASE_URL/Administration/Artiste/Delete/1" "GET /Administration/Artiste/Delete/1"
log "" log ""
info "── Administration Commentaire ──────────────────────────" info "── Administration Commentaire ──────────────────────────"
verifier_endpoint GET "$BASE_URL/Administration/Commentaire" "GET /Administration/Commentaire (liste)" verifier_endpoint GET "$BASE_URL/Administration/Commentaires" "GET /Administration/Commentaires (liste)"
verifier_endpoint GET "$BASE_URL/Administration/Commentaire/Delete/1" "GET /Administration/Commentaire/Delete/1" verifier_endpoint GET "$BASE_URL/Administration/Commentaire/Delete/1" "GET /Administration/Commentaire/Delete/1"
log "" log ""
info "── Administration Style ────────────────────────────────" info "── Administration Style ────────────────────────────────"
verifier_endpoint GET "$BASE_URL/Administration/Style" "GET /Administration/Style (liste)" verifier_endpoint GET "$BASE_URL/Administration/Styles" "GET /Administration/Styles (liste)"
verifier_endpoint GET "$BASE_URL/Administration/Style/Create" "GET /Administration/Style/Create" verifier_endpoint GET "$BASE_URL/Administration/Style/Create" "GET /Administration/Style/Create"
verifier_endpoint GET "$BASE_URL/Administration/Style/Edit/1" "GET /Administration/Style/Edit/1" verifier_endpoint GET "$BASE_URL/Administration/Style/Edit/1" "GET /Administration/Style/Edit/1"
verifier_endpoint GET "$BASE_URL/Administration/Style/Delete/1" "GET /Administration/Style/Delete/1" verifier_endpoint GET "$BASE_URL/Administration/Style/Delete/1" "GET /Administration/Style/Delete/1"
log "" log ""
info "── Administration Titre ────────────────────────────────" info "── Administration Titre ────────────────────────────────"
verifier_endpoint GET "$BASE_URL/Administration/Titre" "GET /Administration/Titre (liste)" verifier_endpoint GET "$BASE_URL/Administration/Titres" "GET /Administration/Titres (liste)"
verifier_endpoint GET "$BASE_URL/Administration/Titre/Create" "GET /Administration/Titre/Create" verifier_endpoint GET "$BASE_URL/Administration/Titre/Create" "GET /Administration/Titre/Create"
verifier_endpoint GET "$BASE_URL/Administration/Titre/Edit/1" "GET /Administration/Titre/Edit/1" verifier_endpoint GET "$BASE_URL/Administration/Titre/Edit/1" "GET /Administration/Titre/Edit/1"
verifier_endpoint GET "$BASE_URL/Administration/Titre/Delete/1" "GET /Administration/Titre/Delete/1" verifier_endpoint GET "$BASE_URL/Administration/Titre/Delete/1" "GET /Administration/Titre/Delete/1"
@@ -221,7 +221,4 @@ cat >> "$RAPPORT_FICHIER" <<EOF
Total : $TOTAL Total : $TOTAL
Réussis : $REUSSIS Réussis : $REUSSIS
Échecs : $ECHECS Échecs : $ECHECS
EOF EOF
# Code de sortie non nul si des échecs ont été détectés — permet à la CI de bloquer
exit "$ECHECS"