Merge pull request '#149 j3/feat/filter' (#189) from j3/feat/filter into dev

Reviewed-on: https://10.4.0.131/gitea/DI1-P4-E1/Webzine/pulls/189
This commit is contained in:
c.bobin
2026-04-02 16:38:51 +02:00
15 changed files with 154 additions and 79 deletions

View File

@@ -25,12 +25,7 @@
<ProjectReference Include="..\Webzine.Business.Contracts\Webzine.Business.Contracts.csproj" />
<ProjectReference Include="..\Webzine.Entity\Webzine.Entity.csproj" />
<ProjectReference Include="..\Webzine.Repository.Contracts\Webzine.Repository.Contracts.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.Extensions.Logging.Abstractions">
<HintPath>..\..\..\..\..\..\..\.nuget\packages\microsoft.extensions.logging.abstractions\10.0.5\lib\net10.0\Microsoft.Extensions.Logging.Abstractions.dll</HintPath>
</Reference>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-preview.1.25080.5" />
</ItemGroup>
</Project>

View File

@@ -36,11 +36,9 @@ public class ArtisteController : Controller
/// <returns>Redirection.</returns>
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_ordre);
return this.View(artistes);
}
/// <summary>
@@ -60,14 +58,6 @@ public class ArtisteController : Controller
[HttpPost]
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.
var artiste = new Artiste
{
@@ -114,11 +104,6 @@ public class ArtisteController : Controller
Biographie = model.Biographie,
};
if (!this.ModelState.IsValid)
{
return this.View(artiste);
}
this.artisteRepository.Update(artiste);
return this.RedirectToAction("Index");

View File

@@ -83,11 +83,6 @@ namespace Webzine.WebApplication.Areas.Administration.Controllers
{
var commentaire = this.commentaireRepository.Find(model.IdCommentaire);
if (!this.ModelState.IsValid)
{
return this.View(commentaire);
}
if (commentaire != null)
{
this.commentaireRepository.Delete(commentaire);

View File

@@ -5,8 +5,11 @@ using Microsoft.AspNetCore.Mvc;
using Webzine.Business.Contracts;
using Webzine.Business.Contracts.Dto;
/// <summary>
/// Contrôleur pour gérer le tableau de bord de l'administration.
/// </summary>
[Area("Administration")]
public class DashboardController : Controller // TODO à refaire
public class DashboardController : Controller
{
private readonly ILogger<DashboardController> logger;
private readonly IDashboardService dashboardService;

View File

@@ -63,11 +63,6 @@ public class StyleController : Controller
[HttpPost]
public IActionResult Create(StyleCreateViewModel model)
{
if (!this.ModelState.IsValid)
{
return this.View(model);
}
var style = new Style
{
Libelle = model.Libelle,
@@ -124,7 +119,6 @@ public class StyleController : Controller
/// </summary>
/// <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>
[HttpGet]
public IActionResult Edit(int id)
{
var style = this.styleRepository.Find(id);
@@ -151,11 +145,6 @@ public class StyleController : Controller
[HttpPost]
public IActionResult Edit(StyleEditViewModel model)
{
if (!this.ModelState.IsValid)
{
return this.View(model);
}
var style = this.styleRepository.Find(model.IdStyle);
if (style == null)
{

View File

@@ -95,13 +95,8 @@ public class TitreController : Controller
[HttpPost]
public IActionResult Create(TitreAdminDTO model)
{
if (this.ModelState.IsValid)
{
this.titreAdminService.CreerTitre(model);
return this.RedirectToAction("Index");
}
return this.View(model);
this.titreAdminService.CreerTitre(model);
return this.RedirectToAction("Index");
}
/// <summary>
@@ -152,13 +147,8 @@ public class TitreController : Controller
[HttpPost]
public IActionResult Edit(TitreAdminDTO model)
{
if (this.ModelState.IsValid)
{
this.titreAdminService.ModifierTitre(model);
return this.RedirectToAction("Index");
}
return this.View(model);
this.titreAdminService.ModifierTitre(model);
return this.RedirectToAction("Index");
}
/// <summary>

View File

@@ -21,6 +21,7 @@
/// </summary>
/// <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="titreRepository"></param>
public AccueilController(
ILogger<AccueilController> logger,
IConfiguration configuration,

View File

@@ -2,12 +2,14 @@ namespace Webzine.WebApplication.Controllers;
using Microsoft.AspNetCore.Mvc;
/// <summary>
/// Controller de version de l'API.
/// </summary>
public class ApiController : ControllerBase
{
private readonly ILogger<ApiController> logger;
/// <summary>
/// Initializes a new instance of the <see cref="ApiController"/> class.
/// Initialise une nouvelle instance de la classe <see cref="ApiController"/>.
/// </summary>
/// <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.
/// </summary>
/// <returns>Un objet JSON avec les propriétés "nom" et "version".</returns>
[HttpGet]
public IActionResult Version()
{
this.logger.LogInformation("Get Version was called");

View File

@@ -7,7 +7,6 @@
/// <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
{

View File

@@ -118,7 +118,7 @@ namespace Webzine.WebApplication.Controllers
this.titreRepository.IncrementNbLikes(titre);
}
return this.RedirectToAction("Details", new { id = model.IdTitre });
return this.RedirectToAction("Index", new { id = model.IdTitre });
}
/// <summary>
@@ -129,12 +129,6 @@ namespace Webzine.WebApplication.Controllers
[HttpPost]
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);
if (titre == null)
@@ -155,7 +149,7 @@ namespace Webzine.WebApplication.Controllers
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)

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

@@ -15,6 +15,7 @@ using Webzine.Repository;
using Webzine.Repository.Contracts;
using Webzine.WebApplication.Configuration;
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.
@@ -27,8 +28,11 @@ try
// Ajoute les services necessaires pour permettre l'utilisation des
// controllers avec des vues.
builder.Services.AddControllersWithViews()
builder.Services.AddControllersWithViews(options =>
{
options.Filters.Add<ValidationActionFilter>();
options.Filters.Add<GlobalExceptionFilter>();
})
// Ajoute la compilation des vues lors de l'execution de l'application.
// Cela nous evite de recompiler l'application a chaque modification de vue.
// Necessite le package Nuget Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.
@@ -87,6 +91,9 @@ try
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
// Ajoute le service de compression des réponses HTTP pour réduire la taille des données envoyées au client et améliorer les performances de l'application.
builder.Services.AddResponseCompression();

View File

@@ -60,13 +60,14 @@
<div class="d-flex gap-2">
<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">
<i class="fa fa-thumbs-up me-1"></i> Like
</button>
</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
</a>
@@ -88,7 +89,7 @@
class="img-fluid rounded shadow"
alt="Jaquette"
loading="lazy"
fetchpriority="high" />
fetchpriority="high"/>
</div>
</div>
@@ -121,7 +122,7 @@
<input name="Auteur"
class="form-control input-full"
placeholder="Votre nom"
required />
required/>
</div>
</div>
@@ -131,10 +132,10 @@
</label>
<div class="col">
<textarea name="Contenu"
rows="3"
class="form-control input-full"
placeholder="Votre commentaire..."
required></textarea>
rows="3"
class="form-control input-full"
placeholder="Votre commentaire..."
required></textarea>
</div>
</div>
@@ -156,7 +157,7 @@
<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))
{
@@ -169,7 +170,7 @@
width="50"
height="50"
class="rounded-circle me-3 shadow-sm"
alt="avatar" />
alt="avatar"/>
<div>
<strong>@comment.Auteur</strong>,

View File

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