Files
step3/api/QuestionsApi/Controllers/QuestionsController.cs
T

185 lines
6.5 KiB
C#

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MongoDB.Driver;
using QuestionsApi.Models;
using QuestionsApi.Services;
using System.Security.Claims;
namespace QuestionsApi.Controllers;
[ApiController]
[Route("api/[controller]")]
public class QuestionsController : ControllerBase
{
private readonly QuestionStore _questions;
private readonly QuestionAttemptStore _attempts;
public QuestionsController(QuestionStore questions, QuestionAttemptStore attempts)
{
_questions = questions;
_attempts = attempts;
}
[HttpGet("systems")]
[Authorize(Roles = "USER,SUPER")]
public async Task<IActionResult> Systems()
{
var systems = await _questions.GetSystemsAsync(User.IsInRole("SUPER"));
return Ok(systems);
}
[HttpPost("search")]
[Authorize(Roles = "USER,SUPER")]
public async Task<IActionResult> Search([FromBody] QuestionSearchRequest req)
{
var questions = await _questions.SearchAsync(req, User.IsInRole("SUPER"));
var percentages = await _attempts.GetPercentagesByQuestionAsync(questions.Select(q => q.ExternalId));
return Ok(questions.Select(q =>
{
percentages.TryGetValue(q.ExternalId, out var questionPercentages);
return QuestionResponse.FromModel(q, questionPercentages);
}).ToList());
}
[HttpGet("{id}")]
[Authorize(Roles = "USER,SUPER")]
public async Task<IActionResult> Get(string id)
{
var question = await _questions.GetByIdAsync(id);
if (question is null || (!question.IsPublished && !User.IsInRole("SUPER")))
return NotFound();
var percentages = await _attempts.GetPercentagesForQuestionAsync(question.ExternalId);
return Ok(QuestionResponse.FromModel(question, percentages));
}
[HttpPost("attempts/first")]
[Authorize(Roles = "USER,SUPER")]
public async Task<IActionResult> RecordFirstAttempt([FromBody] FirstQuestionAttemptRequest req)
{
if (string.IsNullOrWhiteSpace(req.QuestionExternalId))
return BadRequest("questionExternalId required");
if (string.IsNullOrWhiteSpace(req.ChoiceId))
return BadRequest("choiceId required");
var question = await _questions.GetByExternalIdAsync(req.QuestionExternalId.Trim());
if (question is null || (!question.IsPublished && !User.IsInRole("SUPER")))
return NotFound();
if (!question.Choices.Any(c => c.Id == req.ChoiceId.Trim()))
return BadRequest("choiceId must match a choice id");
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier)
?? User.FindFirstValue(ClaimTypes.Name)
?? User.Identity?.Name
?? "";
if (string.IsNullOrWhiteSpace(userId))
return Unauthorized();
await _attempts.RecordFirstAttemptAsync(
question.ExternalId,
userId,
req.ChoiceId.Trim(),
req.ChoiceId.Trim() == question.CorrectChoiceId);
return Ok(new FirstQuestionAttemptResponse
{
QuestionExternalId = question.ExternalId,
ChoiceSelectionPercentages = await _attempts.GetPercentagesForQuestionAsync(question.ExternalId)
});
}
[HttpPost]
[Authorize(Roles = "SUPER")]
public async Task<IActionResult> Create([FromBody] UpsertQuestionRequest req)
{
var validation = ValidateQuestion(req);
if (validation is not null)
return BadRequest(validation);
var created = await _questions.CreateAsync(req);
if (created is null)
return Conflict("Question externalId already exists");
return Ok(QuestionResponse.FromModel(created));
}
[HttpPost("bulk")]
[Authorize(Roles = "SUPER")]
public async Task<IActionResult> BulkUpsert([FromBody] BulkUpsertQuestionsRequest req)
{
if (req.Questions.Count == 0)
return BadRequest("questions required");
foreach (var question in req.Questions)
{
var validation = ValidateQuestion(question);
if (validation is not null)
return BadRequest($"{question.ExternalId}: {validation}");
}
var upserted = await _questions.UpsertManyByExternalIdAsync(req.Questions);
return Ok(new { upserted });
}
[HttpPut("{id}")]
[Authorize(Roles = "SUPER")]
public async Task<IActionResult> Update(string id, [FromBody] UpsertQuestionRequest req)
{
var validation = ValidateQuestion(req);
if (validation is not null)
return BadRequest(validation);
try
{
var updated = await _questions.UpdateAsync(id, req);
if (updated is null)
return NotFound();
return Ok(QuestionResponse.FromModel(updated));
}
catch (MongoWriteException ex) when (ex.WriteError.Category == ServerErrorCategory.DuplicateKey)
{
return Conflict("Question externalId already exists");
}
}
[HttpDelete("{id}")]
[Authorize(Roles = "SUPER")]
public async Task<IActionResult> Delete(string id)
{
var deleted = await _questions.DeleteAsync(id);
if (!deleted)
return NotFound();
return Ok("Deleted");
}
private static string? ValidateQuestion(UpsertQuestionRequest req)
{
if (string.IsNullOrWhiteSpace(req.ExternalId))
return "externalId required";
if (string.IsNullOrWhiteSpace(req.Stem))
return "stem required";
if (string.IsNullOrWhiteSpace(req.LeadIn))
return "leadIn required";
if (req.Choices.Count < 2)
return "at least two choices required";
if (string.IsNullOrWhiteSpace(req.CorrectChoiceId))
return "correctChoiceId required";
if (!req.Choices.Any(c => c.Id == req.CorrectChoiceId))
return "correctChoiceId must match a choice id";
if (string.IsNullOrWhiteSpace(req.Explanation))
return "explanation required";
if (req.ChoiceSelectionPercentages.Any(entry => entry.Value is < 0 or > 100))
return "choiceSelectionPercentages values must be between 0 and 100";
if (string.IsNullOrWhiteSpace(req.System))
return "system required";
if (string.IsNullOrWhiteSpace(req.Discipline))
return "discipline required";
if (req.Difficulty is < 1 or > 5)
return "difficulty must be between 1 and 5";
return null;
}
}