support for recipe ingredient associations

This commit is contained in:
2023-12-04 11:55:26 -06:00
parent 47a82e18a7
commit ca53f2e1b2
20 changed files with 391 additions and 47 deletions

View File

@@ -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);
}
}
}

View File

@@ -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);

View File

@@ -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();
}
}
}

View File

@@ -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<UnbinderDbContext>();
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<T>(IEnumerable<T> seedData);
private static void WriteIfNotInitialized<T>(IEnumerable<T> existingItems, IEnumerable<T> seedData, Callback<T> addRange)
{
if (existingItems.Any()) {
Console.WriteLine("Record already exists in the database");
return;
}
addRange(seedData);
}
private static void AddRelations(UnbinderDbContext context, IEnumerable<string> names, int id)
{
foreach (var ing in context.Ingredients)
{
if (names.Contains(ing.Name))
{
context.RecipeIngredients.Add(new RecipeIngredient
{
RecipeId = id,
IngredientId = ing.IngredientId,
});
}
}
}
}
}

View File

@@ -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
{

View File

@@ -11,5 +11,6 @@ namespace Unbinder.DB
public DbSet<Recipe> Recipes { get; set; }
public DbSet<Ingredient> Ingredients { get; set; }
public DbSet<RecipeIngredient> RecipeIngredients { get; set; }
}
}

View File

@@ -0,0 +1,107 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<int>("IngredientId")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("IngredientId"));
b.Property<string>("Description")
.HasColumnType("nvarchar(max)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("IngredientId");
b.ToTable("Ingredients");
});
modelBuilder.Entity("Unbinder.Models.Recipe", b =>
{
b.Property<int>("RecipeId")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("RecipeId"));
b.Property<string>("Author")
.HasColumnType("nvarchar(max)");
b.Property<string>("MainImageUrl")
.HasColumnType("nvarchar(max)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("RecipeText")
.HasColumnType("nvarchar(max)");
b.Property<string>("S3Url")
.HasColumnType("nvarchar(max)");
b.Property<string>("ShortDescription")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("RecipeId");
b.ToTable("Recipes");
});
modelBuilder.Entity("Unbinder.Models.RecipeIngredient", b =>
{
b.Property<int>("RecipeIngredientId")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("RecipeIngredientId"));
b.Property<string>("Amount")
.HasColumnType("nvarchar(max)");
b.Property<int>("IngredientId")
.HasColumnType("int");
b.Property<int>("RecipeId")
.HasColumnType("int");
b.Property<string>("Unit")
.HasColumnType("nvarchar(max)");
b.HasKey("RecipeIngredientId");
b.ToTable("RecipeIngredients");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,57 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Unbinder.Migrations
{
/// <inheritdoc />
public partial class RecipeIngredientRelation : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "ImageUrl",
table: "Recipes",
newName: "S3Url");
migrationBuilder.AddColumn<string>(
name: "MainImageUrl",
table: "Recipes",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.CreateTable(
name: "RecipeIngredients",
columns: table => new
{
RecipeIngredientId = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
RecipeId = table.Column<int>(type: "int", nullable: false),
IngredientId = table.Column<int>(type: "int", nullable: false),
Amount = table.Column<string>(type: "nvarchar(max)", nullable: true),
Unit = table.Column<string>(type: "nvarchar(max)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_RecipeIngredients", x => x.RecipeIngredientId);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "RecipeIngredients");
migrationBuilder.DropColumn(
name: "MainImageUrl",
table: "Recipes");
migrationBuilder.RenameColumn(
name: "S3Url",
table: "Recipes",
newName: "ImageUrl");
}
}
}

View File

@@ -52,7 +52,7 @@ namespace Unbinder.Migrations
b.Property<string>("Author")
.HasColumnType("nvarchar(max)");
b.Property<string>("ImageUrl")
b.Property<string>("MainImageUrl")
.HasColumnType("nvarchar(max)");
b.Property<string>("Name")
@@ -62,6 +62,9 @@ namespace Unbinder.Migrations
b.Property<string>("RecipeText")
.HasColumnType("nvarchar(max)");
b.Property<string>("S3Url")
.HasColumnType("nvarchar(max)");
b.Property<string>("ShortDescription")
.IsRequired()
.HasColumnType("nvarchar(max)");
@@ -70,6 +73,31 @@ namespace Unbinder.Migrations
b.ToTable("Recipes");
});
modelBuilder.Entity("Unbinder.Models.RecipeIngredient", b =>
{
b.Property<int>("RecipeIngredientId")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("RecipeIngredientId"));
b.Property<string>("Amount")
.HasColumnType("nvarchar(max)");
b.Property<int>("IngredientId")
.HasColumnType("int");
b.Property<int>("RecipeId")
.HasColumnType("int");
b.Property<string>("Unit")
.HasColumnType("nvarchar(max)");
b.HasKey("RecipeIngredientId");
b.ToTable("RecipeIngredients");
});
#pragma warning restore 612, 618
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -1,5 +1,5 @@
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using Microsoft.Data.SqlClient;
using Unbinder.DB;
using Unbinder.Repositories;
using Unbinder.Services;

View File

@@ -0,0 +1,8 @@
using Unbinder.Models;
namespace Unbinder.Repositories
{
public interface IRecipeIngredientRepository : IBaseRepository<RecipeIngredient>
{
}
}

View File

@@ -0,0 +1,31 @@
using Unbinder.DB;
using Unbinder.Models;
namespace Unbinder.Repositories
{
public class RecipeIngredientRepository(UnbinderDbContext context) : BaseRepository<RecipeIngredient>(context), IRecipeIngredientRepository
{
// inapplicable methods intentionally left blank
public override IEnumerable<RecipeIngredient>? 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;
}
}
}

View File

@@ -26,6 +26,7 @@
<ItemGroup>
<Folder Include="DB\SQL\" />
<Folder Include="secrets\" />
<Folder Include="TagHelpers\" />
<Folder Include="__notes__\" />
<Folder Include="ViewModels\" />
</ItemGroup>

View File

@@ -0,0 +1,13 @@
@model IEnumerable<Recipe>
@{
ViewBag.Title = "Manage";
}
<div>
<h1>Manage My Recipes</h1>
<div id="recipe-list-view">
</div>
</div>

View File

@@ -1,6 +1,17 @@
@model Recipe
@{
var editMode = ViewBag.EditMode ?? false;
}
<div id="recipe-details-by-id">
<h1>@Model.Name</h1>
<p>@Model.ShortDescription</p>
@if (Model.RecipeText != null)
{
<div id="recipe-text">
<p>@Model.RecipeText</p>
</div>
}
</div>

View File

@@ -0,0 +1,8 @@
@model IEnumerable<Ingredient>
<div id="ingredient-list">
@foreach (var ingredient in Model)
{
<p>@ingredient.Name</p>
}
</div>

View File

@@ -0,0 +1,12 @@
@model Recipe
<div class="w-full flex">
<div class="flex flex-col w-1/2">
<a class="text-xl" asp-controller="Recipe" asp-route-recipeId="@Model.RecipeId">@Model.Name</a>
<p>@Model.ShortDescription</p>
</div>
<div class="flex w-1/2">
<button>Edit</button>
<button>Delete</button>
</div>
</div>