(Rebase) Resolution de merge conflit entre dev et j3/feat/pagination

This commit is contained in:
Loic Masi
2026-04-04 20:11:20 +02:00
29 changed files with 260 additions and 92 deletions

View File

@@ -74,14 +74,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
{
@@ -121,18 +113,16 @@ public class ArtisteController : Controller
[HttpPost]
public IActionResult Edit(ArtisteEditViewModel model)
{
var artiste = new Artiste
{
IdArtiste = model.Id,
Nom = model.Nom,
Biographie = model.Biographie,
};
var artiste = this.artisteRepository.Find(model.Id);
if (!this.ModelState.IsValid)
if (artiste == null)
{
return this.View(artiste);
return this.RedirectToAction("Index");
}
artiste.Nom = model.Nom;
artiste.Biographie = model.Biographie;
this.artisteRepository.Update(artiste);
return this.RedirectToAction("Index");

View File

@@ -96,11 +96,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

@@ -2,9 +2,10 @@ namespace Webzine.WebApplication.Areas.Administration.Controllers;
using Microsoft.AspNetCore.Mvc;
using ViewModels.Styles;
using Webzine.Entity;
using Webzine.Repository.Contracts;
using Webzine.WebApplication.Areas.Administration.ViewModels.Style;
/// <summary>
/// Controleur pour la gestion des styles dans l'administration du webzine.
@@ -77,11 +78,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,
@@ -138,7 +134,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);
@@ -165,11 +160,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

@@ -114,13 +114,35 @@ public class TitreController : Controller
[HttpPost]
public IActionResult Create(TitreAdminDTO model)
{
if (this.ModelState.IsValid)
if (!this.ModelState.IsValid)
{
this.titreAdminService.CreerTitre(model);
return this.RedirectToAction("Index");
var form = new AdminTitreForm
{
IdArtiste = model.IdArtiste,
Libelle = model.Libelle,
Album = model.Album,
Chronique = model.Chronique,
DateSortie = model.DateSortie,
Duree = model.Duree,
UrlJaquette = model.UrlJaquette,
UrlEcoute = model.UrlEcoute,
Styles = model.Styles,
Artistes = this.artisteRepository.FindAll().Select(a => new SelectListItem
{
Value = a.IdArtiste.ToString(),
Text = a.Nom,
}).ToList(),
AllStyles = this.styleRepository.FindAll().Select(s => new SelectListItem
{
Value = s.IdStyle.ToString(),
Text = s.Libelle,
}).ToList(),
};
return this.View(form);
}
return this.View(model);
this.titreAdminService.CreerTitre(model);
return this.RedirectToAction("Index");
}
/// <summary>
@@ -171,13 +193,36 @@ public class TitreController : Controller
[HttpPost]
public IActionResult Edit(TitreAdminDTO model)
{
if (this.ModelState.IsValid)
if (!this.ModelState.IsValid)
{
this.titreAdminService.ModifierTitre(model);
return this.RedirectToAction("Index");
var form = new AdminTitreForm
{
Id = model.Id,
IdArtiste = model.IdArtiste,
Libelle = model.Libelle,
Album = model.Album,
Chronique = model.Chronique,
DateSortie = model.DateSortie,
Duree = model.Duree,
UrlJaquette = model.UrlJaquette,
UrlEcoute = model.UrlEcoute,
Styles = model.Styles,
Artistes = this.artisteRepository.FindAll().Select(a => new SelectListItem
{
Value = a.IdArtiste.ToString(),
Text = a.Nom,
}).ToList(),
AllStyles = this.styleRepository.FindAll().Select(s => new SelectListItem
{
Value = s.IdStyle.ToString(),
Text = s.Libelle,
}).ToList(),
};
return this.View(form);
}
return this.View(model);
this.titreAdminService.ModifierTitre(model);
return this.RedirectToAction("Index");
}
/// <summary>

View File

@@ -14,11 +14,11 @@
/// <summary>
/// Définit le nom de l'artiste.
/// </summary>
public string Nom { get; set; }
public string? Nom { get; set; }
/// <summary>
/// Définit la biographie de l'artiste.
/// </summary>
public string Biographie { get; set; }
public string? Biographie { get; set; }
}
}

View File

