Compare commits
169 Commits
e9eec03a54
...
a92762b85b
Author | SHA1 | Date | |
---|---|---|---|
a92762b85b | |||
0abfc60fe9 | |||
3e82b1d01e | |||
cac2b8ace7 | |||
7a78dcb339 | |||
046b85ec4a | |||
79f7688980 | |||
9f291dcfed | |||
4bc74d0917 | |||
60e9b06a21 | |||
ad1c5a3180 | |||
a2ed5c2e58 | |||
a4adb7c582 | |||
9631cb4b6b | |||
73cc43af3c | |||
d0217c2166 | |||
e2ffd6f976 | |||
a7c0e0dea7 | |||
eadd104808 | |||
0c7b85351c | |||
4841d539fc | |||
d4680b934e | |||
dbb1a04c36 | |||
6ba4e41de9 | |||
85f4d61ff0 | |||
a03d3c5218 | |||
7f66b73e7a | |||
44acb94aa5 | |||
4d49ce33c2 | |||
e83d19b699 | |||
8801bea801 | |||
a2bbd112d9 | |||
43c1bc1611 | |||
ab6584065b | |||
85d8f2bf6f | |||
7d85b8625f | |||
1a3a9e00e7 | |||
2069b38dbd | |||
f2c297fd88 | |||
c75f1e4042 | |||
47883bfe4a | |||
a7dd5b5f74 | |||
efc016cc05 | |||
f09b59c500 | |||
f82de591c8 | |||
6c34225aab | |||
6fc68a1fb6 | |||
96b36e0650 | |||
140f8bbf53 | |||
13af9e5434 | |||
2391ca1ae1 | |||
190ea16b02 | |||
093670385e | |||
0488df2ed1 | |||
aa5c725e50 | |||
9192e9b0f8 | |||
e13a14fb2e | |||
5f2f648eaa | |||
19f0eeb9bf | |||
e03f8867ae | |||
c5056eddf2 | |||
7306c0ce86 | |||
cbbd67d9f6 | |||
8dc2ff21c9 | |||
2feb46a533 | |||
f8ad3b2970 | |||
8abd75506c | |||
1d35e8a838 | |||
89955968bf | |||
38a9ea605a | |||
2ae3c32ae4 | |||
bd28df53de | |||
b4ec5844e4 | |||
9cc55e516d | |||
44e072a723 | |||
3951fb26da | |||
07c41be42d | |||
c1d8891c44 | |||
b78c6d2ea5 | |||
c6b8ca523b | |||
8a1e5aca15 | |||
ff080390f8 | |||
0b507b90a1 | |||
38ffb3c7e1 | |||
c94ea4a624 | |||
f71758ca69 | |||
5d8a4a3803 | |||
f31293d886 | |||
3a079206b0 | |||
21cd712667 | |||
66aba04156 | |||
4476b1b3e1 | |||
2c90678141 | |||
c7bc6ca8fa | |||
4de4e8dfa1 | |||
d91acd36f7 | |||
22dd766db3 | |||
e22c2b3049 | |||
2719142538 | |||
3129e5e564 | |||
4bafefa4dc | |||
b43d7bab84 | |||
57f67391f1 | |||
e0756e0967 | |||
56544938ac | |||
840b59fcba | |||
bad22090a3 | |||
7e8a398741 | |||
cefd02f202 | |||
b62b85fccb | |||
0e93992beb | |||
c597d65256 | |||
9e55b459fc | |||
e953c52092 | |||
8b29c6f999 | |||
c1633b0b51 | |||
80978c652a | |||
fbcf6fe586 | |||
e43d1294c4 | |||
4e12a4b7fc | |||
3d3c43b944 | |||
54b1565537 | |||
7e240bd584 | |||
21fe7845f8 | |||
c9d9d5bc62 | |||
56e2b948ee | |||
11fa15fe62 | |||
04f6657ed3 | |||
2fdcc486ce | |||
3957d65370 | |||
d57a61d5ca | |||
78006f79d0 | |||
065d786dd7 | |||
9e3de4b6dc | |||
7d4be012cd | |||
b311206ff1 | |||
3c63ebc613 | |||
2d1f599bbf | |||
3611e4be34 | |||
f459cbdfda | |||
862fbe15ed | |||
36ae3e5c99 | |||
235196f8e5 | |||
bbb2d4bd04 | |||
1fff881df4 | |||
0e3deafca0 | |||
c67db54eeb | |||
3f1b7d9ac6 | |||
d6dbe55e46 | |||
549d9d7e99 | |||
ac13a6352b | |||
1f519e60b1 | |||
5f4429098d | |||
d87025c8b4 | |||
d2084efa7d | |||
d1ea0c7337 | |||
d5c89fa6ca | |||
6c684372df | |||
e07b234eb2 | |||
e675962c35 | |||
04d4caf2bd | |||
3218fbf4e3 | |||
99656133c9 | |||
9dc8917aa5 | |||
5d3a74a89e | |||
0f3f1d866a | |||
86d5eceeaf | |||
3057bb8dfc | |||
f2fb7bd732 |
11
.devcontainer/Dockerfile
Normal file
11
.devcontainer/Dockerfile
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
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>
|
31
.devcontainer/devcontainer.json
Normal file
31
.devcontainer/devcontainer.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// 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'"
|
||||||
|
|
||||||
|
}
|
3
.devcontainer/noop.txt
Normal file
3
.devcontainer/noop.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
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,6 +1,5 @@
|
|||||||
|
# Created by https://www.toptal.com/developers/gitignore/api/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=aspnetcore,visualstudiocode,dotnetcore,python,database,node
|
||||||
# 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
|
||||||
@ -30,7 +29,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*/
|
||||||
@ -272,7 +271,287 @@ __pycache__/
|
|||||||
# Cake - Uncomment if you are using it
|
# Cake - Uncomment if you are using it
|
||||||
# tools/
|
# tools/
|
||||||
|
|
||||||
### vscode ###
|
### Database ###
|
||||||
|
*.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
|
||||||
@ -280,4 +559,20 @@ __pycache__/
|
|||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
*.code-workspace
|
*.code-workspace
|
||||||
|
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/vscode,aspnetcore
|
# Local History for Visual Studio Code
|
||||||
|
.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
Normal file
0
.gitmodules
vendored
Normal file
26
.vscode/launch.json
vendored
Normal file
26
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"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
Normal file
7
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"python.defaultInterpreterPath": "/opt/conda/bin/python",
|
||||||
|
"dotnet.dotnetPath": "/opt/conda/lib/dotnet/",
|
||||||
|
"omnisharp.dotNetCliPaths": [
|
||||||
|
"/opt/conda/lib/dotnet/dotnet"
|
||||||
|
]
|
||||||
|
}
|
41
.vscode/tasks.json
vendored
Normal file
41
.vscode/tasks.json
vendored
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
9
.woodpecker.yaml
Normal file
9
.woodpecker.yaml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
steps:
|
||||||
|
- name: build
|
||||||
|
image: debian
|
||||||
|
commands:
|
||||||
|
- echo "This is the build step"
|
||||||
|
- name: a-test-step
|
||||||
|
image: debian
|
||||||
|
commands:
|
||||||
|
- echo "Testing.."
|
56
Jenkinsfile
vendored
Normal file
56
Jenkinsfile
vendored
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
Props.Shop/Adafruit.Tests/AdafruitShopTest.cs
Normal file
21
Props.Shop/Adafruit.Tests/AdafruitShopTest.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
89
Props.Shop/Adafruit.Tests/Api/FakeProductListingManager.cs
Normal file
89
Props.Shop/Adafruit.Tests/Api/FakeProductListingManager.cs
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
Props.Shop/Adafruit.Tests/Api/ListingParserTest.cs
Normal file
21
Props.Shop/Adafruit.Tests/Api/ListingParserTest.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
Props.Shop/Adafruit.Tests/Api/SearchManagerTest.cs
Normal file
21
Props.Shop/Adafruit.Tests/Api/SearchManagerTest.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
158548
Props.Shop/Adafruit.Tests/Assets/products.json
Normal file
158548
Props.Shop/Adafruit.Tests/Assets/products.json
Normal file
File diff suppressed because it is too large
Load Diff
36
Props.Shop/Adafruit.Tests/Props.Shop.Adafruit.Tests.csproj
Normal file
36
Props.Shop/Adafruit.Tests/Props.Shop.Adafruit.Tests.csproj
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<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>
|
171
Props.Shop/Adafruit/AdafruitShop.cs
Normal file
171
Props.Shop/Adafruit/AdafruitShop.cs
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
19
Props.Shop/Adafruit/Api/IProductListingManager.cs
Normal file
19
Props.Shop/Adafruit/Api/IProductListingManager.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
52
Props.Shop/Adafruit/Api/ListingsParser.cs
Normal file
52
Props.Shop/Adafruit/Api/ListingsParser.cs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
125
Props.Shop/Adafruit/Api/LiveProductListingManager.cs
Normal file
125
Props.Shop/Adafruit/Api/LiveProductListingManager.cs
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
61
Props.Shop/Adafruit/Api/SearchManager.cs
Normal file
61
Props.Shop/Adafruit/Api/SearchManager.cs
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
Props.Shop/Adafruit/Persistence/Configuration.cs
Normal file
17
Props.Shop/Adafruit/Persistence/Configuration.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
Props.Shop/Adafruit/Persistence/ProductListingCacheData.cs
Normal file
23
Props.Shop/Adafruit/Persistence/ProductListingCacheData.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
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()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
Props.Shop/Adafruit/Props.Shop.Adafruit.csproj
Normal file
17
Props.Shop/Adafruit/Props.Shop.Adafruit.csproj
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<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>
|
8
Props.Shop/Framework/Currency.cs
Normal file
8
Props.Shop/Framework/Currency.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace Props.Shop.Framework
|
||||||
|
{
|
||||||
|
public enum Currency
|
||||||
|
{
|
||||||
|
CAD,
|
||||||
|
USD
|
||||||
|
}
|
||||||
|
}
|
108
Props.Shop/Framework/Filters.cs
Normal file
108
Props.Shop/Framework/Filters.cs
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
Props.Shop/Framework/IOption.cs
Normal file
14
Props.Shop/Framework/IOption.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
23
Props.Shop/Framework/IShop.cs
Normal file
23
Props.Shop/Framework/IShop.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
55
Props.Shop/Framework/ProductListing.cs
Normal file
55
Props.Shop/Framework/ProductListing.cs
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
Props.Shop/Framework/Props.Shop.Framework.csproj
Normal file
11
Props.Shop/Framework/Props.Shop.Framework.csproj
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
20
Props.Shop/Framework/SupportedFeatures.cs
Normal file
20
Props.Shop/Framework/SupportedFeatures.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
Props.Tests/Props.Tests.csproj
Normal file
24
Props.Tests/Props.Tests.csproj
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<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>
|
12
Props.Tests/UnitTest1.cs
Normal file
12
Props.Tests/UnitTest1.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Props.Tests;
|
||||||
|
|
||||||
|
public class UnitTest1
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Test1()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
6
Props/.editorconfig
Normal file
6
Props/.editorconfig
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
[*.{js,jsx,ts,tsx,vue}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
quote_type = double
|
31
Props/.eslintrc.js
Normal file
31
Props/.eslintrc.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
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
Normal file
36
Props/.vscode/launch.json
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"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
Normal file
9
Props/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"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
Normal file
51
Props/.vscode/tasks.json
vendored
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"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": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
20
Props/Areas/Identity/IdentityHostingStartup.cs
Normal file
20
Props/Areas/Identity/IdentityHostingStartup.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
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) => {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
Props/Areas/Identity/Pages/Account/ConfirmEmail.cshtml
Normal file
7
Props/Areas/Identity/Pages/Account/ConfirmEmail.cshtml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
@page
|
||||||
|
@model ConfirmEmailModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Confirm email";
|
||||||
|
}
|
||||||
|
|
||||||
|
<h1>@ViewData["Title"]</h1>
|
48
Props/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs
Normal file
48
Props/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
83
Props/Areas/Identity/Pages/Account/Login.cshtml
Normal file
83
Props/Areas/Identity/Pages/Account/Login.cshtml
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
@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" />
|
||||||
|
}
|
112
Props/Areas/Identity/Pages/Account/Login.cshtml.cs
Normal file
112
Props/Areas/Identity/Pages/Account/Login.cshtml.cs
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
Props/Areas/Identity/Pages/Account/Logout.cshtml
Normal file
21
Props/Areas/Identity/Pages/Account/Logout.cshtml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
@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>
|
45
Props/Areas/Identity/Pages/Account/Logout.cshtml.cs
Normal file
45
Props/Areas/Identity/Pages/Account/Logout.cshtml.cs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
50
Props/Areas/Identity/Pages/Account/Manage/ManageNavPages.cs
Normal file
50
Props/Areas/Identity/Pages/Account/Manage/ManageNavPages.cs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
32
Props/Areas/Identity/Pages/Account/Manage/_Layout.cshtml
Normal file
32
Props/Areas/Identity/Pages/Account/Manage/_Layout.cshtml
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
@{
|
||||||
|
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)
|
||||||
|
}
|
24
Props/Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml
Normal file
24
Props/Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
@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. *@
|
@ -0,0 +1,10 @@
|
|||||||
|
@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>
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
@using Props.Areas.Identity.Pages.Account.Manage
|
59
Props/Areas/Identity/Pages/Account/Register.cshtml
Normal file
59
Props/Areas/Identity/Pages/Account/Register.cshtml
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
@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" />
|
||||||
|
}
|
115
Props/Areas/Identity/Pages/Account/Register.cshtml.cs
Normal file
115
Props/Areas/Identity/Pages/Account/Register.cshtml.cs
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
@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. *@
|
@ -0,0 +1,62 @@
|
|||||||
|
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
Props/Areas/Identity/Pages/Account/_ViewImports.cshtml
Normal file
1
Props/Areas/Identity/Pages/Account/_ViewImports.cshtml
Normal file
@ -0,0 +1 @@
|
|||||||
|
@using Props.Areas.Identity.Pages.Account
|
18
Props/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml
Normal file
18
Props/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<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>
|
5
Props/Areas/Identity/Pages/_ViewImports.cshtml
Normal file
5
Props/Areas/Identity/Pages/_ViewImports.cshtml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
@using Microsoft.AspNetCore.Identity
|
||||||
|
@using Props.Areas.Identity
|
||||||
|
@using Props.Areas.Identity.Pages
|
||||||
|
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
|
@using Props.Models.User
|
3
Props/Areas/Identity/Pages/_ViewStart.cshtml
Normal file
3
Props/Areas/Identity/Pages/_ViewStart.cshtml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@{
|
||||||
|
Layout = "/Pages/Shared/_Layout.cshtml";
|
||||||
|
}
|
11
Props/Controllers/ApiControllerBase.cs
Normal file
11
Props/Controllers/ApiControllerBase.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Props.Controllers
|
||||||
|
{
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[Controller]")]
|
||||||
|
public class ApiControllerBase : ControllerBase
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
38
Props/Controllers/SearchController.cs
Normal file
38
Props/Controllers/SearchController.cs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
211
Props/Controllers/SearchOutlineController.cs
Normal file
211
Props/Controllers/SearchOutlineController.cs
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
Props/Controllers/UserController.cs
Normal file
14
Props/Controllers/UserController.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Props.Controllers
|
||||||
|
{
|
||||||
|
public class UserController : ApiControllerBase
|
||||||
|
{
|
||||||
|
[HttpGet]
|
||||||
|
[Route("LoggedIn")]
|
||||||
|
public IActionResult GetLoggedIn()
|
||||||
|
{
|
||||||
|
return Ok(User.Identity.IsAuthenticated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
82
Props/Data/ApplicationDbContext.cs
Normal file
82
Props/Data/ApplicationDbContext.cs
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
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()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
499
Props/Data/Migrations/20210817042955_InitialCreate.Designer.cs
generated
Normal file
499
Props/Data/Migrations/20210817042955_InitialCreate.Designer.cs
generated
Normal file
@ -0,0 +1,499 @@
|
|||||||
|
// <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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
404
Props/Data/Migrations/20210817042955_InitialCreate.cs
Normal file
404
Props/Data/Migrations/20210817042955_InitialCreate.cs
Normal file
@ -0,0 +1,404 @@
|
|||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
497
Props/Data/Migrations/ApplicationDbContextModelSnapshot.cs
Normal file
497
Props/Data/Migrations/ApplicationDbContextModelSnapshot.cs
Normal file
@ -0,0 +1,497 @@
|
|||||||
|
// <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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
Props/Extensions/ProductListingExtensions.cs
Normal file
17
Props/Extensions/ProductListingExtensions.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
18
Props/Models/Search/ProductListingInfo.cs
Normal file
18
Props/Models/Search/ProductListingInfo.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
34
Props/Models/Search/QueryWordInfo.cs
Normal file
34
Props/Models/Search/QueryWordInfo.cs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
88
Props/Models/Search/SearchOutline.cs
Normal file
88
Props/Models/Search/SearchOutline.cs
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
19
Props/Models/User/ApplicationPreferences.cs
Normal file
19
Props/Models/User/ApplicationPreferences.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
38
Props/Models/User/ApplicationUser.cs
Normal file
38
Props/Models/User/ApplicationUser.cs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
43
Props/Models/User/ResultsPreferences.cs
Normal file
43
Props/Models/User/ResultsPreferences.cs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
Props/Models/User/SearchOutlinePreferences.cs
Normal file
34
Props/Models/User/SearchOutlinePreferences.cs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
Props/Options/MetricsOptions.cs
Normal file
9
Props/Options/MetricsOptions.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace Props.Options
|
||||||
|
{
|
||||||
|
public class MetricsOptions
|
||||||
|
{
|
||||||
|
public const string Metrics = "Metrics";
|
||||||
|
public int MaxQueryWords { get; set; }
|
||||||
|
public int MaxProductListings { get; set; }
|
||||||
|
}
|
||||||
|
}
|
11
Props/Options/ModulesOptions.cs
Normal file
11
Props/Options/ModulesOptions.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
9
Props/Options/SearchOptions.cs
Normal file
9
Props/Options/SearchOptions.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace Props.Options
|
||||||
|
{
|
||||||
|
public class SearchOptions
|
||||||
|
{
|
||||||
|
public const string Search = "Search";
|
||||||
|
public int MaxResults { get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
8
Props/Options/TextualOptions.cs
Normal file
8
Props/Options/TextualOptions.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace Props.Options
|
||||||
|
{
|
||||||
|
public class TextualOptions
|
||||||
|
{
|
||||||
|
public const string Textual = "Textual";
|
||||||
|
public string Dir { get; set; }
|
||||||
|
}
|
||||||
|
}
|
26
Props/Pages/Error.cshtml
Normal file
26
Props/Pages/Error.cshtml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
@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>
|
32
Props/Pages/Error.cshtml.cs
Normal file
32
Props/Pages/Error.cshtml.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
106
Props/Pages/Index.cshtml
Normal file
106
Props/Pages/Index.cshtml
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
@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>
|
25
Props/Pages/Index.cshtml.cs
Normal file
25
Props/Pages/Index.cshtml.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
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()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8
Props/Pages/Privacy.cshtml
Normal file
8
Props/Pages/Privacy.cshtml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
@page
|
||||||
|
@model PrivacyModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Privacy Policy";
|
||||||
|
}
|
||||||
|
<h1>@ViewData["Title"]</h1>
|
||||||
|
|
||||||
|
<p>Use this page to detail your site's privacy policy.</p>
|
24
Props/Pages/Privacy.cshtml.cs
Normal file
24
Props/Pages/Privacy.cshtml.cs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
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()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
302
Props/Pages/Search.cshtml
Normal file
302
Props/Pages/Search.cshtml
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
@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>
|
26
Props/Pages/Search.cshtml.cs
Normal file
26
Props/Pages/Search.cshtml.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
76
Props/Pages/Shared/_Layout.cshtml
Normal file
76
Props/Pages/Shared/_Layout.cshtml
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
@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>
|
27
Props/Pages/Shared/_LoginPartial.cshtml
Normal file
27
Props/Pages/Shared/_LoginPartial.cshtml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
@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>
|
2
Props/Pages/Shared/_ValidationScriptsPartial.cshtml
Normal file
2
Props/Pages/Shared/_ValidationScriptsPartial.cshtml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
|
||||||
|
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
|
6
Props/Pages/_ViewImports.cshtml
Normal file
6
Props/Pages/_ViewImports.cshtml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
@using Microsoft.AspNetCore.Identity
|
||||||
|
@using Props
|
||||||
|
@using Props.Data
|
||||||
|
@namespace Props.Pages
|
||||||
|
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
|
@addTagHelper *, Props
|
4
Props/Pages/_ViewStart.cshtml
Normal file
4
Props/Pages/_ViewStart.cshtml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
@{
|
||||||
|
Layout = "_Layout";
|
||||||
|
ViewData["Specific"] = null;
|
||||||
|
}
|
23
Props/Program.cs
Normal file
23
Props/Program.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
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>();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
28
Props/Properties/launchSettings.json
Normal file
28
Props/Properties/launchSettings.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
96
Props/Props.csproj
Normal file
96
Props/Props.csproj
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
<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>
|
3
Props/ScaffoldingReadMe.txt
Normal file
3
Props/ScaffoldingReadMe.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Support for ASP.NET Core Identity was added to your project.
|
||||||
|
|
||||||
|
For setup and configuration information, see https://go.microsoft.com/fwlink/?linkid=2116645.
|
18
Props/Services/Modules/IMetricsManager.cs
Normal file
18
Props/Services/Modules/IMetricsManager.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
13
Props/Services/Modules/ISearchManager.cs
Normal file
13
Props/Services/Modules/ISearchManager.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
16
Props/Services/Modules/IShopManager.cs
Normal file
16
Props/Services/Modules/IShopManager.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
113
Props/Services/Modules/LiveMetricsManager.cs
Normal file
113
Props/Services/Modules/LiveMetricsManager.cs
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
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…
Reference in New Issue
Block a user