Merge branch 'enhanced_categories'

This commit is contained in:
2025-07-18 11:14:22 +02:00
8 changed files with 403 additions and 2 deletions

View File

@@ -0,0 +1,111 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using QuotifyBE.Data;
using QuotifyBE.Entities;
using QuotifyBE.DTOs;
using System.Threading.Tasks;
using QuotifyBE.Mapping;
using Microsoft.AspNetCore.Cors;
using Microsoft.EntityFrameworkCore;
namespace QuotifyBE.Controllers;
[ApiController]
[EnableCors]
[Route("api/v1/categories")]
[Produces("application/json")]
public class CategoryController : ControllerBase
{
private readonly ApplicationDbContext _db;
private readonly GeneralUseHelpers guhf;
public CategoryController(ApplicationDbContext db, GeneralUseHelpers GUHF)
{
_db = db;
guhf = GUHF;
}
// GET /api/v1/categories
/// <summary>
/// Get every category
/// </summary>
/// <remarks>
/// Can (and will) return an empty list if no categories are found in DB. <br/>
/// Has CORS set.
/// </remarks>
/// <response code="200">Returned on valid request</response>
// /// <response code="404">Returned when there are no categories to list</response>
[HttpGet]
[EnableCors]
[ProducesResponseType(typeof(CategoryShortDTO), 200)]
// [ProducesResponseType(typeof(ErrorDTO), 404)]
public async Task<IActionResult> GetQuotePage()
{
// The following seems to be a bad idea, so I leave it as is. ~eee4
//
// int totalCategories = await _db.Categories.CountAsync();
//
// if (totalCategories <= 0)
// {
// return NotFound(new ErrorDTO { Status = "error", Error_msg = "No categories to list" });
// }
// Get all the categories
List<Category> categories = await _db.Categories
.ToListAsync();
// Convert them to a list of DTO
List<CategoryShortDTO> result = categories
.Select(c => c.ToCategoryShortDTO())
.ToList();
// Return to user
return Ok(result);
}
// POST /api/v1/categories
/// <summary>
/// [AUTHED] Create a new category
/// </summary>
/// <remarks>
/// Allows authorized users to create categories. <br/>
/// Important! Category names are case insensitive. <br/>
/// Has CORS set.
/// </remarks>
/// <response code="200">Returned on valid request</response>
/// <response code="406">Returned when such category already exists (case insensitive)</response>
[HttpPost]
[Authorize]
[EnableCors]
[ProducesResponseType(typeof(CategoryShortDTO), 200)]
[ProducesResponseType(typeof(ErrorDTO), 406)]
public async Task<IActionResult> PostNewCategory([FromBody] NewCategoryDTO formCategory)
{
// Check if such category doesn't already exist
Category? cat = await _db.Categories.FirstOrDefaultAsync(c => c.Name.ToLower() == formCategory.Name.ToLower());
if (cat != null)
{
return StatusCode(406, new ErrorDTO { Status = "error", Error_msg = "This category already exists" });
}
// Create new category
cat = new Category
{
Name = formCategory.Name,
Description = formCategory.Description,
CreatedAt = DateTime.UtcNow
};
// Add to DB
await _db.Categories.AddAsync(cat);
await _db.SaveChangesAsync();
// And send back to the user as DTO
return Ok(cat.ToCategoryShortDTO());
}
}

9
DTOs/CategoryShortDTO.cs Normal file
View File

@@ -0,0 +1,9 @@
namespace QuotifyBE.DTOs;
public record class CategoryShortDTO
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public DateTime? CreatedAt { get; set; } = DateTime.UtcNow;
};

6
DTOs/NewCategoryDTO.cs Normal file
View File

@@ -0,0 +1,6 @@
namespace QuotifyBE.DTOs;
public class NewCategoryDTO
{
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
}

View File

@@ -1,8 +1,10 @@
namespace QuotifyBE.Entities
namespace QuotifyBE.Entities
{
public class Category
{
public int Id { get; set; }
public string? Name { get; set; }
required public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public DateTime? CreatedAt { get; set; } = DateTime.UtcNow;
}
}

View File

@@ -0,0 +1,19 @@
using QuotifyBE.DTOs;
using QuotifyBE.Entities;
namespace QuotifyBE.Mapping;
public static class CategoryMapping
{
public static CategoryShortDTO ToCategoryShortDTO(this Category category)
{
return new CategoryShortDTO
{
Id = category.Id,
Name = category.Name,
Description = category.Description,
CreatedAt = category.CreatedAt
};
}
}

View File

@@ -0,0 +1,190 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using QuotifyBE.Data;
#nullable disable
namespace QuotifyBE.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20250718084441_more_category_data")]
partial class more_category_data
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("QuotifyBE.Entities.Category", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime?>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Categories");
});
modelBuilder.Entity("QuotifyBE.Entities.Image", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Url")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Images");
});
modelBuilder.Entity("QuotifyBE.Entities.Quote", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Author")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int?>("ImageId")
.HasColumnType("integer");
b.Property<DateTime>("LastUpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Text")
.IsRequired()
.HasColumnType("text");
b.Property<int>("UserId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("ImageId");
b.HasIndex("UserId");
b.ToTable("Quotes");
});
modelBuilder.Entity("QuotifyBE.Entities.QuoteCategory", b =>
{
b.Property<int>("QuoteId")
.HasColumnType("integer");
b.Property<int>("CategoryId")
.HasColumnType("integer");
b.HasKey("QuoteId", "CategoryId");
b.HasIndex("CategoryId");
b.ToTable("QuoteCategories");
});
modelBuilder.Entity("QuotifyBE.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("PasswordHash")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Role")
.HasColumnType("integer");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("QuotifyBE.Entities.Quote", b =>
{
b.HasOne("QuotifyBE.Entities.Image", "Image")
.WithMany()
.HasForeignKey("ImageId");
b.HasOne("QuotifyBE.Entities.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Image");
b.Navigation("User");
});
modelBuilder.Entity("QuotifyBE.Entities.QuoteCategory", b =>
{
b.HasOne("QuotifyBE.Entities.Category", "Category")
.WithMany()
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("QuotifyBE.Entities.Quote", "Quote")
.WithMany("QuoteCategories")
.HasForeignKey("QuoteId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Category");
b.Navigation("Quote");
});
modelBuilder.Entity("QuotifyBE.Entities.Quote", b =>
{
b.Navigation("QuoteCategories");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,57 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace QuotifyBE.Migrations
{
/// <inheritdoc />
public partial class more_category_data : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "Categories",
type: "text",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
migrationBuilder.AddColumn<DateTime>(
name: "CreatedAt",
table: "Categories",
type: "timestamp with time zone",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Description",
table: "Categories",
type: "text",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "CreatedAt",
table: "Categories");
migrationBuilder.DropColumn(
name: "Description",
table: "Categories");
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "Categories",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "text");
}
}
}

View File

@@ -30,7 +30,14 @@ namespace QuotifyBE.Migrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime?>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");