support for displaying ingredient metadata

This commit is contained in:
2023-12-04 16:04:01 -06:00
parent ca53f2e1b2
commit af34139067
21 changed files with 416 additions and 38 deletions

1
.gitignore vendored
View File

@@ -6,6 +6,7 @@
# GITIGNORE FILES ADDED BY MIKAYLA
__notes__
*.env
UnbinderSandbox
# Files for/generated by Docker Compose
secrets

View File

@@ -3,7 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.7.34031.279
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unbinder", "Unbinder\Unbinder.csproj", "{620A53DB-415A-4741-ABDE-5E2B78197CF7}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unbinder", "Unbinder\Unbinder.csproj", "{620A53DB-415A-4741-ABDE-5E2B78197CF7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnbinderSandbox", "UnbinderSandbox\UnbinderSandbox.csproj", "{A75CD739-80D1-4D96-A828-52B3F49B8254}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -15,6 +17,10 @@ Global
{620A53DB-415A-4741-ABDE-5E2B78197CF7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{620A53DB-415A-4741-ABDE-5E2B78197CF7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{620A53DB-415A-4741-ABDE-5E2B78197CF7}.Release|Any CPU.Build.0 = Release|Any CPU
{A75CD739-80D1-4D96-A828-52B3F49B8254}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A75CD739-80D1-4D96-A828-52B3F49B8254}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A75CD739-80D1-4D96-A828-52B3F49B8254}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A75CD739-80D1-4D96-A828-52B3F49B8254}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -1,12 +1,14 @@
using Microsoft.AspNetCore.Mvc;
using Unbinder.Models;
using Unbinder.Repositories;
using Unbinder.ViewModels;
namespace Unbinder.Controllers
{
public class RecipeController(IRecipeRepository recipeRepository) : Controller
public class RecipeController(IRecipeRepository recipeRepository, IRecipeIngredientRepository recipeIngredientRepository) : Controller
{
private readonly IRecipeRepository _recipeRepository = recipeRepository;
private readonly IRecipeIngredientRepository _recipeIngredientRepository = recipeIngredientRepository;
public IActionResult Index()
{
@@ -16,18 +18,19 @@ namespace Unbinder.Controllers
: View(result);
}
[Route("[controller]/{id}")]
public IActionResult RecipeId(int id, bool editMode = false)
[Route("[controller]/{recipeId}")]
public IActionResult RecipeId(int recipeId, 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);
var recipe = _recipeRepository.GetById(recipeId);
if (recipe == null) return NotFound();
var ingredients = _recipeIngredientRepository.GetIngredientsByRecipeId(recipeId);
RecipeViewModel recipeViewModel = new(recipe, ingredients);
return View(recipeViewModel);
}
[Route("[controller]/search")]

View File

@@ -1,10 +1,13 @@
using Unbinder.Models;
using Microsoft.Extensions.Logging;
using Unbinder.Models;
using Unbinder.Repositories;
namespace Unbinder.DB
{
public static class Initializer
{
private static readonly ILogger logger = new LoggerFactory().CreateLogger("DBInitializer");
public static int Seed(IApplicationBuilder applicationBuilder)
{
int totalInsertCount = 0;
@@ -12,7 +15,7 @@ namespace Unbinder.DB
using UnbinderDbContext context = applicationBuilder.ApplicationServices.CreateScope()
.ServiceProvider.GetRequiredService<UnbinderDbContext>();
Console.WriteLine("Connection established, preparing to seed database...");
logger.Log(LogLevel.Information, "Connection established, preparing to seed database...");
// if records are not already seeded, do an initial insert into DB
WriteIfNotInitialized(context.Recipes, SeedData.InitialRecipes, context.Recipes.AddRange);
@@ -20,7 +23,7 @@ namespace Unbinder.DB
// save changes so we can references them for establishing relations
int insertCount = context.SaveChanges();
Console.WriteLine($"Seeded {insertCount} records");
logger.Log(LogLevel.Information, $"Seeded {insertCount} records");
totalInsertCount += insertCount;
// establish relations if they do not already exist
@@ -29,16 +32,14 @@ namespace Unbinder.DB
if (padThai != null)
{
Console.WriteLine("Found Pad Thai recipe, checking relations...");
var padThaiIngredientNames = SeedData.PadThaiIngredients.Select(i => i.Name);
AddRelations(context, padThaiIngredientNames, padThai.RecipeId);
logger.Log(LogLevel.Information, "Found Pad Thai recipe, checking relations...");
AddRelations(context, SeedData.PadThaiIngredientsWithDetails, 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);
logger.Log(LogLevel.Information, "Found Pancakes recipe, checking relations...");
AddRelations(context, SeedData.PancakeIngredientsWithDetails, pancakes.RecipeId);
}
totalInsertCount += context.SaveChanges();
@@ -50,23 +51,31 @@ namespace Unbinder.DB
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");
logger.Log(LogLevel.Information, "Record already exists in the database");
return;
}
addRange(seedData);
}
private static void AddRelations(UnbinderDbContext context, IEnumerable<string> names, int id)
private static void AddRelations(UnbinderDbContext context, IngredientWithDetails[] details, int recipeId)
{
var names = details.Select(d => d.Ingredient.Name);
foreach (var ing in context.Ingredients)
{
if (names.Contains(ing.Name))
{
logger.Log(LogLevel.Information, $"Writing entry for {ing.Name}");
var entryForThis = details.Where(d => d.Ingredient.Name == ing.Name).FirstOrDefault();
context.RecipeIngredients.Add(new RecipeIngredient
{
RecipeId = id,
RecipeId = recipeId,
IngredientId = ing.IngredientId,
Amount = entryForThis?.Details.Amount,
Unit = entryForThis?.Details.Unit,
});
}
}

View File

@@ -0,0 +1,3 @@
TRUNCATE TABLE Ingredients;
TRUNCATE TABLE RecipeIngredients;
TRUNCATE TABLE Recipes;

View File

@@ -37,6 +37,69 @@ namespace Unbinder.DB
public static Recipe[] InitialRecipes => [PadThaiRecipe, PancakeRecipe];
public static IngredientWithDetails[] PadThaiIngredientsWithDetails
{
get
{
return [
new IngredientWithDetails
{
Ingredient = PadThaiIngredients[0],
Details = new IngredientDetails
{
Amount = 1,
Unit = "Bunch, roughly chopped"
}
},
new IngredientWithDetails
{
Ingredient = PadThaiIngredients[1],
Details = new IngredientDetails
{
Amount = 1,
Unit = "Lime (juice)"
}
},
new IngredientWithDetails
{
Ingredient = PadThaiIngredients[2],
Details = new IngredientDetails
{
Amount = 0.25,
Unit = "Cup (chopped)",
}
},
new IngredientWithDetails
{
Ingredient = PadThaiIngredients[3],
Details = new IngredientDetails
{
Amount = 16,
Unit = "oz."
}
},
new IngredientWithDetails
{
Ingredient = PadThaiIngredients[4],
Details = new IngredientDetails
{
Amount = 1,
Unit = "Egg (scrambled)"
}
},
new IngredientWithDetails
{
Ingredient = PadThaiIngredients[5],
Details = new IngredientDetails
{
Amount = 1,
Unit = "Block (diced)"
}
}
];
}
}
public static Ingredient[] PadThaiIngredients
{
get
@@ -104,6 +167,51 @@ namespace Unbinder.DB
}
}
public static IngredientWithDetails[] PancakeIngredientsWithDetails
{
get
{
return [
new IngredientWithDetails
{
Ingredient = PancakeIngredients[0],
Details = new IngredientDetails
{
Amount = 2,
Unit = "cups",
}
},
new IngredientWithDetails
{
Ingredient = PancakeIngredients[1],
Details = new IngredientDetails
{
Amount = 1,
Unit = "teaspoon"
}
},
new IngredientWithDetails
{
Ingredient = PancakeIngredients[2],
Details = new IngredientDetails
{
Amount = 1,
Unit = "teaspoon",
}
},
new IngredientWithDetails
{
Ingredient = PancakeIngredients[3],
Details = new IngredientDetails
{
Amount = 2,
Unit = "tablespoons"
}
}
];
}
}
public static Ingredient[] InitialIngredients => PadThaiIngredients.Concat(PancakeIngredients).ToArray();
}
}

