Finished Project
This commit is contained in:
@@ -5,6 +5,8 @@ VisualStudioVersion = 17.11.35222.181
|
|||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Francesco.Recipes.World", "Francesco.Recipes.World\Francesco.Recipes.World.csproj", "{4D1BBCF4-8E06-4584-A383-B14BEC558408}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Francesco.Recipes.World", "Francesco.Recipes.World\Francesco.Recipes.World.csproj", "{4D1BBCF4-8E06-4584-A383-B14BEC558408}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrancescosRecipeWorld Mock", "FrancescosRecipeWorld Mock\FrancescosRecipeWorld Mock.csproj", "{04FD3555-3497-4F6B-B2C3-1B22EEFA676B}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -15,6 +17,10 @@ Global
|
|||||||
{4D1BBCF4-8E06-4584-A383-B14BEC558408}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{4D1BBCF4-8E06-4584-A383-B14BEC558408}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{4D1BBCF4-8E06-4584-A383-B14BEC558408}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{4D1BBCF4-8E06-4584-A383-B14BEC558408}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{4D1BBCF4-8E06-4584-A383-B14BEC558408}.Release|Any CPU.Build.0 = Release|Any CPU
|
{4D1BBCF4-8E06-4584-A383-B14BEC558408}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{04FD3555-3497-4F6B-B2C3-1B22EEFA676B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{04FD3555-3497-4F6B-B2C3-1B22EEFA676B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{04FD3555-3497-4F6B-B2C3-1B22EEFA676B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{04FD3555-3497-4F6B-B2C3-1B22EEFA676B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -50,5 +50,4 @@
|
|||||||
<Folder Include="Services\MediaFile\" />
|
<Folder Include="Services\MediaFile\" />
|
||||||
<Folder Include="Services\Ingredient\" />
|
<Folder Include="Services\Ingredient\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -4,8 +4,10 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Francesco.Recipes.World.Data;
|
using Francesco.Recipes.World.Data;
|
||||||
|
using Francesco.Recipes.World.Models;
|
||||||
using Francesco.Recipes.World.Models.BackendModels.Category;
|
using Francesco.Recipes.World.Models.BackendModels.Category;
|
||||||
using Francesco.Recipes.World.Models.BackendModels.Recipe;
|
using Francesco.Recipes.World.Models.BackendModels.Recipe;
|
||||||
|
using Francesco.Recipes.World.Views.Category;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
public class CategoryRepository : ICategoryRepository
|
public class CategoryRepository : ICategoryRepository
|
||||||
@@ -44,11 +46,44 @@
|
|||||||
|
|
||||||
public async Task<IEnumerable<Category>> GetAllCategoriesWithRecipesAsync()
|
public async Task<IEnumerable<Category>> GetAllCategoriesWithRecipesAsync()
|
||||||
{
|
{
|
||||||
return await _context.Categories
|
var categories = await _context.Categories
|
||||||
.Include(c => c.Recipes)
|
.Include(c => c.Recipes.OrderByDescending(r => r.CreatedAt).Take(3))
|
||||||
.ThenInclude(r => r.MediaFiles)
|
.ThenInclude(r => r.MediaFiles.Take(2))
|
||||||
.AsSplitQuery()
|
.AsSplitQuery()
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return categories;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<CategoryRecipesViewModel>> GetAllCategoriesWithRecipesViewModelAsync()
|
||||||
|
{
|
||||||
|
var categories = await _context.Categories
|
||||||
|
.Select(c => new CategoryRecipesViewModel
|
||||||
|
{
|
||||||
|
Category = c,
|
||||||
|
Recipes = c.Recipes
|
||||||
|
.OrderByDescending(r => r.CreatedAt)
|
||||||
|
.Take(3)
|
||||||
|
.Select(r => new RecipeCardViewModel
|
||||||
|
{
|
||||||
|
Id = r.Id,
|
||||||
|
Name = r.Name,
|
||||||
|
CookingTime = r.CookingTime,
|
||||||
|
IsFavorite = r.IsFavorite,
|
||||||
|
ImageData = r.MediaFiles
|
||||||
|
.OrderBy(m => m.Id)
|
||||||
|
.Select(m => m.Data)
|
||||||
|
.FirstOrDefault(),
|
||||||
|
MimeType = r.MediaFiles
|
||||||
|
.OrderBy(m => m.Id)
|
||||||
|
.Select(m => m.MimeType)
|
||||||
|
.FirstOrDefault(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.AsSplitQuery()
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
|
return categories;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Francesco.Recipes.World.Models.BackendModels.Category;
|
using Francesco.Recipes.World.Models.BackendModels.Category;
|
||||||
using Francesco.Recipes.World.Models.BackendModels.Recipe;
|
using Francesco.Recipes.World.Models.BackendModels.Recipe;
|
||||||
|
using Francesco.Recipes.World.Views.Category;
|
||||||
|
|
||||||
public interface ICategoryRepository
|
public interface ICategoryRepository
|
||||||
{
|
{
|
||||||
@@ -15,5 +16,7 @@
|
|||||||
Task<IEnumerable<Recipe>> GetRecipesByCategoryAsync(Guid categoryId);
|
Task<IEnumerable<Recipe>> GetRecipesByCategoryAsync(Guid categoryId);
|
||||||
|
|
||||||
Task<IEnumerable<Category>> GetAllCategoriesWithRecipesAsync();
|
Task<IEnumerable<Category>> GetAllCategoriesWithRecipesAsync();
|
||||||
|
|
||||||
|
Task<IEnumerable<CategoryRecipesViewModel>> GetAllCategoriesWithRecipesViewModelAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
.Where(r => r.IsFavorite)
|
.Where(r => r.IsFavorite)
|
||||||
.Include(r => r.Favorite)
|
.Include(r => r.Favorite)
|
||||||
.Include(r => r.MediaFiles)
|
.Include(r => r.MediaFiles)
|
||||||
|
.Take(6)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
Task<Recipe> CreateRecipeForCategoryAsync(Category category, string name, string description, Difficulty difficulty, int servings, TimeSpan preparationTime, TimeSpan cookingTime);
|
Task<Recipe> CreateRecipeForCategoryAsync(Category category, string name, string description, Difficulty difficulty, int servings, TimeSpan preparationTime, TimeSpan cookingTime);
|
||||||
|
|
||||||
Task<bool> DeleteRecipeAsync(Guid recipeId);
|
Task<bool> DeleteRecipeAsync(Guid recipeId);
|
||||||
|
|
||||||
Task<IEnumerable<SearchViewModel>> SearchInRecipesAndIngredients(string searchTerm);
|
Task<IEnumerable<SearchViewModel>> SearchInRecipesAndIngredients(string searchTerm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -198,35 +198,6 @@
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Expression<Func<Recipe, SearchViewModel>> SearchViewModelSelector()
|
|
||||||
{
|
|
||||||
return r => new SearchViewModel
|
|
||||||
{
|
|
||||||
Id = r.Id,
|
|
||||||
Name = r.Name,
|
|
||||||
Description = r.Description,
|
|
||||||
IsFavorite = r.IsFavorite,
|
|
||||||
ImageData = r.MediaFiles
|
|
||||||
.Where(m => m.MimeType != null && m.MimeType.StartsWith(ContentType.Image))
|
|
||||||
.Select(m => m.Data)
|
|
||||||
.FirstOrDefault(),
|
|
||||||
MimeType = r.MediaFiles
|
|
||||||
.Where(m => m.MimeType != null && m.MimeType.StartsWith(ContentType.Image))
|
|
||||||
.Select(m => m.MimeType)
|
|
||||||
.FirstOrDefault(),
|
|
||||||
Ingredients = r.RecipeIngredients.Select(ri => ri.Ingredient.Name).ToList(),
|
|
||||||
TotalTime = r.PreparationTime.Add(r.CookingTime),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IQueryable<Recipe> ApplyRecipeSearchFilter(IQueryable<Recipe> query, string normalizedSearchTerm)
|
|
||||||
{
|
|
||||||
return query.Where(r =>
|
|
||||||
EF.Functions.Like(r.Name.ToLower(), $"%{normalizedSearchTerm}%") ||
|
|
||||||
(r.Description != null && EF.Functions.Like(r.Description.ToLower(), $"%{normalizedSearchTerm}%")) ||
|
|
||||||
r.RecipeIngredients.Any(ri => EF.Functions.Like(ri.Ingredient.Name.ToLower(), $"%{normalizedSearchTerm}%")));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> DeleteRecipeAsync(Guid recipeId)
|
public async Task<bool> DeleteRecipeAsync(Guid recipeId)
|
||||||
{
|
{
|
||||||
var recipe = await GetRecipeByIdAsync(recipeId);
|
var recipe = await GetRecipeByIdAsync(recipeId);
|
||||||
@@ -259,9 +230,9 @@
|
|||||||
_context.MediaFiles.RemoveRange(recipe.MediaFiles);
|
_context.MediaFiles.RemoveRange(recipe.MediaFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recipe.Favorit != null && recipe.Favorit.Id != Guid.Empty)
|
if (recipe.Favorite != null && recipe.Favorite.Id != Guid.Empty)
|
||||||
{
|
{
|
||||||
_context.Remove(recipe.Favorit);
|
_context.Remove(recipe.Favorite);
|
||||||
}
|
}
|
||||||
|
|
||||||
_context.Recipes.Remove(recipe);
|
_context.Recipes.Remove(recipe);
|
||||||
@@ -269,5 +240,34 @@
|
|||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Expression<Func<Recipe, SearchViewModel>> SearchViewModelSelector()
|
||||||
|
{
|
||||||
|
return r => new SearchViewModel
|
||||||
|
{
|
||||||
|
Id = r.Id,
|
||||||
|
Name = r.Name,
|
||||||
|
Description = r.Description,
|
||||||
|
IsFavorite = r.IsFavorite,
|
||||||
|
ImageData = r.MediaFiles
|
||||||
|
.Where(m => m.MimeType != null && m.MimeType.StartsWith(ContentType.Image))
|
||||||
|
.Select(m => m.Data)
|
||||||
|
.FirstOrDefault(),
|
||||||
|
MimeType = r.MediaFiles
|
||||||
|
.Where(m => m.MimeType != null && m.MimeType.StartsWith(ContentType.Image))
|
||||||
|
.Select(m => m.MimeType)
|
||||||
|
.FirstOrDefault(),
|
||||||
|
Ingredients = r.RecipeIngredients.Select(ri => ri.Ingredient.Name).ToList(),
|
||||||
|
TotalTime = r.PreparationTime.Add(r.CookingTime),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IQueryable<Recipe> ApplyRecipeSearchFilter(IQueryable<Recipe> query, string normalizedSearchTerm)
|
||||||
|
{
|
||||||
|
return query.Where(r =>
|
||||||
|
EF.Functions.Like(r.Name.ToLower(), $"%{normalizedSearchTerm}%") ||
|
||||||
|
(r.Description != null && EF.Functions.Like(r.Description.ToLower(), $"%{normalizedSearchTerm}%")) ||
|
||||||
|
r.RecipeIngredients.Any(ri => EF.Functions.Like(ri.Ingredient.Name.ToLower(), $"%{normalizedSearchTerm}%")));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
namespace Francesco.Recipes.World.Views.Category
|
namespace Francesco.Recipes.World.Views.Category
|
||||||
{
|
{
|
||||||
|
using Francesco.Recipes.World.Models;
|
||||||
using Francesco.Recipes.World.Models.BackendModels.Category;
|
using Francesco.Recipes.World.Models.BackendModels.Category;
|
||||||
using Francesco.Recipes.World.Models.BackendModels.Recipe;
|
|
||||||
|
|
||||||
public class CategoryRecipesViewModel
|
public class CategoryRecipesViewModel
|
||||||
{
|
{
|
||||||
public Category Category { get; set; } = new ();
|
public Category Category { get; set; } = new ();
|
||||||
|
|
||||||
public IEnumerable<Recipe> Recipes { get; set; } = new List<Recipe>();
|
public IEnumerable<RecipeCardViewModel> Recipes { get; set; } = new List<RecipeCardViewModel>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,39 +32,41 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@foreach (var recipe in category.Recipes)
|
|
||||||
{
|
|
||||||
var mediaFile = recipe.MediaFiles?.FirstOrDefault();
|
|
||||||
var imageData = mediaFile?.Data;
|
|
||||||
var mimeType = mediaFile?.MimeType;
|
|
||||||
|
|
||||||
<div class="col-md-3 mb-4">
|
@foreach (var recipe in category.Recipes)
|
||||||
<div class="card h-100">
|
{
|
||||||
<div class="recipe-image">
|
var imageData = recipe.ImageData;
|
||||||
@if (imageData != null && mimeType != null)
|
var mimeType = recipe.MimeType;
|
||||||
{
|
|
||||||
<img src="data:@mimeType;base64,@Convert.ToBase64String(imageData)" alt="@recipe.Name" class="card-img-top" />
|
<div class="col-md-3 mb-4">
|
||||||
}
|
<div class="card h-100">
|
||||||
else
|
<div class="recipe-image">
|
||||||
{
|
@if (imageData != null && mimeType != null)
|
||||||
<img src="/images/placeholder.png" alt="@recipe.Name" class="card-img-top" />
|
{
|
||||||
}
|
<img src="data:@mimeType;base64,@Convert.ToBase64String(imageData)" alt="@recipe.Name" class="card-img-top" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<img src="/images/placeholder.png" alt="@recipe.Name" class="card-img-top" />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body d-flex flex-column">
|
||||||
|
<h5 class="card-title">@recipe.Name</h5>
|
||||||
|
<p class="card-text text-muted mb-2">@recipe.CookingTime</p>
|
||||||
|
<div id="favorite-button-@recipe.Id">
|
||||||
|
@await Html.PartialAsync("_FavoriteButton", recipe)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body d-flex flex-column">
|
|
||||||
<h5 class="card-title">@recipe.Name</h5>
|
|
||||||
<p class="card-text text-muted mb-2">@recipe.CookingTime</p>
|
|
||||||
|
|
||||||
<div id="favorite-button-@recipe.Id">
|
<a href="/Details/@recipe.Id"
|
||||||
@await Html.PartialAsync("_FavoriteButton", recipe)
|
class="btn btn-primary mt-auto">Details</a>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<a href="/Details/@recipe.Id"
|
|
||||||
class="btn btn-primary mt-auto">Details</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="col-md-3 mb-4">
|
<div class="col-md-3 mb-4">
|
||||||
<div class="card h-100 text-center">
|
<div class="card h-100 text-center">
|
||||||
|
|||||||
@@ -64,7 +64,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a href="/Recipe/Details/@recipe.Id" class="btn btn-primary btn-sm">Details</a>
|
<a href="/Details/@recipe.Id" class="btn btn-primary btn-sm">Details</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
@model IEnumerable<Francesco.Recipes.World.Views.Category.CategoryRecipesViewModel>
|
|
||||||
|
|
||||||
@{
|
|
||||||
ViewData["Title"] = "Category Recipes";
|
|
||||||
}
|
|
||||||
|
|
||||||
<h1>Category Recipes</h1>
|
|
||||||
|
|
||||||
@foreach (var categoryRecipes in Model)
|
|
||||||
{
|
|
||||||
<div class="category-section">
|
|
||||||
<h2>@categoryRecipes.Category.Name</h2>
|
|
||||||
<a asp-action="Create" asp-route-categoryId="@categoryRecipes.Category.Id" class="btn btn-primary">Rezept erstellen</a>
|
|
||||||
<div class="recipes">
|
|
||||||
@foreach (var recipe in categoryRecipes.Recipes)
|
|
||||||
{
|
|
||||||
<div class="recipe-card">
|
|
||||||
<div class="recipe-image">
|
|
||||||
@if (recipe.MediaFiles.Any() && recipe.MediaFiles.First().Data != null)
|
|
||||||
{
|
|
||||||
var mediaFile = recipe.MediaFiles.First();
|
|
||||||
if (mediaFile.Data != null)
|
|
||||||
{
|
|
||||||
<img src="data:@mediaFile.MimeType;base64,@Convert.ToBase64String(mediaFile.Data)" alt="@recipe.Name" class="img-fluid" />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="recipe-info">
|
|
||||||
<h3>@recipe.Name</h3>
|
|
||||||
<p>@recipe.Description</p>
|
|
||||||
<p><strong>Difficulty:</strong> @recipe.Difficulty</p>
|
|
||||||
<p><strong>Servings:</strong> @recipe.Servings</p>
|
|
||||||
<p><strong>Preparation Time:</strong> @recipe.PreparationTime</p>
|
|
||||||
<p><strong>Cooking Time:</strong> @recipe.CookingTime</p>
|
|
||||||
<div class="recipe-favorite">
|
|
||||||
@if (recipe.IsFavorite)
|
|
||||||
{
|
|
||||||
<form method="post" asp-action="RemoveFavorite" asp-controller="Recipe">
|
|
||||||
<input type="hidden" name="recipeId" value="@recipe.Id" />
|
|
||||||
<button type="submit" class="btn btn-danger">Remove from Favorites</button>
|
|
||||||
</form>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<form method="post" asp-action="AddFavorite" asp-controller="Recipe">
|
|
||||||
<input type="hidden" name="recipeId" value="@recipe.Id" />
|
|
||||||
<button type="submit" class="btn btn-primary">Add to Favorites</button>
|
|
||||||
</form>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<div class="recipe-card add-recipe-card">
|
|
||||||
<a asp-action="Create" asp-route-categoryId="@categoryRecipes.Category.Id" class="btn btn-primary">Rezept hinzufügen</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.category-section {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recipes {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recipe-card {
|
|
||||||
width: 200px;
|
|
||||||
margin: 1rem;
|
|
||||||
padding: 1rem;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.recipe-image img {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recipe-info {
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-recipe-card {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background-color: #f8f8f8;
|
|
||||||
border: 2px dashed #ccc;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@
|
|||||||
|
|
||||||
<div class="text-center mt-4">
|
<div class="text-center mt-4">
|
||||||
<button type="submit" class="btn btn-success btn-lg">Rezept speichern</button>
|
<button type="submit" class="btn btn-success btn-lg">Rezept speichern</button>
|
||||||
<a asp-action="Index" asp-controller="Category" class="btn btn-secondary btn-lg ml-2">Abbrechen</a>
|
<a asp-action="Index" asp-controller="Home" class="btn btn-secondary btn-lg ml-2">Abbrechen</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
Instructions = Model.Instructions.ToList()
|
Instructions = Model.Instructions.ToList()
|
||||||
})
|
})
|
||||||
|
|
||||||
<form hx-post="@($"/{Model.Id}/Delete")"
|
<form hx-delete="@($"/{Model.Id}/Delete")"
|
||||||
hx-confirm="Sind Sie sicher, dass Sie dieses Rezept löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden."
|
hx-confirm="Sind Sie sicher, dass Sie dieses Rezept löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden."
|
||||||
hx-redirect="/">
|
hx-redirect="/">
|
||||||
@Html.AntiForgeryToken()
|
@Html.AntiForgeryToken()
|
||||||
@@ -173,34 +173,37 @@
|
|||||||
return Math.round(quantity * 10) / 10;
|
return Math.round(quantity * 10) / 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.addSelectedIngredientsToShoppingList = function(recipeIdParam) {
|
||||||
|
const idToUse = recipeIdParam || recipeId;
|
||||||
|
|
||||||
window.addSelectedIngredientsToShoppingList = function(recipeIdParam) {
|
var form = document.getElementById('ingredient-form');
|
||||||
const idToUse = recipeIdParam || recipeId;
|
var formData = new FormData(form);
|
||||||
|
var selectedIngredientIds = formData.getAll('ingredientIds');
|
||||||
|
|
||||||
var form = document.getElementById('ingredient-form');
|
fetch('/ShoppingList/CreateOrAddIngredients', {
|
||||||
var formData = new FormData(form);
|
method: 'POST',
|
||||||
var selectedIngredientIds = formData.getAll('ingredientIds');
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ recipeId: idToUse, ingredientIds: selectedIngredientIds })
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
throw new Error('Failed to update shopping list');
|
||||||
|
})
|
||||||
|
.then(result => {
|
||||||
|
localStorage.setItem('shoppingListId', result.shoppingListId);
|
||||||
|
alert('Einkaufsliste aktualisiert.');
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert('Fehler beim Aktualisieren der Einkaufsliste.');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
var token = document.querySelector('input[name="__RequestVerificationToken"]').value;
|
|
||||||
|
|
||||||
var response = await fetch('/ShoppingList/CreateOrAddIngredients', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'RequestVerificationToken': token
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ recipeId: '@Model.Id', ingredientIds: selectedIngredientIds })
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
var result = await response.json();
|
|
||||||
localStorage.setItem('shoppingListId', result.shoppingListId);
|
|
||||||
alert('Zutaten wurden zur Einkaufsliste hinzugefügt.');
|
|
||||||
} else {
|
|
||||||
alert('Fehler beim Hinzufügen zur Einkaufsliste.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe
|
@model Francesco.Recipes.World.Models.IFavoritable
|
||||||
|
|
||||||
<form hx-post="@Url.Action(Model.IsFavorite ? "RemoveFavorite" : "AddFavorite", "Recipe")"
|
<form hx-post="@Url.Action(Model.IsFavorite ? "RemoveFavorite" : "AddFavorite", "Recipe")"
|
||||||
hx-target="this"
|
hx-target="this"
|
||||||
|
|||||||
@@ -36,3 +36,4 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script src="~/js/site.js" asp-append-version="true" defer></script>
|
||||||
|
|||||||
@@ -21,13 +21,9 @@
|
|||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
<button type="button" class="btn-delete" onclick="Francesco.removeIngredient('@Model.Ingredients[i].Id')">🗑️</button>
|
<button type="button" class="btn-delete" onclick="Francesco.removeIngredient('@Model.Ingredients[i].Id')">🗑️</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="button" class="btn-add" onclick="Francesco.addIngredient()">Zutat hinzufügen</button>
|
<button type="button" class="btn-add" onclick="Francesco.addIngredient()">Zutat hinzufügen</button>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -23,17 +23,12 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Startseite</a>
|
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Startseite</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link text-dark" asp-area="" asp-controller="Recipes" asp-action="Index">Rezepte</a>
|
<a class="nav-link text-dark" asp-area="" asp-controller="ShoppingList" asp-action="Details">Einkaufsliste</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link text-dark" asp-area="" asp-controller="Category" asp-action="Index">Kategorien</a>
|
<a class="nav-link text-dark" asp-area="" asp-controller="Favorite" asp-action="Index">Favoriten</a>
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link text-dark" asp-area="" asp-controller="ShoppingList" asp-action="Index">Einkaufsliste</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link text-dark" asp-area="" asp-controller="Favorites" asp-action="Index">Favoriten</a>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
|
|||||||
@@ -413,3 +413,4 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
})(window, document);
|
})(window, document);
|
||||||
|
console.log("Francesco-Objekt initialisiert:", window.Francesco);
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<RootNamespace>FrancescosRecipeWorld_Mock</RootNamespace>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.11" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||||
|
<PackageReference Include="Moq" Version="4.20.72" />
|
||||||
|
<PackageReference Include="MSTest" Version="3.6.4" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Francesco.Recipes.World\Francesco.Recipes.World.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
using Francesco.Recipes.World.Controller.Recipe;
|
||||||
|
using Francesco.Recipes.World.Data;
|
||||||
|
using Francesco.Recipes.World.Models;
|
||||||
|
using Francesco.Recipes.World.Models.BackendModels.Instruction;
|
||||||
|
using Francesco.Recipes.World.Models.BackendModels.MediaFile;
|
||||||
|
using Francesco.Recipes.World.Models.BackendModels.Recipe;
|
||||||
|
using Francesco.Recipes.World.Models.BackendModels.RecipeIngredient;
|
||||||
|
using Francesco.Recipes.World.Repositories.Category;
|
||||||
|
using Francesco.Recipes.World.Repositories.Favorit;
|
||||||
|
using Francesco.Recipes.World.Repositories.Ingredient;
|
||||||
|
using Francesco.Recipes.World.Repositories.Instruction;
|
||||||
|
using Francesco.Recipes.World.Repositories.MediaFile;
|
||||||
|
using Francesco.Recipes.World.Repositories.Recipe;
|
||||||
|
using Francesco.Recipes.World.Repositories.Unit;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Moq;
|
||||||
|
|
||||||
|
namespace FrancescosRecipeWorld_Mock
|
||||||
|
{
|
||||||
|
[TestClass]
|
||||||
|
public sealed class FrancescoDamicoUnittest2
|
||||||
|
{
|
||||||
|
private Mock<IFavoriteRepository> _mockFavoriteRepository;
|
||||||
|
private Mock<IRecipeRepository> _mockRecipeRepository;
|
||||||
|
private RecipeController _recipeController;
|
||||||
|
|
||||||
|
[TestInitialize]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
_mockFavoriteRepository = new Mock<IFavoriteRepository>();
|
||||||
|
_mockRecipeRepository = new Mock<IRecipeRepository>();
|
||||||
|
|
||||||
|
var options = new DbContextOptionsBuilder<FrancescosRecipesWorldDbContext>()
|
||||||
|
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||||
|
.Options;
|
||||||
|
|
||||||
|
_recipeController = new RecipeController(
|
||||||
|
_mockRecipeRepository.Object,
|
||||||
|
Mock.Of<IUnitRepository>(),
|
||||||
|
Mock.Of<ICategoryRepository>(),
|
||||||
|
Mock.Of<IIngredientRepository>(),
|
||||||
|
Mock.Of<IMediaFileRepository>(),
|
||||||
|
Mock.Of<IInstructionRepository>(),
|
||||||
|
_mockFavoriteRepository.Object,
|
||||||
|
new FrancescosRecipesWorldDbContext(options)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test 1: AddFavorite(Guid recipeId) -> PartialViewResult mit FavoriteButtonViewModel
|
||||||
|
/// Parameter: recipeId (Guid)
|
||||||
|
/// Rückgabewert: PartialViewResult
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public async Task FrancescoDamico_UnitTest1()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var recipeId = Guid.NewGuid();
|
||||||
|
_mockFavoriteRepository
|
||||||
|
.Setup(x => x.AddFavoriteAsync(recipeId))
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _recipeController.AddFavorite(recipeId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsNotNull(result);
|
||||||
|
Assert.IsInstanceOfType(result, typeof(PartialViewResult));
|
||||||
|
|
||||||
|
var partialViewResult = result as PartialViewResult;
|
||||||
|
Assert.AreEqual("_FavoriteButton", partialViewResult.ViewName);
|
||||||
|
Assert.IsInstanceOfType(partialViewResult.Model, typeof(FavoritButtonViewModel));
|
||||||
|
|
||||||
|
var model = partialViewResult.Model as FavoritButtonViewModel;
|
||||||
|
Assert.AreEqual(recipeId, model.Id);
|
||||||
|
Assert.IsTrue(model.IsFavorite);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test 2: RemoveFavorite(Guid recipeId) -> PartialViewResult mit FavoriteButtonViewModel
|
||||||
|
/// Parameter: recipeId (Guid)
|
||||||
|
/// Rückgabewert: PartialViewResult
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public async Task FrancescoDamico_UnitTest2()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var recipeId = Guid.NewGuid();
|
||||||
|
_mockFavoriteRepository
|
||||||
|
.Setup(x => x.RemoveFavoriteAsync(recipeId))
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _recipeController.RemoveFavorite(recipeId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsNotNull(result);
|
||||||
|
Assert.IsInstanceOfType(result, typeof(PartialViewResult));
|
||||||
|
|
||||||
|
var partialViewResult = result as PartialViewResult;
|
||||||
|
Assert.AreEqual("_FavoriteButton", partialViewResult.ViewName);
|
||||||
|
Assert.IsInstanceOfType(partialViewResult.Model, typeof(FavoritButtonViewModel));
|
||||||
|
|
||||||
|
var model = partialViewResult.Model as FavoritButtonViewModel;
|
||||||
|
Assert.AreEqual(recipeId, model.Id);
|
||||||
|
Assert.IsFalse(model.IsFavorite);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test 3: Details(Guid recipeId) -> ViewResult mit Recipe Modell
|
||||||
|
/// Parameter: recipeId (Guid)
|
||||||
|
/// Rückgabewert: ViewResult mit Recipe-Objekt
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public async Task FrancescoDamico_UnitTest3()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var recipeId = Guid.NewGuid();
|
||||||
|
var expectedRecipe = new Recipe
|
||||||
|
{
|
||||||
|
Id = recipeId,
|
||||||
|
Name = "Spaghetti Carbonara",
|
||||||
|
Description = "Klassisches italienisches Pasta-Gericht",
|
||||||
|
Difficulty = Difficulty.Easy,
|
||||||
|
Servings = 4,
|
||||||
|
PreparationTime = new TimeSpan(0, 10, 0),
|
||||||
|
CookingTime = new TimeSpan(0, 20, 0),
|
||||||
|
IsFavorite = false,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
RecipeIngredients = new List<RecipeIngredient>(),
|
||||||
|
Instructions = new List<Instruction>(),
|
||||||
|
MediaFiles = new List<MediaFile>()
|
||||||
|
};
|
||||||
|
|
||||||
|
_mockRecipeRepository
|
||||||
|
.Setup(x => x.GetRecipeByIdAsync(recipeId))
|
||||||
|
.ReturnsAsync(expectedRecipe);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _recipeController.Details(recipeId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsNotNull(result);
|
||||||
|
Assert.IsInstanceOfType(result, typeof(ViewResult));
|
||||||
|
|
||||||
|
var viewResult = result as ViewResult;
|
||||||
|
Assert.IsInstanceOfType(viewResult.Model, typeof(Recipe));
|
||||||
|
|
||||||
|
var model = viewResult.Model as Recipe;
|
||||||
|
Assert.AreEqual(recipeId, model.Id);
|
||||||
|
Assert.AreEqual("Spaghetti Carbonara", model.Name);
|
||||||
|
Assert.AreEqual(Difficulty.Easy, model.Difficulty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user