chore: removed everything related to instructions

This commit is contained in:
2025-09-20 22:42:33 +03:00
parent b6778046c2
commit 8ea18f1096
50 changed files with 0 additions and 1953 deletions

View File

@@ -1,166 +0,0 @@
using GamificationService.Exceptions.Services.Instruction;
using GamificationService.Models.Database;
using GamificationService.Models.DTO;
using GamificationService.Models.Messages.Instructions;
using GamificationService.Services.Instructions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
namespace GamificationService.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize(Policy = "User")]
public class InstructionController : ControllerBase
{
private readonly IInstructionService _instructionService;
private readonly UserManager<ApplicationUser> _userManager;
private readonly ILogger<InstructionController> _logger;
public InstructionController(IInstructionService instructionService, UserManager<ApplicationUser> userManager, ILogger<InstructionController> logger)
{
_instructionService = instructionService;
_userManager = userManager;
_logger = logger;
}
/// <summary>
/// Create a new instruction.
/// </summary>
/// <param name="model">The instruction model.</param>
/// <returns><see cref="InstructionDTO"/> which was created</returns>
/// <response code="200">Returns the created instruction</response>
[HttpPost]
[Authorize(Policy = "Admin")]
public async Task<IActionResult> CreateInstruction([FromBody] CreateInstructionRequest model)
{
var instruction = await _instructionService.CreateInstruction(model);
return Ok(instruction);
}
/// <summary>
/// Update an existing instruction.
/// </summary>
/// <param name="model">The instruction model. Id must match the object which is being updated.</param>
/// <returns><see cref="bool"/></returns>
/// <response code="200"></response>
/// <response code="404">If the instruction is not found</response>
[HttpPut]
[Authorize(Policy = "Admin")]
public async Task<IActionResult> UpdateInstruction([FromBody] UpdateInstructionRequest model)
{
var instruction = await _instructionService.UpdateInstructionById(model);
return Ok(instruction);
}
/// <summary>
/// Delete an existing instruction.
/// </summary>
/// <param name="id">The ID of the instruction to delete.</param>
/// <returns><see cref="bool"/></returns>
/// <response code="200"></response>
/// <response code="404">If the instruction is not found</response>
[HttpDelete]
[Authorize(Policy = "Admin")]
public async Task<IActionResult> DeleteInstruction(long id)
{
try
{
return Ok(await _instructionService.DeleteInstructionById(id));
}
catch (InstructionNotFoundException)
{
return NotFound();
}
}
/// <summary>
/// Retrieve all instructions for the authenticated user.
/// </summary>
/// <returns>A list of <see cref="InstructionDTO"/> for the user.</returns>
/// <response code="200">Returns the list of all instructions</response>
[HttpGet("all")]
public async Task<IActionResult> GetAllInstructions()
{
string username = User.Claims.First(c => c.Type == "username").Value;
long userId = (await _userManager.FindByNameAsync(username))!.Id;
return Ok(_instructionService.GetAllInstructions(userId));
}
/// <summary>
/// Retrieve all completed instructions for the authenticated user.
/// </summary>
/// <returns>A list of <see cref="InstructionDTO"/> that are completed for the user.</returns>
/// <response code="200">Returns the list of completed instructions</response>
[HttpGet("completed")]
public async Task<IActionResult> GetCompletedInstructions()
{
string username = User.Claims.First(c => c.Type == "username").Value;
long userId = (await _userManager.FindByNameAsync(username))!.Id;
return Ok(_instructionService.GetCompletedInstructions(userId));
}
/// <summary>
/// Retrieve all unfinished instructions for the authenticated user.
/// </summary>
/// <returns>A list of <see cref="InstructionDTO"/> that are unfinished for the user.</returns>
/// <response code="200">Returns the list of unfinished instructions</response>
[HttpGet("unfinished")]
public async Task<IActionResult> GetUnfinishedInstructions()
{
string username = User.Claims.First(c => c.Type == "username").Value;
long userId = (await _userManager.FindByNameAsync(username))!.Id;
return Ok(_instructionService.GetUnfinishedInstructions(userId));
}
/// <summary>
/// Retrieve instructions by category ID for the authenticated user.
/// </summary>
/// <param name="id">The ID of the category to filter instructions.</param>
/// <returns>A list of <see cref="InstructionDTO"/> for the specified category.</returns>
/// <response code="200">Returns the list of instructions for the specified category</response>
/// <response code="404">If the category is not found</response>
[HttpGet("category/{id}")]
public async Task<IActionResult> GetInstructionsByCategoryId(long id)
{
try
{
string username = User.Claims.First(c => c.Type == "username").Value;
long userId = (await _userManager.FindByNameAsync(username))!.Id;
return Ok(_instructionService.GetInstructionsByCategoryId(userId, id));
}
catch (CategoryNotFoundException)
{
return NotFound();
}
}
/// <summary>
/// Retrieve a specific instruction by its ID for the authenticated user.
/// </summary>
/// <param name="id">The ID of the instruction to retrieve.</param>
/// <returns><see cref="InstructionDTO"/> for the specified instruction.</returns>
/// <response code="200">Returns the instruction with the specified ID</response>
/// <response code="404">If the instruction is not found</response>
[HttpGet("{id}")]
public async Task<IActionResult> GetInstructionById(long id)
{
try
{
string username = User.Claims.First(c => c.Type == "username").Value;
long userId = (await _userManager.FindByNameAsync(username))!.Id;
return Ok(_instructionService.GetInstructionById(userId, id));
}
catch(InstructionNotFoundException)
{
return NotFound();
}
}
}

View File