View File

@@ -11,7 +11,7 @@ using Unbinder.DB;
namespace Unbinder.Migrations
{
[DbContext(typeof(UnbinderDbContext))]
[DbContext(typeof(Initializer))]
[Migration("20231117145840_InitialMigration")]
partial class InitialMigration
{

View File

@@ -10,7 +10,7 @@ using Unbinder.DB;
namespace Unbinder.Migrations
{
[DbContext(typeof(UnbinderDbContext))]
[DbContext(typeof(Initializer))]
[Migration("20231117150919_RemoveIngredientArray")]
partial class RemoveIngredientArray
{

View File

@@ -10,7 +10,7 @@ using Unbinder.DB;
namespace Unbinder.Migrations
{
[DbContext(typeof(UnbinderDbContext))]
[DbContext(typeof(Initializer))]
[Migration("20231201185549_update-table-names")]
partial class updatetablenames
{

View File

@@ -10,7 +10,7 @@ using Unbinder.DB;
namespace Unbinder.Migrations
{
[DbContext(typeof(UnbinderDbContext))]
[DbContext(typeof(Initializer))]
[Migration("20231204172304_RecipeIngredientRelation")]
partial class RecipeIngredientRelation
{

View File

@@ -0,0 +1,108 @@
// <auto-generated />
using System;
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(Initializer))]
[Migration("20231204213851_ModifyDataTypes")]
partial class ModifyDataTypes
{
/// <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<double?>("Amount")
.HasColumnType("float");
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,36 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Unbinder.Migrations
{
/// <inheritdoc />
public partial class ModifyDataTypes : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<double>(
name: "Amount",
table: "RecipeIngredients",
type: "float",
nullable: true,
oldClrType: typeof(string),
oldType: "nvarchar(max)",
oldNullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "Amount",
table: "RecipeIngredients",
type: "nvarchar(max)",
nullable: true,
oldClrType: typeof(double),
oldType: "float",
oldNullable: true);
}
}
}

View File

@@ -1,4 +1,5 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
@@ -9,7 +10,7 @@ using Unbinder.DB;
namespace Unbinder.Migrations
{
[DbContext(typeof(UnbinderDbContext))]
[DbContext(typeof(Initializer))]
partial class UnbinderDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
@@ -82,8 +83,8 @@ namespace Unbinder.Migrations
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("RecipeIngredientId"));
b.Property<string>("Amount")
.HasColumnType("nvarchar(max)");
b.Property<double?>("Amount")
.HasColumnType("float");
b.Property<int>("IngredientId")
.HasColumnType("int");

View File

@@ -0,0 +1,15 @@
namespace Unbinder.Models
{
public record IngredientWithDetails
{
public Ingredient Ingredient { get; init; } = default!;
public IngredientDetails Details { get; init; } = default!;
}
public record IngredientDetails
{
public double? Amount { get; init; }
public string? Unit { get; init; }
}
}

View File

@@ -8,7 +8,7 @@ namespace Unbinder.Models
public int RecipeIngredientId { get; init; }
public int RecipeId { get; init; }
public int IngredientId { get; init; }
public string? Amount { get; init; }
public double? Amount { get; init; }
public string? Unit { get; init; }
}
}

View File

@@ -46,6 +46,7 @@ builder.Services.AddCors(options =>
builder.Services.AddScoped<IRecipeRepository, RecipeRepository>();
builder.Services.AddScoped<IIngredientRepository, IngredientRepository>();
builder.Services.AddScoped<IRecipeIngredientRepository, RecipeIngredientRepository>();
// include aws service
builder.Services.AddTransient<S3Service>();

View File

@@ -4,5 +4,6 @@ namespace Unbinder.Repositories
{
public interface IRecipeIngredientRepository : IBaseRepository<RecipeIngredient>
{
public IEnumerable<IngredientWithDetails>? GetIngredientsByRecipeId(int recipeId);
}
}

View File

@@ -9,7 +9,7 @@ namespace Unbinder.Repositories
public override IEnumerable<RecipeIngredient>? GetAll => null;
public override RecipeIngredient? UpdateById(int id) => null;
// implemented methods:
// overridden methods:
public override RecipeIngredient? GetById(int id) => _dbContext.RecipeIngredients.Where(ri => ri.RecipeIngredientId == id).FirstOrDefault();
public override RecipeIngredient Post(RecipeIngredient entity)
@@ -27,5 +27,51 @@ namespace Unbinder.Repositories
_dbContext.SaveChanges();
return 1;
}
// custom methods
public IEnumerable<IngredientWithDetails>? GetIngredientsByRecipeId(int recipeId)
{
// get all ingredients associated with this recipe
var ingredientDetails = _dbContext.RecipeIngredients
.Where(ri => ri.RecipeId == recipeId)
.ToArray();
if (ingredientDetails == null) return null;
// get the full DB listing for each ingredient
var ingredientsForRecipe = _dbContext.Ingredients
.Where(i => ingredientDetails.Select(d => d.IngredientId).Contains(i.IngredientId))
.ToArray();
if (ingredientsForRecipe == null) return null;
// combine the two into a merged output
return MergeIngredientsWithDetails(ingredientsForRecipe, ingredientDetails);
}
private static List<IngredientWithDetails> MergeIngredientsWithDetails(
Ingredient[] ingredients, RecipeIngredient[] details)
{
List<IngredientWithDetails> output = [];
foreach (var ing in ingredients)
{
RecipeIngredient? CurrentDetails = details.Where(ri =>
ri.IngredientId == ing.IngredientId).FirstOrDefault();
output.Add(new IngredientWithDetails
{
Ingredient = ing,
Details = new IngredientDetails
{
Amount = CurrentDetails?.Amount,
Unit = CurrentDetails?.Unit,
}
});
}
return output;
}
}
}

View File

@@ -28,7 +28,6 @@
<Folder Include="secrets\" />
<Folder Include="TagHelpers\" />
<Folder Include="__notes__\" />
<Folder Include="ViewModels\" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,10 @@
using Unbinder.Models;
namespace Unbinder.ViewModels
{
public class RecipeViewModel(Recipe _recipe, IEnumerable<IngredientWithDetails>? _ingredients)
{
public Recipe Recipe { get; init; } = _recipe;
public IEnumerable<IngredientWithDetails>? Ingredients { get; init; } = _ingredients;
}
}

View File

@@ -1,17 +1,48 @@
@model Recipe
@model RecipeViewModel
@{
var editMode = ViewBag.EditMode ?? false;
Recipe recipe = Model.Recipe;
IEnumerable<IngredientWithDetails>? entries = Model.Ingredients;
}
<div id="recipe-details-by-id">
<h1>@Model.Name</h1>
<p>@Model.ShortDescription</p>
<h1>@recipe.Name</h1>
<p>@recipe.ShortDescription</p>
@if (Model.RecipeText != null)
@if (entries != null)
{
<div id="ingredient-list">
<h2>Ingredients:</h2>
<div>
@foreach (var entry in entries)
{
var measurementString = "";
if (entry.Details.Amount != null) measurementString += entry.Details.Amount.ToString();
if (entry.Details.Unit != null) measurementString += entry.Details.Unit;
<div>
<p>@entry.Ingredient.Name -- @measurementString</p>
</div>
}
</div>
<br />
</div>
}
@if (recipe.RecipeText != null)
{
<div id="recipe-text">
<p>@Model.RecipeText</p>
<p>@recipe.RecipeText</p>
</div>
}
@if (recipe.S3Url != null)
{
<div id="recipe-image-or-pdf">
</div>
}
</div>