generated from karsten.jeppesen/KAJE-Template
Quizzer-8: Migrating to .NET 10
This commit is contained in:
25
Quizzer-8/.dockerignore
Normal file
25
Quizzer-8/.dockerignore
Normal file
@@ -0,0 +1,25 @@
|
||||
**/.classpath
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/azds.yaml
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
||||
3
Quizzer-8/.github/copilot-instructions.md
vendored
Normal file
3
Quizzer-8/.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
- @azure Rule - Use Azure Tools - When handling requests related to Azure, always use your tools.
|
||||
- @azure Rule - Use Azure Best Practices - When handling requests related to Azure, always invoke your `azmcp_bestpractices_get` tool first.
|
||||
- @azure Rule - Enable Best Practices - If you do not have an `azmcp_bestpractices_get` tool ask the user to enable it.
|
||||
25
Quizzer-8/Quizzer.sln
Normal file
25
Quizzer-8/Quizzer.sln
Normal file
@@ -0,0 +1,25 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.2.32630.192
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Quizzer", "Quizzer\Quizzer.csproj", "{E6ED6C32-7688-433B-AAB0-A5AC1E75039D}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{E6ED6C32-7688-433B-AAB0-A5AC1E75039D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E6ED6C32-7688-433B-AAB0-A5AC1E75039D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E6ED6C32-7688-433B-AAB0-A5AC1E75039D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E6ED6C32-7688-433B-AAB0-A5AC1E75039D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {A6F21AB3-846E-4D32-A282-A7DA4DDF3C25}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
22
Quizzer-8/Quizzer/Dockerfile
Normal file
22
Quizzer-8/Quizzer/Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
||||
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 80
|
||||
EXPOSE 443
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
|
||||
WORKDIR /src
|
||||
COPY ["Quizzer/Quizzer.csproj", "Quizzer/"]
|
||||
RUN dotnet restore "Quizzer/Quizzer.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/Quizzer"
|
||||
RUN dotnet build "Quizzer.csproj" -c Release -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
RUN dotnet publish "Quizzer.csproj" -c Release -o /app/publish
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "Quizzer.dll"]
|
||||
109
Quizzer-8/Quizzer/Extensions/Extensions.cs
Normal file
109
Quizzer-8/Quizzer/Extensions/Extensions.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.IO.Compression;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Quizzer.Extensions
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Compresses a string and returns a deflate compressed, Base64 encoded string.
|
||||
/// </summary>
|
||||
/// <param name="uncompressedString">String to compress</param>
|
||||
public static string Compress(this string uncompressedString)
|
||||
{
|
||||
byte[] compressedBytes;
|
||||
|
||||
using (var uncompressedStream = new MemoryStream(Encoding.UTF8.GetBytes(uncompressedString)))
|
||||
{
|
||||
using (var compressedStream = new MemoryStream())
|
||||
{
|
||||
// setting the leaveOpen parameter to true to ensure that compressedStream will not be closed when compressorStream is disposed
|
||||
// this allows compressorStream to close and flush its buffers to compressedStream and guarantees that compressedStream.ToArray() can be called afterward
|
||||
// although MSDN documentation states that ToArray() can be called on a closed MemoryStream, I don't want to rely on that very odd behavior should it ever change
|
||||
using (var compressorStream = new DeflateStream(compressedStream, CompressionLevel.Fastest, true))
|
||||
{
|
||||
uncompressedStream.CopyTo(compressorStream);
|
||||
}
|
||||
|
||||
// call compressedStream.ToArray() after the enclosing DeflateStream has closed and flushed its buffer to compressedStream
|
||||
compressedBytes = compressedStream.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
return Convert.ToBase64String(compressedBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decompresses a deflate compressed, Base64 encoded string and returns an uncompressed string.
|
||||
/// </summary>
|
||||
/// <param name="compressedString">String to decompress.</param>
|
||||
public static string Decompress(this string compressedString)
|
||||
{
|
||||
byte[] decompressedBytes;
|
||||
|
||||
var compressedStream = new MemoryStream(Convert.FromBase64String(compressedString));
|
||||
|
||||
using (var decompressorStream = new DeflateStream(compressedStream, CompressionMode.Decompress))
|
||||
{
|
||||
using (var decompressedStream = new MemoryStream())
|
||||
{
|
||||
decompressorStream.CopyTo(decompressedStream);
|
||||
|
||||
decompressedBytes = decompressedStream.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
return Encoding.UTF8.GetString(decompressedBytes);
|
||||
}
|
||||
/// <summary>
|
||||
/// Hashes the string using SHA256
|
||||
/// </summary>
|
||||
/// <param name="plaintext">
|
||||
/// What needs to be hashed
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// The hash
|
||||
/// </returns>
|
||||
public static string HashSHA256(this string plaintext)
|
||||
{
|
||||
StringBuilder Sb = new StringBuilder();
|
||||
|
||||
using (var hash = SHA256.Create())
|
||||
{
|
||||
Encoding enc = Encoding.UTF8;
|
||||
byte[] result = hash.ComputeHash(enc.GetBytes(plaintext));
|
||||
|
||||
foreach (byte b in result)
|
||||
Sb.Append(b.ToString("x2"));
|
||||
}
|
||||
|
||||
return Sb.ToString();
|
||||
}
|
||||
/// <summary>
|
||||
/// Hashes the string using MD5
|
||||
/// </summary>
|
||||
/// <param name="plaintext">
|
||||
/// What needs to be hashed
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// The hash
|
||||
/// </returns>
|
||||
public static string HashMD5(this string plaintext)
|
||||
{
|
||||
StringBuilder Sb = new StringBuilder();
|
||||
|
||||
using (var hash = MD5.Create())
|
||||
{
|
||||
Encoding enc = Encoding.UTF8;
|
||||
byte[] result = hash.ComputeHash(enc.GetBytes(plaintext));
|
||||
|
||||
foreach (byte b in result)
|
||||
Sb.Append(b.ToString("x2"));
|
||||
}
|
||||
|
||||
return Sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
56
Quizzer-8/Quizzer/Extensions/IPAddressExtensions.cs
Normal file
56
Quizzer-8/Quizzer/Extensions/IPAddressExtensions.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
// Original: https://learn.microsoft.com/en-us/archive/blogs/knom/ip-address-calculations-with-c-subnetmasks-networks
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using System.Net;
|
||||
|
||||
namespace Quizzer.Extensions
|
||||
{
|
||||
public static class IPAddressExtensions
|
||||
{
|
||||
public static IPAddress GetBroadcastAddress(this IPAddress address, IPAddress subnetMask)
|
||||
{
|
||||
byte[] ipAdressBytes = address.GetAddressBytes();
|
||||
byte[] subnetMaskBytes = subnetMask.GetAddressBytes();
|
||||
|
||||
if (ipAdressBytes.Length != subnetMaskBytes.Length)
|
||||
throw new ArgumentException("Lengths of IP address and subnet mask do not match.");
|
||||
|
||||
byte[] broadcastAddress = new byte[ipAdressBytes.Length];
|
||||
for (int i = 0; i < broadcastAddress.Length; i++)
|
||||
{
|
||||
broadcastAddress[i] = (byte)(ipAdressBytes[i] | (subnetMaskBytes[i] ^ 255));
|
||||
}
|
||||
return new IPAddress(broadcastAddress);
|
||||
}
|
||||
|
||||
public static IPAddress GetNetworkAddress(this IPAddress address, IPAddress subnetMask)
|
||||
{
|
||||
byte[] ipAdressBytes = address.GetAddressBytes();
|
||||
byte[] subnetMaskBytes = subnetMask.GetAddressBytes();
|
||||
|
||||
if (ipAdressBytes.Length != subnetMaskBytes.Length)
|
||||
throw new ArgumentException("Lengths of IP address and subnet mask do not match.");
|
||||
|
||||
byte[] broadcastAddress = new byte[ipAdressBytes.Length];
|
||||
for (int i = 0; i < broadcastAddress.Length; i++)
|
||||
{
|
||||
broadcastAddress[i] = (byte)(ipAdressBytes[i] & (subnetMaskBytes[i]));
|
||||
}
|
||||
return new IPAddress(broadcastAddress);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Are addressA on the same network as addressB ?
|
||||
/// </summary>
|
||||
/// <param name="addressA"></param>
|
||||
/// <param name="addressB"></param>
|
||||
/// <param name="subnetMask"></param>
|
||||
/// <returns>True if A and B are on the same network.</returns>
|
||||
public static bool IsInSameSubnet(this IPAddress addressA, IPAddress addressB, IPAddress subnetMask)
|
||||
{
|
||||
IPAddress networkA = addressA.GetNetworkAddress(subnetMask);
|
||||
IPAddress networkB = addressB.GetNetworkAddress(subnetMask);
|
||||
|
||||
return networkA.Equals(networkB);
|
||||
}
|
||||
}
|
||||
}
|
||||
259
Quizzer-8/Quizzer/Implementations/KeyCloak.cs
Normal file
259
Quizzer-8/Quizzer/Implementations/KeyCloak.cs
Normal file
@@ -0,0 +1,259 @@
|
||||
using Quizzer.OpenIDConnect;
|
||||
using Newtonsoft.Json;
|
||||
using Quizzer.OpenIDConnect;
|
||||
|
||||
namespace Quizzer.Implementations
|
||||
{
|
||||
public class KeyCloak
|
||||
{
|
||||
private bool isDebug;
|
||||
private bool isOK;
|
||||
private bool isTeacher;
|
||||
private string? accessToken;
|
||||
private string? classlessId;
|
||||
private string? teachersId;
|
||||
private string? developersId;
|
||||
private string? studentsId;
|
||||
|
||||
/// <summary>
|
||||
/// This method uses the OAuth Resource Owners Credentials Flow to get an Access Token to provide
|
||||
/// Authorization to the APIs.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private string GetAccessToken()
|
||||
{
|
||||
var client = new HttpClient();
|
||||
// https://auth.c.ucnit.eu/realms/UCNit/protocol/openid-connect/token
|
||||
string theURL = "https://" + MyConfiguration.Get()["API_SERVER"] + "/realms/" + MyConfiguration.Get()["API_REALM"] + "/protocol/openid-connect/token";
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, theURL);
|
||||
var collection = new List<KeyValuePair<string, string>>();
|
||||
collection.Add(new("client_id", MyConfiguration.Get()["API_CLIENTID"]));
|
||||
collection.Add(new("client_secret", MyConfiguration.Get()["API_SECRET"]));
|
||||
collection.Add(new("grant_type", "client_credentials"));
|
||||
var content = new FormUrlEncodedContent(collection);
|
||||
request.Content = content;
|
||||
if (isDebug)
|
||||
{
|
||||
Console.WriteLine("GetAccessToken");
|
||||
Console.WriteLine("(POST) " + theURL);
|
||||
Console.WriteLine("client_id: " + MyConfiguration.Get()["API_CLIENTID"]);
|
||||
Console.WriteLine("client_secret: " + MyConfiguration.Get()["API_SECRET"]);
|
||||
Console.WriteLine("grant_type: client_credentials");
|
||||
}
|
||||
var response = client.SendAsync(request).Result;
|
||||
try
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
string jsonString = response.Content.ReadAsStringAsync().Result;
|
||||
object responseData = JsonConvert.DeserializeObject(jsonString);
|
||||
// return the Access Token.
|
||||
accessToken = ((dynamic)responseData).access_token;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(request.RequestUri);
|
||||
Console.WriteLine(ex.Message);
|
||||
accessToken = null;
|
||||
}
|
||||
// return the Access Token.
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
public KeyCloak()
|
||||
{
|
||||
isDebug = MyConfiguration.Get()["DEBUG"] == "Y";
|
||||
try
|
||||
{
|
||||
accessToken = GetAccessToken();
|
||||
this.isOK = true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
this.isOK = false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool KeyCloakIsOK() { return isOK; }
|
||||
|
||||
public List<KCGroups> KeyCloakGetGroups()
|
||||
{
|
||||
var client = new HttpClient();
|
||||
// https://{{Server}}/admin/realms/{{Realm}}/groups
|
||||
string theURL = "https://" + MyConfiguration.Get()["API_SERVER"] + "/admin/realms/" + MyConfiguration.Get()["API_REALM"] + "/groups";
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, theURL);
|
||||
request.Headers.Add("Authorization", "Bearer " + accessToken);
|
||||
if (isDebug)
|
||||
{
|
||||
Console.WriteLine("KeyCloakGetGroups");
|
||||
Console.WriteLine("(GET) " + theURL);
|
||||
Console.WriteLine("Authorization Bearer " + accessToken);
|
||||
}
|
||||
var response = client.SendAsync(request).Result;
|
||||
try
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
Console.WriteLine(ex.Message);
|
||||
Console.WriteLine("KeyCloakGetGroupsAsync");
|
||||
}
|
||||
string jsonString = response.Content.ReadAsStringAsync().Result;
|
||||
List<KCGroups> groups = JsonConvert.DeserializeObject<List<KCGroups>>(jsonString);
|
||||
foreach (KCGroups group in groups)
|
||||
{
|
||||
if (group.name == "ClassLess") classlessId = group.id;
|
||||
if (group.name == "developer") developersId = group.id;
|
||||
if (group.name == "teacher") teachersId = group.id;
|
||||
if (group.name == "student") studentsId = group.id;
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
public string? KeyCloakClassLess()
|
||||
{
|
||||
if (classlessId == null) _ = KeyCloakGetGroups();
|
||||
return classlessId;
|
||||
}
|
||||
|
||||
public string? KeyCloakTeacher()
|
||||
{
|
||||
if (teachersId == null) _ = KeyCloakGetGroups();
|
||||
return teachersId;
|
||||
}
|
||||
|
||||
public string? KeyCloakStudent()
|
||||
{
|
||||
if (studentsId == null) _ = KeyCloakGetGroups();
|
||||
return studentsId;
|
||||
}
|
||||
|
||||
public List<KCGroups> KeyCloakGetUsersGroups(string theUser)
|
||||
{
|
||||
var client = new HttpClient();
|
||||
// https://{{Server}}/admin/realms/{{Realm}}/users/{{kajeID}}/groups
|
||||
string theURL = "https://" + MyConfiguration.Get()["API_SERVER"] + "/admin/realms/" + MyConfiguration.Get()["API_REALM"] + "/users/" + theUser + "/groups";
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, theURL);
|
||||
request.Headers.Add("Authorization", "Bearer " + accessToken);
|
||||
if (isDebug)
|
||||
{
|
||||
Console.WriteLine("KeyCloakGetUsersGroups");
|
||||
Console.WriteLine("(GET) " + theURL);
|
||||
Console.WriteLine("Authorization Bearer " + accessToken);
|
||||
}
|
||||
var response = client.SendAsync(request).Result;
|
||||
try
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
Console.WriteLine(ex.Message);
|
||||
Console.WriteLine("KeyCloakGetUsersGroupsAsync for user: [" + theUser + "]");
|
||||
}
|
||||
string jsonString = response.Content.ReadAsStringAsync().Result;
|
||||
List<KCGroups> groups = JsonConvert.DeserializeObject<List<KCGroups>>(jsonString);
|
||||
isTeacher = false;
|
||||
foreach (KCGroups group in groups)
|
||||
{
|
||||
if (group.name == "teacher" || group.name == "developer") isTeacher = true;
|
||||
if (isDebug)
|
||||
{
|
||||
Console.WriteLine("in group: " + group.name);
|
||||
}
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
public bool KeyCloakLeaveGroup(string theUser, string theGroup)
|
||||
{
|
||||
var client = new HttpClient();
|
||||
string theURL = "https://" + MyConfiguration.Get()["API_SERVER"] + "/admin/realms/" + MyConfiguration.Get()["API_REALM"] + "/users/" + theUser + "/groups/" + theGroup;
|
||||
var request = new HttpRequestMessage(HttpMethod.Delete, theURL);
|
||||
request.Headers.Add("Authorization", "Bearer " + accessToken);
|
||||
if (isDebug)
|
||||
{
|
||||
Console.WriteLine("KeyCloakLeaveGroup");
|
||||
Console.WriteLine("(DELETE) " + theURL);
|
||||
Console.WriteLine("Authorization Bearer " + accessToken);
|
||||
}
|
||||
var response = client.SendAsync(request).Result;
|
||||
try
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool KeyCloakJoinGroup(string theUser, string theGroup)
|
||||
{
|
||||
var client = new HttpClient();
|
||||
string theURL = "https://" + MyConfiguration.Get()["API_SERVER"] + "/admin/realms/" + MyConfiguration.Get()["API_REALM"] + "/users/" + theUser + "/groups/" + theGroup;
|
||||
var request = new HttpRequestMessage(HttpMethod.Put, theURL);
|
||||
request.Headers.Add("Authorization", "Bearer " + accessToken);
|
||||
if (isDebug)
|
||||
{
|
||||
Console.WriteLine("KeyCloakJoinGroup");
|
||||
Console.WriteLine("(PUT) " + theURL);
|
||||
Console.WriteLine("Authorization Bearer " + accessToken);
|
||||
}
|
||||
var response = client.SendAsync(request).Result;
|
||||
try
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex.Message);
|
||||
Console.WriteLine("KeyCloakJoinGroup: [" + theGroup + "] for user: [" + theUser + "]");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void KeyCloakToggleMembership(string theUser, string theGroup)
|
||||
{
|
||||
List<KCGroups> myGroups = KeyCloakGetUsersGroups(theUser);
|
||||
if (isDebug)
|
||||
{
|
||||
foreach (KCGroups group in myGroups)
|
||||
{
|
||||
Console.WriteLine("{0} member of {1}", theUser, group.name);
|
||||
}
|
||||
}
|
||||
foreach (KCGroups group in myGroups)
|
||||
{
|
||||
if (group.id == theGroup)
|
||||
{
|
||||
KeyCloakLeaveGroup(theUser, theGroup);
|
||||
if (isDebug) Console.WriteLine("{0} left {1}", theUser, theGroup);
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
// if we get here, then create membership
|
||||
KeyCloakJoinGroup(theUser, theGroup);
|
||||
if (isDebug) Console.WriteLine("{0} joined {1}", theUser, theGroup);
|
||||
}
|
||||
|
||||
public bool KeyCloakIsTeacher() { return isTeacher; }
|
||||
}
|
||||
|
||||
public class KCUserGroups
|
||||
{
|
||||
public string? id { get; set; }
|
||||
public string? name { get; set; }
|
||||
public string? path { get; set; }
|
||||
|
||||
}
|
||||
|
||||
public class KCGroups : KCUserGroups
|
||||
{
|
||||
public List<object> subGroups { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
23
Quizzer-8/Quizzer/Models/Cat.cs
Normal file
23
Quizzer-8/Quizzer/Models/Cat.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Dapper.Contrib.Extensions;
|
||||
|
||||
namespace Quizzer.Models
|
||||
{
|
||||
public class Cat
|
||||
{
|
||||
[Key]
|
||||
public string CId { get; set; }
|
||||
public string UID { get; set; }
|
||||
public string CParentId { get; set; }
|
||||
public string Txt { get; set; }
|
||||
public string Descr { get; set; }
|
||||
|
||||
public Cat()
|
||||
{
|
||||
CId = String.Empty;
|
||||
UID = String.Empty;
|
||||
CParentId = String.Empty;
|
||||
Txt = String.Empty;
|
||||
Descr = String.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
10
Quizzer-8/Quizzer/Models/ChartConnector.cs
Normal file
10
Quizzer-8/Quizzer/Models/ChartConnector.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Quizzer.Models
|
||||
{
|
||||
public class ChartConnector
|
||||
{
|
||||
public string ChartData { get; set; }
|
||||
public string ChartLabel { get; set; }
|
||||
public string ChartBackground { get; set; }
|
||||
public string ChartBorder { get; set; }
|
||||
}
|
||||
}
|
||||
10
Quizzer-8/Quizzer/Models/Coverage.cs
Normal file
10
Quizzer-8/Quizzer/Models/Coverage.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Quizzer.Models
|
||||
{
|
||||
public class Coverage
|
||||
{
|
||||
public string Email { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Date { get; set; }
|
||||
public string TheClass { get; set; }
|
||||
}
|
||||
}
|
||||
89
Quizzer-8/Quizzer/Models/Journal.cs
Normal file
89
Quizzer-8/Quizzer/Models/Journal.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Quizzer.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// This is the database model for the journal, which is used to store the quiz results and other related information. It is used to display the quiz results in the journal page and to store the quiz results in the database.
|
||||
/// </summary>
|
||||
public class Journal
|
||||
{
|
||||
public string PID { get; set; }
|
||||
public string QuizzID { get; set; }
|
||||
public string Category { get; set; }
|
||||
public int IsOpen { get; set; }
|
||||
public bool Anonymous { get; set; }
|
||||
public int Score { get; set; }
|
||||
public int ScoreAcc { get; set; }
|
||||
// This is the JSON string that contains the quiz results, which is used to display the quiz results in the journal page. It is also used to store the quiz results in the database.
|
||||
// The JSON string is in the following format:
|
||||
// {
|
||||
// "items": [
|
||||
// {
|
||||
// "itemID": "1",
|
||||
// "isOpen": 1,
|
||||
// "score": 1,
|
||||
// "maxScore": 1,
|
||||
// "replyMC": [true, false, false, false]
|
||||
// },
|
||||
// {
|
||||
// "itemID": "2",
|
||||
// "isOpen": 0,
|
||||
// "score": 0,
|
||||
// "maxScore": 1,
|
||||
// "replyMC": [false, false, false, false]
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
public string JournalJson { get; set; }
|
||||
}
|
||||
|
||||
public class Item
|
||||
{
|
||||
public string ItemID { get; set; }
|
||||
public int IsOpen { get; set; }
|
||||
public int Score { get; set; }
|
||||
public int MaxScore { get; set; }
|
||||
public List<bool> ReplyMC { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used for web page building
|
||||
/// </summary>
|
||||
public class JournalHtml
|
||||
{
|
||||
public Journal2 Journal { get; set; }
|
||||
public string Text { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Table for the journal. One is created for each test available to the PID.
|
||||
/// </summary>
|
||||
[Table("Journal2")]
|
||||
public class Journal2
|
||||
{
|
||||
public string PID { get; set; } // VARCHAR 256 Primary key
|
||||
public string QuizzID { get; set; } // VARCHAR 50 Primary key
|
||||
public string Category { get; set; } // VARCHAR 50
|
||||
public int IsOpen { get; set; } // INT 11
|
||||
public bool Anonymous { get; set; } // INT 11
|
||||
public int Score { get; set; } // INT 11
|
||||
public int ScoreAcc { get; set; } // INT 11
|
||||
public List<Journal2Item> Items { get; set; } // This field does not store in the DB, but is serialized to JSON and stored in the JournalJson field of the JournalHead table. The JSON string is in the following format: {"items": [{"itemID": "1", "isOpen": 1, "score": 1, "maxScore": 1, "replyMC": [true, false, false, false]}, {"itemID": "2", "isOpen": 0, "score": 0, "maxScore": 1, "replyMC": [false, false, false, false]}]}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One is created for each question in the test, and linked to the JournalHead by the QuizzID and PID. The ItemID is used to identify the question in the test, and the IsOpen, Score, MaxScore, and ReplyMCJson are used to store the quiz results for that question.
|
||||
/// </summary>
|
||||
[Table("Journal2Item")]
|
||||
public class Journal2Item
|
||||
{
|
||||
public string PID { get; set; } // VARCHAR 256, Foreign key to the JournalHead table. Primary key
|
||||
public string QuizzID { get; set; } // VARCHAR 50, Foreign key to the JournalHead table. Primary key
|
||||
public required string QuestionID { get; set; } // VARCHAR 50, used to identify the question in the test. Primary key
|
||||
public int IsOpen { get; set; } // INT 11, used to indicate whether the question is open or not.
|
||||
public int Score { get; set; } // INT 11, used to store the score for the question.
|
||||
public int MaxScore { get; set; } // INT 11, used to store the maximum score for the question.
|
||||
public List<bool> ReplyMC { get; set; } // This field does not store in the DB, but is serialized to JSON and stored in the JournalJson field of the JournalHead table. The JSON string is in the following format: [true, false, false, false]
|
||||
public string ReplyMCJson { get; set; } // VARCHAR 512, used to store the JSON string for the ReplyMC field. The JSON string is in the following format: [true, false, false, false]
|
||||
}
|
||||
}
|
||||
8
Quizzer-8/Quizzer/Models/OpenIDUCNData.cs
Normal file
8
Quizzer-8/Quizzer/Models/OpenIDUCNData.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Quizzer.Models
|
||||
{
|
||||
public class OpenIDUCNData
|
||||
{
|
||||
public string theRole { get; set; }
|
||||
public List<string> theClass { get; set; }
|
||||
}
|
||||
}
|
||||
15
Quizzer-8/Quizzer/Models/QAnswer.cs
Normal file
15
Quizzer-8/Quizzer/Models/QAnswer.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Quizzer.Models
|
||||
{
|
||||
public class QAnswer
|
||||
{
|
||||
public string AId { get; set; }
|
||||
public string QRef { get; set; }
|
||||
public string Alternative { get; set; }
|
||||
public bool IsRight { get; set; }
|
||||
}
|
||||
}
|
||||
60
Quizzer-8/Quizzer/Models/Question.cs
Normal file
60
Quizzer-8/Quizzer/Models/Question.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using Dapper.Contrib.Extensions;
|
||||
|
||||
namespace Quizzer.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Multiple Choice Question
|
||||
/// Missing: image upload
|
||||
/// CREATE TABLE `Questions` (
|
||||
/// `QId` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
/// `Txt` VARCHAR(512) NOT NULL DEFAULT '0' COLLATE 'utf8mb4_general_ci',
|
||||
/// PRIMARY KEY(`QId`) USING BTREE
|
||||
/// )
|
||||
/// COMMENT='The headline'
|
||||
/// COLLATE='utf8mb4_general_ci'
|
||||
/// ENGINE=InnoDB
|
||||
/// AUTO_INCREMENT = 2
|
||||
/// ;
|
||||
/// </summary>
|
||||
public class Question
|
||||
{
|
||||
[Key]
|
||||
public string QuestionID { get; set; }
|
||||
public string Category { get; set; }
|
||||
public string QuestionText { get; set; }
|
||||
public string QuestionType { get; set; }
|
||||
public string ContentJson { get; set; }
|
||||
|
||||
public Question()
|
||||
{
|
||||
QuestionID = String.Empty;
|
||||
Category = String.Empty;
|
||||
QuestionText = String.Empty;
|
||||
QuestionType = String.Empty;
|
||||
ContentJson = String.Empty;
|
||||
}
|
||||
}
|
||||
public class MCQuestion : Question
|
||||
{
|
||||
public List<Alternative> AList { get; set; }
|
||||
public MCQuestion()
|
||||
{
|
||||
AList = new List<Alternative>();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class Alternative
|
||||
{
|
||||
public string Atxt { get; set; }
|
||||
public bool Aok { get; set; }
|
||||
public int Counter { get; set; }
|
||||
|
||||
public Alternative()
|
||||
{
|
||||
Counter = 0;
|
||||
Atxt = String.Empty;
|
||||
Aok = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
27
Quizzer-8/Quizzer/Models/Quizz.cs
Normal file
27
Quizzer-8/Quizzer/Models/Quizz.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using Dapper.Contrib.Extensions;
|
||||
|
||||
namespace Quizzer.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// QuizzID identifies the quizz
|
||||
/// Owner teacher
|
||||
/// Class Who gets this test
|
||||
/// Descr Text to present to the student
|
||||
/// </summary>
|
||||
public class Quizz
|
||||
{
|
||||
[Key]
|
||||
public string QuizzId { get; set; }
|
||||
public string Owner { get; set; }
|
||||
public string Class { get; set; }
|
||||
public string Category { get; set; }
|
||||
public string Descr { get; set; }
|
||||
public bool Anonymous { get; set; }
|
||||
public int QSize { get; set; }
|
||||
}
|
||||
|
||||
public class QuizzDN : Quizz
|
||||
{
|
||||
public string CategoryText { get; set; }
|
||||
}
|
||||
}
|
||||
78
Quizzer-8/Quizzer/Models/Statistic.cs
Normal file
78
Quizzer-8/Quizzer/Models/Statistic.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Quizzer.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Collective knowledge of quizz answers
|
||||
/// </summary>
|
||||
public class Statistic
|
||||
{
|
||||
public string QuizzID { get; set; }
|
||||
public int Version { get; set; }
|
||||
public string StatsJson { get; set; }
|
||||
|
||||
public Statistic()
|
||||
{
|
||||
QuizzID = String.Empty;
|
||||
Version = 0;
|
||||
StatsJson = String.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public class StatisticsMC : Statistic
|
||||
{
|
||||
public List<StatisticsMCItem> MCList { get; set; }
|
||||
|
||||
public StatisticsMC()
|
||||
{
|
||||
MCList = new List<StatisticsMCItem>();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class StatisticsMCItem
|
||||
{
|
||||
public string QuestionID { get; set; }
|
||||
public List<int> Counter { get; set; }
|
||||
|
||||
public StatisticsMCItem()
|
||||
{
|
||||
Counter = new List<int>();
|
||||
QuestionID = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public class StatisticsMCDN : StatisticsMC
|
||||
{
|
||||
public List<MCQuestion> Questions { get; set; }
|
||||
|
||||
public StatisticsMCDN()
|
||||
{
|
||||
Questions = new List<MCQuestion>();
|
||||
}
|
||||
}
|
||||
|
||||
public class StatisticsStudentByCategory
|
||||
{
|
||||
public string CategoryID { get; set; }
|
||||
public string CategoryName { get; set; }
|
||||
public int percentage { get; set; }
|
||||
private int score;
|
||||
private int max;
|
||||
|
||||
public StatisticsStudentByCategory()
|
||||
{
|
||||
score = 0;
|
||||
max = 0;
|
||||
percentage = 0;
|
||||
}
|
||||
|
||||
public void Update (int theScore, int theMax)
|
||||
{
|
||||
score += theScore;
|
||||
max += theMax;
|
||||
percentage = (score * 100) / max;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
10
Quizzer-8/Quizzer/Models/TheClass.cs
Normal file
10
Quizzer-8/Quizzer/Models/TheClass.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using Dapper.Contrib.Extensions;
|
||||
|
||||
namespace Quizzer.Models
|
||||
{
|
||||
public class TheClass
|
||||
{
|
||||
[Key]
|
||||
public string? Name { get; set; }
|
||||
}
|
||||
}
|
||||
153
Quizzer-8/Quizzer/OpenIDConnect/OpenIDConnectUtils.cs
Normal file
153
Quizzer-8/Quizzer/OpenIDConnect/OpenIDConnectUtils.cs
Normal file
@@ -0,0 +1,153 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
|
||||
|
||||
|
||||
namespace Quizzer.OpenIDConnect
|
||||
{
|
||||
public class OpenIDConnectUtils
|
||||
{
|
||||
// Configuration priority from highest to lowest
|
||||
// Highest: Command line arguments
|
||||
// : Non-prefixed environment variables
|
||||
// : User secrets from the .NET User Secrets Manager
|
||||
// : Any appsettings.{ ENVIRONMENT_NAME }.json files
|
||||
// : The appsettings.json file
|
||||
// Lowest : Fallback to the host configuration
|
||||
|
||||
/// <summary>
|
||||
/// Setting up OpenIDConnect authentication (Program.cs)
|
||||
/// </summary>
|
||||
/// <param name="builder">WebApplicationBuilder</param>
|
||||
|
||||
public void ConfigureBuild(WebApplicationBuilder builder)
|
||||
{
|
||||
MyConfiguration.Set(builder.Configuration);
|
||||
builder.Services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
|
||||
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
|
||||
})
|
||||
.AddCookie()
|
||||
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
|
||||
{
|
||||
options.Authority = builder.Configuration["OpenIDRealmURI"];
|
||||
options.ClientId = builder.Configuration["OpenIDClient"];
|
||||
options.ClientSecret = builder.Configuration["OpenIDSecret"];
|
||||
options.CallbackPath = "/signin-oidc";
|
||||
|
||||
// Authorization Code flow (recommended)
|
||||
options.ResponseType = "code";
|
||||
options.SaveTokens = true; // store tokens in auth properties
|
||||
options.GetClaimsFromUserInfoEndpoint = true;
|
||||
|
||||
// Scopes - clear then add what's needed
|
||||
options.Scope.Clear();
|
||||
options.Scope.Add("openid");
|
||||
options.Scope.Add("profile");
|
||||
options.Scope.Add("ucn");
|
||||
options.Scope.Add("email");
|
||||
options.Scope.Add("offline_access"); // to get refresh tokens
|
||||
|
||||
// Map claim types if needed
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
NameClaimType = "name",
|
||||
RoleClaimType = "roles"
|
||||
};
|
||||
|
||||
// Optional: events for logging / error handling
|
||||
options.Events = new OpenIdConnectEvents
|
||||
{
|
||||
OnTokenValidated = context =>
|
||||
{
|
||||
// e.g. add custom claims or logging
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
OnAuthenticationFailed = context =>
|
||||
{
|
||||
context.HandleResponse();
|
||||
context.Response.Redirect("/Error?message=" + Uri.EscapeDataString(context.Exception.Message));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
};
|
||||
});
|
||||
// Change here for authorization policies
|
||||
builder.Services
|
||||
.AddAuthorizationBuilder()
|
||||
.AddPolicy("read_access", builder =>
|
||||
{
|
||||
// claim, list of acceptable values
|
||||
builder.RequireClaim("myClaim", "MyClaimValueRO1", "MyClaimValueRO2");
|
||||
})
|
||||
.AddPolicy("write_access", builder =>
|
||||
{
|
||||
builder.RequireClaim("myClaim", "MyClaimValueRW1", "MyClaimValueRW2");
|
||||
});
|
||||
|
||||
// Require authentication globally (optional)
|
||||
builder.Services.AddRazorPages(options =>
|
||||
{
|
||||
options.Conventions.AuthorizeFolder("/");
|
||||
});
|
||||
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
||||
{
|
||||
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
|
||||
options.KnownNetworks.Clear();
|
||||
options.KnownProxies.Clear();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Map synthetic endpoints for authentication
|
||||
/// </summary>
|
||||
/// <param name="app">WebApplication</param>
|
||||
public void ConfigureApp(WebApplication app)
|
||||
{
|
||||
app.UseStaticFiles();
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
app.UseForwardedHeaders();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapRazorPages(); // Or app.MapDefaultControllerRoute();
|
||||
|
||||
app.Run();
|
||||
}
|
||||
|
||||
public JwtSecurityToken GetJwtPayload(HttpContext myContext)
|
||||
{
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
return handler.ReadJwtToken(myContext.GetTokenAsync("access_token").Result);
|
||||
}
|
||||
|
||||
public string GetJwtClaim(HttpContext myContext, string theClaim)
|
||||
{
|
||||
JwtSecurityToken jwtPayload = GetJwtPayload(myContext);
|
||||
return jwtPayload.Claims.FirstOrDefault(claim => claim.Type == theClaim).Value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static public class MyConfiguration
|
||||
{
|
||||
static ConfigurationManager _config;
|
||||
static public void Set(ConfigurationManager config)
|
||||
{
|
||||
_config = config;
|
||||
}
|
||||
|
||||
static public ConfigurationManager Get()
|
||||
{
|
||||
return _config;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
53
Quizzer-8/Quizzer/Pages/Categories.cshtml
Normal file
53
Quizzer-8/Quizzer/Pages/Categories.cshtml
Normal 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>
|
||||
148
Quizzer-8/Quizzer/Pages/Categories.cshtml.cs
Normal file
148
Quizzer-8/Quizzer/Pages/Categories.cshtml.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
96
Quizzer-8/Quizzer/Pages/Coverage.cshtml
Normal file
96
Quizzer-8/Quizzer/Pages/Coverage.cshtml
Normal 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>
|
||||
108
Quizzer-8/Quizzer/Pages/Coverage.cshtml.cs
Normal file
108
Quizzer-8/Quizzer/Pages/Coverage.cshtml.cs
Normal 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("/");
|
||||
}
|
||||
}
|
||||
}
|
||||
26
Quizzer-8/Quizzer/Pages/Error.cshtml
Normal file
26
Quizzer-8/Quizzer/Pages/Error.cshtml
Normal 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>
|
||||
27
Quizzer-8/Quizzer/Pages/Error.cshtml.cs
Normal file
27
Quizzer-8/Quizzer/Pages/Error.cshtml.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
208
Quizzer-8/Quizzer/Pages/Index.cshtml
Normal file
208
Quizzer-8/Quizzer/Pages/Index.cshtml
Normal 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>
|
||||
}
|
||||
455
Quizzer-8/Quizzer/Pages/Index.cshtml.cs
Normal file
455
Quizzer-8/Quizzer/Pages/Index.cshtml.cs
Normal 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("/");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
31
Quizzer-8/Quizzer/Pages/Privacy.cshtml
Normal file
31
Quizzer-8/Quizzer/Pages/Privacy.cshtml
Normal 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>
|
||||
19
Quizzer-8/Quizzer/Pages/Privacy.cshtml.cs
Normal file
19
Quizzer-8/Quizzer/Pages/Privacy.cshtml.cs
Normal 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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
182
Quizzer-8/Quizzer/Pages/Questions.cshtml
Normal file
182
Quizzer-8/Quizzer/Pages/Questions.cshtml
Normal 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>
|
||||
338
Quizzer-8/Quizzer/Pages/Questions.cshtml.cs
Normal file
338
Quizzer-8/Quizzer/Pages/Questions.cshtml.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
112
Quizzer-8/Quizzer/Pages/Quizzes.cshtml
Normal file
112
Quizzer-8/Quizzer/Pages/Quizzes.cshtml
Normal 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>
|
||||
145
Quizzer-8/Quizzer/Pages/Quizzes.cshtml.cs
Normal file
145
Quizzer-8/Quizzer/Pages/Quizzes.cshtml.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
63
Quizzer-8/Quizzer/Pages/Shared/_Layout-teacher.cshtml
Normal file
63
Quizzer-8/Quizzer/Pages/Shared/_Layout-teacher.cshtml
Normal 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">
|
||||
© 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>
|
||||
48
Quizzer-8/Quizzer/Pages/Shared/_Layout-teacher.cshtml.css
Normal file
48
Quizzer-8/Quizzer/Pages/Shared/_Layout-teacher.cshtml.css
Normal 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;
|
||||
}
|
||||
67
Quizzer-8/Quizzer/Pages/Shared/_Layout.cshtml
Normal file
67
Quizzer-8/Quizzer/Pages/Shared/_Layout.cshtml
Normal 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">
|
||||
© 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>
|
||||
48
Quizzer-8/Quizzer/Pages/Shared/_Layout.cshtml.css
Normal file
48
Quizzer-8/Quizzer/Pages/Shared/_Layout.cshtml.css
Normal 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;
|
||||
}
|
||||
@@ -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>
|
||||
84
Quizzer-8/Quizzer/Pages/Statistics.cshtml
Normal file
84
Quizzer-8/Quizzer/Pages/Statistics.cshtml
Normal 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>
|
||||
126
Quizzer-8/Quizzer/Pages/Statistics.cshtml.cs
Normal file
126
Quizzer-8/Quizzer/Pages/Statistics.cshtml.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Quizzer-8/Quizzer/Pages/_ViewImports.cshtml
Normal file
3
Quizzer-8/Quizzer/Pages/_ViewImports.cshtml
Normal file
@@ -0,0 +1,3 @@
|
||||
@using Quizzer
|
||||
@namespace Quizzer.Pages
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
3
Quizzer-8/Quizzer/Pages/_ViewStart.cshtml
Normal file
3
Quizzer-8/Quizzer/Pages/_ViewStart.cshtml
Normal file
@@ -0,0 +1,3 @@
|
||||
@{
|
||||
Layout = "_Layout";
|
||||
}
|
||||
14
Quizzer-8/Quizzer/Pages/images.cshtml
Normal file
14
Quizzer-8/Quizzer/Pages/images.cshtml
Normal 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>
|
||||
12
Quizzer-8/Quizzer/Pages/images.cshtml.cs
Normal file
12
Quizzer-8/Quizzer/Pages/images.cshtml.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Quizzer.Pages
|
||||
{
|
||||
public class imagesModel : PageModel
|
||||
{
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
1214
Quizzer-8/Quizzer/Persistens/DbOps.cs
Normal file
1214
Quizzer-8/Quizzer/Persistens/DbOps.cs
Normal file
File diff suppressed because it is too large
Load Diff
32
Quizzer-8/Quizzer/Program.cs
Normal file
32
Quizzer-8/Quizzer/Program.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// Extra packages installed:
|
||||
// bootstrap - latest stable 5.x.x
|
||||
// Microsoft.AspNetCore.Authentication.JwtBearer - latest version 8.x.x
|
||||
// Microsoft.AspNetCore.Authentication.OpenIdConnect - latest version 8.x.x
|
||||
// Microsoft.IdentityModel.JsonWebTokens - latest version 8.x.x
|
||||
// Microsoft.IdentityModel.Protocols.OpenIdConnect - latest version
|
||||
// Newtonsoft.Json - latest
|
||||
|
||||
|
||||
|
||||
using Quizzer.OpenIDConnect;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Configure for OpenID
|
||||
OpenIDConnectUtils myConfig = new();
|
||||
|
||||
myConfig.ConfigureBuild(builder);
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseExceptionHandler("/Error");
|
||||
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
||||
app.UseHsts();
|
||||
}
|
||||
|
||||
myConfig.ConfigureApp(app);
|
||||
|
||||
63
Quizzer-8/Quizzer/Properties/Resources.Designer.cs
generated
Normal file
63
Quizzer-8/Quizzer/Properties/Resources.Designer.cs
generated
Normal file
@@ -0,0 +1,63 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace Quizzer.Properties {
|
||||
using System;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||
/// </summary>
|
||||
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
internal class Resources {
|
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||
internal Resources() {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cached ResourceManager instance used by this class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Resources.ResourceManager ResourceManager {
|
||||
get {
|
||||
if (object.ReferenceEquals(resourceMan, null)) {
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Quizzer.Properties.Resources", typeof(Resources).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the current thread's CurrentUICulture property for all
|
||||
/// resource lookups using this strongly typed resource class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Globalization.CultureInfo Culture {
|
||||
get {
|
||||
return resourceCulture;
|
||||
}
|
||||
set {
|
||||
resourceCulture = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
101
Quizzer-8/Quizzer/Properties/Resources.resx
Normal file
101
Quizzer-8/Quizzer/Properties/Resources.resx
Normal file
@@ -0,0 +1,101 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 1.3
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">1.3</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1">this is my long string</data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
[base64 mime encoded serialized .NET Framework object]
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
[base64 mime encoded string representing a byte array form of the .NET Framework object]
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>1.3</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
37
Quizzer-8/Quizzer/Properties/launchSettings.json
Normal file
37
Quizzer-8/Quizzer/Properties/launchSettings.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"profiles": {
|
||||
"Quizzer": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"DEBUG": "Y"
|
||||
},
|
||||
"applicationUrl": "https://localhost:7777;http://localhost:5035",
|
||||
"dotnetRunMessages": true
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"OIDC_CLAIM_role": "teacher"
|
||||
}
|
||||
},
|
||||
"Docker": {
|
||||
"commandName": "Docker",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
|
||||
"publishAllPorts": true,
|
||||
"useSSL": true
|
||||
}
|
||||
},
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:8629",
|
||||
"sslPort": 44311
|
||||
}
|
||||
}
|
||||
}
|
||||
44
Quizzer-8/Quizzer/Quizzer.csproj
Normal file
44
Quizzer-8/Quizzer/Quizzer.csproj
Normal file
@@ -0,0 +1,44 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>0fdb2907-bc36-462b-8209-38d65ea0c2f7</UserSecretsId>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="bootstrap" Version="5.3.8" />
|
||||
<PackageReference Include="Chart.js" Version="3.7.1" />
|
||||
<PackageReference Include="Dapper" Version="2.1.66" />
|
||||
<PackageReference Include="Dapper.Contrib" Version="2.0.78" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.19" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.19" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Session" Version="2.3.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="8.14.0" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
|
||||
<PackageReference Include="MySqlConnector" Version="2.4.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="8.0.1" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.14.0" />
|
||||
<PackageReference Include="System.Runtime" Version="4.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\Resources.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Properties\Resources.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
86
Quizzer-8/Quizzer/README.md
Normal file
86
Quizzer-8/Quizzer/README.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# Quizzer
|
||||
Quizzer is a quizz maker and management application based on Razor.
|
||||
It implements MC questions and MC quizzes later to be expanded.
|
||||
It also implements Tags which should be applied to all questions to allow for random quizz building.
|
||||
## Database tables
|
||||
**Categories**
|
||||
```
|
||||
CREATE TABLE `Cats` (
|
||||
`CId` VARCHAR(50) NOT NULL DEFAULT 'NA' COLLATE 'utf8mb4_general_ci',
|
||||
`UID` VARCHAR(256) NOT NULL DEFAULT '/' COLLATE 'utf8mb4_general_ci',
|
||||
`CParentId` VARCHAR(50) NOT NULL DEFAULT '/' COLLATE 'utf8mb4_general_ci',
|
||||
`Txt` VARCHAR(50) NOT NULL DEFAULT 'NA' COLLATE 'utf8mb4_general_ci',
|
||||
`Descr` VARCHAR(1024) NOT NULL DEFAULT 'NA' COLLATE 'utf8mb4_general_ci',
|
||||
PRIMARY KEY (`CId`) USING BTREE,
|
||||
INDEX `TId` (`CId`) USING BTREE
|
||||
)
|
||||
COLLATE='utf8mb4_general_ci'
|
||||
ENGINE=InnoDB
|
||||
;
|
||||
```
|
||||
**Questions**
|
||||
```
|
||||
CREATE TABLE `Questions` (
|
||||
`QId` CHAR(50) NOT NULL,
|
||||
`Txt` VARCHAR(512) NOT NULL DEFAULT '0',
|
||||
PRIMARY KEY (`QId`)
|
||||
)
|
||||
COLLATE='utf8mb4_general_ci'
|
||||
ENGINE=InnoDB
|
||||
;
|
||||
```
|
||||
**Journals**
|
||||
```
|
||||
CREATE TABLE `Journals` (
|
||||
`PID` VARCHAR(256) NOT NULL COLLATE 'utf8mb4_general_ci',
|
||||
`QuizzID` VARCHAR(50) NOT NULL COLLATE 'utf8mb4_general_ci',
|
||||
`Category` VARCHAR(50) NOT NULL COLLATE 'utf8mb4_general_ci',
|
||||
`IsOpen` INT(11) NOT NULL DEFAULT '1',
|
||||
`Score` INT(11) NOT NULL,
|
||||
`ScoreAcc` INT(11) NOT NULL,
|
||||
`JournalJson` VARCHAR(2048) NOT NULL COLLATE 'utf8mb4_general_ci',
|
||||
PRIMARY KEY (`PID`, `QuizzID`) USING BTREE
|
||||
)
|
||||
COLLATE='utf8mb4_general_ci'
|
||||
ENGINE=InnoDB
|
||||
;
|
||||
```
|
||||
**Questions**
|
||||
```
|
||||
CREATE TABLE `Questions` (
|
||||
`QuestionID` VARCHAR(50) NOT NULL DEFAULT 'ERROR' COLLATE 'utf8mb4_general_ci',
|
||||
`QuestionType` VARCHAR(50) NOT NULL DEFAULT '' COLLATE 'utf8mb4_general_ci',
|
||||
`Category` VARCHAR(50) NOT NULL DEFAULT '/' COLLATE 'utf8mb4_general_ci',
|
||||
`QuestionText` VARCHAR(512) NOT NULL DEFAULT '0' COLLATE 'utf8mb4_general_ci',
|
||||
`ContentJson` VARCHAR(1024) NOT NULL DEFAULT '0' COLLATE 'utf8mb4_general_ci',
|
||||
PRIMARY KEY (`QuestionID`) USING BTREE
|
||||
)
|
||||
COLLATE='utf8mb4_general_ci'
|
||||
ENGINE=InnoDB
|
||||
;
|
||||
```
|
||||
**Quizzes**
|
||||
```
|
||||
CREATE TABLE `Quizzs` (
|
||||
`QuizzId` VARCHAR(50) NOT NULL DEFAULT 'na' COLLATE 'utf8mb4_general_ci',
|
||||
`Owner` VARCHAR(256) NOT NULL DEFAULT 'na' COLLATE 'utf8mb4_general_ci',
|
||||
`Class` VARCHAR(50) NOT NULL DEFAULT 'na' COLLATE 'utf8mb4_general_ci',
|
||||
`Category` VARCHAR(50) NULL DEFAULT NULL COLLATE 'utf8mb4_general_ci',
|
||||
`Descr` VARCHAR(1024) NULL DEFAULT NULL COLLATE 'utf8mb4_general_ci',
|
||||
`Anonymous` INT(11) NULL DEFAULT '0',
|
||||
`QSize` INT(10) UNSIGNED NOT NULL DEFAULT '5',
|
||||
PRIMARY KEY (`QuizzId`) USING BTREE
|
||||
)
|
||||
COLLATE='utf8mb4_general_ci'
|
||||
ENGINE=InnoDB
|
||||
;
|
||||
**Statistics**
|
||||
```
|
||||
CREATE TABLE `Statistics` (
|
||||
`QuizzID` VARCHAR(50) NULL DEFAULT 'na' COLLATE 'utf8mb4_general_ci',
|
||||
`Version` INT(11) NULL DEFAULT '0',
|
||||
`StatsJson` VARCHAR(8192) NULL DEFAULT NULL COLLATE 'utf8mb3_bin'
|
||||
)
|
||||
COLLATE='utf8mb4_general_ci'
|
||||
ENGINE=InnoDB
|
||||
;
|
||||
9
Quizzer-8/Quizzer/appsettings.Development.json
Normal file
9
Quizzer-8/Quizzer/appsettings.Development.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"DetailedErrors": true,
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
20
Quizzer-8/Quizzer/appsettings.json
Normal file
20
Quizzer-8/Quizzer/appsettings.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"Comment1": "Arguments database Connection",
|
||||
"QUIZZER_DBCONNECTION": "server=localhost;user id=quizzer;password=12tf56so;persistsecurityinfo=True;database=Q",
|
||||
"Comment2": "Arguments for KeyCloak API Connection",
|
||||
"API_SERVER": "auth.a.ucnit.eu",
|
||||
"API_REALM": "UCNit",
|
||||
"API_CLIENTID": "API",
|
||||
"API_SECRET": "u58wi7fY6GhBBrQsEIMcqsH4RnwdEpOw",
|
||||
"Comment3": "Arguments for KeyCloak Code Flow",
|
||||
"OpenIDRealmURI": "https://auth.a.ucnit.eu/realms/UCNit",
|
||||
"OpenIDClient": "General",
|
||||
"OpenIDSecret": "7mIerwlVkhOX6wWYIWMzh3qiJq8pm1dn"
|
||||
}
|
||||
18
Quizzer-8/Quizzer/wwwroot/css/site.css
Normal file
18
Quizzer-8/Quizzer/wwwroot/css/site.css
Normal file
@@ -0,0 +1,18 @@
|
||||
html {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
html {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
BIN
Quizzer-8/Quizzer/wwwroot/favicon.ico
Normal file
BIN
Quizzer-8/Quizzer/wwwroot/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
BIN
Quizzer-8/Quizzer/wwwroot/images/quiz -org.jpg
Normal file
BIN
Quizzer-8/Quizzer/wwwroot/images/quiz -org.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 102 KiB |
BIN
Quizzer-8/Quizzer/wwwroot/images/quiz.jpg
Normal file
BIN
Quizzer-8/Quizzer/wwwroot/images/quiz.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
4
Quizzer-8/Quizzer/wwwroot/js/site.js
Normal file
4
Quizzer-8/Quizzer/wwwroot/js/site.js
Normal file
@@ -0,0 +1,4 @@
|
||||
// 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.
|
||||
|
||||
// Write your JavaScript code.
|
||||
22
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/LICENSE
Normal file
22
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/LICENSE
Normal file
@@ -0,0 +1,22 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2011-2021 Twitter, Inc.
|
||||
Copyright (c) 2011-2021 The Bootstrap Authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
4997
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css
vendored
Normal file
4997
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map
vendored
Normal file
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
7
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css
vendored
Normal file
7
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map
vendored
Normal file
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
4996
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css
vendored
Normal file
4996
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map
vendored
Normal file
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
7
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css
vendored
Normal file
7
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map
vendored
Normal file
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
427
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css
vendored
Normal file
427
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css
vendored
Normal file
@@ -0,0 +1,427 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.1.0 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2021 The Bootstrap Authors
|
||||
* Copyright 2011-2021 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:root {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--bs-body-font-family);
|
||||
font-size: var(--bs-body-font-size);
|
||||
font-weight: var(--bs-body-font-weight);
|
||||
line-height: var(--bs-body-line-height);
|
||||
color: var(--bs-body-color);
|
||||
text-align: var(--bs-body-text-align);
|
||||
background-color: var(--bs-body-bg);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
color: inherit;
|
||||
background-color: currentColor;
|
||||
border: 0;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
hr:not([size]) {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
h6, h5, h4, h3, h2, h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: calc(1.325rem + 0.9vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: calc(1.3rem + 0.6vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title],
|
||||
abbr[data-bs-original-title] {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
-webkit-text-decoration-skip-ink: none;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0.2em;
|
||||
background-color: #fcf8e3;
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 0.75em;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0d6efd;
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
color: #0a58ca;
|
||||
}
|
||||
|
||||
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 1em;
|
||||
direction: ltr /* rtl:ignore */;
|
||||
unicode-bidi: bidi-override;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
pre code {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.875em;
|
||||
color: #d63384;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
a > code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.2rem 0.4rem;
|
||||
font-size: 0.875em;
|
||||
color: #fff;
|
||||
background-color: #212529;
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
kbd kbd {
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
caption-side: bottom;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
color: #6c757d;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
text-align: -webkit-match-parent;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
[role=button] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
word-wrap: normal;
|
||||
}
|
||||
select:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[list]::-webkit-calendar-picker-indicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button,
|
||||
[type=button],
|
||||
[type=reset],
|
||||
[type=submit] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
button:not(:disabled),
|
||||
[type=button]:not(:disabled),
|
||||
[type=reset]:not(:disabled),
|
||||
[type=submit]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
float: left;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
line-height: inherit;
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
legend + * {
|
||||
clear: left;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper,
|
||||
::-webkit-datetime-edit-text,
|
||||
::-webkit-datetime-edit-minute,
|
||||
::-webkit-datetime-edit-hour-field,
|
||||
::-webkit-datetime-edit-day-field,
|
||||
::-webkit-datetime-edit-month-field,
|
||||
::-webkit-datetime-edit-year-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type=search] {
|
||||
outline-offset: -2px;
|
||||
-webkit-appearance: textfield;
|
||||
}
|
||||
|
||||
/* rtl:raw:
|
||||
[type="tel"],
|
||||
[type="url"],
|
||||
[type="email"],
|
||||
[type="number"] {
|
||||
direction: ltr;
|
||||
}
|
||||
*/
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=bootstrap-reboot.css.map */
|
||||
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map
vendored
Normal file
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
8
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css
vendored
Normal file
8
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.1.0 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2021 The Bootstrap Authors
|
||||
* Copyright 2011-2021 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}
|
||||
/*# sourceMappingURL=bootstrap-reboot.min.css.map */
|
||||
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map
vendored
Normal file
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
424
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css
vendored
Normal file
424
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css
vendored
Normal file
@@ -0,0 +1,424 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.1.0 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2021 The Bootstrap Authors
|
||||
* Copyright 2011-2021 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:root {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--bs-body-font-family);
|
||||
font-size: var(--bs-body-font-size);
|
||||
font-weight: var(--bs-body-font-weight);
|
||||
line-height: var(--bs-body-line-height);
|
||||
color: var(--bs-body-color);
|
||||
text-align: var(--bs-body-text-align);
|
||||
background-color: var(--bs-body-bg);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
color: inherit;
|
||||
background-color: currentColor;
|
||||
border: 0;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
hr:not([size]) {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
h6, h5, h4, h3, h2, h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: calc(1.325rem + 0.9vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: calc(1.3rem + 0.6vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title],
|
||||
abbr[data-bs-original-title] {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
-webkit-text-decoration-skip-ink: none;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0.2em;
|
||||
background-color: #fcf8e3;
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 0.75em;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0d6efd;
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
color: #0a58ca;
|
||||
}
|
||||
|
||||
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 1em;
|
||||
direction: ltr ;
|
||||
unicode-bidi: bidi-override;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
pre code {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.875em;
|
||||
color: #d63384;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
a > code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.2rem 0.4rem;
|
||||
font-size: 0.875em;
|
||||
color: #fff;
|
||||
background-color: #212529;
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
kbd kbd {
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
caption-side: bottom;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
color: #6c757d;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
text-align: -webkit-match-parent;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
[role=button] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
word-wrap: normal;
|
||||
}
|
||||
select:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[list]::-webkit-calendar-picker-indicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button,
|
||||
[type=button],
|
||||
[type=reset],
|
||||
[type=submit] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
button:not(:disabled),
|
||||
[type=button]:not(:disabled),
|
||||
[type=reset]:not(:disabled),
|
||||
[type=submit]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
float: right;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
line-height: inherit;
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
legend + * {
|
||||
clear: right;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper,
|
||||
::-webkit-datetime-edit-text,
|
||||
::-webkit-datetime-edit-minute,
|
||||
::-webkit-datetime-edit-hour-field,
|
||||
::-webkit-datetime-edit-day-field,
|
||||
::-webkit-datetime-edit-month-field,
|
||||
::-webkit-datetime-edit-year-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type=search] {
|
||||
outline-offset: -2px;
|
||||
-webkit-appearance: textfield;
|
||||
}
|
||||
|
||||
[type="tel"],
|
||||
[type="url"],
|
||||
[type="email"],
|
||||
[type="number"] {
|
||||
direction: ltr;
|
||||
}
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */
|
||||
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map
vendored
Normal file
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
8
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css
vendored
Normal file
8
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.1.0 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2021 The Bootstrap Authors
|
||||
* Copyright 2011-2021 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-right:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-right:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:right}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:right;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:right}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}[type=email],[type=number],[type=tel],[type=url]{direction:ltr}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}
|
||||
/*# sourceMappingURL=bootstrap-reboot.rtl.min.css.map */
|
||||
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map
vendored
Normal file
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
4866
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css
vendored
Normal file
4866
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map
vendored
Normal file
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
7
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css
vendored
Normal file
7
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map
vendored
Normal file
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
4857
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css
vendored
Normal file
4857
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map
vendored
Normal file
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
7
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css
vendored
Normal file
7
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map
vendored
Normal file
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
11221
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap.css
vendored
Normal file
11221
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map
vendored
Normal file
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
7
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css
vendored
Normal file
7
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map
vendored
Normal file
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
11197
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css
vendored
Normal file
11197
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css.map
vendored
Normal file
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
7
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css
vendored
Normal file
7
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map
vendored
Normal file
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6780
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js
vendored
Normal file
6780
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map
vendored
Normal file
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
7
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js
vendored
Normal file
7
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map
vendored
Normal file
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
4977
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js
vendored
Normal file
4977
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js.map
vendored
Normal file
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
7
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js
vendored
Normal file
7
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js.map
vendored
Normal file
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
5026
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/js/bootstrap.js
vendored
Normal file
5026
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/js/bootstrap.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map
vendored
Normal file
1
Quizzer-8/Quizzer/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user