@@ -1,278 +0,0 @@
using AutoMapper;
using GamificationService.Exceptions.Services.Instruction;
using GamificationService.Exceptions.Services.InstructionTest;
using GamificationService.Models.BasicResponses;
using GamificationService.Models.Database;
using GamificationService.Models.DTO;
using GamificationService.Services.InstructionTests;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
namespace GamificationService.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize(Policy = "User")]
public class InstructionTestController : ControllerBase
{
private readonly IInstructionTestsService _instructionTestsService;
private readonly UserManager<ApplicationUser> _userManager;
private readonly ILogger<InstructionTestController> _logger;
private readonly IMapper _mapper;
public InstructionTestController(IInstructionTestsService instructionTestsService, UserManager<ApplicationUser> userManager, ILogger<InstructionTestController> logger, IMapper mapper)
{
_instructionTestsService = instructionTestsService;
_userManager = userManager;
_logger = logger;
_mapper = mapper;
}
/// <summary>
/// Gets an instruction test by its ID.
/// </summary>
/// <param name="id">The ID of the instruction test.</param>
/// <returns>An <see cref="InstructionTestDTO"/> containing the instruction test DTO if found, or a 404 Not Found if not found.</returns>
/// <response code="200">Returns the instruction test DTO</response>
/// <response code="404">If the instruction test is not found</response>
[HttpGet("{id}")]
public IActionResult GetInstructionTestById(long id)
{
// TODO: verify admin access / user ownership
try
{
var instructionTest = _instructionTestsService.GetInstructionTestById(id);
return Ok(_mapper.Map<InstructionTestDTO>(instructionTest));
}
catch (InstructionTestNotFoundException)
{
return NotFound();
}
}
/// <summary>
/// Gets an instruction test by its instruction ID.
/// </summary>
/// <param name="id">The ID of the instruction.</param>
/// <returns>An <see cref="InstructionTestDTO"/> containing the instruction test DTO if found, or a 404 Not Found if not found.</returns>
/// <response code="200">Returns the instruction test DTO</response>
/// <response code="404">If the instruction is not found</response>
[HttpGet("instruction/{id}")]
public IActionResult GetInstructionTestsByInstructionId(long id)
{
// TODO: verify admin access / user ownership
try
{
var instructionTest = _instructionTestsService.GetInstructionTestsByInstructionId(id);
return Ok(_mapper.Map<InstructionTestDTO>(instructionTest));
}
catch (InstructionTestNotFoundException)
{
return Ok(new List<InstructionTestDTO>());
}
catch (InstructionNotFoundException)
{
return NotFound(new BasicResponse()
{
Code = 404,
Message = "Instruction not found"
});
}
}
/// <summary>
/// Gets all instruction test questions by instruction test ID.
/// </summary>
/// <param name="instructionTestId">The ID of the instruction test.</param>
/// <returns>A list of <see cref="InstructionTestQuestionDTO"/> containing the instruction test questions if found, or a 404 Not Found if not found.</returns>
/// <response code="200">Returns the instruction test questions</response>
/// <response code="404">If the instruction test questions are not found</response>
[HttpGet("{instructionTestId}/questions")]
public IActionResult GetInstructionTestQuestionsByInstructionTestId(long instructionTestId)
{
// TODO: verify admin access / user ownership
try
{
var instructionTestQuestions = _instructionTestsService.GetInstructionTestQuestionsByInstructionTestId(instructionTestId);
return Ok(instructionTestQuestions);
}
catch (InstructionTestNotFoundException)
{
return NotFound();
}
}
/// <summary>
/// Gets all instruction test results for authorized user by instruction ID.
/// </summary>
/// <param name="instructionTestId">The ID of the instruction.</param>
/// <returns>A list of <see cref="InstructionTestResultDTO"/> containing the instruction test results if found, or a 404 Not Found if not found.</returns>
/// <response code="200">Returns the instruction test results</response>
/// <response code="404">If the instruction test results are not found</response>
[HttpGet("/{instructionTestId}/results")]
public async Task<IActionResult> GetUserInstructionTestResultsByInstructionTestId(long instructionTestId)
{
// TODO: verify user ownership
string username = User.Claims.First(c => c.Type == "username").Value;
long userId = (await _userManager.FindByNameAsync(username))!.Id;
try
{
var instructionTestResults = _instructionTestsService.GetUserInstructionTestResultsByInstructionTestId(userId, instructionTestId);
return Ok(instructionTestResults);
}
catch (InstructionTestNotFoundException)
{
return NotFound();
}
}
/// <summary>
/// Gets all instruction test results for a specific user by instruction test ID (admin access).
/// </summary>
/// <param name="userId">The ID of the user whose results are being requested.</param>
/// <param name="instructionTestId">The ID of the instruction.</param>
/// <returns>A list of <see cref="InstructionTestResultDTO"/> containing the instruction test results if found, or a 404 Not Found if not found.</returns>
/// <response code="200">Returns the instruction test results</response>
/// <response code="404">If the instruction test results are not found</response>
/// <response code="403">If the user is not an admin</response>
[HttpGet("{instructionTestId}/user/{userId}/results")]
[Authorize(Roles = "Admin")]
public IActionResult GetInstructionTestResultsForUserByInstructionTestId(long userId, long instructionTestId)
{
try
{
var instructionTestResults = _instructionTestsService.GetUserInstructionTestResultsByInstructionTestId(userId, instructionTestId);
return Ok(instructionTestResults);
}
catch (InstructionTestNotFoundException)
{
return NotFound();
}
}
/// <summary>
/// Gets all instruction test results for a user by user ID.
/// </summary>
/// <param name="id">The ID of the user.</param>
/// <returns>A list of <see cref="InstructionTestResultDTO"/> containing the instruction test results if found, or a 404 Not Found if not found.</returns>
/// <response code="200">Returns the instruction test results</response>
/// <response code="404">If the instruction test results are not found</response>
[HttpGet("user/{id}/results")]
public IActionResult GetInstructionTestResultsByUserId(long id)
{
// TODO: verify admin access / user ownership
try
{
var instructionTestResults = _instructionTestsService.GetInstructionTestResultsByUserId(id);
return Ok(instructionTestResults);
}
catch (InstructionTestNotFoundException)
{
return NotFound();
}
}
/// <summary>
/// Gets all completed instruction test results for a user by user ID.
/// </summary>
/// <param name="id">The ID of the user.</param>
/// <returns>A list of <see cref="InstructionTestResultDTO"/> containing the instruction test results if found, or a 404 Not Found if not found.</returns>
/// <response code="200">Returns the instruction test results</response>
/// <response code="404">If the instruction test results are not found</response>
[HttpGet("user/{id}/completed")]
public IActionResult GetCompletedInstructionTestsByUserId(long id)
{
// TODO: verify admin access / user ownership
try
{
var instructionTestResults = _instructionTestsService.GetCompletedInstructionTestsByUserId(id);
return Ok(instructionTestResults);
}
catch (InstructionTestNotFoundException)
{
return NotFound();
}
}
/// <summary>
/// Creates a new instruction test.
/// </summary>
/// <param name="model">The instruction test model.</param>
/// <returns>A <see cref="InstructionTestDTO"/> containing the created instruction test if successful, or a 500 Internal Server Error if not successful.</returns>
/// <response code="200">Returns the created instruction test</response>
[HttpPost]
public async Task<IActionResult> CreateInstructionTest([FromBody] InstructionTestCreateDTO model)
{
try
{
var instructionTest = await _instructionTestsService.CreateInstructionTest(model);
return Ok(instructionTest);
}
catch (Exception)
{
return StatusCode(500, "Failed to create instruction test");
}
}
/// <summary>
/// Updates an existing instruction test.
/// </summary>
/// <param name="model">The instruction test model.</param>
/// <returns>A <see cref="InstructionTestDTO"/> containing the updated instruction test if successful, or a 500 Internal Server Error if not successful.</returns>
/// <response code="200">Returns the updated instruction test</response>
[HttpPut]
[Authorize(Policy = "Admin")]
public async Task<IActionResult> UpdateInstructionTest([FromBody] InstructionTestCreateDTO model)
{
try
{
var instructionTest = await _instructionTestsService.UpdateInstructionTest(model);
return Ok(instructionTest);
}
catch (Exception)
{
return StatusCode(500, "Failed to update instruction test");
}
}
/// <summary>
/// Deletes an existing instruction test.
/// </summary>
/// <param name="id">The ID of the instruction test to delete.</param>
/// <returns>A <see cref="bool"/></returns>
/// <response code="200">Returns the deletion status.</response>
[HttpDelete("{id}")]
[Authorize(Policy = "Admin")]
public async Task<IActionResult> DeleteInstructionTest(long id)
{
try
{
await _instructionTestsService.DeleteInstructionTestByIdAsync(id);
return Ok();
}
catch (Exception)
{
return StatusCode(500, "Failed to delete instruction test");
}
}
[HttpPost("submit")]
public async Task<IActionResult> SubmitInstructionTest([FromBody] InstructionTestSubmissionDTO model)
{
// TODO: verify user access
string username = User.Claims.First(c => c.Type == "username").Value;
long userId = (await _userManager.FindByNameAsync(username))!.Id;
try
{
await _instructionTestsService.SubmitInstructionTestAsync(userId, model);
return Ok();
}
catch (Exception)
{
return StatusCode(500, "Failed to submit instruction test");
}
}
}

View File

@@ -18,12 +18,6 @@ public class ApplicationContext : IdentityDbContext<ApplicationUser, Application
public DbSet<UserRole> UserRoles { get; set; }
public DbSet<RoleRight> RoleRights { get; set; }
public DbSet<UserProfile> UserProfiles { get; set; }
public DbSet<Instruction> Instructions { get; set; }
public DbSet<InstructionCategory> InstructionCategories { get; set; }
public DbSet<InstructionParagraph> InstructionParagraphs { get; set; }
public DbSet<InstructionTest> InstructionTests { get; set; }
public DbSet<InstructionTestQuestion> InstructionTestQuestions { get; set; }
public DbSet<InstructionTestResult> InstructionTestResults { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
@@ -34,17 +28,5 @@ public class ApplicationContext : IdentityDbContext<ApplicationUser, Application
modelBuilder.Entity<RoleRight>()
.HasKey(rr => new { rr.RoleId, rr.RightId });
modelBuilder.Entity<InstructionTest>()
.HasMany(itq => itq.Questions);
modelBuilder.Entity<InstructionTestResult>()
.HasOne(itr => itr.InstructionTest);
modelBuilder.Entity<Instruction>()
.HasOne(i => i.Category);
modelBuilder.Entity<Instruction>()
.HasMany(i => i.Paragraphs);
}
}

View File

