Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Backend/Altafraner.AfraApp/Database/AfraAppContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,19 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
.WithMany();
});

modelBuilder.Entity<ProfundumFeedbackEntry>(e =>
{
e.HasKey(fe => new
{
fe.AnkerId, fe.SlotId, fe.BetroffenePersonId
});

e.HasOne(fe => fe.Einschreibung)
.WithMany()
.HasForeignKey(fe => new { fe.BetroffenePersonId, fe.SlotId })
.IsRequired();
});

/*
* This is a bit annoying, but we'll have to do it because of a bug in the Npgsql provider.
* By default, it'll use '\0' as the default value for char columns, as it is the default value for char in C#.
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace Altafraner.AfraApp.Migrations
{
/// <inheritdoc />
public partial class AllowFeedbackPerInstanceAndSlot : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
// 1. Drop existing constraints and indices that are being replaced
migrationBuilder.DropForeignKey(
name: "fk_profundum_feedback_entries_personen_betroffene_person_id",
table: "profundum_feedback_entries");

migrationBuilder.DropForeignKey(
name: "fk_profundum_feedback_entries_profunda_instanzen_instanz_id",
table: "profundum_feedback_entries");

migrationBuilder.DropPrimaryKey(
name: "pk_profundum_feedback_entries",
table: "profundum_feedback_entries");

migrationBuilder.DropIndex(
name: "ix_profundum_feedback_entries_betroffene_person_id",
table: "profundum_feedback_entries");

migrationBuilder.DropIndex(
name: "ix_profundum_feedback_entries_instanz_id",
table: "profundum_feedback_entries");

// 2. Prepare the table for data migration
// We rename the old column to keep the data accessible while we populate the new slot_id
migrationBuilder.RenameColumn(
name: "instanz_id",
table: "profundum_feedback_entries",
newName: "old_instanz_id");

migrationBuilder.AddColumn<Guid>(
name: "slot_id",
table: "profundum_feedback_entries",
type: "uuid",
nullable: true);

// 3. Migrate the data
// Logic: Map (Person, Instance) to (Person, Slot) via the Enrollment table.
// Selection: If multiple slots exist for an enrollment, prefer Q2 (Quartal == 2).
// Otherwise, pick the first available slot.
migrationBuilder.Sql(@"
UPDATE ""profundum_feedback_entries"" f
SET ""slot_id"" = (
SELECT e.""slot_id""
FROM ""profunda_einschreibungen"" e
INNER JOIN ""profunda_slots"" s ON e.""slot_id"" = s.""id""
WHERE e.""betroffene_person_id"" = f.""betroffene_person_id""
AND e.""profundum_instanz_id"" = f.""old_instanz_id""
ORDER BY
CASE WHEN s.""quartal"" = 2 THEN 0 ELSE 1 END,
s.""quartal"" ASC
LIMIT 1
);
");

// 4. Validation: Fail if feedback exists for a user/instance without an enrollment
migrationBuilder.Sql(@"
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM ""profundum_feedback_entries"" WHERE ""slot_id"" IS NULL) THEN
RAISE EXCEPTION 'Migration failed: Found feedback entry for a person/instance without a corresponding enrollment.';
END IF;
END $$;
");

// 5. Finalize schema changes
migrationBuilder.DropColumn(
name: "old_instanz_id",
table: "profundum_feedback_entries");

migrationBuilder.AlterColumn<Guid>(
name: "slot_id",
table: "profundum_feedback_entries",
type: "uuid",
nullable: false,
oldNullable: true);

// Recreate PK with the new slot_id column
migrationBuilder.AddPrimaryKey(
name: "pk_profundum_feedback_entries",
table: "profundum_feedback_entries",
columns: new[] { "anker_id", "slot_id", "betroffene_person_id" });

migrationBuilder.CreateIndex(
name: "ix_profundum_feedback_entries_betroffene_person_id_slot_id",
table: "profundum_feedback_entries",
columns: new[] { "betroffene_person_id", "slot_id" });

// Link feedback directly to the enrollment
migrationBuilder.AddForeignKey(
name: "fk_profundum_feedback_entries_profunda_einschreibungen_betroffe~",
table: "profundum_feedback_entries",
columns: new[] { "betroffene_person_id", "slot_id" },
principalTable: "profunda_einschreibungen",
principalColumns: new[] { "betroffene_person_id", "slot_id" },
onDelete: ReferentialAction.Cascade);
}

/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
throw new NotImplementedException();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// <auto-generated />
// <auto-generated />
using System;
using System.Collections.Generic;
using Altafraner.AfraApp;
Expand All @@ -21,7 +21,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.2")
.HasAnnotation("ProductVersion", "10.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63);

NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "anwesenheits_status", new[] { "anwesend", "entschuldigt", "fehlend" });
Expand Down Expand Up @@ -413,9 +413,9 @@ protected override void BuildModel(ModelBuilder modelBuilder)
.HasColumnType("uuid")
.HasColumnName("anker_id");

b.Property<Guid>("InstanzId")
b.Property<Guid>("SlotId")
.HasColumnType("uuid")
.HasColumnName("instanz_id");
.HasColumnName("slot_id");

b.Property<Guid>("BetroffenePersonId")
.HasColumnType("uuid")
Expand All @@ -425,14 +425,11 @@ protected override void BuildModel(ModelBuilder modelBuilder)
.HasColumnType("integer")
.HasColumnName("grad");

b.HasKey("AnkerId", "InstanzId", "BetroffenePersonId")
b.HasKey("AnkerId", "SlotId", "BetroffenePersonId")
.HasName("pk_profundum_feedback_entries");

b.HasIndex("BetroffenePersonId")
.HasDatabaseName("ix_profundum_feedback_entries_betroffene_person_id");

b.HasIndex("InstanzId")
.HasDatabaseName("ix_profundum_feedback_entries_instanz_id");
b.HasIndex("BetroffenePersonId", "SlotId")
.HasDatabaseName("ix_profundum_feedback_entries_betroffene_person_id_slot_id");

b.ToTable("profundum_feedback_entries");
});
Expand Down Expand Up @@ -1262,25 +1259,16 @@ protected override void BuildModel(ModelBuilder modelBuilder)
.IsRequired()
.HasConstraintName("fk_profundum_feedback_entries_profundum_feedback_anker_anker_id");

b.HasOne("Altafraner.AfraApp.User.Domain.Models.Person", "BetroffenePerson")
.WithMany()
.HasForeignKey("BetroffenePersonId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_profundum_feedback_entries_personen_betroffene_person_id");

b.HasOne("Altafraner.AfraApp.Profundum.Domain.Models.ProfundumInstanz", "Instanz")
b.HasOne("Altafraner.AfraApp.Profundum.Domain.Models.ProfundumEinschreibung", "Einschreibung")
.WithMany()
.HasForeignKey("InstanzId")
.HasForeignKey("BetroffenePersonId", "SlotId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_profundum_feedback_entries_profunda_instanzen_instanz_id");
.HasConstraintName("fk_profundum_feedback_entries_profunda_einschreibungen_betroffe~");

b.Navigation("Anker");

b.Navigation("BetroffenePerson");

b.Navigation("Instanz");
b.Navigation("Einschreibung");
});

modelBuilder.Entity("Altafraner.AfraApp.Profundum.Domain.Models.ProfundaDefinitionDependency", b =>
Expand Down
21 changes: 12 additions & 9 deletions Backend/Altafraner.AfraApp/Profundum/API/Endpoints/Bewertung.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ public static void MapBewertungEndpoints(this IEndpointRouteBuilder app)
kategorie.MapPut("/{id:guid}", UpdateKategorieAsync);

bewertung.MapGet("/{profundumId:guid}", GetAnkerForProfundum);
bewertung.MapGet("/{profundumId:guid}/{studentId:guid}", GetBewertungAsync);
bewertung.MapPut("/{profundumId:guid}/{studentId:guid}", UpdateBewertungAsync);
bewertung.MapGet("/{instanzId:guid}/{slotId:guid}/{studentId:guid}", GetBewertungAsync);
bewertung.MapPut("/{instanzId:guid}/{slotId:guid}/{studentId:guid}", UpdateBewertungAsync);

bewertung.MapGet("/control/status", GetStatusAsync)
.RequireAuthorization(AuthorizationPolicies.ProfundumsVerantwortlich);
Expand Down Expand Up @@ -223,32 +223,35 @@ private static async Task<Results<Ok<AnkerOverview>, ForbidHttpResult>> GetAnker
}

private static async Task<Results<Ok<Dictionary<Guid, int?>>, ForbidHttpResult>> GetBewertungAsync(Guid studentId,
Guid profundumId,
Guid instanzId,
Guid slotId,
FeedbackService feedbackService,
UserAccessor userAccessor)
{
var user = await userAccessor.GetUserAsync();
if (!await feedbackService.MayProvideFeedbackForProfundumAsync(user, profundumId))
if (!await feedbackService.MayProvideFeedbackForProfundumAsync(user, instanzId))
return TypedResults.Forbid();

var feedback = await feedbackService.GetFeedback(studentId, profundumId);
var feedback = await feedbackService.GetFeedback(studentId, instanzId, slotId);
return TypedResults.Ok(feedback.ToDictionary(f => f.Key.Id, f => f.Value));
}

private static async Task<Results<NoContent, ForbidHttpResult, BadRequest<HttpValidationProblemDetails>>>
UpdateBewertungAsync(Guid studentId,
Guid profundumId,
Guid instanzId,
Guid slotId,
Dictionary<Guid, int?> bewertungen,
FeedbackService feedbackService,
UserAccessor userAccessor)
{
var user = await userAccessor.GetUserAsync();
if (!await feedbackService.MayProvideFeedbackForProfundumAsync(user, profundumId))
if (!await feedbackService.MayProvideFeedbackForProfundumAsync(user, instanzId))
return TypedResults.Forbid();
try
{
await feedbackService.UpdateFeedback(studentId,
profundumId,
instanzId,
slotId,
bewertungen.Where(b => b.Value.HasValue)
.ToDictionary(f => f.Key, f => f.Value!.Value));

Expand Down Expand Up @@ -284,7 +287,7 @@ private static async Task<Ok<Dictionary<Guid, IEnumerable<FeedbackOverview>>>>
{
Instanz = i,
Slot = slot,
Status = bewertungsStatus.Single(e => e.instanz.Id == i.Id).status
Status = bewertungsStatus.Single(e => e.instanz.Id == i.Id && e.slot.Id == slot.Id).status
});
dict.Add(slot.Id, data);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
using System.ComponentModel.DataAnnotations;
using Altafraner.AfraApp.User.Domain.Models;
using Microsoft.EntityFrameworkCore;

namespace Altafraner.AfraApp.Profundum.Domain.Models.Bewertung;

/// <summary>
/// A db record representing a Profundum Bewertung.
/// </summary>
[PrimaryKey(nameof(AnkerId), nameof(InstanzId), nameof(BetroffenePersonId))]
public class ProfundumFeedbackEntry
{
/// <summary>
Expand All @@ -21,24 +18,19 @@ public class ProfundumFeedbackEntry
public ProfundumFeedbackAnker Anker { get; set; } = null!;

/// <summary>
/// The profundum instanz the Bewertung is for
/// The actual Profundum enrollment.
/// </summary>
public Guid InstanzId { get; set; }
public ProfundumEinschreibung Einschreibung { get; set; } = null!;

/// <summary>
/// The actual Profundum Instanz
/// The ID of the slot for the enrollment (part of composite key)
/// </summary>
public ProfundumInstanz Instanz { get; set; } = null!;
protected internal Guid SlotId { get; set; }

/// <summary>
/// The person that received the Bewertung
/// The ID of the person affected by the enrollment (part of composite key)
/// </summary>
public Guid BetroffenePersonId { get; set; }

/// <summary>
/// The actual person that received the Bewertung
/// </summary>
public Person BetroffenePerson { get; set; } = null!;
protected internal Guid BetroffenePersonId { get; set; }

/// <summary>
/// The grade given for the Kriterium
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,18 @@ public async Task<byte[]> GenerateFileForPerson(Person user, int schuljahr, bool
.Include(e => e.Anker)
.ThenInclude(a => a.Kategorie)
.ThenInclude(e => e.Fachbereiche)
.Include(e => e.Instanz)
.ThenInclude(e => e.Profundum)
.Include(e => e.Instanz)
.ThenInclude(e => e.Slots)
.Include(e => e.Instanz)
.ThenInclude(e => e.Verantwortliche)
.Where(e => e.BetroffenePerson == user)
.Where(e => e.Instanz.Slots.Any(s => s.Jahr == schuljahr && quartale.Contains(s.Quartal)))
.Include(e => e.Einschreibung)
.ThenInclude(e => e.ProfundumInstanz)
.ThenInclude(e => e!.Profundum)
.Include(e => e.Einschreibung)
.ThenInclude(e => e.ProfundumInstanz)
.ThenInclude(e => e!.Slots)
.Include(e => e.Einschreibung)
.ThenInclude(e => e.ProfundumInstanz)
.ThenInclude(e => e!.Verantwortliche)
.Where(e => e.Einschreibung.ProfundumInstanz != null)
.Where(e => e.Einschreibung.BetroffenePerson == user)
.Where(e => e.Einschreibung.Slot.Jahr == schuljahr && quartale.Contains(e.Einschreibung.Slot.Quartal))
.ToArrayAsync();

var meta = new ProfundumFeedbackPdfData.MetaData(ausgabedatum.ToString("dd.MM.yyyy"),
Expand All @@ -76,9 +80,9 @@ private ProfundumFeedbackPdfData FeedbackToInputData(ProfundumFeedbackEntry[] fe
Person? userGm,
ProfundumFeedbackPdfData.MetaData meta)
{
var profunda = feedback.Select(e => e.Instanz)
.DistinctBy(e => e.Profundum)
.Select(e => new ProfundumFeedbackPdfData.Profundum(e.Profundum.Bezeichnung,
var profunda = feedback.Select(e => e.Einschreibung.ProfundumInstanz)
.DistinctBy(e => e!.Profundum)
.Select(e => new ProfundumFeedbackPdfData.Profundum(e!.Profundum.Bezeichnung,
e.Verantwortliche.Select(v => new PersonInfoMinimal(v))));

var feedbackByKategorie = feedback.GroupBy(e => e.Anker, e => e.Grad)
Expand Down Expand Up @@ -159,14 +163,18 @@ public async Task<byte[]> GenerateFileBatched(BatchingModes mode,
.Include(e => e.Anker)
.ThenInclude(a => a.Kategorie)
.ThenInclude(e => e.Fachbereiche)
.Include(e => e.Instanz)
.ThenInclude(e => e.Profundum)
.Include(e => e.Instanz)
.ThenInclude(e => e.Slots)
.Include(e => e.Instanz)
.ThenInclude(e => e.Verantwortliche)
.Where(e => e.Instanz.Slots.Any(s => s.Jahr == schuljahr && quartale.Contains(s.Quartal)))
.GroupBy(e => e.BetroffenePersonId)
.Include(e => e.Einschreibung)
.ThenInclude(e => e.ProfundumInstanz)
.ThenInclude(e => e!.Profundum)
.Include(e => e.Einschreibung)
.ThenInclude(e => e.ProfundumInstanz)
.ThenInclude(e => e!.Slots)
.Include(e => e.Einschreibung)
.ThenInclude(e => e.ProfundumInstanz)
.ThenInclude(e => e!.Verantwortliche)
.Where(e => e.Einschreibung.ProfundumInstanz != null && e.Einschreibung.Slot.Jahr == schuljahr &&
quartale.Contains(e.Einschreibung.Slot.Quartal))
.GroupBy(e => e.Einschreibung.BetroffenePersonId)
.ToDictionaryAsync(e => e.Key, e => e.ToArray());

var meta = new ProfundumFeedbackPdfData.MetaData(ausgabedatum.ToString("dd.MM.yyyy"),
Expand Down
Loading
Loading