@@ -10,12 +10,15 @@
/// <summary>
/// Nom de l'artiste.
/// </summary>
[Required]
[Required(ErrorMessage = "Le nom de l'auteur est obligatoire.")]
[StringLength(50, ErrorMessage = "Le nom ne doit pas dépasser 50 caractères.")]
public string Nom { get; set; }
/// <summary>
/// Biographie de l'artiste.
/// </summary>
/// </summary>*
[Required(ErrorMessage = "La biographie ne peux pas etre vide.")]
public string Biographie { get; set; }
}
}

View File

@@ -16,12 +16,11 @@
/// <summary>
/// Nom de l'artiste.
/// </summary>
[Required]
[Required(ErrorMessage = "Le nom de l'auteur est obligatoire.")]
[StringLength(50, ErrorMessage = "Le nom ne doit pas dépasser 50 caractères.")]
public string Nom { get; set; }
/// <summary>
/// Biographie de l'artiste.
/// </summary>
public string Biographie { get; set; }
}
}

View File

@@ -2,7 +2,7 @@
// Copyright (c) Equipe 1 - BOBIN, MASI, NODON, VETU. All rights reserved.
// </copyright>
namespace Webzine.WebApplication.Areas.Administration.ViewModels.Style
namespace Webzine.WebApplication.Areas.Administration.ViewModels.Styles
{
using System.ComponentModel.DataAnnotations;
@@ -14,7 +14,7 @@ namespace Webzine.WebApplication.Areas.Administration.ViewModels.Style
/// <summary>
/// Obtient ou définit le libellé du style.
/// </summary>
[Required]
[Required(ErrorMessage = "Le libelle du style est obligatoire.")]
public string Libelle { get; set; }
}
}

View File

@@ -2,7 +2,7 @@
// Copyright (c) Equipe 1 - BOBIN, MASI, NODON, VETU. All rights reserved.
// </copyright>
namespace Webzine.WebApplication.Areas.Administration.ViewModels.Style
namespace Webzine.WebApplication.Areas.Administration.ViewModels.Styles
{
/// <summary>
/// ViewModel pour la suppression d'un style en administration.
@@ -17,6 +17,6 @@ namespace Webzine.WebApplication.Areas.Administration.ViewModels.Style
/// <summary>
/// Obtient ou définit le libellé du style.
/// </summary>
public string Libelle { get; set; }
public string? Libelle { get; set; }
}
}

View File

@@ -2,7 +2,7 @@
// Copyright (c) Equipe 1 - BOBIN, MASI, NODON, VETU. All rights reserved.
// </copyright>
namespace Webzine.WebApplication.Areas.Administration.ViewModels.Style
namespace Webzine.WebApplication.Areas.Administration.ViewModels.Styles
{
using System.ComponentModel.DataAnnotations;
@@ -19,7 +19,7 @@ namespace Webzine.WebApplication.Areas.Administration.ViewModels.Style
/// <summary>
/// Obtient ou definit le libelle du style.
/// </summary>
[Required]
[Required(ErrorMessage = "Le libelle du style est obligatoire.")]
public string Libelle { get; set; }
}
}

View File

@@ -1,5 +1,7 @@
namespace Webzine.WebApplication.Areas.Administration.ViewModels.Titre;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.Rendering;
/// <summary>
@@ -15,36 +17,47 @@ public class AdminTitreForm
/// <summary>
/// Définit l'identifiant de l'artiste associé au titre.
/// </summary>
[Required(ErrorMessage = "L'id de l'artiste est obligatoire.")]
public int IdArtiste { get; set; }
/// <summary>
/// Définit le titre du titre.
/// </summary>
[Required(ErrorMessage = "Le labelle est obligatoire.")]
public string Libelle { get; set; }
/// <summary>
/// Définit le nom de l'album associé au titre.
/// </summary>
[Required(ErrorMessage = "L'album est obligatoire.")]
public string Album { get; set; }
/// <summary>
/// Définit la chronique du titre, peut-être une critique ou une description du titre.
/// </summary>
[Required(ErrorMessage = "La chronique est obligatoire.")]
public string Chronique { get; set; }
/// <summary>
/// Définit la date de sortie du titre.
/// </summary>
[Required(ErrorMessage = "La date de est obligatoire.")]
public DateTime DateSortie { get; set; }
/// <summary>
/// Définit la durée du titre en secondes.
/// </summary>
[Required(ErrorMessage = "La durée est obligatoire.")]
public int Duree { get; set; }
/// <summary>
/// Définit l'URL de la jaquette de l'album associé au titre.
/// </summary>
[Required(ErrorMessage = "L'Url de la jaquette est obligatoire.")]
public string UrlJaquette { get; set; }
/// <summary>