@@ -14,12 +14,6 @@ public class UnitOfWork : IDisposable
private GenericRepository<RefreshToken> _refreshTokenRepository;
private GenericRepository<RoleRight> _roleRightRepository;
private GenericRepository<UserRole> _userRoleRepository;
private GenericRepository<Instruction> _instructionRepository;
private GenericRepository<InstructionParagraph> _instructionParagraphRepository;
private GenericRepository<InstructionCategory> _instructionCategoryRepository;
private GenericRepository<InstructionTest> _instructionTestRepository;
private GenericRepository<InstructionTestQuestion> _instructionTestQuestionRepository;
private GenericRepository<InstructionTestResult> _instructionTestResultRepository;
#endregion
@@ -105,83 +99,8 @@ public class UnitOfWork : IDisposable
return _userRoleRepository;
}
}
public GenericRepository<Instruction> InstructionRepository
{
get
{
if (this._instructionRepository == null)
{
this._instructionRepository = new GenericRepository<Instruction>(_context);
}
return _instructionRepository;
}
}
public GenericRepository<InstructionParagraph> InstructionParagraphRepository
{
get
{
if (this._instructionParagraphRepository == null)
{
this._instructionParagraphRepository = new GenericRepository<InstructionParagraph>(_context);
}
return _instructionParagraphRepository;
}
}
public GenericRepository<InstructionCategory> InstructionCategoryRepository
{
get
{
if (this._instructionCategoryRepository == null)
{
this._instructionCategoryRepository = new GenericRepository<InstructionCategory>(_context);
}
return _instructionCategoryRepository;
}
}
public GenericRepository<InstructionTest> InstructionTestRepository
{
get
{
if (this._instructionTestRepository == null)
{
this._instructionTestRepository = new GenericRepository<InstructionTest>(_context);
}
return _instructionTestRepository;
}
}
public GenericRepository<InstructionTestQuestion> InstructionTestQuestionRepository
{
get
{
if (this._instructionTestQuestionRepository == null)
{
this._instructionTestQuestionRepository = new GenericRepository<InstructionTestQuestion>(_context);
}
return _instructionTestQuestionRepository;
}
}
public GenericRepository<InstructionTestResult> InstructionTestResultRepository
{
get
{
if (this._instructionTestResultRepository == null)
{
this._instructionTestResultRepository = new GenericRepository<InstructionTestResult>(_context);
}
return _instructionTestResultRepository;
}
}
#endregion
public bool Save()
{

View File

@@ -1,25 +0,0 @@
namespace GamificationService.Exceptions.Services.Instruction;
public class CategoryNotFoundException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="CategoryNotFoundException"/> class.
/// </summary>
public CategoryNotFoundException() : base() { }
/// <summary>
/// Initializes a new instance of the <see cref="CategoryNotFoundException"/> class with a specified error message.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public CategoryNotFoundException(string message) : base(message) { }
/// <summary>
/// Initializes a new instance of the <see cref="CategoryNotFoundException"/> class with a specified error message and a reference to the inner exception that is the cause of this exception.
/// </summary>
/// <param name="message">The error message that explains the reason for the exception.</param>
/// <param name="innerException">The exception that is the cause of the current exception, or a null reference if no inner exception is specified.</param>
public CategoryNotFoundException(string message, Exception innerException) : base(message, innerException) { }
}

View File

@@ -1,24 +0,0 @@
namespace GamificationService.Exceptions.Services.Instruction;
public class InstructionAccessException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="InstructionAccessException"/> class.
/// </summary>
public InstructionAccessException() : base() { }
/// <summary>
/// Initializes a new instance of the <see cref="InstructionAccessException"/> class with a specified error message.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public InstructionAccessException(string message) : base(message) { }
/// <summary>
/// Initializes a new instance of the <see cref="InstructionAccessException"/> class with a specified error message and a reference to the inner exception that is the cause of this exception.
/// </summary>
/// <param name="message">The error message that explains the reason for the exception.</param>
/// <param name="innerException">The exception that is the cause of the current exception, or a null reference if no inner exception is specified.</param>
public InstructionAccessException(string message, Exception innerException) : base(message, innerException) { }
}

View File

@@ -1,24 +0,0 @@
namespace GamificationService.Exceptions.Services.Instruction;
public class InstructionCreationException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="InstructionCreationException"/> class.
/// </summary>
public InstructionCreationException() : base() { }
/// <summary>
/// Initializes a new instance of the <see cref="InstructionCreationException"/> class with a specified error message.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public InstructionCreationException(string message) : base(message) { }
/// <summary>
/// Initializes a new instance of the <see cref="InstructionCreationException"/> class with a specified error message and a reference to the inner exception that is the cause of this exception.
/// </summary>
/// <param name="message">The error message that explains the reason for the exception.</param>
/// <param name="innerException">The exception that is the cause of the current exception, or a null reference if no inner exception is specified.</param>
public InstructionCreationException(string message, Exception innerException) : base(message, innerException) { }
}

View File

@@ -1,25 +0,0 @@
namespace GamificationService.Exceptions.Services.Instruction;
public class InstructionDeletionException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="InstructionDeletionException"/> class.
/// </summary>
public InstructionDeletionException() : base() { }
/// <summary>
/// Initializes a new instance of the <see cref="InstructionDeletionException"/> class with a specified error message.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public InstructionDeletionException(string message) : base(message) { }
/// <summary>
/// Initializes a new instance of the <see cref="InstructionDeletionException"/> class with a specified error message and a reference to the inner exception that is the cause of this exception.
/// </summary>
/// <param name="message">The error message that explains the reason for the exception.</param>
/// <param name="innerException">The exception that is the cause of the current exception, or a null reference if no inner exception is specified.</param>
public InstructionDeletionException(string message, Exception innerException) : base(message, innerException) { }
}

View File

@@ -1,25 +0,0 @@
namespace GamificationService.Exceptions.Services.Instruction;
public class InstructionNotFoundException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="InstructionNotFoundException"/> class.
/// </summary>
public InstructionNotFoundException() : base() { }
/// <summary>
/// Initializes a new instance of the <see cref="InstructionNotFoundException"/> class with a specified error message.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public InstructionNotFoundException(string message) : base(message) { }
/// <summary>
/// Initializes a new instance of the <see cref="InstructionNotFoundException"/> class with a specified error message and a reference to the inner exception that is the cause of this exception.
/// </summary>
/// <param name="message">The error message that explains the reason for the exception.</param>
/// <param name="innerException">The exception that is the cause of the current exception, or a null reference if no inner exception is specified.</param>
public InstructionNotFoundException(string message, Exception innerException) : base(message, innerException) { }
}

View File

@@ -1,25 +0,0 @@
namespace GamificationService.Exceptions.Services.Instruction;
public class InstructionUpdateException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="InstructionUpdateException"/> class.
/// </summary>
public InstructionUpdateException() : base() { }
/// <summary>
/// Initializes a new instance of the <see cref="InstructionUpdateException"/> class with a specified error message.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public InstructionUpdateException(string message) : base(message) { }
/// <summary>
/// Initializes a new instance of the <see cref="InstructionUpdateException"/> class with a specified error message and a reference to the inner exception that is the cause of this exception.
/// </summary>
/// <param name="message">The error message that explains the reason for the exception.</param>
/// <param name="innerException">The exception that is the cause of the current exception, or a null reference if no inner exception is specified.</param>
public InstructionUpdateException(string message, Exception innerException) : base(message, innerException) { }
}

View File

@@ -1,26 +0,0 @@
namespace GamificationService.Exceptions.Services.InstructionTest;
/// <summary>
/// Represents an exception that occurs when there is a conflict in instruction test operations.
/// </summary>
public class InstructionTestConflictException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="InstructionTestConflictException"/> class.
/// </summary>
public InstructionTestConflictException() : base() { }
/// <summary>
/// Initializes a new instance of the <see cref="InstructionTestConflictException"/> class with a specified error message.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public InstructionTestConflictException(string message) : base(message) { }
/// <summary>
/// Initializes a new instance of the <see cref="InstructionTestConflictException"/> class with a specified error message and a reference to the inner exception that is the cause of this exception.
/// </summary>
/// <param name="message">The error message that explains the reason for the exception.</param>
/// <param name="innerException">The exception that is the cause of the current exception, or a null reference if no inner exception is specified.</param>
public InstructionTestConflictException(string message, Exception innerException) : base(message, innerException) { }
}

View File

@@ -1,26 +0,0 @@
namespace GamificationService.Exceptions.Services.InstructionTest;
/// <summary>
/// Represents an exception that occurs during the creation of an instruction test.
/// </summary>
public class InstructionTestCreationException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="InstructionTestCreationException"/> class.
/// </summary>
public InstructionTestCreationException() : base() { }
/// <summary>
/// Initializes a new instance of the <see cref="InstructionTestCreationException"/> class with a specified error message.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public InstructionTestCreationException(string message) : base(message) { }
/// <summary>
/// Initializes a new instance of the <see cref="InstructionTestCreationException"/> class with a specified error message and a reference to the inner exception that is the cause of this exception.
/// </summary>
/// <param name="message">The error message that explains the reason for the exception.</param>
/// <param name="innerException">The exception that is the cause of the current exception, or a null reference if no inner exception is specified.</param>
public InstructionTestCreationException(string message, Exception innerException) : base(message, innerException) { }
}

