Added search and results profile persistence when logged in.
This commit is contained in:
parent
bbb2d4bd04
commit
235196f8e5
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
{
|
@ -0,0 +1,9 @@
|
||||
using MultiShop.Shared.Models;
|
||||
|
||||
namespace MultiShop.Client.Extensions.Models
|
||||
{
|
||||
public static class SearchProfileExtensions
|
||||
{
|
||||
|
||||
}
|
||||
}
|
26
src/MultiShop/Client/Listing/ListingView.cs
Normal file
26
src/MultiShop/Client/Listing/ListingView.cs
Normal file
@ -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<ProductListingInfo> 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;
|
||||
}
|
||||
}
|
||||
}
|
102
src/MultiShop/Client/Listing/TableView.razor
Normal file
102
src/MultiShop/Client/Listing/TableView.razor
Normal file
@ -0,0 +1,102 @@
|
||||
@using MultiShop.Shared.Models
|
||||
@using Pages
|
||||
@inherits ListingView
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Price</th>
|
||||
<th scope="col">Shipping</th>
|
||||
<th scope="col">Purchases</th>
|
||||
<th scope="col">Rating</th>
|
||||
<th scope="col">Reviews</th>
|
||||
<th scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@if (!Status.SearchConfiguring && !Status.Searching)
|
||||
{
|
||||
<tbody>
|
||||
<Virtualize Items="@Listings" Context="product">
|
||||
<tr>
|
||||
<th scope="row" @key="product.Listing">
|
||||
<div class="text-truncate">@product.Listing.Name</div>
|
||||
<small>From @product.ShopName</small>
|
||||
@if (product.Listing.ConvertedPrices)
|
||||
{
|
||||
<span class="ml-3 mr-1 badge badge-warning">Converted price</span>
|
||||
}
|
||||
@foreach (ResultsProfile.Category c in product.Tops)
|
||||
{
|
||||
<span class="mx-1 badge badge-primary">@GetCategoryTag(c)</span>
|
||||
}
|
||||
</th>
|
||||
<td>
|
||||
@if (product.Listing.UpperPrice != product.Listing.LowerPrice)
|
||||
{
|
||||
<div class="text-truncate">
|
||||
@product.Listing.LowerPrice to @product.Listing.UpperPrice
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="text-truncate">
|
||||
@GetOrNA(product.Listing.LowerPrice)
|
||||
</div>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-truncate">
|
||||
@GetOrNA(product.Listing.Shipping)
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-truncate">
|
||||
@GetOrNA(product.Listing.PurchaseCount)
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-truncate">
|
||||
@(product.Listing.Rating != null ? string.Format("{0:P2}", product.Listing.Rating) : "N/A")
|
||||
</div>
|
||||
</td>
|
||||
<td>@GetOrNA(product.Listing.ReviewCount)</td>
|
||||
<td>
|
||||
<a href="@product.Listing.URL" class="btn btn-primary" target="_blank">View</a>
|
||||
</td>
|
||||
</tr>
|
||||
</Virtualize>
|
||||
</tbody>
|
||||
}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@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";
|
||||
}
|
||||
}
|
7
src/MultiShop/Client/Listing/Views.cs
Normal file
7
src/MultiShop/Client/Listing/Views.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace MultiShop.Client.Listing
|
||||
{
|
||||
public enum Views
|
||||
{
|
||||
Table
|
||||
}
|
||||
}
|
@ -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
|
||||
|
||||
<div class="my-2">
|
||||
<div class="input-group my-2">
|
||||
<input type="text" class="form-control" placeholder="What are you looking for?" aria-label="What are you looking for?" id="search-input" @bind="Query" @onkeyup="@(async (a) => {if (a.Code == "Enter" || a.Code == "NumpadEnter") await PerformSearch(Query);})" disabled="@searching">
|
||||
<input type="text" class="form-control" placeholder="What are you looking for?" aria-label="What are you looking for?" id="search-input" @bind="Query" @onkeyup="@(async (a) => {if (a.Code == "Enter" || a.Code == "NumpadEnter") await PerformSearch(Query);})" disabled="@status.Searching" >
|
||||
<div class="input-group-append">
|
||||
<button class=@ToggleSearchConfigButtonCss type="button" @onclick="@(() => showSearchConfiguration = !showSearchConfiguration)" title="Configure"><span class="oi oi-cog align-text-top"></span></button>
|
||||
<button class="btn btn-outline-primary" type="button" @onclick="@(async () => await PerformSearch(Query))" disabled="@searching">Search</button>
|
||||
<ToggleableButton class="btn btn-outline-secondary" title="Configure" OnToggleCallback="@((t) => status.SearchConfiguring = t)"><span class="oi oi-cog align-text-top"></span></ToggleableButton>
|
||||
<button class="btn btn-outline-primary" type="button" @onclick="@(async () => await PerformSearch(Query))" disabled="@status.Searching">Search</button>
|
||||
</div>
|
||||
</div>
|
||||
@if (showSearchConfiguration)
|
||||
@if (status.SearchConfiguring)
|
||||
{
|
||||
<div class="mb-2 mt-4 py-2">
|
||||
<h4>Configuration</h4>
|
||||
@ -24,8 +21,8 @@
|
||||
<h6 class="card-subtitle mb-2 text-muted">How many results from each store?</h6>
|
||||
<p class="card-text">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.</p>
|
||||
<div class="form-group">
|
||||
<label for="quantitySlider">Quantity: @activeProfile.maxResults</label>
|
||||
<input class="form-control-range" type="range" id="quantitySlider" min="1" max="200" step="1" @bind="activeProfile.maxResults" @bind:event="oninput">
|
||||
<label for="quantitySlider">Quantity: @activeSearchProfile.MaxResults</label>
|
||||
<input class="form-control-range" type="range" id="quantitySlider" min="1" max="200" step="1" @bind="activeSearchProfile.MaxResults" @bind:event="oninput">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -38,10 +35,10 @@
|
||||
<div class="input-group-prepend">
|
||||
<label class="input-group-text" for="currency-select">Currency</label>
|
||||
</div>
|
||||
<select class="form-control custom-select" id="currency-select" @bind="activeProfile.currency">
|
||||
<select class="form-control custom-select" id="currency-select" @bind="activeSearchProfile.Currency">
|
||||
@foreach (Currency currency in Enum.GetValues<Currency>())
|
||||
{
|
||||
@if (currency == activeProfile.currency)
|
||||
@if (currency == activeSearchProfile.Currency)
|
||||
{
|
||||
<option selected>@currency</option>
|
||||
}
|
||||
@ -60,11 +57,11 @@
|
||||
<h6 class="card-subtitle mb-2 text-muted">We'll crop out the lower rated stuff.</h6>
|
||||
<p class="card-text">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.</p>
|
||||
<div class="form-group">
|
||||
<label for="ratingSlider">Minimum rating: @(string.Format("{0:P0}", activeProfile.minRating))</label>
|
||||
<input class="form-control-range" type="range" id="ratingSlider" min="0" max="1" step="0.01" @bind="activeProfile.minRating" @bind:event="oninput">
|
||||
<label for="ratingSlider">Minimum rating: @(string.Format("{0:P0}", activeSearchProfile.MinRating))</label>
|
||||
<input class="form-control-range" type="range" id="ratingSlider" min="0" max="1" step="0.01" @bind="activeSearchProfile.MinRating" @bind:event="oninput">
|
||||
</div>
|
||||
<div class="form-group form-check">
|
||||
<input class="form-check-input" type="checkbox" id="keepUnratedCheckbox" @bind="activeProfile.keepUnrated">
|
||||
<input class="form-check-input" type="checkbox" id="keepUnratedCheckbox" @bind="activeSearchProfile.KeepUnrated">
|
||||
<label class="form-check-label" for="keepUnratedCheckbox">Keep unrated results</label>
|
||||
</div>
|
||||
</div>
|
||||
@ -77,11 +74,11 @@
|
||||
<div class="input-group my-2">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text">
|
||||
<input type="checkbox" @bind="activeProfile.enableUpperPrice">
|
||||
<input type="checkbox" @bind="activeSearchProfile.EnableUpperPrice">
|
||||
</div>
|
||||
<span class="input-group-text">Upper limit</span>
|
||||
</div>
|
||||
<input type="number" class="form-control" @bind="activeProfile.UpperPrice" disabled="@(!activeProfile.enableUpperPrice)">
|
||||
<input type="number" class="form-control" @bind="activeSearchProfile.UpperPrice" disabled="@(!activeSearchProfile.EnableUpperPrice)">
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text">.00</span>
|
||||
</div>
|
||||
@ -90,7 +87,7 @@
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">Lower limit</span>
|
||||
</div>
|
||||
<input type="number" class="form-control" @bind="activeProfile.lowerPrice">
|
||||
<input type="number" class="form-control" @bind="activeSearchProfile.LowerPrice">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">.00</span>
|
||||
</div>
|
||||
@ -105,7 +102,7 @@
|
||||
@foreach (string shop in Shops.Keys)
|
||||
{
|
||||
<div class="form-group form-check my-2">
|
||||
<input class="form-check-input" type="checkbox" id=@(shop + "Checkbox") @bind="activeProfile.shopStates[shop]" disabled="@(!activeProfile.shopStates.IsToggleable(shop))">
|
||||
<input class="form-check-input" type="checkbox" id=@(shop + "Checkbox") @bind="@(activeSearchProfile.ShopStates[shop])" disabled="@(!activeSearchProfile.ShopStates.IsShopToggleable(shop))">
|
||||
<label class="form-check-label" for=@(shop + "Checkbox")>@shop enabled</label>
|
||||
</div>
|
||||
}
|
||||
@ -120,10 +117,10 @@
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">Minimum purchases</span>
|
||||
</div>
|
||||
<input type="number" class="form-control" min="0" step="1" @bind="activeProfile.minPurchases">
|
||||
<input type="number" class="form-control" min="0" step="1" @bind="activeSearchProfile.MinPurchases">
|
||||
</div>
|
||||
<div class="form-group form-check my-2">
|
||||
<input class="form-check-input" type="checkbox" id="keepNullPurchasesCheckbox" @bind="activeProfile.keepUnknownPurchaseCount">
|
||||
<input class="form-check-input" type="checkbox" id="keepNullPurchasesCheckbox" @bind="activeSearchProfile.KeepUnknownPurchaseCount">
|
||||
<label class="form-check-label" for="keepNullPurchasesCheckbox">Keep unknown listings</label>
|
||||
</div>
|
||||
</div>
|
||||
@ -137,10 +134,10 @@
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">Minimum reviews</span>
|
||||
</div>
|
||||
<input type="number" class="form-control" min="0" step="1" @bind="activeProfile.minReviews">
|
||||
<input type="number" class="form-control" min="0" step="1" @bind="activeSearchProfile.MinReviews">
|
||||
</div>
|
||||
<div class="form-group form-check my-2">
|
||||
<input class="form-check-input" type="checkbox" id="keepNullRatingsCheckbox" @bind="activeProfile.keepUnknownRatingCount">
|
||||
<input class="form-check-input" type="checkbox" id="keepNullRatingsCheckbox" @bind="activeSearchProfile.KeepUnknownRatingCount">
|
||||
<label class="form-check-label" for="keepNullRatingsCheckbox">Keep unknown listings</label>
|
||||
</div>
|
||||
</div>
|
||||
@ -153,17 +150,17 @@
|
||||
<div class="input-group my-2">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">
|
||||
<input type="checkbox" @bind="activeProfile.enableMaxShippingFee">
|
||||
<input type="checkbox" @bind="activeSearchProfile.EnableMaxShippingFee">
|
||||
</span>
|
||||
<span class="input-group-text">Max shipping</span>
|
||||
</div>
|
||||
<input type="number" class="form-control" min="0" step="1" @bind="activeProfile.MaxShippingFee" disabled="@(!activeProfile.enableMaxShippingFee)">
|
||||
<input type="number" class="form-control" min="0" step="1" @bind="activeSearchProfile.MaxShippingFee" disabled="@(!activeSearchProfile.EnableMaxShippingFee)">
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text">.00</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group form-check my-2">
|
||||
<input class="form-check-input" type="checkbox" id="keepNullShipping" @bind="activeProfile.keepUnknownShipping">
|
||||
<input class="form-check-input" type="checkbox" id="keepNullShipping" @bind="activeSearchProfile.KeepUnknownShipping">
|
||||
<label class="form-check-label" for="keepNullShipping">Keep unknown listings</label>
|
||||
</div>
|
||||
</div>
|
||||
@ -175,9 +172,9 @@
|
||||
|
||||
<div class="my-3 py-2">
|
||||
<div class="d-inline-flex" style="width: 100%; border-bottom-style: solid; border-bottom-width: 1px; border-color: lightgray;">
|
||||
<button type="button" class=@ToggleResultsConfigurationcss @onclick="@(() => showResultsConfiguration = !showResultsConfiguration)"><span class="oi oi-sort-descending"></span></button>
|
||||
<ToggleableButton class="btn btn-outline-secondary btn-tab" OnToggleCallback="@(t => status.ResultsConfiguring = t)"><span class="oi oi-sort-descending"></span></ToggleableButton>
|
||||
</div>
|
||||
@if (showResultsConfiguration)
|
||||
@if (status.ResultsConfiguring)
|
||||
{
|
||||
<div style="border-color: lightgray;" class="p-1">
|
||||
<div class="card m-2" style="max-width: 23em;">
|
||||
@ -197,7 +194,7 @@
|
||||
|
||||
<div class="d-flex flex-wrap" style="width: 100%; border-bottom-width: 1px; border-bottom-style: solid; border-bottom-color: lightgray;">
|
||||
<div class="align-self-end">
|
||||
@if (searching)
|
||||
@if (status.Searching)
|
||||
{
|
||||
@if (listings.Count != 0)
|
||||
{
|
||||
@ -216,7 +213,7 @@
|
||||
}
|
||||
else if (listings.Count != 0)
|
||||
{
|
||||
@if (organizing)
|
||||
@if (status.Organizing)
|
||||
{
|
||||
<div class="spinner-border spinner-border-sm text-success my-auto mr-1" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
@ -228,7 +225,7 @@
|
||||
<span class="text-muted">Looked through @resultsChecked listings and found @listings.Count viable results.</span>
|
||||
}
|
||||
}
|
||||
else if (searched)
|
||||
else if (status.Searched)
|
||||
{
|
||||
<span class="text-muted">We've found @resultsChecked listings and unfortunately none matched your search.</span>
|
||||
}
|
||||
@ -241,74 +238,7 @@
|
||||
<div>
|
||||
@if (listings.Count > 0)
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Price</th>
|
||||
<th scope="col">Shipping</th>
|
||||
<th scope="col">Purchases</th>
|
||||
<th scope="col">Rating</th>
|
||||
<th scope="col">Reviews</th>
|
||||
<th scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@if (!showSearchConfiguration && !searching) {
|
||||
<tbody>
|
||||
<Virtualize Items="@listings" Context="product">
|
||||
<tr>
|
||||
<th scope="row" @key="product.Listing">
|
||||
<div class="text-truncate">@product.Listing.Name</div>
|
||||
<small>From @product.ShopName</small>
|
||||
@if (product.Listing.ConvertedPrices)
|
||||
{
|
||||
<span class="ml-3 mr-1 badge badge-warning">Converted price</span>
|
||||
}
|
||||
@foreach (ResultsProfile.Category c in product.Tops)
|
||||
{
|
||||
<span class="mx-1 badge badge-primary">@CategoryTags(c)</span>
|
||||
}
|
||||
</th>
|
||||
<td>
|
||||
@if (product.Listing.UpperPrice != product.Listing.LowerPrice)
|
||||
{
|
||||
<div class="text-truncate">
|
||||
@product.Listing.LowerPrice to @product.Listing.UpperPrice
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="text-truncate">
|
||||
@GetOrNA(product.Listing.LowerPrice)
|
||||
</div>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-truncate">
|
||||
@GetOrNA(product.Listing.Shipping)
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-truncate">
|
||||
@GetOrNA(product.Listing.PurchaseCount)
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-truncate">
|
||||
@(product.Listing.Rating != null ? string.Format("{0:P2}", product.Listing.Rating) : "N/A")
|
||||
</div>
|
||||
</td>
|
||||
<td>@GetOrNA(product.Listing.ReviewCount)</td>
|
||||
<td>
|
||||
<a href="@product.Listing.URL" class="btn btn-primary" target="_blank">View</a>
|
||||
</td>
|
||||
</tr>
|
||||
</Virtualize>
|
||||
</tbody>
|
||||
}
|
||||
</table>
|
||||
</div>
|
||||
@listingViews[CurrentView]
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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<AuthenticationState> AuthenticationStateTask { get; set; }
|
||||
|
||||
[CascadingParameter(Name = "Shops")]
|
||||
public Dictionary<string, IShop> 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<Views, ListingView> 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<ProductListingInfo> listings = new List<ProductListingInfo>();
|
||||
|
||||
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, ListingView>() {
|
||||
{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<SearchProfile>();
|
||||
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<ResultsProfile>();
|
||||
} 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<ProductListingInfo>>();
|
||||
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<ResultsProfile.Category> order)
|
||||
private async Task Organize(IList<ResultsProfile.Category> order)
|
||||
{
|
||||
if (searching || listings.Count <= 1) return;
|
||||
organizing = true;
|
||||
if (status.Searching || listings.Count <= 1) return;
|
||||
status.Organizing = true;
|
||||
Comparison<ProductListingInfo> comparer = (a, b) =>
|
||||
{
|
||||
foreach (ResultsProfile.Category category in activeResultsProfile.Order)
|
||||
@ -162,13 +186,15 @@ namespace MultiShop.Client.Pages
|
||||
return 0;
|
||||
};
|
||||
|
||||
Func<(int, int), Task<int>> partition = async (ilh) => {
|
||||
Func<(int, int), Task<int>> 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,13 +202,14 @@ 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);
|
||||
|
||||
@ -191,11 +218,13 @@ namespace MultiShop.Client.Pages
|
||||
(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; }
|
||||
}
|
||||
}
|
||||
}
|
@ -5,9 +5,3 @@ 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;
|
||||
}
|
@ -22,7 +22,6 @@ namespace MultiShop.Client
|
||||
|
||||
builder.Services.AddHttpClient("MultiShop.ServerAPI", client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)).AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
|
||||
|
||||
// Supply HttpClient instances that include access tokens when making requests to the server project
|
||||
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("MultiShop.ServerAPI"));
|
||||
Action<HttpClient> configureClient = client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress);
|
||||
|
||||
|
@ -22,7 +22,7 @@
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public List<TItem> Items { get; set; }
|
||||
public IList<TItem> Items { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string AdditionalListClasses { get; set; }
|
||||
|
@ -36,7 +36,7 @@
|
||||
<a class="nav-link" href="authentication/profile">Hello, @auth.User.Identity.Name!</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link btn btn-outline-primary" @onclick="BeginSignOut">Log out</button>
|
||||
<button class="nav-link btn btn-link" @onclick="BeginSignOut">Log out</button>
|
||||
</li>
|
||||
</Authorized>
|
||||
<NotAuthorized>
|
||||
|
28
src/MultiShop/Client/Shared/ToggleableButton.razor
Normal file
28
src/MultiShop/Client/Shared/ToggleableButton.razor
Normal file
@ -0,0 +1,28 @@
|
||||
<button @attributes="AdditionalAttributes" class=@ButtonClasses type="button" @onclick="OnClick" title="Configure">@ChildContent</button>
|
||||
|
||||
@code {
|
||||
[Parameter(CaptureUnmatchedValues = true)]
|
||||
public IReadOnlyDictionary<string, object> AdditionalAttributes { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment ChildContent { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<bool> OnToggleCallback { get; set; }
|
||||
|
||||
private async Task OnClick() {
|
||||
state = !state;
|
||||
await OnToggleCallback.InvokeAsync(state);
|
||||
}
|
||||
|
||||
private bool state;
|
||||
|
||||
private string ButtonClasses
|
||||
{
|
||||
get
|
||||
{
|
||||
IReadOnlyDictionary<string, object> t = AdditionalAttributes;
|
||||
return (state ? "active " : "") + (AdditionalAttributes["class"] as string);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
63
src/MultiShop/Server/Controllers/ProfileController.cs
Normal file
63
src/MultiShop/Server/Controllers/ProfileController.cs
Normal file
@ -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<ApplicationUser> userManager;
|
||||
private ApplicationDbContext dbContext;
|
||||
public ProfileController(UserManager<ApplicationUser> userManager, ApplicationDbContext dbContext)
|
||||
{
|
||||
this.userManager = userManager;
|
||||
this.dbContext = dbContext;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("Search")]
|
||||
public async Task<IActionResult> GetSearchProfile() {
|
||||
ApplicationUser userModel = await userManager.GetUserAsync(User);
|
||||
return Ok(userModel.SearchProfile);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("Results")]
|
||||
public async Task<IActionResult> GetResultsProfile() {
|
||||
ApplicationUser userModel = await userManager.GetUserAsync(User);
|
||||
return Ok(userModel.ResultsProfile);
|
||||
}
|
||||
|
||||
[HttpPut]
|
||||
[Route("Search")]
|
||||
public async Task<IActionResult> 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<IActionResult> 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -16,10 +16,10 @@ namespace MultiShop.Server.Controllers
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IReadOnlyDictionary<string, string> GetPublicConfiguration() {
|
||||
return new Dictionary<string, string> {
|
||||
public IActionResult GetPublicConfiguration() {
|
||||
return Ok(new Dictionary<string, string> {
|
||||
{"IdentityServer:Registration", configuration["IdentityServer:Registration"]}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -22,20 +22,22 @@ namespace MultiShop.Server.Controllers
|
||||
this.shopAssemblyData = new Dictionary<string, byte[]>();
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetShopModuleNames() {
|
||||
public IActionResult GetShopModuleNames() {
|
||||
List<string> moduleNames = new List<string>();
|
||||
ShopOptions options = configuration.GetSection(ShopOptions.Shop).Get<ShopOptions>();
|
||||
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<ShopOptions>();
|
||||
string shopPath = Path.Join(options.Directory, shopModuleName);
|
||||
shopPath += ".dll";
|
||||
|
@ -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<ApplicationUser>
|
||||
{
|
||||
public ApplicationDbContext(
|
||||
DbContextOptions options,
|
||||
IOptions<OperationalStoreOptions> operationalStoreOptions) : base(options, operationalStoreOptions)
|
||||
public ApplicationDbContext(DbContextOptions options, IOptions<OperationalStoreOptions> operationalStoreOptions) : base(options, operationalStoreOptions)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder) {
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
modelBuilder.Entity<ResultsProfile>()
|
||||
.Property(e => e.Order)
|
||||
.HasConversion(
|
||||
v => JsonSerializer.Serialize(v, null),
|
||||
v => JsonSerializer.Deserialize<List<ResultsProfile.Category>>(v, null),
|
||||
new ValueComparer<IList<ResultsProfile.Category>>(
|
||||
(a, b) => a.SequenceEqual(b),
|
||||
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
|
||||
c => (IList<ResultsProfile.Category>) c.ToList()
|
||||
)
|
||||
);
|
||||
|
||||
modelBuilder.Entity<SearchProfile>()
|
||||
.Property(e => e.ShopStates)
|
||||
.HasConversion(
|
||||
v => JsonSerializer.Serialize(v, null),
|
||||
v => JsonSerializer.Deserialize<SearchProfile.ShopToggler>(v, null),
|
||||
new ValueComparer<SearchProfile.ShopToggler>(
|
||||
(a, b) => a.Equals(b),
|
||||
c => c.GetHashCode(),
|
||||
c => c.Clone()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,86 +1,22 @@
|
||||
// <auto-generated />
|
||||
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<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("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<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ApplicationUserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Order")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ApplicationUserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ResultsProfile");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MultiShop.Shared.Models.SearchProfile", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ApplicationUserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Currency")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("EnableMaxShippingFee")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("EnableUpperPrice")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("KeepUnknownPurchaseCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("KeepUnknownRatingCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("KeepUnknownShipping")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("KeepUnrated")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("LowerPrice")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MaxResults")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MaxShippingFee")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MinPurchases")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<float>("MinRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<int>("MinReviews")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ShopStates")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("UpperPrice")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ApplicationUserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("SearchProfile");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", 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
|
||||
}
|
||||
}
|
@ -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<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ApplicationUserId = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Order = table.Column<string>(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<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ApplicationUserId = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Currency = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
MaxResults = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
MinRating = table.Column<float>(type: "REAL", nullable: false),
|
||||
KeepUnrated = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
EnableUpperPrice = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
UpperPrice = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
LowerPrice = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
MinPurchases = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
KeepUnknownPurchaseCount = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
MinReviews = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
KeepUnknownRatingCount = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
EnableMaxShippingFee = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
MaxShippingFee = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
KeepUnknownShipping = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
ShopStates = table.Column<string>(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");
|
||||
|
@ -1,9 +1,9 @@
|
||||
// <auto-generated />
|
||||
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<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("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<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ApplicationUserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Order")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ApplicationUserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ResultsProfile");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MultiShop.Shared.Models.SearchProfile", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ApplicationUserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Currency")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("EnableMaxShippingFee")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("EnableUpperPrice")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("KeepUnknownPurchaseCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("KeepUnknownRatingCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("KeepUnknownShipping")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("KeepUnrated")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("LowerPrice")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MaxResults")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MaxShippingFee")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MinPurchases")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<float>("MinRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<int>("MinReviews")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ShopStates")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("UpperPrice")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ApplicationUserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("SearchProfile");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", 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
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="5.0.6" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.6" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="5.0.2" />
|
||||
</ItemGroup>
|
||||
|
@ -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<ApplicationDbContext>(options =>
|
||||
options.UseSqlite(
|
||||
Configuration.GetConnectionString("DefaultConnection")));
|
||||
services.AddDbContext<ApplicationDbContext>(options => {
|
||||
options.UseLazyLoadingProxies();
|
||||
options.UseSqlite(Configuration.GetConnectionString("DefaultConnection"));
|
||||
});
|
||||
|
||||
services.AddDatabaseDeveloperPageExceptionFilter();
|
||||
|
||||
@ -38,6 +41,8 @@ namespace MultiShop.Server
|
||||
services.AddIdentityServer()
|
||||
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
|
||||
|
||||
services.Configure<IdentityOptions>(Options => Options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier); //Note: Despite default, doesn't work without this.
|
||||
|
||||
services.AddAuthentication()
|
||||
.AddIdentityServerJwt();
|
||||
|
||||
|
Binary file not shown.
Binary file not shown.
72
src/MultiShop/Shared/Models/ProductListingInfo.cs
Normal file
72
src/MultiShop/Shared/Models/ProductListingInfo.cs
Normal file
@ -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<ProductListing>(_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<ResultsProfile.Category> Tops { get; private set; } = new HashSet<ResultsProfile.Category>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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<Category> Order { get; private set; } = new List<Category>(Enum.GetValues<Category>().Length);
|
||||
public int Id { get; set; }
|
||||
public string ApplicationUserId { get; set; }
|
||||
|
||||
[Required]
|
||||
public IList<Category> Order { get; set; }
|
||||
|
||||
public ResultsProfile()
|
||||
{
|
||||
Order = new List<Category>(Enum.GetValues<Category>().Length);
|
||||
foreach (Category category in Enum.GetValues<Category>())
|
||||
{
|
||||
Order.Add(category);
|
||||
}
|
||||
}
|
||||
|
||||
public Category GetCategory(int position)
|
||||
{
|
||||
return Order[position];
|
||||
}
|
||||
|
||||
public enum Category
|
||||
{
|
||||
RatingPriceRatio,
|
119
src/MultiShop/Shared/Models/SearchProfile.cs
Normal file
119
src/MultiShop/Shared/Models/SearchProfile.cs
Normal file
@ -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<string>
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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<ResultsProfile.Category> Tops { get; private set; } = new HashSet<ResultsProfile.Category>();
|
||||
|
||||
public ProductListingInfo(ProductListing listing, string shopName)
|
||||
{
|
||||
this.Listing = listing;
|
||||
this.ShopName = shopName;
|
||||
}
|
||||
}
|
||||
}
|
@ -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<string> shopsEnabled = new HashSet<string>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user