Feature: Hall of Fame Module Implementation (1.0.1)

- Added Hall of Fame module logic (Models, Controller, Service).
- Implemented 'One Entry Per User' and 'Publish/Draft' workflow.
- Updated UI to Grid Layout (Index.razor) and Unified Form (Edit.razor).
- Added Database Migration 01000001 for new columns.
- Bumped version to 1.0.1.
This commit is contained in:
Adam Gaiswinkler
2026-01-15 00:01:55 +01:00
parent 5dfa690432
commit 7114904412
12 changed files with 378 additions and 55 deletions

View File

@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Oqtane.Shared;
using Oqtane.Enums;
@@ -22,6 +23,7 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Controllers
_HallOfFameService = HallOfFameService;
}
// GET: api/<controller>?moduleid=x
// GET: api/<controller>?moduleid=x
[HttpGet]
[Authorize(Policy = PolicyNames.ViewModule)]
@@ -30,7 +32,11 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Controllers
int ModuleId;
if (int.TryParse(moduleid, out ModuleId) && IsAuthorizedEntityId(EntityNames.Module, ModuleId))
{
return await _HallOfFameService.GetHallOfFamesAsync(ModuleId);
var list = await _HallOfFameService.GetHallOfFamesAsync(ModuleId);
// Filter: Show only Published unless user has Edit permissions (simplified check for now, can be expanded)
// For now, let's filter in memory or service. The requirement says: "Hauptseite zeigt nur Published".
// We will filter here.
return list.Where(item => item.Status == "Published");
}
else
{
@@ -58,6 +64,25 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Controllers
}
}
// GET api/<controller>/user/5?moduleid=x
[HttpGet("user/{userid}")]
[Authorize(Policy = PolicyNames.ViewModule)]
public async Task<Models.HallOfFame> GetByUserId(int userid, string moduleid)
{
int ModuleId;
if (int.TryParse(moduleid, out ModuleId) && IsAuthorizedEntityId(EntityNames.Module, ModuleId))
{
var list = await _HallOfFameService.GetHallOfFamesAsync(ModuleId);
return list.FirstOrDefault(item => item.UserId == userid);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized HallOfFame GetByUserId Attempt {UserId} {ModuleId}", userid, moduleid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
}
// POST api/<controller>
[HttpPost]
[Authorize(Policy = PolicyNames.EditModule)]
@@ -65,6 +90,15 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Controllers
{
if (ModelState.IsValid && IsAuthorizedEntityId(EntityNames.Module, HallOfFame.ModuleId))
{
// Enforce one entry per user
var allEntries = await _HallOfFameService.GetHallOfFamesAsync(HallOfFame.ModuleId);
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);
HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
return null;
}
HallOfFame = await _HallOfFameService.AddHallOfFameAsync(HallOfFame);
}
else
@@ -83,7 +117,17 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Controllers
{
if (ModelState.IsValid && HallOfFame.HallOfFameId == id && IsAuthorizedEntityId(EntityNames.Module, HallOfFame.ModuleId))
{
HallOfFame = await _HallOfFameService.UpdateHallOfFameAsync(HallOfFame);
var existing = await _HallOfFameService.GetHallOfFameAsync(id, HallOfFame.ModuleId);
if (existing != null && existing.UserId == HallOfFame.UserId)
{
HallOfFame = await _HallOfFameService.UpdateHallOfFameAsync(HallOfFame);
}
else
{
_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;
HallOfFame = null;
}
}
else
{

View File

@@ -0,0 +1,130 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Oqtane.Databases.Interfaces;
using Oqtane.Migrations;
using SZUAbsolventenverein.Module.HallOfFame.Migrations.EntityBuilders;
using SZUAbsolventenverein.Module.HallOfFame.Repository;
namespace SZUAbsolventenverein.Module.HallOfFame.Migrations
{
[DbContext(typeof(HallOfFameContext))]
[Migration("SZUAbsolventenverein.Module.HallOfFame.01.00.00.01")]
public class AddHallOfFameColumns : MultiDatabaseMigration
{
public AddHallOfFameColumns(IDatabase database) : base(database)
{
}
protected override void Up(MigrationBuilder migrationBuilder)
{
var entityBuilder = new HallOfFameEntityBuilder(migrationBuilder, ActiveDatabase);
// Add new columns manually since we are upgrading an existing table
// Note: Integer columns are generated as default/nullable depending on definition, Oqtane Helpers handle generic types.
// Using logic similar to Oqtane.Migrations.EntityBuilders
if (ActiveDatabase.Name == "Sqlite") // Sqlite specific or generic
{
// Generic AddColumn: Table, Name, Type, Nullable
// However, Oqtane EntityBuilder usually builds tables.
// We will use migrationBuilder directly via helper if possible or standard AddColumn.
migrationBuilder.AddColumn<int>(
name: "Year",
table: "SZUAbsolventenvereinHallOfFame",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<string>(
name: "Description",
table: "SZUAbsolventenvereinHallOfFame",
nullable: true); // Allow nulls initially or empty? MaxString usually nullable in Oqtane context? Let's check.
migrationBuilder.AddColumn<string>(
name: "Image",
table: "SZUAbsolventenvereinHallOfFame",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Link",
table: "SZUAbsolventenvereinHallOfFame",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Status",
table: "SZUAbsolventenvereinHallOfFame",
maxLength: 50,
nullable: true);
migrationBuilder.AddColumn<int>(
name: "UserId",
table: "SZUAbsolventenvereinHallOfFame",
nullable: false,
defaultValue: 0);
}
else
{
// For SQL Server / others, simply use same AddColumn but allow EF Core to handle types
migrationBuilder.AddColumn<int>(
name: "Year",
table: "SZUAbsolventenvereinHallOfFame",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<string>(
name: "Description",
table: "SZUAbsolventenvereinHallOfFame",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Image",
table: "SZUAbsolventenvereinHallOfFame",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Link",
table: "SZUAbsolventenvereinHallOfFame",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Status",
table: "SZUAbsolventenvereinHallOfFame",
maxLength: 50,
nullable: true);
migrationBuilder.AddColumn<int>(
name: "UserId",
table: "SZUAbsolventenvereinHallOfFame",
nullable: false,
defaultValue: 0);
}
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Year",
table: "SZUAbsolventenvereinHallOfFame");
migrationBuilder.DropColumn(
name: "Description",
table: "SZUAbsolventenvereinHallOfFame");
migrationBuilder.DropColumn(
name: "Image",
table: "SZUAbsolventenvereinHallOfFame");
migrationBuilder.DropColumn(
name: "Link",
table: "SZUAbsolventenvereinHallOfFame");
migrationBuilder.DropColumn(
name: "Status",
table: "SZUAbsolventenvereinHallOfFame");
migrationBuilder.DropColumn(
name: "UserId",
table: "SZUAbsolventenvereinHallOfFame");
}
}
}

View File

@@ -25,6 +25,12 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Migrations.EntityBuilders
HallOfFameId = AddAutoIncrementColumn(table,"HallOfFameId");
ModuleId = AddIntegerColumn(table,"ModuleId");
Name = AddMaxStringColumn(table,"Name");
Year = AddIntegerColumn(table,"Year");
Description = AddMaxStringColumn(table,"Description");
Image = AddMaxStringColumn(table,"Image");
Link = AddMaxStringColumn(table,"Link");
Status = AddStringColumn(table,"Status", 50);
UserId = AddIntegerColumn(table,"UserId");
AddAuditableColumns(table);
return this;
}
@@ -32,5 +38,12 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Migrations.EntityBuilders
public OperationBuilder<AddColumnOperation> HallOfFameId { get; set; }
public OperationBuilder<AddColumnOperation> ModuleId { get; set; }
public OperationBuilder<AddColumnOperation> Name { get; set; }
public OperationBuilder<AddColumnOperation> Year { get; set; }
public OperationBuilder<AddColumnOperation> Description { get; set; }
public OperationBuilder<AddColumnOperation> Image { get; set; }
public OperationBuilder<AddColumnOperation> Link { get; set; }
public OperationBuilder<AddColumnOperation> Status { get; set; }
public OperationBuilder<AddColumnOperation> UserId { get; set; }
}
}

View File

@@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<Version>1.0.0</Version>
<Version>1.0.1</Version>
<Product>SZUAbsolventenverein.Module.HallOfFame</Product>
<Authors>SZUAbsolventenverein</Authors>
<Company>SZUAbsolventenverein</Company>

View File

@@ -54,6 +54,20 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Services
}
}
public Task<Models.HallOfFame> GetHallOfFameByUserIdAsync(int UserId, int ModuleId)
{
if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, ModuleId, PermissionNames.View))
{
// Assuming Repository doesn't have specific method yet, using LINQ on GetHallOfFames
return Task.FromResult(_HallOfFameRepository.GetHallOfFames(ModuleId).FirstOrDefault(item => item.UserId == UserId));
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized HallOfFame GetByUserId Attempt {UserId} {ModuleId}", UserId, ModuleId);
return null;
}
}
public Task<Models.HallOfFame> AddHallOfFameAsync(Models.HallOfFame HallOfFame)
{
if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, HallOfFame.ModuleId, PermissionNames.Edit))