View File

@@ -1,25 +0,0 @@
namespace GamificationService.Exceptions.Services.InstructionTest;
/// <summary>
/// Represents an exception that occurs during the deletion of an instruction test.
/// </summary>
public class InstructionTestDeletionException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="InstructionTestDeletionException"/> class.
/// </summary>
public InstructionTestDeletionException() : base() { }
/// <summary>
/// Initializes a new instance of the <see cref="InstructionTestDeletionException"/> class with a specified error message.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public InstructionTestDeletionException(string message) : base(message) { }
/// <summary>
/// Initializes a new instance of the <see cref="InstructionTestDeletionException"/> class with a specified error message and a reference to the inner exception that is the cause of this exception.
/// </summary>
/// <param name="message">The error message that explains the reason for the exception.</param>
/// <param name="innerException">The exception that is the cause of the current exception, or a null reference if no inner exception is specified.</param>
public InstructionTestDeletionException(string message, Exception innerException) : base(message, innerException) { }
}

View File

@@ -1,27 +0,0 @@
namespace GamificationService.Exceptions.Services.InstructionTest;
/// <summary>
/// Represents an exception that occurs when an instruction test is not found.
/// </summary>
public class InstructionTestNotFoundException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="InstructionTestNotFoundException"/> class.
/// </summary>
public InstructionTestNotFoundException() : base() { }
/// <summary>
/// Initializes a new instance of the <see cref="InstructionTestNotFoundException"/> class with a specified error message.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public InstructionTestNotFoundException(string message) : base(message) { }
/// <summary>
/// Initializes a new instance of the <see cref="InstructionTestNotFoundException"/> class with a specified error message and a reference to the inner exception that is the cause of this exception.
/// </summary>
/// <param name="message">The error message that explains the reason for the exception.</param>
/// <param name="innerException">The exception that is the cause of the current exception, or a null reference if no inner exception is specified.</param>
public InstructionTestNotFoundException(string message, Exception innerException) : base(message, innerException) { }
}

View File

@@ -1,27 +0,0 @@
namespace GamificationService.Exceptions.Services.InstructionTest;
/// <summary>
/// Represents an exception that occurs when an instruction test submission is failed.
/// </summary>
public class InstructionTestSubmissionException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="InstructionTestSubmissionException"/> class.
/// </summary>
public InstructionTestSubmissionException() : base() { }
/// <summary>
/// Initializes a new instance of the <see cref="InstructionTestSubmissionException"/> class with a specified error message.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public InstructionTestSubmissionException(string message) : base(message) { }
/// <summary>
/// Initializes a new instance of the <see cref="InstructionTestSubmissionException"/> class with a specified error message and a reference to the inner exception that is the cause of this exception.
/// </summary>
/// <param name="message">The error message that explains the reason for the exception.</param>
/// <param name="innerException">The exception that is the cause of the current exception, or a null reference if no inner exception is specified.</param>
public InstructionTestSubmissionException(string message, Exception innerException) : base(message, innerException) { }
}

View File

@@ -8,12 +8,10 @@ using GamificationService.Logs;
using GamificationService.Mapper;
using GamificationService.Services.Cookies;
using GamificationService.Services.CurrentUsers;
using GamificationService.Services.InstructionTests;
using GamificationService.Services.JWT;
using GamificationService.Services.NotificationService;
using GamificationService.Services.Rights;
using GamificationService.Services.Roles;
using GamificationService.Services.UsersProfile;
using GamificationService.Utils;
using GamificationService.Utils.Factory;
using Microsoft.AspNetCore.Authentication.JwtBearer;
@@ -129,8 +127,6 @@ public static class BackendServicesExtensions
{
public static IServiceCollection AddBackendServices(this IServiceCollection services)
{
services.AddScoped<IUserProfileService, UserProfileService>();
services.AddScoped<IInstructionTestsService, InstructionTestsService>();
return services;
}
}

View File

@@ -46,124 +46,6 @@ public class MappingProfile : Profile
.ForMember(x => x.ProfilePicture, opt => opt.MapFrom(src => src.ProfilePicture));
#endregion
#region InstructionParagraphMapping
CreateMap<InstructionParagraph, InstructionParagraphDTO>()
.ForMember(x => x.Id, opt => opt.MapFrom(src => src.Id))
.ForMember(x => x.Text, opt => opt.MapFrom(src => src.Id))
.ForMember(x => x.InstructionId, opt => opt.MapFrom(src => src.InstructionId))
.ForMember(x => x.Order, opt => opt.MapFrom(src => src.Order))
.ForMember(x => x.ImageUrl, opt => opt.MapFrom(src => src.ImageUrl))
.ForMember(x => x.VideoUrl, opt => opt.MapFrom(src => src.VideoUrl));
#endregion
#region InstructionMapping
CreateMap<Instruction, InstructionDTO>()
.ForMember(x => x.Id, opt => opt.MapFrom(src => src.Id))
.ForMember(x => x.Title, opt => opt.MapFrom(src => src.Title))
.ForMember(x => x.Description, opt => opt.MapFrom(src => src.Description))
.ForMember(x => x.CategoryId, opt => opt.MapFrom(src => src.CategoryId))
.ForMember(x => x.AssignDate, opt => opt.MapFrom(src => src.AssignDate))
.ForMember(x => x.AssignDate, opt => opt.MapFrom(src => src.DeadlineDate))
.ForMember(x => x.IsEnabled, opt => opt.MapFrom(src => src.IsEnabled))
.ForMember(x => x.Paragraphs, opt => opt.MapFrom(src => src.Paragraphs));
#endregion
#region InstructionCreateMapping
CreateMap<InstructionCreateDTO, Instruction>()
.ForMember(x => x.Id, opt => opt.MapFrom(src => src.Id))
.ForMember(x => x.Title, opt => opt.MapFrom(src => src.Title))
.ForMember(x => x.Description, opt => opt.MapFrom(src => src.Description))
.ForMember(x => x.Paragraphs, opt => opt.MapFrom(src => src.Paragraphs))
.ForMember(x => x.CategoryId, opt => opt.MapFrom(src => src.CategoryId))
.ForMember(x => x.AssignDate, opt => opt.MapFrom(src => src.AssignDate))
.ForMember(x => x.DeadlineDate, opt => opt.MapFrom(src => src.DeadlineDate))
.ForMember(x => x.IsEnabled, opt => opt.MapFrom(src => src.IsEnabled));
#endregion
#region InstructionParagraphCreateMapping
CreateMap<InstructionParagraphCreateDTO, InstructionParagraph>()
.ForMember(x => x.Id, opt => opt.MapFrom(src => src.Id))
.ForMember(x => x.Text, opt => opt.MapFrom(src => src.Text))
.ForMember(x => x.InstructionId, opt => opt.MapFrom(src => src.InstructionId))
.ForMember(x => x.Order, opt => opt.MapFrom(src => src.Order))
.ForMember(x => x.ImageUrl, opt => opt.MapFrom(src => src.ImageUrl))
.ForMember(x => x.VideoUrl, opt => opt.MapFrom(src => src.VideoUrl));
#endregion
#region InstructionCategoryMapping
CreateMap<InstructionCategory, InstructionCategoryDTO>()
.ForMember(x => x.Id, opt => opt.MapFrom(src => src.Id))
.ForMember(x => x.Title, opt => opt.MapFrom(src => src.Title));
#endregion
#region InstructionCategoryCreateMapping
CreateMap<InstructionCategoryCreateDTO, InstructionCategory>()
.ForMember(x => x.Id, opt => opt.MapFrom(src => src.Id))
.ForMember(x => x.Title, opt => opt.MapFrom(src => src.Title));
#endregion
#region InstructionTestMapping
CreateMap<InstructionTestCreateDTO, InstructionTest>()
.ForMember(x => x.Id, opt => opt.MapFrom(src => src.Id))
.ForMember(x => x.Title, opt => opt.MapFrom(src => src.Title))
.ForMember(x => x.Questions, opt => opt.MapFrom(src => src.Questions))
.ForMember(x => x.ScoreCalcMethod, opt => opt.MapFrom(src => src.ScoreCalcMethod))
.ForMember(x => x.MinScore, opt => opt.MapFrom(src => src.MinScore))
.ForMember(x => x.MaxAttempts, opt => opt.MapFrom(src => src.MaxAttempts));
CreateMap<InstructionTest, InstructionTestDTO>()
.ForMember(x => x.Id, opt => opt.MapFrom(src => src.Id))
.ForMember(x => x.Title, opt => opt.MapFrom(src => src.Title))
.ForMember(x => x.ScoreCalcMethod, opt => opt.MapFrom(src => src.ScoreCalcMethod))
.ForMember(x => x.MinScore, opt => opt.MapFrom(src => src.MinScore))
.ForMember(x => x.MaxAttempts, opt => opt.MapFrom(src => src.MaxAttempts))
.ForMember(x => x.Questions, opt => opt.MapFrom(src => src.Questions));
#endregion
#region InstructionTestQuestionCreateMapping
CreateMap<InstructionTestQuestionCreateDTO, InstructionTestQuestion>()
.ForMember(x => x.Question, opt => opt.MapFrom(src => src.Question))
.ForMember(x => x.Answers, opt => opt.MapFrom(src => src.Answers))
.ForMember(x => x.Order, opt => opt.MapFrom(src => src.Order))
.ForMember(x => x.IsMultipleAnswer, opt => opt.MapFrom(src => src.IsMultipleAnswer))
.ForMember(x => x.CorrectAnswers, opt => opt.MapFrom(src => src.CorrectAnswers));
#endregion
#region InstructionTestQuestionMapping
CreateMap<InstructionTestQuestion, InstructionTestQuestionDTO>()
.ForMember(x => x.Id, opt => opt.MapFrom(src => src.Id))
.ForMember(x => x.Question, opt => opt.MapFrom(src => src.Question))
.ForMember(x => x.Answers, opt => opt.MapFrom(src => src.Answers))
.ForMember(x => x.Order, opt => opt.MapFrom(src => src.Order))
.ForMember(x => x.IsMultipleAnswer, opt => opt.MapFrom(src => src.IsMultipleAnswer));
#endregion
#region InstructionTestResultMapping
CreateMap<InstructionTestResult, InstructionTestResultDTO>()
.ForMember(x => x.Id, opt => opt.MapFrom(src => src.Id))
.ForMember(x => x.UserId, opt => opt.MapFrom(src => src.UserId))
.ForMember(x => x.InstructionTestId, opt => opt.MapFrom(src => src.InstructionTestId))
.ForMember(x => x.Score, opt => opt.MapFrom(src => src.Score));
#endregion
}
}

