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/vscode,aspnetcore
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=vscode,aspnetcore
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/aspnetcore,visualstudiocode,dotnetcore,python,database,node
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=aspnetcore,visualstudiocode,dotnetcore,python,database,node
|
||||
|
||||
### ASPNETCore ###
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
@ -30,7 +29,7 @@ bld/
|
||||
# Visual Studio 2015 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
wwwroot/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
@ -272,7 +271,287 @@ __pycache__/
|
||||
# Cake - Uncomment if you are using it
|
||||
# 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/settings.json
|
||||
!.vscode/tasks.json
|
||||
@ -280,4 +559,20 @@ __pycache__/
|
||||
!.vscode/extensions.json
|
||||
*.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