Compare commits

...

6 Commits

Author SHA1 Message Date
e249a8c203 Fix: Debug.sh 2026-04-13 10:46:11 +02:00
6b23a40196 feat: Implement a real-time character counter and limit validation for the rich text editor, including timer management. 2026-02-26 17:59:50 +01:00
5f2e7a9b56 Refactor: wrap form labels in a div.col-sm-3 for improved layout and styling consistency. 2026-02-26 17:25:38 +01:00
f42c3fe9f2 feat: Relaxed file upload authorization from 'Edit' to 'View' permission and updated the year input help text. 2026-02-26 17:08:52 +01:00
16cb602d3a fix: Correct HallOfFame add and update operations to require View permission instead of Edit. 2026-02-26 16:56:50 +01:00
bfa8ff158c feat: Rich-Text-Editor, Bild-Skalierung, PDF-Fix & Zeichenlimit
- Bild-Skalierung in Kartenansicht gefixt (object-position: top, 300px)
- Admin-Slider für Zeichenlimit (4–32.000) als Modul-Setting
- Textarea durch RichTextEditor (Quill.js) ersetzt
- PDF: HTML-Parsing, Einzelperson-Filter, Autorisierung für alle User
2026-02-26 16:26:06 +01:00
8 changed files with 354 additions and 193 deletions

View File

@@ -75,10 +75,7 @@ else
</div> </div>
} }
<div class="lead text-dark mb-4" style="line-height: 1.7; font-size: 1.1rem;"> <div class="lead text-dark mb-4" style="line-height: 1.7; font-size: 1.1rem;">
@foreach (var line in (_item.Description?.Replace("\t", " ").Split('\n') ?? Array.Empty<string>())) @((MarkupString)(_item.Description ?? ""))
{
<div class="hof-description-line">@line</div>
}
</div> </div>
</div> </div>
@@ -250,9 +247,16 @@ else
} }
} }
private string GetPdfApiBase()
{
var aliasPath = PageState.Alias.Path;
var prefix = !string.IsNullOrEmpty(aliasPath) ? $"/{aliasPath}" : "";
return $"{prefix}/api/HallOfFamePdf";
}
private void ShowPdfPreview() private void ShowPdfPreview()
{ {
_pdfPreviewUrl = $"/api/HallOfFamePdf?moduleid={ModuleState.ModuleId}"; _pdfPreviewUrl = $"{GetPdfApiBase()}?moduleid={ModuleState.ModuleId}&id={_id}&authmoduleid={ModuleState.ModuleId}";
_showPdfModal = true; _showPdfModal = true;
} }
@@ -264,8 +268,8 @@ else
private async Task DownloadPdf() private async Task DownloadPdf()
{ {
var url = $"/api/HallOfFamePdf?moduleid={ModuleState.ModuleId}&download=true"; var url = $"{GetPdfApiBase()}?moduleid={ModuleState.ModuleId}&id={_id}&download=true&authmoduleid={ModuleState.ModuleId}";
await JSRuntime.InvokeVoidAsync("eval", $"var a = document.createElement('a'); a.href = '{url}'; a.download = 'HallOfFame.pdf'; document.body.appendChild(a); a.click(); document.body.removeChild(a);"); await JSRuntime.InvokeVoidAsync("eval", $"var a = document.createElement('a'); a.href = '{url}'; a.download = 'HallOfFame_{_item?.Name ?? "export"}.pdf'; document.body.appendChild(a); a.click(); document.body.removeChild(a);");
} }

View File

