Quizzer-8: Migrating to .NET 10

This commit is contained in:
Karsten Jeppesen
2026-05-01 11:40:09 +02:00
parent 15445b6e4a
commit f22c7dfb0c
116 changed files with 79176 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
@page
@model Quizzer.Pages.CategoriesModel
@Html.AntiForgeryToken()
@{
ViewData["Title"] = "Quizzer";
Layout = "_Layout-teacher";
}
<div class="container-fluid">
<div class="row">
<div class="col-xl-12" style="background-color: lightgreen">
<h1><center>Category Management</center></h1>
</div>
</div>
<div class=row>
<div class="col-xl-12" style="background-color: lemonchiffon">
<form class="form-horizontal" method="post">
@Html.Raw(Model.CatSelectList)
<label for="newTag" class="form-label">New Tag</label>
<input class="form-control" type="text" id="newTag" placeholder="Your new category" name="newCategory" aria-label="default input example" />
<button type="submit" class="btn btn-primary" name="action" value="newCategory">Submit</button>
</form>
</div>
</div>
@if (Model.CatSelected != null)
{
<div class=row>
<div class="col-xl-12" style="background-color: lightblue">
<table style="width:100%">
<tr>
<td style="width:max-content">
<h5>@Model.CatSelected.Txt</h5>
</td>
<td>
<form asp-page="" method="post">
<input type="hidden" name="action" value="removeCategory" />
<input type="hidden" name="target" value="@Model.CatSelected.CId" />
<button type="submit" class="btn btn-sm btn-outline-danger"><i class="bi bi-trash"></i></button>
Remove category (incl children)
</form>
</td>
</tr>
</table>
<form class="form-horizontal" method="post">
<input type="hidden" name="categoryParent" value="@Model.CatSelected.CId">
<label for="CatSelected_Descr" class="form-label">Quizz description</label>
@Html.TextAreaFor(m => Model.CatSelected.Descr, 10,40, htmlAttributes: new {style="width: 100%; max-width: 100%;" })<br>
<button type="submit" class="btn btn-primary" name="action" value="editCategory">Change description for @Model.CatSelected.Txt</button>
</form>
</div>
</div>
}
</div>

View File

@@ -0,0 +1,148 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Newtonsoft.Json;
using Quizzer.Extensions;
using Quizzer.Models;
using Quizzer.OpenIDConnect;
using Quizzer.Persistens;
using System.Collections;
namespace Quizzer.Pages
{
public class CategoriesModel : PageModel
{
[BindProperty]
public OpenIDUCNData ucnData { get; set; }
[BindProperty]
public string UserID { get; set; }
[BindProperty]
public string CatSelectList { get; set; }
[BindProperty]
public Cat CatSelected { get; set; }
[BindProperty]
public string myFullName { get; set; }
private bool isDebug;
private string KCUserID { get; set; }
private DbOps cManager = new();
private OpenIDConnectUtils OpenIDUtils = new();
private Cat categoryCookie;
private const string cookieName = "CategoriesCookie";
private void GetMyCookie()
{
string jsonString = HttpContext.Request.Cookies[cookieName];
if (jsonString != null)
{
categoryCookie = JsonConvert.DeserializeObject<Cat>(jsonString);
return;
}
// Here if no cookie found
categoryCookie = new Cat
{
CId = "",
CParentId = "",
Descr = "",
Txt = "",
};
// Save the new cookie
SetMyCookie();
}
private void SetMyCookie()
{
string jsonResult = JsonConvert.SerializeObject(categoryCookie);
Response.Cookies.Append(cookieName, jsonResult, new Microsoft.AspNetCore.Http.CookieOptions
{
Secure = true,
HttpOnly = true,
SameSite = Microsoft.AspNetCore.Http.SameSiteMode.None,
});
}
private void RemoveMyCookie()
{
Response.Cookies.Delete(cookieName);
}
//
private void DumpEnvironment()
{
Console.WriteLine("HEADER DUMP START");
foreach (var e in HttpContext.Request.Headers)
{
Console.WriteLine(e.Key + ":" + e.Value);
}
Console.WriteLine("HEADER DUMP END");
Console.WriteLine("ENVIRONMENT DUMP START");
foreach (DictionaryEntry e in System.Environment.GetEnvironmentVariables())
{
Console.WriteLine(e.Key + ":" + e.Value);
}
Console.WriteLine("ENVIRONMENT DUMP END");
}
public void Common()
{
string? ucnDataJson = OpenIDUtils.GetJwtClaim(HttpContext, "ucn");
KCUserID = OpenIDUtils.GetJwtClaim(HttpContext, "sub");
ucnData = JsonConvert.DeserializeObject<OpenIDUCNData>(ucnDataJson);
UserID = OpenIDUtils.GetJwtClaim(HttpContext, "email").HashSHA256();
myFullName = OpenIDUtils.GetJwtClaim(HttpContext, "name");
}
public void Always()
{
Common();
GetMyCookie();
CatSelectList = cManager.MakeCList(UserID, "radioBtn", categoryCookie.CId);
if (categoryCookie.CId != "")
{
CatSelected = categoryCookie;
}
}
public IActionResult OnGet()
{
Always();
if (ucnData.theRole != "teacher" && ucnData.theRole != "developer") return Redirect("/");
return Page();
}
public IActionResult OnPost(string action = "na", string categoryParent = "/", string newCategory = "na", string target="na")
{
//tagPath = JsonConvert.DeserializeObject<List<Tags>>(theTagPath);
Common();
switch (action)
{
case "newCategory":
categoryCookie = cManager.SetCat(UserID, categoryParent, newCategory);
break;
case "editCategory":
cManager.UpdateCatdescr(categoryParent, CatSelected.Descr);
break;
case "removeCategory":
cManager.DeleteCategory(target);
break;
default:
categoryCookie = cManager.GetCat(categoryParent);
break;
}
SetMyCookie();
return Redirect("/Categories");
}
}
}

View File

