From ca53f2e1b2b2a3b520ab495e6c4f0abe64299aee Mon Sep 17 00:00:00 2001 From: Mikayla Dobson Date: Mon, 4 Dec 2023 11:55:26 -0600 Subject: [PATCH] support for recipe ingredient associations --- Unbinder/Controllers/HomeController.cs | 23 +--- Unbinder/Controllers/RecipeController.cs | 4 +- .../Controllers/RecipeIngredientController.cs | 15 +++ Unbinder/DB/Initializer.cs | 77 +++++++++---- Unbinder/DB/SeedData.cs | 3 +- Unbinder/DB/UnbinderDbContext.cs | 1 + ...72304_RecipeIngredientRelation.Designer.cs | 107 ++++++++++++++++++ ...20231204172304_RecipeIngredientRelation.cs | 57 ++++++++++ .../UnbinderDbContextModelSnapshot.cs | 30 ++++- Unbinder/Models/Recipe.cs | 8 +- Unbinder/Models/RecipeImage.cs | 13 +++ Unbinder/Models/RecipeIngredient.cs | 14 +++ Unbinder/Program.cs | 2 +- .../IRecipeIngredientRepository.cs | 8 ++ .../RecipeIngredientRepository.cs | 31 +++++ Unbinder/Unbinder.csproj | 1 + Unbinder/Views/Recipe/Manage.cshtml | 13 +++ Unbinder/Views/Recipe/RecipeId.cshtml | 11 ++ .../Shared/Partials/_IngredientList.cshtml | 8 ++ .../Shared/Partials/_RecipeManagerItem.cshtml | 12 ++ 20 files changed, 391 insertions(+), 47 deletions(-) create mode 100644 Unbinder/Controllers/RecipeIngredientController.cs create mode 100644 Unbinder/Migrations/20231204172304_RecipeIngredientRelation.Designer.cs create mode 100644 Unbinder/Migrations/20231204172304_RecipeIngredientRelation.cs create mode 100644 Unbinder/Models/RecipeImage.cs create mode 100644 Unbinder/Models/RecipeIngredient.cs create mode 100644 Unbinder/Repositories/IRecipeIngredientRepository.cs create mode 100644 Unbinder/Repositories/RecipeIngredientRepository.cs create mode 100644 Unbinder/Views/Recipe/Manage.cshtml create mode 100644 Unbinder/Views/Shared/Partials/_IngredientList.cshtml create mode 100644 Unbinder/Views/Shared/Partials/_RecipeManagerItem.cshtml diff --git a/Unbinder/Controllers/HomeController.cs b/Unbinder/Controllers/HomeController.cs index 5964b20..4666c1e 100644 --- a/Unbinder/Controllers/HomeController.cs +++ b/Unbinder/Controllers/HomeController.cs @@ -1,5 +1,4 @@ -using Amazon.S3; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Unbinder.Services; namespace Unbinder.Controllers @@ -19,31 +18,13 @@ namespace Unbinder.Controllers return View(); } - string keys = ""; - foreach (var entry in response.S3Objects) - { - keys += entry.Key + ", "; - } - - if (keys != "") logger.Log(LogLevel.Information, $"Found keys: {keys ?? "(none)"}"); return View(response.S3Objects); } catch (Exception ex) { - HandleError(ex, logger); + logger.Log(LogLevel.Error, ex.Message); return View(); } } - - private static void HandleError(Exception ex, ILogger logger) - { - if (ex is AmazonS3Exception s3Exception) - { - logger.Log(LogLevel.Warning, s3Exception.ErrorCode); - logger.Log(LogLevel.Warning, s3Exception.Message); - } - else - logger.Log(LogLevel.Error, ex.Message); - } } } diff --git a/Unbinder/Controllers/RecipeController.cs b/Unbinder/Controllers/RecipeController.cs index 4087836..7631a40 100644 --- a/Unbinder/Controllers/RecipeController.cs +++ b/Unbinder/Controllers/RecipeController.cs @@ -17,12 +17,14 @@ namespace Unbinder.Controllers } [Route("[controller]/{id}")] - public IActionResult RecipeId(int id) + public IActionResult RecipeId(int id, bool editMode = false) { var result = _recipeRepository.GetById(id); Console.WriteLine(result == null ? "No result found" : result); + ViewBag.EditMode = editMode; + return result == null ? NotFound() : View(result); diff --git a/Unbinder/Controllers/RecipeIngredientController.cs b/Unbinder/Controllers/RecipeIngredientController.cs new file mode 100644 index 0000000..69f67ff --- /dev/null +++ b/Unbinder/Controllers/RecipeIngredientController.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Mvc; +using Unbinder.Repositories; + +namespace Unbinder.Controllers +{ + public class RecipeIngredientController(IRecipeIngredientRepository repository) : Controller + { + private readonly IRecipeIngredientRepository _repository = repository; + + public IActionResult Index() + { + return View(); + } + } +} diff --git a/Unbinder/DB/Initializer.cs b/Unbinder/DB/Initializer.cs index f7c2711..2f703e9 100644 --- a/Unbinder/DB/Initializer.cs +++ b/Unbinder/DB/Initializer.cs @@ -1,4 +1,5 @@ using Unbinder.Models; +using Unbinder.Repositories; namespace Unbinder.DB { @@ -6,32 +7,70 @@ namespace Unbinder.DB { public static int Seed(IApplicationBuilder applicationBuilder) { - UnbinderDbContext context = applicationBuilder.ApplicationServices.CreateScope() + int totalInsertCount = 0; + + using UnbinderDbContext context = applicationBuilder.ApplicationServices.CreateScope() .ServiceProvider.GetRequiredService(); Console.WriteLine("Connection established, preparing to seed database..."); - if (!context.Recipes.Any()) - { - context.Recipes.AddRange(SeedData.InitialRecipes); - } - else - { - Console.WriteLine("Recipes already exist in the database"); - } - - if (!context.Ingredients.Any()) - { - context.Ingredients.AddRange(SeedData.PadThaiIngredients); - } - else - { - Console.WriteLine("Ingredients already exist in the database"); - } + // if records are not already seeded, do an initial insert into DB + WriteIfNotInitialized(context.Recipes, SeedData.InitialRecipes, context.Recipes.AddRange); + WriteIfNotInitialized(context.Ingredients, SeedData.InitialIngredients, context.Ingredients.AddRange); + // save changes so we can references them for establishing relations int insertCount = context.SaveChanges(); Console.WriteLine($"Seeded {insertCount} records"); - return insertCount; + totalInsertCount += insertCount; + + // establish relations if they do not already exist + Recipe? padThai = context.Recipes.Where(r => r.Name == "Pad Thai").FirstOrDefault(); + Recipe? pancakes = context.Recipes.Where(r => r.Name == "Pancakes").FirstOrDefault(); + + if (padThai != null) + { + Console.WriteLine("Found Pad Thai recipe, checking relations..."); + var padThaiIngredientNames = SeedData.PadThaiIngredients.Select(i => i.Name); + AddRelations(context, padThaiIngredientNames, padThai.RecipeId); + } + + if (pancakes != null) + { + Console.WriteLine("Found Pancakes recipe, checking relations..."); + var pancakeIngredientNames = SeedData.PancakeIngredients.Select(i => i.Name); + AddRelations(context, pancakeIngredientNames, pancakes.RecipeId); + } + + totalInsertCount += context.SaveChanges(); + return totalInsertCount; + } + + private delegate void Callback(IEnumerable seedData); + + private static void WriteIfNotInitialized(IEnumerable existingItems, IEnumerable seedData, Callback addRange) + { + if (existingItems.Any()) { + Console.WriteLine("Record already exists in the database"); + return; + } + + addRange(seedData); + } + + private static void AddRelations(UnbinderDbContext context, IEnumerable names, int id) + { + foreach (var ing in context.Ingredients) + { + if (names.Contains(ing.Name)) + { + context.RecipeIngredients.Add(new RecipeIngredient + { + RecipeId = id, + IngredientId = ing.IngredientId, + }); + } + } + } } } diff --git a/Unbinder/DB/SeedData.cs b/Unbinder/DB/SeedData.cs index c277a15..df6a23a 100644 --- a/Unbinder/DB/SeedData.cs +++ b/Unbinder/DB/SeedData.cs @@ -31,10 +31,11 @@ namespace Unbinder.DB "3. Add the wet ingredients to the dry ingredients and whisk until just combined. Let the batter rest for 5 minutes.\n" + "4. Heat a large nonstick skillet or griddle over medium-low heat. Add a little butter to the pan and swirl to coat. Add ⅓ cup of the batter to the pan and cook until the edges are set and bubbles form on the surface, about 2 minutes. Flip and cook for 1 minute more. Repeat with the remaining batter.\n" + "5. Serve with butter and maple syrup.", + S3Url = "https://unbinder.s3.us-east-2.amazonaws.com/Pancake+Recipe.pdf", }; } - public static Recipe[] InitialRecipes => new Recipe[] { PadThaiRecipe, PancakeRecipe }; + public static Recipe[] InitialRecipes => [PadThaiRecipe, PancakeRecipe]; public static Ingredient[] PadThaiIngredients { diff --git a/Unbinder/DB/UnbinderDbContext.cs b/Unbinder/DB/UnbinderDbContext.cs index 31f666d..8c85ab2 100644 --- a/Unbinder/DB/UnbinderDbContext.cs +++ b/Unbinder/DB/UnbinderDbContext.cs @@ -11,5 +11,6 @@ namespace Unbinder.DB public DbSet Recipes { get; set; } public DbSet Ingredients { get; set; } + public DbSet RecipeIngredients { get; set; } } } diff --git a/Unbinder/Migrations/20231204172304_RecipeIngredientRelation.Designer.cs b/Unbinder/Migrations/20231204172304_RecipeIngredientRelation.Designer.cs new file mode 100644 index 0000000..6f557a5 --- /dev/null +++ b/Unbinder/Migrations/20231204172304_RecipeIngredientRelation.Designer.cs @@ -0,0 +1,107 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Unbinder.DB; + +#nullable disable + +namespace Unbinder.Migrations +{ + [DbContext(typeof(UnbinderDbContext))] + [Migration("20231204172304_RecipeIngredientRelation")] + partial class RecipeIngredientRelation + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Unbinder.Models.Ingredient", b => + { + b.Property("IngredientId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("IngredientId")); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("IngredientId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Unbinder.Models.Recipe", b => + { + b.Property("RecipeId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RecipeId")); + + b.Property("Author") + .HasColumnType("nvarchar(max)"); + + b.Property("MainImageUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RecipeText") + .HasColumnType("nvarchar(max)"); + + b.Property("S3Url") + .HasColumnType("nvarchar(max)"); + + b.Property("ShortDescription") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("RecipeId"); + + b.ToTable("Recipes"); + }); + + modelBuilder.Entity("Unbinder.Models.RecipeIngredient", b => + { + b.Property("RecipeIngredientId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RecipeIngredientId")); + + b.Property("Amount") + .HasColumnType("nvarchar(max)"); + + b.Property("IngredientId") + .HasColumnType("int"); + + b.Property("RecipeId") + .HasColumnType("int"); + + b.Property("Unit") + .HasColumnType("nvarchar(max)"); + + b.HasKey("RecipeIngredientId"); + + b.ToTable("RecipeIngredients"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Unbinder/Migrations/20231204172304_RecipeIngredientRelation.cs b/Unbinder/Migrations/20231204172304_RecipeIngredientRelation.cs new file mode 100644 index 0000000..8124749 --- /dev/null +++ b/Unbinder/Migrations/20231204172304_RecipeIngredientRelation.cs @@ -0,0 +1,57 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Unbinder.Migrations +{ + /// + public partial class RecipeIngredientRelation : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "ImageUrl", + table: "Recipes", + newName: "S3Url"); + + migrationBuilder.AddColumn( + name: "MainImageUrl", + table: "Recipes", + type: "nvarchar(max)", + nullable: true); + + migrationBuilder.CreateTable( + name: "RecipeIngredients", + columns: table => new + { + RecipeIngredientId = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + RecipeId = table.Column(type: "int", nullable: false), + IngredientId = table.Column(type: "int", nullable: false), + Amount = table.Column(type: "nvarchar(max)", nullable: true), + Unit = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_RecipeIngredients", x => x.RecipeIngredientId); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "RecipeIngredients"); + + migrationBuilder.DropColumn( + name: "MainImageUrl", + table: "Recipes"); + + migrationBuilder.RenameColumn( + name: "S3Url", + table: "Recipes", + newName: "ImageUrl"); + } + } +} diff --git a/Unbinder/Migrations/UnbinderDbContextModelSnapshot.cs b/Unbinder/Migrations/UnbinderDbContextModelSnapshot.cs index 9137be5..b5bc5e5 100644 --- a/Unbinder/Migrations/UnbinderDbContextModelSnapshot.cs +++ b/Unbinder/Migrations/UnbinderDbContextModelSnapshot.cs @@ -52,7 +52,7 @@ namespace Unbinder.Migrations b.Property("Author") .HasColumnType("nvarchar(max)"); - b.Property("ImageUrl") + b.Property("MainImageUrl") .HasColumnType("nvarchar(max)"); b.Property("Name") @@ -62,6 +62,9 @@ namespace Unbinder.Migrations b.Property("RecipeText") .HasColumnType("nvarchar(max)"); + b.Property("S3Url") + .HasColumnType("nvarchar(max)"); + b.Property("ShortDescription") .IsRequired() .HasColumnType("nvarchar(max)"); @@ -70,6 +73,31 @@ namespace Unbinder.Migrations b.ToTable("Recipes"); }); + + modelBuilder.Entity("Unbinder.Models.RecipeIngredient", b => + { + b.Property("RecipeIngredientId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("RecipeIngredientId")); + + b.Property("Amount") + .HasColumnType("nvarchar(max)"); + + b.Property("IngredientId") + .HasColumnType("int"); + + b.Property("RecipeId") + .HasColumnType("int"); + + b.Property("Unit") + .HasColumnType("nvarchar(max)"); + + b.HasKey("RecipeIngredientId"); + + b.ToTable("RecipeIngredients"); + }); #pragma warning restore 612, 618 } } diff --git a/Unbinder/Models/Recipe.cs b/Unbinder/Models/Recipe.cs index 42a5226..974806b 100644 --- a/Unbinder/Models/Recipe.cs +++ b/Unbinder/Models/Recipe.cs @@ -5,13 +5,15 @@ namespace Unbinder.Models [Table("Recipes")] public record Recipe { + // required properties public int RecipeId { get; init; } public string Name {get; init; } = default!; public string ShortDescription { get; init; } = default!; + + // optional properties + public string? S3Url { get; init; } public string? Author { get; init; } - public string? RecipeText { get; init; } - - public string? ImageUrl { get; init; } + public string? MainImageUrl { get; init; } } } diff --git a/Unbinder/Models/RecipeImage.cs b/Unbinder/Models/RecipeImage.cs new file mode 100644 index 0000000..45df64e --- /dev/null +++ b/Unbinder/Models/RecipeImage.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace Unbinder.Models +{ + [Table("RecipeImages")] + public record RecipeImage + { + public int RecipeImageId { get; init; } + public int RecipeId { get; init; } + public string? ImageUrl { get; init; } + public string? ImageAlt { get; init; } + } +} diff --git a/Unbinder/Models/RecipeIngredient.cs b/Unbinder/Models/RecipeIngredient.cs new file mode 100644 index 0000000..1810874 --- /dev/null +++ b/Unbinder/Models/RecipeIngredient.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace Unbinder.Models +{ + [Table("RecipeIngredients")] + public record RecipeIngredient + { + public int RecipeIngredientId { get; init; } + public int RecipeId { get; init; } + public int IngredientId { get; init; } + public string? Amount { get; init; } + public string? Unit { get; init; } + } +} diff --git a/Unbinder/Program.cs b/Unbinder/Program.cs index cb5fb90..a5192c7 100644 --- a/Unbinder/Program.cs +++ b/Unbinder/Program.cs @@ -1,5 +1,5 @@ -using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; +using Microsoft.Data.SqlClient; using Unbinder.DB; using Unbinder.Repositories; using Unbinder.Services; diff --git a/Unbinder/Repositories/IRecipeIngredientRepository.cs b/Unbinder/Repositories/IRecipeIngredientRepository.cs new file mode 100644 index 0000000..04835e2 --- /dev/null +++ b/Unbinder/Repositories/IRecipeIngredientRepository.cs @@ -0,0 +1,8 @@ +using Unbinder.Models; + +namespace Unbinder.Repositories +{ + public interface IRecipeIngredientRepository : IBaseRepository + { + } +} diff --git a/Unbinder/Repositories/RecipeIngredientRepository.cs b/Unbinder/Repositories/RecipeIngredientRepository.cs new file mode 100644 index 0000000..190d44c --- /dev/null +++ b/Unbinder/Repositories/RecipeIngredientRepository.cs @@ -0,0 +1,31 @@ +using Unbinder.DB; +using Unbinder.Models; + +namespace Unbinder.Repositories +{ + public class RecipeIngredientRepository(UnbinderDbContext context) : BaseRepository(context), IRecipeIngredientRepository + { + // inapplicable methods intentionally left blank + public override IEnumerable? GetAll => null; + public override RecipeIngredient? UpdateById(int id) => null; + + // implemented methods: + public override RecipeIngredient? GetById(int id) => _dbContext.RecipeIngredients.Where(ri => ri.RecipeIngredientId == id).FirstOrDefault(); + + public override RecipeIngredient Post(RecipeIngredient entity) + { + _dbContext.RecipeIngredients.Add(entity); + _dbContext.SaveChanges(); + return entity; + } + + public override int DeleteById(int id) + { + var recipeIngredient = GetById(id); + if (recipeIngredient == null) return 0; + _dbContext.RecipeIngredients.Remove(recipeIngredient); + _dbContext.SaveChanges(); + return 1; + } + } +} diff --git a/Unbinder/Unbinder.csproj b/Unbinder/Unbinder.csproj index ea4b29c..3a21977 100644 --- a/Unbinder/Unbinder.csproj +++ b/Unbinder/Unbinder.csproj @@ -26,6 +26,7 @@ + diff --git a/Unbinder/Views/Recipe/Manage.cshtml b/Unbinder/Views/Recipe/Manage.cshtml new file mode 100644 index 0000000..d86e6df --- /dev/null +++ b/Unbinder/Views/Recipe/Manage.cshtml @@ -0,0 +1,13 @@ +@model IEnumerable + +@{ + ViewBag.Title = "Manage"; +} + +
+

Manage My Recipes

+ +
+ +
+
\ No newline at end of file diff --git a/Unbinder/Views/Recipe/RecipeId.cshtml b/Unbinder/Views/Recipe/RecipeId.cshtml index 2e6ad0e..8ddc4c0 100644 --- a/Unbinder/Views/Recipe/RecipeId.cshtml +++ b/Unbinder/Views/Recipe/RecipeId.cshtml @@ -1,6 +1,17 @@ @model Recipe +@{ + var editMode = ViewBag.EditMode ?? false; +} +

@Model.Name

@Model.ShortDescription

+ + @if (Model.RecipeText != null) + { +
+

@Model.RecipeText

+
+ }
\ No newline at end of file diff --git a/Unbinder/Views/Shared/Partials/_IngredientList.cshtml b/Unbinder/Views/Shared/Partials/_IngredientList.cshtml new file mode 100644 index 0000000..1560568 --- /dev/null +++ b/Unbinder/Views/Shared/Partials/_IngredientList.cshtml @@ -0,0 +1,8 @@ +@model IEnumerable + +
+ @foreach (var ingredient in Model) + { +

@ingredient.Name

+ } +
\ No newline at end of file diff --git a/Unbinder/Views/Shared/Partials/_RecipeManagerItem.cshtml b/Unbinder/Views/Shared/Partials/_RecipeManagerItem.cshtml new file mode 100644 index 0000000..ada7e5f --- /dev/null +++ b/Unbinder/Views/Shared/Partials/_RecipeManagerItem.cshtml @@ -0,0 +1,12 @@ +@model Recipe + +
+
+ @Model.Name +

@Model.ShortDescription

+
+
+ + +
+
\ No newline at end of file