View File

@@ -1,14 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace GamificationService.Models.DTO;
public class InstructionCategoryCreateDTO
{
public long? Id { get; set; }
[Required(ErrorMessage = "Title is required")]
public string Title { get; set; } = null!;
}

View File

@@ -1,10 +0,0 @@
namespace GamificationService.Models.DTO;
public class InstructionCategoryDTO
{
public long? Id { get; set; }
public string? Title { get; set; } = null!;
}

View File

@@ -1,38 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace GamificationService.Models.DTO;
public class InstructionCreateDTO
{
public long? Id { get; set; }
[Required(ErrorMessage = "Title is required")]
public string Title { get; set; } = null!;
public string? Description { get; set; }
[Required(ErrorMessage = "Paragraphs are required")]
public List<InstructionParagraphCreateDTO> Paragraphs { get; set; } = null!;
[Required(ErrorMessage = "Category id is required")]
public long CategoryId { get; set; }
/// <summary>
/// If AssignDate is set, the instruction will be automatically enabled
/// when the date is reached. If it's not set, the test will automatically
/// obtain the current date as its AssignDate as soon as the instruction
/// will be enabled by the IsEnabled parameter.
/// </summary>
public DateTime? AssignDate { get; set; }
/// <summary>
/// When deadline is reached, no more submissions are allowed for this instruction.
/// </summary>
public DateTime? DeadlineDate { get; set; }
/// <summary>
/// Disabled instructions cannot be seen by users.
/// Tests for such instructions cannot be submitted either.
/// </summary>
public bool IsEnabled { get; set; } = false;
}

View File

@@ -1,21 +0,0 @@
namespace GamificationService.Models.DTO;
public class InstructionDTO
{
public long? Id { get; set; }
public string? Title { get; set; }
public string? Description { get; set; }
public List<InstructionParagraphDTO> Paragraphs { get; set; } = new List<InstructionParagraphDTO>();
public long? CategoryId { get; set; }
public DateTime? AssignDate { get; set; }
public DateTime? DeadlineDate { get; set; }
public bool IsEnabled { get; set; }
}

View File

@@ -1,25 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace GamificationService.Models.DTO;
public class InstructionParagraphCreateDTO
{
public long? Id { get; set; }
public long? InstructionId { get; set; }
/// <summary>
/// Order defines the order of the paragraphs inside the instruction.
/// There must not be two paragraphs with the same order.
/// </summary>
public int Order { get; set; }
[Required(ErrorMessage = "Text is required")]
public string Text { get; set; } = null!;
public string? ImageUrl { get; set; }
public string? VideoUrl { get; set; }
}

View File

@@ -1,21 +0,0 @@
namespace GamificationService.Models.DTO;
public class InstructionParagraphDTO
{
public long? Id { get; set; }
public long? InstructionId { get; set; }
/// <summary>
/// Order defines the order of the paragraphs inside the instruction.
/// There must not be two paragraphs with the same order.
/// </summary>
public int Order { get; set; }
public string? Text { get; set; } = null!;
public string? ImageUrl { get; set; }
public string? VideoUrl { get; set; }
}

View File

@@ -1,20 +0,0 @@
using System.ComponentModel.DataAnnotations;
using GamificationService.Utils.Enums;
namespace GamificationService.Models.DTO;
public class InstructionTestCreateDTO
{
public long? Id { get; set; }
public string? Title { get; set; }
[Required(ErrorMessage = "Questions must be specified")]
public ICollection<InstructionTestQuestionCreateDTO> Questions { get; set; } = null!;
public int MaxAttempts { get; set; } = 10;
[Range(0, 1.0, ErrorMessage = "Minimum score must be between 0.6 and 1.0")]
public double MinScore { get; set; } = 0.6;
public InstructionTestScoreCalcMethod ScoreCalcMethod { get; set; } = InstructionTestScoreCalcMethod.MaxGrade;
}

View File

@@ -1,23 +0,0 @@
using System.ComponentModel.DataAnnotations;
using GamificationService.Utils.Enums;
namespace GamificationService.Models.DTO;
public class InstructionTestDTO
{
public long? Id { get; set; }
// Reserved just in case
[StringLength(300, ErrorMessage = "Title cannot be longer than 300 characters")]
public string? Title { get; set; }
public int MaxAttempts { get; set; } = 10;
public double MinScore { get; set; } = 0.6;
public List<InstructionTestQuestionDTO> Questions { get; set; } = new List<InstructionTestQuestionDTO>();
public InstructionTestScoreCalcMethod ScoreCalcMethod { get; set; } = InstructionTestScoreCalcMethod.MaxGrade;
}

View File

@@ -1,26 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace GamificationService.Models.DTO;
public class InstructionTestQuestionCreateDTO
{
public long? Id { get; set; }
public bool IsMultipleAnswer { get; set; }
/// <summary>
/// Question will be displayed in the paragraph with the same order number.
/// There can be multiple questions attached to the same paragraph.
/// </summary>
public int Order { get; set; }
[Required(ErrorMessage = "Must have question text")]
public string Question { get; set; } = null!;
[Required(ErrorMessage = "Must have answer options")]
public ICollection<string> Answers { get; set; } = null!;
[Required(ErrorMessage = "Must have correct answers")]
public ICollection<int> CorrectAnswers { get; set; } = null!;
}

View File

@@ -1,25 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace GamificationService.Models.DTO;
public class InstructionTestQuestionDTO
{
public long? Id { get; set; }
public bool IsMultipleAnswer { get; set; }
/// <summary>
/// Question will be displayed in the paragraph with the same order number.
/// There can be multiple questions attached to the same paragraph.
/// </summary>
public int Order { get; set; }
[Required(ErrorMessage = "Must have question text")]
public string Question { get; set; } = null!;
[Required(ErrorMessage = "Must have answer options")]
public ICollection<string> Answers { get; set; } = null!;
public ICollection<int>? CorrectAnswers { get; set; }
}

