Added Categories

This commit is contained in:
Mia Rose Winter 2024-01-26 13:05:48 +01:00
parent da4e19665a
commit c53ecd2ada
Signed by: miawinter
GPG key ID: 4B6F6A83178F595E
8 changed files with 737 additions and 4 deletions

View file

@ -51,6 +51,25 @@ public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options
.HasConversion(dateTimeOffsetUtcConverter); .HasConversion(dateTimeOffsetUtcConverter);
article.HasQueryFilter(a => !a.IsDeleted); article.HasQueryFilter(a => !a.IsDeleted);
article.ToTable("Articles");
});
builder.Entity<Category>(category => {
category.HasKey(c => c.Id);
category.Property(c => c.Name).IsRequired().HasMaxLength(128);
category.Property(c => c.Color).IsRequired().HasDefaultValue(CategoryColors.Default);
category.HasMany<Article>().WithMany()
.UsingEntity<ArticleCategory>(
ac => ac.HasOne(a => a.Article).WithMany().OnDelete(DeleteBehavior.NoAction),
ac => ac.HasOne(a => a.Category).WithMany().OnDelete(DeleteBehavior.NoAction),
articleCategory => {
articleCategory.HasKey(ac => ac.Id);
articleCategory.ToTable("ArticleCategories");
articleCategory.HasQueryFilter(ac => !ac.Article.IsDeleted);
});
category.ToTable("Categories");
}); });
} }
} }

View file

@ -8,7 +8,7 @@ public enum ArticleStatus {
Published = 2 Published = 2
} }
// TODO:: Add category / tags for MVP // TODO:: Add tags for MVP ?
// TODO:: Archive System (Notice / Redirect to new content?) (Deprecation date?) // TODO:: Archive System (Notice / Redirect to new content?) (Deprecation date?)
// TODO:: Reference used files? // TODO:: Reference used files?

View file

@ -0,0 +1,7 @@
namespace Wave.Data;
public class ArticleCategory {
public int Id { get; set; }
public required Article Article { get; set; }
public required Category Category { get; set; }
}

22
Wave/Data/Category.cs Normal file
View file

@ -0,0 +1,22 @@
using System.ComponentModel.DataAnnotations;
namespace Wave.Data;
// We want to order categories based on their Color/Role,
// so more important ones can be displayed first
public enum CategoryColors {
Primary = 1,
Dangerous = 5,
Important = 10,
Informative = 15,
Secondary = 20,
Default = 25,
Extra = 50,
}
public class Category {
public Guid Id { get; set; }
[MaxLength(128)]
public required string Name { get; set; }
public CategoryColors Color { get; set; } = CategoryColors.Default;
}

View file