@@ -4,51 +4,76 @@
@using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Forms
@namespace SZUAbsolventenverein.Module.HallOfFame @namespace SZUAbsolventenverein.Module.HallOfFame
@implements IDisposable
@inherits ModuleBase @inherits ModuleBase
@inject IHallOfFameService HallOfFameService @inject IHallOfFameService HallOfFameService
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject IStringLocalizer<Edit> Localizer @inject IStringLocalizer<Edit> Localizer
@inject ISettingService SettingService
<form @ref="form" class="@(validated ? " was-validated" : "needs-validation" )" novalidate> <form @ref="form" class="@(validated ? " was-validated" : "needs-validation" )" novalidate>
<div class="container"> <div class="container">
<div class="row mb-3 align-items-center"> <div class="row mb-3 align-items-center">
<Label Class="col-sm-3 col-form-label" For="name" HelpText="Gib deinen Namen ein" ResourceKey="Name">Name: </Label> <div class="col-sm-3">
<Label For="name" HelpText="Gib deinen Namen ein" ResourceKey="Name">Name:</Label>
</div>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="name" class="form-control" @bind="@_name" required maxlength="120" /> <input id="name" class="form-control" @bind="@_name" required maxlength="120" />
<div class="invalid-feedback">Bitte gib einen Namen ein (max. 120 Zeichen).</div> <div class="invalid-feedback">Bitte gib einen Namen ein (max. 120 Zeichen).</div>
</div> </div>
</div> </div>
<div class="row mb-3 align-items-center"> <div class="row mb-3 align-items-center">
<Label Class="col-sm-3 col-form-label" For="year" HelpText="Jahrgang (z.B. 2020)" ResourceKey="Year">Jahrgang: </Label> <div class="col-sm-3">
<Label For="year" HelpText="Gib das Jahr ein, in dem du die Matura abgeschlossen hast (z.B. 2020)"
ResourceKey="Year">Jahrgang: </Label>
</div>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="year" type="number" class="form-control" @bind="@_year" required min="1900" max="@Int32.MaxValue" /> <input id="year" type="number" class="form-control" @bind="@_year" required min="1900"
max="@Int32.MaxValue" />
<div class="invalid-feedback">Bitte gib einen gültigen Jahrgang ein.</div> <div class="invalid-feedback">Bitte gib einen gültigen Jahrgang ein.</div>
</div> </div>
</div> </div>
<div class="row mb-3 align-items-center"> <div class="row mb-3">
<Label Class="col-sm-3 col-form-label" For="description" HelpText="Kurzbeschreibung / Werdegang" ResourceKey="Description">Beschreibung: </Label> <div class="col-sm-3">
<Label For="description" HelpText="Kurzbeschreibung / Werdegang" ResourceKey="Description">Beschreibung:
</Label>
</div>
<div class="col-sm-9"> <div class="col-sm-9">
<textarea id="description" class="form-control" @bind="@_description" required rows="5" maxlength="500"></textarea> @if (_descriptionLoaded)
<div class="text-muted small">@(_description?.Length ?? 0) / 500 Zeichen</div> {
<div class="invalid-feedback">Bitte gib eine Beschreibung ein.</div> <RichTextEditor Content="@_description" @ref="@_richTextEditorRef"
Placeholder="Beschreibe deinen Werdegang..."></RichTextEditor>
<div class="text-muted small mt-1">
Aktuell: <strong
class="@(_currentCharCount > _charLimit ? "text-danger" : "text-success")">@_currentCharCount</strong>
von maximal @_charLimit Zeichen.
</div>
}
</div> </div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<Label Class="col-sm-3 col-form-label" For="image" HelpText="Porträtfoto hochladen (JPG/PNG)" ResourceKey="Image">Foto: </Label> <div class="col-sm-3">
<Label For="image" HelpText="Porträtfoto hochladen (JPG/PNG)" ResourceKey="Image">Foto: </Label>
</div>
<div class="col-sm-9"> <div class="col-sm-9">
<div class="mb-2"> <div class="mb-2">
@if (!string.IsNullOrEmpty(_image)) @if (!string.IsNullOrEmpty(_image))
{ {
<div class="position-relative d-inline-block"> <div class="position-relative d-inline-block">
<img src="@_image" style="max-height: 150px; border-radius: 8px; margin-bottom: 10px; border: 1px solid #ddd;" alt="Vorschau" /> <img src="@_image"
<button type="button" class="btn btn-sm btn-danger" style="position: absolute; top: 5px; right: 5px;" @onclick="RemoveImage" title="Bild entfernen"> style="max-height: 150px; border-radius: 8px; margin-bottom: 10px; border: 1px solid #ddd;"
alt="Vorschau" />
<button type="button" class="btn btn-sm btn-danger"
style="position: absolute; top: 5px; right: 5px;" @onclick="RemoveImage"
title="Bild entfernen">
<i class="oi oi-x"></i> <i class="oi oi-x"></i>
</button> </button>
</div> </div>
} }
else else
{ {
<div class="bg-light d-flex align-items-center justify-content-center text-muted" style="height: 150px; width: 150px; border: 2px dashed #ddd; border-radius: 8px;"> <div class="bg-light d-flex align-items-center justify-content-center text-muted"
style="height: 150px; width: 150px; border: 2px dashed #ddd; border-radius: 8px;">
Kein Bild Kein Bild
</div> </div>
} }
@@ -65,24 +90,30 @@
</div> </div>
</div> </div>
<div class="row mb-3 align-items-center"> <div class="row mb-3 align-items-center">
<Label Class="col-sm-3 col-form-label" For="link" HelpText="Externer Link (optional)" ResourceKey="Link">Link: </Label> <div class="col-sm-3">
<Label For="link" HelpText="Externer Link (optional)" ResourceKey="Link">Link: </Label>
</div>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="link" type="url" class="form-control" @bind="@_link" placeholder="https://" /> <input id="link" type="url" class="form-control" @bind="@_link" placeholder="https://" />
<div class="invalid-feedback">Bitte gib eine gültige URL ein (startet mit http:// oder https://).</div> <div class="invalid-feedback">Bitte gib eine gültige URL ein (startet mit http:// oder https://).</div>
</div> </div>
</div> </div>
<div class="row mb-3 align-items-center"> <div class="row mb-3 align-items-center">
<Label Class="col-sm-3 col-form-label" For="status" HelpText="Status" ResourceKey="Status">Status: </Label> <div class="col-sm-3">
<Label For="status" HelpText="Status" ResourceKey="Status">Status: </Label>
</div>
<div class="col-sm-9"> <div class="col-sm-9">
<p>Aktuell: <strong>@(_status ?? "Neu")</strong></p> <p>Aktuell: <strong>@(_status ?? "Neu")</strong></p>
</div> </div>
</div> </div>
</div> </div>
<div class="mt-4 d-flex justify-content-between align-items-center"> <div class="mt-4 d-flex justify-content-between align-items-center">
<div> <div>
<button type="button" class="btn btn-secondary me-2" @onclick="@(() => Save("Draft"))" disabled="@_uploading">Als Entwurf speichern</button> <button type="button" class="btn btn-secondary me-2" @onclick="@(() => Save("Draft"))"
<button type="button" class="btn btn-primary" @onclick="@(() => Save("Published"))" disabled="@_uploading">Veröffentlichen</button> disabled="@_uploading">Als Entwurf speichern</button>
<button type="button" class="btn btn-primary" @onclick="@(() => Save("Published"))"
disabled="@_uploading">Veröffentlichen</button>
<NavLink class="btn btn-link ms-2" href="@NavigateUrl()">Abbrechen</NavLink> <NavLink class="btn btn-link ms-2" href="@NavigateUrl()">Abbrechen</NavLink>
</div> </div>
@if (PageState.Action == "Edit") @if (PageState.Action == "Edit")
@@ -96,25 +127,28 @@
<br /><br /> <br /><br />
@if (PageState.Action == "Edit") @if (PageState.Action == "Edit")
{ {
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon"></AuditInfo> <AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon">
</AuditInfo>
} }
</form> </form>
@code { @code {
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View; // Logic handles checking user own entry public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
public override string Actions => "Add,Edit"; public override string Actions => "Add,Edit";
public override string Title => "Hall of Fame Eintrag verwalten"; public override string Title => "Hall of Fame Eintrag verwalten";
public override List<Resource> Resources => new List<Resource>() public override List<Resource> Resources => new List<Resource>()
{ {
new Stylesheet("_content/SZUAbsolventenverein.Module.HallOfFame/Module.css") new Stylesheet("_content/SZUAbsolventenverein.Module.HallOfFame/Module.css")
}; };
private ElementReference form; private ElementReference form;
private bool validated = false; private bool validated = false;
private bool _uploading = false; private bool _uploading = false;
private RichTextEditor _richTextEditorRef;
private bool _descriptionLoaded = false;
private int _id; private int _id;
private string _name; private string _name;
@@ -128,22 +162,56 @@
private DateTime _createdon; private DateTime _createdon;
private string _modifiedby; private string _modifiedby;
private DateTime _modifiedon; private DateTime _modifiedon;
private int _charLimit = 500;
private int _currentCharCount = 0;
private System.Timers.Timer _timer;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
_timer = new System.Timers.Timer(1000); // 1 sekunde
_timer.Elapsed += async (s, e) =>
{
if (_richTextEditorRef != null && _descriptionLoaded)
{
try
{
var html = await _richTextEditorRef.GetHtml();
var plainText = System.Text.RegularExpressions.Regex.Replace(html ?? "", "<.*?>", String.Empty);
plainText = System.Net.WebUtility.HtmlDecode(plainText);
if (_currentCharCount != plainText.Length)
{
_currentCharCount = plainText.Length;
await InvokeAsync(StateHasChanged);
}
}
catch { } // Ignore interop errors during disposal
}
};
_timer.Start();
try try
{ {
// Load character limit setting
var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId);
var charLimitStr = SettingService.GetSetting(settings, "CharLimit", "500");
if (int.TryParse(charLimitStr, out var parsed) && parsed >= 4 && parsed <= 32000)
{
_charLimit = parsed;
}
_descriptionLoaded = false;
if (PageState.Action == "Edit") if (PageState.Action == "Edit")
{ {
_id = Int32.Parse(PageState.QueryString["id"]); _id = Int32.Parse(PageState.QueryString["id"]);
HallOfFame HallOfFame = await HallOfFameService.GetHallOfFameAsync(_id, ModuleState.ModuleId); HallOfFame HallOfFame = await HallOfFameService.GetHallOfFameAsync(_id, ModuleState.ModuleId);
if (HallOfFame != null) if (HallOfFame != null)
{ {
if (HallOfFame.UserId != PageState.User.UserId) if (HallOfFame.UserId != PageState.User.UserId)
{ {
NavigationManager.NavigateTo(NavigateUrl()); NavigationManager.NavigateTo(NavigateUrl());
return; return;
} }
_name = HallOfFame.Name; _name = HallOfFame.Name;
@@ -152,7 +220,7 @@
_image = HallOfFame.Image; _image = HallOfFame.Image;
_link = HallOfFame.Link; _link = HallOfFame.Link;
_status = HallOfFame.Status; _status = HallOfFame.Status;
_createdby = HallOfFame.CreatedBy; _createdby = HallOfFame.CreatedBy;
_createdon = HallOfFame.CreatedOn; _createdon = HallOfFame.CreatedOn;
_modifiedby = HallOfFame.ModifiedBy; _modifiedby = HallOfFame.ModifiedBy;
@@ -161,12 +229,13 @@
} }
else // Add Mode else // Add Mode
{ {
var existing = await HallOfFameService.GetHallOfFameByUserIdAsync(PageState.User.UserId, ModuleState.ModuleId); var existing = await HallOfFameService.GetHallOfFameByUserIdAsync(PageState.User.UserId, ModuleState.ModuleId);
if (existing != null) if (existing != null)
{ {
NavigationManager.NavigateTo(EditUrl(existing.HallOfFameId.ToString())); NavigationManager.NavigateTo(EditUrl(existing.HallOfFameId.ToString()));
} }
} }
_descriptionLoaded = true;
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -223,10 +292,27 @@
{ {
try try
{ {
ClearModuleMessage();
validated = true; validated = true;
// Get the HTML content from the rich text editor
if (_richTextEditorRef != null)
{
_description = await _richTextEditorRef.GetHtml();
}
var interop = new Oqtane.UI.Interop(JSRuntime); var interop = new Oqtane.UI.Interop(JSRuntime);
if (await interop.FormValid(form)) if (await interop.FormValid(form))
{ {
// Custom character limit validation for rich text
var plainText = System.Text.RegularExpressions.Regex.Replace(_description ?? "", "<.*?>", String.Empty);
plainText = System.Net.WebUtility.HtmlDecode(plainText);
if (plainText.Length > _charLimit)
{
AddModuleMessage($"Fehler: Die Beschreibung ist zu lang (Aktuell {plainText.Length}, Maximal {_charLimit} Zeichen).",
MessageType.Warning);
return;
}
_status = status; _status = status;
if (PageState.Action == "Add") if (PageState.Action == "Add")
@@ -247,7 +333,7 @@
else else
{ {
HallOfFame HallOfFame = await HallOfFameService.GetHallOfFameAsync(_id, ModuleState.ModuleId); HallOfFame HallOfFame = await HallOfFameService.GetHallOfFameAsync(_id, ModuleState.ModuleId);
if (HallOfFame.UserId == PageState.User.UserId) if (HallOfFame.UserId == PageState.User.UserId)
{ {
HallOfFame.Name = _name; HallOfFame.Name = _name;
HallOfFame.Year = _year; HallOfFame.Year = _year;
@@ -255,7 +341,7 @@
HallOfFame.Image = _image; HallOfFame.Image = _image;
HallOfFame.Link = _link; HallOfFame.Link = _link;
HallOfFame.Status = _status; HallOfFame.Status = _status;
await HallOfFameService.UpdateHallOfFameAsync(HallOfFame); await HallOfFameService.UpdateHallOfFameAsync(HallOfFame);
await logger.LogInformation("HallOfFame Updated {HallOfFame}", HallOfFame); await logger.LogInformation("HallOfFame Updated {HallOfFame}", HallOfFame);
} }
@@ -287,4 +373,10 @@
AddModuleMessage("Fehler beim Löschen des Eintrags.", MessageType.Error); AddModuleMessage("Fehler beim Löschen des Eintrags.", MessageType.Error);
} }
} }
public void Dispose()
{
_timer?.Stop();
_timer?.Dispose();
}
} }

View File

@@ -8,6 +8,7 @@
@inject IHallOfFameService HallOfFameService @inject IHallOfFameService HallOfFameService
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject IStringLocalizer<Index> Localizer @inject IStringLocalizer<Index> Localizer
@inject ISettingService SettingService
@if (_HallOfFames == null) @if (_HallOfFames == null)
{ {
@@ -16,53 +17,79 @@
else else
{ {
<div class="row mb-4 align-items-center"> <div class="row mb-4 align-items-center">
<div class="col-md-6"> <div class="col-md-6">
<div class="input-group"> <div class="input-group">
<span class="input-group-text"><i class="oi oi-magnifying-glass"></i></span> <span class="input-group-text"><i class="oi oi-magnifying-glass"></i></span>
<input type="text" class="form-control" placeholder="Suchen nach Namen oder Begriffen..." @bind="_searchText" @bind:event="oninput" /> <input type="text" class="form-control" placeholder="Suchen nach Namen oder Begriffen..."
</div> @bind="_searchText" @bind:event="oninput" />
</div> </div>
<div class="col-md-3"> </div>
<div class="input-group"> <div class="col-md-3">
<select class="form-select" @bind="_sortOption"> <div class="input-group">
<option value="CreatedOn">Datum</option> <select class="form-select" @bind="_sortOption">
<option value="Name">Name</option> <option value="CreatedOn">Datum</option>
<option value="Year">Jahrgang</option> <option value="Name">Name</option>
</select> <option value="Year">Jahrgang</option>
<button class="btn btn-outline-secondary" type="button" @onclick="ToggleSortDirection" title="@(_sortAscending ? "Aufsteigend" : "Absteigend")"> </select>
<i class="oi @(_sortAscending ? "oi-arrow-top" : "oi-arrow-bottom")"></i> <button class="btn btn-outline-secondary" type="button" @onclick="ToggleSortDirection"
</button> title="@(_sortAscending ? "Aufsteigend" : "Absteigend")">
</div> <i class="oi @(_sortAscending ? "oi-arrow-top" : "oi-arrow-bottom")"></i>
</button>
</div> </div>
<div class="col-md-3 text-end"> </div>
@if (PageState.User != null) <div class="col-md-3 text-end">
@if (PageState.User != null)
{ {
if (_myEntry != null) if (_myEntry != null)
{ {
<ActionLink Action="Edit" Parameters="@($"id=" + _myEntry.HallOfFameId.ToString())" Text="Mein Eintrag" /> <ActionLink Action="Edit" Parameters="@($"id=" + _myEntry.HallOfFameId.ToString())" Text="Mein Eintrag" />
} }
else else
{ {
<ActionLink Action="Add" Text="Eintragen" /> <ActionLink Action="Add" Text="Eintragen" />
} }
} }
</div> </div>
</div> </div>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin + ";" + RoleNames.Host))
{
<div class="row mb-3 align-items-center">
<div class="col-md-8">
<div class="d-flex align-items-center gap-2">
<label class="form-label mb-0 text-nowrap" style="font-size: 0.85rem;"><i
class="oi oi-pencil me-1"></i>Zeichenlimit:</label>
<input type="range" class="form-range flex-grow-1" min="4" max="32000" step="1" value="@_charLimit"
@oninput="OnCharLimitChanged" style="min-width: 200px;" />
<input type="number" class="form-control form-control-sm" style="width: 90px;" min="4" max="32000"
value="@_charLimit" @onchange="OnCharLimitInputChanged" />
<button class="btn btn-sm btn-outline-primary" @onclick="SaveCharLimit" title="Zeichenlimit speichern">
<i class="oi oi-check"></i>
</button>
</div>
</div>
<div class="col-md-4 text-muted" style="font-size: 0.8rem;">
Maximale Zeichen für Beschreibungen
</div>
</div>
}
@if (@_HallOfFames.Count != 0) @if (@_HallOfFames.Count != 0)
{ {
<div class="row"> <div class="row">
@foreach (var item in FilteredHallOfFames) @foreach (var item in FilteredHallOfFames)
{ {
<div class="col-md-4 mb-3"> <div class="col-md-4 mb-3">
<div class="card h-100"> <div class="card h-100">
@if (!string.IsNullOrEmpty(item.Image)) @if (!string.IsNullOrEmpty(item.Image))
{ {
<img src="@item.Image" class="card-img-top" alt="@item.Name" style="max-height: 200px; object-fit: cover;"> <img src="@item.Image" class="card-img-top" alt="@item.Name"
style="height: 300px; object-fit: cover; object-position: top;">
} }
<div class="card-body d-flex flex-column"> <div class="card-body d-flex flex-column">
<h5 class="card-title">@item.Name (@item.Year)</h5> <h5 class="card-title">@item.Name (@item.Year)</h5>
@if (item.IsReported && UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin + ";" + RoleNames.Host)) @if (item.IsReported && UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin + ";" +
RoleNames.Host))
{ {
<div class="alert alert-danger p-1 mb-2" style="font-size: 0.8rem;"> <div class="alert alert-danger p-1 mb-2" style="font-size: 0.8rem;">
<i class="oi oi-warning me-1"></i> <strong>Dieser Eintrag wurde gemeldet!</strong> <i class="oi oi-warning me-1"></i> <strong>Dieser Eintrag wurde gemeldet!</strong>
@@ -75,17 +102,16 @@ else
flex-grow: 1; flex-grow: 1;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
"> ">
@foreach (var line in (item.Description?.Replace("\t", " ").Split('\n') ?? Array.Empty<string>())) @((MarkupString)(item.Description ?? ""))
{
<div class="hof-description-line">@line</div>
}
</div> </div>
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<ActionLink Action="Details" Parameters="@($"id=" + item.HallOfFameId.ToString())" Class="btn btn-sm btn-outline-primary" Text="Details ansehen" /> <ActionLink Action="Details" Parameters="@($"id=" + item.HallOfFameId.ToString())"
Class="btn btn-sm btn-outline-primary" Text="Details ansehen" />
<div> <div>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin + ";" + RoleNames.Host)) @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin + ";" + RoleNames.Host))
{ {
<button class="btn btn-sm btn-outline-danger me-1" @onclick="@(() => Delete(item.HallOfFameId))" title="Löschen"><i class="oi oi-trash"></i></button> <button class="btn btn-sm btn-outline-danger me-1" @onclick="@(() => Delete(item.HallOfFameId))"
title="Löschen"><i class="oi oi-trash"></i></button>
} }
</div> </div>
</div> </div>
@@ -98,24 +124,25 @@ else
else else
{ {
<div class="alert alert-info"> <div class="alert alert-info">
Es sind noch keine Hall-of-Fame-Einträge veröffentlicht. Es sind noch keine Hall-of-Fame-Einträge veröffentlicht.
</div> </div>
} }
} }
@code { @code {
public override List<Resource> Resources => new List<Resource>() public override List<Resource> Resources => new List<Resource>()
{ {
new Stylesheet("_content/SZUAbsolventenverein.Module.HallOfFame/Module.css"), new Stylesheet("_content/SZUAbsolventenverein.Module.HallOfFame/Module.css"),
new Script("_content/SZUAbsolventenverein.Module.HallOfFame/Module.js") new Script("_content/SZUAbsolventenverein.Module.HallOfFame/Module.js")
}; };
List<HallOfFame> _HallOfFames; List<HallOfFame> _HallOfFames;
HallOfFame _myEntry; HallOfFame _myEntry;
string _searchText = ""; string _searchText = "";
string _sortOption = "CreatedOn"; string _sortOption = "CreatedOn";
bool _sortAscending = false; bool _sortAscending = false;
int _charLimit = 500;
IEnumerable<HallOfFame> FilteredHallOfFames IEnumerable<HallOfFame> FilteredHallOfFames
{ {
@@ -124,19 +151,19 @@ else
var items = _HallOfFames.AsEnumerable(); var items = _HallOfFames.AsEnumerable();
if (!string.IsNullOrEmpty(_searchText)) if (!string.IsNullOrEmpty(_searchText))
{ {
items = items.Where(i => items = items.Where(i =>
(i.Name?.Contains(_searchText, StringComparison.OrdinalIgnoreCase) ?? false) || (i.Name?.Contains(_searchText, StringComparison.OrdinalIgnoreCase) ?? false) ||
(i.Description?.Contains(_searchText, StringComparison.OrdinalIgnoreCase) ?? false) (i.Description?.Contains(_searchText, StringComparison.OrdinalIgnoreCase) ?? false)
); );
} }
items = _sortOption switch items = _sortOption switch
{ {
"Name" => _sortAscending ? items.OrderBy(i => i.Name) : items.OrderByDescending(i => i.Name), "Name" => _sortAscending ? items.OrderBy(i => i.Name) : items.OrderByDescending(i => i.Name),
"Year" => _sortAscending ? items.OrderBy(i => i.Year) : items.OrderByDescending(i => i.Year), "Year" => _sortAscending ? items.OrderBy(i => i.Year) : items.OrderByDescending(i => i.Year),
"CreatedOn" or _ => _sortAscending ? items.OrderBy(i => i.CreatedOn) : items.OrderByDescending(i => i.CreatedOn) "CreatedOn" or _ => _sortAscending ? items.OrderBy(i => i.CreatedOn) : items.OrderByDescending(i => i.CreatedOn)
}; };
return items; return items;
} }
} }
@@ -165,11 +192,19 @@ else
{ {
var allEntries = await HallOfFameService.GetHallOfFamesAsync(ModuleState.ModuleId); var allEntries = await HallOfFameService.GetHallOfFamesAsync(ModuleState.ModuleId);
_HallOfFames = allEntries.Where(i => i.Status == "Published").ToList(); _HallOfFames = allEntries.Where(i => i.Status == "Published").ToList();
if (PageState.User != null) if (PageState.User != null)
{ {
_myEntry = await HallOfFameService.GetHallOfFameByUserIdAsync(PageState.User.UserId, ModuleState.ModuleId); _myEntry = await HallOfFameService.GetHallOfFameByUserIdAsync(PageState.User.UserId, ModuleState.ModuleId);
} }
// Load character limit setting
var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId);
var charLimitStr = SettingService.GetSetting(settings, "CharLimit", "500");
if (int.TryParse(charLimitStr, out var parsed) && parsed >= 4 && parsed <= 32000)
{
_charLimit = parsed;
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -178,6 +213,38 @@ else
} }
} }
private void OnCharLimitChanged(ChangeEventArgs e)
{
if (int.TryParse(e.Value?.ToString(), out var val) && val >= 4 && val <= 32000)
{
_charLimit = val;
}
}
private void OnCharLimitInputChanged(ChangeEventArgs e)
{
if (int.TryParse(e.Value?.ToString(), out var val))
{
_charLimit = Math.Clamp(val, 4, 32000);
}
}
private async Task SaveCharLimit()
{
try
{
var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId);
SettingService.SetSetting(settings, "CharLimit", _charLimit.ToString());
await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId);
AddModuleMessage($"Zeichenlimit auf {_charLimit} gesetzt.", MessageType.Success);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving CharLimit {Error}", ex.Message);
AddModuleMessage("Fehler beim Speichern des Zeichenlimits.", MessageType.Error);
}
}
private async Task Delete(int hallOfFameId) private async Task Delete(int hallOfFameId)
{ {
try try

View File

@@ -10,4 +10,4 @@ cp -f "../Server/bin/Debug/$TargetFramework/$ProjectName.Server.Oqtane.pdb" "../
cp -f "../Shared/bin/Debug/$TargetFramework/$ProjectName.Shared.Oqtane.dll" "../../oqtane.framework/Oqtane.Server/bin/Debug/$TargetFramework/" cp -f "../Shared/bin/Debug/$TargetFramework/$ProjectName.Shared.Oqtane.dll" "../../oqtane.framework/Oqtane.Server/bin/Debug/$TargetFramework/"
cp -f "../Shared/bin/Debug/$TargetFramework/$ProjectName.Shared.Oqtane.pdb" "../../oqtane.framework/Oqtane.Server/bin/Debug/$TargetFramework/" cp -f "../Shared/bin/Debug/$TargetFramework/$ProjectName.Shared.Oqtane.pdb" "../../oqtane.framework/Oqtane.Server/bin/Debug/$TargetFramework/"
cp -rf "../Server/wwwroot/"* "../../oqtane.framework/Oqtane.Server/wwwroot/_content/$ProjectName/" cp -rf "../Server/wwwroot/"* "../../oqtane.framework/Oqtane.Server/wwwroot/"

View File

@@ -1,85 +0,0 @@
# PDF-Design: Modern Glassmorphism Layout
## Design-Konzept
Das PDF-Layout verwendet ein **modernes Glassmorphism-Design** — inspiriert von Apple Keynote-Slides und Behance Case Studies. Jede Seite besteht aus einem vollflächigen Hintergrundbild mit zwei **schwebenden Glass-Cards** (Titel oben, Beschreibung unten).
## Aufbau (Layer-System)
```
┌─────────────────────────────────┐
│ LAYER 1: Vollbild-Hintergrund │ ← Edge-to-Edge Bild
│ │
│ LAYER 2: Oberes Dark-Overlay │ ← Kontrast für Titelkarte
│ ┌───────────────────────────┐ │
│ │ ███ TITELKARTE (Glass) ███ │ ← Name + Jahr
│ └───────────────────────────┘ │
│ │
│ (Bild sichtbar) │
│ │
│ LAYER 3: Unteres Dark-Overlay │ ← Kontrast für Beschreibung
│ ┌───────────────────────────┐ │
│ │ ███ BESCHREIBUNG (Glass) ██ │ ← Bio-Text
│ └───────────────────────────┘ │
└─────────────────────────────────┘
```
## Glassmorphism-Technik (Build-Safe)
Jede Karte nutzt die `Decoration` API mit mehrfachen `.Before()` Layern:
```csharp
.Decoration(card =>
{
// 1. Weicher Schatten (sehr dezent, außen)
card.Before()
.Border(4f).BorderColor("#18000000")
.CornerRadius(20);
// 2. Lichtkante (innerer Glas-Rim)
card.Before()
.Border(1f).BorderColor("#44FFFFFF")
.CornerRadius(20);
// 3. Halbtransparenter dunkler Hintergrund
card.Before()
.Background("#C8181828")
.CornerRadius(20);
// 4. Inhalt (Text)
card.Content()
.PaddingVertical(28)
.PaddingHorizontal(36)
.Column(inner => { /* ... */ });
});
```
## Design-Details
| Element | Stil |
|---------|------|
| **Name** | 36pt, ExtraBold, Uppercase, Weiß, Letter-Spacing 0.5 |
| **Trennlinie** | 1.5pt, halbtransparent Weiß (#55FFFFFF) |
| **Jahrgang** | 15pt, gedämpft (#CCFFFFFF), Letter-Spacing 1.5 |
| **Beschreibungs-Header** | 14pt, SemiBold, gedämpft, Letter-Spacing 2 |
| **Beschreibungstext** | 11pt, 1.5 Zeilenhöhe, leicht gedämpft (#E8FFFFFF) |
| **Titelkarte Radius** | 20px |
| **Beschreibungskarte Radius** | 16px |
| **Overlay oben** | 220pt Höhe, #99000000 |
| **Overlay unten** | 280pt Höhe, #AA000000 |
| **Seiten-Padding** | 40pt rundum |
## Vorteile dieses Ansatzes
-**100% Build-Safe**: Keine SkiaSharp-Abhängigkeit, rein QuestPDF Fluent API
-**Stabil auf jedem Bild**: Kontrast-Overlays garantieren Lesbarkeit
-**Visuell hochwertig**: Glasskarte + Tiefe + Typografie-Hierarchie
-**Strukturell unverändert**: Name oben, Beschreibung unten, Bild vollflächig
## Build-Befehl
```bash
dotnet build Server/SZUAbsolventenverein.Module.HallOfFame.Server.csproj
```
Letzte erfolgreiche Kompilierung: 19. Februar 2026 — 0 Fehler, 0 Warnungen.

View File

@@ -63,7 +63,7 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Controllers
return HallOfFame; return HallOfFame;
} }
else else
{ {
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized HallOfFame Get Attempt {HallOfFameId} {ModuleId}", id, moduleid); _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized HallOfFame Get Attempt {HallOfFameId} {ModuleId}", id, moduleid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null; return null;
@@ -76,7 +76,7 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Controllers
public async Task<Models.HallOfFame> GetByUserId(int userid, string moduleid) public async Task<Models.HallOfFame> GetByUserId(int userid, string moduleid)
{ {
int ModuleId; int ModuleId;
if (int.TryParse(moduleid, out ModuleId) && IsAuthorizedEntityId(EntityNames.Module, ModuleId)) if (int.TryParse(moduleid, out ModuleId) && IsAuthorizedEntityId(EntityNames.Module, ModuleId))
{ {
var list = await _HallOfFameService.GetHallOfFamesAsync(ModuleId); var list = await _HallOfFameService.GetHallOfFamesAsync(ModuleId);
return list.FirstOrDefault(item => item.UserId == userid); return list.FirstOrDefault(item => item.UserId == userid);
@@ -91,7 +91,7 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Controllers
// POST api/<controller> // POST api/<controller>
[HttpPost] [HttpPost]
[Authorize(Policy = PolicyNames.EditModule)] [Authorize(Policy = PolicyNames.ViewModule)]
public async Task<Models.HallOfFame> Post([FromBody] Models.HallOfFame HallOfFame) public async Task<Models.HallOfFame> Post([FromBody] Models.HallOfFame HallOfFame)
{ {
if (ModelState.IsValid && IsAuthorizedEntityId(EntityNames.Module, HallOfFame.ModuleId)) if (ModelState.IsValid && IsAuthorizedEntityId(EntityNames.Module, HallOfFame.ModuleId))
@@ -101,8 +101,8 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Controllers
if (allEntries.Any(e => e.UserId == HallOfFame.UserId)) if (allEntries.Any(e => e.UserId == HallOfFame.UserId))
{ {
_logger.Log(LogLevel.Error, this, LogFunction.Security, "User {UserId} already has a Hall of Fame entry.", HallOfFame.UserId); _logger.Log(LogLevel.Error, this, LogFunction.Security, "User {UserId} already has a Hall of Fame entry.", HallOfFame.UserId);
HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
return null; return null;
} }
HallOfFame = await _HallOfFameService.AddHallOfFameAsync(HallOfFame); HallOfFame = await _HallOfFameService.AddHallOfFameAsync(HallOfFame);
@@ -118,7 +118,7 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Controllers
// PUT api/<controller>/5 // PUT api/<controller>/5
[HttpPut("{id}")] [HttpPut("{id}")]
[Authorize(Policy = PolicyNames.EditModule)] [Authorize(Policy = PolicyNames.ViewModule)]
public async Task<Models.HallOfFame> Put(int id, [FromBody] Models.HallOfFame HallOfFame) public async Task<Models.HallOfFame> Put(int id, [FromBody] Models.HallOfFame HallOfFame)
{ {
if (ModelState.IsValid && HallOfFame.HallOfFameId == id && IsAuthorizedEntityId(EntityNames.Module, HallOfFame.ModuleId)) if (ModelState.IsValid && HallOfFame.HallOfFameId == id && IsAuthorizedEntityId(EntityNames.Module, HallOfFame.ModuleId))
@@ -126,13 +126,13 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Controllers
var existing = await _HallOfFameService.GetHallOfFameAsync(id, HallOfFame.ModuleId); var existing = await _HallOfFameService.GetHallOfFameAsync(id, HallOfFame.ModuleId);
if (existing != null && existing.UserId == HallOfFame.UserId) if (existing != null && existing.UserId == HallOfFame.UserId)
{ {
HallOfFame = await _HallOfFameService.UpdateHallOfFameAsync(HallOfFame); HallOfFame = await _HallOfFameService.UpdateHallOfFameAsync(HallOfFame);
} }
else else
{ {
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized HallOfFame Put Attempt by User {UserId} for Entry {HallOfFameId}", HallOfFame.UserId, id); _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized HallOfFame Put Attempt by User {UserId} for Entry {HallOfFameId}", HallOfFame.UserId, id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
HallOfFame = null; HallOfFame = null;
} }
} }
else else
@@ -201,7 +201,7 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Controllers
} }
} }
[HttpPost("upload")] [HttpPost("upload")]
[Authorize(Policy = PolicyNames.EditModule)] [Authorize(Policy = PolicyNames.ViewModule)]
public async Task<IActionResult> Upload(IFormFile file) public async Task<IActionResult> Upload(IFormFile file)
{ {
if (file == null || file.Length == 0) return BadRequest("Keine Datei ausgewählt."); if (file == null || file.Length == 0) return BadRequest("Keine Datei ausgewählt.");

View File

@@ -8,7 +8,10 @@ using Oqtane.Infrastructure;
using Oqtane.Controllers; using Oqtane.Controllers;
using SZUAbsolventenverein.Module.HallOfFame.Services; using SZUAbsolventenverein.Module.HallOfFame.Services;
using System.Linq; using System.Linq;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Text.RegularExpressions;
using System.Net;
using QuestPDF.Fluent; using QuestPDF.Fluent;
using QuestPDF.Helpers; using QuestPDF.Helpers;
using QuestPDF.Infrastructure; using QuestPDF.Infrastructure;
@@ -27,10 +30,10 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Controllers
_environment = environment; _environment = environment;
} }
// GET: api/<controller>?moduleid=x&download=true/false // GET: api/<controller>?moduleid=x&download=true/false&id=y
[HttpGet] [HttpGet]
[Authorize(Policy = PolicyNames.ViewModule)] [Authorize(Policy = PolicyNames.ViewModule)]
public async Task<IActionResult> Get(string moduleid, bool download = false) public async Task<IActionResult> Get(string moduleid, bool download = false, int? id = null)
{ {
int ModuleId; int ModuleId;
if (int.TryParse(moduleid, out ModuleId) && IsAuthorizedEntityId(EntityNames.Module, ModuleId)) if (int.TryParse(moduleid, out ModuleId) && IsAuthorizedEntityId(EntityNames.Module, ModuleId))
@@ -38,6 +41,16 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Controllers
var entries = await _hallOfFameService.GetHallOfFamesAsync(ModuleId); var entries = await _hallOfFameService.GetHallOfFamesAsync(ModuleId);
var publishedEntries = entries.Where(e => e.Status == "Published").ToList(); var publishedEntries = entries.Where(e => e.Status == "Published").ToList();
// If a specific entry ID is provided, filter to just that entry
if (id.HasValue)
{
publishedEntries = publishedEntries.Where(e => e.HallOfFameId == id.Value).ToList();
if (!publishedEntries.Any())
{
return NotFound();
}
}
var document = Document.Create(container => var document = Document.Create(container =>
{ {
foreach (var entry in publishedEntries) foreach (var entry in publishedEntries)
@@ -116,11 +129,7 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Controllers
}); });
// ═══ BESCHREIBUNGSKARTE (unten) ═══ // ═══ BESCHREIBUNGSKARTE (unten) ═══
var description = entry.Description ?? ""; var sections = ConvertHtmlToLines(entry.Description ?? "");
var sections = description.Split('\n')
.Select(s => s.Trim())
.Where(s => !string.IsNullOrEmpty(s))
.ToArray();
column.Item().ExtendVertical().AlignBottom() column.Item().ExtendVertical().AlignBottom()
.Border(5f).BorderColor("#20000000") .Border(5f).BorderColor("#20000000")
@@ -134,7 +143,7 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Controllers
.PaddingHorizontal(32) .PaddingHorizontal(32)
.Column(descColumn => .Column(descColumn =>
{ {
if (sections.Length > 0) if (sections.Count > 0)
{ {
// Überschrift // Überschrift
descColumn.Item() descColumn.Item()
@@ -184,5 +193,79 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Controllers
return Forbid(); return Forbid();
} }
} }
/// <summary>
/// Converts HTML content (from Quill.js rich text editor) to a list of plain text lines
/// suitable for rendering in QuestPDF.
/// Handles: ol/ul lists → numbered/bulleted lines, p → paragraphs, br → newlines,
/// strips all other tags and decodes HTML entities.
/// </summary>
private static List<string> ConvertHtmlToLines(string html)
{
if (string.IsNullOrWhiteSpace(html))
return new List<string>();
var lines = new List<string>();
// Process ordered lists: <ol>...</ol>
html = Regex.Replace(html, @"<ol[^>]*>(.*?)</ol>", match =>
{
var listContent = match.Groups[1].Value;
var items = Regex.Matches(listContent, @"<li[^>]*>(.*?)</li>", RegexOptions.Singleline);
int counter = 1;
var result = "";
foreach (Match item in items)
{
var text = StripHtmlTags(item.Groups[1].Value).Trim();
if (!string.IsNullOrEmpty(text))
result += $"\n{counter}. {text}";
counter++;
}
return result;
}, RegexOptions.Singleline | RegexOptions.IgnoreCase);
// Process unordered lists: <ul>...</ul>
html = Regex.Replace(html, @"<ul[^>]*>(.*?)</ul>", match =>
{
var listContent = match.Groups[1].Value;
var items = Regex.Matches(listContent, @"<li[^>]*>(.*?)</li>", RegexOptions.Singleline);
var result = "";
foreach (Match item in items)
{
var text = StripHtmlTags(item.Groups[1].Value).Trim();
if (!string.IsNullOrEmpty(text))
result += $"\n• {text}";
}
return result;
}, RegexOptions.Singleline | RegexOptions.IgnoreCase);
// Replace <br>, <br/>, <br /> with newline
html = Regex.Replace(html, @"<br\s*/?>", "\n", RegexOptions.IgnoreCase);
// Replace </p>, </div>, </h1>-</h6> with newline
html = Regex.Replace(html, @"</(?:p|div|h[1-6])>", "\n", RegexOptions.IgnoreCase);
// Strip all remaining HTML tags
html = StripHtmlTags(html);
// Decode HTML entities (&nbsp; &amp; etc.)
html = WebUtility.HtmlDecode(html);
// Split into lines, trim, filter empty
var rawLines = html.Split('\n')
.Select(s => s.Trim())
.Where(s => !string.IsNullOrEmpty(s))
.ToList();
return rawLines;
}
/// <summary>
/// Strips all HTML tags from a string, leaving only the text content.
/// </summary>
private static string StripHtmlTags(string html)
{
return Regex.Replace(html, @"<[^>]+>", "");
}
} }
} }

View File

@@ -76,7 +76,7 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Services
public Task<Models.HallOfFame> AddHallOfFameAsync(Models.HallOfFame HallOfFame) public Task<Models.HallOfFame> AddHallOfFameAsync(Models.HallOfFame HallOfFame)
{ {
if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, HallOfFame.ModuleId, PermissionNames.Edit)) if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, HallOfFame.ModuleId, PermissionNames.View))
{ {
HallOfFame = _HallOfFameRepository.AddHallOfFame(HallOfFame); HallOfFame = _HallOfFameRepository.AddHallOfFame(HallOfFame);
_logger.Log(LogLevel.Information, this, LogFunction.Create, "HallOfFame Added {HallOfFame}", HallOfFame); _logger.Log(LogLevel.Information, this, LogFunction.Create, "HallOfFame Added {HallOfFame}", HallOfFame);
@@ -91,7 +91,7 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Services
public Task<Models.HallOfFame> UpdateHallOfFameAsync(Models.HallOfFame HallOfFame) public Task<Models.HallOfFame> UpdateHallOfFameAsync(Models.HallOfFame HallOfFame)
{ {
if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, HallOfFame.ModuleId, PermissionNames.Edit)) if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, HallOfFame.ModuleId, PermissionNames.View))
{ {
HallOfFame = _HallOfFameRepository.UpdateHallOfFame(HallOfFame); HallOfFame = _HallOfFameRepository.UpdateHallOfFame(HallOfFame);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "HallOfFame Updated {HallOfFame}", HallOfFame); _logger.Log(LogLevel.Information, this, LogFunction.Update, "HallOfFame Updated {HallOfFame}", HallOfFame);
@@ -189,7 +189,7 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Services
} }
public async Task<string> UploadFileAsync(Stream stream, string fileName, int ModuleId) public async Task<string> UploadFileAsync(Stream stream, string fileName, int ModuleId)
{ {
if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, ModuleId, PermissionNames.Edit)) if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, ModuleId, PermissionNames.View))
{ {
var extension = Path.GetExtension(fileName).ToLower(); var extension = Path.GetExtension(fileName).ToLower();