@@ -0,0 +1,96 @@
@page
@model Quizzer.Pages.CoverageModel
@Html.AntiForgeryToken()
@{
ViewData["Title"] = "Quizzer";
Layout = "_Layout-teacher";
}
<div class="container-fluid">
<div class="row">
<div class="col-xl-12" style="background-color: lightgreen">
<h1><center>Quiz Coverage</center></h1>
</div>
</div>
<div class="row">
<div class="col-xl-12">
<form class="form-horizontal" method="post">
<input type="hidden" name="action" value="selectClass" />
<select id="selectedClass" name="selectedClass" onchange="javascript:this.form.submit()">
<option>Select Class</option>
@foreach (var item in @Model.theClasses)
{
<option value="@item">@item</option>
}
</select>
</form>
</div>
</div>
@if (Model.currentClass != "" && Model.registrations.Count > 0)
{
<div class="row">
<div class="col-xl-12">
<h3>@Model.registrations[0].TheClass</h3>
<table class="table">
<thead>
<tr>
<th scope="col">Name</th>
@for (var counter = 0; counter < Model.dates.Count(); counter++)
{
<th scope="col">@(counter + 1)</th>
}
</tr>
</thead>
<tbody>
@foreach (var student in Model.persons)
{
<tr>
<td>@student.Name</td>
@foreach (var theDate in Model.dates)
{
var res = Model.registrations.Where(x => x.Name == student.Name && x.Date == theDate);
@if (res.Count() == 0)
{
<td>-</td>
}
else
{
<td>+</td>
}
}
</tr>
}
</tbody>
</table>
</div>
<div class="col-xl-12">
<table class="table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Date</th>
<th scope="col">Attendees</th>
</tr>
</thead>
<tbody>
@for (var index = 0; index < Model.dates.Count(); index++)
{
<tr>
<td>@(index + 1)</td>
<td>@Model.dates[index]</td>
<td>@Model.registrations.Where(x => x.Date == Model.dates[index]).Count()</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
@if (Model.currentClass != "" && Model.registrations.Count == 0)
{
<div class="row">
<div class="col-xl-12">
<h5>No registrations</h5>
</div>
</div>
}
</div>

View File

@@ -0,0 +1,108 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Newtonsoft.Json;
using Quizzer.Extensions;
using Quizzer.Implementations;
using Quizzer.Models;
using Quizzer.OpenIDConnect;
using Quizzer.Persistens;
using System.Diagnostics;
using System.IdentityModel.Tokens.Jwt;
namespace Quizzer.Pages
{
public class CoverageModel : PageModel
{
[BindProperty]
public List<Coverage>? registrations { get; set; }
[BindProperty]
public List<string>? dates { get; set; }
[BindProperty]
public List<Coverage>? persons { get; set; }
[BindProperty]
public List<string>? theClasses { get; set; }
[BindProperty]
public string? currentClass { get; set; }
[BindProperty]
public string myFullName { get; set; }
public OpenIDUCNData ucnData { get; set; }
private bool isDebug;
public string UserID { get; set; }
private string KCUserID { get; set; }
private DbOps dbMgr = new();
private OpenIDConnectUtils OpenIDUtils = new();
public void Common()
{
string? ucnDataJson = OpenIDUtils.GetJwtClaim(HttpContext, "ucn");
KCUserID = OpenIDUtils.GetJwtClaim(HttpContext, "sub");
ucnData = JsonConvert.DeserializeObject<OpenIDUCNData>(ucnDataJson);
UserID = OpenIDUtils.GetJwtClaim(HttpContext, "email").HashSHA256();
myFullName = OpenIDUtils.GetJwtClaim(HttpContext, "name");
}
private void GetMyCookie()
{
currentClass = HttpContext.Request.Cookies["CoverageCookie"];
SetMyCookie();
}
private void SetMyCookie()
{
Response.Cookies.Append("CoverageCookie", currentClass, new Microsoft.AspNetCore.Http.CookieOptions
{
Secure = true,
HttpOnly = true,
SameSite = Microsoft.AspNetCore.Http.SameSiteMode.None,
});
}
public IActionResult OnGet()
{
Common();
if (ucnData.theRole != "teacher" && ucnData.theRole != "developer") return Redirect("/");
currentClass = "";
// List of classes
KeyCloak KCApi = new KeyCloak();
List<KCGroups> myGroups = KCApi.KeyCloakGetUsersGroups(KCUserID);
theClasses = new();
foreach (var group in myGroups)
{
if (group.name.Contains("-CSD-")) theClasses.Add(group.name);
}
SetMyCookie();
return Page();
}
public IActionResult OnPost(string action = "na", string selectedClass = "")
{
Common();
if (ucnData.theRole != "teacher" && ucnData.theRole != "developer") return Redirect("/");
if ( selectedClass != "")
{
currentClass= selectedClass;
SetMyCookie();
registrations = dbMgr.GetCoverage(currentClass);
// Ascending list of dates
dates = dbMgr.GetCoverageDates(currentClass);
// Ascending combined list of persons attending
persons = dbMgr.GetCoveragePersons(currentClass);
// List of classes
KeyCloak KCApi = new KeyCloak();
List<KCGroups> myGroups = KCApi.KeyCloakGetUsersGroups(KCUserID);
theClasses = new();
foreach (var group in myGroups)
{
if (group.name.Contains("-CSD-")) theClasses.Add(group.name);
}
return Page();
}
else return Redirect("/");
}
}
}

View File

@@ -0,0 +1,26 @@
@page
@model ErrorModel
@{
ViewData["Title"] = "Error";
}
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (Model.ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@Model.RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to the <strong>Development</strong> environment displays detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>

View File

@@ -0,0 +1,27 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.Diagnostics;
namespace Quizzer.Pages
{
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
public string? RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
private readonly ILogger<ErrorModel> _logger;
public ErrorModel(ILogger<ErrorModel> logger)
{
_logger = logger;
}
public void OnGet()
{
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
}
}
}

View File

@@ -0,0 +1,208 @@
@page
@model IndexModel
@{
ViewData["Title"] = "Quizzer";
}
<div class="container-fluid">
@if (Model.ucnData.theRole == "teacher" || Model.ucnData.theRole == "developer")
{
<div class="col-lg-12" style="background-color:cornflowerblue">
<h1>@Model.myFullName: Teacher</h1>
</div>
}
@if (Model.ucnData.theRole == "student" || Model.ucnData.theRole == "developer")
{
// STUDENT PATH
@if (Model.StudentState == "QuizzSelect")
{
<div class="row">
<div class="col-xxl-12" style="background-color: cornflowerblue">
<h1 style="text-align:center">Quizz Management</h1>
</div>
<div class="col-xxl-12" style="background-color: cornflowerblue">
@Model.myFullName, @Model.ucnData.theClass.FirstOrDefault()
</div>
</div>
<div class="row">
<div class="col-lg-12">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th></th>
<th>Description</th>
<th>Your Score</th>
<th>Max Score</th>
<th>Reset</th>
</tr>
</thead>
@foreach (var item in Model.PageTests)
{
<tr>
<td>
<div class="form-group">
<form asp-page="" method="post">
<input type="hidden" name="email" value="@item.Journal.PID" />
<input type="hidden" name="value" value="@item.Journal.QuizzID" />
@if (item.Journal.IsOpen == 1)
{
<input type="hidden" name="action" value="startTest" />
<button type="button" class="btn btn-outline-primary" onclick="javascript:this.form.submit()">Take Quizz</button>
}
else
{
<input type="hidden" name="action" value="reviewTest" />
<button type="button" class="btn btn-outline-primary" onclick="javascript:this.form.submit()">Review</button>
}
</form>
</div>
</td>
<td>
@item.Text
</td>
<td>
@item.Journal.Score
</td>
<td>
@item.Journal.ScoreAcc
</td>
<td>
@if (item.Journal.Anonymous)
{
<form asp-page="" method="post">
<input type="hidden" name="email" value="@item.Journal.PID" />
<input type="hidden" name="target" value="@item.Journal.QuizzID" />
<input type="hidden" name="action" value="deleteMyTest" />
<button type="button" class="btn btn-warning" onclick="javascript:this.form.submit()">Reset Test</button>
</form>
}
</td>
</tr>
}
</table>
</div>
</div>
// if review has been selected then show it
@if (Model.PageMCQuestionReview != null)
{
<div class="row">
<div class="col-lg-12">
<h2>Review</h2>
</div>
</div>
@for (int qindex = 0; qindex < Model.PageMCQuestionReview.Count; qindex++)
{
<div class="row" style="background-color:white; opacity: 0.8">
<div class="col-lg-12">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th style="width: 10%"></th>
<th>@Model.PageMCQuestionReview[qindex].QuestionText</th>
</tr>
</thead>
@for (int aindex = 0; aindex < Model.PageMCResponseReview[qindex].Count; aindex++)
{
<tr>
<td>
@if (Model.PageMCResponseReview[qindex][aindex])
{
<i class="bi bi-caret-right-fill" />
}
</td>
@if (Model.PageMCQuestionReview[qindex].AList[aindex].Aok)
{
<td style="background-color:lightgreen">
@Model.PageMCQuestionReview[qindex].AList[aindex].Atxt
</td>
}
else
{
<td>
@Model.PageMCQuestionReview[qindex].AList[aindex].Atxt
</td>
}
</tr>
}
</table>
</div>
</div>
}
}
}
@if (Model.StudentState == "QuizzRunning")
{
// We are doing a Quizz
<div class="row" style="background-color:white; opacity: 0.8">
<div class="col-lg-12">
<h2 style="text-align:center">@Model.PageMCQuestion.QuestionText</h2>
</div>
<form method="post">
<input type="hidden" name="action" value="registerAnswer" />
<input type="hidden" name="quizzInProgress" value="@Model.PageQuizzID" />
<table class="table">
@for (int indx = 0; indx < @Model.PageMCQuestion.AList.Count(); indx++)
{
<tr>
<td><input asp-for="@Model.PageMCQuestion.AList[@Model.PageMCRandomized[@indx]].Aok" /></td>
<td>@Model.PageMCQuestion.AList[@Model.PageMCRandomized[@indx]].Atxt</td>
</tr>
}
</table>
<button type="submit" class="btn btn-success" name="action" value="create"><i class="bi bi-forward"></i></button>
</form>
</div>
}
}
@if (Model.PageChartConnectors != null)
{
@if (Model.PageChartConnectors.Count > 2)
{
<div class="row">
<div class="col-xxl-12">
<h2>Visual</h2><br />
<canvas id="myChart" width="400" height="400"></canvas>
</div>
</div>
}
}
</div>
@section scripts {
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
var chartdata = @Html.Raw(Json.Serialize(Model.PageChartConnectors));
var theLabels = [];
var theDatas = [];
var theBackgrounds = [];
var theBorders = [];
$.each(chartdata, function (index, value) {
theLabels.push(value.chartLabel);
theDatas.push(value.chartData);
theBackgrounds.push(value.chartBackground.toString())
theBorders.push(value.chartBorder.toString())
});
var ctx = document.getElementById('myChart').getContext('2d');
var theOptions = {
responsive: false,
maintainAspectRatio: true,
scale: {
ticks: {
min: 0,
max: 100
}
}
};
var chart = new Chart(ctx, {
type: 'polarArea',
options: theOptions,
data: {
labels: theLabels,
datasets: [{
label: 'demo',
backgroundColor: theBackgrounds,
borderColor: theBorders,
data: theDatas
}]
}
});
</script>
}

View File

@@ -0,0 +1,455 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Newtonsoft.Json;
using Quizzer.Extensions;
using Quizzer.Models;
using Quizzer.OpenIDConnect;
using Quizzer.Persistens;
using System.Collections;
using System.IdentityModel.Tokens.Jwt;
namespace Quizzer.Pages
{
public class IndexModel : PageModel
{
// Generat landing page
// Teachers get navigation options
// Students are presented with selection of available tests
[BindProperty]
public string PageQuizzID { get; set; }
[BindProperty]
public List<JournalHtml> PageTests { get; set; }
[BindProperty]
public OpenIDUCNData ucnData { get; set; }
[BindProperty]
public string UserID { get; set; }
[BindProperty]
public string StudentState { get; set; }
[BindProperty]
public MCQuestion PageMCQuestion { get; set; }
[BindProperty]
public List<MCQuestion> PageMCQuestionReview { get; set; }
[BindProperty]
public List<List<bool>> PageMCResponseReview { get; set; }
[BindProperty]
public List<int> PageMCRandomized { get; set; }
[BindProperty]
public string myFullName { get; set; }
[BindProperty]
public List<ChartConnector> PageChartConnectors { get; set; }
private readonly ILogger<IndexModel> _logger;
public IndexModel(ILogger<IndexModel> logger)
{
_logger = logger;
}
private bool isDebug;
private string KCUserID { get; set; }
private DbOps dbManager = new();
private OpenIDConnectUtils OpenIDUtils = new();
private void DumpEnvironment()
{
Console.WriteLine("HEADER DUMP START");
foreach (var e in HttpContext.Request.Headers)
{
Console.WriteLine(e.Key + ":" + e.Value);
}
Console.WriteLine("HEADER DUMP END");
Console.WriteLine("ENVIRONMENT DUMP START");
foreach (DictionaryEntry e in System.Environment.GetEnvironmentVariables())
{
Console.WriteLine(e.Key + ":" + e.Value);
}
Console.WriteLine("ENVIRONMENT DUMP END");
}
public void Common()
{
isDebug = MyConfiguration.Get()["DEBUG"] == "Y";
string? ucnDataJson = OpenIDUtils.GetJwtClaim(HttpContext, "ucn");
KCUserID = OpenIDUtils.GetJwtClaim(HttpContext, "sub");
ucnData = JsonConvert.DeserializeObject<OpenIDUCNData>(ucnDataJson);
UserID = OpenIDUtils.GetJwtClaim(HttpContext, "email").HashSHA256();
myFullName = OpenIDUtils.GetJwtClaim(HttpContext, "name");
}
private void Msg(string msg)
{
Boolean debug = true;
if (debug)
{
Console.WriteLine(msg);
}
}
private void Journal(string Msg)
{
Console.WriteLine("JOURNAL:" + Msg);
}
// =============================================================
// From SSO session:
// OIDC_CLAIM_email = "KAJE@ucn.dk"
// OIDC_CLAIM_family_name = "Jeppesen"
// OIDC_CLAIM_given_name = "Karsten"
// OIDC_CLAIM_name = "Karsten Jeppesen"
List<Question> PopulateQList(string theCategory)
{
// Get the questions in the category
List<Question> list = dbManager.GetQuestions(theCategory);
// Now get the subordinate categories
List<Cat> catList = dbManager.GetChildCategories(theCategory);
foreach (Cat cat in catList)
{
list.AddRange(PopulateQList(cat.CId));
}
return list;
}
private void PrepareForUser(string theRole, string theClass)
{
foreach (Quizz theQuizz in dbManager.GetQuizzesClass(theClass))
{
// Make sure that for a quizz is generated for this individual
Journal2 theJournal = dbManager.GetJournal2(UserID, theQuizz.QuizzId);
if (theJournal == null)
{
// Make up a new journal
Journal2 newJournal = new()
{
PID = UserID,
QuizzID = theQuizz.QuizzId,
Category = theQuizz.Category,
Anonymous = theQuizz.Anonymous,
IsOpen = 1,
Score = 0,
ScoreAcc = 0,
};
//List<Item>items = JsonConvert.DeserializeObject<List<Item>>(theJournal.JournalJson);
newJournal.Items = new();
List<Question> myQuestions = PopulateQList(theQuizz.Category);
//List<Question> myQuestions = dbManager.GetQuestions(theQuizz.Category);
int theSize = (myQuestions.Count < theQuizz.QSize) ? myQuestions.Count : theQuizz.QSize;
Random rnd = new Random();
while (theSize < myQuestions.Count)
{
myQuestions.RemoveAt(rnd.Next(myQuestions.Count));
}
// we should now have a random selection of theSize questions
// Lets mix the deck
for (int indx = 0; indx < myQuestions.Count; indx++)
{
int target = rnd.Next(myQuestions.Count);
myQuestions.Add(myQuestions[target]);
myQuestions.RemoveAt(target);
}
foreach (Question question in myQuestions)
{
newJournal.ScoreAcc++;
newJournal.Items.Add(new Journal2Item
{
QuestionID = question.QuestionID,
IsOpen = 1,
Score = 0,
MaxScore = 1,
});
}
dbManager.PostJournal2(newJournal);
}
}
}
private void BuildTestSelection()
{
List<JournalHtml> myTests = new();
List<Journal2> myJournals = dbManager.GetJournal2(UserID);
foreach (Journal2 myJournal in myJournals)
{
Quizz theQuizz = dbManager.GetQuizz(myJournal.QuizzID);
if (theQuizz == null) continue;
JournalHtml myHtml = new()
{
Journal = myJournal,
Text = theQuizz.Descr,
};
myTests.Add(myHtml);
}
PageTests = myTests;
}
private bool PresentQuestion()
{
Journal2 myJournal = dbManager.GetJournal2(UserID, PageQuizzID);
if (myJournal == null)
{
Response.Cookies.Delete("PageQuizzID");
return false;
}
//List<Item> myItems = JsonConvert.DeserializeObject<List<Item>>(myJournal.JournalJson);
Random rnd = new Random();
foreach (var item in myJournal.Items)
{
if (item.IsOpen == 1)
{
// Get the question
MCQuestion myMCQuestion = dbManager.GetMCQuestion(item.QuestionID);
List<int> myRndSeq = new List<int>();
for (int indx = 0; indx < myMCQuestion.AList.Count(); indx++)
{
myRndSeq.Add(indx);
myMCQuestion.AList[indx].Aok = false;
}
PageMCRandomized = new List<int>();
while (myRndSeq.Count > 0)
{
int indx = rnd.Next(myRndSeq.Count);
PageMCRandomized.Add(myRndSeq[indx]);
myRndSeq.RemoveAt(indx);
}
PageMCQuestion = myMCQuestion;
return true;
}
}
FinalizeTest();
return false;
}
private void FinalizeTest()
{
Journal2 myJournal = dbManager.GetJournal2(UserID, PageQuizzID);
myJournal.IsOpen = 0;
dbManager.UpdateJournal2(myJournal);
Response.Cookies.Delete("PageQuizzID");
}
private void RegisterAnswer()
{
Journal2 myJournal = dbManager.GetJournal2(UserID, PageQuizzID);
foreach (var item in myJournal.Items)
{
if (item.IsOpen == 1)
{
// Get the question
MCQuestion myMCQuestion = dbManager.GetMCQuestion(item.QuestionID);
if (myMCQuestion != null)
{
int theScore = item.MaxScore;
int answers = 0;
item.ReplyMC = new();
for (int indx = 0; indx < myMCQuestion.AList.Count(); indx++)
{
item.ReplyMC.Add(PageMCQuestion.AList[indx].Aok);
if (myMCQuestion.AList[indx].Aok != PageMCQuestion.AList[indx].Aok) theScore = 0;
if (PageMCQuestion.AList[indx].Aok) answers++;
}
if (answers != 1) return; // Must have one answer and one only
item.Score = theScore;
myJournal.Score += theScore;
item.IsOpen = 0;
dbManager.UpdateJournal2(myJournal);
// Now register statistics
List<int> counter = new List<int>();
for (int indx = 0; indx < myMCQuestion.AList.Count(); indx++)
{
if (PageMCQuestion.AList[indx].Aok) { counter.Add(1); } else { counter.Add(0); }
}
dbManager.UpdateStats(PageQuizzID, item.QuestionID, counter);
PageMCQuestion.AList.Clear();
}
return;
}
}
return;
}
private void PrepareReview()
{
PageMCQuestionReview = new();
PageMCResponseReview = new();
List<StatisticsStudentByCategory> StatStudent = new();
Journal2 myJournal = dbManager.GetJournal2(UserID, PageQuizzID);
if (myJournal == null)
{
Response.Cookies.Delete("PageQuizzID");
return;
}
for (int index = 0; index < myJournal.Items.Count; index++)
{
string background;
MCQuestion question = dbManager.GetMCQuestion(myJournal.Items[index].QuestionID);
PageMCQuestionReview.Add(question);
PageMCResponseReview.Add(myJournal.Items[index].ReplyMC);
// create statistics
StatisticsStudentByCategory theStat = StatStudent.Find(x => x.CategoryID == question.Category);
if (theStat == null)
{ // need a new one
Cat cat = dbManager.GetCat(question.Category);
StatStudent.Add(new StatisticsStudentByCategory { CategoryID = cat.CId, CategoryName = cat.Txt });
theStat = StatStudent[StatStudent.Count - 1];
}
theStat.Update(myJournal.Items[index].Score, myJournal.Items[index].MaxScore);
// https://social.msdn.microsoft.com/Forums/en-US/7f96525d-7253-4f08-87f8-88ae526834f9/razor-pages-with-chartjs?forum=aspdotnetcore
PageChartConnectors = new List<ChartConnector>();
//background = "getRandomColor()";
var random = new Random();
foreach (StatisticsStudentByCategory item in StatStudent)
{
background = String.Format("#{0:X6}", random.Next(0x1000000)); // = "#A197B9"
PageChartConnectors.Add(new ChartConnector
{
ChartLabel = item.CategoryName,
ChartData = item.percentage.ToString(),
ChartBackground = background,
ChartBorder = "#FFBBBB"
});
}
//PageChartConnectors.Add(new ChartConnector
//{
// ChartLabel = "100%",
// ChartData = "100",
// ChartBackground = "#000000",
// ChartBorder = "#FFBBBB"
//});
//PageChartConnectors.Add(new ChartConnector { ChartLabel = "Jan", ChartData = "31", ChartBackground = "#FF0000", ChartBorder = "#000000" });
}
}
private void CheckPageQuizzID()
{
if (PageQuizzID == null)
{
//PrepareForUser(ucnData.theRole, ucnData.theClass.FirstOrDefault());
foreach (var item in ucnData.theClass)
{
PrepareForUser(ucnData.theRole, item);
}
BuildTestSelection();
if (ucnData.theRole == "student" || ucnData.theRole == "developer") StudentState = "QuizzSelect";
}
else
{
if (!QuizzRunning())
{
//PageQuizzID = null;
if (ucnData.theRole == "student" || ucnData.theRole == "developer") StudentState = "QuizzSelect";
PrepareForUser(ucnData.theRole, ucnData.theClass.FirstOrDefault());
BuildTestSelection();
// Review selected test?
PrepareReview();
}
}
}
private void CheckTheClass()
{
if (ucnData.theClass.Count > 0)
{
foreach (var item in ucnData.theClass)
{
if (item.Contains("-CSD-"))
{
if (dbManager.GetTheClasses().Find(x => x.Name == item) == null) dbManager.CreateTheClass(new TheClass { Name = item });
}
}
}
}
public IActionResult OnGet(string category, string count)
{
try
{
// if the entity can not be categorized then send him to classselect
Common();
}
catch
{
return Redirect("https://ClassSelect.a.ucnit.eu");
}
if ((ucnData.theClass.Count == 0) || (ucnData.theClass[0] == "PleaseSelectAClass"))
{
// No class found
return Redirect("https://ClassSelect.a.ucnit.eu");
}
PageQuizzID = HttpContext.Request.Cookies["PageQuizzID"];
CheckPageQuizzID();
// Next line should be replaced by call to KeyCloakGetUsersGroups
// CheckTheClass();
return Page();
}
private bool QuizzRunning()
{
StudentState = "QuizzRunning";
return PresentQuestion();
}
public IActionResult OnPost(string action, string value, string quizzInProgress, string target)
{
Common();
switch (action)
{
case "startTest":
// NEW: Coverage: register who takes the test
Quizz theQuizz = dbManager.GetQuizz(value);
if (! theQuizz.Anonymous)
{ // Only register coverage if the test is not anonymous
dbManager.SetCoverage(OpenIDUtils.GetJwtClaim(HttpContext, "email"), OpenIDUtils.GetJwtClaim(HttpContext, "name"), ucnData.theClass.FirstOrDefault());
}
if (isDebug) Console.WriteLine(OpenIDUtils.GetJwtClaim(HttpContext, "name") + ": REMOTE ADDR: " + HttpContext.Request.Headers["X-Forwarded-For"]);
Console.WriteLine(OpenIDUtils.GetJwtClaim(HttpContext, "name") + ": REMOTE ADDR: " + HttpContext.Request.Headers["X-Forwarded-For"]);
PageQuizzID = value;
QuizzRunning();
break;
case "reviewTest":
PageQuizzID = value;
break;
case "QuizzRunning":
QuizzRunning();
break;
case "registerAnswer":
PageQuizzID = quizzInProgress;
RegisterAnswer();
break;
case "deleteMyTest": //Student should not be able to delete journal as this resets the journal
dbManager.DeleteJournal2(UserID, target);
break;
default:
break;
}
if (PageQuizzID != null)
{
Response.Cookies.Append("PageQuizzID", PageQuizzID, new Microsoft.AspNetCore.Http.CookieOptions
{
Secure = true,
HttpOnly = true,
SameSite = Microsoft.AspNetCore.Http.SameSiteMode.None,
});
}
return Redirect("/");
}
}
}

View File

@@ -0,0 +1,31 @@
@page
@model Quizzer.Pages.IndexModel
@{
ViewData["Title"] = "Privacy Policy";
}
<div class="container-fluid">
<div class="row">
<div class="col-xl-12" style="background-color: lightpink">
<h1><center>@ViewData["Title"]</center></h1>
</div>
</div>
<div class="row">
<div class="col-xl-12">
<h5>Opsamlede data</h5>
<p>Et personhenførbart datasæt opsamles: [Person for- og efternavn, E-mail adresse, Klasse, Dato for test]</p>
<h5>Formål med opsamlingen af datasættet</h5>
<p>Kvalitetsvurdering af undervisning og didaktik.</p>
<h5>Hvem har adgang til data?</h5>
<p>Autentiserede og autoriserede lærere ved UCN (UCN login kræves)</p>
<h5>Lovmæssig baggrund for opsamlingen af datasættet</h5>
<p>Legitim interesse med henvisning til formål</p>
<h5>Hvordan afgøres hvornår datasættet ikke længere tjener et formål?</h5>
<p>Når sidste session i undervisningsgang i forløbet er afsluttet</p>
<h5>Destruktion eller anonymisering af datasættet</h5>
<p>Datasættet destrueres automatisk senest i starten af januar og i starten af juli</p>
<h5>Kontaktperson</h5>
<p>Lektor Karsten Jeppesen, kaje@ucn.dk</p>
</div>
</div>
</div>

View File

@@ -0,0 +1,19 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Quizzer.Pages
{
public class PrivacyModel : PageModel
{
private readonly ILogger<PrivacyModel> _logger;
public PrivacyModel(ILogger<PrivacyModel> logger)
{
_logger = logger;
}
public void OnGet()
{
}
}
}

View File

@@ -0,0 +1,182 @@
@page
@model Quizzer.Pages.QuestionsModel
@Html.AntiForgeryToken()
@{
ViewData["Title"] = "Quizzer";
Layout = "_Layout-teacher";
}
<div class="container-fluid">
<div class="row">
<div class="col-xl-12" style="background-color: lightgreen">
<h1><center>Q Management</center></h1>
</div>
</div>
<div class="row">
<div class="col-xl-12" style="background-color: lemonchiffon">
<form class="form-horizontal" method="post">
<input type="hidden" name="action" value="changeCategory" />
Subject: @Html.Raw(Model.CatMenu)<br /><br />
</form>
<form class="form-horizontal" method="post">
<input type="hidden" name="action" value="changeQText" />
<label for="qText">Question Text:</label>
<input id="qText" type="text" class="form-control" placeholder="Enter the question" name="changeTo" value="@Model.questionCookie.QuestionText" style="width:100%" onchange="javascript:this.form.submit()">
</form>
<table class="table table-striped" style="width:100%">
<tr>
<th>Correct</th>
<th>Text</th>
</tr>
@if (Model.questionCookie.AList != null)
{
@for (var indx = 0; indx < Model.questionCookie.AList.Count(); indx++)
{
<tr>
<td>
<form class="form-horizontal" method="post">
<input type="hidden" name="action" value="changeAOk" />
<input type="hidden" name="tochange" value="@indx" />
@if (@Model.questionCookie.AList[@indx].Aok)
{
<input type="hidden" name="changeTo" value="off" />
<button type="submit" class="btn btn-outline-success btn-sm" checked onchange="javascript:this.form.submit()"><i class="bi bi-check2-circle"></i></button>
//<input type="checkbox" class="form-check-input" name="changeTo" checked onchange="javascript:this.form.submit()">
}
else
{
<input type="hidden" name="changeTo" value="on" />
<button type="submit" class="btn btn-outline-danger btn-sm" checked onchange="javascript:this.form.submit()"><i class="bi bi-x-circle"></i></button>
//<input type="checkbox" class="form-check-input" name="changeTo" onchange="javascript:this.form.submit()">
}
</form>
</td>
<td>
<form class="form-horizontal" method="post">
<input type="hidden" name="action" value="changeAText" />
<input type="hidden" name="tochange" value="@indx" />
<input id="qText" type="text" class="form-control" placeholder="Alternative text" name="changeTo" value="@Model.questionCookie.AList[@indx].Atxt" style="width:100%" onchange="javascript:this.form.submit()">
</form>
</td>
</tr>
}
}
</table>
<form class="form-horizontal" method="post">
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
@if (Model.isSubmittable)
{
<button type="submit" class="btn btn-success" name="action" value="create">Submit</button>
}
else
{
<input type="hidden" name="action" value="update" />
<button type="submit" class="btn btn-success" name="action" value="create" disabled>Submit</button>
}
</div>
</div>
</form>
</div>
</div>
<div class="row">
<div class="col-12" style="background-color: lemonchiffon">
<h3>Scan Question</h3>
<form asp-page="" method="post">
<input type="hidden" name="action" value="scan" />
<label for="q_txt">Question text</label>
<div class="form-group green-border-focus">
<textarea class="form-control" name="tochange" rows="5" id="q_txt" onchange="javascript:this.form.submit()"></textarea>
</div>
</form>
</div>
</div>
<div class="row">
<div class="col-12" style="background-color: lemonchiffon">
<h3>Edit Question</h3>
<form asp-page="" method="post">
<input type="hidden" name="action" value="edit" />
<label for="q_txt">Question text</label>
<input type="text" name="txt" id="q_txt" size="100%" onchange="javascript:this.form.submit()" />
</form>
</div>
</div>
@if (Model.questionCookie.Category != null && Model.questionsInCat != null)
{
<div class="row">
<div class="col-12" style="background-color: paleturquoise">
<table class="table" style="width:100%">
<tr>
<td>
<h5>Questions in selected category : @Model.questionsInCat.Count()</h5>
</td>
<td>
<table class="table" style="width:100%">
<thead>
<th>AIKEN format</th>
<th></th>
</thead>
<tr>
<td>
Download
</td>
<td>
<form asp-page="" method="post">
<input type="hidden" name="action" value="download" />
<button type="submit" class="btn btn-outline-danger btn-sm" checked onchange="javascript:this.form.submit()"><i class="bi bi-download"></i></button>
</form>
</td>
</tr>
<tr>
<td>
UpLoad
</td>
<td>
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="action" value="upload" />
<button type="submit" class="btn btn-outline-danger btn-sm" checked onchange="javascript:this.form.submit()"><i class="bi bi-upload"></i></button>
<input type="file" asp-for="Upload" />
</form>
</td>
</tr>
</table>
</td>
</tr>
</table>
<table class="table table-striped" style="width:100%">
<tr>
<th>Question</th>
<th>Delete</th>
</tr>
@foreach (var item in Model.mcQuestionsInCat)
{
<tr>
<td>
@item.QuestionText
@foreach (var altItem in item.AList)
{
@if (altItem.Aok)
{
<li style="background-color:lightgreen">@altItem.Atxt</li>
}
else
{
<li>@altItem.Atxt</li>
}
}
</td>
<td>
<form asp-page="" method="post">
<input type="hidden" name="action" value="delete" />
<input type="hidden" name="tochange" value="@item.QuestionID" />
<button type="submit" class="btn btn-sm btn-outline-danger"><i class="bi bi-trash"></i></button>
</form>
</td>
</tr>
}
</table>
</div>
</div>
}
<div class="row">
</div>
</div>

View File

@@ -0,0 +1,338 @@
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Newtonsoft.Json;
using Quizzer.Extensions;
using Quizzer.Models;
using Quizzer.OpenIDConnect;
using Quizzer.Persistens;
using System.Diagnostics;
using System.IdentityModel.Tokens.Jwt;
using System.Text;
namespace Quizzer.Pages
{
public class QuestionsModel : PageModel
{
[BindProperty]
public OpenIDUCNData ucnData { get; set; }
[BindProperty]
public string UserID { get; set; }
[BindProperty]
public string CatMenu { get; set; }
[BindProperty]
public bool isSubmittable { get; set; }
[BindProperty]
public List<Question> questionsInCat { get; set; }
[BindProperty]
public List<MCQuestion> mcQuestionsInCat { get; set; }
[BindProperty]
public MCQuestion questionCookie { get; set; }
[BindProperty]
public IFormFile Upload { get; set; }
[BindProperty]
public string myFullName { get; set; }
private bool isDebug;
private string KCUserID { get; set; }
private DbOps qManager = new DbOps();
private OpenIDConnectUtils OpenIDUtils = new();
private const string cookieName = "QuestionCookie";
private void GetMyCookie()
{
string jsonString = HttpContext.Request.Cookies[cookieName];
if (jsonString != null)
{
questionCookie = JsonConvert.DeserializeObject<MCQuestion>(jsonString);
return;
}
// Here if no cookie found
questionCookie = new MCQuestion
{
QuestionText = "",
AList = new List<Alternative> { new Alternative
{
Aok = false,
Atxt = "",
Counter = 0
}
}
};
// Save the new cookie
SetMyCookie();
}
private void SetMyCookie()
{
string jsonResult = JsonConvert.SerializeObject(questionCookie);
Response.Cookies.Append(cookieName, jsonResult, new Microsoft.AspNetCore.Http.CookieOptions
{
Secure = true,
HttpOnly = true,
SameSite = Microsoft.AspNetCore.Http.SameSiteMode.None,
});
}
private void RemoveMyCookie()
{
Response.Cookies.Delete(cookieName);
}
public void AlwaysIn()
{
isDebug = MyConfiguration.Get()["DEBUG"] == "Y";
string? ucnDataJson = OpenIDUtils.GetJwtClaim(HttpContext, "ucn");
KCUserID = OpenIDUtils.GetJwtClaim(HttpContext, "sub");
ucnData = JsonConvert.DeserializeObject<OpenIDUCNData>(ucnDataJson);
UserID = OpenIDUtils.GetJwtClaim(HttpContext, "email").HashSHA256();
myFullName = OpenIDUtils.GetJwtClaim(HttpContext, "name");
GetMyCookie();
}
public void AlwaysOut()
{
CatMenu = qManager.MakeCList(UserID, "DropDown", questionCookie.Category);
isSubmittable = true;
if (questionCookie.Category == "" || questionCookie.Category == null)
{
isSubmittable = false;
questionsInCat = null;
}
else
{
questionsInCat = qManager.GetQuestions(questionCookie.Category);
mcQuestionsInCat = new();
foreach ( var item in questionsInCat )
{
MCQuestion newQ= new MCQuestion();
newQ.QuestionID = item.QuestionID;
newQ.QuestionText = item.QuestionText;
newQ.AList = JsonConvert.DeserializeObject<List<Alternative>>(item.ContentJson);
mcQuestionsInCat.Add(newQ);
}
}
try
{
isSubmittable = questionCookie.QuestionText != "" && questionCookie.AList.Where(x => x.Aok == true).Count() == 1 && questionCookie.Category != null;
}
catch
{
RemoveMyCookie();
GetMyCookie();
isSubmittable = false;
}
}
/// <summary>
/// Will attempt to extract questions encoded in the Eiken format.
/// The answer letters (A,B,C etc.) and the word "ANSWER" must be capitalised as shown below, otherwise the import will fail.
/// What is True?
/// A. True
/// B. False
/// ANSWER: A
///
/// What is True?
/// A) True
/// B) False
/// ANSWER: A
///
/// </summary>
/// <param name="theFile">
/// A byte array of the uploaded text file
/// </param>
private void EikenDecode(byte[] theFile)
{
string[] lines = System.Text.Encoding.Default.GetString(theFile).Split(new string[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
char state = 'q'; // state: looking for question text
MCQuestion theQuestion = new(); ;
foreach (string theLine in lines)
{
if (state == 'q')
{
if (theLine.Length == 0) continue;
// something here
theQuestion = new();
theQuestion.QuestionText = theLine;
state = 'A'; // looking for first alternative
continue;
}
if (theLine[0] == state && (theLine[1] == '.' || theLine[1] == ')'))
{ // alternative
theQuestion.AList.Add(new Alternative
{
Aok = false,
Atxt = theLine.Substring(3, theLine.Length - 3),
});
state++;
continue;
}
if (theLine.StartsWith("ANSWER: "))
{
int correct = theLine[8] - 'A';
theQuestion.AList[correct].Aok = true;
theQuestion.Category = questionCookie.Category;
theQuestion.QuestionType = questionCookie.QuestionType;
qManager.InsertMCQuestion(theQuestion);
state = 'q';
continue;
}
}
}
public IActionResult OnGet()
{
AlwaysIn();
AlwaysOut();
if (ucnData.theRole != "teacher" && ucnData.theRole != "developer") return Redirect("/");
return Page();
}
// Will also accept ChatGPT/CoPilot format
//What is the main difference between IPv4 and IPv6?
//
//A) IPv4 uses 32-bit addresses, IPv6 uses 128-bit addresses
//B) IPv4 supports multicast, IPv6 does not
//C) IPv4 is used for local networks, IPv6 is used for the internet
//D) IPv4 uses hexadecimal notation, IPv6 uses binary notation
//E) IPv4 is faster than IPv6
private void OnPostScan(string qText)
{
char[] delim = { '\r', '\n' };
string[] myStrings = qText.Split(delim);
int counter = 0;
// Identify the Question part
while (counter < myStrings.Length)
{
if (myStrings[counter] != "")
{
questionCookie.QuestionText = myStrings[counter];
counter++;
break;
}
counter++;
}
if (counter >= myStrings.Length) return;
// start from scratch
questionCookie.AList.Clear();
while (counter < myStrings.Length)
{
if (myStrings[counter] != "")
{
string alt = myStrings[counter].Trim();
if (alt.Length > 3)
if (alt[1] == ')' || alt[1] == '.')
alt = alt.Remove(0, 3);
questionCookie.AList.Add(new Alternative
{
Atxt = alt,
Aok = false
});
}
counter++;
}
questionCookie.AList.Add(new Alternative
{
Atxt = "",
Aok = false
});
}
public IActionResult OnPost(string action = "NA", string tochange = "", string changeTo = "", string newCategory = "")
{
AlwaysIn();
if (ucnData.theRole != "teacher" && ucnData.theRole != "developer") return Redirect("/");
switch (action)
{
case "changeCategory":
questionCookie.Category = newCategory;
break;
case "changeQText":
questionCookie.QuestionText = changeTo;
break;
case "changeAText":
questionCookie.AList[int.Parse(tochange)].Atxt = changeTo;
if (int.Parse(tochange) == questionCookie.AList.Count - 1)
{
questionCookie.AList.Add(new Alternative
{
Atxt = "",
Aok = false
});
}
break;
case "changeAOk":
// tochange: "0" changeTo: on
questionCookie.AList[int.Parse(tochange)].Aok = (changeTo == "on");
break;
case "create":
qManager.InsertMCQuestion(questionCookie);
questionCookie.QuestionText = "";
questionCookie.QuestionID = "";
questionCookie.AList.Clear();
break;
case "scan":
OnPostScan(tochange);
break;
case "delete":
if (tochange != "") qManager.DeleteQuestion(tochange);
break;
case "download":
Cat cat = qManager.GetCat(questionCookie.Category);
string output = qManager.ExportQuestionsEiken(questionCookie.Category);
string fileName = cat.Txt + "-Eiken.txt";
/* Not such good an idea. just keep as subjects */
/*
while (cat.CParentId != "/")
{
cat = qManager.GetCat(cat.CParentId);
fileName = cat.Txt + "-" + fileName;
}
*/
var contentType = "text/plain";
var bytes = Encoding.UTF8.GetBytes(output);
var result = new FileContentResult(bytes, contentType);
result.FileDownloadName = fileName;
SetMyCookie();
return result;
break;
case "upload": // https://www.learnrazorpages.com/razor-pages/forms/file-upload may benefit from async
if (Upload != null)
{
Stream myStream = Upload.OpenReadStream();
byte[] myBuffer = new byte[myStream.Length];
myStream.Position = 0;
myStream.Read(myBuffer, 0, myBuffer.Length);
myStream.Close();
EikenDecode(myBuffer);
}
break;
default:
break;
}
CatMenu = qManager.MakeCList(UserID, "DropDown", questionCookie.Category);
isSubmittable = questionCookie.AList.Where(x => x.Aok == true).Count() == 1 && questionCookie.Category != null;
SetMyCookie();
AlwaysOut();
return Page();
}
}
}

View File

@@ -0,0 +1,112 @@
@page
@model Quizzer.Pages.QuizzesModel
@{
ViewData["Title"] = "Quizzer";
Layout = "_Layout-teacher";
}
<div class="container-fluid">
<div class="row">
<div class="col-xl-12" style="background-color: lightgreen">
<h1><center>Quizz Management</center></h1>
</div>
</div>
<div class=row>
<div class="col-xl-12" style="background-color: lemonchiffon">
<table>
<tr>
<td>
Select Category
</td>
<td>
<form class="form-horizontal" method="post">
<input type="hidden" name="action" value="changeCategory" />
@Html.Raw(Model.CategorySelect)
</form>
</td>
</tr>
<tr>
<td>
Select Class
</td>
<td>
<form class="form-horizontal" method="post">
<input type="hidden" name="action" value="changeClass" />
<select name="target" onchange="javascript:this.form.submit()">
<option value=""> Select the Class </option>
@foreach (var item in @Model.TheClassList)
{
@if (item == Model.pageQuizz.Class)
{
<option value="@item" selected> @item </option>
}
else
{
<option value="@item"> @item </option>
}
}
</select>
</form>
</td>
</tr>
</table>
<form class="form-horizontal" method="post">
@*
<label for="newQuizz_Class" class="form-label">Class</label>
@Html.TextBoxFor(m => m.pageQuizz.Class)
@Html.DropDownListFor(m => m.pageQuizz.Class, new SelectList(Model.TheClassList), "Select the Class")
*@
<br />
<label for="newQuizz_Descr" class="form-label">Description</label>
@Html.TextAreaFor(m => m.pageQuizz.Descr, 10,40, htmlAttributes: new {style="width: 100%; max-width: 100%;" })<br />
<label for="newQuizz_Anonymous" class="form-label">Anonymous quiz</label>
@Html.CheckBoxFor(m => m.pageQuizz.Anonymous)<br />
<label for="newQuizz_Qsize" class="form-label">Number of questions</label>
@Html.TextBoxFor(m => m.pageQuizz.QSize, new { @type = "number" })
<button type="submit" class="btn btn-primary" name="action" value="createQuizz">Submit</button>
</form>
</div>
</div>
<div class=row>
<div class="col-xl-12" style="background-color: lightgreen">
<h5>Tests in system</h5>
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>Class</th>
<th>Size</th>
<th>Category</th>
<th>Anonymous</th>
<th>Description</th>
</tr>
</thead>
@foreach (var item in @Model.QuizzList)
{
<tr>
<td>
@item.Class
</td>
<td>
@item.QSize
</td>
<td>
@item.CategoryText
</td>
<td>
@item.Anonymous
</td>
<td>
@item.Descr
</td>
<td>
<form asp-page="" method="post">
<input type="hidden" name="action" value="delete" />
<input type="hidden" name="target" value="@item.QuizzId" />
<button type="submit" class="btn btn-sm btn-outline-danger"><i class="bi bi-trash"></i></button>
</form>
</td>
</tr>
}
</table>
</div>
</div>
</div>

View File

@@ -0,0 +1,145 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Newtonsoft.Json;
using Quizzer.Extensions;
using Quizzer.Implementations;
using Quizzer.Models;
using Quizzer.OpenIDConnect;
using Quizzer.Persistens;
using System.IdentityModel.Tokens.Jwt;
namespace Quizzer.Pages
{
public class QuizzesModel : PageModel
{
[BindProperty]
public OpenIDUCNData ucnData { get; set; }
[BindProperty]
public string UserID { get; set; }
[BindProperty]
public List<QuizzDN> QuizzList { get; set; }
[BindProperty]
public string CategorySelect { get; set; }
[BindProperty]
public List<string> TheClassList { get; set; }
[BindProperty]
public Quizz pageQuizz { get; set; }
[BindProperty]
public string myFullName { get; set; }
private bool isDebug;
private string KCUserID { get; set; }
private DbOps dbManager = new();
private OpenIDConnectUtils OpenIDUtils = new();
private Quizz quizzCookie;
public void Common()
{
isDebug = MyConfiguration.Get()["DEBUG"] == "Y";
string? ucnDataJson = OpenIDUtils.GetJwtClaim(HttpContext, "ucn");
KCUserID = OpenIDUtils.GetJwtClaim(HttpContext, "sub");
ucnData = JsonConvert.DeserializeObject<OpenIDUCNData>(ucnDataJson);
UserID = OpenIDUtils.GetJwtClaim(HttpContext, "email").HashSHA256();
myFullName = OpenIDUtils.GetJwtClaim(HttpContext, "name");
}
private void GetMyCookie()
{
string jsonString = HttpContext.Request.Cookies["quizzCookie"];
if (jsonString != null)
{
quizzCookie = JsonConvert.DeserializeObject<Quizz>(jsonString);
return;
}
// Here if no cookie found
quizzCookie = new Quizz
{
Category = "",
Class = "",
Anonymous = false,
Descr = "",
QSize = 5,
QuizzId = "",
};
// Save the new cookie
SetMyCookie();
}
private void SetMyCookie()
{
string jsonResult = JsonConvert.SerializeObject(quizzCookie);
Response.Cookies.Append("quizzCookie", jsonResult, new Microsoft.AspNetCore.Http.CookieOptions
{
Secure = true,
HttpOnly = true,
SameSite = Microsoft.AspNetCore.Http.SameSiteMode.None,
});
}
private void RemoveMyCookie()
{
Response.Cookies.Delete("quizzCookie");
}
public IActionResult OnGet()
{
Common();
if (ucnData.theRole != "teacher" && ucnData.theRole != "developer") return Redirect("/");
GetMyCookie();
pageQuizz = JsonConvert.DeserializeObject<Quizz>(JsonConvert.SerializeObject(quizzCookie));
QuizzList = dbManager.GetQuizzesTeacher(UserID);
CategorySelect = dbManager.MakeCList(UserID, "DropDownIndented", quizzCookie.Category);
TheClassList = new();
KeyCloak KCApi = new KeyCloak();
List<KCGroups> myGroups = KCApi.KeyCloakGetUsersGroups(KCUserID);
foreach (var group in myGroups)
{
if (group.name.Contains("-CSD-")) TheClassList.Add(group.name);
}
return Page();
}
public IActionResult OnPost(string action = "na", string categoryParent = "", string newCategory = "na", string target = "na")
{
Common();
GetMyCookie();
//tagPath = JsonConvert.DeserializeObject<List<Tags>>(theTagPath);
switch (action)
{
case "createQuizz":
quizzCookie.Descr = pageQuizz.Descr;
quizzCookie.Anonymous = pageQuizz.Anonymous;
quizzCookie.QSize = pageQuizz.QSize;
quizzCookie.Owner = UserID;
dbManager.InsertQuizz(quizzCookie);
break;
case "changeCategory":
quizzCookie.Category = newCategory;
break;
case "changeClass":
pageQuizz.Class = quizzCookie.Class = target;
break;
case "delete":
dbManager.DeleteQuizz(target);
dbManager.DeleteStatistics(target);
dbManager.DeleteJournalByQuizz(target);
break;
case "na":
if (newCategory != "na") quizzCookie.Category = newCategory;
break;
default:
break;
}
SetMyCookie();
return Redirect("/Quizzes");
}
}
}

View File

@@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Quizzer</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css">
<link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container">
<a class="navbar-brand" asp-area="" asp-page="/Index">Quizzer</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Questions">Questions</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Categories">Categories</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Quizzes">Quizzes</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Statistics">Statistics</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Coverage">Coverage</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>
<footer class="border-top footer text-muted">
<div class="container">
&copy; 2022 - Quizzer
</div>
</footer>
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
@RenderSection("Scripts", required: false)
</body>
</html>

View File

@@ -0,0 +1,48 @@
/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification
for details on configuring this project to bundle and minify static web assets. */
a.navbar-brand {
white-space: normal;
text-align: center;
word-break: break-all;
}
a {
color: #0077cc;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.nav-pills .nav-link.active, .nav-pills .show > .nav-link {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.border-top {
border-top: 1px solid #e5e5e5;
}
.border-bottom {
border-bottom: 1px solid #e5e5e5;
}
.box-shadow {
box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
}
button.accept-policy {
font-size: 1rem;
line-height: inherit;
}
.footer {
position: absolute;
bottom: 0;
width: 100%;
white-space: nowrap;
line-height: 60px;
}

View File

@@ -0,0 +1,67 @@
<!DOCTYPE html>
<html lang="en">
@model IndexModel
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Quizzer</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css">
<link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container">
<a class="navbar-brand" asp-area="" asp-page="/Index">Quizzer</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
<ul class="navbar-nav flex-grow-1">
@if (Model.ucnData.theRole == "teacher" || Model.ucnData.theRole == "developer")
{
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Questions">Questions</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Categories">Categories</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Quizzes">Quizzes</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Statistics">Statistics</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Coverage">Coverage</a>
</li>
}
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<div class="container-fluid" style="background-image: url('images/quiz.jpg'); height:100vh;">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>
<footer class="border-top footer text-muted">
<div class="container">
&copy; 2022 - Quizzer
</div>
</footer>
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
@RenderSection("Scripts", required: false)
</body>
</html>

View File

@@ -0,0 +1,48 @@
/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification
for details on configuring this project to bundle and minify static web assets. */
a.navbar-brand {
white-space: normal;
text-align: center;
word-break: break-all;
}
a {
color: #0077cc;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.nav-pills .nav-link.active, .nav-pills .show > .nav-link {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.border-top {
border-top: 1px solid #e5e5e5;
}
.border-bottom {
border-bottom: 1px solid #e5e5e5;
}
.box-shadow {
box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
}
button.accept-policy {
font-size: 1rem;
line-height: inherit;
}
.footer {
position: absolute;
bottom: 0;
width: 100%;
white-space: nowrap;
line-height: 60px;
}

View File

@@ -0,0 +1,2 @@
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>

View File

@@ -0,0 +1,84 @@
@page
@model Quizzer.Pages.StatisticsModel
@Html.AntiForgeryToken()
@{
ViewData["Title"] = "Quizzer";
Layout = "_Layout-teacher";
}
<div class="container-fluid">
<div class="row">
<div class="col-xl-12" style="background-color: lightgreen">
<h1><center>Statistics</center></h1>
</div>
</div>
<div class="row">
<div class="col-xl-12">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th></th>
<th>Class</th>
<th>Quizz</th>
</tr>
</thead>
@foreach (var item in Model.quizzList)
{
<tr>
<td>
<form class="form-horizontal" method="post">
<input type="hidden" name="action" value="selectQuizz" />
<input type="hidden" name="changeTo" value="@item.QuizzId" />
<button type="button" class="btn btn-outline-primary" onclick="javascript:this.form.submit()">Stats</button>
</form>
</td>
<td>@item.Class</td>
<td>@item.CategoryText</td>
<td>
<form asp-page="" method="post">
<input type="hidden" name="action" value="delete" />
<input type="hidden" name="changeTo" value="@item.QuizzId" />
<button type="submit" class="btn btn-sm btn-outline-danger"><i class="bi bi-trash"></i></button>
</form>
</td>
</tr>
}
</table>
</div>
</div>
@if (Model.PageStatistics != null)
{
@if (Model.PageStatistics.Questions.Count > 0 && Model.PageStatistics.MCList.Count > 0)
{
<div class="row">
<h3>Statistics</h3><br>
@for (int Qindex = 0; Qindex < Model.PageStatistics.Questions.Count; Qindex++)
{
<table class="table table-striped table-bordered">
<thead>
<tr>
<th style="width: 10%">Count</th>
<th>@Model.PageStatistics.Questions[Qindex].QuestionText</th>
</tr>
</thead>
@for (int Aindex = 0; Aindex < @Model.PageStatistics.Questions[Qindex].AList.Count; Aindex++)
{
<tr>
<td class="align-text-top">@Model.PageStatistics.MCList[Qindex].Counter[Aindex]</td>
@if (@Model.PageStatistics.Questions[Qindex].AList[Aindex].Aok)
{
<td style="background-color:lightgreen">@Model.PageStatistics.Questions[Qindex].AList[Aindex].Atxt</td>
}
else
{
<td>@Model.PageStatistics.Questions[Qindex].AList[Aindex].Atxt</td>
}
</tr>
}
</table>
}
</div>
}
}
<div class="row">
</div>
</div>

View File

@@ -0,0 +1,126 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Newtonsoft.Json;
using Quizzer.Extensions;
using Quizzer.Models;
using Quizzer.OpenIDConnect;
using Quizzer.Persistens;
using System.IdentityModel.Tokens.Jwt;
namespace Quizzer.Pages
{
public class StatisticsModel : PageModel
{
[BindProperty]
public OpenIDUCNData ucnData { get; set; }
[BindProperty]
public string UserID { get; set; }
[BindProperty]
public List<QuizzDN> quizzList { get; set; }
[BindProperty]
public StatisticsMCDN PageStatistics { get; set; }
[BindProperty]
public string myFullName { get; set; }
private bool isDebug;
private string KCUserID { get; set; }
private DbOps dbManager = new();
private OpenIDConnectUtils OpenIDUtils = new();
public string statisticsCookie { get; set; }
private const string cookieName = "StatisticsCookie";
private void GetMyCookie()
{
string jsonString = HttpContext.Request.Cookies[cookieName];
if (jsonString != null)
{
statisticsCookie = JsonConvert.DeserializeObject<string>(jsonString);
return;
}
// Here if no cookie found
statisticsCookie = "";
// Save the new cookie
SetMyCookie();
}
private void SetMyCookie()
{
string jsonResult = JsonConvert.SerializeObject(statisticsCookie);
Response.Cookies.Append(cookieName, jsonResult, new Microsoft.AspNetCore.Http.CookieOptions
{
Secure = true,
HttpOnly = true,
SameSite = Microsoft.AspNetCore.Http.SameSiteMode.None,
});
}
private void RemoveMyCookie()
{
Response.Cookies.Delete(cookieName);
}
//
public void Common()
{
isDebug = MyConfiguration.Get()["DEBUG"] == "Y";
string? ucnDataJson = OpenIDUtils.GetJwtClaim(HttpContext, "ucn");
KCUserID = OpenIDUtils.GetJwtClaim(HttpContext, "sub");
ucnData = JsonConvert.DeserializeObject<OpenIDUCNData>(ucnDataJson);
UserID = OpenIDUtils.GetJwtClaim(HttpContext, "email").HashSHA256();
myFullName = OpenIDUtils.GetJwtClaim(HttpContext, "name");
}
public IActionResult OnGet()
{
Common();
if (ucnData.theRole != "teacher" && ucnData.theRole != "developer") return Redirect("/");
GetMyCookie();
quizzList = dbManager.GetQuizzesTeacher(UserID);
if (statisticsCookie != "")
{
// traverse all statistics
PageStatistics = dbManager.GetStatisticsMCDN(statisticsCookie);
}
else
{
PageStatistics = null;
}
return Page();
}
public IActionResult OnPost(string action = "na", string changeTo = "na")
{
Common();
GetMyCookie();
switch (action)
{
case "selectQuizz":
statisticsCookie = changeTo;
break;
case "delete":
dbManager.DeleteStatistics(changeTo);
break;
case "na":
break;
default:
break;
}
SetMyCookie();
return Redirect("/Statistics");
}
}
}

View File

@@ -0,0 +1,3 @@
@using Quizzer
@namespace Quizzer.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

View File

@@ -0,0 +1,3 @@
@{
Layout = "_Layout";
}

View File

@@ -0,0 +1,14 @@
@page
@model Quizzer.Pages.QuestionsModel
@Html.AntiForgeryToken()
@{
ViewData["Title"] = "Quizzer";
Layout = "_Layout-teacher";
}
<div class="container-fluid">
<div class="row">
<div class="col-xl-12" style="background-color: lightgreen">
<h1><center>Image Management</center></h1>
</div>
</div>
</div>

View File

@@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Quizzer.Pages
{
public class imagesModel : PageModel
{
public void OnGet()
{
}
}
}