Compare commits
No commits in common. "a92762b85b7adde0ba219356f3d82fd25b8b9408" and "e9eec03a540fbf21f5b812f149f75e921f86827e" have entirely different histories.
a92762b85b
...
e9eec03a54
@ -1,11 +0,0 @@
|
|||||||
FROM mcr.microsoft.com/devcontainers/anaconda:1-3
|
|
||||||
|
|
||||||
# Copy environment.yml (if found) to a temp location so we update the environment. Also
|
|
||||||
# copy "noop.txt" so the COPY instruction does not fail if no environment.yml exists.
|
|
||||||
COPY environment.yml* .devcontainer/noop.txt /tmp/conda-tmp/
|
|
||||||
RUN if [ -f "/tmp/conda-tmp/environment.yml" ]; then umask 0002 && /opt/conda/bin/conda env update -n base -f /tmp/conda-tmp/environment.yml; fi \
|
|
||||||
&& rm -rf /tmp/conda-tmp
|
|
||||||
|
|
||||||
# [Optional] Uncomment this section to install additional OS packages.
|
|
||||||
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
|
||||||
# && apt-get -y install --no-install-recommends <your-package-list-here>
|
|
@ -1,31 +0,0 @@
|
|||||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
|
||||||
// README at: https://github.com/devcontainers/templates/tree/main/src/debian
|
|
||||||
{
|
|
||||||
"name": "Debian",
|
|
||||||
"build": {
|
|
||||||
"context": "..",
|
|
||||||
"dockerfile": "Dockerfile"
|
|
||||||
},
|
|
||||||
|
|
||||||
"customizations": {
|
|
||||||
"vscode": {
|
|
||||||
"extensions": [
|
|
||||||
"ms-dotnettools.vscode-dotnet-runtime",
|
|
||||||
"svelte.svelte-vscode",
|
|
||||||
"syler.sass-indented",
|
|
||||||
"dbaeumer.vscode-eslint",
|
|
||||||
"ms-dotnettools.csharp"
|
|
||||||
],
|
|
||||||
"settings": {
|
|
||||||
"python.defaultInterpreterPath": "/opt/conda/bin/python",
|
|
||||||
"dotnet.dotnetPath": "/opt/conda/lib/dotnet/",
|
|
||||||
"omnisharp.dotNetCliPaths": [
|
|
||||||
"/opt/conda/lib/dotnet/dotnet"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"postCreateCommand": "bash -i -c 'conda init'"
|
|
||||||
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
This file is copied into the container along with environment.yml* from the
|
|
||||||
parent folder. This is done to prevent the Dockerfile COPY instruction from
|
|
||||||
failing if no environment.yml is found.
|
|
307
.gitignore
vendored
307
.gitignore
vendored
@ -1,5 +1,6 @@
|
|||||||
# Created by https://www.toptal.com/developers/gitignore/api/aspnetcore,visualstudiocode,dotnetcore,python,database,node
|
|
||||||
# Edit at https://www.toptal.com/developers/gitignore?templates=aspnetcore,visualstudiocode,dotnetcore,python,database,node
|
# Created by https://www.toptal.com/developers/gitignore/api/vscode,aspnetcore
|
||||||
|
# Edit at https://www.toptal.com/developers/gitignore?templates=vscode,aspnetcore
|
||||||
|
|
||||||
### ASPNETCore ###
|
### ASPNETCore ###
|
||||||
## Ignore Visual Studio temporary files, build results, and
|
## Ignore Visual Studio temporary files, build results, and
|
||||||
@ -29,7 +30,7 @@ bld/
|
|||||||
# Visual Studio 2015 cache/options directory
|
# Visual Studio 2015 cache/options directory
|
||||||
.vs/
|
.vs/
|
||||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||||
wwwroot/
|
#wwwroot/
|
||||||
|
|
||||||
# MSTest test Results
|
# MSTest test Results
|
||||||
[Tt]est[Rr]esult*/
|
[Tt]est[Rr]esult*/
|
||||||
@ -271,287 +272,7 @@ __pycache__/
|
|||||||
# Cake - Uncomment if you are using it
|
# Cake - Uncomment if you are using it
|
||||||
# tools/
|
# tools/
|
||||||
|
|
||||||
### Database ###
|
### vscode ###
|
||||||
*.accdb
|
|
||||||
*.db
|
|
||||||
*.dbf
|
|
||||||
*.mdb
|
|
||||||
*.sqlite3
|
|
||||||
|
|
||||||
### DotnetCore ###
|
|
||||||
# .NET Core build folders
|
|
||||||
bin/
|
|
||||||
obj/
|
|
||||||
|
|
||||||
# Common node modules locations
|
|
||||||
/node_modules
|
|
||||||
/wwwroot/node_modules
|
|
||||||
|
|
||||||
### Node ###
|
|
||||||
# Logs
|
|
||||||
logs
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
.pnpm-debug.log*
|
|
||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
|
||||||
|
|
||||||
# Runtime data
|
|
||||||
pids
|
|
||||||
*.pid
|
|
||||||
*.seed
|
|
||||||
*.pid.lock
|
|
||||||
|
|
||||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
|
||||||
lib-cov
|
|
||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
|
||||||
coverage
|
|
||||||
*.lcov
|
|
||||||
|
|
||||||
# nyc test coverage
|
|
||||||
.nyc_output
|
|
||||||
|
|
||||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
|
||||||
.grunt
|
|
||||||
|
|
||||||
# Bower dependency directory (https://bower.io/)
|
|
||||||
bower_components
|
|
||||||
|
|
||||||
# node-waf configuration
|
|
||||||
.lock-wscript
|
|
||||||
|
|
||||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
|
||||||
build/Release
|
|
||||||
|
|
||||||
# Dependency directories
|
|
||||||
jspm_packages/
|
|
||||||
|
|
||||||
# Snowpack dependency directory (https://snowpack.dev/)
|
|
||||||
web_modules/
|
|
||||||
|
|
||||||
# TypeScript cache
|
|
||||||
*.tsbuildinfo
|
|
||||||
|
|
||||||
# Optional npm cache directory
|
|
||||||
.npm
|
|
||||||
|
|
||||||
# Optional eslint cache
|
|
||||||
.eslintcache
|
|
||||||
|
|
||||||
# Microbundle cache
|
|
||||||
.rpt2_cache/
|
|
||||||
.rts2_cache_cjs/
|
|
||||||
.rts2_cache_es/
|
|
||||||
.rts2_cache_umd/
|
|
||||||
|
|
||||||
# Optional REPL history
|
|
||||||
.node_repl_history
|
|
||||||
|
|
||||||
# Output of 'npm pack'
|
|
||||||
*.tgz
|
|
||||||
|
|
||||||
# Yarn Integrity file
|
|
||||||
.yarn-integrity
|
|
||||||
|
|
||||||
# dotenv environment variables file
|
|
||||||
.env
|
|
||||||
.env.test
|
|
||||||
.env.production
|
|
||||||
|
|
||||||
# parcel-bundler cache (https://parceljs.org/)
|
|
||||||
.cache
|
|
||||||
.parcel-cache
|
|
||||||
|
|
||||||
# Next.js build output
|
|
||||||
.next
|
|
||||||
out
|
|
||||||
|
|
||||||
# Nuxt.js build / generate output
|
|
||||||
.nuxt
|
|
||||||
dist
|
|
||||||
|
|
||||||
# Gatsby files
|
|
||||||
.cache/
|
|
||||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
|
||||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
|
||||||
# public
|
|
||||||
|
|
||||||
# vuepress build output
|
|
||||||
.vuepress/dist
|
|
||||||
|
|
||||||
# Serverless directories
|
|
||||||
.serverless/
|
|
||||||
|
|
||||||
# FuseBox cache
|
|
||||||
.fusebox/
|
|
||||||
|
|
||||||
# DynamoDB Local files
|
|
||||||
.dynamodb/
|
|
||||||
|
|
||||||
# TernJS port file
|
|
||||||
.tern-port
|
|
||||||
|
|
||||||
# Stores VSCode versions used for testing VSCode extensions
|
|
||||||
.vscode-test
|
|
||||||
|
|
||||||
# yarn v2
|
|
||||||
.yarn/cache
|
|
||||||
.yarn/unplugged
|
|
||||||
.yarn/build-state.yml
|
|
||||||
.yarn/install-state.gz
|
|
||||||
.pnp.*
|
|
||||||
|
|
||||||
### Node Patch ###
|
|
||||||
# Serverless Webpack directories
|
|
||||||
.webpack/
|
|
||||||
|
|
||||||
# Optional stylelint cache
|
|
||||||
.stylelintcache
|
|
||||||
|
|
||||||
# SvelteKit build / generate output
|
|
||||||
.svelte-kit
|
|
||||||
|
|
||||||
### Python ###
|
|
||||||
# Byte-compiled / optimized / DLL files
|
|
||||||
*.py[cod]
|
|
||||||
*$py.class
|
|
||||||
|
|
||||||
# C extensions
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Distribution / packaging
|
|
||||||
.Python
|
|
||||||
build/
|
|
||||||
develop-eggs/
|
|
||||||
dist/
|
|
||||||
downloads/
|
|
||||||
eggs/
|
|
||||||
.eggs/
|
|
||||||
lib/
|
|
||||||
lib64/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
wheels/
|
|
||||||
share/python-wheels/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
MANIFEST
|
|
||||||
|
|
||||||
# PyInstaller
|
|
||||||
# Usually these files are written by a python script from a template
|
|
||||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
||||||
*.manifest
|
|
||||||
*.spec
|
|
||||||
|
|
||||||
# Installer logs
|
|
||||||
pip-log.txt
|
|
||||||
pip-delete-this-directory.txt
|
|
||||||
|
|
||||||
# Unit test / coverage reports
|
|
||||||
htmlcov/
|
|
||||||
.tox/
|
|
||||||
.nox/
|
|
||||||
.coverage
|
|
||||||
.coverage.*
|
|
||||||
nosetests.xml
|
|
||||||
coverage.xml
|
|
||||||
*.cover
|
|
||||||
*.py,cover
|
|
||||||
.hypothesis/
|
|
||||||
.pytest_cache/
|
|
||||||
cover/
|
|
||||||
|
|
||||||
# Translations
|
|
||||||
*.mo
|
|
||||||
*.pot
|
|
||||||
|
|
||||||
# Django stuff:
|
|
||||||
local_settings.py
|
|
||||||
db.sqlite3
|
|
||||||
db.sqlite3-journal
|
|
||||||
|
|
||||||
# Flask stuff:
|
|
||||||
instance/
|
|
||||||
.webassets-cache
|
|
||||||
|
|
||||||
# Scrapy stuff:
|
|
||||||
.scrapy
|
|
||||||
|
|
||||||
# Sphinx documentation
|
|
||||||
docs/_build/
|
|
||||||
|
|
||||||
# PyBuilder
|
|
||||||
.pybuilder/
|
|
||||||
target/
|
|
||||||
|
|
||||||
# Jupyter Notebook
|
|
||||||
.ipynb_checkpoints
|
|
||||||
|
|
||||||
# IPython
|
|
||||||
profile_default/
|
|
||||||
ipython_config.py
|
|
||||||
|
|
||||||
# pyenv
|
|
||||||
# For a library or package, you might want to ignore these files since the code is
|
|
||||||
# intended to run in multiple environments; otherwise, check them in:
|
|
||||||
# .python-version
|
|
||||||
|
|
||||||
# pipenv
|
|
||||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
||||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
||||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
||||||
# install all needed dependencies.
|
|
||||||
#Pipfile.lock
|
|
||||||
|
|
||||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
|
||||||
__pypackages__/
|
|
||||||
|
|
||||||
# Celery stuff
|
|
||||||
celerybeat-schedule
|
|
||||||
celerybeat.pid
|
|
||||||
|
|
||||||
# SageMath parsed files
|
|
||||||
*.sage.py
|
|
||||||
|
|
||||||
# Environments
|
|
||||||
.venv
|
|
||||||
env/
|
|
||||||
venv/
|
|
||||||
ENV/
|
|
||||||
env.bak/
|
|
||||||
venv.bak/
|
|
||||||
|
|
||||||
# Spyder project settings
|
|
||||||
.spyderproject
|
|
||||||
.spyproject
|
|
||||||
|
|
||||||
# Rope project settings
|
|
||||||
.ropeproject
|
|
||||||
|
|
||||||
# mkdocs documentation
|
|
||||||
/site
|
|
||||||
|
|
||||||
# mypy
|
|
||||||
.mypy_cache/
|
|
||||||
.dmypy.json
|
|
||||||
dmypy.json
|
|
||||||
|
|
||||||
# Pyre type checker
|
|
||||||
.pyre/
|
|
||||||
|
|
||||||
# pytype static type analyzer
|
|
||||||
.pytype/
|
|
||||||
|
|
||||||
# Cython debug symbols
|
|
||||||
cython_debug/
|
|
||||||
|
|
||||||
### VisualStudioCode ###
|
|
||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/settings.json
|
!.vscode/settings.json
|
||||||
!.vscode/tasks.json
|
!.vscode/tasks.json
|
||||||
@ -559,20 +280,4 @@ cython_debug/
|
|||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
*.code-workspace
|
*.code-workspace
|
||||||
|
|
||||||
# Local History for Visual Studio Code
|
# End of https://www.toptal.com/developers/gitignore/api/vscode,aspnetcore
|
||||||
.history/
|
|
||||||
|
|
||||||
### VisualStudioCode Patch ###
|
|
||||||
# Ignore all local history of files
|
|
||||||
.history
|
|
||||||
.ionide
|
|
||||||
|
|
||||||
# Support for Project snippet scope
|
|
||||||
!.vscode/*.code-snippets
|
|
||||||
|
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/aspnetcore,visualstudiocode,dotnetcore,python,database,node
|
|
||||||
|
|
||||||
# Props
|
|
||||||
shop-data
|
|
||||||
Props/shops
|
|
||||||
output
|
|
0
.gitmodules
vendored
0
.gitmodules
vendored
26
.vscode/launch.json
vendored
26
.vscode/launch.json
vendored
@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
// Use IntelliSense to find out which attributes exist for C# debugging
|
|
||||||
// Use hover for the description of the existing attributes
|
|
||||||
// For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md
|
|
||||||
"name": ".NET Core Launch (console)",
|
|
||||||
"type": "coreclr",
|
|
||||||
"request": "launch",
|
|
||||||
"preLaunchTask": "build",
|
|
||||||
// If you have changed target frameworks, make sure to update the program path.
|
|
||||||
"program": "${workspaceFolder}/Props.Shop/Adafruit.Tests/bin/Debug/net8.0/Props.Shop.Adafruit.Tests.dll",
|
|
||||||
"args": [],
|
|
||||||
"cwd": "${workspaceFolder}/Props.Shop/Adafruit.Tests",
|
|
||||||
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
|
|
||||||
"console": "internalConsole",
|
|
||||||
"stopAtEntry": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": ".NET Core Attach",
|
|
||||||
"type": "coreclr",
|
|
||||||
"request": "attach"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"python.defaultInterpreterPath": "/opt/conda/bin/python",
|
|
||||||
"dotnet.dotnetPath": "/opt/conda/lib/dotnet/",
|
|
||||||
"omnisharp.dotNetCliPaths": [
|
|
||||||
"/opt/conda/lib/dotnet/dotnet"
|
|
||||||
]
|
|
||||||
}
|
|
41
.vscode/tasks.json
vendored
41
.vscode/tasks.json
vendored
@ -1,41 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "2.0.0",
|
|
||||||
"tasks": [
|
|
||||||
{
|
|
||||||
"label": "build",
|
|
||||||
"command": "dotnet",
|
|
||||||
"type": "process",
|
|
||||||
"args": [
|
|
||||||
"build",
|
|
||||||
"${workspaceFolder}/Props.Shop/Props.Shop.sln",
|
|
||||||
"/property:GenerateFullPaths=true",
|
|
||||||
"/consoleloggerparameters:NoSummary;ForceNoAlign"
|
|
||||||
],
|
|
||||||
"problemMatcher": "$msCompile"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "publish",
|
|
||||||
"command": "dotnet",
|
|
||||||
"type": "process",
|
|
||||||
"args": [
|
|
||||||
"publish",
|
|
||||||
"${workspaceFolder}/Props.Shop/Props.Shop.sln",
|
|
||||||
"/property:GenerateFullPaths=true",
|
|
||||||
"/consoleloggerparameters:NoSummary;ForceNoAlign"
|
|
||||||
],
|
|
||||||
"problemMatcher": "$msCompile"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "watch",
|
|
||||||
"command": "dotnet",
|
|
||||||
"type": "process",
|
|
||||||
"args": [
|
|
||||||
"watch",
|
|
||||||
"run",
|
|
||||||
"--project",
|
|
||||||
"${workspaceFolder}/Props.Shop/Props.Shop.sln"
|
|
||||||
],
|
|
||||||
"problemMatcher": "$msCompile"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
steps:
|
|
||||||
- name: build
|
|
||||||
image: debian
|
|
||||||
commands:
|
|
||||||
- echo "This is the build step"
|
|
||||||
- name: a-test-step
|
|
||||||
image: debian
|
|
||||||
commands:
|
|
||||||
- echo "Testing.."
|
|
56
Jenkinsfile
vendored
56
Jenkinsfile
vendored
@ -1,56 +0,0 @@
|
|||||||
pipeline {
|
|
||||||
agent {
|
|
||||||
kubernetes {
|
|
||||||
cloud 'Reslate Systems'
|
|
||||||
defaultContainer 'conda'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
environment {
|
|
||||||
DOTNET_SYSTEM_GLOBALIZATION_INVARIANT = "1"
|
|
||||||
}
|
|
||||||
stages {
|
|
||||||
stage("Install") {
|
|
||||||
steps {
|
|
||||||
sh 'conda update conda -y -q'
|
|
||||||
sh 'conda env update -n base --file environment.yml'
|
|
||||||
sh 'conda run -n base dotnet restore props.sln'
|
|
||||||
sh 'npm install --prefix ./Props'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stage("Test Props.Shop") {
|
|
||||||
steps {
|
|
||||||
sh returnStatus: true, script: 'conda run -n base dotnet test --logger xunit --no-restore Props.Shop/**/*.Tests.csproj'
|
|
||||||
xunit([xUnitDotNet(excludesPattern: '', pattern: 'Props.Shop/*.Tests/TestResults/*.xml', stopProcessingIfError: true)])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stage("Publish Props.Shop") {
|
|
||||||
steps {
|
|
||||||
sh '''#!/bin/bash
|
|
||||||
for file in Props.Shop/**/*.csproj
|
|
||||||
do
|
|
||||||
conda run -n base dotnet publish --configuration Release --output output/shop-modules $file
|
|
||||||
done
|
|
||||||
'''
|
|
||||||
fingerprint 'output/shop-modules/**/Props.Shop.*'
|
|
||||||
sh 'mkdir -p ./Props/shops/'
|
|
||||||
sh 'cp ./output/shop-modules/*.dll ./output/shop-modules/*.deps.json ./Props/shops/.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stage("Test Props") {
|
|
||||||
steps {
|
|
||||||
sh returnStatus: true, script: 'conda run -n base dotnet test --logger xunit --no-restore Props.Tests/Props.Tests.csproj'
|
|
||||||
xunit([xUnitDotNet(excludesPattern: '', pattern: 'Props.Tests/TestResults/*.xml', stopProcessingIfError: true)])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stage("Publish Props") {
|
|
||||||
steps {
|
|
||||||
sh 'conda run -n base dotnet publish --configuration Release --output output/props/props-linux-x64 --runtime linux-x64 --self-contained Props'
|
|
||||||
sh 'conda run -n base dotnet publish --configuration Release --output output/props/props-win-x64 --runtime win-x64 --self-contained Props'
|
|
||||||
fingerprint 'output/props/**/Props*'
|
|
||||||
sh 'conda run -n base tar -czf output/props-linux-x64.tar.gz output/props/props-linux-x64'
|
|
||||||
sh 'conda run -n base zip -r output/props-win-x64.zip output/props/props-win-x64'
|
|
||||||
archiveArtifacts artifacts: 'output/*.tar.gz,output/*.zip', followSymlinks: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Props.Shop.Framework;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace Props.Shop.Adafruit.Tests
|
|
||||||
{
|
|
||||||
public class AdafruitShopTest
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public async void TestSearch()
|
|
||||||
{
|
|
||||||
AdafruitShop mockAdafruitShop = new AdafruitShop();
|
|
||||||
await mockAdafruitShop.Initialize(null, LoggerFactory.Create(builder =>
|
|
||||||
{
|
|
||||||
builder.AddXUnit();
|
|
||||||
}));
|
|
||||||
Assert.NotEmpty(mockAdafruitShop.Search("raspberry pi", new Filters()).ToEnumerable());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,89 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Props.Shop.Adafruit.Api;
|
|
||||||
using Props.Shop.Framework;
|
|
||||||
|
|
||||||
namespace Props.Shop.Adafruit.Tests.Api
|
|
||||||
{
|
|
||||||
public class FakeProductListingManager : IProductListingManager
|
|
||||||
{
|
|
||||||
private bool disposedValue;
|
|
||||||
public DateTime? LastDownload { get; private set; }
|
|
||||||
private ProductListingsParser parser = new ProductListingsParser();
|
|
||||||
private readonly ConcurrentDictionary<string, ProductListing> activeProductListingUrls = new ConcurrentDictionary<string, ProductListing>();
|
|
||||||
|
|
||||||
|
|
||||||
public Task<IReadOnlyDictionary<string, IList<ProductListing>>> ProductListings { get; private set; }
|
|
||||||
|
|
||||||
public async Task<ProductListing> GetProductListingFromIdentifier(string url)
|
|
||||||
{
|
|
||||||
if (disposedValue) throw new ObjectDisposedException("ProductListingManager");
|
|
||||||
await ProductListings;
|
|
||||||
return activeProductListingUrls[url];
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RefreshProductListings()
|
|
||||||
{
|
|
||||||
if (disposedValue) throw new ObjectDisposedException("ProductListingManager");
|
|
||||||
if ((LastDownload != null && DateTime.UtcNow - LastDownload <= TimeSpan.FromMilliseconds(5 * 60 * 1000)) || (ProductListings != null && !ProductListings.IsCompleted)) return;
|
|
||||||
ProductListings = DownloadListings();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task<IReadOnlyDictionary<string, IList<ProductListing>>> DownloadListings()
|
|
||||||
{
|
|
||||||
if (disposedValue) throw new ObjectDisposedException("ProductListingManager");
|
|
||||||
LastDownload = DateTime.UtcNow;
|
|
||||||
using (Stream stream = File.OpenRead("./Assets/products.json"))
|
|
||||||
{
|
|
||||||
parser.BuildProductListings(stream);
|
|
||||||
|
|
||||||
}
|
|
||||||
Dictionary<string, IList<ProductListing>> listingNames = new Dictionary<string, IList<ProductListing>>();
|
|
||||||
activeProductListingUrls.Clear();
|
|
||||||
foreach (ProductListing product in parser.ProductListings)
|
|
||||||
{
|
|
||||||
activeProductListingUrls.TryAdd(product.URL, product);
|
|
||||||
IList<ProductListing> sameProducts = listingNames.GetValueOrDefault(product.Name);
|
|
||||||
if (sameProducts == null)
|
|
||||||
{
|
|
||||||
sameProducts = new List<ProductListing>();
|
|
||||||
listingNames.Add(product.Name, sameProducts);
|
|
||||||
}
|
|
||||||
|
|
||||||
sameProducts.Add(product);
|
|
||||||
}
|
|
||||||
return Task.FromResult<IReadOnlyDictionary<string, IList<ProductListing>>>(listingNames);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void StartUpdateTimer(int delay = 300000, int period = 300000)
|
|
||||||
{
|
|
||||||
RefreshProductListings();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void StopUpdateTimer()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (!disposedValue)
|
|
||||||
{
|
|
||||||
if (disposing)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
disposedValue = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Dispose(disposing: true);
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using Props.Shop.Adafruit.Api;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace Props.Shop.Adafruit.Tests
|
|
||||||
{
|
|
||||||
public class ListingParserTest
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public void TestParsing()
|
|
||||||
{
|
|
||||||
ProductListingsParser mockParser = new ProductListingsParser();
|
|
||||||
using (Stream stream = File.OpenRead("./Assets/products.json"))
|
|
||||||
{
|
|
||||||
mockParser.BuildProductListings(stream);
|
|
||||||
}
|
|
||||||
Assert.NotEmpty(mockParser.ProductListings);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Props.Shop.Adafruit.Api;
|
|
||||||
using Props.Shop.Adafruit.Persistence;
|
|
||||||
using Props.Shop.Framework;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace Props.Shop.Adafruit.Tests.Api
|
|
||||||
{
|
|
||||||
public class LiveProductListingManagerTest
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public async Task CacheTest()
|
|
||||||
{
|
|
||||||
// TODO: Improve testability of caching system, IProductListingManager, and implement here.
|
|
||||||
//Given
|
|
||||||
ProductListingsParser parser = new ProductListingsParser();
|
|
||||||
using (Stream stream = File.OpenRead("./Assets/products.json"))
|
|
||||||
{
|
|
||||||
parser.BuildProductListings(stream);
|
|
||||||
}
|
|
||||||
Dictionary<string, IList<ProductListing>> listingNames = new Dictionary<string, IList<ProductListing>>();
|
|
||||||
foreach (ProductListing product in parser.ProductListings)
|
|
||||||
{
|
|
||||||
IList<ProductListing> sameProducts = listingNames.GetValueOrDefault(product.Name);
|
|
||||||
if (sameProducts == null)
|
|
||||||
{
|
|
||||||
sameProducts = new List<ProductListing>();
|
|
||||||
listingNames.Add(product.Name, sameProducts);
|
|
||||||
}
|
|
||||||
|
|
||||||
sameProducts.Add(product);
|
|
||||||
}
|
|
||||||
ProductListingCacheData cache = new ProductListingCacheData(listingNames);
|
|
||||||
await Task.Delay(500);
|
|
||||||
LiveProductListingManager mockLiveProductListingManager = new LiveProductListingManager(null, new Logger<LiveProductListingManager>(LoggerFactory.Create((builder) => builder.AddXUnit())), cache);
|
|
||||||
//When
|
|
||||||
mockLiveProductListingManager.RefreshProductListings();
|
|
||||||
//Then
|
|
||||||
Assert.True(cache.LastUpdatedUtc.Equals(mockLiveProductListingManager.LastDownload));
|
|
||||||
Assert.NotEmpty(await mockLiveProductListingManager.ProductListings);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Props.Shop.Adafruit.Api;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace Props.Shop.Adafruit.Tests.Api
|
|
||||||
{
|
|
||||||
public class SearchManagerTest
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public void SearchTest()
|
|
||||||
{
|
|
||||||
FakeProductListingManager stubProductListingManager = new FakeProductListingManager();
|
|
||||||
SearchManager searchManager = new SearchManager(stubProductListingManager);
|
|
||||||
stubProductListingManager.RefreshProductListings();
|
|
||||||
searchManager.Similarity = 0.8f;
|
|
||||||
Assert.NotEmpty(searchManager.Search("Raspberry Pi").ToEnumerable());
|
|
||||||
searchManager.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -1,36 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
|
||||||
<InvariantGlobalization>true</InvariantGlobalization>
|
|
||||||
<IsPackable>false</IsPackable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="MartinCostello.Logging.XUnit" Version="0.2.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
|
||||||
<PackageReference Include="System.Linq.Async" Version="5.1.0" />
|
|
||||||
<PackageReference Include="xunit" Version="2.4.1" />
|
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="coverlet.collector" Version="3.1.0">
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="XunitXml.TestLogger" Version="3.0.70" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Adafruit\Props.Shop.Adafruit.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Content Include="Assets\**">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</Content>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
@ -1,171 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Props.Shop.Adafruit.Api;
|
|
||||||
using Props.Shop.Adafruit.Persistence;
|
|
||||||
using Props.Shop.Framework;
|
|
||||||
|
|
||||||
namespace Props.Shop.Adafruit
|
|
||||||
{
|
|
||||||
public class AdafruitShop : IShop, IDisposable
|
|
||||||
{
|
|
||||||
private string workspaceDir;
|
|
||||||
private ILoggerFactory loggerFactory;
|
|
||||||
private ILogger<AdafruitShop> logger;
|
|
||||||
private SearchManager searchManager;
|
|
||||||
private Configuration configuration;
|
|
||||||
private HttpClient http;
|
|
||||||
private bool disposedValue;
|
|
||||||
|
|
||||||
public string ShopName => "Adafruit";
|
|
||||||
|
|
||||||
public string ShopDescription => "A electronic component online hardware company.";
|
|
||||||
|
|
||||||
public string ShopModuleAuthor => "Reslate";
|
|
||||||
|
|
||||||
public SupportedFeatures SupportedFeatures => new SupportedFeatures(
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
public async ValueTask Initialize(string workspaceDir, ILoggerFactory loggerFactory)
|
|
||||||
{
|
|
||||||
workspaceDir = workspaceDir ?? "";
|
|
||||||
this.workspaceDir = workspaceDir;
|
|
||||||
this.loggerFactory = loggerFactory;
|
|
||||||
logger = loggerFactory.CreateLogger<AdafruitShop>();
|
|
||||||
http = new HttpClient();
|
|
||||||
http.BaseAddress = new Uri("http://www.adafruit.com/api/");
|
|
||||||
string configPath = Path.Combine(workspaceDir, Configuration.FILE_NAME);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
configuration = JsonSerializer.Deserialize<Configuration>(File.ReadAllText(configPath));
|
|
||||||
}
|
|
||||||
catch (JsonException e)
|
|
||||||
{
|
|
||||||
logger.LogWarning("Could not read JSON file \"{0}\": {1}", configPath, e.Message);
|
|
||||||
}
|
|
||||||
catch (ArgumentException)
|
|
||||||
{
|
|
||||||
logger.LogWarning("No working directory path provided.");
|
|
||||||
}
|
|
||||||
catch (DirectoryNotFoundException)
|
|
||||||
{
|
|
||||||
logger.LogWarning("Directory \"{0}\" could not be found.", Path.GetDirectoryName(configPath));
|
|
||||||
}
|
|
||||||
catch (FileNotFoundException)
|
|
||||||
{
|
|
||||||
logger.LogWarning("File \"{0}\" could not be found.", configPath);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (configuration == null)
|
|
||||||
{
|
|
||||||
configuration = new Configuration();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ProductListingCacheData listingData = null;
|
|
||||||
string cachePath = Path.Combine(workspaceDir, ProductListingCacheData.FILE_NAME);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (Stream fileStream = File.OpenRead(cachePath))
|
|
||||||
{
|
|
||||||
listingData = await JsonSerializer.DeserializeAsync<ProductListingCacheData>(fileStream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (JsonException e)
|
|
||||||
{
|
|
||||||
logger.LogWarning("Could not read JSON file \"{0}\": {1}", cachePath, e.Message);
|
|
||||||
}
|
|
||||||
catch (ArgumentException)
|
|
||||||
{
|
|
||||||
logger.LogWarning("No working directory path provided.");
|
|
||||||
}
|
|
||||||
catch (DirectoryNotFoundException)
|
|
||||||
{
|
|
||||||
logger.LogWarning("Directory \"{0}\" could not be found.", Path.GetDirectoryName(cachePath));
|
|
||||||
}
|
|
||||||
catch (FileNotFoundException)
|
|
||||||
{
|
|
||||||
logger.LogWarning("File \"{0}\" could not be found.", cachePath);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (configuration == null)
|
|
||||||
{
|
|
||||||
configuration = new Configuration();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LiveProductListingManager productListingManager = new LiveProductListingManager(http, loggerFactory.CreateLogger<LiveProductListingManager>(), listingData, configuration.MinDownloadInterval);
|
|
||||||
this.searchManager = new SearchManager(productListingManager, configuration.Similarity);
|
|
||||||
productListingManager.StartUpdateTimer(delay: 0, configuration.CacheLifespan);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ProductListing> GetProductFromIdentifier(string identifier)
|
|
||||||
{
|
|
||||||
return await searchManager.ProductListingManager.GetProductListingFromIdentifier(identifier);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IAsyncEnumerable<ProductListing> Search(string query, Filters filters)
|
|
||||||
{
|
|
||||||
return searchManager.Search(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async ValueTask SaveData()
|
|
||||||
{
|
|
||||||
if (workspaceDir != null)
|
|
||||||
{
|
|
||||||
logger.LogDebug("Saving data in \"{0}\"...", workspaceDir);
|
|
||||||
string configurationPath = Path.Combine(workspaceDir, Configuration.FILE_NAME);
|
|
||||||
File.Delete(configurationPath);
|
|
||||||
await File.WriteAllTextAsync(Path.Combine(workspaceDir, Configuration.FILE_NAME), JsonSerializer.Serialize(configuration));
|
|
||||||
|
|
||||||
string productListingCachePath = Path.Combine(workspaceDir, ProductListingCacheData.FILE_NAME);
|
|
||||||
File.Delete(productListingCachePath);
|
|
||||||
using (Stream fileStream = File.OpenWrite(productListingCachePath))
|
|
||||||
{
|
|
||||||
await JsonSerializer.SerializeAsync(fileStream, new ProductListingCacheData(await searchManager.ProductListingManager.ProductListings));
|
|
||||||
}
|
|
||||||
logger.LogDebug("Completed saving data.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async ValueTask DisposeAsync()
|
|
||||||
{
|
|
||||||
Dispose(true);
|
|
||||||
await DisposeAsyncCore();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual async ValueTask DisposeAsyncCore()
|
|
||||||
{
|
|
||||||
await SaveData();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (!disposedValue)
|
|
||||||
{
|
|
||||||
if (disposing)
|
|
||||||
{
|
|
||||||
http.Dispose();
|
|
||||||
searchManager.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
disposedValue = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Dispose(disposing: true);
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Props.Shop.Framework;
|
|
||||||
|
|
||||||
namespace Props.Shop.Adafruit.Api
|
|
||||||
{
|
|
||||||
public interface IProductListingManager : IDisposable
|
|
||||||
{
|
|
||||||
public Task<IReadOnlyDictionary<string, IList<ProductListing>>> ProductListings { get; }
|
|
||||||
public void RefreshProductListings();
|
|
||||||
public void StartUpdateTimer(int delay = 1000 * 60 * 5, int period = 1000 * 60 * 5);
|
|
||||||
public void StopUpdateTimer();
|
|
||||||
|
|
||||||
public DateTime? LastDownload { get; }
|
|
||||||
|
|
||||||
public Task<ProductListing> GetProductListingFromIdentifier(string url);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Net.Http;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using Props.Shop.Framework;
|
|
||||||
|
|
||||||
namespace Props.Shop.Adafruit.Api
|
|
||||||
{
|
|
||||||
public class ProductListingsParser
|
|
||||||
{
|
|
||||||
public IEnumerable<ProductListing> ProductListings { get; private set; }
|
|
||||||
public void BuildProductListings(Stream stream)
|
|
||||||
{
|
|
||||||
using (StreamReader streamReader = new StreamReader(stream))
|
|
||||||
{
|
|
||||||
DateTime startTime = DateTime.UtcNow;
|
|
||||||
dynamic data = JArray.Load(new JsonTextReader(streamReader));
|
|
||||||
List<ProductListing> parsed = new List<ProductListing>();
|
|
||||||
foreach (dynamic item in data)
|
|
||||||
{
|
|
||||||
if (item.products_discontinued == 0)
|
|
||||||
{
|
|
||||||
ProductListing res = new ProductListing();
|
|
||||||
res.TimeFetchedUtc = startTime;
|
|
||||||
res.Name = item.product_name;
|
|
||||||
res.LowerPrice = item.product_price;
|
|
||||||
res.UpperPrice = res.LowerPrice;
|
|
||||||
foreach (dynamic discount in item.discount_pricing)
|
|
||||||
{
|
|
||||||
if (discount.discounted_price < res.LowerPrice)
|
|
||||||
{
|
|
||||||
res.LowerPrice = discount.discounted_price;
|
|
||||||
}
|
|
||||||
if (discount.discounted_price > res.UpperPrice)
|
|
||||||
{
|
|
||||||
res.UpperPrice = discount.discounted_price;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res.URL = item.product_url;
|
|
||||||
res.InStock = item.product_stock > 0;
|
|
||||||
parsed.Add(res);
|
|
||||||
res.Identifier = res.URL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ProductListings = parsed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,125 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Props.Shop.Adafruit.Persistence;
|
|
||||||
using Props.Shop.Framework;
|
|
||||||
|
|
||||||
namespace Props.Shop.Adafruit.Api
|
|
||||||
{
|
|
||||||
public class LiveProductListingManager : IProductListingManager
|
|
||||||
{
|
|
||||||
private ILogger<LiveProductListingManager> logger;
|
|
||||||
private bool disposedValue;
|
|
||||||
private int minDownloadInterval;
|
|
||||||
public DateTime? LastDownload { get; private set; }
|
|
||||||
private object refreshLock = new object();
|
|
||||||
private volatile Task<IReadOnlyDictionary<string, IList<ProductListing>>> productListingsTask;
|
|
||||||
|
|
||||||
public Task<IReadOnlyDictionary<string, IList<ProductListing>>> ProductListings => productListingsTask;
|
|
||||||
private readonly ConcurrentDictionary<string, ProductListing> identifierMap = new ConcurrentDictionary<string, ProductListing>();
|
|
||||||
|
|
||||||
private ProductListingsParser parser = new ProductListingsParser();
|
|
||||||
private HttpClient httpClient;
|
|
||||||
private Timer updateTimer;
|
|
||||||
|
|
||||||
public LiveProductListingManager(HttpClient httpClient, ILogger<LiveProductListingManager> logger, ProductListingCacheData productListingCacheData = null, int minDownloadInterval = 5 * 60 * 1000)
|
|
||||||
{
|
|
||||||
this.logger = logger;
|
|
||||||
this.minDownloadInterval = minDownloadInterval;
|
|
||||||
this.httpClient = httpClient;
|
|
||||||
if (productListingCacheData != null)
|
|
||||||
{
|
|
||||||
productListingsTask = Task.FromResult(productListingCacheData.ProductListings);
|
|
||||||
LastDownload = productListingCacheData.LastUpdatedUtc;
|
|
||||||
logger.LogInformation("{0} Cached product listings loaded. Listing saved at {1}", productListingCacheData.ProductListings.Count, productListingCacheData.LastUpdatedUtc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RefreshProductListings()
|
|
||||||
{
|
|
||||||
lock (refreshLock)
|
|
||||||
{
|
|
||||||
if (disposedValue) throw new ObjectDisposedException("ProductListingManager");
|
|
||||||
if ((LastDownload != null && DateTime.UtcNow - LastDownload <= TimeSpan.FromMilliseconds(minDownloadInterval)) || (productListingsTask != null && !productListingsTask.IsCompleted)) return;
|
|
||||||
LastDownload = DateTime.UtcNow;
|
|
||||||
logger.LogDebug("Refreshing listings ({0}).", LastDownload);
|
|
||||||
productListingsTask = DownloadListings();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ProductListing> GetProductListingFromIdentifier(string identifier)
|
|
||||||
{
|
|
||||||
if (disposedValue) throw new ObjectDisposedException("ProductListingManager");
|
|
||||||
await productListingsTask;
|
|
||||||
return identifierMap[identifier];
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<IReadOnlyDictionary<string, IList<ProductListing>>> DownloadListings()
|
|
||||||
{
|
|
||||||
if (disposedValue) throw new ObjectDisposedException("ProductListingManager");
|
|
||||||
logger.LogDebug("Beginning listing database download.");
|
|
||||||
HttpResponseMessage responseMessage = await httpClient.GetAsync("products");
|
|
||||||
parser.BuildProductListings(responseMessage.Content.ReadAsStream());
|
|
||||||
logger.LogDebug("Listing database parsed.");
|
|
||||||
Dictionary<string, IList<ProductListing>> listingNames = new Dictionary<string, IList<ProductListing>>();
|
|
||||||
identifierMap.Clear();
|
|
||||||
foreach (ProductListing product in parser.ProductListings)
|
|
||||||
{
|
|
||||||
identifierMap.TryAdd(product.Identifier, product);
|
|
||||||
IList<ProductListing> sameProducts = listingNames.GetValueOrDefault(product.Name);
|
|
||||||
if (sameProducts == null)
|
|
||||||
{
|
|
||||||
sameProducts = new List<ProductListing>();
|
|
||||||
listingNames.Add(product.Name, sameProducts);
|
|
||||||
}
|
|
||||||
|
|
||||||
sameProducts.Add(product);
|
|
||||||
}
|
|
||||||
logger.LogDebug("Downloaded listings organized.");
|
|
||||||
return listingNames;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void StartUpdateTimer(int delay = 1000 * 60 * 5, int period = 1000 * 60 * 5)
|
|
||||||
{
|
|
||||||
if (disposedValue) throw new ObjectDisposedException("ProductListingManager");
|
|
||||||
if (updateTimer != null) throw new InvalidOperationException("Update timer already started.");
|
|
||||||
logger.LogInformation("Starting update timer.");
|
|
||||||
updateTimer = new Timer((state) =>
|
|
||||||
{
|
|
||||||
RefreshProductListings();
|
|
||||||
}, null, delay, period);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void StopUpdateTimer()
|
|
||||||
{
|
|
||||||
if (disposedValue) throw new ObjectDisposedException("ProductListingManager");
|
|
||||||
if (updateTimer != null) throw new InvalidOperationException("Update timer not started.");
|
|
||||||
logger.LogInformation("Stopping update timer.");
|
|
||||||
updateTimer.Dispose();
|
|
||||||
updateTimer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (!disposedValue)
|
|
||||||
{
|
|
||||||
if (disposing)
|
|
||||||
{
|
|
||||||
updateTimer?.Dispose();
|
|
||||||
updateTimer = null;
|
|
||||||
}
|
|
||||||
disposedValue = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Dispose(disposing: true);
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,61 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using FuzzySharp;
|
|
||||||
using FuzzySharp.Extractor;
|
|
||||||
using Props.Shop.Framework;
|
|
||||||
|
|
||||||
namespace Props.Shop.Adafruit.Api
|
|
||||||
{
|
|
||||||
public class SearchManager : IDisposable
|
|
||||||
{
|
|
||||||
public float Similarity { get; set; }
|
|
||||||
public IProductListingManager ProductListingManager { get; private set; }
|
|
||||||
private bool disposedValue;
|
|
||||||
|
|
||||||
public SearchManager(IProductListingManager productListingManager, float similarity = 0.8f)
|
|
||||||
{
|
|
||||||
this.ProductListingManager = productListingManager ?? throw new ArgumentNullException("productListingManager");
|
|
||||||
this.Similarity = similarity;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async IAsyncEnumerable<ProductListing> Search(string query)
|
|
||||||
{
|
|
||||||
// TODO: Implement indexed search.
|
|
||||||
if (ProductListingManager.ProductListings == null) {
|
|
||||||
ProductListingManager.RefreshProductListings();
|
|
||||||
}
|
|
||||||
IReadOnlyDictionary<string, IList<ProductListing>> productListings = await ProductListingManager.ProductListings;
|
|
||||||
if (productListings == null) throw new InvalidAsynchronousStateException("productListings can't be null");
|
|
||||||
foreach (ExtractedResult<string> listingNames in Process.ExtractAll(query, productListings.Keys, cutoff: (int)(Similarity * 100)))
|
|
||||||
{
|
|
||||||
foreach (ProductListing same in productListings[listingNames.Value])
|
|
||||||
{
|
|
||||||
yield return same;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (!disposedValue)
|
|
||||||
{
|
|
||||||
if (disposing)
|
|
||||||
{
|
|
||||||
ProductListingManager.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
disposedValue = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Dispose(disposing: true);
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
namespace Props.Shop.Adafruit.Persistence
|
|
||||||
{
|
|
||||||
public class Configuration
|
|
||||||
{
|
|
||||||
public const string FILE_NAME = "config.json";
|
|
||||||
public int MinDownloadInterval { get; set; }
|
|
||||||
public int CacheLifespan { get; set; }
|
|
||||||
public float Similarity { get; set; }
|
|
||||||
|
|
||||||
public Configuration()
|
|
||||||
{
|
|
||||||
MinDownloadInterval = 5 * 60 * 1000;
|
|
||||||
Similarity = 0.8f;
|
|
||||||
CacheLifespan = 5 * 60 * 1000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using Props.Shop.Framework;
|
|
||||||
|
|
||||||
namespace Props.Shop.Adafruit.Persistence
|
|
||||||
{
|
|
||||||
public class ProductListingCacheData
|
|
||||||
{
|
|
||||||
public const string FILE_NAME = "Product-listing-cache.json";
|
|
||||||
public DateTime LastUpdatedUtc { get; set; }
|
|
||||||
public IReadOnlyDictionary<string, IList<ProductListing>> ProductListings { get; set; }
|
|
||||||
|
|
||||||
public ProductListingCacheData(IReadOnlyDictionary<string, IList<ProductListing>> productListings)
|
|
||||||
{
|
|
||||||
this.ProductListings = productListings;
|
|
||||||
LastUpdatedUtc = DateTime.UtcNow;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ProductListingCacheData()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
|
||||||
<InvariantGlobalization>true</InvariantGlobalization>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="FuzzySharp" Version="2.0.2" />
|
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Framework\Props.Shop.Framework.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
@ -1,8 +0,0 @@
|
|||||||
namespace Props.Shop.Framework
|
|
||||||
{
|
|
||||||
public enum Currency
|
|
||||||
{
|
|
||||||
CAD,
|
|
||||||
USD
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,108 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace Props.Shop.Framework
|
|
||||||
{
|
|
||||||
public class Filters
|
|
||||||
{
|
|
||||||
public Currency Currency { get; set; } = Currency.CAD;
|
|
||||||
private float minRatingNormalized = 0.8f;
|
|
||||||
public int MinRating
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return (int)(minRatingNormalized * 100f);
|
|
||||||
}
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (value < 0 || value > 100) return;
|
|
||||||
minRatingNormalized = value / 100f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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 KeepUnknownReviewCount { get; set; } = true;
|
|
||||||
public bool EnableMaxShipping { get; set; }
|
|
||||||
private int maxShippingFee;
|
|
||||||
|
|
||||||
public int MaxShippingFee
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return maxShippingFee;
|
|
||||||
}
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (EnableMaxShipping) maxShippingFee = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public bool KeepUnknownShipping { get; set; } = true;
|
|
||||||
|
|
||||||
public override bool Equals(object obj)
|
|
||||||
{
|
|
||||||
if (obj == null || GetType() != obj.GetType())
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Filters other = (Filters)obj;
|
|
||||||
return
|
|
||||||
Currency == other.Currency &&
|
|
||||||
MinRating == other.MinRating &&
|
|
||||||
KeepUnrated == other.KeepUnrated &&
|
|
||||||
EnableUpperPrice == other.EnableUpperPrice &&
|
|
||||||
UpperPrice == other.UpperPrice &&
|
|
||||||
LowerPrice == other.LowerPrice &&
|
|
||||||
MinPurchases == other.MinPurchases &&
|
|
||||||
KeepUnknownPurchaseCount == other.KeepUnknownPurchaseCount &&
|
|
||||||
MinReviews == other.MinReviews &&
|
|
||||||
KeepUnknownReviewCount == other.KeepUnknownReviewCount &&
|
|
||||||
EnableMaxShipping == other.EnableMaxShipping &&
|
|
||||||
MaxShippingFee == other.MaxShippingFee &&
|
|
||||||
KeepUnknownShipping == other.KeepUnknownShipping;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return HashCode.Combine(
|
|
||||||
Currency,
|
|
||||||
MinRating,
|
|
||||||
UpperPrice,
|
|
||||||
LowerPrice,
|
|
||||||
MinPurchases,
|
|
||||||
MinReviews,
|
|
||||||
MaxShippingFee);
|
|
||||||
}
|
|
||||||
public Filters Copy()
|
|
||||||
{
|
|
||||||
return (Filters)this.MemberwiseClone();
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Validate(ProductListing listing)
|
|
||||||
{
|
|
||||||
if (listing.Shipping == null && !KeepUnknownShipping || (EnableMaxShipping && listing.Shipping > MaxShippingFee)) return false;
|
|
||||||
float shippingDifference = listing.Shipping != null ? listing.Shipping.Value : 0;
|
|
||||||
if (!(listing.LowerPrice + shippingDifference >= LowerPrice && (!EnableUpperPrice || listing.UpperPrice + shippingDifference <= UpperPrice))) return false;
|
|
||||||
if ((listing.Rating == null && !KeepUnrated) && MinRating > (listing.Rating == null ? 0 : listing.Rating)) return false;
|
|
||||||
if ((listing.PurchaseCount == null && !KeepUnknownPurchaseCount) || MinPurchases > (listing.PurchaseCount == null ? 0 : listing.PurchaseCount)) return false;
|
|
||||||
if ((listing.ReviewCount == null && !KeepUnknownReviewCount) || MinReviews > (listing.ReviewCount == null ? 0 : listing.ReviewCount)) return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace Props.Shop.Framework
|
|
||||||
{
|
|
||||||
public interface IOption
|
|
||||||
{
|
|
||||||
public string Name { get; }
|
|
||||||
public string Description { get; }
|
|
||||||
public bool Required { get; }
|
|
||||||
public string GetValue();
|
|
||||||
public bool SetValue(string value);
|
|
||||||
public Type Type { get; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Props.Shop.Framework
|
|
||||||
{
|
|
||||||
public interface IShop : IAsyncDisposable
|
|
||||||
{
|
|
||||||
string ShopName { get; }
|
|
||||||
string ShopDescription { get; }
|
|
||||||
string ShopModuleAuthor { get; }
|
|
||||||
|
|
||||||
public IAsyncEnumerable<ProductListing> Search(string query, Filters filters);
|
|
||||||
|
|
||||||
public Task<ProductListing> GetProductFromIdentifier(string identifier);
|
|
||||||
|
|
||||||
ValueTask Initialize(string workspaceDir, ILoggerFactory loggerFactory);
|
|
||||||
public SupportedFeatures SupportedFeatures { get; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,55 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace Props.Shop.Framework
|
|
||||||
{
|
|
||||||
public class ProductListing
|
|
||||||
{
|
|
||||||
public float LowerPrice { get; set; }
|
|
||||||
public float UpperPrice { get; set; }
|
|
||||||
public float? Shipping { get; set; }
|
|
||||||
public string Name { get; set; }
|
|
||||||
public string URL { get; set; }
|
|
||||||
public string ImageURL { get; set; }
|
|
||||||
public float? Rating { get; set; }
|
|
||||||
public int? PurchaseCount { get; set; }
|
|
||||||
public int? ReviewCount { get; set; }
|
|
||||||
public bool ConvertedPrices { get; set; }
|
|
||||||
public bool? InStock { get; set; }
|
|
||||||
public string Identifier { get; set; }
|
|
||||||
public DateTime TimeFetchedUtc { get; set; }
|
|
||||||
|
|
||||||
public override bool Equals(object obj)
|
|
||||||
{
|
|
||||||
if (obj == null || GetType() != obj.GetType())
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ProductListing other = obj as ProductListing;
|
|
||||||
return
|
|
||||||
this.LowerPrice == other.LowerPrice &&
|
|
||||||
this.UpperPrice == other.UpperPrice &&
|
|
||||||
this.Shipping == other.Shipping &&
|
|
||||||
this.Name == other.Name &&
|
|
||||||
this.URL == other.URL &&
|
|
||||||
this.ImageURL == other.ImageURL &&
|
|
||||||
this.Rating == other.Rating &&
|
|
||||||
this.PurchaseCount == other.PurchaseCount &&
|
|
||||||
this.ReviewCount == other.ReviewCount &&
|
|
||||||
this.ConvertedPrices == other.ConvertedPrices &&
|
|
||||||
this.InStock == other.InStock &&
|
|
||||||
this.Identifier == other.Identifier &&
|
|
||||||
this.TimeFetchedUtc == other.TimeFetchedUtc;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return (Name, URL, UpperPrice, LowerPrice, ImageURL).GetHashCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ProductListing Copy()
|
|
||||||
{
|
|
||||||
return MemberwiseClone() as ProductListing;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
@ -1,20 +0,0 @@
|
|||||||
namespace Props.Shop.Framework
|
|
||||||
{
|
|
||||||
public class SupportedFeatures
|
|
||||||
{
|
|
||||||
bool Shipping { get; }
|
|
||||||
bool Rating { get; }
|
|
||||||
bool ReviewCount { get; }
|
|
||||||
bool PurchaseCount { get; }
|
|
||||||
bool InStock { get; }
|
|
||||||
|
|
||||||
public SupportedFeatures(bool shipping, bool rating, bool reviewCount, bool purchaseCount, bool inStock)
|
|
||||||
{
|
|
||||||
this.Shipping = shipping;
|
|
||||||
this.Rating = rating;
|
|
||||||
this.ReviewCount = reviewCount;
|
|
||||||
this.PurchaseCount = purchaseCount;
|
|
||||||
this.InStock = inStock;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<InvariantGlobalization>true</InvariantGlobalization>
|
|
||||||
<IsPackable>false</IsPackable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
|
|
||||||
<PackageReference Include="xunit" Version="2.4.1" />
|
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="coverlet.collector" Version="3.1.0">
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="XunitXml.TestLogger" Version="3.0.70" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
@ -1,12 +0,0 @@
|
|||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace Props.Tests;
|
|
||||||
|
|
||||||
public class UnitTest1
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public void Test1()
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
[*.{js,jsx,ts,tsx,vue}]
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 4
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
insert_final_newline = true
|
|
||||||
quote_type = double
|
|
@ -1,31 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"es2021": true,
|
|
||||||
"node": true,
|
|
||||||
},
|
|
||||||
"extends": "eslint:recommended",
|
|
||||||
"parser": "@babel/eslint-parser",
|
|
||||||
"rules": {
|
|
||||||
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
|
|
||||||
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
|
|
||||||
"indent": [
|
|
||||||
"error",
|
|
||||||
4
|
|
||||||
],
|
|
||||||
"quotes": [
|
|
||||||
"error",
|
|
||||||
"double"
|
|
||||||
],
|
|
||||||
"semi": [
|
|
||||||
"error",
|
|
||||||
"always"
|
|
||||||
],
|
|
||||||
"comma-dangle": ["error", "only-multiline"],
|
|
||||||
"space-before-function-paren": ["error", {
|
|
||||||
"anonymous": "always",
|
|
||||||
"named": "never",
|
|
||||||
"asyncArrow": "always"
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
};
|
|
36
Props/.vscode/launch.json
vendored
36
Props/.vscode/launch.json
vendored
@ -1,36 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
// Use IntelliSense to find out which attributes exist for C# debugging
|
|
||||||
// Use hover for the description of the existing attributes
|
|
||||||
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
|
|
||||||
"name": ".NET Core Launch (web)",
|
|
||||||
"type": "coreclr",
|
|
||||||
"request": "launch",
|
|
||||||
"preLaunchTask": "build",
|
|
||||||
// If you have changed target frameworks, make sure to update the program path.
|
|
||||||
"program": "${workspaceFolder}/bin/Debug/net7.0/Props.dll",
|
|
||||||
"args": [],
|
|
||||||
"cwd": "${workspaceFolder}",
|
|
||||||
"stopAtEntry": false,
|
|
||||||
// Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
|
|
||||||
"serverReadyAction": {
|
|
||||||
"action": "openExternally",
|
|
||||||
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
|
|
||||||
},
|
|
||||||
"env": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
},
|
|
||||||
"sourceFileMap": {
|
|
||||||
"/Views": "${workspaceFolder}/Views"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": ".NET Core Attach",
|
|
||||||
"type": "coreclr",
|
|
||||||
"request": "attach",
|
|
||||||
"processId": "${command:pickProcess}"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
9
Props/.vscode/settings.json
vendored
9
Props/.vscode/settings.json
vendored
@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"todo-tree.filtering.excludeGlobs": [
|
|
||||||
"**/node_modules",
|
|
||||||
"**/wwwroot"
|
|
||||||
],
|
|
||||||
"todo-tree.regex.regex": "((//|#|<!--|;|@\\*|/\\*|^)\\s*($TAGS)|^\\s*- \\[ \\])",
|
|
||||||
"todo-tree.regex.subTagRegex": "(\\*@)",
|
|
||||||
"editor.formatOnSave": true,
|
|
||||||
}
|
|
51
Props/.vscode/tasks.json
vendored
51
Props/.vscode/tasks.json
vendored
@ -1,51 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "2.0.0",
|
|
||||||
"tasks": [
|
|
||||||
{
|
|
||||||
"label": "build",
|
|
||||||
"command": "dotnet",
|
|
||||||
"type": "process",
|
|
||||||
"args": [
|
|
||||||
"build",
|
|
||||||
"${workspaceFolder}/Props.csproj",
|
|
||||||
"/property:GenerateFullPaths=true",
|
|
||||||
"/consoleloggerparameters:NoSummary"
|
|
||||||
],
|
|
||||||
"problemMatcher": "$msCompile"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "publish",
|
|
||||||
"command": "dotnet",
|
|
||||||
"type": "process",
|
|
||||||
"args": [
|
|
||||||
"publish",
|
|
||||||
"${workspaceFolder}/Props.csproj",
|
|
||||||
"/property:GenerateFullPaths=true",
|
|
||||||
"/consoleloggerparameters:NoSummary"
|
|
||||||
],
|
|
||||||
"problemMatcher": "$msCompile"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "watch",
|
|
||||||
"command": "dotnet",
|
|
||||||
"type": "process",
|
|
||||||
"args": [
|
|
||||||
"watch",
|
|
||||||
"run",
|
|
||||||
"${workspaceFolder}/Props.csproj",
|
|
||||||
"/property:GenerateFullPaths=true",
|
|
||||||
"/consoleloggerparameters:NoSummary"
|
|
||||||
],
|
|
||||||
"problemMatcher": "$msCompile"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "reset database",
|
|
||||||
"command": "py",
|
|
||||||
"type": "process",
|
|
||||||
"args": [
|
|
||||||
"${workspaceFolder}/../scripts/reset_db.py"
|
|
||||||
],
|
|
||||||
"problemMatcher": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Microsoft.AspNetCore.Hosting;
|
|
||||||
using Microsoft.AspNetCore.Identity;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Props.Data;
|
|
||||||
using Props.Models;
|
|
||||||
|
|
||||||
[assembly: HostingStartup(typeof(Props.Areas.Identity.IdentityHostingStartup))]
|
|
||||||
namespace Props.Areas.Identity
|
|
||||||
{
|
|
||||||
public class IdentityHostingStartup : IHostingStartup
|
|
||||||
{
|
|
||||||
public void Configure(IWebHostBuilder builder)
|
|
||||||
{
|
|
||||||
builder.ConfigureServices((context, services) => {
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
@page
|
|
||||||
@model ConfirmEmailModel
|
|
||||||
@{
|
|
||||||
ViewData["Title"] = "Confirm email";
|
|
||||||
}
|
|
||||||
|
|
||||||
<h1>@ViewData["Title"]</h1>
|
|
@ -1,48 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Identity;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
|
||||||
using Microsoft.AspNetCore.WebUtilities;
|
|
||||||
using Props.Models;
|
|
||||||
using Props.Models.User;
|
|
||||||
|
|
||||||
namespace Props.Areas.Identity.Pages.Account
|
|
||||||
{
|
|
||||||
[AllowAnonymous]
|
|
||||||
public class ConfirmEmailModel : PageModel
|
|
||||||
{
|
|
||||||
private readonly UserManager<ApplicationUser> _userManager;
|
|
||||||
|
|
||||||
public ConfirmEmailModel(UserManager<ApplicationUser> userManager)
|
|
||||||
{
|
|
||||||
_userManager = userManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
[TempData]
|
|
||||||
public string StatusMessage { get; set; }
|
|
||||||
|
|
||||||
public async Task<IActionResult> OnGetAsync(string userId, string code)
|
|
||||||
{
|
|
||||||
if (userId == null || code == null)
|
|
||||||
{
|
|
||||||
return RedirectToPage("/Index");
|
|
||||||
}
|
|
||||||
|
|
||||||
var user = await _userManager.FindByIdAsync(userId);
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
return NotFound($"Unable to load user with ID '{userId}'.");
|
|
||||||
}
|
|
||||||
|
|
||||||
code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code));
|
|
||||||
var result = await _userManager.ConfirmEmailAsync(user, code);
|
|
||||||
StatusMessage = result.Succeeded ? "Thank you for confirming your email." : "Error confirming your email.";
|
|
||||||
return Page();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,83 +0,0 @@
|
|||||||
@page
|
|
||||||
@model LoginModel
|
|
||||||
|
|
||||||
@{
|
|
||||||
ViewData["Title"] = "Log in";
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="jumbotron text-center">
|
|
||||||
<img alt="Props logo" src="~/images/logo-simplified.svg" class="img-fluid" style="max-height: 150px;"
|
|
||||||
asp-append-version="true" />
|
|
||||||
<h1 class="mt-3 mb-4 display-1">@ViewData["Title"]</h1>
|
|
||||||
<p>Welcome back!</p>
|
|
||||||
</div>
|
|
||||||
<div class="jumbotron sub flex-grow-1">
|
|
||||||
<div class="py-4 row justify-content-md-center">
|
|
||||||
<div class="col-md-4">
|
|
||||||
<form id="account" method="post">
|
|
||||||
<h4>Use a local account to log in.</h4>
|
|
||||||
<hr />
|
|
||||||
<div asp-validation-summary="All" class="text-danger"></div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label asp-for="Input.Email" class="form-label"></label>
|
|
||||||
<input asp-for="Input.Email" class="form-control" />
|
|
||||||
<span asp-validation-for="Input.Email" class="text-danger"></span>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label asp-for="Input.Password" class="form-label"></label>
|
|
||||||
<input asp-for="Input.Password" class="form-control" />
|
|
||||||
<span asp-validation-for="Input.Password" class="text-danger"></span>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<div class="form-check">
|
|
||||||
<input asp-for="Input.RememberMe" class="form-check-input" />
|
|
||||||
<label asp-for="Input.RememberMe" class="form-check-label">
|
|
||||||
@Html.DisplayNameFor(m => m.Input.RememberMe)
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<button type="submit" class="btn btn-primary">Log in</button>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg">
|
|
||||||
<a class="link-secondary" id="forgot-password" asp-page="./ForgotPassword">Forgot your
|
|
||||||
password?</a>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg">
|
|
||||||
<a class="link-secondary" asp-page="./Register" asp-route-returnUrl="@Model.ReturnUrl">Register
|
|
||||||
as a new user</a>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg">
|
|
||||||
<a class="link-secondary" id="resend-confirmation" asp-page="./ResendEmailConfirmation">Resend
|
|
||||||
email confirmation</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
@if ((Model.ExternalLogins?.Count ?? 0) != 0)
|
|
||||||
{
|
|
||||||
<div class="col-md-6 md-offset-2">
|
|
||||||
<h4>Use another service to log in.</h4>
|
|
||||||
<hr />
|
|
||||||
<form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post"
|
|
||||||
class="form-horizontal">
|
|
||||||
<div>
|
|
||||||
<p>
|
|
||||||
@foreach (var provider in Model.ExternalLogins)
|
|
||||||
{
|
|
||||||
<button type="submit" class="btn btn-primary" name="provider" value="@provider.Name"
|
|
||||||
title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
|
|
||||||
}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
@section Scripts {
|
|
||||||
<partial name="_ValidationScriptsPartial" />
|
|
||||||
}
|
|
@ -1,112 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text.Encodings.Web;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.Authentication;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Identity;
|
|
||||||
using Microsoft.AspNetCore.Identity.UI.Services;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Props.Models;
|
|
||||||
using Props.Models.User;
|
|
||||||
|
|
||||||
namespace Props.Areas.Identity.Pages.Account
|
|
||||||
{
|
|
||||||
[AllowAnonymous]
|
|
||||||
public class LoginModel : PageModel
|
|
||||||
{
|
|
||||||
private readonly UserManager<ApplicationUser> _userManager;
|
|
||||||
private readonly SignInManager<ApplicationUser> _signInManager;
|
|
||||||
private readonly ILogger<LoginModel> _logger;
|
|
||||||
|
|
||||||
public LoginModel(SignInManager<ApplicationUser> signInManager,
|
|
||||||
ILogger<LoginModel> logger,
|
|
||||||
UserManager<ApplicationUser> userManager)
|
|
||||||
{
|
|
||||||
_userManager = userManager;
|
|
||||||
_signInManager = signInManager;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
[BindProperty]
|
|
||||||
public InputModel Input { get; set; }
|
|
||||||
|
|
||||||
public IList<AuthenticationScheme> ExternalLogins { get; set; }
|
|
||||||
|
|
||||||
public string ReturnUrl { get; set; }
|
|
||||||
|
|
||||||
[TempData]
|
|
||||||
public string ErrorMessage { get; set; }
|
|
||||||
|
|
||||||
public class InputModel
|
|
||||||
{
|
|
||||||
[Required]
|
|
||||||
[EmailAddress]
|
|
||||||
public string Email { get; set; }
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
[DataType(DataType.Password)]
|
|
||||||
public string Password { get; set; }
|
|
||||||
|
|
||||||
[Display(Name = "Remember me?")]
|
|
||||||
public bool RememberMe { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task OnGetAsync(string returnUrl = null)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(ErrorMessage))
|
|
||||||
{
|
|
||||||
ModelState.AddModelError(string.Empty, ErrorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
returnUrl ??= Url.Content("~/");
|
|
||||||
|
|
||||||
// Clear the existing external cookie to ensure a clean login process
|
|
||||||
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
|
|
||||||
|
|
||||||
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
|
|
||||||
|
|
||||||
ReturnUrl = returnUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
|
|
||||||
{
|
|
||||||
returnUrl ??= Url.Content("~/");
|
|
||||||
|
|
||||||
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
|
|
||||||
|
|
||||||
if (ModelState.IsValid)
|
|
||||||
{
|
|
||||||
// This doesn't count login failures towards account lockout
|
|
||||||
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
|
|
||||||
var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
|
|
||||||
if (result.Succeeded)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("User logged in.");
|
|
||||||
return LocalRedirect(returnUrl);
|
|
||||||
}
|
|
||||||
if (result.RequiresTwoFactor)
|
|
||||||
{
|
|
||||||
return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe });
|
|
||||||
}
|
|
||||||
if (result.IsLockedOut)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("User account locked out.");
|
|
||||||
return RedirectToPage("./Lockout");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
|
|
||||||
return Page();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we got this far, something failed, redisplay form
|
|
||||||
return Page();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
@page
|
|
||||||
@model LogoutModel
|
|
||||||
@{
|
|
||||||
ViewData["Title"] = "Log out";
|
|
||||||
}
|
|
||||||
|
|
||||||
<header>
|
|
||||||
<h1>@ViewData["Title"]</h1>
|
|
||||||
@{
|
|
||||||
if (User.Identity.IsAuthenticated)
|
|
||||||
{
|
|
||||||
<form class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="@Url.Page("/", new { area = "" })" method="post">
|
|
||||||
<button type="submit" class="nav-link btn btn-link text-dark">Click here to Logout</button>
|
|
||||||
</form>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<p>You have successfully logged out of the application.</p>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</header>
|
|
@ -1,45 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Identity;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Props.Models;
|
|
||||||
using Props.Models.User;
|
|
||||||
|
|
||||||
namespace Props.Areas.Identity.Pages.Account
|
|
||||||
{
|
|
||||||
[AllowAnonymous]
|
|
||||||
public class LogoutModel : PageModel
|
|
||||||
{
|
|
||||||
private readonly SignInManager<ApplicationUser> _signInManager;
|
|
||||||
private readonly ILogger<LogoutModel> _logger;
|
|
||||||
|
|
||||||
public LogoutModel(SignInManager<ApplicationUser> signInManager, ILogger<LogoutModel> logger)
|
|
||||||
{
|
|
||||||
_signInManager = signInManager;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnGet()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IActionResult> OnPost(string returnUrl = null)
|
|
||||||
{
|
|
||||||
await _signInManager.SignOutAsync();
|
|
||||||
_logger.LogInformation("User logged out.");
|
|
||||||
if (returnUrl != null)
|
|
||||||
{
|
|
||||||
return LocalRedirect(returnUrl);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return RedirectToPage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
|
||||||
|
|
||||||
namespace Props.Areas.Identity.Pages.Account.Manage
|
|
||||||
{
|
|
||||||
public static class ManageNavPages
|
|
||||||
{
|
|
||||||
public static string Index => "Index";
|
|
||||||
|
|
||||||
public static string Email => "Email";
|
|
||||||
|
|
||||||
public static string ChangePassword => "ChangePassword";
|
|
||||||
|
|
||||||
public static string DownloadPersonalData => "DownloadPersonalData";
|
|
||||||
|
|
||||||
public static string DeletePersonalData => "DeletePersonalData";
|
|
||||||
|
|
||||||
public static string ExternalLogins => "ExternalLogins";
|
|
||||||
|
|
||||||
public static string PersonalData => "PersonalData";
|
|
||||||
|
|
||||||
public static string TwoFactorAuthentication => "TwoFactorAuthentication";
|
|
||||||
|
|
||||||
public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index);
|
|
||||||
|
|
||||||
public static string EmailNavClass(ViewContext viewContext) => PageNavClass(viewContext, Email);
|
|
||||||
|
|
||||||
public static string ChangePasswordNavClass(ViewContext viewContext) => PageNavClass(viewContext, ChangePassword);
|
|
||||||
|
|
||||||
public static string DownloadPersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, DownloadPersonalData);
|
|
||||||
|
|
||||||
public static string DeletePersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, DeletePersonalData);
|
|
||||||
|
|
||||||
public static string ExternalLoginsNavClass(ViewContext viewContext) => PageNavClass(viewContext, ExternalLogins);
|
|
||||||
|
|
||||||
public static string PersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, PersonalData);
|
|
||||||
|
|
||||||
public static string TwoFactorAuthenticationNavClass(ViewContext viewContext) => PageNavClass(viewContext, TwoFactorAuthentication);
|
|
||||||
|
|
||||||
private static string PageNavClass(ViewContext viewContext, string page)
|
|
||||||
{
|
|
||||||
var activePage = viewContext.ViewData["ActivePage"] as string
|
|
||||||
?? System.IO.Path.GetFileNameWithoutExtension(viewContext.ActionDescriptor.DisplayName);
|
|
||||||
return string.Equals(activePage, page, StringComparison.OrdinalIgnoreCase) ? "active" : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
@{
|
|
||||||
if (ViewData.TryGetValue("ParentLayout", out var parentLayout))
|
|
||||||
{
|
|
||||||
Layout = (string)parentLayout;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Layout = "/Areas/Identity/Pages/_Layout.cshtml";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<h1>Manage your account</h1>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4>Change your account settings</h4>
|
|
||||||
<hr />
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-3">
|
|
||||||
<partial name="_ManageNav" />
|
|
||||||
</div>
|
|
||||||
<div class="col-md-9">
|
|
||||||
@RenderBody()
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
@section Scripts {
|
|
||||||
@RenderSection("Scripts", required: false)
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
@inject SignInManager<ApplicationUser> SignInManager
|
|
||||||
@{
|
|
||||||
var hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any();
|
|
||||||
}
|
|
||||||
<ul class="nav nav-pills flex-column">
|
|
||||||
<li class="nav-item"><a class="nav-link @ManageNavPages.IndexNavClass(ViewContext)" id="profile"
|
|
||||||
asp-page="./Index">Profile</a></li>
|
|
||||||
<li class="nav-item"><a class="nav-link @ManageNavPages.EmailNavClass(ViewContext)" id="email"
|
|
||||||
asp-page="./Email">Email</a></li>
|
|
||||||
<li class="nav-item"><a class="nav-link @ManageNavPages.ChangePasswordNavClass(ViewContext)" id="change-password"
|
|
||||||
asp-page="./ChangePassword">Password</a></li>
|
|
||||||
@if (hasExternalLogins)
|
|
||||||
{
|
|
||||||
<li id="external-logins" class="nav-item"><a id="external-login"
|
|
||||||
class="nav-link @ManageNavPages.ExternalLoginsNavClass(ViewContext)" asp-page="./ExternalLogins">External
|
|
||||||
logins</a></li>
|
|
||||||
}
|
|
||||||
<li class="nav-item"><a class="nav-link @ManageNavPages.TwoFactorAuthenticationNavClass(ViewContext)"
|
|
||||||
id="two-factor" asp-page="./TwoFactorAuthentication">Two-factor authentication</a></li>
|
|
||||||
<li class="nav-item"><a class="nav-link @ManageNavPages.PersonalDataNavClass(ViewContext)" id="personal-data"
|
|
||||||
asp-page="./PersonalData">Personal data</a></li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
@* TODO: Finish styling account page. *@
|
|
@ -1,10 +0,0 @@
|
|||||||
@model string
|
|
||||||
|
|
||||||
@if (!String.IsNullOrEmpty(Model))
|
|
||||||
{
|
|
||||||
var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success";
|
|
||||||
<div class="alert alert-@statusMessageClass alert-dismissible" role="alert">
|
|
||||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
|
|
||||||
@Model
|
|
||||||
</div>
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
@using Props.Areas.Identity.Pages.Account.Manage
|
|
@ -1,59 +0,0 @@
|
|||||||
@page
|
|
||||||
@model RegisterModel
|
|
||||||
@{
|
|
||||||
ViewData["Title"] = "Register";
|
|
||||||
}
|
|
||||||
<div class="jumbotron text-center">
|
|
||||||
<img alt="Props logo" src="~/images/logo-simplified.svg" class="img-fluid" style="max-height: 150px;" asp-append-version="true" />
|
|
||||||
<h1 class="mt-3 mb-4 display-1">@ViewData["Title"]</h1>
|
|
||||||
<p>Create more projects and access them across your devices! Join the community and show off your projects to the world!</p>
|
|
||||||
</div>
|
|
||||||
<div class="jumbotron sub flex-grow-1">
|
|
||||||
<div class="py-3 row justify-content-md-center">
|
|
||||||
<div class="col-md-4">
|
|
||||||
<form asp-route-returnUrl="@Model.ReturnUrl" method="post">
|
|
||||||
<h4>Create a new account.</h4>
|
|
||||||
<hr />
|
|
||||||
<div asp-validation-summary="All" class="text-danger"></div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label asp-for="Input.Email" class="form-label"></label>
|
|
||||||
<input asp-for="Input.Email" class="form-control" />
|
|
||||||
<span asp-validation-for="Input.Email" class="text-danger"></span>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label asp-for="Input.Password" class="form-label"></label>
|
|
||||||
<input asp-for="Input.Password" class="form-control" />
|
|
||||||
<span asp-validation-for="Input.Password" class="text-danger"></span>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label asp-for="Input.ConfirmPassword" class="form-label"></label>
|
|
||||||
<input asp-for="Input.ConfirmPassword" class="form-control" />
|
|
||||||
<span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary">Register</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
@if ((Model.ExternalLogins?.Count ?? 0) != 0)
|
|
||||||
{
|
|
||||||
<div class="col-md-6 md-offset-2">
|
|
||||||
<h4>Use another service to register.</h4>
|
|
||||||
<hr />
|
|
||||||
<form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal">
|
|
||||||
<div>
|
|
||||||
<p>
|
|
||||||
@foreach (var provider in Model.ExternalLogins)
|
|
||||||
{
|
|
||||||
<button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
|
|
||||||
}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
@section Scripts {
|
|
||||||
<partial name="_ValidationScriptsPartial" />
|
|
||||||
}
|
|
@ -1,115 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Encodings.Web;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.Authentication;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Identity;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
|
||||||
using Microsoft.AspNetCore.WebUtilities;
|
|
||||||
using Microsoft.AspNetCore.Identity.UI.Services;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Props.Models.User;
|
|
||||||
|
|
||||||
namespace Props.Areas.Identity.Pages.Account
|
|
||||||
{
|
|
||||||
[AllowAnonymous]
|
|
||||||
public class RegisterModel : PageModel
|
|
||||||
{
|
|
||||||
private readonly SignInManager<ApplicationUser> _signInManager;
|
|
||||||
private readonly UserManager<ApplicationUser> _userManager;
|
|
||||||
private readonly ILogger<RegisterModel> _logger;
|
|
||||||
private readonly IEmailSender _emailSender;
|
|
||||||
|
|
||||||
public RegisterModel(
|
|
||||||
UserManager<ApplicationUser> userManager,
|
|
||||||
SignInManager<ApplicationUser> signInManager,
|
|
||||||
ILogger<RegisterModel> logger,
|
|
||||||
IEmailSender emailSender)
|
|
||||||
{
|
|
||||||
_userManager = userManager;
|
|
||||||
_signInManager = signInManager;
|
|
||||||
_logger = logger;
|
|
||||||
_emailSender = emailSender;
|
|
||||||
}
|
|
||||||
|
|
||||||
[BindProperty]
|
|
||||||
public InputModel Input { get; set; }
|
|
||||||
|
|
||||||
public string ReturnUrl { get; set; }
|
|
||||||
|
|
||||||
public IList<AuthenticationScheme> ExternalLogins { get; set; }
|
|
||||||
|
|
||||||
public class InputModel
|
|
||||||
{
|
|
||||||
[Required]
|
|
||||||
[EmailAddress]
|
|
||||||
[Display(Name = "Email")]
|
|
||||||
public string Email { get; set; }
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
|
|
||||||
[DataType(DataType.Password)]
|
|
||||||
[Display(Name = "Password")]
|
|
||||||
public string Password { get; set; }
|
|
||||||
|
|
||||||
[DataType(DataType.Password)]
|
|
||||||
[Display(Name = "Confirm password")]
|
|
||||||
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
|
|
||||||
public string ConfirmPassword { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task OnGetAsync(string returnUrl = null)
|
|
||||||
{
|
|
||||||
ReturnUrl = returnUrl;
|
|
||||||
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
|
|
||||||
{
|
|
||||||
returnUrl ??= Url.Content("~/");
|
|
||||||
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
|
|
||||||
if (ModelState.IsValid)
|
|
||||||
{
|
|
||||||
var user = new ApplicationUser { UserName = Input.Email, Email = Input.Email };
|
|
||||||
var result = await _userManager.CreateAsync(user, Input.Password);
|
|
||||||
if (result.Succeeded)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("User created a new account with password.");
|
|
||||||
|
|
||||||
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
|
||||||
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
|
|
||||||
var callbackUrl = Url.Page(
|
|
||||||
"/Account/ConfirmEmail",
|
|
||||||
pageHandler: null,
|
|
||||||
values: new { area = "Identity", userId = user.Id, code = code, returnUrl = returnUrl },
|
|
||||||
protocol: Request.Scheme);
|
|
||||||
|
|
||||||
await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
|
|
||||||
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
|
|
||||||
|
|
||||||
if (_userManager.Options.SignIn.RequireConfirmedAccount)
|
|
||||||
{
|
|
||||||
return RedirectToPage("RegisterConfirmation", new { email = Input.Email, returnUrl = returnUrl });
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await _signInManager.SignInAsync(user, isPersistent: false);
|
|
||||||
return LocalRedirect(returnUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
foreach (var error in result.Errors)
|
|
||||||
{
|
|
||||||
ModelState.AddModelError(string.Empty, error.Description);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we got this far, something failed, redisplay form
|
|
||||||
return Page();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
@page
|
|
||||||
@model RegisterConfirmationModel
|
|
||||||
@{
|
|
||||||
ViewData["Title"] = "Register confirmation";
|
|
||||||
}
|
|
||||||
|
|
||||||
<h1>@ViewData["Title"]</h1>
|
|
||||||
@{
|
|
||||||
if (@Model.DisplayConfirmAccountLink)
|
|
||||||
{
|
|
||||||
<p>
|
|
||||||
This app does not currently have a real email sender registered, see <a href="https://aka.ms/aspaccountconf">these
|
|
||||||
docs</a> for how to configure a real email sender.
|
|
||||||
Normally this would be emailed: <a id="confirm-link" href="@Model.EmailConfirmationUrl">Click here to confirm your
|
|
||||||
account</a>
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<p>
|
|
||||||
Please check your email to confirm your account.
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@* TODO: implement account confirmation. *@
|
|
@ -1,62 +0,0 @@
|
|||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Identity;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
|
||||||
using Microsoft.AspNetCore.WebUtilities;
|
|
||||||
using Microsoft.AspNetCore.Identity.UI.Services;
|
|
||||||
using Props.Models.User;
|
|
||||||
|
|
||||||
namespace Props.Areas.Identity.Pages.Account
|
|
||||||
{
|
|
||||||
[AllowAnonymous]
|
|
||||||
public class RegisterConfirmationModel : PageModel
|
|
||||||
{
|
|
||||||
private readonly UserManager<ApplicationUser> _userManager;
|
|
||||||
private readonly IEmailSender _sender;
|
|
||||||
|
|
||||||
public RegisterConfirmationModel(UserManager<ApplicationUser> userManager, IEmailSender sender)
|
|
||||||
{
|
|
||||||
_userManager = userManager;
|
|
||||||
_sender = sender;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Email { get; set; }
|
|
||||||
|
|
||||||
public bool DisplayConfirmAccountLink { get; set; }
|
|
||||||
|
|
||||||
public string EmailConfirmationUrl { get; set; }
|
|
||||||
|
|
||||||
public async Task<IActionResult> OnGetAsync(string email, string returnUrl = null)
|
|
||||||
{
|
|
||||||
if (email == null)
|
|
||||||
{
|
|
||||||
return RedirectToPage("/Index");
|
|
||||||
}
|
|
||||||
|
|
||||||
var user = await _userManager.FindByEmailAsync(email);
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
return NotFound($"Unable to load user with email '{email}'.");
|
|
||||||
}
|
|
||||||
|
|
||||||
Email = email;
|
|
||||||
// Once you add a real email sender, you should remove this code that lets you confirm the account
|
|
||||||
DisplayConfirmAccountLink = true;
|
|
||||||
if (DisplayConfirmAccountLink)
|
|
||||||
{
|
|
||||||
var userId = await _userManager.GetUserIdAsync(user);
|
|
||||||
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
|
||||||
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
|
|
||||||
EmailConfirmationUrl = Url.Page(
|
|
||||||
"/Account/ConfirmEmail",
|
|
||||||
pageHandler: null,
|
|
||||||
values: new { area = "Identity", userId = userId, code = code, returnUrl = returnUrl },
|
|
||||||
protocol: Request.Scheme);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Page();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
@using Props.Areas.Identity.Pages.Account
|
|
@ -1,18 +0,0 @@
|
|||||||
<environment include="Development">
|
|
||||||
<script src="~/Identity/lib/jquery-validation/dist/jquery.validate.js"></script>
|
|
||||||
<script src="~/Identity/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
|
|
||||||
</environment>
|
|
||||||
<environment exclude="Development">
|
|
||||||
<script src="https://ajax.aspnetcdn.com/ajax/jquery.validate/1.17.0/jquery.validate.min.js"
|
|
||||||
asp-fallback-src="~/Identity/lib/jquery-validation/dist/jquery.validate.min.js"
|
|
||||||
asp-fallback-test="window.jQuery && window.jQuery.validator"
|
|
||||||
crossorigin="anonymous"
|
|
||||||
integrity="sha384-rZfj/ogBloos6wzLGpPkkOr/gpkBNLZ6b6yLy4o+ok+t/SAKlL5mvXLr0OXNi1Hp">
|
|
||||||
</script>
|
|
||||||
<script src="https://ajax.aspnetcdn.com/ajax/jquery.validation.unobtrusive/3.2.9/jquery.validate.unobtrusive.min.js"
|
|
||||||
asp-fallback-src="~/Identity/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"
|
|
||||||
asp-fallback-test="window.jQuery && window.jQuery.validator && window.jQuery.validator.unobtrusive"
|
|
||||||
crossorigin="anonymous"
|
|
||||||
integrity="sha384-ifv0TYDWxBHzvAk2Z0n8R434FL1Rlv/Av18DXE43N/1rvHyOG4izKst0f2iSLdds">
|
|
||||||
</script>
|
|
||||||
</environment>
|
|
@ -1,5 +0,0 @@
|
|||||||
@using Microsoft.AspNetCore.Identity
|
|
||||||
@using Props.Areas.Identity
|
|
||||||
@using Props.Areas.Identity.Pages
|
|
||||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
|
||||||
@using Props.Models.User
|
|
@ -1,3 +0,0 @@
|
|||||||
@{
|
|
||||||
Layout = "/Pages/Shared/_Layout.cshtml";
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace Props.Controllers
|
|
||||||
{
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/[Controller]")]
|
|
||||||
public class ApiControllerBase : ControllerBase
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Net;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Props.Models.Search;
|
|
||||||
using Props.Services.Modules;
|
|
||||||
using Props.Shop.Framework;
|
|
||||||
|
|
||||||
namespace Props.Controllers
|
|
||||||
{
|
|
||||||
public class SearchController : ApiControllerBase
|
|
||||||
{
|
|
||||||
private SearchOutline defaultOutline = new SearchOutline();
|
|
||||||
ISearchManager searchManager;
|
|
||||||
|
|
||||||
public SearchController(ISearchManager searchManager)
|
|
||||||
{
|
|
||||||
this.searchManager = searchManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
[Route("AvailableShops")]
|
|
||||||
public async Task<IActionResult> GetAvailableShops()
|
|
||||||
{
|
|
||||||
return Ok(await searchManager.ShopManager.GetAllShopNames());
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
[Route("SearchShops/{search}/")]
|
|
||||||
public async Task<IActionResult> GetSearch(string searchQuery, [FromQuery] SearchOutline searchOutline)
|
|
||||||
{
|
|
||||||
if (searchQuery == null) return BadRequest();
|
|
||||||
|
|
||||||
return Ok(await searchManager.Search(searchQuery, searchOutline));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,211 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Identity;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Props.Data;
|
|
||||||
using Props.Models.Search;
|
|
||||||
using Props.Models.User;
|
|
||||||
using Props.Shop.Framework;
|
|
||||||
|
|
||||||
namespace Props.Controllers
|
|
||||||
{
|
|
||||||
public class SearchOutlineController : ApiControllerBase
|
|
||||||
{
|
|
||||||
private ApplicationDbContext dbContext;
|
|
||||||
private UserManager<ApplicationUser> userManager;
|
|
||||||
public SearchOutlineController(UserManager<ApplicationUser> userManager, ApplicationDbContext dbContext)
|
|
||||||
{
|
|
||||||
this.userManager = userManager;
|
|
||||||
this.dbContext = dbContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpDelete]
|
|
||||||
[Authorize]
|
|
||||||
[Route("{name:required}")]
|
|
||||||
public async Task<IActionResult> DeleteSearchOutline(string name)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(name))
|
|
||||||
{
|
|
||||||
return BadRequest();
|
|
||||||
}
|
|
||||||
ApplicationUser user = await userManager.GetUserAsync(User);
|
|
||||||
SearchOutlinePreferences searchOutlinePrefs = user.searchOutlinePreferences;
|
|
||||||
searchOutlinePrefs.SearchOutlines.Remove(searchOutlinePrefs.SearchOutlines.Single((outline) => name.Equals(outline.Name)));
|
|
||||||
await userManager.UpdateAsync(user);
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
[Authorize]
|
|
||||||
[Route("{name:required}")]
|
|
||||||
public async Task<IActionResult> PostSearchOutline(string name)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(name)) return BadRequest();
|
|
||||||
ApplicationUser user = await userManager.GetUserAsync(User);
|
|
||||||
SearchOutline searchOutline = user.searchOutlinePreferences.SearchOutlines.SingleOrDefault((outline) => name.Equals(outline.Name));
|
|
||||||
if (searchOutline != null) return BadRequest();
|
|
||||||
searchOutline = new SearchOutline();
|
|
||||||
searchOutline.Name = name;
|
|
||||||
user.searchOutlinePreferences.SearchOutlines.Add(searchOutline);
|
|
||||||
await userManager.UpdateAsync(user);
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPut]
|
|
||||||
[Authorize]
|
|
||||||
[Route("{name:required}/Filters")]
|
|
||||||
public async Task<IActionResult> PutFilters(string name, Filters filters)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(name)) return BadRequest();
|
|
||||||
ApplicationUser user = await userManager.GetUserAsync(User);
|
|
||||||
SearchOutline searchOutline = await GetSearchOutlineByName(name);
|
|
||||||
if (searchOutline == null) return BadRequest();
|
|
||||||
searchOutline.Filters = filters;
|
|
||||||
await userManager.UpdateAsync(user);
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPut]
|
|
||||||
[Authorize]
|
|
||||||
[Route("{outlineName:required}/DisabledShops")]
|
|
||||||
public async Task<IActionResult> PutShopSelection(string outlineName, ISet<string> disabledShops)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(outlineName)) return BadRequest();
|
|
||||||
if (disabledShops == null) return BadRequest();
|
|
||||||
ApplicationUser user = await userManager.GetUserAsync(User);
|
|
||||||
SearchOutline searchOutline = await GetSearchOutlineByName(outlineName);
|
|
||||||
if (searchOutline == null) return BadRequest();
|
|
||||||
|
|
||||||
searchOutline.DisabledShops.Clear();
|
|
||||||
searchOutline.DisabledShops.UnionWith(disabledShops);
|
|
||||||
await userManager.UpdateAsync(user);
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPut]
|
|
||||||
[Authorize]
|
|
||||||
[Route("{oldName:required}/Name/{newName:required}")]
|
|
||||||
public async Task<IActionResult> PutName(string oldName, string newName)
|
|
||||||
{
|
|
||||||
if (oldName == newName) return BadRequest();
|
|
||||||
ApplicationUser user = await userManager.GetUserAsync(User);
|
|
||||||
SearchOutline outline = await GetSearchOutlineByName(oldName);
|
|
||||||
if (outline == null) return BadRequest();
|
|
||||||
if (user.searchOutlinePreferences.SearchOutlines.Any((outline) => outline.Name.Equals(newName))) return BadRequest();
|
|
||||||
outline.Name = newName;
|
|
||||||
if (user.searchOutlinePreferences.NameOfLastUsed == oldName)
|
|
||||||
{
|
|
||||||
user.searchOutlinePreferences.NameOfLastUsed = newName;
|
|
||||||
}
|
|
||||||
await userManager.UpdateAsync(user);
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPut]
|
|
||||||
[Authorize]
|
|
||||||
[Route("{name:required}/LastUsed")]
|
|
||||||
public async Task<IActionResult> PutLastUsed(string name)
|
|
||||||
{
|
|
||||||
SearchOutline outline = await GetSearchOutlineByName(name);
|
|
||||||
if (outline == null) return BadRequest();
|
|
||||||
ApplicationUser user = await userManager.GetUserAsync(User);
|
|
||||||
user.searchOutlinePreferences.NameOfLastUsed = name;
|
|
||||||
await userManager.UpdateAsync(user);
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
[Authorize]
|
|
||||||
[Route("{name:required}/Filters")]
|
|
||||||
public async Task<IActionResult> GetFilters(string name)
|
|
||||||
{
|
|
||||||
Filters filters = (await GetSearchOutlineByName(name))?.Filters;
|
|
||||||
if (filters == null) return BadRequest();
|
|
||||||
return Ok(filters);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
[Authorize]
|
|
||||||
[Route("{name:required}/DisabledShops")]
|
|
||||||
public async Task<IActionResult> GetDisabledShops(string name)
|
|
||||||
{
|
|
||||||
SearchOutline searchOutline = await GetSearchOutlineByName(name);
|
|
||||||
if (searchOutline == null)
|
|
||||||
{
|
|
||||||
return BadRequest();
|
|
||||||
}
|
|
||||||
return Ok(searchOutline.DisabledShops);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
[Authorize]
|
|
||||||
[Route("Names")]
|
|
||||||
public async Task<IActionResult> GetSearchOutlineNames()
|
|
||||||
{
|
|
||||||
ApplicationUser user = await userManager.GetUserAsync(User);
|
|
||||||
return Ok(user.searchOutlinePreferences.SearchOutlines.Select((outline, Index) => outline.Name));
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
[Authorize]
|
|
||||||
[Route("LastUsed")]
|
|
||||||
public async Task<IActionResult> GetLastSearchOutlineName()
|
|
||||||
{
|
|
||||||
SearchOutline searchOutline = await GetLastUsedSearchOutline();
|
|
||||||
|
|
||||||
return Ok(searchOutline?.Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
[Route("DefaultDisabledShops")]
|
|
||||||
public IActionResult GetDefaultDisabledShops()
|
|
||||||
{
|
|
||||||
return Ok(new SearchOutline.ShopSelector());
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
[Route("DefaultFilters")]
|
|
||||||
public IActionResult GetDefaultFilter()
|
|
||||||
{
|
|
||||||
return Ok(new Filters());
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
[Route("DefaultName")]
|
|
||||||
public async Task<IActionResult> GetDefaultName()
|
|
||||||
{
|
|
||||||
string nameTemplate = "Search Outline {0}";
|
|
||||||
if (User.Identity.IsAuthenticated)
|
|
||||||
{
|
|
||||||
ApplicationUser user = await userManager.GetUserAsync(User);
|
|
||||||
int number = user.searchOutlinePreferences.SearchOutlines.Count;
|
|
||||||
string name = null;
|
|
||||||
do
|
|
||||||
{
|
|
||||||
name = string.Format(nameTemplate, number);
|
|
||||||
number += 1;
|
|
||||||
} while (user.searchOutlinePreferences.SearchOutlines.Any((outline) => name.Equals(outline.Name)));
|
|
||||||
return Ok(name);
|
|
||||||
}
|
|
||||||
return Ok("Search Outline");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<SearchOutline> GetLastUsedSearchOutline()
|
|
||||||
{
|
|
||||||
if (!User.Identity.IsAuthenticated) return null;
|
|
||||||
ApplicationUser user = await userManager.GetUserAsync(User);
|
|
||||||
return user.searchOutlinePreferences.SearchOutlines.SingleOrDefault((outline) => outline.Name.Equals(user.searchOutlinePreferences.NameOfLastUsed));
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<SearchOutline> GetSearchOutlineByName(string name)
|
|
||||||
{
|
|
||||||
if (name == null) throw new ArgumentNullException("name");
|
|
||||||
if (!User.Identity.IsAuthenticated) return null;
|
|
||||||
ApplicationUser user = await userManager.GetUserAsync(User);
|
|
||||||
return user.searchOutlinePreferences.SearchOutlines.SingleOrDefault(outline => outline.Name.Equals(name));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace Props.Controllers
|
|
||||||
{
|
|
||||||
public class UserController : ApiControllerBase
|
|
||||||
{
|
|
||||||
[HttpGet]
|
|
||||||
[Route("LoggedIn")]
|
|
||||||
public IActionResult GetLoggedIn()
|
|
||||||
{
|
|
||||||
return Ok(User.Identity.IsAuthenticated);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,82 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Microsoft.Extensions.Localization;
|
|
||||||
using Props.Models;
|
|
||||||
using Props.Models.Search;
|
|
||||||
using Props.Models.User;
|
|
||||||
using Props.Shop.Framework;
|
|
||||||
|
|
||||||
namespace Props.Data
|
|
||||||
{
|
|
||||||
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
|
|
||||||
{
|
|
||||||
public DbSet<QueryWordInfo> QueryWords { get; set; }
|
|
||||||
|
|
||||||
public DbSet<ProductListingInfo> ProductListingInfos { get; set; }
|
|
||||||
|
|
||||||
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
|
|
||||||
: base(options)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
base.OnModelCreating(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity<ResultsPreferences>()
|
|
||||||
.Property(e => e.Order)
|
|
||||||
.HasConversion(
|
|
||||||
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
|
|
||||||
v => JsonSerializer.Deserialize<List<ResultsPreferences.Category>>(v, (JsonSerializerOptions)null),
|
|
||||||
new ValueComparer<IList<ResultsPreferences.Category>>(
|
|
||||||
(a, b) => a.SequenceEqual(b),
|
|
||||||
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
|
|
||||||
c => (IList<ResultsPreferences.Category>)c.ToList()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
modelBuilder.Entity<SearchOutline>()
|
|
||||||
.Property(e => e.DisabledShops)
|
|
||||||
.HasConversion(
|
|
||||||
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
|
|
||||||
v => JsonSerializer.Deserialize<SearchOutline.ShopSelector>(v, (JsonSerializerOptions)null),
|
|
||||||
new ValueComparer<SearchOutline.ShopSelector>(
|
|
||||||
(a, b) => a.Equals(b),
|
|
||||||
c => c.GetHashCode(),
|
|
||||||
c => new SearchOutline.ShopSelector(c)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
modelBuilder.Entity<SearchOutline>()
|
|
||||||
.Property(e => e.Filters)
|
|
||||||
.HasConversion(
|
|
||||||
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
|
|
||||||
v => JsonSerializer.Deserialize<Filters>(v, (JsonSerializerOptions)null),
|
|
||||||
new ValueComparer<Filters>(
|
|
||||||
(a, b) => a.Equals(b),
|
|
||||||
c => c.GetHashCode(),
|
|
||||||
c => c.Copy()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
modelBuilder.Entity<ProductListingInfo>()
|
|
||||||
.Property(e => e.ProductListing)
|
|
||||||
.HasConversion(
|
|
||||||
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
|
|
||||||
v => JsonSerializer.Deserialize<ProductListing>(v, (JsonSerializerOptions)null),
|
|
||||||
new ValueComparer<ProductListing>(
|
|
||||||
(a, b) => a.Equals(b),
|
|
||||||
c => c.GetHashCode(),
|
|
||||||
c => c.Copy()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,499 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Props.Data;
|
|
||||||
|
|
||||||
namespace Props.Data.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(ApplicationDbContext))]
|
|
||||||
[Migration("20210817042955_InitialCreate")]
|
|
||||||
partial class InitialCreate
|
|
||||||
{
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder
|
|
||||||
.HasAnnotation("ProductVersion", "5.0.8");
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("ConcurrencyStamp")
|
|
||||||
.IsConcurrencyToken()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("NormalizedName")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("NormalizedName")
|
|
||||||
.IsUnique()
|
|
||||||
.HasDatabaseName("RoleNameIndex");
|
|
||||||
|
|
||||||
b.ToTable("AspNetRoles");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("ClaimType")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("ClaimValue")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("RoleId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("RoleId");
|
|
||||||
|
|
||||||
b.ToTable("AspNetRoleClaims");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("ClaimType")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("ClaimValue")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("UserId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("AspNetUserClaims");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("LoginProvider")
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("ProviderKey")
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("ProviderDisplayName")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("UserId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("LoginProvider", "ProviderKey");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("AspNetUserLogins");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("UserId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("RoleId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("UserId", "RoleId");
|
|
||||||
|
|
||||||
b.HasIndex("RoleId");
|
|
||||||
|
|
||||||
b.ToTable("AspNetUserRoles");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("UserId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("LoginProvider")
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Value")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("UserId", "LoginProvider", "Name");
|
|
||||||
|
|
||||||
b.ToTable("AspNetUserTokens");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Props.Models.ResultsPreferences", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("ApplicationUserId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Order")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("ProfileName")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("ApplicationUserId")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("ResultsPreferences");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Props.Models.Search.ProductListingInfo", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<uint>("Hits")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("ProductListing")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("ProductListingIdentifier")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("ShopName")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("ProductListingInfos");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Props.Models.Search.QueryWordInfo", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<uint>("Hits")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("Word")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("QueryWords");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Props.Models.Search.SearchOutline", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("DisabledShops")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Filters")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<int>("SearchOutlinePreferencesId")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("SearchOutlinePreferencesId");
|
|
||||||
|
|
||||||
b.ToTable("SearchOutline");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Props.Models.User.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("Props.Models.User.SearchOutlinePreferences", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("ApplicationUserId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("NameOfLastUsed")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("ApplicationUserId")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("SearchOutlinePreferences");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Props.Shared.Models.User.ApplicationPreferences", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("ApplicationUserId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<bool>("DarkMode")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<bool>("EnableSearchHistory")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("ApplicationUserId")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("ApplicationPreferences");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("QueryWordInfoQueryWordInfo", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("FollowingId")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<int>("PrecedingId")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.HasKey("FollowingId", "PrecedingId");
|
|
||||||
|
|
||||||
b.HasIndex("PrecedingId");
|
|
||||||
|
|
||||||
b.ToTable("QueryWordInfoQueryWordInfo");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("RoleId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Props.Models.User.ApplicationUser", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Props.Models.User.ApplicationUser", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("RoleId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("Props.Models.User.ApplicationUser", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Props.Models.User.ApplicationUser", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Props.Models.ResultsPreferences", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Props.Models.User.ApplicationUser", "ApplicationUser")
|
|
||||||
.WithOne("ResultsPreferences")
|
|
||||||
.HasForeignKey("Props.Models.ResultsPreferences", "ApplicationUserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("ApplicationUser");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Props.Models.Search.SearchOutline", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Props.Models.User.SearchOutlinePreferences", "SearchOutlinePreferences")
|
|
||||||
.WithMany("SearchOutlines")
|
|
||||||
.HasForeignKey("SearchOutlinePreferencesId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("SearchOutlinePreferences");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Props.Models.User.SearchOutlinePreferences", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Props.Models.User.ApplicationUser", "ApplicationUser")
|
|
||||||
.WithOne("searchOutlinePreferences")
|
|
||||||
.HasForeignKey("Props.Models.User.SearchOutlinePreferences", "ApplicationUserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("ApplicationUser");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Props.Shared.Models.User.ApplicationPreferences", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Props.Models.User.ApplicationUser", "ApplicationUser")
|
|
||||||
.WithOne("ApplicationPreferences")
|
|
||||||
.HasForeignKey("Props.Shared.Models.User.ApplicationPreferences", "ApplicationUserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("ApplicationUser");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("QueryWordInfoQueryWordInfo", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Props.Models.Search.QueryWordInfo", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("FollowingId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("Props.Models.Search.QueryWordInfo", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("PrecedingId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Props.Models.User.ApplicationUser", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("ApplicationPreferences")
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("ResultsPreferences")
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("searchOutlinePreferences")
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Props.Models.User.SearchOutlinePreferences", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("SearchOutlines");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,404 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
namespace Props.Data.Migrations
|
|
||||||
{
|
|
||||||
public partial class InitialCreate : Migration
|
|
||||||
{
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "AspNetRoles",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
Name = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
|
|
||||||
NormalizedName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
|
|
||||||
ConcurrencyStamp = table.Column<string>(type: "TEXT", nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_AspNetRoles", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "AspNetUsers",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
UserName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
|
|
||||||
NormalizedUserName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
|
|
||||||
Email = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
|
|
||||||
NormalizedEmail = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
|
|
||||||
EmailConfirmed = table.Column<bool>(type: "INTEGER", nullable: false),
|
|
||||||
PasswordHash = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
SecurityStamp = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
ConcurrencyStamp = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
PhoneNumber = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
PhoneNumberConfirmed = table.Column<bool>(type: "INTEGER", nullable: false),
|
|
||||||
TwoFactorEnabled = table.Column<bool>(type: "INTEGER", nullable: false),
|
|
||||||
LockoutEnd = table.Column<DateTimeOffset>(type: "TEXT", nullable: true),
|
|
||||||
LockoutEnabled = table.Column<bool>(type: "INTEGER", nullable: false),
|
|
||||||
AccessFailedCount = table.Column<int>(type: "INTEGER", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_AspNetUsers", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "ProductListingInfos",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
|
||||||
.Annotation("Sqlite:Autoincrement", true),
|
|
||||||
ShopName = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
Hits = table.Column<uint>(type: "INTEGER", nullable: false),
|
|
||||||
ProductListing = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
ProductListingIdentifier = table.Column<string>(type: "TEXT", nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_ProductListingInfos", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "QueryWords",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
|
||||||
.Annotation("Sqlite:Autoincrement", true),
|
|
||||||
Word = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
Hits = table.Column<uint>(type: "INTEGER", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_QueryWords", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "AspNetRoleClaims",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
|
||||||
.Annotation("Sqlite:Autoincrement", true),
|
|
||||||
RoleId = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
ClaimType = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
ClaimValue = table.Column<string>(type: "TEXT", nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_AspNetRoleClaims_AspNetRoles_RoleId",
|
|
||||||
column: x => x.RoleId,
|
|
||||||
principalTable: "AspNetRoles",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "ApplicationPreferences",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
|
||||||
.Annotation("Sqlite:Autoincrement", true),
|
|
||||||
ApplicationUserId = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
EnableSearchHistory = table.Column<bool>(type: "INTEGER", nullable: false),
|
|
||||||
DarkMode = table.Column<bool>(type: "INTEGER", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_ApplicationPreferences", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_ApplicationPreferences_AspNetUsers_ApplicationUserId",
|
|
||||||
column: x => x.ApplicationUserId,
|
|
||||||
principalTable: "AspNetUsers",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "AspNetUserClaims",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
|
||||||
.Annotation("Sqlite:Autoincrement", true),
|
|
||||||
UserId = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
ClaimType = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
ClaimValue = table.Column<string>(type: "TEXT", nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_AspNetUserClaims", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_AspNetUserClaims_AspNetUsers_UserId",
|
|
||||||
column: x => x.UserId,
|
|
||||||
principalTable: "AspNetUsers",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "AspNetUserLogins",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
LoginProvider = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
|
||||||
ProviderKey = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
|
||||||
ProviderDisplayName = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
UserId = table.Column<string>(type: "TEXT", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey });
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_AspNetUserLogins_AspNetUsers_UserId",
|
|
||||||
column: x => x.UserId,
|
|
||||||
principalTable: "AspNetUsers",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "AspNetUserRoles",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
UserId = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
RoleId = table.Column<string>(type: "TEXT", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId });
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_AspNetUserRoles_AspNetRoles_RoleId",
|
|
||||||
column: x => x.RoleId,
|
|
||||||
principalTable: "AspNetRoles",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_AspNetUserRoles_AspNetUsers_UserId",
|
|
||||||
column: x => x.UserId,
|
|
||||||
principalTable: "AspNetUsers",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "AspNetUserTokens",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
UserId = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
LoginProvider = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
|
||||||
Name = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
|
||||||
Value = table.Column<string>(type: "TEXT", nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_AspNetUserTokens_AspNetUsers_UserId",
|
|
||||||
column: x => x.UserId,
|
|
||||||
principalTable: "AspNetUsers",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "ResultsPreferences",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
|
||||||
.Annotation("Sqlite:Autoincrement", true),
|
|
||||||
ApplicationUserId = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
Order = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
ProfileName = table.Column<string>(type: "TEXT", nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_ResultsPreferences", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_ResultsPreferences_AspNetUsers_ApplicationUserId",
|
|
||||||
column: x => x.ApplicationUserId,
|
|
||||||
principalTable: "AspNetUsers",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "SearchOutlinePreferences",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
|
||||||
.Annotation("Sqlite:Autoincrement", true),
|
|
||||||
ApplicationUserId = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
NameOfLastUsed = table.Column<string>(type: "TEXT", nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_SearchOutlinePreferences", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_SearchOutlinePreferences_AspNetUsers_ApplicationUserId",
|
|
||||||
column: x => x.ApplicationUserId,
|
|
||||||
principalTable: "AspNetUsers",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "QueryWordInfoQueryWordInfo",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
FollowingId = table.Column<int>(type: "INTEGER", nullable: false),
|
|
||||||
PrecedingId = table.Column<int>(type: "INTEGER", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_QueryWordInfoQueryWordInfo", x => new { x.FollowingId, x.PrecedingId });
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_QueryWordInfoQueryWordInfo_QueryWords_FollowingId",
|
|
||||||
column: x => x.FollowingId,
|
|
||||||
principalTable: "QueryWords",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_QueryWordInfoQueryWordInfo_QueryWords_PrecedingId",
|
|
||||||
column: x => x.PrecedingId,
|
|
||||||
principalTable: "QueryWords",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "SearchOutline",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
|
||||||
.Annotation("Sqlite:Autoincrement", true),
|
|
||||||
SearchOutlinePreferencesId = table.Column<int>(type: "INTEGER", nullable: false),
|
|
||||||
Name = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
Filters = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
DisabledShops = table.Column<string>(type: "TEXT", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_SearchOutline", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_SearchOutline_SearchOutlinePreferences_SearchOutlinePreferencesId",
|
|
||||||
column: x => x.SearchOutlinePreferencesId,
|
|
||||||
principalTable: "SearchOutlinePreferences",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_ApplicationPreferences_ApplicationUserId",
|
|
||||||
table: "ApplicationPreferences",
|
|
||||||
column: "ApplicationUserId",
|
|
||||||
unique: true);
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_AspNetRoleClaims_RoleId",
|
|
||||||
table: "AspNetRoleClaims",
|
|
||||||
column: "RoleId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "RoleNameIndex",
|
|
||||||
table: "AspNetRoles",
|
|
||||||
column: "NormalizedName",
|
|
||||||
unique: true);
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_AspNetUserClaims_UserId",
|
|
||||||
table: "AspNetUserClaims",
|
|
||||||
column: "UserId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_AspNetUserLogins_UserId",
|
|
||||||
table: "AspNetUserLogins",
|
|
||||||
column: "UserId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_AspNetUserRoles_RoleId",
|
|
||||||
table: "AspNetUserRoles",
|
|
||||||
column: "RoleId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "EmailIndex",
|
|
||||||
table: "AspNetUsers",
|
|
||||||
column: "NormalizedEmail");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "UserNameIndex",
|
|
||||||
table: "AspNetUsers",
|
|
||||||
column: "NormalizedUserName",
|
|
||||||
unique: true);
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_QueryWordInfoQueryWordInfo_PrecedingId",
|
|
||||||
table: "QueryWordInfoQueryWordInfo",
|
|
||||||
column: "PrecedingId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_ResultsPreferences_ApplicationUserId",
|
|
||||||
table: "ResultsPreferences",
|
|
||||||
column: "ApplicationUserId",
|
|
||||||
unique: true);
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_SearchOutline_SearchOutlinePreferencesId",
|
|
||||||
table: "SearchOutline",
|
|
||||||
column: "SearchOutlinePreferencesId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_SearchOutlinePreferences_ApplicationUserId",
|
|
||||||
table: "SearchOutlinePreferences",
|
|
||||||
column: "ApplicationUserId",
|
|
||||||
unique: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "ApplicationPreferences");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "AspNetRoleClaims");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "AspNetUserClaims");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "AspNetUserLogins");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "AspNetUserRoles");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "AspNetUserTokens");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "ProductListingInfos");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "QueryWordInfoQueryWordInfo");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "ResultsPreferences");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "SearchOutline");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "AspNetRoles");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "QueryWords");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "SearchOutlinePreferences");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "AspNetUsers");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,497 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Props.Data;
|
|
||||||
|
|
||||||
namespace Props.Data.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(ApplicationDbContext))]
|
|
||||||
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
|
|
||||||
{
|
|
||||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder
|
|
||||||
.HasAnnotation("ProductVersion", "5.0.8");
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("ConcurrencyStamp")
|
|
||||||
.IsConcurrencyToken()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("NormalizedName")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("NormalizedName")
|
|
||||||
.IsUnique()
|
|
||||||
.HasDatabaseName("RoleNameIndex");
|
|
||||||
|
|
||||||
b.ToTable("AspNetRoles");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("ClaimType")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("ClaimValue")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("RoleId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("RoleId");
|
|
||||||
|
|
||||||
b.ToTable("AspNetRoleClaims");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("ClaimType")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("ClaimValue")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("UserId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("AspNetUserClaims");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("LoginProvider")
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("ProviderKey")
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("ProviderDisplayName")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("UserId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("LoginProvider", "ProviderKey");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("AspNetUserLogins");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("UserId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("RoleId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("UserId", "RoleId");
|
|
||||||
|
|
||||||
b.HasIndex("RoleId");
|
|
||||||
|
|
||||||
b.ToTable("AspNetUserRoles");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("UserId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("LoginProvider")
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Value")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("UserId", "LoginProvider", "Name");
|
|
||||||
|
|
||||||
b.ToTable("AspNetUserTokens");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Props.Models.ResultsPreferences", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("ApplicationUserId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Order")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("ProfileName")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("ApplicationUserId")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("ResultsPreferences");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Props.Models.Search.ProductListingInfo", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<uint>("Hits")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("ProductListing")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("ProductListingIdentifier")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("ShopName")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("ProductListingInfos");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Props.Models.Search.QueryWordInfo", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<uint>("Hits")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("Word")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("QueryWords");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Props.Models.Search.SearchOutline", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("DisabledShops")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Filters")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<int>("SearchOutlinePreferencesId")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("SearchOutlinePreferencesId");
|
|
||||||
|
|
||||||
b.ToTable("SearchOutline");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Props.Models.User.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("Props.Models.User.SearchOutlinePreferences", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("ApplicationUserId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("NameOfLastUsed")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("ApplicationUserId")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("SearchOutlinePreferences");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Props.Shared.Models.User.ApplicationPreferences", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("ApplicationUserId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<bool>("DarkMode")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<bool>("EnableSearchHistory")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("ApplicationUserId")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("ApplicationPreferences");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("QueryWordInfoQueryWordInfo", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("FollowingId")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<int>("PrecedingId")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.HasKey("FollowingId", "PrecedingId");
|
|
||||||
|
|
||||||
b.HasIndex("PrecedingId");
|
|
||||||
|
|
||||||
b.ToTable("QueryWordInfoQueryWordInfo");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("RoleId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Props.Models.User.ApplicationUser", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Props.Models.User.ApplicationUser", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("RoleId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("Props.Models.User.ApplicationUser", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Props.Models.User.ApplicationUser", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Props.Models.ResultsPreferences", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Props.Models.User.ApplicationUser", "ApplicationUser")
|
|
||||||
.WithOne("ResultsPreferences")
|
|
||||||
.HasForeignKey("Props.Models.ResultsPreferences", "ApplicationUserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("ApplicationUser");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Props.Models.Search.SearchOutline", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Props.Models.User.SearchOutlinePreferences", "SearchOutlinePreferences")
|
|
||||||
.WithMany("SearchOutlines")
|
|
||||||
.HasForeignKey("SearchOutlinePreferencesId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("SearchOutlinePreferences");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Props.Models.User.SearchOutlinePreferences", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Props.Models.User.ApplicationUser", "ApplicationUser")
|
|
||||||
.WithOne("searchOutlinePreferences")
|
|
||||||
.HasForeignKey("Props.Models.User.SearchOutlinePreferences", "ApplicationUserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("ApplicationUser");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Props.Shared.Models.User.ApplicationPreferences", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Props.Models.User.ApplicationUser", "ApplicationUser")
|
|
||||||
.WithOne("ApplicationPreferences")
|
|
||||||
.HasForeignKey("Props.Shared.Models.User.ApplicationPreferences", "ApplicationUserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("ApplicationUser");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("QueryWordInfoQueryWordInfo", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Props.Models.Search.QueryWordInfo", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("FollowingId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("Props.Models.Search.QueryWordInfo", null)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("PrecedingId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Props.Models.User.ApplicationUser", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("ApplicationPreferences")
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("ResultsPreferences")
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("searchOutlinePreferences")
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Props.Models.User.SearchOutlinePreferences", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("SearchOutlines");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
using Props.Shop.Framework;
|
|
||||||
|
|
||||||
namespace Props.Extensions
|
|
||||||
{
|
|
||||||
public static class ProductListingExtensions
|
|
||||||
{
|
|
||||||
public static float? GetRatingToPriceRatio(this ProductListing productListing)
|
|
||||||
{
|
|
||||||
int reviewFactor = productListing.ReviewCount.HasValue ? productListing.ReviewCount.Value : 1;
|
|
||||||
int purchaseFactor = productListing.PurchaseCount.HasValue ? productListing.PurchaseCount.Value : 1;
|
|
||||||
return (productListing.Rating * (reviewFactor > purchaseFactor ? reviewFactor : purchaseFactor)) / (productListing.LowerPrice * productListing.UpperPrice);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Props.Shop.Framework;
|
|
||||||
|
|
||||||
namespace Props.Models.Search
|
|
||||||
{
|
|
||||||
public class ProductListingInfo
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
|
|
||||||
public string ShopName { get; set; }
|
|
||||||
|
|
||||||
public uint Hits { get; set; }
|
|
||||||
|
|
||||||
public ProductListing ProductListing { get; set; }
|
|
||||||
|
|
||||||
public string ProductListingIdentifier { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
|
|
||||||
namespace Props.Models.Search
|
|
||||||
{
|
|
||||||
public class QueryWordInfo
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
public string Word { get; set; }
|
|
||||||
public uint Hits { get; set; }
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
public virtual ISet<QueryWordInfo> Preceding { get; set; }
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
public virtual ISet<QueryWordInfo> Following { get; set; }
|
|
||||||
|
|
||||||
public QueryWordInfo()
|
|
||||||
{
|
|
||||||
this.Preceding = new HashSet<QueryWordInfo>();
|
|
||||||
this.Following = new HashSet<QueryWordInfo>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public QueryWordInfo(ISet<QueryWordInfo> preceding, ISet<QueryWordInfo> following)
|
|
||||||
{
|
|
||||||
this.Preceding = preceding;
|
|
||||||
this.Following = following;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,88 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.Linq;
|
|
||||||
using Props.Models.User;
|
|
||||||
using Props.Shop.Framework;
|
|
||||||
|
|
||||||
namespace Props.Models.Search
|
|
||||||
{
|
|
||||||
public class SearchOutline
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
|
|
||||||
public int SearchOutlinePreferencesId { get; set; }
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
public virtual SearchOutlinePreferences SearchOutlinePreferences { get; set; }
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
public string Name { get; set; }
|
|
||||||
|
|
||||||
public Filters Filters { get; set; }
|
|
||||||
[Required]
|
|
||||||
public ShopSelector DisabledShops { get; set; }
|
|
||||||
|
|
||||||
public sealed class ShopSelector : HashSet<string>
|
|
||||||
{
|
|
||||||
public bool this[string name]
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return this.Contains(name);
|
|
||||||
}
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (value)
|
|
||||||
{
|
|
||||||
this.Add(name);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
this.Remove(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ShopSelector()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public ShopSelector(IEnumerable<string> disabledShops) : base(disabledShops)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Equals(object obj)
|
|
||||||
{
|
|
||||||
if (obj == null || GetType() != obj.GetType())
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
SearchOutline other = (SearchOutline)obj;
|
|
||||||
return
|
|
||||||
Id == other.Id &&
|
|
||||||
Name.Equals(other.Name) &&
|
|
||||||
Filters.Equals(other.Filters) &&
|
|
||||||
DisabledShops.Equals(other.DisabledShops);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return HashCode.Combine(Id, Name, Filters, DisabledShops);
|
|
||||||
}
|
|
||||||
|
|
||||||
public SearchOutline()
|
|
||||||
{
|
|
||||||
this.Filters = new Filters();
|
|
||||||
this.DisabledShops = new ShopSelector();
|
|
||||||
}
|
|
||||||
|
|
||||||
public SearchOutline(string name, Filters filters, ShopSelector disabled)
|
|
||||||
{
|
|
||||||
this.Name = name;
|
|
||||||
this.Filters = filters;
|
|
||||||
this.DisabledShops = disabled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using Props.Models.User;
|
|
||||||
|
|
||||||
namespace Props.Shared.Models.User
|
|
||||||
{
|
|
||||||
public class ApplicationPreferences
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
public string ApplicationUserId { get; set; }
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
public virtual ApplicationUser ApplicationUser { get; set; }
|
|
||||||
|
|
||||||
public bool EnableSearchHistory { get; set; } = true;
|
|
||||||
public bool DarkMode { get; set; } = false;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.Identity;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Props.Models.Search;
|
|
||||||
using Props.Shared.Models.User;
|
|
||||||
|
|
||||||
namespace Props.Models.User
|
|
||||||
{
|
|
||||||
public class ApplicationUser : IdentityUser
|
|
||||||
{
|
|
||||||
[Required]
|
|
||||||
public virtual SearchOutlinePreferences searchOutlinePreferences { get; set; }
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
public virtual ResultsPreferences ResultsPreferences { get; private set; }
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
public virtual ApplicationPreferences ApplicationPreferences { get; private set; }
|
|
||||||
|
|
||||||
public ApplicationUser()
|
|
||||||
{
|
|
||||||
searchOutlinePreferences = new SearchOutlinePreferences();
|
|
||||||
ResultsPreferences = new ResultsPreferences();
|
|
||||||
ApplicationPreferences = new ApplicationPreferences();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ApplicationUser(SearchOutlinePreferences searchOutlinePreferences, ResultsPreferences resultsPreferences, ApplicationPreferences applicationPreferences)
|
|
||||||
{
|
|
||||||
this.searchOutlinePreferences = searchOutlinePreferences;
|
|
||||||
this.ResultsPreferences = resultsPreferences;
|
|
||||||
this.ApplicationPreferences = applicationPreferences;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text.Json;
|
|
||||||
using Props.Models.User;
|
|
||||||
|
|
||||||
namespace Props.Models
|
|
||||||
{
|
|
||||||
public class ResultsPreferences
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
public string ApplicationUserId { get; set; }
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
public virtual ApplicationUser ApplicationUser { get; set; }
|
|
||||||
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
public IList<Category> Order { get; set; }
|
|
||||||
|
|
||||||
public string ProfileName { get; set; }
|
|
||||||
|
|
||||||
public ResultsPreferences()
|
|
||||||
{
|
|
||||||
Order = new List<Category>(Enum.GetValues<Category>().Length);
|
|
||||||
foreach (Category category in Enum.GetValues<Category>())
|
|
||||||
{
|
|
||||||
Order.Add(category);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum Category
|
|
||||||
{
|
|
||||||
RatingPriceRatio,
|
|
||||||
Reviews,
|
|
||||||
Purchases,
|
|
||||||
Price,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using Props.Models.Search;
|
|
||||||
using Props.Models.User;
|
|
||||||
|
|
||||||
namespace Props.Models.User
|
|
||||||
{
|
|
||||||
public class SearchOutlinePreferences
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
public string ApplicationUserId { get; set; }
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
public virtual ApplicationUser ApplicationUser { get; set; }
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
public virtual IList<SearchOutline> SearchOutlines { get; set; }
|
|
||||||
|
|
||||||
public string NameOfLastUsed { get; set; }
|
|
||||||
|
|
||||||
public SearchOutlinePreferences()
|
|
||||||
{
|
|
||||||
SearchOutlines = new List<SearchOutline>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public SearchOutlinePreferences(List<SearchOutline> searchOutlines, string nameOfLastUsed)
|
|
||||||
{
|
|
||||||
this.SearchOutlines = searchOutlines;
|
|
||||||
this.NameOfLastUsed = nameOfLastUsed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
namespace Props.Options
|
|
||||||
{
|
|
||||||
public class MetricsOptions
|
|
||||||
{
|
|
||||||
public const string Metrics = "Metrics";
|
|
||||||
public int MaxQueryWords { get; set; }
|
|
||||||
public int MaxProductListings { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
namespace Props.Options
|
|
||||||
{
|
|
||||||
public class ModulesOptions
|
|
||||||
{
|
|
||||||
public const string Modules = "Modules";
|
|
||||||
public string ModulesDir { get; set; }
|
|
||||||
public string ModuleDataDir { get; set; }
|
|
||||||
public bool RecursiveLoad { get; set; }
|
|
||||||
public string ShopRegex { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
namespace Props.Options
|
|
||||||
{
|
|
||||||
public class SearchOptions
|
|
||||||
{
|
|
||||||
public const string Search = "Search";
|
|
||||||
public int MaxResults { get; set; }
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
namespace Props.Options
|
|
||||||
{
|
|
||||||
public class TextualOptions
|
|
||||||
{
|
|
||||||
public const string Textual = "Textual";
|
|
||||||
public string Dir { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
@page
|
|
||||||
@model ErrorModel
|
|
||||||
@{
|
|
||||||
ViewData["Title"] = "Error";
|
|
||||||
}
|
|
||||||
|
|
||||||
<h1 class="text-danger">Error.</h1>
|
|
||||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
|
||||||
|
|
||||||
@if (Model.ShowRequestId)
|
|
||||||
{
|
|
||||||
<p>
|
|
||||||
<strong>Request ID:</strong> <code>@Model.RequestId</code>
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
|
|
||||||
<h3>Development Mode</h3>
|
|
||||||
<p>
|
|
||||||
Swapping to the <strong>Development</strong> environment displays detailed information about the error that occurred.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
|
||||||
It can result in displaying sensitive information from exceptions to end users.
|
|
||||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
|
||||||
and restarting the app.
|
|
||||||
</p>
|
|
@ -1,32 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Props.Pages
|
|
||||||
{
|
|
||||||
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
|
||||||
[IgnoreAntiforgeryToken]
|
|
||||||
public class ErrorModel : PageModel
|
|
||||||
{
|
|
||||||
public string RequestId { get; set; }
|
|
||||||
|
|
||||||
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
|
||||||
|
|
||||||
private readonly ILogger<ErrorModel> _logger;
|
|
||||||
|
|
||||||
public ErrorModel(ILogger<ErrorModel> logger)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnGet()
|
|
||||||
{
|
|
||||||
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,106 +0,0 @@
|
|||||||
@page
|
|
||||||
@using Microsoft.AspNetCore.Mvc.Localization
|
|
||||||
@model IndexModel
|
|
||||||
@inject IViewLocalizer Localizer
|
|
||||||
@{
|
|
||||||
ViewData["Title"] = "Home page";
|
|
||||||
}
|
|
||||||
|
|
||||||
<section class="jumbotron d-flex flex-column align-items-center">
|
|
||||||
<div>
|
|
||||||
<img alt="Props logo" src="~/images/logo.svg" class="img-fluid" style="max-height: 540px;"
|
|
||||||
asp-append-version="true" />
|
|
||||||
</div>
|
|
||||||
<div class="text-center px-3 my-2 concise">
|
|
||||||
<h1 class="my-2 display-1">Props</h1>
|
|
||||||
<p>
|
|
||||||
@Localizer["description"]
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="jumbotron sub">
|
|
||||||
<div class="container d-flex flex-column align-items-center py-2 concise">
|
|
||||||
<i class="bi bi-search" style="font-size: 5rem;"></i>
|
|
||||||
<h2 class="mb-3 mt-4">@Localizer["help.title"]</h2>
|
|
||||||
<form class="concise my-4">
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="text" class="form-control" placeholder="What are you looking for?" aria-label="Search"
|
|
||||||
aria-describedby="search-btn">
|
|
||||||
<button class="btn btn-outline-primary" type="button" id="search-btn">Search</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<p class="text-center">
|
|
||||||
@Localizer["help.searchIntroduction"]
|
|
||||||
</p>
|
|
||||||
<p class="text-center">
|
|
||||||
@Localizer["help.additionalInfo"]
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="container d-flex flex-column align-items-center my-3 less-concise">
|
|
||||||
<h2 class="mb-3 mt-4">Our Mission</h2>
|
|
||||||
<p class="text-center">
|
|
||||||
@Localizer["mission"]
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<hr class="concise">
|
|
||||||
|
|
||||||
<section class="container d-flex flex-column align-items-center">
|
|
||||||
<div class="less-concise d-flex flex-column align-items-center">
|
|
||||||
<h2 class="mb-3 mt-4">Features</h2>
|
|
||||||
<p class="center">
|
|
||||||
@Localizer["features.description"]
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div style="width: 100%;" data-simplebar>
|
|
||||||
<div class="row px-2 py-3 flex-nowrap">
|
|
||||||
<div class="card mx-2" style="width: 32rem;">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">@Localizer["feature.shoppingList.title"]</h5>
|
|
||||||
<h6 class="card-subtitle mb-3 text-muted">
|
|
||||||
<slot name="subtitle">@Localizer["feature.shoppingList.subtitle"]</slot>
|
|
||||||
</h6>
|
|
||||||
<p class="card-text">
|
|
||||||
@Localizer["feature.shoppingList.text"]
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card mx-2" style="width: 32rem;">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">@Localizer["feature.productComparison.title"]</h5>
|
|
||||||
<h6 class="card-subtitle mb-3 text-muted">
|
|
||||||
<slot name="subtitle">@Localizer["feature.productComparison.subtitle"]</slot>
|
|
||||||
</h6>
|
|
||||||
<p class="card-text">
|
|
||||||
@Localizer["feature.productComparison.text"]
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card mx-2" style="width: 32rem;">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">@Localizer["feature.autoSearch.title"]</h5>
|
|
||||||
<h6 class="card-subtitle mb-3 text-muted">
|
|
||||||
<slot name="subtitle">@Localizer["feature.autoSearch.subtitle"]</slot>
|
|
||||||
</h6>
|
|
||||||
<p class="card-text">
|
|
||||||
@Localizer["feature.autoSearch.text"]
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card mx-2" style="width: 32rem;">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">@Localizer["feature.sharing.title"]</h5>
|
|
||||||
<h6 class="card-subtitle mb-3 text-muted">
|
|
||||||
<slot name="subtitle">@Localizer["feature.sharing.subtitle"]</slot>
|
|
||||||
</h6>
|
|
||||||
<p class="card-text">
|
|
||||||
@Localizer["feature.sharing.text"]
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
@ -1,25 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Props.Pages
|
|
||||||
{
|
|
||||||
public class IndexModel : PageModel
|
|
||||||
{
|
|
||||||
private readonly ILogger<IndexModel> _logger;
|
|
||||||
|
|
||||||
public IndexModel(ILogger<IndexModel> logger)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnGet()
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
@page
|
|
||||||
@model PrivacyModel
|
|
||||||
@{
|
|
||||||
ViewData["Title"] = "Privacy Policy";
|
|
||||||
}
|
|
||||||
<h1>@ViewData["Title"]</h1>
|
|
||||||
|
|
||||||
<p>Use this page to detail your site's privacy policy.</p>
|
|
@ -1,24 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Props.Pages
|
|
||||||
{
|
|
||||||
public class PrivacyModel : PageModel
|
|
||||||
{
|
|
||||||
private readonly ILogger<PrivacyModel> _logger;
|
|
||||||
|
|
||||||
public PrivacyModel(ILogger<PrivacyModel> logger)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnGet()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,302 +0,0 @@
|
|||||||
@page
|
|
||||||
@using Microsoft.AspNetCore.Mvc.Localization
|
|
||||||
@model SearchModel
|
|
||||||
@inject IViewLocalizer Localizer
|
|
||||||
|
|
||||||
@{
|
|
||||||
ViewData["Title"] = "Search";
|
|
||||||
ViewData["Specific"] = "Search";
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="flex-grow-1 d-flex flex-column" x-data="search">
|
|
||||||
<div class="mt-4 mb-3 less-concise">
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="text" class="form-control border-primary" placeholder="What are you looking for?"
|
|
||||||
aria-label="Search" aria-describedby="search-btn" id="search-bar" value="@Model.SearchQuery"
|
|
||||||
x-model="query">
|
|
||||||
<button class="btn btn-outline-secondary" type="button" id="configuration-toggle" data-bs-toggle="collapse"
|
|
||||||
data-bs-target="#configuration"><i class="bi bi-sliders"></i></button>
|
|
||||||
<button class="btn btn-primary" id="search-btn" x-on:click="submitSearch" x-on:keyup="">Search</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="collapse tear" id="configuration">
|
|
||||||
<div class="d-flex">
|
|
||||||
<h1 class="my-3 display-2 mx-auto">
|
|
||||||
<i class="bi bi-sliders" x-show="!timeoutInProgress"></i>
|
|
||||||
<i class="bi bi-cloud-arrow-up" x-show="timeoutInProgress"></i>
|
|
||||||
Configuration
|
|
||||||
</h1>
|
|
||||||
<button class="btn align-self-start m-3" type="button" id="configuration-close" data-bs-toggle="collapse"
|
|
||||||
data-bs-target="#configuration"><i class="bi bi-x-lg"></i></button>
|
|
||||||
</div>
|
|
||||||
<div class="container">
|
|
||||||
<div class="row my-3">
|
|
||||||
<div class="col-lg-3 px-2">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
<h3>Search Outlines</h3>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto" x-show="loggedIn">
|
|
||||||
<button class="btn" x-show="!creatingSearchOutline"
|
|
||||||
x-on:click="createSearchOutlineWithGeneratedName"
|
|
||||||
x-bind:disabled="creatingSearchOutline">
|
|
||||||
<i class="bi bi-plus-lg"></i>
|
|
||||||
</button>
|
|
||||||
<div x-show="creatingSearchOutline" class="spinner-border me-2" role="status">
|
|
||||||
<span class="visually-hidden">Loading...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row px-3">
|
|
||||||
<div style="max-height: 28em;" data-simplebar>
|
|
||||||
<template x-for="(current, i) in searchOutlines">
|
|
||||||
<div class="clean-radio d-flex">
|
|
||||||
<input type="radio" x-bind:id="`${i}-selector`" name="search-outline"
|
|
||||||
x-bind:value="i" x-model="selectedSearchOutline"
|
|
||||||
x-on:click="loadSearchOutline(current)" x-bind:disabled="!searchOutline.ready">
|
|
||||||
<label class="flex-grow-1" x-bind:for="`${i}-selector`">
|
|
||||||
<span class="me-auto" x-text="current"></span>
|
|
||||||
</label>
|
|
||||||
<button class="btn m-1" x-show="loggedIn" x-on:click="deleteSearchOutline(current)"
|
|
||||||
x-bind:disabled="deletingSearchOutline || (searchOutlines.length < 2)">
|
|
||||||
<i class="bi bi-trash"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="text-muted text-center my-3" x-show="!loggedIn">
|
|
||||||
<h3><i class="bi bi-box-arrow-in-right"></i></h3>
|
|
||||||
<p>This is where all your search outlines will show up. <a asp-area="Identity"
|
|
||||||
asp-page="/Account/Login">Sign in</a> to create search outlines!</p>
|
|
||||||
</div>
|
|
||||||
<template x-if="(searchOutlines.length < 2) && (loggedIn)">
|
|
||||||
<div class="text-muted text-center my-3 p-3">
|
|
||||||
<p>Add more search outlines by clicking the <i class="bi bi-plus-lg"></i> above!</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg px-4">
|
|
||||||
<div class="row">
|
|
||||||
<input class="title-input less-concise mx-4"
|
|
||||||
x-bind:class="searchOutline.ready ? '' : 'invisible'" type="text"
|
|
||||||
x-model="searchOutlines[selectedSearchOutline]" x-on:change="SearchOutlineNameChange"
|
|
||||||
x-bind:disabled="(!loggedIn) || (changingName)">
|
|
||||||
</div>
|
|
||||||
<div class="row justify-content-md-center" x-bind:class="searchOutline.ready ? '' : 'invisible'">
|
|
||||||
<section class="col-md px-3">
|
|
||||||
<h3>Price</h3>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="max-price" class="form-label">Maximum Price</label>
|
|
||||||
<div class="input-group">
|
|
||||||
<div class="input-group-text">
|
|
||||||
<input class="form-check-input mt-0" type="checkbox" id="max-price-enabled"
|
|
||||||
x-model="searchOutline.filters.enableUpperPrice"
|
|
||||||
x-on:change="searchOutlineChanged">
|
|
||||||
</div>
|
|
||||||
<span class="input-group-text">$</span>
|
|
||||||
<input type="number" class="form-control" min="0" id="max-price"
|
|
||||||
x-model="searchOutline.filters.upperPrice"
|
|
||||||
x-bind:disabled="!searchOutline.filters.enableUpperPrice"
|
|
||||||
x-on:change="searchOutlineChanged">
|
|
||||||
<span class="input-group-text">.00</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="min-price" class="form-label">Minimum Price</label>
|
|
||||||
<div class="input-group">
|
|
||||||
<span class="input-group-text">$</span>
|
|
||||||
<input type="number" class="form-control" min="0" id="min-price"
|
|
||||||
x-model="searchOutline.filters.lowerPrice" x-on:change="searchOutlineChanged">
|
|
||||||
<span class="input-group-text">.00</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="max-shipping" class="form-label">Maximum Shipping Fee</label>
|
|
||||||
<div class="input-group">
|
|
||||||
<div class="input-group-text">
|
|
||||||
<input class="form-check-input mt-0" type="checkbox" id="max-shipping-enabled"
|
|
||||||
x-model="searchOutline.filters.enableMaxShipping"
|
|
||||||
x-on:change="searchOutlineChanged">
|
|
||||||
</div>
|
|
||||||
<span class="input-group-text">$</span>
|
|
||||||
<input type="number" class="form-control" min="0" id="max-shipping"
|
|
||||||
x-model="searchOutline.filters.maxShippingFee"
|
|
||||||
x-bind:disabled="!searchOutline.filters.enableMaxShipping"
|
|
||||||
x-on:change="searchOutlineChanged">
|
|
||||||
<span class="input-group-text">.00</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input" type="checkbox" id="keep-unknown-shipping"
|
|
||||||
x-model="searchOutline.filters.keepUnknownShipping"
|
|
||||||
x-on:change="searchOutlineChanged">
|
|
||||||
<label class="form-check-label" for="keep-unknown-shipping">Keep Unknown
|
|
||||||
Shipping</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section class="col-md px-3">
|
|
||||||
<h3>Metrics</h3>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="min-purchases" class="form-label">Minimum Purchases</label>
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="number" class="form-control" min="0" id="min-purchases"
|
|
||||||
x-model="searchOutline.filters.minPurchases" x-on:change="searchOutlineChanged">
|
|
||||||
<span class="input-group-text">Purchases</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input" type="checkbox" id="keep-unknown-purchases"
|
|
||||||
x-model="searchOutline.filters.keepUnknownPurchaseCount"
|
|
||||||
x-on:change="searchOutlineChanged">
|
|
||||||
<label class="form-check-label" for="keep-unknown-purchases">Keep Unknown
|
|
||||||
Purchases</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="min-reviews" class="form-label">Minimum Reviews</label>
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="number" class="form-control" min="0" id="min-reviews"
|
|
||||||
x-model="searchOutline.filters.minReviews" x-on:change="searchOutlineChanged">
|
|
||||||
<span class="input-group-text">Reviews</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input" type="checkbox" id="keep-unknown-reviews"
|
|
||||||
x-model="searchOutline.filters.keepUnknownReviewCount"
|
|
||||||
x-on:change="searchOutlineChanged">
|
|
||||||
<label class="form-check-label" for="keep-unknown-reviews">Keep Unknown Number
|
|
||||||
of
|
|
||||||
Reviews</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-1">
|
|
||||||
<label for="min-rating" class="form-label">Minimum Rating</label>
|
|
||||||
<input type="range" class="form-range" id="min-rating" min="0" max="100" step="1"
|
|
||||||
x-model="searchOutline.filters.minRating" x-on:change="searchOutlineChanged">
|
|
||||||
<div id="min-rating-display" class="form-text"
|
|
||||||
x-text="searchOutline.filters.minRating + '%'">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input" type="checkbox" id="keep-unrated"
|
|
||||||
x-model="searchOutline.filters.keepUnrated" x-on:change="searchOutlineChanged">
|
|
||||||
<label class="form-check-label" for="keep-unrated">Keep Unrated Items</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section class="col-md px-3">
|
|
||||||
<h3>Shops Enabled</h3>
|
|
||||||
<template x-for="shopName in Object.keys(searchOutline.shopToggles)">
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input" type="checkbox" value=""
|
|
||||||
x-bind:id="`${encodeURIComponent(shopName)}-enabled`"
|
|
||||||
x-model="searchOutline.shopToggles[shopName]"
|
|
||||||
x-on:change="searchOutlineChanged">
|
|
||||||
<label class="form-check-label"
|
|
||||||
x-bind:for="`${encodeURIComponent(shopName)}-enabled`" x-text="shopName">
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
<div x-show="!searchOutline.ready">
|
|
||||||
<div class="spinner-border center-overlay" role="status">
|
|
||||||
<span class="visually-hidden">Loading...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="content-pages" class="multipage mt-3 invisible">
|
|
||||||
<ul class="nav nav-pills selectors">
|
|
||||||
<li class="nav-item" role="presentation">
|
|
||||||
<button type="button" data-bs-toggle="pill" data-bs-target="#quick-picks-slide"><i
|
|
||||||
class="bi bi-stopwatch"></i></button>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item" role="presentation">
|
|
||||||
<button type="button" data-bs-toggle="pill" data-bs-target="#results-slide"><i
|
|
||||||
class="bi bi-view-list"></i></button>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item" role="presentation">
|
|
||||||
<button type="button" data-bs-toggle="pill" data-bs-target="#info-slide"><i
|
|
||||||
class="bi bi-info-lg"></i></button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<div class="multipage-slides tab-content">
|
|
||||||
<div class="multipage-slide tab-pane fade" id="quick-picks-slide">
|
|
||||||
<div class="multipage-title">
|
|
||||||
<h1 class="display-2"><i class="bi bi-stopwatch"></i> Quick Picks</h1>
|
|
||||||
<template x-if="hasResults()">
|
|
||||||
<p>@Localizer["quickPicks.searched"]</p>
|
|
||||||
</template>
|
|
||||||
<template x-if="!hasResults()">
|
|
||||||
<p>@Localizer["quickPicks.prompt"]</p>
|
|
||||||
</template>
|
|
||||||
<hr class="less-concise">
|
|
||||||
</div>
|
|
||||||
<div class="multipage-content">
|
|
||||||
<template x-if="hasResults()">
|
|
||||||
<template x-if="results.bestPrice">
|
|
||||||
<p>Here's the listing with the lowest price.</p>
|
|
||||||
<div>
|
|
||||||
@* TODO: Implement best price display here *@
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
@* TODO: Add display for top results. *@
|
|
||||||
</template>
|
|
||||||
<template x-if="!hasResults()">
|
|
||||||
<div
|
|
||||||
class="text-center less-concise text-muted flex-grow-1 justify-content-center d-flex flex-column">
|
|
||||||
<h2>@Localizer["notSearched"]</h2>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="multipage-slide tab-pane fade" id="results-slide" x-data>
|
|
||||||
<div class="multipage-title">
|
|
||||||
<h2><i class="bi bi-view-list"></i> Results</h2>
|
|
||||||
<template x-if="hasResults()">
|
|
||||||
<p>@Localizer["results.searched"]</p>
|
|
||||||
</template>
|
|
||||||
<template x-if="!hasResults()">
|
|
||||||
<p>@Localizer["results.prompt"]</p>
|
|
||||||
</template>
|
|
||||||
<hr class="less-concise">
|
|
||||||
</div>
|
|
||||||
<div class="multipage-content">
|
|
||||||
<template x-if="hasResults()">
|
|
||||||
@* TODO: Display results with UI for sorting and changing views. *@
|
|
||||||
</template>
|
|
||||||
<template x-if="!hasResults()">
|
|
||||||
<div
|
|
||||||
class="text-center less-concise text-muted flex-grow-1 justify-content-center d-flex flex-column">
|
|
||||||
<h2>@Localizer["notSearched"]</h2>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="multipage-slide tab-pane fade" id="info-slide">
|
|
||||||
<div class="multipage-content">
|
|
||||||
<div class="less-concise text-muted flex-grow-1 justify-content-center d-flex flex-column">
|
|
||||||
<h1 class="display-3"><i class="bi bi-info-circle"></i> Get Started!</h1>
|
|
||||||
<ol>
|
|
||||||
<li>@Localizer["instructions.type"]</li>
|
|
||||||
<li>@Localizer["instructions.configure"]</li>
|
|
||||||
<li>@Localizer["instructions.search"]</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,26 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Castle.Core.Internal;
|
|
||||||
using Microsoft.AspNetCore.Identity;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
|
||||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Props.Data;
|
|
||||||
using Props.Extensions;
|
|
||||||
using Props.Models.Search;
|
|
||||||
using Props.Models.User;
|
|
||||||
using Props.Services.Modules;
|
|
||||||
using Props.Shop.Framework;
|
|
||||||
|
|
||||||
namespace Props.Pages
|
|
||||||
{
|
|
||||||
public class SearchModel : PageModel
|
|
||||||
{
|
|
||||||
[BindProperty(Name = "q", SupportsGet = true)]
|
|
||||||
public string SearchQuery { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,76 +0,0 @@
|
|||||||
@using Microsoft.AspNetCore.Identity
|
|
||||||
@using Props.Models.User
|
|
||||||
@inject SignInManager<ApplicationUser> SignInManager
|
|
||||||
@inject UserManager<ApplicationUser> UserManager
|
|
||||||
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>@ViewData["Title"] - Props</title>
|
|
||||||
<script src="~/js/site.js" asp-append-version="true"></script>
|
|
||||||
@if (!string.IsNullOrEmpty((ViewData["Specific"] as string)))
|
|
||||||
{
|
|
||||||
@* Adds page specific scripts semi-automatically. *@
|
|
||||||
<script defer src="@($"~/js/specific/{(ViewData["Specific"])}.js")" asp-append-version="true"></script>
|
|
||||||
}
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body class="theme-light">
|
|
||||||
<header>
|
|
||||||
<nav id="nav">
|
|
||||||
<div class="container-fluid">
|
|
||||||
<a class="navbar-brand" asp-area="" asp-page="/Index">Props</a>
|
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarContent">
|
|
||||||
<i class="bi bi-list" style="width: 100%; height: auto;"></i>
|
|
||||||
</button>
|
|
||||||
<div class="collapse navbar-collapse" id="navbarContent">
|
|
||||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
|
||||||
<li class="nav-item">
|
|
||||||
<nav-link class="nav-link" asp-area="" asp-page="/Index">Home</nav-link>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<nav-link class="nav-link" asp-area="" asp-page="/Search">Search</nav-link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<ul class="navbar-nav mb-2 mb-lg-0">
|
|
||||||
@if (SignInManager.IsSignedIn(User))
|
|
||||||
{
|
|
||||||
<li class="nav-item">
|
|
||||||
<nav-link class="nav-link" asp-area="Identity" asp-page="/Account/Manage/Index"
|
|
||||||
title="Manage">Hello @User.Identity.Name!</nav-link>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<form class="form-inline" asp-area="Identity" asp-page="/Account/Logout"
|
|
||||||
asp-route-returnUrl="@Url.Page("/", new { area = "" })" method="post">
|
|
||||||
<button type="submit" class="nav-link btn btn-link">Logout</button>
|
|
||||||
</form>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<li class="nav-item">
|
|
||||||
<nav-link class="nav-link" asp-area="Identity" asp-page="/Account/Register">Register
|
|
||||||
</nav-link>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<nav-link class="nav-link" asp-area="Identity" asp-page="/Account/Login">Login</nav-link>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
<main role="main">
|
|
||||||
@RenderBody()
|
|
||||||
</main>
|
|
||||||
<footer id="footer">
|
|
||||||
© 2021 - Props - <a asp-area="" asp-page="/Privacy">Privacy</a>
|
|
||||||
</footer>
|
|
||||||
@await RenderSectionAsync("Scripts", required: false)
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
@ -1,27 +0,0 @@
|
|||||||
@using Microsoft.AspNetCore.Identity
|
|
||||||
@using Props.Models.User
|
|
||||||
@inject SignInManager<ApplicationUser> SignInManager
|
|
||||||
@inject UserManager<ApplicationUser> UserManager
|
|
||||||
|
|
||||||
<ul class="navbar-nav">
|
|
||||||
@if (SignInManager.IsSignedIn(User))
|
|
||||||
{
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">Hello @User.Identity.Name!</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<form class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="@Url.Page("/", new { area = "" })" method="post">
|
|
||||||
<button type="submit" class="nav-link btn btn-link text-dark">Logout</button>
|
|
||||||
</form>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Register">Register</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Login">Login</a>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
@ -1,2 +0,0 @@
|
|||||||
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
|
|
||||||
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
|
|
@ -1,6 +0,0 @@
|
|||||||
@using Microsoft.AspNetCore.Identity
|
|
||||||
@using Props
|
|
||||||
@using Props.Data
|
|
||||||
@namespace Props.Pages
|
|
||||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
|
||||||
@addTagHelper *, Props
|
|
@ -1,4 +0,0 @@
|
|||||||
@{
|
|
||||||
Layout = "_Layout";
|
|
||||||
ViewData["Specific"] = null;
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Hosting;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using System.IO;
|
|
||||||
using System;
|
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
namespace Props
|
|
||||||
{
|
|
||||||
public class Program
|
|
||||||
{
|
|
||||||
public static void Main(string[] args)
|
|
||||||
{
|
|
||||||
CreateHostBuilder(args).Build().Run();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
|
||||||
Host.CreateDefaultBuilder(args)
|
|
||||||
.ConfigureWebHostDefaults(webBuilder =>
|
|
||||||
{
|
|
||||||
webBuilder.UseWebRoot(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "wwwroot/")).UseStartup<Startup>();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"iisSettings": {
|
|
||||||
"windowsAuthentication": false,
|
|
||||||
"anonymousAuthentication": true,
|
|
||||||
"iisExpress": {
|
|
||||||
"applicationUrl": "http://localhost:31014",
|
|
||||||
"sslPort": 44369
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"profiles": {
|
|
||||||
"IIS Express": {
|
|
||||||
"commandName": "IISExpress",
|
|
||||||
"launchBrowser": true,
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Props": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"dotnetRunMessages": "true",
|
|
||||||
"launchBrowser": true,
|
|
||||||
"applicationUrl": "https://localhost:5001;http://localhost:5000",
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,96 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
|
||||||
<!-- https://docs.microsoft.com/en-us/dotnet/core/project-sdk/msbuild-props for more information. -->
|
|
||||||
<!-- Publish arguments: https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-publish -->
|
|
||||||
<!-- Single file docs: https://docs.microsoft.com/en-us/dotnet/core/deploying/single-file/overview -->
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
|
||||||
<PublishSingleFile>true</PublishSingleFile>
|
|
||||||
<SelfContained>true</SelfContained>
|
|
||||||
<PublishTrimmed>false</PublishTrimmed>
|
|
||||||
<PublishReadyToRun>true</PublishReadyToRun>
|
|
||||||
<InvariantGlobalization>true</InvariantGlobalization>
|
|
||||||
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
|
|
||||||
<UserSecretsId>aspnet-Props-20A2A991-EC61-4C06-91D2-953482026A7B</UserSecretsId>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<None Update="app.db" CopyToOutputDirectory="PreserveNewest" ExcludeFromSingleFile="true" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.7" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.7" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="8.0.7" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.7">
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="8.0.7" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.7" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.7" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.7">
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="8.0.3" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Props.Shop\Framework\Props.Shop.Framework.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<!-- Removing ASP.Net Core Identity static content -->
|
|
||||||
<!-- See https://docs.microsoft.com/en-us/aspnet/core/security/authentication/identity?view=aspnetcore-6.0&tabs=visual-studio#prevent-publish-of-static-identity-assets -->
|
|
||||||
<PropertyGroup>
|
|
||||||
<ResolveStaticWebAssetsInputsDependsOn>RemoveIdentityAssets</ResolveStaticWebAssetsInputsDependsOn>
|
|
||||||
</PropertyGroup>
|
|
||||||
<Target Name="RemoveIdentityAssets">
|
|
||||||
<ItemGroup>
|
|
||||||
<StaticWebAsset Remove="@(StaticWebAsset)" Condition="%(SourceId) == 'Microsoft.AspNetCore.Identity.UI'" />
|
|
||||||
</ItemGroup>
|
|
||||||
</Target>
|
|
||||||
|
|
||||||
<!-- For embedding content into single file. -->
|
|
||||||
<ItemGroup>
|
|
||||||
<Content Remove="wwwroot\**" />
|
|
||||||
<None Include="wwwroot\**" ExcludeFromSingleFile="false">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<!-- Ignore client-side asset compilation configuration. -->
|
|
||||||
<Content Remove="package.json" />
|
|
||||||
<Content Remove="package-lock.json" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<!-- Modules for all the shops. -->
|
|
||||||
<ItemGroup>
|
|
||||||
<None Update="shops\**\*" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" ExcludeFromSingleFile="true" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<!-- Watch configurations. -->
|
|
||||||
<ItemGroup>
|
|
||||||
<Watch Include="assets\**\*.js;assets\**\*.scss" Exclude="wwwroot\**\*;node_modules\**\*;**\*.js.map;obj\**\*;bin\**\*" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Ensure Node.js is installed -->
|
|
||||||
<Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" !Exists('node_modules') ">
|
|
||||||
<Exec Command="node --version" ContinueOnError="false">
|
|
||||||
<Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
|
|
||||||
</Exec>
|
|
||||||
<Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE." />
|
|
||||||
<Message Importance="high" Text="Restoring dependencies using 'npm'. This may take several minutes..." />
|
|
||||||
<Exec WorkingDirectory="./" Command="npm install" />
|
|
||||||
</Target>
|
|
||||||
|
|
||||||
<!-- Build static resources -->
|
|
||||||
<Target Name="BuildWebpack" BeforeTargets="Build">
|
|
||||||
<Message Importance="high" Text="Building client-side assets..." />
|
|
||||||
<Exec Command="npm run build:dev" Condition=" '$(Configuration)' == 'Debug' " LogStandardErrorAsError="true">
|
|
||||||
<Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
|
|
||||||
</Exec>
|
|
||||||
<Exec Command="npm run build:prod" Condition=" '$(Configuration)' == 'Release' " LogStandardErrorAsError="true">
|
|
||||||
<Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
|
|
||||||
</Exec>
|
|
||||||
<Error Condition="'$(ErrorCode)' != '0'" Text="There was an issue attempting to build the static assets. To continue, fix client-side asset errors and try again." />
|
|
||||||
</Target>
|
|
||||||
</Project>
|
|
@ -1,3 +0,0 @@
|
|||||||
Support for ASP.NET Core Identity was added to your project.
|
|
||||||
|
|
||||||
For setup and configuration information, see https://go.microsoft.com/fwlink/?linkid=2116645.
|
|
@ -1,18 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Props.Models.Search;
|
|
||||||
using Props.Shop.Framework;
|
|
||||||
|
|
||||||
namespace Props.Services.Modules
|
|
||||||
{
|
|
||||||
public interface IMetricsManager
|
|
||||||
{
|
|
||||||
public IEnumerable<ProductListingInfo> RetrieveTopListings(int max = 10);
|
|
||||||
|
|
||||||
public IEnumerable<string> RetrieveCommonKeywords(int max = 50);
|
|
||||||
|
|
||||||
public void RegisterSearchQuery(string query);
|
|
||||||
|
|
||||||
public void RegisterProductListing(ProductListing productListing, string shopName);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Props.Models.Search;
|
|
||||||
using Props.Shop.Framework;
|
|
||||||
|
|
||||||
namespace Props.Services.Modules
|
|
||||||
{
|
|
||||||
public interface ISearchManager
|
|
||||||
{
|
|
||||||
public IShopManager ShopManager { get; }
|
|
||||||
public Task<IEnumerable<ProductListing>> Search(string query, SearchOutline searchOutline);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Props.Models.Search;
|
|
||||||
using Props.Shop.Framework;
|
|
||||||
|
|
||||||
namespace Props.Services.Modules
|
|
||||||
{
|
|
||||||
public interface IShopManager : IAsyncDisposable
|
|
||||||
{
|
|
||||||
public ValueTask<IEnumerable<string>> GetAllShopNames();
|
|
||||||
public ValueTask<IShop> GetShop(string name);
|
|
||||||
public ValueTask<IEnumerable<IShop>> GetAllShops();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,113 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Props.Data;
|
|
||||||
using Props.Models.Search;
|
|
||||||
using Props.Options;
|
|
||||||
using Props.Shop.Framework;
|
|
||||||
|
|
||||||
namespace Props.Services.Modules
|
|
||||||
{
|
|
||||||
public class LiveMetricsManager : IMetricsManager
|
|
||||||
{
|
|
||||||
private MetricsOptions metricsOptions;
|
|
||||||
private ILogger<LiveMetricsManager> logger;
|
|
||||||
private ApplicationDbContext dbContext;
|
|
||||||
private IQueryable<ProductListingInfo> leastPopularProductListings;
|
|
||||||
private IQueryable<QueryWordInfo> leastPopularQueryWords;
|
|
||||||
public LiveMetricsManager(ApplicationDbContext dbContext, ILogger<LiveMetricsManager> logger, IConfiguration configuration)
|
|
||||||
{
|
|
||||||
this.metricsOptions = configuration.GetSection(MetricsOptions.Metrics).Get<MetricsOptions>();
|
|
||||||
this.logger = logger;
|
|
||||||
this.dbContext = dbContext;
|
|
||||||
leastPopularProductListings = from listing in dbContext.ProductListingInfos orderby listing.Hits ascending select listing;
|
|
||||||
leastPopularQueryWords = from word in dbContext.QueryWords orderby word.Hits ascending select word;
|
|
||||||
}
|
|
||||||
public IEnumerable<ProductListingInfo> RetrieveTopListings(int max)
|
|
||||||
{
|
|
||||||
if (dbContext.ProductListingInfos == null) return null;
|
|
||||||
return (from l in dbContext.ProductListingInfos
|
|
||||||
orderby l.Hits descending
|
|
||||||
select l).Take(max);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<string> RetrieveCommonKeywords(int max)
|
|
||||||
{
|
|
||||||
if (dbContext.QueryWords == null) return null;
|
|
||||||
return (from k in dbContext.QueryWords
|
|
||||||
orderby k.Hits descending
|
|
||||||
select k.Word).Take(max);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RegisterSearchQuery(string query)
|
|
||||||
{
|
|
||||||
query = query.ToLower();
|
|
||||||
string[] tokens = query.Split(' ');
|
|
||||||
QueryWordInfo[] wordInfos = new QueryWordInfo[tokens.Length];
|
|
||||||
for (int wordIndex = 0; wordIndex < tokens.Length; wordIndex++)
|
|
||||||
{
|
|
||||||
QueryWordInfo queryWordInfo = dbContext.QueryWords.Where((k) => k.Word.ToLower().Equals(tokens[wordIndex])).SingleOrDefault() ?? new QueryWordInfo();
|
|
||||||
if (queryWordInfo.Hits == 0)
|
|
||||||
{
|
|
||||||
queryWordInfo.Word = tokens[wordIndex];
|
|
||||||
dbContext.QueryWords.Add(queryWordInfo);
|
|
||||||
}
|
|
||||||
queryWordInfo.Hits += 1;
|
|
||||||
wordInfos[wordIndex] = queryWordInfo;
|
|
||||||
}
|
|
||||||
for (int wordIndex = 0; wordIndex < tokens.Length; wordIndex++)
|
|
||||||
{
|
|
||||||
for (int beforeIndex = 0; beforeIndex < wordIndex; beforeIndex++)
|
|
||||||
{
|
|
||||||
wordInfos[wordIndex].Preceding.Add(wordInfos[beforeIndex]);
|
|
||||||
}
|
|
||||||
for (int afterIndex = wordIndex; afterIndex < tokens.Length; afterIndex++)
|
|
||||||
{
|
|
||||||
wordInfos[wordIndex].Following.Add(wordInfos[afterIndex]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CullQueryWords();
|
|
||||||
dbContext.SaveChanges();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RegisterProductListing(ProductListing productListing, string shopName)
|
|
||||||
{
|
|
||||||
ProductListingInfo productListingInfo =
|
|
||||||
(from info in dbContext.ProductListingInfos
|
|
||||||
where info.ProductListingIdentifier.Equals(productListing.Identifier)
|
|
||||||
select info).SingleOrDefault() ?? new ProductListingInfo();
|
|
||||||
if (productListingInfo.Hits == 0)
|
|
||||||
{
|
|
||||||
dbContext.Add(productListingInfo);
|
|
||||||
}
|
|
||||||
productListingInfo.ShopName = shopName;
|
|
||||||
productListingInfo.ProductListing = productListing;
|
|
||||||
productListingInfo.ProductListingIdentifier = productListing.Identifier;
|
|
||||||
productListingInfo.Hits += 1;
|
|
||||||
|
|
||||||
CullProductListings();
|
|
||||||
dbContext.SaveChanges();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CullProductListings()
|
|
||||||
{
|
|
||||||
int surplus = dbContext.ProductListingInfos.Count() - metricsOptions.MaxProductListings;
|
|
||||||
if (surplus > 0)
|
|
||||||
{
|
|
||||||
dbContext.RemoveRange(leastPopularProductListings.Take(surplus));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CullQueryWords()
|
|
||||||
{
|
|
||||||
int surplus = dbContext.QueryWords.Count() - metricsOptions.MaxQueryWords;
|
|
||||||
if (surplus > 0)
|
|
||||||
{
|
|
||||||
dbContext.RemoveRange(leastPopularQueryWords.Take(surplus));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user