View File

@@ -11,6 +11,8 @@
<label class="col-md-3 col-form-label">Nom de l'artiste<span class="text-danger">*</span></label>
<div class="col-md-9">
<input asp-for="Nom" class="form-control" />
<span asp-validation-for="Nom" class="text-danger"></span>
</div>
</div>
@@ -19,6 +21,8 @@
<label class="col-md-3 col-form-label">Biographie</label>
<div class="col-md-9">
<textarea asp-for="Biographie" class="form-control" rows="5"></textarea>
<span asp-validation-for="Biographie" class="text-danger"></span>
</div>
</div>

View File

@@ -6,6 +6,8 @@
<label class="col-md-3 col-form-label">Nom de l'artiste<span class="text-danger">*</span></label>
<div class="col-md-9">
<input asp-for="Nom" class="form-control" />
<span asp-validation-for="Nom" class="text-danger"></span>
</div>
</div>
@@ -14,7 +16,10 @@
<label class="col-md-3 col-form-label">Biographie</label>
<div class="col-md-9">
<textarea asp-for="Biographie" class="form-control" rows="5"></textarea>
<span asp-validation-for="Biographie" class="text-danger"></span>
</div>
</div>
<!-- BOUTONS -->

View File

@@ -1,4 +1,4 @@
@model Webzine.WebApplication.Areas.Administration.ViewModels.Style.StyleCreateViewModel
@model Webzine.WebApplication.Areas.Administration.ViewModels.Styles.StyleCreateViewModel
@{
ViewData["Title"] = "Créer un style";

View File

@@ -1,4 +1,4 @@
@model Webzine.WebApplication.Areas.Administration.ViewModels.Style.StyleDeleteViewModel
@model Webzine.WebApplication.Areas.Administration.ViewModels.Styles.StyleDeleteViewModel
@{
ViewData["Title"] = "Supprimer un style";

View File

@@ -1,4 +1,4 @@
@model Webzine.WebApplication.Areas.Administration.ViewModels.Style.StyleEditViewModel
@model Webzine.WebApplication.Areas.Administration.ViewModels.Styles.StyleEditViewModel
@{
ViewData["Title"] = "Editer un style";

View File

@@ -8,6 +8,8 @@
<select asp-for="IdArtiste"
asp-items="Model.Artistes"
class="form-select"></select>
<span asp-validation-for="IdArtiste" class="text-danger"></span>
</div>
</div>
@@ -16,6 +18,7 @@
<label class="col-md-3 col-form-label">Titre<span class="text-danger">*</span></label>
<div class="col-md-9">
<input asp-for="Libelle" class="form-control"/>
<span asp-validation-for="Libelle" class="text-danger"></span>
</div>
</div>
@@ -24,6 +27,7 @@
<label class="col-md-3 col-form-label">Album<span class="text-danger">*</span></label>
<div class="col-md-9">
<input asp-for="Album" class="form-control"/>
<span asp-validation-for="Album" class="text-danger"></span>
</div>
</div>
@@ -34,6 +38,8 @@
<textarea asp-for="Chronique"
class="form-control"
rows="5"></textarea>
<span asp-validation-for="Chronique" class="text-danger"></span>
</div>
</div>
@@ -55,6 +61,8 @@
class="form-control"
type="number"
min="0" />
<span asp-validation-for="Duree" class="text-danger"></span>
<span class="input-group-text text-muted">seconds</span>
</div>
</div>
@@ -66,6 +74,8 @@
<div class="col-md-9">
<input asp-for="UrlJaquette"
class="form-control"/>
<span asp-validation-for="UrlJaquette" class="text-danger"></span>
</div>
</div>

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,7 +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<GlobalExceptionFilter>();
options.Filters.Add<ValidationActionFilter>();
})
// Ajoute la compilation des vues lors de l'execution de l'application.
// Cela nous evite de recompiler l'application a chaque modification de vue.
@@ -87,6 +92,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>

View File

@@ -16,6 +16,6 @@
},
"AllowedHosts": "*",
"EfPerformance": {
"SeuilMs": 10
"SeuilMs": 60
}
}