diff --git a/.gitea/workflows/deploy-prod.yaml b/.gitea/workflows/deploy-prod.yaml new file mode 100644 index 0000000..0bb9c60 --- /dev/null +++ b/.gitea/workflows/deploy-prod.yaml @@ -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é !" \ No newline at end of file diff --git a/Webzine.WebApplication/Areas/Administration/ViewModels/DashboardViewModel.cs b/Webzine.Business.Contracts/Dto/DashboardDTO.cs similarity index 90% rename from Webzine.WebApplication/Areas/Administration/ViewModels/DashboardViewModel.cs rename to Webzine.Business.Contracts/Dto/DashboardDTO.cs index 106779d..9509c6c 100644 --- a/Webzine.WebApplication/Areas/Administration/ViewModels/DashboardViewModel.cs +++ b/Webzine.Business.Contracts/Dto/DashboardDTO.cs @@ -1,9 +1,9 @@ -namespace Webzine.WebApplication.Areas.Administration.ViewModels; +namespace Webzine.Business.Contracts.Dto; /// -/// ViewModel pour le tableau de bord de l'administration du webzine. +/// DTO pour le tableau de bord de l'administration du webzine. /// -public class DashboardViewModel +public class DashboardDTO { /// /// Définit le nombre total d'artistes chroniqués dans le webzine. diff --git a/Webzine.Business.Contracts/IDashboardService.cs b/Webzine.Business.Contracts/IDashboardService.cs new file mode 100644 index 0000000..8bde456 --- /dev/null +++ b/Webzine.Business.Contracts/IDashboardService.cs @@ -0,0 +1,16 @@ +using Webzine.Business.Contracts.Dto; + +namespace Webzine.Business.Contracts; + +/// +/// 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. +/// +public interface IDashboardService +{ + /// + /// Calcule et retourne toutes les statistiques du tableau de bord en une seule passe. + /// + /// Un contenant les agrégats calculés. + DashboardDTO GetDashboardData(); +} \ No newline at end of file diff --git a/Webzine.Business/DashboardService.cs b/Webzine.Business/DashboardService.cs new file mode 100644 index 0000000..d9e9812 --- /dev/null +++ b/Webzine.Business/DashboardService.cs @@ -0,0 +1,71 @@ +using Webzine.Business.Contracts; +using Webzine.Entity; +using Webzine.Repository.Contracts; + +namespace Webzine.Business; + +using Contracts.Dto; + +/// +/// Implémentation de . +/// Orchestre plusieurs appels aux repositories pour produire les statistiques du tableau de bord. +/// +public class DashboardService : IDashboardService +{ + private readonly IArtisteRepository artisteRepository; + private readonly ITitreRepository titreRepository; + private readonly IStyleRepository styleRepository; + + /// + /// Initializes a new instance of the class. + /// + /// Repository des artistes. + /// Repository des titres. + /// Repository des styles. + public DashboardService( + IArtisteRepository artisteRepository, + ITitreRepository titreRepository, + IStyleRepository styleRepository) + { + this.artisteRepository = artisteRepository; + this.titreRepository = titreRepository; + this.styleRepository = styleRepository; + } + + /// + public DashboardDTO GetDashboardData() + { + IEnumerable 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), + }; + } +} \ No newline at end of file diff --git a/Webzine.Repository.Contracts/IArtisteRepository.cs b/Webzine.Repository.Contracts/IArtisteRepository.cs index bc13f76..ee170b5 100644 --- a/Webzine.Repository.Contracts/IArtisteRepository.cs +++ b/Webzine.Repository.Contracts/IArtisteRepository.cs @@ -51,5 +51,18 @@ namespace Webzine.Repository.Contracts /// Nom de l'artiste. /// IEnumarble. qui contient la chaine de caractere. IEnumerable Search(string nom); + + /// + /// Récupère le nombre total d'artistes dans la collection. + /// + /// Le nombre total d'artistes. + int Count(); + + /// + /// Récupère le nombre d'artistes correspondant au prédicat fourni. + /// + /// Le prédicat de filtrage. + /// Le nombre d'artistes correspondants. + int Count(Func predicate); } } \ No newline at end of file diff --git a/Webzine.Repository.Contracts/IStyleRepository.cs b/Webzine.Repository.Contracts/IStyleRepository.cs index d260039..0ea9ba3 100644 --- a/Webzine.Repository.Contracts/IStyleRepository.cs +++ b/Webzine.Repository.Contracts/IStyleRepository.cs @@ -37,5 +37,11 @@ namespace Webzine.Repository.Contracts /// /// L'objet style à mettre à jour. + /// Récupère le nombre total de styles dans la liste des styles. + /// + /// Le nombre total de styles présents dans la liste. + int Count(); } } \ No newline at end of file diff --git a/Webzine.Repository/DbArtisteRepository.cs b/Webzine.Repository/DbArtisteRepository.cs index fa6ff18..f6f868b 100644 --- a/Webzine.Repository/DbArtisteRepository.cs +++ b/Webzine.Repository/DbArtisteRepository.cs @@ -83,8 +83,8 @@ namespace Webzine.Repository try { Artiste artiste = this.context.Artistes - .Include(a => a.Titres) - .First(a => a.IdArtiste == id); + .Include(a => a.Titres) + .FirstOrDefault(a => a.IdArtiste == id); return artiste; } catch (Exception ex) @@ -176,5 +176,37 @@ namespace Webzine.Repository throw new Exception("Erreur lors de la recherche d'artiste {error}", ex); } } + + /// + 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; + } + } + + /// + public int Count(Func 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; + } + } } } \ No newline at end of file diff --git a/Webzine.Repository/DbStyleRepository.cs b/Webzine.Repository/DbStyleRepository.cs index c350a4f..a189ffd 100644 --- a/Webzine.Repository/DbStyleRepository.cs +++ b/Webzine.Repository/DbStyleRepository.cs @@ -178,4 +178,20 @@ public class DbStyleRepository : IStyleRepository throw; } } + + /// + 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; + } + } } \ No newline at end of file diff --git a/Webzine.Repository/DbTitreRepository.cs b/Webzine.Repository/DbTitreRepository.cs index dad798d..1c199a1 100644 --- a/Webzine.Repository/DbTitreRepository.cs +++ b/Webzine.Repository/DbTitreRepository.cs @@ -245,15 +245,13 @@ public class DbTitreRepository : ITitreRepository try { this.logger.LogDebug("Recherche du titre avec l'ID: {IdTitre}", idTitre); - this.logger.LogDebug("Préparation de la requête avec les inclusions Artiste, Styles et Commentaires"); var titre = this.context.Titres .Include(t => t.Artiste) .Include(t => t.Styles) .Include(t => t.Commentaires) - .First(t => t.IdTitre == idTitre); + .FirstOrDefault(t => t.IdTitre == idTitre); - this.logger.LogDebug("Titre trouvé: {Libelle}", titre.Libelle); return titre; } catch (InvalidOperationException ex) diff --git a/Webzine.Repository/LocalArtisteRepository.cs b/Webzine.Repository/LocalArtisteRepository.cs index 69021c5..0388da3 100644 --- a/Webzine.Repository/LocalArtisteRepository.cs +++ b/Webzine.Repository/LocalArtisteRepository.cs @@ -93,5 +93,17 @@ namespace Webzine.Repository .Where(a => a.Nom.ToLower().Contains(mot.ToLower())) .ToList(); } + + /// + public int Count() + { + return this.dataStore.Artistes.Count; + } + + /// + public int Count(Func predicate) + { + return this.dataStore.Artistes.Count(predicate); + } } } \ No newline at end of file diff --git a/Webzine.Repository/LocalStyleRepository.cs b/Webzine.Repository/LocalStyleRepository.cs index 0b4d16c..e750ff2 100644 --- a/Webzine.Repository/LocalStyleRepository.cs +++ b/Webzine.Repository/LocalStyleRepository.cs @@ -56,4 +56,10 @@ public class LocalStyleRepository : IStyleRepository { throw new NotSupportedException("Mode local"); } + + /// + public int Count() + { + return this.dataStore.Styles.Count; + } } \ No newline at end of file diff --git a/Webzine.WebApplication/Areas/Administration/Controllers/ArtisteController.cs b/Webzine.WebApplication/Areas/Administration/Controllers/ArtisteController.cs index fcaf920..e6ad7e5 100644 --- a/Webzine.WebApplication/Areas/Administration/Controllers/ArtisteController.cs +++ b/Webzine.WebApplication/Areas/Administration/Controllers/ArtisteController.cs @@ -50,14 +50,37 @@ public class ArtisteController : Controller /// Redirection. public IActionResult Create() { - var model = new AdminArtisteForm + return this.View(); + } + + /// + /// Formulaire de création d'un artiste. + /// + /// Paramètre nécessaire pour la création d'un artiste. + /// Redirection sur la page Index. + [HttpPost] + public IActionResult Create(ArtisteCreateViewModel model) + { + // vérifier si les données sont corrects. + if (!this.ModelState.IsValid) { - Id = 0, - Nom = string.Empty, - Biographie = string.Empty, + // 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. + var artiste = new Artiste + { + Nom = model.Nom, + Biographie = model.Biographie, }; - return this.View(model); + // Persister les données. + this.artisteRepository.Add(artiste); + + // Renvoyer sur la page Index. + return this.RedirectToAction("Index"); } /// @@ -69,14 +92,37 @@ public class ArtisteController : Controller { var artiste = this.artisteRepository.Find(id); - var model = new AdminArtisteForm + if (artiste == null) { - Id = artiste.IdArtiste, - Nom = artiste.Nom, - Biographie = artiste.Biographie, + return this.RedirectToAction("Index"); + } + + return this.View(artiste); + } + + /// + /// Traitement du formulaire de modification d'un artiste. + /// + /// Paramètre d'un artiste. + /// Redirection sur Index. + [HttpPost] + public IActionResult Edit(ArtisteEditViewModel model) + { + var artiste = new Artiste + { + IdArtiste = model.Id, + Nom = model.Nom, + Biographie = model.Biographie, }; - return this.View(model); + if (!this.ModelState.IsValid) + { + return this.View(artiste); + } + + this.artisteRepository.Update(artiste); + + return this.RedirectToAction("Index"); } /// @@ -87,6 +133,12 @@ public class ArtisteController : Controller public IActionResult Delete(int id) { var artiste = this.artisteRepository.Find(id); + + if (artiste == null) + { + return this.RedirectToAction("Index"); + } + var model = new AdminArtisteForm { Id = id, diff --git a/Webzine.WebApplication/Areas/Administration/Controllers/CommentaireController.cs b/Webzine.WebApplication/Areas/Administration/Controllers/CommentaireController.cs index c60e9bd..b954e14 100644 --- a/Webzine.WebApplication/Areas/Administration/Controllers/CommentaireController.cs +++ b/Webzine.WebApplication/Areas/Administration/Controllers/CommentaireController.cs @@ -53,6 +53,11 @@ namespace Webzine.WebApplication.Areas.Administration.Controllers { var commentaire = this.commentaireRepository.Find(id); + if (commentaire == null) + { + return this.RedirectToAction("Index"); + } + var model = new CommentaireDeleteViewModel { IdCommentaire = commentaire.IdCommentaire, diff --git a/Webzine.WebApplication/Areas/Administration/Controllers/DashboardController.cs b/Webzine.WebApplication/Areas/Administration/Controllers/DashboardController.cs index 2f314ad..97f1721 100644 --- a/Webzine.WebApplication/Areas/Administration/Controllers/DashboardController.cs +++ b/Webzine.WebApplication/Areas/Administration/Controllers/DashboardController.cs @@ -1,30 +1,26 @@ namespace Webzine.WebApplication.Areas.Administration.Controllers; +using Webzine.Business.Contracts; +using Webzine.Business.Contracts.Dto; using Microsoft.AspNetCore.Mvc; - using Webzine.Repository.Contracts; -using Webzine.WebApplication.Areas.Administration.ViewModels; [Area("Administration")] public class DashboardController : Controller { private readonly ILogger logger; - private readonly IStyleRepository styleRepository; - private readonly IArtisteRepository artisteRepository; - private readonly ITitreRepository titreRepository; + private readonly IDashboardService dashboardService; /// /// Initializes a new instance of the class. /// Initialise une nouvelle instance de la classe . /// /// Service de journalisation injecté. - /// Repository des styles injecté. - public DashboardController(ILogger logger, IStyleRepository styleRepository, IArtisteRepository artisteRepository, ITitreRepository titreRepository) + /// Service de calcul des statistiques du tableau de bord. + public DashboardController(ILogger logger, IDashboardService dashboardService) { this.logger = logger; - this.styleRepository = styleRepository; - this.artisteRepository = artisteRepository; - this.titreRepository = titreRepository; + this.dashboardService = dashboardService; this.logger.LogInformation("Initialisation du contrôleur TitreController."); } @@ -35,42 +31,8 @@ public class DashboardController : Controller /// La vue Index du tableau de bord. public IActionResult Index() { - var artisteLePlusChronique = this.titreRepository.FindAll() - .GroupBy(t => t.Artiste) - .OrderByDescending(g => g.Count()) - .First(); + DashboardDTO data = dashboardService.GetDashboardData(); - var albumLePlusChronique = this.titreRepository.FindAll() - .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); + return this.View(data); } } \ No newline at end of file diff --git a/Webzine.WebApplication/Areas/Administration/Controllers/StyleController.cs b/Webzine.WebApplication/Areas/Administration/Controllers/StyleController.cs index e6741f9..1283fd0 100644 --- a/Webzine.WebApplication/Areas/Administration/Controllers/StyleController.cs +++ b/Webzine.WebApplication/Areas/Administration/Controllers/StyleController.cs @@ -1,109 +1,170 @@ -namespace Webzine.WebApplication.Areas.Administration.Controllers -{ - using Microsoft.AspNetCore.Mvc; +namespace Webzine.WebApplication.Areas.Administration.Controllers; - using Webzine.Repository.Contracts; - using Webzine.WebApplication.Areas.Administration.ViewModels.Style; +using Microsoft.AspNetCore.Mvc; + +using Webzine.Entity; +using Webzine.Repository.Contracts; +using Webzine.WebApplication.Areas.Administration.ViewModels.Style; + +/// +/// Controleur pour la gestion des styles dans l'administration du webzine. +/// +[Area("Administration")] +public class StyleController : Controller +{ + private readonly ILogger logger; + private readonly IStyleRepository styleRepository; /// - /// Contrôleur pour la gestion des styles dans l'administration du webzine. + /// Initializes a new instance of the class. /// - [Area("Administration")] - public class StyleController : Controller + /// Service de journalisation injecte. + /// Repository des styles injecte. + public StyleController( + ILogger logger, + IStyleRepository styleRepository) { - private readonly ILogger logger; - private readonly IStyleRepository styleRepository; + this.logger = logger; + this.styleRepository = styleRepository; - /// - /// Initializes a new instance of the class. - /// Initialise une nouvelle instance de la classe . - /// - /// Service de journalisation injecté. - /// Repository des styles injecté. - public StyleController( - ILogger logger, - IStyleRepository styleRepository) + this.logger.LogInformation("Initialisation du controleur StyleController."); + } + + /// + /// Affiche la liste des styles dans la vue Index. + /// + /// La vue Index avec la liste des styles. + public IActionResult Index() + { + IEnumerable