View File

@@ -1,12 +0,0 @@
namespace GamificationService.Models.DTO;
public class InstructionTestResultDTO
{
public long? Id { get; set; }
public long? InstructionTestId { get; set; }
public long? UserId { get; set; }
public int Score { get; set; }
}

View File

@@ -1,12 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace GamificationService.Models.DTO;
public class InstructionTestSubmissionDTO
{
[Required(ErrorMessage = "InstructionTestId is required")]
public int InstructionTestId { get; set; }
[Required(ErrorMessage = "Answers must be provided")]
public List<List<int>> Answers { get; set; } = null!;
}

View File

@@ -1,29 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace GamificationService.Models.Database;
public class Instruction
{
[Key]
public long Id { get; set; }
[Required(ErrorMessage = "Title is required")]
public string Title { get; set; } = null!;
public string? Description { get; set; }
public long CategoryId { get; set; }
[Required(ErrorMessage = "Category must be specified")]
public InstructionCategory Category { get; set; } = null!;
public virtual ICollection<InstructionParagraph> Paragraphs { get; set; }
public long? InstructionTestId { get; set; }
public InstructionTest? InstructionTest { get; set; }
public DateTime? AssignDate { get; set; } = DateTime.UtcNow;
public DateTime? DeadlineDate { get; set; }
public bool IsEnabled { get; set; }
}

View File

@@ -1,13 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace GamificationService.Models.Database;
public class InstructionCategory
{
[Key]
public long Id { get; set; }
[Required(ErrorMessage = "Title is required")]
public string Title { get; set; } = null!;
}

View File

@@ -1,21 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace GamificationService.Models.Database;
public class InstructionParagraph
{
[Key]
public long Id { get; set; }
public long InstructionId { get; set; }
[Required(ErrorMessage = "Must be linked to instruction")]
public int Order { get; set; }
[Required(ErrorMessage = "Paragraph text is required")]
public string Text { get; set; } = null!;
public string? ImageUrl { get; set; }
public string? VideoUrl { get; set; }
}

View File

@@ -1,23 +0,0 @@
using System.ComponentModel.DataAnnotations;
using GamificationService.Utils.Enums;
namespace GamificationService.Models.Database;
public class InstructionTest
{
[Key]
public long Id { get; set; }
// Reserved just in case
[MaxLength(300, ErrorMessage = "Title cannot be longer than 300 characters")]
public string? Title { get; set; }
public virtual ICollection<InstructionTestQuestion> Questions { get; set; }
public int MaxAttempts { get; set; } = 10;
public double MinScore { get; set; } = 0.6;
public InstructionTestScoreCalcMethod ScoreCalcMethod { get; set; } = InstructionTestScoreCalcMethod.MaxGrade;
}

View File

@@ -1,26 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace GamificationService.Models.Database;
public class InstructionTestQuestion
{
[Key]
public long Id { get; set; }
[Required(ErrorMessage = "Must be tied to an instruction test")]
public InstructionTest InstructionTest { get; set; } = null!;
public long InstructionTestId { get; set; }
public int Order { get; set; }
public bool IsMultipleAnswer { get; set; }
[Required(ErrorMessage = "Must have question text")]
public string Question { get; set; } = null!;
[Required(ErrorMessage = "Must have answer options")]
public ICollection<string> Answers { get; set; } = null!;
[Required(ErrorMessage = "Must have correct answer ids")]
public ICollection<int> CorrectAnswers { get; set; } = null!;
}

View File

@@ -1,20 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace GamificationService.Models.Database;
public class InstructionTestResult
{
[Key]
public long Id { get; set; }
public long InstructionTestId { get; set; }
[Required(ErrorMessage = "Instruction test is required")]
public virtual InstructionTest InstructionTest { get; set; } = null!;
public long UserId { get; set; }
[Required(ErrorMessage = "User is required")]
public ApplicationUser User { get; set; } = null!;
[Range(0, 100, ErrorMessage = "Score must be a number from 0 to 100")]
public int Score { get; set; }
}

View File

@@ -1,6 +0,0 @@
namespace GamificationService.Models.Messages.InstructionTests;
public class CreateInstructionTestRequest
{
}

View File

@@ -1,6 +0,0 @@
namespace GamificationService.Models.Messages.InstructionTests;
public class CreateInstructionTestResponse
{
}

View File

@@ -1,6 +0,0 @@
namespace GamificationService.Models.Messages.InstructionTests;
public class UpdateInstructionTestRequest
{
}

View File

@@ -1,6 +0,0 @@
namespace GamificationService.Models.Messages.InstructionTests;
public class UpdateInstructionTestResponse
{
}

View File

@@ -1,37 +0,0 @@
using System.ComponentModel.DataAnnotations;
using GamificationService.Models.DTO;
namespace GamificationService.Models.Messages.Instructions;
public class CreateInstructionRequest
{
[Required(ErrorMessage = "Title is required")]
public string Title { get; set; } = null!;
public string? Description { get; set; }
[Required(ErrorMessage = "Paragraphs are required")]
public List<InstructionParagraphCreateDTO> Paragraphs { get; set; } = null!;
[Required(ErrorMessage = "Category id is required")]
public long CategoryId { get; set; }
/// <summary>
/// If AssignDate is set, the instruction will be automatically enabled
/// when the date is reached. If it's not set, the test will automatically
/// obtain the current date as its AssignDate as soon as the instruction
/// will be enabled by the IsEnabled parameter.
/// </summary>
public DateTime? AssignDate { get; set; }
/// <summary>
/// When deadline is reached, no more submissions are allowed for this instruction.
/// </summary>
public DateTime? DeadlineDate { get; set; }
/// <summary>
/// Disabled instructions cannot be seen by users.
/// Tests for such instructions cannot be submitted either.
/// </summary>
public bool IsEnabled { get; set; } = false;
}

View File

@@ -1,6 +0,0 @@
namespace GamificationService.Models.Messages.Instructions;
public class CreateInstructionResponse
{
}

View File

@@ -1,40 +0,0 @@
using System.ComponentModel.DataAnnotations;
using GamificationService.Models.DTO;
namespace GamificationService.Models.Messages.Instructions;
public class UpdateInstructionRequest
{
public long Id { get; set; }
[Required(ErrorMessage = "Title is required")]
public string Title { get; set; } = null!;
public string? Description { get; set; }
[Required(ErrorMessage = "Paragraphs are required")]
public List<InstructionParagraphCreateDTO> Paragraphs { get; set; } = null!;
[Required(ErrorMessage = "Category id is required")]
public long CategoryId { get; set; }
/// <summary>
/// If AssignDate is set, the instruction will be automatically enabled
/// when the date is reached. If it's not set, the test will automatically
/// obtain the current date as its AssignDate as soon as the instruction
/// will be enabled by the IsEnabled parameter.
/// </summary>
public DateTime? AssignDate { get; set; }
/// <summary>
/// When deadline is reached, no more submissions are allowed for this instruction.
/// </summary>
public DateTime? DeadlineDate { get; set; }
/// <summary>
/// Disabled instructions cannot be seen by users.
/// Tests for such instructions cannot be submitted either.
/// </summary>
public bool IsEnabled { get; set; } = false;
}

View File

@@ -1,6 +0,0 @@
namespace GamificationService.Models.Messages.Instructions;
public class UpdateInstructionResponse
{
}

View File

@@ -29,12 +29,6 @@ builder.Services.AddLogging();
#endregion
#region SMTP
builder.Services.AddEmail(builder.Configuration);
#endregion
#region Database
builder.Services.AddDatabase(builder.Configuration);

View File

@@ -1,22 +0,0 @@
using GamificationService.Models.Database;
using GamificationService.Models.DTO;
namespace GamificationService.Services.InstructionTests;
public interface IInstructionTestsService
{
public Task<InstructionTest> CreateInstructionTest(InstructionTest instructionTest);
public Task<InstructionTestDTO> CreateInstructionTest(InstructionTestCreateDTO instructionTest);
public Task<bool> UpdateInstructionTest(InstructionTest instructionTest);
public Task<bool> UpdateInstructionTest(InstructionTestCreateDTO instructionTest);
public Task<bool> DeleteInstructionTestByIdAsync(long id);
public Task<InstructionTestResultDTO> SubmitInstructionTestAsync(long userId, InstructionTestSubmissionDTO submission);
public InstructionTestDTO GetInstructionTestById(long id);
public List<InstructionTestDTO> GetInstructionTestsByInstructionId(long instructionId);
public List<InstructionTestQuestionDTO> GetInstructionTestQuestionsByInstructionTestId(long instructionTestId);
public List<InstructionTestResultDTO> GetUserInstructionTestResultsByInstructionTestId(long userId, long instructionId);
public List<InstructionTestResultDTO> GetInstructionTestResultsByUserId(long userId);
public List<InstructionTestResultDTO> GetCompletedInstructionTestsByUserId(long userId);
}

