support for recipe ingredient associations
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
15
Unbinder/Controllers/RecipeIngredientController.cs
Normal file
15
Unbinder/Controllers/RecipeIngredientController.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
107
Unbinder/Migrations/20231204172304_RecipeIngredientRelation.Designer.cs
generated
Normal file
107
Unbinder/Migrations/20231204172304_RecipeIngredientRelation.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
13
Unbinder/Models/RecipeImage.cs
Normal file
13
Unbinder/Models/RecipeImage.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
14
Unbinder/Models/RecipeIngredient.cs
Normal file
14
Unbinder/Models/RecipeIngredient.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Unbinder.DB;
|
||||
using Unbinder.Repositories;
|
||||
using Unbinder.Services;
|
||||
|
||||
8
Unbinder/Repositories/IRecipeIngredientRepository.cs
Normal file
8
Unbinder/Repositories/IRecipeIngredientRepository.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using Unbinder.Models;
|
||||
|
||||
namespace Unbinder.Repositories
|
||||
{
|
||||
public interface IRecipeIngredientRepository : IBaseRepository<RecipeIngredient>
|
||||
{
|
||||
}
|
||||
}
|
||||
31
Unbinder/Repositories/RecipeIngredientRepository.cs
Normal file
31
Unbinder/Repositories/RecipeIngredientRepository.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@
|
||||
<ItemGroup>
|
||||
<Folder Include="DB\SQL\" />
|
||||
<Folder Include="secrets\" />
|
||||
<Folder Include="TagHelpers\" />
|
||||
<Folder Include="__notes__\" />
|
||||
<Folder Include="ViewModels\" />
|
||||
</ItemGroup>
|
||||
|
||||
13
Unbinder/Views/Recipe/Manage.cshtml
Normal file
13
Unbinder/Views/Recipe/Manage.cshtml
Normal file
@@ -0,0 +1,13 @@
|
||||
@model IEnumerable<Recipe>
|
||||
|
||||
@{
|
||||
ViewBag.Title = "Manage";
|
||||
}
|
||||
|
||||
<div>
|
||||
<h1>Manage My Recipes</h1>
|
||||
|
||||
<div id="recipe-list-view">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -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>
|
||||
8
Unbinder/Views/Shared/Partials/_IngredientList.cshtml
Normal file
8
Unbinder/Views/Shared/Partials/_IngredientList.cshtml
Normal file
@@ -0,0 +1,8 @@
|
||||
@model IEnumerable<Ingredient>
|
||||
|
||||
<div id="ingredient-list">
|
||||
@foreach (var ingredient in Model)
|
||||
{
|
||||
<p>@ingredient.Name</p>
|
||||
}
|
||||
</div>
|
||||
12
Unbinder/Views/Shared/Partials/_RecipeManagerItem.cshtml
Normal file
12
Unbinder/Views/Shared/Partials/_RecipeManagerItem.cshtml
Normal 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>
|
||||
Reference in New Issue
Block a user