diff --git a/QuizApp.Api/QuizApiClient.cs b/QuizApp.Api/QuizApiClient.cs
index d7316e8..dfa5a39 100644
--- a/QuizApp.Api/QuizApiClient.cs
+++ b/QuizApp.Api/QuizApiClient.cs
@@ -107,6 +107,24 @@ public partial interface IQuizApiClient
/// A server side error occurred.
System.Threading.Tasks.Task> GetAllQuestionsEndpointAsync(System.Threading.CancellationToken cancellationToken);
+ /// Success
+ /// A server side error occurred.
+ System.Threading.Tasks.Task> GetQuestionsByQuizIdEndpointAsync(int quizId);
+
+ /// A cancellation token that can be used by other objects or threads to receive notice of cancellation.
+ /// Success
+ /// A server side error occurred.
+ System.Threading.Tasks.Task> GetQuestionsByQuizIdEndpointAsync(int quizId, System.Threading.CancellationToken cancellationToken);
+
+ /// Success
+ /// A server side error occurred.
+ System.Threading.Tasks.Task SaveOrUpdateQuestionAsync(SaveOrUpdateQuestionRequest saveOrUpdateQuestionRequest);
+
+ /// A cancellation token that can be used by other objects or threads to receive notice of cancellation.
+ /// Success
+ /// A server side error occurred.
+ System.Threading.Tasks.Task SaveOrUpdateQuestionAsync(SaveOrUpdateQuestionRequest saveOrUpdateQuestionRequest, System.Threading.CancellationToken cancellationToken);
+
}
[System.CodeDom.Compiler.GeneratedCode("NSwag", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
@@ -892,6 +910,175 @@ public virtual async System.Threading.Tasks.Task DeleteQuizEndpointAsync(D
}
}
+ /// Success
+ /// A server side error occurred.
+ public virtual System.Threading.Tasks.Task> GetQuestionsByQuizIdEndpointAsync(int quizId)
+ {
+ return GetQuestionsByQuizIdEndpointAsync(quizId, System.Threading.CancellationToken.None);
+ }
+
+ /// A cancellation token that can be used by other objects or threads to receive notice of cancellation.
+ /// Success
+ /// A server side error occurred.
+ public virtual async System.Threading.Tasks.Task> GetQuestionsByQuizIdEndpointAsync(int quizId, System.Threading.CancellationToken cancellationToken)
+ {
+ if (quizId == null)
+ throw new System.ArgumentNullException("quizId");
+
+ var client_ = _httpClient;
+ var disposeClient_ = false;
+ try
+ {
+ using (var request_ = new System.Net.Http.HttpRequestMessage())
+ {
+ request_.Method = new System.Net.Http.HttpMethod("GET");
+ request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json"));
+
+ var urlBuilder_ = new System.Text.StringBuilder();
+ if (!string.IsNullOrEmpty(_baseUrl)) urlBuilder_.Append(_baseUrl);
+ // Operation Path: "question/getquestionbyquizid"
+ urlBuilder_.Append("question/getquestionbyquizid");
+ urlBuilder_.Append('?');
+ urlBuilder_.Append(System.Uri.EscapeDataString("QuizId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(quizId, System.Globalization.CultureInfo.InvariantCulture))).Append('&');
+ urlBuilder_.Length--;
+
+ PrepareRequest(client_, request_, urlBuilder_);
+
+ var url_ = urlBuilder_.ToString();
+ request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute);
+
+ PrepareRequest(client_, request_, url_);
+
+ var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
+ var disposeResponse_ = true;
+ try
+ {
+ var headers_ = new System.Collections.Generic.Dictionary>();
+ foreach (var item_ in response_.Headers)
+ headers_[item_.Key] = item_.Value;
+ if (response_.Content != null && response_.Content.Headers != null)
+ {
+ foreach (var item_ in response_.Content.Headers)
+ headers_[item_.Key] = item_.Value;
+ }
+
+ ProcessResponse(client_, response_);
+
+ var status_ = (int)response_.StatusCode;
+ if (status_ == 200)
+ {
+ var objectResponse_ = await ReadObjectResponseAsync>(response_, headers_, cancellationToken).ConfigureAwait(false);
+ if (objectResponse_.Object == null)
+ {
+ throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
+ }
+ return objectResponse_.Object;
+ }
+ else
+ {
+ var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
+ throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null);
+ }
+ }
+ finally
+ {
+ if (disposeResponse_)
+ response_.Dispose();
+ }
+ }
+ }
+ finally
+ {
+ if (disposeClient_)
+ client_.Dispose();
+ }
+ }
+
+ /// Success
+ /// A server side error occurred.
+ public virtual System.Threading.Tasks.Task SaveOrUpdateQuestionAsync(SaveOrUpdateQuestionRequest saveOrUpdateQuestionRequest)
+ {
+ return SaveOrUpdateQuestionAsync(saveOrUpdateQuestionRequest, System.Threading.CancellationToken.None);
+ }
+
+ /// A cancellation token that can be used by other objects or threads to receive notice of cancellation.
+ /// Success
+ /// A server side error occurred.
+ public virtual async System.Threading.Tasks.Task SaveOrUpdateQuestionAsync(SaveOrUpdateQuestionRequest saveOrUpdateQuestionRequest, System.Threading.CancellationToken cancellationToken)
+ {
+ if (saveOrUpdateQuestionRequest == null)
+ throw new System.ArgumentNullException("saveOrUpdateQuestionRequest");
+
+ var client_ = _httpClient;
+ var disposeClient_ = false;
+ try
+ {
+ using (var request_ = new System.Net.Http.HttpRequestMessage())
+ {
+ var json_ = Newtonsoft.Json.JsonConvert.SerializeObject(saveOrUpdateQuestionRequest, JsonSerializerSettings);
+ var content_ = new System.Net.Http.StringContent(json_);
+ content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json");
+ request_.Content = content_;
+ request_.Method = new System.Net.Http.HttpMethod("POST");
+ request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json"));
+
+ var urlBuilder_ = new System.Text.StringBuilder();
+ if (!string.IsNullOrEmpty(_baseUrl)) urlBuilder_.Append(_baseUrl);
+ // Operation Path: "question/savequestions"
+ urlBuilder_.Append("question/savequestions");
+
+ PrepareRequest(client_, request_, urlBuilder_);
+
+ var url_ = urlBuilder_.ToString();
+ request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute);
+
+ PrepareRequest(client_, request_, url_);
+
+ var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
+ var disposeResponse_ = true;
+ try
+ {
+ var headers_ = new System.Collections.Generic.Dictionary>();
+ foreach (var item_ in response_.Headers)
+ headers_[item_.Key] = item_.Value;
+ if (response_.Content != null && response_.Content.Headers != null)
+ {
+ foreach (var item_ in response_.Content.Headers)
+ headers_[item_.Key] = item_.Value;
+ }
+
+ ProcessResponse(client_, response_);
+
+ var status_ = (int)response_.StatusCode;
+ if (status_ == 200)
+ {
+ var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false);
+ if (objectResponse_.Object == null)
+ {
+ throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
+ }
+ return objectResponse_.Object;
+ }
+ else
+ {
+ var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
+ throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null);
+ }
+ }
+ finally
+ {
+ if (disposeResponse_)
+ response_.Dispose();
+ }
+ }
+ }
+ finally
+ {
+ if (disposeClient_)
+ client_.Dispose();
+ }
+ }
+
protected struct ObjectResponseResult
{
public ObjectResponseResult(T responseObject, string responseText)
diff --git a/QuizApp.Client/Pages/AddQuestion.razor b/QuizApp.Client/Pages/AddQuestion.razor
new file mode 100644
index 0000000..aa4ee1e
--- /dev/null
+++ b/QuizApp.Client/Pages/AddQuestion.razor
@@ -0,0 +1,90 @@
+@page "/add-question/{quizId:int}"
+@using QuizApp.Api
+@using QuizApp.Common.DTO
+@using System.Collections.ObjectModel
+@inject IQuizApiClient apiClient
+@inject DialogService DialogService
+@inject NotificationService NotificationService
+@inject NavigationManager NavigationManager
+
+
+ Add Questions & Answers
+
+
+
+
+ Add question
+
+
+
+
+
+
+
+ @foreach (var answer in data.Answers)
+ {
+ -
+ @answer.Text
+
+ @(answer.IsCorrect ? "Correct" : "Incorrect")
+
+
+ }
+
+
+
+
+
+
+ Save
+
+
+
+@code {
+ [Parameter] public int QuizId { get; set; }
+
+ private ObservableCollection Questions = new ObservableCollection();
+
+ private async Task SaveQuestion()
+ {
+ var result = await apiClient.SaveOrUpdateQuestionAsync(new Common.Request.SaveOrUpdateQuestionRequest
+ {
+ QuizId = QuizId,
+ QuestionDTOs = Questions,
+ });
+
+ if(result > 0)
+ {
+ NotificationService.Notify(NotificationSeverity.Success, "Save questions successfully");
+ }
+ }
+
+ private async Task AddNewQuestion()
+ {
+ DialogService.OnClose += AddNewQuestionClosed;
+ await DialogService.OpenAsync("Add New Question");
+ }
+
+ private void AddNewQuestionClosed(dynamic obj)
+ {
+ var questions = obj as QuestionDTO;
+ if (questions is not null)
+ {
+ Questions.Add(questions);
+ StateHasChanged();
+ }
+ }
+
+ protected override async Task OnInitializedAsync()
+ {
+ try
+ {
+ var questionDTOs = await apiClient.GetQuestionsByQuizIdEndpointAsync(QuizId);
+ Questions = new ObservableCollection(questionDTOs);
+ }
+ catch(Exception ex)
+ {
+
+ }
+ }
+}
diff --git a/QuizApp.Client/Pages/AddQuestionDialog.razor b/QuizApp.Client/Pages/AddQuestionDialog.razor
new file mode 100644
index 0000000..c63aaa8
--- /dev/null
+++ b/QuizApp.Client/Pages/AddQuestionDialog.razor
@@ -0,0 +1,49 @@
+@using QuizApp.Common.DTO
+@using System.Collections.ObjectModel
+@inject DialogService dialogService
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Add Answer
+ Save
+
+
+@code {
+ private QuestionDTO TheQuestion = new QuestionDTO();
+ private ObservableCollection Answers { get; set; } = new ObservableCollection();
+
+ private async Task AddAnswer()
+ {
+ Answers.Add(new AnswerDTO { Text = string.Empty, IsCorrect = false });
+ }
+
+ private void RemoveAnswer(AnswerDTO answer)
+ {
+ Answers.Remove(answer);
+ }
+
+ private void Save()
+ {
+ TheQuestion.Answers = Answers;
+ dialogService.Close(TheQuestion);
+ }
+}
diff --git a/QuizApp.Client/Pages/QuizPage.razor b/QuizApp.Client/Pages/QuizPage.razor
index ecba345..c74e4eb 100644
--- a/QuizApp.Client/Pages/QuizPage.razor
+++ b/QuizApp.Client/Pages/QuizPage.razor
@@ -5,6 +5,7 @@
@inject IQuizApiClient apiClient
@inject DialogService DialogService
@inject NotificationService NotificationService
+@inject NavigationManager NavigationManager
Quiz Manager
@@ -20,7 +21,7 @@ else if (!Quizzes.Any())
}
else
{
-
+
@@ -43,6 +44,14 @@ else
Quizzes = new ObservableCollection(a);
}
+ private void OnRowClick(DataGridRowMouseEventArgs selectedItem)
+ {
+ if (selectedItem.Data?.Id.HasValue == true)
+ {
+ NavigationManager.NavigateTo($"/add-question/{selectedItem.Data.Id.Value}");
+ }
+ }
+
private async Task AddNewQuiz()
{
DialogService.OnClose += AddNewQuizClosed;
diff --git a/QuizApp.Common/Request/GetQuestionByQuizIdRequest.cs b/QuizApp.Common/Request/GetQuestionByQuizIdRequest.cs
new file mode 100644
index 0000000..72dd2fa
--- /dev/null
+++ b/QuizApp.Common/Request/GetQuestionByQuizIdRequest.cs
@@ -0,0 +1,6 @@
+namespace QuizApp.Common.Request;
+
+public class GetQuestionByQuizIdRequest
+{
+ public int QuizId { get; set; }
+}
diff --git a/QuizApp.Common/Request/SaveOrUpdateQuestionRequest.cs b/QuizApp.Common/Request/SaveOrUpdateQuestionRequest.cs
new file mode 100644
index 0000000..497615d
--- /dev/null
+++ b/QuizApp.Common/Request/SaveOrUpdateQuestionRequest.cs
@@ -0,0 +1,10 @@
+using QuizApp.Common.DTO;
+
+namespace QuizApp.Common.Request;
+
+public class SaveOrUpdateQuestionRequest
+{
+ public int QuizId { get; set; }
+ public int QuestionId { get; set; }
+ public IEnumerable QuestionDTOs { get; set; }
+}
diff --git a/QuizApp.Database/Repositories/IQuizRepository.cs b/QuizApp.Database/Repositories/IQuizRepository.cs
index 4a96fe2..2f328d7 100644
--- a/QuizApp.Database/Repositories/IQuizRepository.cs
+++ b/QuizApp.Database/Repositories/IQuizRepository.cs
@@ -7,7 +7,9 @@ public interface IQuizRepository
Task DeleteQuiz(int quizId);
Task SaveOrUpdateQuiz(QuizDTO quizDTO);
Task SaveOrUpdateQuestion(QuestionDTO questionDto);
+ Task SaveOrUpdateQuestion(IEnumerable questionDTOs, int quizId);
Task> GetQuestions();
Task> GetAllQuizzes();
+ Task> GetQuestionsByQuizId(int quizId);
Task SaveSubmissionAsync(int userId, int quizId, IEnumerable userAnswers);
}
\ No newline at end of file
diff --git a/QuizApp.Database/Repositories/QuizRepository.cs b/QuizApp.Database/Repositories/QuizRepository.cs
index cfea40b..00b97d5 100644
--- a/QuizApp.Database/Repositories/QuizRepository.cs
+++ b/QuizApp.Database/Repositories/QuizRepository.cs
@@ -118,6 +118,86 @@ public async Task SaveOrUpdateQuestion(QuestionDTO questionDto)
return questionModel.Id;
}
+ public async Task SaveOrUpdateQuestion(IEnumerable questionDtos, int quizId)
+ {
+ var savedQuestionIds = new List();
+
+ var quizModel = db.Quizzes.SingleOrDefault(x => x.Id == quizId);
+ if (quizModel is null)
+ {
+ throw new InvalidOperationException($"Cannot find quiz with id = {quizId}");
+ }
+
+ foreach (var questionDto in questionDtos)
+ {
+ Question questionModel;
+
+ if (questionDto.Id > 0)
+ {
+ // Update existing question
+ questionModel = db.Questions.Include(q => q.Answers).SingleOrDefault(q => q.Id == questionDto.Id);
+ if (questionModel is null)
+ {
+ throw new InvalidOperationException($"Cannot find question with id = {questionDto.Id}");
+ }
+
+ questionModel.Title = questionDto.Title;
+
+ // Update or remove existing answers
+ var existingAnswers = questionModel.Answers.ToList();
+ foreach (var existingAnswer in existingAnswers)
+ {
+ var answerDto = questionDto.Answers.SingleOrDefault(a => a.Id == existingAnswer.Id);
+ if (answerDto != null)
+ {
+ existingAnswer.Text = answerDto.Text;
+ existingAnswer.IsCorrect = answerDto.IsCorrect;
+ }
+ else
+ {
+ db.Answers.Remove(existingAnswer);
+ }
+ }
+
+ // Add new answers
+ var newAnswers = questionDto.Answers.Where(a => a.Id == 0).ToList();
+ foreach (var newAnswerDto in newAnswers)
+ {
+ var newAnswer = new Answer
+ {
+ Text = newAnswerDto.Text,
+ IsCorrect = newAnswerDto.IsCorrect,
+ Question = questionModel
+ };
+ questionModel.Answers.Add(newAnswer);
+ }
+ }
+ else
+ {
+ // Create new question
+ questionModel = new Question
+ {
+ QuizId = quizModel.Id,
+ Quiz = quizModel,
+ Title = questionDto.Title,
+ Answers = questionDto.Answers.Select(a => new Answer
+ {
+ Text = a.Text,
+ IsCorrect = a.IsCorrect
+ }).ToList()
+ };
+
+ db.Questions.Add(questionModel);
+ }
+
+ await db.SaveChangesAsync();
+
+ savedQuestionIds.Add(questionModel.Id);
+ }
+
+ return savedQuestionIds.Count;
+ }
+
public async Task> GetQuestions()
{
return await db.Questions
@@ -207,4 +287,29 @@ public async Task DeleteQuiz(int quizId)
await db.SaveChangesAsync();
return true;
}
+
+ public async Task> GetQuestionsByQuizId(int quizId)
+ {
+ var questions = await db.Questions.Where(q => q.QuizId == quizId)
+ .Include(q => q.Answers)
+ .ToListAsync();
+
+ if(questions.Count == 0)
+ {
+ return Enumerable.Empty();
+ }
+
+ return questions.Select(q => new QuestionDTO
+ {
+ Id = q.Id,
+ QuizId = quizId,
+ Title = q.Title,
+ Answers = q.Answers.Select(a => new AnswerDTO
+ {
+ Id = a.Id,
+ Text = a.Text,
+ IsCorrect = a.IsCorrect
+ }).ToList()
+ });
+ }
}
\ No newline at end of file
diff --git a/QuizApp.Test/QuizRepositoryTest.cs b/QuizApp.Test/QuizRepositoryTest.cs
index 01173b9..a881396 100644
--- a/QuizApp.Test/QuizRepositoryTest.cs
+++ b/QuizApp.Test/QuizRepositoryTest.cs
@@ -19,6 +19,56 @@ public QuizRepositoryTest()
quizRepository = new QuizRepository(new QuizContext(Options));
}
+ [Fact]
+ public async Task GetQuestionsByQuizIdOK()
+ {
+ using (var setupContext = new QuizContext(Options))
+ {
+ // Ensure the database is clean
+ await setupContext.Database.EnsureDeletedAsync();
+ await setupContext.Database.EnsureCreatedAsync();
+
+ // Seed the required quiz
+ setupContext.Quizzes.Add(new Quiz
+ {
+ Id = 1,
+ QuizName = "General Knowledge",
+ Description = "GK",
+ });
+
+ setupContext.Questions.Add(new Question
+ {
+ QuizId = 1,
+ Title = "What is the capital of France?",
+ Answers = new List
+ {
+ new Answer { Text = "Paris", IsCorrect = true },
+ new Answer { Text = "London", IsCorrect = false }
+ }
+ });
+
+ setupContext.Questions.Add(new Question
+ {
+ QuizId = 1,
+ Title = "What is the capital of Viet Nam?",
+ Answers = new List
+ {
+ new Answer { Text = "Hanoi", IsCorrect = true },
+ new Answer { Text = "HCMC", IsCorrect = false }
+ }
+ });
+
+ await setupContext.SaveChangesAsync();
+ }
+
+ // Act
+ var result = await quizRepository.GetQuestionsByQuizId(1);
+
+ // Assert
+ result.Should().NotBeEmpty();
+ result.Count().Should().Be(2);
+ }
+
[Fact]
public async Task DeleteQuizOK()
{
diff --git a/QuizApp/Endpoints/Question/GetQuestionsByQuizIdEndpoint.cs b/QuizApp/Endpoints/Question/GetQuestionsByQuizIdEndpoint.cs
new file mode 100644
index 0000000..0eaea92
--- /dev/null
+++ b/QuizApp/Endpoints/Question/GetQuestionsByQuizIdEndpoint.cs
@@ -0,0 +1,32 @@
+using FastEndpoints;
+using QuizApp.Common.DTO;
+using QuizApp.Common.Request;
+using QuizApp.Database.Repositories;
+
+namespace QuizApp.Endpoints.Question;
+
+public class GetQuestionsByQuizIdEndpoint : Endpoint>
+{
+ private readonly IQuizRepository quizRepository;
+ public GetQuestionsByQuizIdEndpoint(IQuizRepository quizRepository)
+ {
+ this.quizRepository = quizRepository;
+ }
+
+ public override void Configure()
+ {
+ Get("question/getquestionbyquizid");
+ AllowAnonymous();
+ }
+
+ public override async Task HandleAsync(GetQuestionByQuizIdRequest r, CancellationToken ct)
+ {
+ var result = await quizRepository.GetQuestionsByQuizId(r.QuizId);
+ if (result == null)
+ {
+ ThrowError("Cannot get all questions");
+ }
+
+ await SendAsync(result);
+ }
+}
diff --git a/QuizApp/Endpoints/Question/SaveOrUpdateQuestion.cs b/QuizApp/Endpoints/Question/SaveOrUpdateQuestion.cs
new file mode 100644
index 0000000..4fed9f7
--- /dev/null
+++ b/QuizApp/Endpoints/Question/SaveOrUpdateQuestion.cs
@@ -0,0 +1,32 @@
+using FastEndpoints;
+using QuizApp.Common.Request;
+using QuizApp.Database.Repositories;
+
+namespace QuizApp.Endpoints.Question
+{
+ public class SaveOrUpdateQuestion : Endpoint
+ {
+ private readonly IQuizRepository quizRepository;
+ public SaveOrUpdateQuestion(IQuizRepository quizRepository)
+ {
+ this.quizRepository = quizRepository;
+ }
+
+ public override void Configure()
+ {
+ Post("question/savequestions");
+ AllowAnonymous();
+ }
+
+ public override async Task HandleAsync(SaveOrUpdateQuestionRequest r, CancellationToken ct)
+ {
+ var result = await quizRepository.SaveOrUpdateQuestion(r.QuestionDTOs, r.QuizId);
+ if (result < 0)
+ {
+ ThrowError("Cannot save all questions");
+ }
+
+ await SendAsync(result);
+ }
+ }
+}