View File

@@ -1,294 +0,0 @@
using AutoMapper;
using GamificationService.Database.Repositories;
using GamificationService.Exceptions.Services.Instruction;
using GamificationService.Exceptions.Services.InstructionTest;
using GamificationService.Models.Database;
using GamificationService.Models.DTO;
using GamificationService.Utils.Enums;
namespace GamificationService.Services.InstructionTests;
public class InstructionTestsService : IInstructionTestsService
{
private readonly ILogger<InstructionTestsService> _logger;
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public InstructionTestsService(ILogger<InstructionTestsService> logger, UnitOfWork unitOfWork, IMapper mapper)
{
_logger = logger;
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<InstructionTestDTO> CreateInstructionTest(InstructionTestCreateDTO instructionTest)
{
return _mapper.Map<InstructionTestDTO>(await CreateInstructionTest(_mapper.Map<InstructionTest>(instructionTest)));
}
public async Task<bool> DeleteInstructionTestByIdAsync(long id)
{
var instructionTest = _unitOfWork.InstructionTestRepository.GetByID(id);
if (instructionTest == null)
{
_logger.LogError("Instruction test with id {Id} not found", id);
throw new InstructionTestNotFoundException();
}
// Find all questions
var questions = _unitOfWork.InstructionTestQuestionRepository.Get(q => q.InstructionTestId == id);
// Start transaction
await _unitOfWork.BeginTransactionAsync();
foreach (var question in questions)
{
_unitOfWork.InstructionTestQuestionRepository.Delete(question);
}
// Delete instruction test
_unitOfWork.InstructionTestRepository.Delete(instructionTest);
if (await _unitOfWork.SaveAsync())
{
await _unitOfWork.CommitAsync();
_logger.LogInformation("Instruction test deleted ({Id})", id);
return true;
}
else
{
_logger.LogError("Failed to delete instruction test ({Id})", id);
throw new InstructionTestDeletionException();
}
}
public List<InstructionTestResultDTO> GetCompletedInstructionTestsByUserId(long userId)
{
var userTestAttempts = _unitOfWork.InstructionTestResultRepository.Get(
q => q.UserId == userId).ToList();
var userInstructionTests = _unitOfWork.InstructionTestRepository.Get(
q => userTestAttempts.Any(a => a.InstructionTestId == q.Id)).ToList();
var conclusiveUserTestResults = new List<InstructionTestResultDTO>();
foreach (var instructionTest in userInstructionTests)
{
var scoreCalcMethod = instructionTest.ScoreCalcMethod;
int maxScore = 0;
if (scoreCalcMethod == InstructionTestScoreCalcMethod.AverageGrade)
{
maxScore = (int)Math.Round(userTestAttempts.Where(q => q.InstructionTestId == instructionTest.Id).Average(q => q.Score));
}
else
{
maxScore = userTestAttempts.Where(q => q.InstructionTestId == instructionTest.Id).Max(q => q.Score);
}
if (maxScore >= instructionTest.MinScore)
{
conclusiveUserTestResults.Add(_mapper.Map<InstructionTestResultDTO>(userTestAttempts.First(q => q.InstructionTestId == instructionTest.Id)));
}
}
return conclusiveUserTestResults;
}
public InstructionTestDTO GetInstructionTestById(long id)
{
var instructionTest = _unitOfWork.InstructionTestRepository.GetByID(id);
if (instructionTest == null)
{
_logger.LogError("Instruction test with id {Id} not found", id);
throw new InstructionTestNotFoundException();
}
return _mapper.Map<InstructionTestDTO>(instructionTest);
}
public List<InstructionTestQuestionDTO> GetInstructionTestQuestionsByInstructionTestId(long instructionTestId)
{
var questions = _unitOfWork.InstructionTestQuestionRepository.Get(q => q.InstructionTestId == instructionTestId);
return _mapper.Map<List<InstructionTestQuestionDTO>>(questions);
}
public List<InstructionTestResultDTO> GetUserInstructionTestResultsByInstructionTestId(long userId, long instructionTestId)
{
var userTestResults = _unitOfWork.InstructionTestResultRepository.Get(
q => q.UserId == userId && q.InstructionTestId == instructionTestId).ToList();
return _mapper.Map<List<InstructionTestResultDTO>>(userTestResults);
}
public List<InstructionTestResultDTO> GetInstructionTestResultsByUserId(long userId)
{
var userTestResults = _unitOfWork.InstructionTestResultRepository.Get(
q => q.UserId == userId).ToList();
return _mapper.Map<List<InstructionTestResultDTO>>(userTestResults);
}
public List<InstructionTestDTO> GetInstructionTestsByInstructionId(long instructionId)
{
var instructionTest = (_unitOfWork.InstructionRepository.GetByID(instructionId)
?? throw new InstructionNotFoundException())
.InstructionTest ?? throw new InstructionTestNotFoundException();
return _mapper.Map<List<InstructionTestDTO>>(new List<InstructionTest>() {instructionTest});
}
public async Task<InstructionTestResultDTO> SubmitInstructionTestAsync(long userId, InstructionTestSubmissionDTO submission)
{
// Retrieve the test and questions
var instructionTest = _unitOfWork.InstructionTestRepository.GetByID(submission.InstructionTestId);
if (instructionTest == null)
{
_logger.LogError("Instruction test with id {Id} not found", submission.InstructionTestId);
throw new InstructionTestNotFoundException();
}
// Check remaining attempts
var userTestAttempts = _unitOfWork.InstructionTestResultRepository.Get(
q => q.UserId == userId && q.InstructionTestId == submission.InstructionTestId).ToList();
if (userTestAttempts.Count >= instructionTest.MaxAttempts)
{
_logger.LogWarning("User {UserId}: denied submission for test {InstructionTestId}: max attempts reached", userId, submission.InstructionTestId);
throw new InstructionTestSubmissionException();
}
var questions = _unitOfWork.InstructionTestQuestionRepository.Get(q => q.InstructionTestId == submission.InstructionTestId).ToList();
// Verify answers amount
if (questions.Count != submission.Answers.Count)
{
_logger.LogWarning("User {UserId}: denied submission for test {InstructionTestId}: wrong number of answers", userId, submission.InstructionTestId);
throw new InstructionTestSubmissionException();
}
// Evaluate answers
double score = 0;
int maxErrorPerQuestion = 1;
for (int i = 0; i < questions.Count; i++)
{
var question = questions[i];
// User answers for the question without duplicate options
var answer = submission.Answers[i].Distinct();
if (question.IsMultipleAnswer)
{
int correctUserAnswersCount = 0;
int incorrectUserAnswersCount = 0;
int correctAnswersCount = question.CorrectAnswers.Count;
foreach (var option in answer)
{
if (question.CorrectAnswers.Contains(option))
{
correctUserAnswersCount++;
}
else
{
incorrectUserAnswersCount++;
}
}
if (incorrectUserAnswersCount > maxErrorPerQuestion || correctUserAnswersCount == 0)
{
// Nothing scored for the question
continue;
}
// One question is worth 1 point max
double questionScore = correctUserAnswersCount / (double)correctAnswersCount;
// Add the question score, or half of it if an error is present
score += incorrectUserAnswersCount > 0 ? questionScore /= 2 : questionScore;
}
else
{
score += question.CorrectAnswers.Contains(answer.First()) ? 1 : 0;
}
}
score = Math.Round(score / questions.Count)*100;
// Add test result
await _unitOfWork.BeginTransactionAsync();
InstructionTestResult newTestResult = new InstructionTestResult()
{
UserId = userId,
InstructionTestId = submission.InstructionTestId,
Score = (int)score
};
_unitOfWork.InstructionTestResultRepository.Insert(newTestResult);
if (!await _unitOfWork.SaveAsync())
{
_logger.LogError("Failed to save test result for user {UserId} and test {InstructionTestId}", userId, submission.InstructionTestId);
throw new InstructionTestSubmissionException();
}
await _unitOfWork.CommitAsync();
return _mapper.Map<InstructionTestResultDTO>(newTestResult);
}
public async Task<bool> UpdateInstructionTest(InstructionTestCreateDTO instructionTest)
{
return await UpdateInstructionTest(_mapper.Map<InstructionTest>(instructionTest));
}
public async Task<InstructionTest> CreateInstructionTest(InstructionTest instructionTest)
{
instructionTest.Id = 0;
await _unitOfWork.BeginTransactionAsync();
await _unitOfWork.InstructionTestRepository.InsertAsync(instructionTest);
if (await _unitOfWork.SaveAsync() == false)
{
_logger.LogError("Failure to create instruction test");
throw new InstructionTestCreationException();
}
await _unitOfWork.CommitAsync();
_logger.LogInformation($"Created instruction test {instructionTest.Id}");
return instructionTest;
}
public async Task<bool> UpdateInstructionTest(InstructionTest instructionTest)
{
var existingInstructionTest = _unitOfWork.InstructionTestRepository.GetByID(instructionTest.Id);
if (existingInstructionTest == null)
{
throw new InstructionTestNotFoundException();
}
await _unitOfWork.BeginTransactionAsync();
_unitOfWork.InstructionTestQuestionRepository.DeleteRange(existingInstructionTest.Questions);
if (await _unitOfWork.SaveAsync() == false)
{
_logger.LogError($"Failure to create existing questions for instruction test {instructionTest.Id} during update");
throw new InstructionTestCreationException();
}
_unitOfWork.InstructionTestRepository.Update(instructionTest);
if (await _unitOfWork.SaveAsync() == false)
{
_logger.LogError($"Failure to update instruction test {instructionTest.Id}");
}
await _unitOfWork.CommitAsync();
return true;
}
}

View File

@@ -1,21 +0,0 @@
using GamificationService.Models.Database;
using GamificationService.Models.DTO;
using GamificationService.Models.Messages.Instructions;
namespace GamificationService.Services.Instructions;
public interface IInstructionService
{
public Task<Instruction> CreateInstruction(Instruction model);
public Task<bool> UpdateInstructionById(Instruction model);
public Task<bool> DeleteInstructionById(long instructionId);
public Task<InstructionDTO> CreateInstruction(CreateInstructionRequest model);
public Task<bool> UpdateInstructionById(UpdateInstructionRequest model);
public List<InstructionDTO> GetAllInstructions(long userId);
public List<InstructionDTO> GetInstructionsByCategoryId(long userId, long categoryId);
public List<InstructionDTO> GetCompletedInstructions(long userId);
public List<InstructionDTO> GetUnfinishedInstructions(long userId);
public List<InstructionDTO> GetInstructionById(long userId, long instructionId);
}

View File

@@ -1,174 +0,0 @@
using AutoMapper;
using GamificationService.Database.Repositories;
using GamificationService.Exceptions.Services.Instruction;
using GamificationService.Models.Database;
using GamificationService.Models.DTO;
using GamificationService.Models.Messages.Instructions;
using GamificationService.Services.InstructionTests;
namespace GamificationService.Services.Instructions;
public class InstructionService : IInstructionService
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
private readonly ILogger<InstructionService> _logger;
private readonly IInstructionTestsService _instructionTestService;
public InstructionService(UnitOfWork unitOfWork, IMapper mapper, ILogger<InstructionService> logger, IInstructionTestsService instructionTestService)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
_logger = logger;
_instructionTestService = instructionTestService;
}
public async Task<Instruction> CreateInstruction(Instruction model)
{
model.Id = 0;
await _unitOfWork.BeginTransactionAsync();
await _unitOfWork.InstructionRepository.InsertAsync(model);
if(await _unitOfWork.SaveAsync() == false)
{
_logger.LogError($"Instruction {model.Id} could not be created");
throw new InstructionCreationException();
}
await _unitOfWork.CommitAsync();
_logger.LogInformation($"Instruction {model.Id} created");
return model;
}
public async Task<InstructionDTO> CreateInstruction(CreateInstructionRequest model)
{
Instruction newInstruction = new Instruction()
{
Title = model.Title,
Description = model.Description,
CategoryId = model.CategoryId,
Paragraphs = _mapper.Map<List<InstructionParagraph>>(model.Paragraphs),
AssignDate = model.AssignDate,
DeadlineDate = model.DeadlineDate,
IsEnabled = model.IsEnabled
};
return _mapper.Map<InstructionDTO>(await CreateInstruction(newInstruction));
}
public async Task<bool> DeleteInstructionById(long instructionId)
{
Instruction? instruction = _unitOfWork.InstructionRepository.GetByID(instructionId);
if (instruction == null)
{
throw new InstructionNotFoundException();
}
_unitOfWork.InstructionRepository.Delete(instruction);
if (await _unitOfWork.SaveAsync() == false)
{
_logger.LogError($"Instruction {instructionId} could not be deleted");
throw new InstructionDeletionException();
}
_logger.LogInformation($"Instruction {instructionId} deleted");
return true;
}
public List<InstructionDTO> GetAllInstructions(long userId)
{
// TODO: select accessible only
var instructions = _unitOfWork.InstructionRepository.Get().ToList();
return _mapper.Map<List<InstructionDTO>>(instructions);
}
public List<InstructionDTO> GetCompletedInstructions(long userId)
{
var completedTests = _instructionTestService.GetCompletedInstructionTestsByUserId(userId);
var instructions = _unitOfWork.InstructionRepository.Get()
.Where(i => completedTests.Any(t => i.InstructionTestId == t.Id));
return _mapper.Map<List<InstructionDTO>>(instructions);
}
public List<InstructionDTO> GetInstructionById(long userId, long instructionId)
{
// TODO: select accessible only
var instruction = _unitOfWork.InstructionRepository.GetByID(instructionId);
return _mapper.Map<List<InstructionDTO>>(instruction);
}
public List<InstructionDTO> GetInstructionsByCategoryId(long userId, long categoryId)
{
var category = _unitOfWork.InstructionCategoryRepository.GetByID(categoryId);
if (category == null)
{
throw new CategoryNotFoundException();
}
var instructions = _unitOfWork.InstructionRepository.Get().Where(i => i.CategoryId == categoryId);
return _mapper.Map<List<InstructionDTO>>(instructions);
}
public List<InstructionDTO> GetUnfinishedInstructions(long userId)
{
// TODO: only show accessible
var completedTests = _instructionTestService.GetCompletedInstructionTestsByUserId(userId);
var instructions = _unitOfWork.InstructionRepository.Get()
.Where(i => !completedTests.Any(t => i.InstructionTestId == t.Id));
return _mapper.Map<List<InstructionDTO>>(instructions);
}
public async Task<bool> UpdateInstructionById(Instruction model)
{
var existingInstruction = _unitOfWork.InstructionRepository.GetByID(model.Id);
if (existingInstruction == null)
{
throw new InstructionNotFoundException();
}
await _unitOfWork.BeginTransactionAsync();
_unitOfWork.InstructionParagraphRepository.DeleteRange(existingInstruction.Paragraphs);
if (await _unitOfWork.SaveAsync() == false)
{
_logger.LogError($"Failure to delete older paragraphs for instruction {model.Id} during update");
throw new InstructionUpdateException();
}
_unitOfWork.InstructionRepository.Update(model);
if (await _unitOfWork.SaveAsync() == false)
{
_logger.LogError($"Instruction {model.Id} could not be updated");
throw new InstructionUpdateException();
}
_logger.LogInformation($"Instruction {model.Id} updated");
return true;
}
public async Task<bool> UpdateInstructionById(UpdateInstructionRequest model)
{
Instruction? existingInstruction = _unitOfWork.InstructionRepository.GetByID(model.Id);
if (existingInstruction == null)
{
throw new InstructionNotFoundException();
}
existingInstruction.Title = model.Title;
existingInstruction.Description = model.Description;
existingInstruction.CategoryId = model.CategoryId;
existingInstruction.Paragraphs = _mapper.Map<List<InstructionParagraph>>(model.Paragraphs);
existingInstruction.AssignDate = model.AssignDate;
existingInstruction.DeadlineDate = model.DeadlineDate;
existingInstruction.IsEnabled = model.IsEnabled;
return await UpdateInstructionById(_mapper.Map<Instruction>(model));
}
}