@ -0,0 +1,465 @@
// <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 Wave.Data;
#nullable disable
namespace Wave.Data.Migrations.postgres
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20240126112644_Categories")]
partial class Categories
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("ProviderKey")
.HasColumnType("text");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("RoleId")
.HasColumnType("text");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("Wave.Data.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("AboutTheAuthor")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<string>("Biography")
.IsRequired()
.HasMaxLength(4096)
.HasColumnType("character varying(4096)");
b.Property<string>("BiographyHtml")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<string>("FullName")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Wave.Data.Article", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("AuthorId")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Body")
.IsRequired()
.HasColumnType("text");
b.Property<string>("BodyHtml")
.IsRequired()
.HasColumnType("text");
b.Property<DateTimeOffset>("CreationDate")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<DateTimeOffset>("LastModified")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<DateTimeOffset>("PublishDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("ReviewerId")
.HasColumnType("text");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("AuthorId");
b.HasIndex("ReviewerId");
b.ToTable("Articles", (string)null);
});
modelBuilder.Entity("Wave.Data.ArticleCategory", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<Guid>("ArticleId")
.HasColumnType("uuid");
b.Property<Guid>("CategoryId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("ArticleId");
b.HasIndex("CategoryId");
b.ToTable("ArticleCategories", (string)null);
});
modelBuilder.Entity("Wave.Data.Category", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<int>("Color")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(25);
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.HasKey("Id");
b.ToTable("Categories", (string)null);
});
modelBuilder.Entity("Wave.Data.ProfilePicture", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ApplicationUserId")
.HasColumnType("text");
b.Property<Guid>("ImageId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("ApplicationUserId")
.IsUnique();
b.ToTable("ProfilePictures", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Wave.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Wave.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Wave.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Wave.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Wave.Data.Article", b =>
{
b.HasOne("Wave.Data.ApplicationUser", "Author")
.WithMany()
.HasForeignKey("AuthorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Wave.Data.ApplicationUser", "Reviewer")
.WithMany()
.HasForeignKey("ReviewerId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("Author");
b.Navigation("Reviewer");
});
modelBuilder.Entity("Wave.Data.ArticleCategory", b =>
{
b.HasOne("Wave.Data.Article", "Article")
.WithMany()
.HasForeignKey("ArticleId")
.OnDelete(DeleteBehavior.NoAction)
.IsRequired();
b.HasOne("Wave.Data.Category", "Category")
.WithMany()
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.NoAction)
.IsRequired();
b.Navigation("Article");
b.Navigation("Category");
});
modelBuilder.Entity("Wave.Data.ProfilePicture", b =>
{
b.HasOne("Wave.Data.ApplicationUser", null)
.WithOne("ProfilePicture")
.HasForeignKey("Wave.Data.ProfilePicture", "ApplicationUserId")
.OnDelete(DeleteBehavior.SetNull);
});
modelBuilder.Entity("Wave.Data.ApplicationUser", b =>
{
b.Navigation("ProfilePicture");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,157 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Wave.Data.Migrations.postgres;
/// <inheritdoc />
public partial class Categories : Migration {
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder) {
migrationBuilder.DropForeignKey(
name: "FK_Article_AspNetUsers_AuthorId",
table: "Article");
migrationBuilder.DropForeignKey(
name: "FK_Article_AspNetUsers_ReviewerId",
table: "Article");
migrationBuilder.DropPrimaryKey(
name: "PK_Article",
table: "Article");
migrationBuilder.RenameTable(
name: "Article",
newName: "Articles");
migrationBuilder.RenameIndex(
name: "IX_Article_ReviewerId",
table: "Articles",
newName: "IX_Articles_ReviewerId");
migrationBuilder.RenameIndex(
name: "IX_Article_AuthorId",
table: "Articles",
newName: "IX_Articles_AuthorId");
migrationBuilder.AddPrimaryKey(
name: "PK_Articles",
table: "Articles",
column: "Id");
migrationBuilder.CreateTable(
name: "Categories",
columns: table => new {
Id = table.Column<Guid>(type: "uuid", nullable: false),
Name = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
Color = table.Column<int>(type: "integer", nullable: false, defaultValue: 25)
},
constraints: table => { table.PrimaryKey("PK_Categories", x => x.Id); });
migrationBuilder.CreateTable(
name: "ArticleCategories",
columns: table => new {
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy",
NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
ArticleId = table.Column<Guid>(type: "uuid", nullable: false),
CategoryId = table.Column<Guid>(type: "uuid", nullable: false)
},
constraints: table => {
table.PrimaryKey("PK_ArticleCategories", x => x.Id);
table.ForeignKey(
name: "FK_ArticleCategories_Articles_ArticleId",
column: x => x.ArticleId,
principalTable: "Articles",
principalColumn: "Id");
table.ForeignKey(
name: "FK_ArticleCategories_Categories_CategoryId",
column: x => x.CategoryId,
principalTable: "Categories",
principalColumn: "Id");
});
migrationBuilder.CreateIndex(
name: "IX_ArticleCategories_ArticleId",
table: "ArticleCategories",
column: "ArticleId");
migrationBuilder.CreateIndex(
name: "IX_ArticleCategories_CategoryId",
table: "ArticleCategories",
column: "CategoryId");
migrationBuilder.AddForeignKey(
name: "FK_Articles_AspNetUsers_AuthorId",
table: "Articles",
column: "AuthorId",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Articles_AspNetUsers_ReviewerId",
table: "Articles",
column: "ReviewerId",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder) {
migrationBuilder.DropForeignKey(
name: "FK_Articles_AspNetUsers_AuthorId",
table: "Articles");
migrationBuilder.DropForeignKey(
name: "FK_Articles_AspNetUsers_ReviewerId",
table: "Articles");
migrationBuilder.DropTable(
name: "ArticleCategories");
migrationBuilder.DropTable(
name: "Categories");
migrationBuilder.DropPrimaryKey(
name: "PK_Articles",
table: "Articles");
migrationBuilder.RenameTable(
name: "Articles",
newName: "Article");
migrationBuilder.RenameIndex(
name: "IX_Articles_ReviewerId",
table: "Article",
newName: "IX_Article_ReviewerId");
migrationBuilder.RenameIndex(
name: "IX_Articles_AuthorId",
table: "Article",
newName: "IX_Article_AuthorId");
migrationBuilder.AddPrimaryKey(
name: "PK_Article",
table: "Article",
column: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Article_AspNetUsers_AuthorId",
table: "Article",
column: "AuthorId",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Article_AspNetUsers_ReviewerId",
table: "Article",
column: "ReviewerId",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
}
}

View file

@ -262,8 +262,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.Property<bool>("IsDeleted") b.Property<bool>("IsDeleted")
.HasColumnType("boolean"); .HasColumnType("boolean");
b.Property<DateTimeOffset?>("LastModified") b.Property<DateTimeOffset>("LastModified")
.IsRequired()
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()"); .HasDefaultValueSql("now()");
@ -288,7 +287,51 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.HasIndex("ReviewerId"); b.HasIndex("ReviewerId");
b.ToTable("Article"); b.ToTable("Articles", (string)null);
});
modelBuilder.Entity("Wave.Data.ArticleCategory", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<Guid>("ArticleId")
.HasColumnType("uuid");
b.Property<Guid>("CategoryId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("ArticleId");
b.HasIndex("CategoryId");
b.ToTable("ArticleCategories", (string)null);
});
modelBuilder.Entity("Wave.Data.Category", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<int>("Color")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(25);
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.HasKey("Id");
b.ToTable("Categories", (string)null);
}); });
modelBuilder.Entity("Wave.Data.ProfilePicture", b => modelBuilder.Entity("Wave.Data.ProfilePicture", b =>
@ -382,6 +425,25 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.Navigation("Reviewer"); b.Navigation("Reviewer");
}); });
modelBuilder.Entity("Wave.Data.ArticleCategory", b =>
{
b.HasOne("Wave.Data.Article", "Article")
.WithMany()
.HasForeignKey("ArticleId")
.OnDelete(DeleteBehavior.NoAction)
.IsRequired();
b.HasOne("Wave.Data.Category", "Category")
.WithMany()
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.NoAction)
.IsRequired();
b.Navigation("Article");
b.Navigation("Category");
});
modelBuilder.Entity("Wave.Data.ProfilePicture", b => modelBuilder.Entity("Wave.Data.ProfilePicture", b =>
{ {
b.HasOne("Wave.Data.ApplicationUser", null) b.HasOne("Wave.Data.ApplicationUser", null)

View file

@ -63,6 +63,7 @@
.AddPolicy("ArticleEditPermissions", p => p.RequireRole("Author", "Admin")) .AddPolicy("ArticleEditPermissions", p => p.RequireRole("Author", "Admin"))
.AddPolicy("ArticleReviewPermissions", p => p.RequireRole("Reviewer", "Admin")) .AddPolicy("ArticleReviewPermissions", p => p.RequireRole("Reviewer", "Admin"))
.AddPolicy("ArticleDeletePermissions", p => p.RequireRole("Moderator", "Admin")) .AddPolicy("ArticleDeletePermissions", p => p.RequireRole("Moderator", "Admin"))
.AddPolicy("CategoryManagePermissions", p => p.RequireRole("Admin"))
.AddPolicy("RoleAssignPermissions", p => p.RequireRole("Admin")) .AddPolicy("RoleAssignPermissions", p => p.RequireRole("Admin"))
.AddPolicy("ArticleEditOrReviewPermissions", p => p.RequireRole("Author", "Reviewer", "Admin")); .AddPolicy("ArticleEditOrReviewPermissions", p => p.RequireRole("Author", "Reviewer", "Admin"));