From 235196f8e5faab9408cd762f7d58ab90c1b4cb0a Mon Sep 17 00:00:00 2001 From: Harrison Deng Date: Tue, 25 May 2021 18:09:06 -0500 Subject: [PATCH] Added search and results profile persistence when logged in. --- .../Framework/ProductListing.cs | 15 -- .../Models/ResultProfileExtensions.cs} | 6 +- .../Models/SearchProfileExtensions.cs | 9 + src/MultiShop/Client/Listing/ListingView.cs | 26 ++ src/MultiShop/Client/Listing/TableView.razor | 102 ++++++++ src/MultiShop/Client/Listing/Views.cs | 7 + src/MultiShop/Client/Pages/Search.razor | 130 +++------- src/MultiShop/Client/Pages/Search.razor.cs | 159 +++++++----- src/MultiShop/Client/Pages/Search.razor.css | 6 - src/MultiShop/Client/Program.cs | 1 - .../Client/Shared/DragAndDropList.razor | 2 +- src/MultiShop/Client/Shared/NavMenu.razor | 2 +- .../Client/Shared/ToggleableButton.razor | 28 ++ src/MultiShop/Client/wwwroot/css/app.css | 6 + .../Server/Controllers/ProfileController.cs | 63 +++++ .../PublicApiSettingsController.cs | 6 +- .../Controllers/ShopModulesController.cs | 8 +- .../Server/Data/ApplicationDbContext.cs | 35 ++- ... 20210525224658_InitialCreate.Designer.cs} | 243 +++++++++++++----- ...ema.cs => 20210525224658_InitialCreate.cs} | 74 +++++- .../ApplicationDbContextModelSnapshot.cs | 239 ++++++++++++----- .../Server/Models/ApplicationUser.cs | 13 +- src/MultiShop/Server/MultiShop.Server.csproj | 1 + src/MultiShop/Server/Startup.cs | 11 +- .../MultiShop.Shop.AliExpressModule.dll | Bin 26112 -> 26112 bytes .../modules/MultiShop.Shop.BanggoodModule.dll | Bin 12800 -> 12800 bytes .../Shared/Models/ProductListingInfo.cs | 72 ++++++ .../Shared/{ => Models}/ResultsProfile.cs | 16 +- src/MultiShop/Shared/Models/SearchProfile.cs | 119 +++++++++ src/MultiShop/Shared/ProductListingInfo.cs | 25 -- src/MultiShop/Shared/SearchProfile.cs | 89 ------- 31 files changed, 1047 insertions(+), 466 deletions(-) rename src/MultiShop/{Shared/ResultCategoryExtensions.cs => Client/Extensions/Models/ResultProfileExtensions.cs} (92%) create mode 100644 src/MultiShop/Client/Extensions/Models/SearchProfileExtensions.cs create mode 100644 src/MultiShop/Client/Listing/ListingView.cs create mode 100644 src/MultiShop/Client/Listing/TableView.razor create mode 100644 src/MultiShop/Client/Listing/Views.cs create mode 100644 src/MultiShop/Client/Shared/ToggleableButton.razor create mode 100644 src/MultiShop/Server/Controllers/ProfileController.cs rename src/MultiShop/Server/Data/Migrations/{00000000000000_CreateIdentitySchema.Designer.cs => 20210525224658_InitialCreate.Designer.cs} (76%) rename src/MultiShop/Server/Data/Migrations/{00000000000000_CreateIdentitySchema.cs => 20210525224658_InitialCreate.cs} (77%) create mode 100644 src/MultiShop/Shared/Models/ProductListingInfo.cs rename src/MultiShop/Shared/{ => Models}/ResultsProfile.cs (56%) create mode 100644 src/MultiShop/Shared/Models/SearchProfile.cs delete mode 100644 src/MultiShop/Shared/ProductListingInfo.cs delete mode 100644 src/MultiShop/Shared/SearchProfile.cs diff --git a/src/MultiShop.Shop/Framework/ProductListing.cs b/src/MultiShop.Shop/Framework/ProductListing.cs index 94d3fb9..12c0f2d 100644 --- a/src/MultiShop.Shop/Framework/ProductListing.cs +++ b/src/MultiShop.Shop/Framework/ProductListing.cs @@ -12,20 +12,5 @@ namespace MultiShop.Shop.Framework public int? PurchaseCount { get; set; } public int? ReviewCount { get; set; } public bool ConvertedPrices { get; set; } - - public override bool Equals(object obj) - { - if (obj == null || GetType() != obj.GetType()) - { - return false; - } - ProductListing b = (ProductListing)obj; - return this.URL == b.URL; - } - - public override int GetHashCode() - { - return URL.GetHashCode(); - } } } \ No newline at end of file diff --git a/src/MultiShop/Shared/ResultCategoryExtensions.cs b/src/MultiShop/Client/Extensions/Models/ResultProfileExtensions.cs similarity index 92% rename from src/MultiShop/Shared/ResultCategoryExtensions.cs rename to src/MultiShop/Client/Extensions/Models/ResultProfileExtensions.cs index 1ab77fe..ae315c6 100644 --- a/src/MultiShop/Shared/ResultCategoryExtensions.cs +++ b/src/MultiShop/Client/Extensions/Models/ResultProfileExtensions.cs @@ -1,8 +1,10 @@ using System; +using MultiShop.Shared; +using MultiShop.Shared.Models; -namespace MultiShop.Shared +namespace MultiShop.Client.Extensions.Models { - public static class ResultCategoryExtensions + public static class ResultProfileExtensions { public static int? CompareListings(this ResultsProfile.Category category, ProductListingInfo a, ProductListingInfo b) { diff --git a/src/MultiShop/Client/Extensions/Models/SearchProfileExtensions.cs b/src/MultiShop/Client/Extensions/Models/SearchProfileExtensions.cs new file mode 100644 index 0000000..10c9ae8 --- /dev/null +++ b/src/MultiShop/Client/Extensions/Models/SearchProfileExtensions.cs @@ -0,0 +1,9 @@ +using MultiShop.Shared.Models; + +namespace MultiShop.Client.Extensions.Models +{ + public static class SearchProfileExtensions + { + + } +} \ No newline at end of file diff --git a/src/MultiShop/Client/Listing/ListingView.cs b/src/MultiShop/Client/Listing/ListingView.cs new file mode 100644 index 0000000..bbe13ec --- /dev/null +++ b/src/MultiShop/Client/Listing/ListingView.cs @@ -0,0 +1,26 @@ +using System.Collections; +using System.Collections.Generic; +using Microsoft.AspNetCore.Components; +using MultiShop.Client.Pages; +using MultiShop.Shared.Models; + +namespace MultiShop.Client.Listing +{ + public abstract class ListingView : ComponentBase + { + public abstract Views View { get; } + + [Parameter] + public IList Listings { get; set; } + + [Parameter] + public Search.Status Status { get; set; } + + private protected abstract string GetCategoryTag(ResultsProfile.Category category); + + public ListingView(Search.Status status) + { + this.Status = status; + } + } +} \ No newline at end of file diff --git a/src/MultiShop/Client/Listing/TableView.razor b/src/MultiShop/Client/Listing/TableView.razor new file mode 100644 index 0000000..0bcbe02 --- /dev/null +++ b/src/MultiShop/Client/Listing/TableView.razor @@ -0,0 +1,102 @@ +@using MultiShop.Shared.Models +@using Pages +@inherits ListingView + +
+ + + + + + + + + + + + + @if (!Status.SearchConfiguring && !Status.Searching) + { + + + + + + + + + + + + + + } +
NamePriceShippingPurchasesRatingReviews
+
@product.Listing.Name
+ From @product.ShopName + @if (product.Listing.ConvertedPrices) + { + Converted price + } + @foreach (ResultsProfile.Category c in product.Tops) + { + @GetCategoryTag(c) + } +
+ @if (product.Listing.UpperPrice != product.Listing.LowerPrice) + { +
+ @product.Listing.LowerPrice to @product.Listing.UpperPrice +
+ } + else + { +
+ @GetOrNA(product.Listing.LowerPrice) +
+ } +
+
+ @GetOrNA(product.Listing.Shipping) +
+
+
+ @GetOrNA(product.Listing.PurchaseCount) +
+
+
+ @(product.Listing.Rating != null ? string.Format("{0:P2}", product.Listing.Rating) : "N/A") +
+
@GetOrNA(product.Listing.ReviewCount) + View +
+
+ +@code { + public override Views View => Views.Table; + + public TableView(Search.Status status) : base(status) + { + } + + private protected override string GetCategoryTag(ResultsProfile.Category c) + { + switch (c) + { + case ResultsProfile.Category.RatingPriceRatio: + return "Best rating to price ratio"; + case ResultsProfile.Category.Price: + return "Lowest price"; + case ResultsProfile.Category.Purchases: + return "Most purchases"; + case ResultsProfile.Category.Reviews: + return "Most reviews"; + } + throw new ArgumentException($"{c} does not have an associated string."); + } + + private string GetOrNA(object data, string prepend = null, string append = null) + { + return data != null ? (prepend + data.ToString() + append) : "N/A"; + } +} \ No newline at end of file diff --git a/src/MultiShop/Client/Listing/Views.cs b/src/MultiShop/Client/Listing/Views.cs new file mode 100644 index 0000000..18ea232 --- /dev/null +++ b/src/MultiShop/Client/Listing/Views.cs @@ -0,0 +1,7 @@ +namespace MultiShop.Client.Listing +{ + public enum Views + { + Table + } +} \ No newline at end of file diff --git a/src/MultiShop/Client/Pages/Search.razor b/src/MultiShop/Client/Pages/Search.razor index c66f0e9..027bca2 100644 --- a/src/MultiShop/Client/Pages/Search.razor +++ b/src/MultiShop/Client/Pages/Search.razor @@ -1,19 +1,16 @@ @page "/search/{Query?}" -@using Microsoft.Extensions.Configuration -@using MultiShop.Shared -@inject HttpClient Http -@inject IConfiguration Configuration -@inject IJSRuntime js + +@using MultiShop.Client.Extensions.Models
- +
- - + +
- @if (showSearchConfiguration) + @if (status.SearchConfiguring) {

Configuration

@@ -24,8 +21,8 @@
How many results from each store?

This is the maximum number of results we gather for each store we have access to. The larger the result, the longer it takes to load search queries.

- - + +
@@ -38,10 +35,10 @@
- @foreach (Currency currency in Enum.GetValues()) { - @if (currency == activeProfile.currency) + @if (currency == activeSearchProfile.Currency) { } @@ -60,11 +57,11 @@
We'll crop out the lower rated stuff.

We'll only show products that have a rating greater than or equal to the set minimum rating. Optionally, we can also show those that don't have rating.

- - + +
- +
@@ -77,11 +74,11 @@
- +
Upper limit
- +
.00
@@ -90,7 +87,7 @@
Lower limit
- +
.00
@@ -105,7 +102,7 @@ @foreach (string shop in Shops.Keys) {
- +
} @@ -120,10 +117,10 @@
Minimum purchases
- +
- +
@@ -137,10 +134,10 @@
Minimum reviews
- +
- +
@@ -153,17 +150,17 @@
- + Max shipping
- +
.00
- +
@@ -175,9 +172,9 @@
- +
- @if (showResultsConfiguration) + @if (status.ResultsConfiguring) {
@@ -197,7 +194,7 @@
- @if (searching) + @if (status.Searching) { @if (listings.Count != 0) { @@ -216,7 +213,7 @@ } else if (listings.Count != 0) { - @if (organizing) + @if (status.Organizing) {
Loading... @@ -228,7 +225,7 @@ Looked through @resultsChecked listings and found @listings.Count viable results. } } - else if (searched) + else if (status.Searched) { We've found @resultsChecked listings and unfortunately none matched your search. } @@ -241,74 +238,7 @@
@if (listings.Count > 0) { -
- - - - - - - - - - - - - @if (!showSearchConfiguration && !searching) { - - - - - - - - - - - - - - } -
NamePriceShippingPurchasesRatingReviews
-
@product.Listing.Name
- From @product.ShopName - @if (product.Listing.ConvertedPrices) - { - Converted price - } - @foreach (ResultsProfile.Category c in product.Tops) - { - @CategoryTags(c) - } -
- @if (product.Listing.UpperPrice != product.Listing.LowerPrice) - { -
- @product.Listing.LowerPrice to @product.Listing.UpperPrice -
- } - else - { -
- @GetOrNA(product.Listing.LowerPrice) -
- } -
-
- @GetOrNA(product.Listing.Shipping) -
-
-
- @GetOrNA(product.Listing.PurchaseCount) -
-
-
- @(product.Listing.Rating != null ? string.Format("{0:P2}", product.Listing.Rating) : "N/A") -
-
@GetOrNA(product.Listing.ReviewCount) - View -
-
+ @listingViews[CurrentView] }
diff --git a/src/MultiShop/Client/Pages/Search.razor.cs b/src/MultiShop/Client/Pages/Search.razor.cs index ce941fd..1674874 100644 --- a/src/MultiShop/Client/Pages/Search.razor.cs +++ b/src/MultiShop/Client/Pages/Search.razor.cs @@ -1,71 +1,95 @@ using System; using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Components; -using MultiShop.Shared; +using Microsoft.AspNetCore.Components.Authorization; +using MultiShop.Client.Extensions.Models; +using MultiShop.Client.Listing; +using MultiShop.Shared.Models; using MultiShop.Shop.Framework; using SimpleLogger; namespace MultiShop.Client.Pages { - public partial class Search + public partial class Search : IAsyncDisposable { + [CascadingParameter] + Task AuthenticationStateTask { get; set; } + [CascadingParameter(Name = "Shops")] public Dictionary Shops { get; set; } [Parameter] public string Query { get; set; } - private SearchProfile activeProfile = new SearchProfile(); - private ResultsProfile activeResultsProfile = new ResultsProfile(); + [Inject] + private HttpClient Http { get; set; } - private bool showSearchConfiguration = false; - private bool showResultsConfiguration = false; + private Status status = new Status(); - private string ToggleSearchConfigButtonCss - { - get => "btn btn-outline-secondary" + (showSearchConfiguration ? " active" : ""); - } + private Dictionary listingViews; - private string ToggleResultsConfigurationcss { - get => "btn btn-outline-secondary btn-tab" + (showResultsConfiguration ? " active" : ""); - } + private Views CurrentView = Views.Table; - private bool searched = false; - private bool searching = false; - private bool organizing = false; + private SearchProfile activeSearchProfile; + private ResultsProfile activeResultsProfile; - private int resultsChecked = 0; private List listings = new List(); - - protected override void OnInitialized() - { - foreach (string shop in Shops.Keys) - { - activeProfile.shopStates[shop] = true; - } - base.OnInitialized(); - } + private int resultsChecked = 0; protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); + + AuthenticationState authState = await AuthenticationStateTask; + + listingViews = new Dictionary() { + {Views.Table, new TableView(status)} + }; + + if (authState.User.Identity.IsAuthenticated) { + Logger.Log($"User \"{authState.User.Identity.Name}\" is authenticated. Checking for saved profiles.", LogLevel.Debug); + HttpResponseMessage searchProfileResponse = await Http.GetAsync("Profile/Search"); + if (searchProfileResponse.IsSuccessStatusCode) { + activeSearchProfile = await searchProfileResponse.Content.ReadFromJsonAsync(); + Logger.Log("Received: " + await searchProfileResponse.Content.ReadAsStringAsync()); + Logger.Log("Serialized then deserialized: " + JsonSerializer.Serialize(activeSearchProfile)); + } else { + Logger.Log("Could not load search profile from server. Using default.", LogLevel.Warning); + activeSearchProfile = new SearchProfile(); + } + + HttpResponseMessage resultsProfileResponse = await Http.GetAsync("Profile/Results"); + if (resultsProfileResponse.IsSuccessStatusCode) { + activeResultsProfile = await resultsProfileResponse.Content.ReadFromJsonAsync(); + } else { + Logger.Log("Could not load results profile from server.", LogLevel.Debug); + activeResultsProfile = new ResultsProfile(); + } + } else { + activeSearchProfile = new SearchProfile(); + activeResultsProfile = new ResultsProfile(); + } + activeSearchProfile.ShopStates.TotalShops = Shops.Count; } protected override async Task OnParametersSetAsync() { + await base.OnParametersSetAsync(); if (!string.IsNullOrEmpty(Query)) { await PerformSearch(Query); } - await base.OnParametersSetAsync(); } private async Task PerformSearch(string query) { if (string.IsNullOrWhiteSpace(query)) return; - if (searching) return; - searching = true; + if (status.Searching) return; + status.Searching = true; Logger.Log($"Received search request for \"{query}\".", LogLevel.Debug); resultsChecked = 0; listings.Clear(); @@ -73,10 +97,10 @@ namespace MultiShop.Client.Pages List>(); foreach (string shopName in Shops.Keys) { - if (activeProfile.shopStates[shopName]) + if (activeSearchProfile.ShopStates[shopName]) { Logger.Log($"Querying \"{shopName}\" for products."); - Shops[shopName].SetupSession(query, activeProfile.currency); + Shops[shopName].SetupSession(query, activeSearchProfile.Currency); int shopViableResults = 0; await foreach (ProductListing listing in Shops[shopName]) { @@ -88,12 +112,12 @@ namespace MultiShop.Client.Pages } - if (listing.Shipping == null && !activeProfile.keepUnknownShipping || (activeProfile.enableMaxShippingFee && listing.Shipping > activeProfile.MaxShippingFee)) continue; + if (listing.Shipping == null && !activeSearchProfile.KeepUnknownShipping || (activeSearchProfile.EnableMaxShippingFee && listing.Shipping > activeSearchProfile.MaxShippingFee)) continue; float shippingDifference = listing.Shipping != null ? listing.Shipping.Value : 0; - if (!(listing.LowerPrice + shippingDifference >= activeProfile.lowerPrice && (!activeProfile.enableUpperPrice || listing.UpperPrice + shippingDifference <= activeProfile.UpperPrice))) continue; - if ((listing.Rating == null && !activeProfile.keepUnrated) && activeProfile.minRating > (listing.Rating == null ? 0 : listing.Rating)) continue; - if ((listing.PurchaseCount == null && !activeProfile.keepUnknownPurchaseCount) || activeProfile.minPurchases > (listing.PurchaseCount == null ? 0 : listing.PurchaseCount)) continue; - if ((listing.ReviewCount == null && !activeProfile.keepUnknownRatingCount) || activeProfile.minReviews > (listing.ReviewCount == null ? 0 : listing.ReviewCount)) continue; + if (!(listing.LowerPrice + shippingDifference >= activeSearchProfile.LowerPrice && (!activeSearchProfile.EnableUpperPrice || listing.UpperPrice + shippingDifference <= activeSearchProfile.UpperPrice))) continue; + if ((listing.Rating == null && !activeSearchProfile.KeepUnrated) && activeSearchProfile.MinRating > (listing.Rating == null ? 0 : listing.Rating)) continue; + if ((listing.PurchaseCount == null && !activeSearchProfile.KeepUnknownPurchaseCount) || activeSearchProfile.MinPurchases > (listing.PurchaseCount == null ? 0 : listing.PurchaseCount)) continue; + if ((listing.ReviewCount == null && !activeSearchProfile.KeepUnknownRatingCount) || activeSearchProfile.MinReviews > (listing.ReviewCount == null ? 0 : listing.ReviewCount)) continue; ProductListingInfo info = new ProductListingInfo(listing, shopName); listings.Add(info); @@ -119,7 +143,7 @@ namespace MultiShop.Client.Pages } shopViableResults += 1; - if (shopViableResults >= activeProfile.maxResults) break; + if (shopViableResults >= activeSearchProfile.MaxResults) break; } Logger.Log($"\"{shopName}\" has completed. There are {listings.Count} results in total.", LogLevel.Debug); } @@ -128,8 +152,8 @@ namespace MultiShop.Client.Pages Logger.Log($"Skipping {shopName} since it's disabled."); } } - searching = false; - searched = true; + status.Searching = false; + status.Searched = true; int tagsAdded = 0; foreach (ResultsProfile.Category c in greatest.Keys) @@ -145,10 +169,10 @@ namespace MultiShop.Client.Pages await Organize(activeResultsProfile.Order); } - private async Task Organize(List order) + private async Task Organize(IList order) { - if (searching || listings.Count <= 1) return; - organizing = true; + if (status.Searching || listings.Count <= 1) return; + status.Organizing = true; Comparison comparer = (a, b) => { foreach (ResultsProfile.Category category in activeResultsProfile.Order) @@ -162,13 +186,15 @@ namespace MultiShop.Client.Pages return 0; }; - Func<(int, int), Task> partition = async (ilh) => { + Func<(int, int), Task> partition = async (ilh) => + { ProductListingInfo swapTemp; ProductListingInfo pivot = listings[ilh.Item2]; int lastSwap = ilh.Item1 - 1; for (int j = ilh.Item1; j <= ilh.Item2 - 1; j++) { - if (comparer.Invoke(listings[j], pivot) <= 0) { + if (comparer.Invoke(listings[j], pivot) <= 0) + { lastSwap += 1; swapTemp = listings[lastSwap]; listings[lastSwap] = listings[j]; @@ -176,26 +202,29 @@ namespace MultiShop.Client.Pages } await Task.Yield(); } - swapTemp = listings[lastSwap+1]; - listings[lastSwap+1] = listings[ilh.Item2]; + swapTemp = listings[lastSwap + 1]; + listings[lastSwap + 1] = listings[ilh.Item2]; listings[ilh.Item2] = swapTemp; return lastSwap + 1; }; - Func<(int, int), Task> quickSort = async (ilh) => { + Func<(int, int), Task> quickSort = async (ilh) => + { Stack<(int, int)> iterativeStack = new Stack<(int, int)>(); iterativeStack.Push(ilh); - + while (iterativeStack.Count > 0) { (int, int) lh = iterativeStack.Pop(); int p = await partition.Invoke((lh.Item1, lh.Item2)); - if (p - 1 > lh.Item1) { + if (p - 1 > lh.Item1) + { iterativeStack.Push((lh.Item1, p - 1)); } - if (p + 1 < lh.Item2) { + if (p + 1 < lh.Item2) + { iterativeStack.Push((p + 1, lh.Item2)); } @@ -206,30 +235,26 @@ namespace MultiShop.Client.Pages await quickSort((0, listings.Count - 1)); - organizing = false; + status.Organizing = false; StateHasChanged(); } - - private string GetOrNA(object data, string prepend = null, string append = null) + public async ValueTask DisposeAsync() { - return data != null ? (prepend + data.ToString() + append) : "N/A"; + AuthenticationState authState = await AuthenticationStateTask; + if (authState.User.Identity.IsAuthenticated) { + await Http.PutAsJsonAsync("Profile/Search", activeSearchProfile); + await Http.PutAsJsonAsync("Profile/Results", activeResultsProfile); + } } - private string CategoryTags(ResultsProfile.Category c) + public class Status { - switch (c) - { - case ResultsProfile.Category.RatingPriceRatio: - return "Best rating to price ratio"; - case ResultsProfile.Category.Price: - return "Lowest price"; - case ResultsProfile.Category.Purchases: - return "Most purchases"; - case ResultsProfile.Category.Reviews: - return "Most reviews"; - } - throw new ArgumentException($"{c} does not have an associated string."); + public bool SearchConfiguring { get; set; } + public bool ResultsConfiguring { get; set; } + public bool Organizing { get; set; } + public bool Searching { get; set; } + public bool Searched { get; set; } } } } \ No newline at end of file diff --git a/src/MultiShop/Client/Pages/Search.razor.css b/src/MultiShop/Client/Pages/Search.razor.css index 39685a7..8db9f50 100644 --- a/src/MultiShop/Client/Pages/Search.razor.css +++ b/src/MultiShop/Client/Pages/Search.razor.css @@ -4,10 +4,4 @@ tbody > tr > th > div { .table.table thead th { border-top-style: none; -} - -.btn.btn-tab { - border-bottom-style: none; - border-bottom-left-radius: 0em; - border-bottom-right-radius: 0em; } \ No newline at end of file diff --git a/src/MultiShop/Client/Program.cs b/src/MultiShop/Client/Program.cs index d7b3c3c..b579d30 100644 --- a/src/MultiShop/Client/Program.cs +++ b/src/MultiShop/Client/Program.cs @@ -22,7 +22,6 @@ namespace MultiShop.Client builder.Services.AddHttpClient("MultiShop.ServerAPI", client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)).AddHttpMessageHandler(); - // Supply HttpClient instances that include access tokens when making requests to the server project builder.Services.AddScoped(sp => sp.GetRequiredService().CreateClient("MultiShop.ServerAPI")); Action configureClient = client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress); diff --git a/src/MultiShop/Client/Shared/DragAndDropList.razor b/src/MultiShop/Client/Shared/DragAndDropList.razor index 73a6d73..59a776f 100644 --- a/src/MultiShop/Client/Shared/DragAndDropList.razor +++ b/src/MultiShop/Client/Shared/DragAndDropList.razor @@ -22,7 +22,7 @@ @code { [Parameter] - public List Items { get; set; } + public IList Items { get; set; } [Parameter] public string AdditionalListClasses { get; set; } diff --git a/src/MultiShop/Client/Shared/NavMenu.razor b/src/MultiShop/Client/Shared/NavMenu.razor index 28c1761..6e3514c 100644 --- a/src/MultiShop/Client/Shared/NavMenu.razor +++ b/src/MultiShop/Client/Shared/NavMenu.razor @@ -36,7 +36,7 @@ Hello, @auth.User.Identity.Name! diff --git a/src/MultiShop/Client/Shared/ToggleableButton.razor b/src/MultiShop/Client/Shared/ToggleableButton.razor new file mode 100644 index 0000000..89d332a --- /dev/null +++ b/src/MultiShop/Client/Shared/ToggleableButton.razor @@ -0,0 +1,28 @@ + + +@code { + [Parameter(CaptureUnmatchedValues = true)] + public IReadOnlyDictionary AdditionalAttributes { get; set; } + + [Parameter] + public RenderFragment ChildContent { get; set; } + + [Parameter] + public EventCallback OnToggleCallback { get; set; } + + private async Task OnClick() { + state = !state; + await OnToggleCallback.InvokeAsync(state); + } + + private bool state; + + private string ButtonClasses + { + get + { + IReadOnlyDictionary t = AdditionalAttributes; + return (state ? "active " : "") + (AdditionalAttributes["class"] as string); + } + } +} \ No newline at end of file diff --git a/src/MultiShop/Client/wwwroot/css/app.css b/src/MultiShop/Client/wwwroot/css/app.css index 2bbae17..3c5a10e 100644 --- a/src/MultiShop/Client/wwwroot/css/app.css +++ b/src/MultiShop/Client/wwwroot/css/app.css @@ -26,3 +26,9 @@ html, body { right: 0.75rem; top: 0.5rem; } + +.btn.btn-tab { + border-bottom-style: none; + border-bottom-left-radius: 0em; + border-bottom-right-radius: 0em; +} \ No newline at end of file diff --git a/src/MultiShop/Server/Controllers/ProfileController.cs b/src/MultiShop/Server/Controllers/ProfileController.cs new file mode 100644 index 0000000..58b2916 --- /dev/null +++ b/src/MultiShop/Server/Controllers/ProfileController.cs @@ -0,0 +1,63 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using MultiShop.Server.Data; +using MultiShop.Server.Models; +using MultiShop.Shared.Models; + +namespace MultiShop.Server.Controllers +{ + + [ApiController] + [Authorize] + [Route("[controller]")] + public class ProfileController : ControllerBase + { + private UserManager userManager; + private ApplicationDbContext dbContext; + public ProfileController(UserManager userManager, ApplicationDbContext dbContext) + { + this.userManager = userManager; + this.dbContext = dbContext; + } + + [HttpGet] + [Route("Search")] + public async Task GetSearchProfile() { + ApplicationUser userModel = await userManager.GetUserAsync(User); + return Ok(userModel.SearchProfile); + } + + [HttpGet] + [Route("Results")] + public async Task GetResultsProfile() { + ApplicationUser userModel = await userManager.GetUserAsync(User); + return Ok(userModel.ResultsProfile); + } + + [HttpPut] + [Route("Search")] + public async Task PutSearchProfile(SearchProfile searchProfile) { + ApplicationUser userModel = await userManager.GetUserAsync(User); + if (userModel.SearchProfile.Id != searchProfile.Id || userModel.Id != searchProfile.ApplicationUserId) { + return BadRequest(); + } + dbContext.Entry(userModel.SearchProfile).CurrentValues.SetValues(searchProfile); + await userManager.UpdateAsync(userModel); + return NoContent(); + } + + [HttpPut] + [Route("Results")] + public async Task PutResultsProfile(ResultsProfile resultsProfile) { + ApplicationUser userModel = await userManager.GetUserAsync(User); + if (userModel.ResultsProfile.Id != resultsProfile.Id) { + return BadRequest(); + } + dbContext.Entry(userModel.ResultsProfile).CurrentValues.SetValues(resultsProfile); + await userManager.UpdateAsync(userModel); + return NoContent(); + } + } +} \ No newline at end of file diff --git a/src/MultiShop/Server/Controllers/PublicApiSettingsController.cs b/src/MultiShop/Server/Controllers/PublicApiSettingsController.cs index 602ff2e..f695b29 100644 --- a/src/MultiShop/Server/Controllers/PublicApiSettingsController.cs +++ b/src/MultiShop/Server/Controllers/PublicApiSettingsController.cs @@ -16,10 +16,10 @@ namespace MultiShop.Server.Controllers } [HttpGet] - public IReadOnlyDictionary GetPublicConfiguration() { - return new Dictionary { + public IActionResult GetPublicConfiguration() { + return Ok(new Dictionary { {"IdentityServer:Registration", configuration["IdentityServer:Registration"]} - }; + }); } } } \ No newline at end of file diff --git a/src/MultiShop/Server/Controllers/ShopModulesController.cs b/src/MultiShop/Server/Controllers/ShopModulesController.cs index 250d309..30976c0 100644 --- a/src/MultiShop/Server/Controllers/ShopModulesController.cs +++ b/src/MultiShop/Server/Controllers/ShopModulesController.cs @@ -22,20 +22,22 @@ namespace MultiShop.Server.Controllers this.shopAssemblyData = new Dictionary(); } - public IEnumerable GetShopModuleNames() { + public IActionResult GetShopModuleNames() { + List moduleNames = new List(); ShopOptions options = configuration.GetSection(ShopOptions.Shop).Get(); foreach (string file in Directory.EnumerateFiles(options.Directory)) { if (Path.GetExtension(file).ToLower().Equals(".dll") && !(options.Disabled != null && options.Disabled.Contains(Path.GetFileNameWithoutExtension(file)))) { - yield return Path.GetFileNameWithoutExtension(file); + moduleNames.Add(Path.GetFileNameWithoutExtension(file)); } } + return Ok(moduleNames); } [HttpGet] [Route("{shopModuleName}")] - public ActionResult GetModule(string shopModuleName) { + public IActionResult GetModule(string shopModuleName) { ShopOptions options = configuration.GetSection(ShopOptions.Shop).Get(); string shopPath = Path.Join(options.Directory, shopModuleName); shopPath += ".dll"; diff --git a/src/MultiShop/Server/Data/ApplicationDbContext.cs b/src/MultiShop/Server/Data/ApplicationDbContext.cs index 70ff868..5afb0c5 100644 --- a/src/MultiShop/Server/Data/ApplicationDbContext.cs +++ b/src/MultiShop/Server/Data/ApplicationDbContext.cs @@ -7,15 +7,44 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using MultiShop.Shared.Models; +using System.Text.Json; +using Microsoft.EntityFrameworkCore.ChangeTracking; namespace MultiShop.Server.Data { public class ApplicationDbContext : ApiAuthorizationDbContext { - public ApplicationDbContext( - DbContextOptions options, - IOptions operationalStoreOptions) : base(options, operationalStoreOptions) + public ApplicationDbContext(DbContextOptions options, IOptions operationalStoreOptions) : base(options, operationalStoreOptions) { } + + protected override void OnModelCreating(ModelBuilder modelBuilder) { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity() + .Property(e => e.Order) + .HasConversion( + v => JsonSerializer.Serialize(v, null), + v => JsonSerializer.Deserialize>(v, null), + new ValueComparer>( + (a, b) => a.SequenceEqual(b), + c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())), + c => (IList) c.ToList() + ) + ); + + modelBuilder.Entity() + .Property(e => e.ShopStates) + .HasConversion( + v => JsonSerializer.Serialize(v, null), + v => JsonSerializer.Deserialize(v, null), + new ValueComparer( + (a, b) => a.Equals(b), + c => c.GetHashCode(), + c => c.Clone() + ) + ); + } } } diff --git a/src/MultiShop/Server/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs b/src/MultiShop/Server/Data/Migrations/20210525224658_InitialCreate.Designer.cs similarity index 76% rename from src/MultiShop/Server/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs rename to src/MultiShop/Server/Data/Migrations/20210525224658_InitialCreate.Designer.cs index 8d883dc..a9b3f2c 100644 --- a/src/MultiShop/Server/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs +++ b/src/MultiShop/Server/Data/Migrations/20210525224658_InitialCreate.Designer.cs @@ -1,86 +1,22 @@ // using System; -using MultiShop.Server.Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MultiShop.Server.Data; namespace MultiShop.Server.Data.Migrations { [DbContext(typeof(ApplicationDbContext))] - [Migration("00000000000000_CreateIdentitySchema")] - partial class CreateIdentitySchema + [Migration("20210525224658_InitialCreate")] + partial class InitialCreate { protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "5.0.0-rc.1.20417.2"); - - modelBuilder.Entity("MultiShop.Server.Models.ApplicationUser", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers"); - }); + .HasAnnotation("ProductVersion", "5.0.6"); modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.DeviceFlowCodes", b => { @@ -317,6 +253,154 @@ namespace MultiShop.Server.Data.Migrations b.ToTable("AspNetUserTokens"); }); + modelBuilder.Entity("MultiShop.Server.Models.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("MultiShop.Shared.Models.ResultsProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ApplicationUserId") + .HasColumnType("TEXT"); + + b.Property("Order") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId") + .IsUnique(); + + b.ToTable("ResultsProfile"); + }); + + modelBuilder.Entity("MultiShop.Shared.Models.SearchProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ApplicationUserId") + .HasColumnType("TEXT"); + + b.Property("Currency") + .HasColumnType("INTEGER"); + + b.Property("EnableMaxShippingFee") + .HasColumnType("INTEGER"); + + b.Property("EnableUpperPrice") + .HasColumnType("INTEGER"); + + b.Property("KeepUnknownPurchaseCount") + .HasColumnType("INTEGER"); + + b.Property("KeepUnknownRatingCount") + .HasColumnType("INTEGER"); + + b.Property("KeepUnknownShipping") + .HasColumnType("INTEGER"); + + b.Property("KeepUnrated") + .HasColumnType("INTEGER"); + + b.Property("LowerPrice") + .HasColumnType("INTEGER"); + + b.Property("MaxResults") + .HasColumnType("INTEGER"); + + b.Property("MaxShippingFee") + .HasColumnType("INTEGER"); + + b.Property("MinPurchases") + .HasColumnType("INTEGER"); + + b.Property("MinRating") + .HasColumnType("REAL"); + + b.Property("MinReviews") + .HasColumnType("INTEGER"); + + b.Property("ShopStates") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpperPrice") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId") + .IsUnique(); + + b.ToTable("SearchProfile"); + }); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => { b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) @@ -367,6 +451,29 @@ namespace MultiShop.Server.Data.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); + + modelBuilder.Entity("MultiShop.Shared.Models.ResultsProfile", b => + { + b.HasOne("MultiShop.Server.Models.ApplicationUser", null) + .WithOne("ResultsProfile") + .HasForeignKey("MultiShop.Shared.Models.ResultsProfile", "ApplicationUserId"); + }); + + modelBuilder.Entity("MultiShop.Shared.Models.SearchProfile", b => + { + b.HasOne("MultiShop.Server.Models.ApplicationUser", null) + .WithOne("SearchProfile") + .HasForeignKey("MultiShop.Shared.Models.SearchProfile", "ApplicationUserId"); + }); + + modelBuilder.Entity("MultiShop.Server.Models.ApplicationUser", b => + { + b.Navigation("ResultsProfile") + .IsRequired(); + + b.Navigation("SearchProfile") + .IsRequired(); + }); #pragma warning restore 612, 618 } } diff --git a/src/MultiShop/Server/Data/Migrations/00000000000000_CreateIdentitySchema.cs b/src/MultiShop/Server/Data/Migrations/20210525224658_InitialCreate.cs similarity index 77% rename from src/MultiShop/Server/Data/Migrations/00000000000000_CreateIdentitySchema.cs rename to src/MultiShop/Server/Data/Migrations/20210525224658_InitialCreate.cs index 0e07926..803a8dc 100644 --- a/src/MultiShop/Server/Data/Migrations/00000000000000_CreateIdentitySchema.cs +++ b/src/MultiShop/Server/Data/Migrations/20210525224658_InitialCreate.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; namespace MultiShop.Server.Data.Migrations { - public partial class CreateIdentitySchema : Migration + public partial class InitialCreate : Migration { protected override void Up(MigrationBuilder migrationBuilder) { @@ -191,6 +191,60 @@ namespace MultiShop.Server.Data.Migrations onDelete: ReferentialAction.Cascade); }); + migrationBuilder.CreateTable( + name: "ResultsProfile", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ApplicationUserId = table.Column(type: "TEXT", nullable: true), + Order = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ResultsProfile", x => x.Id); + table.ForeignKey( + name: "FK_ResultsProfile_AspNetUsers_ApplicationUserId", + column: x => x.ApplicationUserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "SearchProfile", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ApplicationUserId = table.Column(type: "TEXT", nullable: true), + Currency = table.Column(type: "INTEGER", nullable: false), + MaxResults = table.Column(type: "INTEGER", nullable: false), + MinRating = table.Column(type: "REAL", nullable: false), + KeepUnrated = table.Column(type: "INTEGER", nullable: false), + EnableUpperPrice = table.Column(type: "INTEGER", nullable: false), + UpperPrice = table.Column(type: "INTEGER", nullable: false), + LowerPrice = table.Column(type: "INTEGER", nullable: false), + MinPurchases = table.Column(type: "INTEGER", nullable: false), + KeepUnknownPurchaseCount = table.Column(type: "INTEGER", nullable: false), + MinReviews = table.Column(type: "INTEGER", nullable: false), + KeepUnknownRatingCount = table.Column(type: "INTEGER", nullable: false), + EnableMaxShippingFee = table.Column(type: "INTEGER", nullable: false), + MaxShippingFee = table.Column(type: "INTEGER", nullable: false), + KeepUnknownShipping = table.Column(type: "INTEGER", nullable: false), + ShopStates = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SearchProfile", x => x.Id); + table.ForeignKey( + name: "FK_SearchProfile_AspNetUsers_ApplicationUserId", + column: x => x.ApplicationUserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + migrationBuilder.CreateIndex( name: "IX_AspNetRoleClaims_RoleId", table: "AspNetRoleClaims", @@ -253,6 +307,18 @@ namespace MultiShop.Server.Data.Migrations name: "IX_PersistedGrants_SubjectId_SessionId_Type", table: "PersistedGrants", columns: new[] { "SubjectId", "SessionId", "Type" }); + + migrationBuilder.CreateIndex( + name: "IX_ResultsProfile_ApplicationUserId", + table: "ResultsProfile", + column: "ApplicationUserId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_SearchProfile_ApplicationUserId", + table: "SearchProfile", + column: "ApplicationUserId", + unique: true); } protected override void Down(MigrationBuilder migrationBuilder) @@ -278,6 +344,12 @@ namespace MultiShop.Server.Data.Migrations migrationBuilder.DropTable( name: "PersistedGrants"); + migrationBuilder.DropTable( + name: "ResultsProfile"); + + migrationBuilder.DropTable( + name: "SearchProfile"); + migrationBuilder.DropTable( name: "AspNetRoles"); diff --git a/src/MultiShop/Server/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/src/MultiShop/Server/Data/Migrations/ApplicationDbContextModelSnapshot.cs index 8dce4be..6284e00 100644 --- a/src/MultiShop/Server/Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/MultiShop/Server/Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -1,9 +1,9 @@ // using System; -using MultiShop.Server.Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using MultiShop.Server.Data; namespace MultiShop.Server.Data.Migrations { @@ -14,71 +14,7 @@ namespace MultiShop.Server.Data.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "5.0.0-rc.1.20417.2"); - - modelBuilder.Entity("MultiShop.Server.Models.ApplicationUser", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers"); - }); + .HasAnnotation("ProductVersion", "5.0.6"); modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.DeviceFlowCodes", b => { @@ -315,6 +251,154 @@ namespace MultiShop.Server.Data.Migrations b.ToTable("AspNetUserTokens"); }); + modelBuilder.Entity("MultiShop.Server.Models.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("MultiShop.Shared.Models.ResultsProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ApplicationUserId") + .HasColumnType("TEXT"); + + b.Property("Order") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId") + .IsUnique(); + + b.ToTable("ResultsProfile"); + }); + + modelBuilder.Entity("MultiShop.Shared.Models.SearchProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ApplicationUserId") + .HasColumnType("TEXT"); + + b.Property("Currency") + .HasColumnType("INTEGER"); + + b.Property("EnableMaxShippingFee") + .HasColumnType("INTEGER"); + + b.Property("EnableUpperPrice") + .HasColumnType("INTEGER"); + + b.Property("KeepUnknownPurchaseCount") + .HasColumnType("INTEGER"); + + b.Property("KeepUnknownRatingCount") + .HasColumnType("INTEGER"); + + b.Property("KeepUnknownShipping") + .HasColumnType("INTEGER"); + + b.Property("KeepUnrated") + .HasColumnType("INTEGER"); + + b.Property("LowerPrice") + .HasColumnType("INTEGER"); + + b.Property("MaxResults") + .HasColumnType("INTEGER"); + + b.Property("MaxShippingFee") + .HasColumnType("INTEGER"); + + b.Property("MinPurchases") + .HasColumnType("INTEGER"); + + b.Property("MinRating") + .HasColumnType("REAL"); + + b.Property("MinReviews") + .HasColumnType("INTEGER"); + + b.Property("ShopStates") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpperPrice") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId") + .IsUnique(); + + b.ToTable("SearchProfile"); + }); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => { b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) @@ -365,6 +449,29 @@ namespace MultiShop.Server.Data.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); + + modelBuilder.Entity("MultiShop.Shared.Models.ResultsProfile", b => + { + b.HasOne("MultiShop.Server.Models.ApplicationUser", null) + .WithOne("ResultsProfile") + .HasForeignKey("MultiShop.Shared.Models.ResultsProfile", "ApplicationUserId"); + }); + + modelBuilder.Entity("MultiShop.Shared.Models.SearchProfile", b => + { + b.HasOne("MultiShop.Server.Models.ApplicationUser", null) + .WithOne("SearchProfile") + .HasForeignKey("MultiShop.Shared.Models.SearchProfile", "ApplicationUserId"); + }); + + modelBuilder.Entity("MultiShop.Server.Models.ApplicationUser", b => + { + b.Navigation("ResultsProfile") + .IsRequired(); + + b.Navigation("SearchProfile") + .IsRequired(); + }); #pragma warning restore 612, 618 } } diff --git a/src/MultiShop/Server/Models/ApplicationUser.cs b/src/MultiShop/Server/Models/ApplicationUser.cs index 74b623f..79f3ff3 100644 --- a/src/MultiShop/Server/Models/ApplicationUser.cs +++ b/src/MultiShop/Server/Models/ApplicationUser.cs @@ -1,12 +1,15 @@ -using Microsoft.AspNetCore.Identity; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Identity; +using MultiShop.Shared.Models; namespace MultiShop.Server.Models { public class ApplicationUser : IdentityUser { + [Required] + public virtual SearchProfile SearchProfile { get; private set; } = new SearchProfile(); + + [Required] + public virtual ResultsProfile ResultsProfile { get; private set; } = new ResultsProfile(); } } diff --git a/src/MultiShop/Server/MultiShop.Server.csproj b/src/MultiShop/Server/MultiShop.Server.csproj index 8fcc3a1..56de510 100644 --- a/src/MultiShop/Server/MultiShop.Server.csproj +++ b/src/MultiShop/Server/MultiShop.Server.csproj @@ -11,6 +11,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + diff --git a/src/MultiShop/Server/Startup.cs b/src/MultiShop/Server/Startup.cs index 05e0f2d..dc2f360 100644 --- a/src/MultiShop/Server/Startup.cs +++ b/src/MultiShop/Server/Startup.cs @@ -10,6 +10,8 @@ using Microsoft.Extensions.Hosting; using System.Linq; using MultiShop.Server.Data; using MultiShop.Server.Models; +using Microsoft.AspNetCore.Identity; +using System.Security.Claims; namespace MultiShop.Server { @@ -26,9 +28,10 @@ namespace MultiShop.Server // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { - services.AddDbContext(options => - options.UseSqlite( - Configuration.GetConnectionString("DefaultConnection"))); + services.AddDbContext(options => { + options.UseLazyLoadingProxies(); + options.UseSqlite(Configuration.GetConnectionString("DefaultConnection")); + }); services.AddDatabaseDeveloperPageExceptionFilter(); @@ -38,6 +41,8 @@ namespace MultiShop.Server services.AddIdentityServer() .AddApiAuthorization(); + services.Configure(Options => Options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier); //Note: Despite default, doesn't work without this. + services.AddAuthentication() .AddIdentityServerJwt(); diff --git a/src/MultiShop/Server/modules/MultiShop.Shop.AliExpressModule.dll b/src/MultiShop/Server/modules/MultiShop.Shop.AliExpressModule.dll index 5b4743f9df6bc35ec74d981220207b3829a8cac2..ae01e7dfffda996f5305a779c65aa5cbfb840cd5 100644 GIT binary patch delta 107 zcmZoT!`N_!aY6@+WbFJc8+&+y1)8TlP;LMG*`x2B%u4xpn-6VX5!}PY(z)r%lF1fv z0Rlb;emwG=yUJs)-kzfqmY-WYc}85j0#q=g7Ago-t@8P!>vZ E0C+Vx`~Uy| delta 107 zcmZoT!`N_!aY6^nE!~$b8+&+y1%CdsZ<*et;@iS9bJebm$vvA_1ov>UF!J1ZG1($6 zK;Y!AdCRn}rMNG0pQD-bZ{p&~GveA6pn~E`P(hIDJT05&Oy6tuJ~*FX-)tX$kPQHq C(lQVL diff --git a/src/MultiShop/Server/modules/MultiShop.Shop.BanggoodModule.dll b/src/MultiShop/Server/modules/MultiShop.Shop.BanggoodModule.dll index 6999c8208a1415344fb770ee37bb23a9d14e13ec..e08aa06eb3cb297400dcfa2f3e92f9eeb3e976b1 100644 GIT binary patch delta 105 zcmZojX-JvS!D3;5^w!3n5G8>l$~$s+nGd*46KgT}^QPeX<_}7HSXuftA`VR6pb;Q& zsCw5N9@aXCojK1A8|HH_nk=B%tN;}>_z4vRs&>eE7(Z+3!WA;r*O?D)-mK}%0RW+k BF0=pu delta 105 zcmZojX-JvS!D1IS`Sr%05G8?~#l2fCw()td;dr)B=t=3^%^#HZu(D*b8l9NDK_ft5 zjZn$I=}O%WyCtIZPR-Pno-Cl*tN;}}@Ea-!RPAxm^5Ab3pT4HT?$isLH*5NG004cg BEph+= diff --git a/src/MultiShop/Shared/Models/ProductListingInfo.cs b/src/MultiShop/Shared/Models/ProductListingInfo.cs new file mode 100644 index 0000000..efa3f38 --- /dev/null +++ b/src/MultiShop/Shared/Models/ProductListingInfo.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json; +using MultiShop.Shared; +using MultiShop.Shop.Framework; + +namespace MultiShop.Shared.Models +{ + public class ProductListingInfo + { + public int Id { get; set; } + + private ProductListing? cachedListing; + + [Required] + private string _listing = null; + public ProductListing Listing + { + get + { + if (cachedListing == null) cachedListing = JsonSerializer.Deserialize(_listing); + return cachedListing.Value; + } + + set + { + _listing = JsonSerializer.Serialize(value); + cachedListing = value; + } + } + public string ShopName { get; private set; } + + public float? RatingToPriceRatio + { + get + { + int reviewFactor = Listing.ReviewCount.HasValue ? Listing.ReviewCount.Value : 1; + int purchaseFactor = Listing.PurchaseCount.HasValue ? Listing.PurchaseCount.Value : 1; + return (Listing.Rating * (reviewFactor > purchaseFactor ? reviewFactor : purchaseFactor)) / (Listing.LowerPrice * Listing.UpperPrice); + } + } + public ISet Tops { get; private set; } = new HashSet(); + + public ProductListingInfo(ProductListing listing, string shopName) + { + this.Listing = listing; + this.ShopName = shopName; + } + + public ProductListingInfo() + { + + } + + public override bool Equals(object obj) + { + + if (obj == null || GetType() != obj.GetType()) + { + return false; + } + ProductListingInfo other = (ProductListingInfo)obj; + return Id == other.Id && ShopName.Equals(other.ShopName) && Listing.Equals(other.Listing); + } + + public override int GetHashCode() + { + return Id; + } + } +} \ No newline at end of file diff --git a/src/MultiShop/Shared/ResultsProfile.cs b/src/MultiShop/Shared/Models/ResultsProfile.cs similarity index 56% rename from src/MultiShop/Shared/ResultsProfile.cs rename to src/MultiShop/Shared/Models/ResultsProfile.cs index 5ff7f69..cc6ba8a 100644 --- a/src/MultiShop/Shared/ResultsProfile.cs +++ b/src/MultiShop/Shared/Models/ResultsProfile.cs @@ -1,26 +1,28 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; +using System.Text.Json; -namespace MultiShop.Shared +namespace MultiShop.Shared.Models { public class ResultsProfile { - public List Order { get; private set; } = new List(Enum.GetValues().Length); + public int Id { get; set; } + public string ApplicationUserId { get; set; } + + [Required] + public IList Order { get; set; } public ResultsProfile() { + Order = new List(Enum.GetValues().Length); foreach (Category category in Enum.GetValues()) { Order.Add(category); } } - public Category GetCategory(int position) - { - return Order[position]; - } - public enum Category { RatingPriceRatio, diff --git a/src/MultiShop/Shared/Models/SearchProfile.cs b/src/MultiShop/Shared/Models/SearchProfile.cs new file mode 100644 index 0000000..df8f855 --- /dev/null +++ b/src/MultiShop/Shared/Models/SearchProfile.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using MultiShop.Shop.Framework; + +namespace MultiShop.Shared.Models +{ + public class SearchProfile + { + public int Id { get; set; } + public string ApplicationUserId { get; set; } + + public Currency Currency { get; set; } = Currency.CAD; + public int MaxResults { get; set; } = 100; + public float MinRating { get; set; } = 0.8f; + public bool KeepUnrated { get; set; } = true; + public bool EnableUpperPrice { get; set; } = false; + private int _upperPrice; + + public int UpperPrice + { + get + { + return _upperPrice; + } + set + { + if (EnableUpperPrice) _upperPrice = value; + } + } + public int LowerPrice { get; set; } + public int MinPurchases { get; set; } + public bool KeepUnknownPurchaseCount { get; set; } = true; + public int MinReviews { get; set; } + public bool KeepUnknownRatingCount { get; set; } = true; + public bool EnableMaxShippingFee { get; set; } + private int _maxShippingFee; + + public int MaxShippingFee + { + get + { + return _maxShippingFee; + } + set + { + if (EnableMaxShippingFee) _maxShippingFee = value; + } + } + public bool KeepUnknownShipping { get; set; } + + [Required] + public ShopToggler ShopStates { get; set; } = new ShopToggler(); + + public sealed class ShopToggler : HashSet + { + public int TotalShops { get; set; } + public bool this[string name] { + get { + return !this.Contains(name); + } + set { + if (value == false && TotalShops - Count <= 1) return; + if (value) + { + this.Remove(name); + } + else + { + this.Add(name); + } + } + } + + public ShopToggler Clone() { + ShopToggler clone = new ShopToggler(); + clone.Union(this); + return clone; + } + + public bool IsShopToggleable(string shop) + { + return (!Contains(shop) && TotalShops - Count > 1) || Contains(shop); + } + } + + public override bool Equals(object obj) + { + if (obj == null || GetType() != obj.GetType()) + { + return false; + } + SearchProfile other = (SearchProfile) obj; + return + Id == other.Id && + Currency == other.Currency && + MaxResults == other.MaxResults && + MinRating == other.MinRating && + KeepUnrated == other.KeepUnrated && + EnableUpperPrice == other.EnableUpperPrice && + UpperPrice == other.UpperPrice && + LowerPrice == other.LowerPrice && + MinPurchases == other.MinPurchases && + KeepUnknownPurchaseCount == other.KeepUnknownPurchaseCount && + MinReviews == other.MinReviews && + KeepUnknownRatingCount == other.KeepUnknownRatingCount && + EnableMaxShippingFee == other.EnableMaxShippingFee && + MaxShippingFee == other.MaxShippingFee && + KeepUnknownShipping == other.KeepUnknownShipping && + ShopStates.Equals(other.ShopStates); + } + + public override int GetHashCode() + { + return Id; + } + } +} \ No newline at end of file diff --git a/src/MultiShop/Shared/ProductListingInfo.cs b/src/MultiShop/Shared/ProductListingInfo.cs deleted file mode 100644 index a1225e3..0000000 --- a/src/MultiShop/Shared/ProductListingInfo.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Collections.Generic; -using MultiShop.Shop.Framework; - -namespace MultiShop.Shared -{ - public class ProductListingInfo - { - public ProductListing Listing { get; private set; } - public string ShopName { get; private set; } - public float? RatingToPriceRatio { - get { - int reviewFactor = Listing.ReviewCount.HasValue ? Listing.ReviewCount.Value : 1; - int purchaseFactor = Listing.PurchaseCount.HasValue ? Listing.PurchaseCount.Value : 1; - return (Listing.Rating * (reviewFactor > purchaseFactor ? reviewFactor : purchaseFactor))/(Listing.LowerPrice * Listing.UpperPrice); - } - } - public ISet Tops { get; private set; } = new HashSet(); - - public ProductListingInfo(ProductListing listing, string shopName) - { - this.Listing = listing; - this.ShopName = shopName; - } - } -} \ No newline at end of file diff --git a/src/MultiShop/Shared/SearchProfile.cs b/src/MultiShop/Shared/SearchProfile.cs deleted file mode 100644 index 02f8ab2..0000000 --- a/src/MultiShop/Shared/SearchProfile.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System.Collections.Generic; -using MultiShop.Shop.Framework; - -namespace MultiShop.Shared -{ - public class SearchProfile - { - public Currency currency; - public int maxResults; - public float minRating; - public bool keepUnrated; - public bool enableUpperPrice; - private int upperPrice; - public int UpperPrice - { - get - { - return upperPrice; - } - set - { - if (enableUpperPrice) upperPrice = value; - } - } - public int lowerPrice; - public int minPurchases; - public bool keepUnknownPurchaseCount; - public int minReviews; - public bool keepUnknownRatingCount; - public bool enableMaxShippingFee; - private int maxShippingFee; - public int MaxShippingFee { - get { - return maxShippingFee; - } - set { - if (enableMaxShippingFee) maxShippingFee = value; - } - } - public bool keepUnknownShipping; - public ShopStateTracker shopStates = new ShopStateTracker(); - - public SearchProfile() - { - currency = Currency.CAD; - maxResults = 100; - minRating = 0.8f; - keepUnrated = true; - enableUpperPrice = false; - upperPrice = 0; - lowerPrice = 0; - minPurchases = 0; - keepUnknownPurchaseCount = true; - minReviews = 0; - keepUnknownRatingCount = true; - enableMaxShippingFee = false; - maxShippingFee = 0; - keepUnknownShipping = true; - } - - public class ShopStateTracker - { - private HashSet shopsEnabled = new HashSet(); - public bool this[string name] - { - get - { - return shopsEnabled.Contains(name); - } - - set - { - if (value == false && !(shopsEnabled.Count > 1)) return; - if (value) - { - shopsEnabled.Add(name); - } - else - { - shopsEnabled.Remove(name); - } - } - } - public bool IsToggleable(string shop) { - return (shopsEnabled.Contains(shop) && shopsEnabled.Count > 1) || !shopsEnabled.Contains(shop); - } - } - } -} \ No newline at end of file