Commit: 4ab1161
Parent: fdc4e25

Submit answers and uploads

Mårten Åsberg committed on 2026-05-04 at 16:43
MatDenDagen/Components/Pages/Submission.razor +87 -0
diff --git a/MatDenDagen/Components/Pages/Submission.razor b/MatDenDagen/Components/Pages/Submission.razor
new file mode 100644
index 0000000..aabd6b9
@@ -0,0 +1,87 @@
@page "/submission"
@using MatDenDagen.Infrastructure.Storage.BlobStorage
@using MatDenDagen.Infrastructure.Storage.Database
@using MatDenDagen.Models
@using Microsoft.AspNetCore.Http
@using Microsoft.EntityFrameworkCore
@inject TimeProvider timeProvider
@inject BlobStorageService blobService
@inject QuestionnaireContext questionnaireContext
@inject NavigationManager navigationManager
<h1>Skicka in svar</h1>
@if (success)
{
<p>Ditt svar har skickats in.</p>
}
@if (questions.Count == 0)
{
<p>Inga frågor hittades.</p>
}
else
{
<form method="post" enctype="multipart/form-data" @formname="Submission">
@foreach (var question in questions)
{
<p>
<label>
<span>@question.Text</span><br/>
<textarea name="answer[@question.Id]"></textarea>
</label>
</p>
}
<p>
<label>
<span>Bilagor</span><br/>
<input type="file" name="files" multiple />
</label>
</p>
<p>
<button type="submit">Skicka</button>
</p>
</form>
}
@code {
[CascadingParameter]
private HttpContext? HttpContext { get; set; }
private bool success = false;
private List<Question> questions = [];
protected override async Task OnInitializedAsync()
{
questions = await questionnaireContext.Questions.ToListAsync(HttpContext.RequestAborted);
if (HttpContext?.Request.Method != "POST")
return;
var form = HttpContext.Request.Form;
var uploads = new List<Upload>();
foreach (var file in form.Files)
{
await using var stream = file.OpenReadStream();
var result = await blobService.SaveBlob(stream, HttpContext.RequestAborted);
uploads.Add(new Upload { Id = result.Id, Name = file.FileName });
}
questionnaireContext.Submissions.Add(new()
{
Id = Guid.CreateVersion7(timeProvider.GetUtcNow()),
Answers = questions.Select(q => new Answer
{
Id = Guid.CreateVersion7(timeProvider.GetUtcNow()),
QuestionId = q.Id,
Text = form[$"answer[{q.Id}]"].ToString()
}).ToList(),
Uploads = uploads
});
await questionnaireContext.SaveChangesAsync(HttpContext.RequestAborted);
success = true;
}
}
MatDenDagen/Components/Pages/UploadTest.razor +0 -62
diff --git a/MatDenDagen/Components/Pages/UploadTest.razor b/MatDenDagen/Components/Pages/UploadTest.razor
deleted file mode 100644
index 44656ef..0000000
@@ -1,62 +0,0 @@
@page "/upload-test"
@using System.IO
@using MatDenDagen.Infrastructure.Storage.BlobStorage
@using Microsoft.AspNetCore.Http
@using System.Runtime.CompilerServices
@using Microsoft.AspNetCore.Mvc.Infrastructure
@using System.Threading
@inject BlobStorageService blobService
@implements IDisposable
<EditForm FormName="UploadTest" Model="@model" OnSubmit="@Submit" enctype="multipart/form-data" Enhance>
<p>
<label>
<span>Fil:</span>
<InputFile name="model.File" required />
</label>
</p>
<p>
<input type="submit" value="Ladda upp" />
</p>
</EditForm>
@if (uploadedId is not null)
{
<p>Filen har id: <code>@uploadedId</code></p>
}
@code {
private readonly CancellationTokenSource cts = new();
[SupplyParameterFromForm]
private UploadTestModel? model { get; set; }
private string? uploadedId { get; set; }
protected override void OnInitialized()
{
model ??= new();
}
private async Task Submit()
{
if (model?.File is not { } file)
{
return;
}
var result = await blobService.SaveBlob(file.OpenReadStream(), cts.Token);
uploadedId = result.Id;
}
public void Dispose()
{
cts.Cancel();
cts.Dispose();
}
private sealed class UploadTestModel
{
public IFormFile? File { get; set; }
}
}
MatDenDagen/Infrastructure/Storage/Database/Migrations/20260503045708_AddUploadsToSubmissions.Designer.cs +122 -0
diff --git a/MatDenDagen/Infrastructure/Storage/Database/Migrations/20260503045708_AddUploadsToSubmissions.Designer.cs b/MatDenDagen/Infrastructure/Storage/Database/Migrations/20260503045708_AddUploadsToSubmissions.Designer.cs
new file mode 100644
index 0000000..02dab10
@@ -0,0 +1,122 @@
// <auto-generated />
using System;
using MatDenDagen.Infrastructure.Storage.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace MatDenDagen.Infrastructure.Storage.Database.Migrations
{
[DbContext(typeof(QuestionnaireContext))]
[Migration("20260503045708_AddUploadsToSubmissions")]
partial class AddUploadsToSubmissions
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.7");
modelBuilder.Entity("MatDenDagen.Models.Answer", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid>("QuestionId")
.HasColumnType("TEXT");
b.Property<Guid?>("SubmissionId")
.HasColumnType("TEXT");
b.Property<string>("Text")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("QuestionId");
b.HasIndex("SubmissionId");
b.ToTable("Answer");
});
modelBuilder.Entity("MatDenDagen.Models.Question", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Text")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Questions");
});
modelBuilder.Entity("MatDenDagen.Models.Submission", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Submissions");
});
modelBuilder.Entity("MatDenDagen.Models.Upload", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid?>("SubmissionId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("SubmissionId");
b.ToTable("Upload");
});
modelBuilder.Entity("MatDenDagen.Models.Answer", b =>
{
b.HasOne("MatDenDagen.Models.Question", null)
.WithMany()
.HasForeignKey("QuestionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("MatDenDagen.Models.Submission", null)
.WithMany("Answers")
.HasForeignKey("SubmissionId");
});
modelBuilder.Entity("MatDenDagen.Models.Upload", b =>
{
b.HasOne("MatDenDagen.Models.Submission", null)
.WithMany("Uploads")
.HasForeignKey("SubmissionId");
});
modelBuilder.Entity("MatDenDagen.Models.Submission", b =>
{
b.Navigation("Answers");
b.Navigation("Uploads");
});
#pragma warning restore 612, 618
}
}
}
MatDenDagen/Infrastructure/Storage/Database/Migrations/20260503045708_AddUploadsToSubmissions.cs +43 -0
diff --git a/MatDenDagen/Infrastructure/Storage/Database/Migrations/20260503045708_AddUploadsToSubmissions.cs b/MatDenDagen/Infrastructure/Storage/Database/Migrations/20260503045708_AddUploadsToSubmissions.cs
new file mode 100644
index 0000000..93dd5c8
@@ -0,0 +1,43 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace MatDenDagen.Infrastructure.Storage.Database.Migrations
{
/// <inheritdoc />
public partial class AddUploadsToSubmissions : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Upload",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: false),
SubmissionId = table.Column<Guid>(type: "TEXT", nullable: true),
},
constraints: table =>
{
table.PrimaryKey("PK_Upload", x => x.Id);
table.ForeignKey(
name: "FK_Upload_Submissions_SubmissionId",
column: x => x.SubmissionId,
principalTable: "Submissions",
principalColumn: "Id"
);
}
);
migrationBuilder.CreateIndex(name: "IX_Upload_SubmissionId", table: "Upload", column: "SubmissionId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(name: "Upload");
}
}
}
MatDenDagen/Infrastructure/Storage/Database/Migrations/QuestionnaireContextModelSnapshot.cs +28 -0
diff --git a/MatDenDagen/Infrastructure/Storage/Database/Migrations/QuestionnaireContextModelSnapshot.cs b/MatDenDagen/Infrastructure/Storage/Database/Migrations/QuestionnaireContextModelSnapshot.cs
index 0b5835c..f03f96c 100644
@@ -68,6 +68,25 @@ namespace MatDenDagen.Infrastructure.Storage.Database.Migrations
b.ToTable("Submissions");
});
modelBuilder.Entity("MatDenDagen.Models.Upload", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid?>("SubmissionId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("SubmissionId");
b.ToTable("Upload");
});
modelBuilder.Entity("MatDenDagen.Models.Answer", b =>
{
b.HasOne("MatDenDagen.Models.Question", null)
@@ -81,9 +100,18 @@ namespace MatDenDagen.Infrastructure.Storage.Database.Migrations
.HasForeignKey("SubmissionId");
});
modelBuilder.Entity("MatDenDagen.Models.Upload", b =>
{
b.HasOne("MatDenDagen.Models.Submission", null)
.WithMany("Uploads")
.HasForeignKey("SubmissionId");
});
modelBuilder.Entity("MatDenDagen.Models.Submission", b =>
{
b.Navigation("Answers");
b.Navigation("Uploads");
});
#pragma warning restore 612, 618
}
MatDenDagen/Infrastructure/Storage/Database/QuestionnaireContext.cs +5 -0
diff --git a/MatDenDagen/Infrastructure/Storage/Database/QuestionnaireContext.cs b/MatDenDagen/Infrastructure/Storage/Database/QuestionnaireContext.cs
index 37f11a7..60f8dbf 100644
@@ -17,10 +17,15 @@ public sealed class QuestionnaireContext(DbContextOptions<QuestionnaireContext>
var submissionBuilder = modelBuilder.Entity<Submission>();
submissionBuilder.HasKey(s => s.Id);
submissionBuilder.HasMany(s => s.Answers).WithOne();
submissionBuilder.HasMany(s => s.Uploads).WithOne();
var answerBuilder = modelBuilder.Entity<Answer>();
answerBuilder.HasKey(a => a.Id);
answerBuilder.Property(a => a.Text);
answerBuilder.HasOne<Question>().WithMany().HasForeignKey(a => a.QuestionId);
var uploadBuilder = modelBuilder.Entity<Upload>();
uploadBuilder.HasKey(u => u.Id);
uploadBuilder.Property(u => u.Name);
}
}
MatDenDagen/Models/Submission.cs +3 -1
diff --git a/MatDenDagen/Models/Submission.cs b/MatDenDagen/Models/Submission.cs
index 1845e90..a01a0b9 100644
@@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
namespace MatDenDagen.Models;
public sealed class Submission
{
public required Guid Id { get; init; }
public required Answer[] Answers { get; init; }
public required List<Answer> Answers { get; init; }
public required List<Upload> Uploads { get; init; }
}
MatDenDagen/Models/Upload.cs +7 -0
diff --git a/MatDenDagen/Models/Upload.cs b/MatDenDagen/Models/Upload.cs
new file mode 100644
index 0000000..a81ff75
@@ -0,0 +1,7 @@
namespace MatDenDagen.Models;
public sealed class Upload
{
public required string Id { get; init; }
public required string Name { get; init; }
}
README.md +2 -1
diff --git a/README.md b/README.md
index 0da5151..2e2cc5b 100644
@@ -4,7 +4,8 @@
- [ ] User pages
- [ ] See the date of **the day**
- [ ] Submit questionnaire
- [x] Submit questionnaire
- [ ] Questionnaire only accepts phone numbers from configured participants
- [ ] Admin pages
- [ ] Password protection
- [ ] Configure possible date span for **the day**