Initial commit
37
.editorconfig
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.py]
|
||||||
|
indent_size = 4
|
||||||
|
continuation_indent_size = 8
|
||||||
|
combine_as_imports = true
|
||||||
|
max_line_length = 88
|
||||||
|
multi_line_output = 4
|
||||||
|
quote_type = single
|
||||||
|
|
||||||
|
[*.js]
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.jsx]
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.scss]
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.ts]
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.tsx]
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.yml]
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.html]
|
||||||
|
indent_size = 4
|
||||||
132
.gitignore
vendored
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
.idea
|
||||||
|
./.idea
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
pip-wheel-metadata/
|
||||||
|
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.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
.pypirc
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
.pg_service.conf
|
||||||
|
.pgpass
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.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
|
||||||
|
.env
|
||||||
|
.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/
|
||||||
29
Pipfile
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
[[source]]
|
||||||
|
url = "https://pypi.org/simple"
|
||||||
|
verify_ssl = true
|
||||||
|
name = "pypi"
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
celery = {extras = ["redis"], version = "*"}
|
||||||
|
Django = "==4.0.2"
|
||||||
|
django-allauth = "*"
|
||||||
|
django-anymail = {extras = ["mailgun"], version = "*"}
|
||||||
|
django-celery-beat = "*"
|
||||||
|
django-celery-results = "*"
|
||||||
|
django-compressor = "*"
|
||||||
|
django-filter = "*"
|
||||||
|
django-measurement = "*"
|
||||||
|
django-setup-cli = "*"
|
||||||
|
django-storages = "*"
|
||||||
|
django-templated-email = "*"
|
||||||
|
paypal-checkout-serversdk = "*"
|
||||||
|
Pillow = "*"
|
||||||
|
redis = "*"
|
||||||
|
psycopg2 = "*"
|
||||||
|
|
||||||
|
[dev-packages]
|
||||||
|
django-debug-toolbar = "*"
|
||||||
|
selenium = "*"
|
||||||
|
|
||||||
|
[requires]
|
||||||
|
python_version = "3.10"
|
||||||
982
Pipfile.lock
generated
Normal file
@ -0,0 +1,982 @@
|
|||||||
|
{
|
||||||
|
"_meta": {
|
||||||
|
"hash": {
|
||||||
|
"sha256": "f7ca5d3f9e367e3324d6fd9af8c1022cff36b1a999eb7afa232ac1af8404c62c"
|
||||||
|
},
|
||||||
|
"pipfile-spec": 6,
|
||||||
|
"requires": {
|
||||||
|
"python_version": "3.10"
|
||||||
|
},
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"name": "pypi",
|
||||||
|
"url": "https://pypi.org/simple",
|
||||||
|
"verify_ssl": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"amqp": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:446b3e8a8ebc2ceafd424ffcaab1c353830d48161256578ed7a65448e601ebed",
|
||||||
|
"sha256:a575f4fa659a2290dc369b000cff5fea5c6be05fe3f2d5e511bcf56c7881c3ef"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==5.1.0"
|
||||||
|
},
|
||||||
|
"asgiref": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2f8abc20f7248433085eda803936d98992f1343ddb022065779f37c5da0181d0",
|
||||||
|
"sha256:88d59c13d634dcffe0510be048210188edd79aeccb6a6c9028cdad6f31d730a9"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==3.5.0"
|
||||||
|
},
|
||||||
|
"billiard": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:299de5a8da28a783d51b197d496bef4f1595dd023a93a4f59dde1886ae905547",
|
||||||
|
"sha256:87103ea78fa6ab4d5c751c4909bcff74617d985de7fa8b672cf8618afd5a875b"
|
||||||
|
],
|
||||||
|
"version": "==3.6.4.0"
|
||||||
|
},
|
||||||
|
"celery": {
|
||||||
|
"extras": [
|
||||||
|
"redis"
|
||||||
|
],
|
||||||
|
"hashes": [
|
||||||
|
"sha256:8aacd02fc23a02760686d63dde1eb0daa9f594e735e73ea8fb15c2ff15cb608c",
|
||||||
|
"sha256:e2cd41667ad97d4f6a2f4672d1c6a6ebada194c619253058b5f23704aaadaa82"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==5.2.3"
|
||||||
|
},
|
||||||
|
"certifi": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872",
|
||||||
|
"sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"
|
||||||
|
],
|
||||||
|
"version": "==2021.10.8"
|
||||||
|
},
|
||||||
|
"cffi": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3",
|
||||||
|
"sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2",
|
||||||
|
"sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636",
|
||||||
|
"sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20",
|
||||||
|
"sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728",
|
||||||
|
"sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27",
|
||||||
|
"sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66",
|
||||||
|
"sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443",
|
||||||
|
"sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0",
|
||||||
|
"sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7",
|
||||||
|
"sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39",
|
||||||
|
"sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605",
|
||||||
|
"sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a",
|
||||||
|
"sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37",
|
||||||
|
"sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029",
|
||||||
|
"sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139",
|
||||||
|
"sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc",
|
||||||
|
"sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df",
|
||||||
|
"sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14",
|
||||||
|
"sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880",
|
||||||
|
"sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2",
|
||||||
|
"sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a",
|
||||||
|
"sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e",
|
||||||
|
"sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474",
|
||||||
|
"sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024",
|
||||||
|
"sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8",
|
||||||
|
"sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0",
|
||||||
|
"sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e",
|
||||||
|
"sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a",
|
||||||
|
"sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e",
|
||||||
|
"sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032",
|
||||||
|
"sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6",
|
||||||
|
"sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e",
|
||||||
|
"sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b",
|
||||||
|
"sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e",
|
||||||
|
"sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954",
|
||||||
|
"sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962",
|
||||||
|
"sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c",
|
||||||
|
"sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4",
|
||||||
|
"sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55",
|
||||||
|
"sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962",
|
||||||
|
"sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023",
|
||||||
|
"sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c",
|
||||||
|
"sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6",
|
||||||
|
"sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8",
|
||||||
|
"sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382",
|
||||||
|
"sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7",
|
||||||
|
"sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc",
|
||||||
|
"sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997",
|
||||||
|
"sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"
|
||||||
|
],
|
||||||
|
"version": "==1.15.0"
|
||||||
|
},
|
||||||
|
"charset-normalizer": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597",
|
||||||
|
"sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3'",
|
||||||
|
"version": "==2.0.12"
|
||||||
|
},
|
||||||
|
"click": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1",
|
||||||
|
"sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==8.0.4"
|
||||||
|
},
|
||||||
|
"click-didyoumean": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:a0713dc7a1de3f06bc0df5a9567ad19ead2d3d5689b434768a6145bff77c0667",
|
||||||
|
"sha256:f184f0d851d96b6d29297354ed981b7dd71df7ff500d82fa6d11f0856bee8035"
|
||||||
|
],
|
||||||
|
"markers": "python_full_version >= '3.6.2' and python_full_version < '4.0.0'",
|
||||||
|
"version": "==0.3.0"
|
||||||
|
},
|
||||||
|
"click-plugins": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b",
|
||||||
|
"sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8"
|
||||||
|
],
|
||||||
|
"version": "==1.1.1"
|
||||||
|
},
|
||||||
|
"click-repl": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:94b3fbbc9406a236f176e0506524b2937e4b23b6f4c0c0b2a0a83f8a64e9194b",
|
||||||
|
"sha256:cd12f68d745bf6151210790540b4cb064c7b13e571bc64b6957d98d120dacfd8"
|
||||||
|
],
|
||||||
|
"version": "==0.2.0"
|
||||||
|
},
|
||||||
|
"cryptography": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0a817b961b46894c5ca8a66b599c745b9a3d9f822725221f0e0fe49dc043a3a3",
|
||||||
|
"sha256:2d87cdcb378d3cfed944dac30596da1968f88fb96d7fc34fdae30a99054b2e31",
|
||||||
|
"sha256:30ee1eb3ebe1644d1c3f183d115a8c04e4e603ed6ce8e394ed39eea4a98469ac",
|
||||||
|
"sha256:391432971a66cfaf94b21c24ab465a4cc3e8bf4a939c1ca5c3e3a6e0abebdbcf",
|
||||||
|
"sha256:39bdf8e70eee6b1c7b289ec6e5d84d49a6bfa11f8b8646b5b3dfe41219153316",
|
||||||
|
"sha256:4caa4b893d8fad33cf1964d3e51842cd78ba87401ab1d2e44556826df849a8ca",
|
||||||
|
"sha256:53e5c1dc3d7a953de055d77bef2ff607ceef7a2aac0353b5d630ab67f7423638",
|
||||||
|
"sha256:596f3cd67e1b950bc372c33f1a28a0692080625592ea6392987dba7f09f17a94",
|
||||||
|
"sha256:5d59a9d55027a8b88fd9fd2826c4392bd487d74bf628bb9d39beecc62a644c12",
|
||||||
|
"sha256:6c0c021f35b421ebf5976abf2daacc47e235f8b6082d3396a2fe3ccd537ab173",
|
||||||
|
"sha256:73bc2d3f2444bcfeac67dd130ff2ea598ea5f20b40e36d19821b4df8c9c5037b",
|
||||||
|
"sha256:74d6c7e80609c0f4c2434b97b80c7f8fdfaa072ca4baab7e239a15d6d70ed73a",
|
||||||
|
"sha256:7be0eec337359c155df191d6ae00a5e8bbb63933883f4f5dffc439dac5348c3f",
|
||||||
|
"sha256:94ae132f0e40fe48f310bba63f477f14a43116f05ddb69d6fa31e93f05848ae2",
|
||||||
|
"sha256:bb5829d027ff82aa872d76158919045a7c1e91fbf241aec32cb07956e9ebd3c9",
|
||||||
|
"sha256:ca238ceb7ba0bdf6ce88c1b74a87bffcee5afbfa1e41e173b1ceb095b39add46",
|
||||||
|
"sha256:ca28641954f767f9822c24e927ad894d45d5a1e501767599647259cbf030b903",
|
||||||
|
"sha256:e0344c14c9cb89e76eb6a060e67980c9e35b3f36691e15e1b7a9e58a0a6c6dc3",
|
||||||
|
"sha256:ebc15b1c22e55c4d5566e3ca4db8689470a0ca2babef8e3a9ee057a8b82ce4b1",
|
||||||
|
"sha256:ec63da4e7e4a5f924b90af42eddf20b698a70e58d86a72d943857c4c6045b3ee"
|
||||||
|
],
|
||||||
|
"version": "==36.0.1"
|
||||||
|
},
|
||||||
|
"defusedxml": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69",
|
||||||
|
"sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||||
|
"version": "==0.7.1"
|
||||||
|
},
|
||||||
|
"deprecated": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d",
|
||||||
|
"sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
|
"version": "==1.2.13"
|
||||||
|
},
|
||||||
|
"django": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:110fb58fb12eca59e072ad59fc42d771cd642dd7a2f2416582aa9da7a8ef954a",
|
||||||
|
"sha256:996495c58bff749232426c88726d8cd38d24c94d7c1d80835aafffa9bc52985a"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==4.0.2"
|
||||||
|
},
|
||||||
|
"django-allauth": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:f5fbb67376177c6a9276516dde98bcb01ac4160a5a27f7b340914dd521d04f12"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.49.0"
|
||||||
|
},
|
||||||
|
"django-anymail": {
|
||||||
|
"extras": [
|
||||||
|
"mailgun"
|
||||||
|
],
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2325932f56f914d96e0a54db850f2b246ed2277b753f75319620d051a51551e2",
|
||||||
|
"sha256:677e937dc9e2671ca7631abb1d94ddc6b840beb3d53c0fbf699e866a6a9ba92f"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==8.5"
|
||||||
|
},
|
||||||
|
"django-appconf": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:ae9f864ee1958c815a965ed63b3fba4874eec13de10236ba063a788f9a17389d",
|
||||||
|
"sha256:be3db0be6c81fa84742000b89a81c016d70ae66a7ccb620cdef592b1f1a6aaa4"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==1.0.5"
|
||||||
|
},
|
||||||
|
"django-celery-beat": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:4eb0e8412e2e05ba0029912a6f80d1054731001eecbcb4d59688c4e07cf4d9d3",
|
||||||
|
"sha256:8a169e11d96faed8b72d505ddbc70e7fe0b16cdc854df43cb209c153ed08d651"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.1.0"
|
||||||
|
},
|
||||||
|
"django-celery-results": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:203cf7321081d09be91738aff715c97bcc769db8c727621049e2786118059dac",
|
||||||
|
"sha256:37b8734ad0038cdeabe9efb09721dfdbe1ff7bf6e1d81ff3e10a1eb23a2b321f"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.3.0"
|
||||||
|
},
|
||||||
|
"django-compressor": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:89f7ba86777b30672c2f9c7557bf2aff87c5890903c73b1fa3ae38acd143e855",
|
||||||
|
"sha256:c4a87bf65f9a534cfaf1c321a000a229c24e50c6d62ba6ab089482db42e819d9"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==3.1"
|
||||||
|
},
|
||||||
|
"django-filter": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:632a251fa8f1aadb4b8cceff932bb52fe2f826dd7dfe7f3eac40e5c463d6836e",
|
||||||
|
"sha256:f4a6737a30104c98d2e2a5fb93043f36dd7978e0c7ddc92f5998e85433ea5063"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==21.1"
|
||||||
|
},
|
||||||
|
"django-measurement": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:b2d40b8b56b4e8277130a2a8cbc1f01f597589a636e0ea7dfbc4e4c05d458cef",
|
||||||
|
"sha256:db1279b04bacf3b48259312adaaefcfe55ca30b1e0933fa37d6c387c156834d5"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==3.2.4"
|
||||||
|
},
|
||||||
|
"django-render-block": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:a01bfdb839e2f6b3f88a99021597484392bbd15d084f9a796e3e5658bae800f4",
|
||||||
|
"sha256:fbdd8be56cefcfd794756a2e62117cc031f9c5de3ef4bb53e9a3f877a359a1a7"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==0.9.1"
|
||||||
|
},
|
||||||
|
"django-setup-cli": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:206f1036e49d4d64012ad2d306fa4015592335a7c6d006ed708d368a13abf5c6",
|
||||||
|
"sha256:5ea5517cd58d2c77a32fe14373cf977d4b5275a6423321ca08851bcade35601c"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.0.17"
|
||||||
|
},
|
||||||
|
"django-storages": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:204a99f218b747c46edbfeeb1310d357f83f90fa6a6024d8d0a3f422570cee84",
|
||||||
|
"sha256:a475edb2f0f04c4f7e548919a751ecd50117270833956ed5bd585c0575d2a5e7"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.12.3"
|
||||||
|
},
|
||||||
|
"django-templated-email": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:49d61840ec551e640adaf341146e94d6f9058ae01df964480850bf988046e5eb",
|
||||||
|
"sha256:bf1b68ffe6c8794c0c50e2ce20e3a166c6d511b3879abbd3cf059a3fc2fe2e60"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==3.0.0"
|
||||||
|
},
|
||||||
|
"django-timezone-field": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:5dd5bd9249382bef8847d3e7e4c32b7be182a4b538f354130d1252ed228892f8",
|
||||||
|
"sha256:7552d2b0f145684b7de3fb5046101c7efd600cc6ba951b15c630fa1e1b83558e"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.5'",
|
||||||
|
"version": "==4.2.3"
|
||||||
|
},
|
||||||
|
"idna": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
|
||||||
|
"sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3'",
|
||||||
|
"version": "==3.3"
|
||||||
|
},
|
||||||
|
"kombu": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:37cee3ee725f94ea8bb173eaab7c1760203ea53bbebae226328600f9d2799610",
|
||||||
|
"sha256:8b213b24293d3417bcf0d2f5537b7f756079e3ea232a8386dcc89a59fd2361a4"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==5.2.4"
|
||||||
|
},
|
||||||
|
"measurement": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:352b20f7f0e553236af7c5ed48d091a51cf26061c1a063f46b31706ff7c0d57a"
|
||||||
|
],
|
||||||
|
"version": "==3.2.0"
|
||||||
|
},
|
||||||
|
"mpmath": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:604bc21bd22d2322a177c73bdb573994ef76e62edd595d17e00aff24b0667e5c",
|
||||||
|
"sha256:79ffb45cf9f4b101a807595bcb3e72e0396202e0b1d25d689134b48c4216a81a"
|
||||||
|
],
|
||||||
|
"version": "==1.2.1"
|
||||||
|
},
|
||||||
|
"oauthlib": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:23a8208d75b902797ea29fd31fa80a15ed9dc2c6c16fe73f5d346f83f6fa27a2",
|
||||||
|
"sha256:6db33440354787f9b7f3a6dbd4febf5d0f93758354060e802f6c06cb493022fe"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==3.2.0"
|
||||||
|
},
|
||||||
|
"packaging": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
|
||||||
|
"sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==21.3"
|
||||||
|
},
|
||||||
|
"paypal-checkout-serversdk": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:80f62ba2d9fe22b58c2ce1f310146acf6037088493398dba8b1bb67b493aee5e",
|
||||||
|
"sha256:e82bf50c249d7383cb4f68d8562a68dde3e7089389f286c111b6689bd3fdad36"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.0.1"
|
||||||
|
},
|
||||||
|
"paypalhttp": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0a712ef72f061e80362cff83b094a7a64cdbbd3e4f11b12657eb1a82acf0f8c9",
|
||||||
|
"sha256:20e00f95ea052f59145b65bc2baf3b8720f449460c96bf7d32f191c8e293d16d",
|
||||||
|
"sha256:251a6e72e2c5140c5372ee6351b64f7af61b5aad9c554618db5782a06205989a"
|
||||||
|
],
|
||||||
|
"version": "==1.0.1"
|
||||||
|
},
|
||||||
|
"pillow": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:011233e0c42a4a7836498e98c1acf5e744c96a67dd5032a6f666cc1fb97eab97",
|
||||||
|
"sha256:0f29d831e2151e0b7b39981756d201f7108d3d215896212ffe2e992d06bfe049",
|
||||||
|
"sha256:12875d118f21cf35604176872447cdb57b07126750a33748bac15e77f90f1f9c",
|
||||||
|
"sha256:14d4b1341ac07ae07eb2cc682f459bec932a380c3b122f5540432d8977e64eae",
|
||||||
|
"sha256:1c3c33ac69cf059bbb9d1a71eeaba76781b450bc307e2291f8a4764d779a6b28",
|
||||||
|
"sha256:1d19397351f73a88904ad1aee421e800fe4bbcd1aeee6435fb62d0a05ccd1030",
|
||||||
|
"sha256:253e8a302a96df6927310a9d44e6103055e8fb96a6822f8b7f514bb7ef77de56",
|
||||||
|
"sha256:2632d0f846b7c7600edf53c48f8f9f1e13e62f66a6dbc15191029d950bfed976",
|
||||||
|
"sha256:335ace1a22325395c4ea88e00ba3dc89ca029bd66bd5a3c382d53e44f0ccd77e",
|
||||||
|
"sha256:413ce0bbf9fc6278b2d63309dfeefe452835e1c78398efb431bab0672fe9274e",
|
||||||
|
"sha256:5100b45a4638e3c00e4d2320d3193bdabb2d75e79793af7c3eb139e4f569f16f",
|
||||||
|
"sha256:514ceac913076feefbeaf89771fd6febde78b0c4c1b23aaeab082c41c694e81b",
|
||||||
|
"sha256:528a2a692c65dd5cafc130de286030af251d2ee0483a5bf50c9348aefe834e8a",
|
||||||
|
"sha256:6295f6763749b89c994fcb6d8a7f7ce03c3992e695f89f00b741b4580b199b7e",
|
||||||
|
"sha256:6c8bc8238a7dfdaf7a75f5ec5a663f4173f8c367e5a39f87e720495e1eed75fa",
|
||||||
|
"sha256:718856856ba31f14f13ba885ff13874be7fefc53984d2832458f12c38205f7f7",
|
||||||
|
"sha256:7f7609a718b177bf171ac93cea9fd2ddc0e03e84d8fa4e887bdfc39671d46b00",
|
||||||
|
"sha256:80ca33961ced9c63358056bd08403ff866512038883e74f3a4bf88ad3eb66838",
|
||||||
|
"sha256:80fe64a6deb6fcfdf7b8386f2cf216d329be6f2781f7d90304351811fb591360",
|
||||||
|
"sha256:81c4b81611e3a3cb30e59b0cf05b888c675f97e3adb2c8672c3154047980726b",
|
||||||
|
"sha256:855c583f268edde09474b081e3ddcd5cf3b20c12f26e0d434e1386cc5d318e7a",
|
||||||
|
"sha256:9bfdb82cdfeccec50aad441afc332faf8606dfa5e8efd18a6692b5d6e79f00fd",
|
||||||
|
"sha256:a5d24e1d674dd9d72c66ad3ea9131322819ff86250b30dc5821cbafcfa0b96b4",
|
||||||
|
"sha256:a9f44cd7e162ac6191491d7249cceb02b8116b0f7e847ee33f739d7cb1ea1f70",
|
||||||
|
"sha256:b5b3f092fe345c03bca1e0b687dfbb39364b21ebb8ba90e3fa707374b7915204",
|
||||||
|
"sha256:b9618823bd237c0d2575283f2939655f54d51b4527ec3972907a927acbcc5bfc",
|
||||||
|
"sha256:cef9c85ccbe9bee00909758936ea841ef12035296c748aaceee535969e27d31b",
|
||||||
|
"sha256:d21237d0cd37acded35154e29aec853e945950321dd2ffd1a7d86fe686814669",
|
||||||
|
"sha256:d3c5c79ab7dfce6d88f1ba639b77e77a17ea33a01b07b99840d6ed08031cb2a7",
|
||||||
|
"sha256:d9d7942b624b04b895cb95af03a23407f17646815495ce4547f0e60e0b06f58e",
|
||||||
|
"sha256:db6d9fac65bd08cea7f3540b899977c6dee9edad959fa4eaf305940d9cbd861c",
|
||||||
|
"sha256:ede5af4a2702444a832a800b8eb7f0a7a1c0eed55b644642e049c98d589e5092",
|
||||||
|
"sha256:effb7749713d5317478bb3acb3f81d9d7c7f86726d41c1facca068a04cf5bb4c",
|
||||||
|
"sha256:f154d173286a5d1863637a7dcd8c3437bb557520b01bddb0be0258dcb72696b5",
|
||||||
|
"sha256:f25ed6e28ddf50de7e7ea99d7a976d6a9c415f03adcaac9c41ff6ff41b6d86ac"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==9.0.1"
|
||||||
|
},
|
||||||
|
"prompt-toolkit": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:30129d870dcb0b3b6a53efdc9d0a83ea96162ffd28ffe077e94215b233dc670c",
|
||||||
|
"sha256:9f1cd16b1e86c2968f2519d7fb31dd9d669916f515612c269d14e9ed52b51650"
|
||||||
|
],
|
||||||
|
"markers": "python_full_version >= '3.6.2'",
|
||||||
|
"version": "==3.0.28"
|
||||||
|
},
|
||||||
|
"psycopg2": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:06f32425949bd5fe8f625c49f17ebb9784e1e4fe928b7cce72edc36fb68e4c0c",
|
||||||
|
"sha256:0762c27d018edbcb2d34d51596e4346c983bd27c330218c56c4dc25ef7e819bf",
|
||||||
|
"sha256:083707a696e5e1c330af2508d8fab36f9700b26621ccbcb538abe22e15485362",
|
||||||
|
"sha256:34b33e0162cfcaad151f249c2649fd1030010c16f4bbc40a604c1cb77173dcf7",
|
||||||
|
"sha256:4295093a6ae3434d33ec6baab4ca5512a5082cc43c0505293087b8a46d108461",
|
||||||
|
"sha256:8cf3878353cc04b053822896bc4922b194792df9df2f1ad8da01fb3043602126",
|
||||||
|
"sha256:8e841d1bf3434da985cc5ef13e6f75c8981ced601fd70cc6bf33351b91562981",
|
||||||
|
"sha256:9572e08b50aed176ef6d66f15a21d823bb6f6d23152d35e8451d7d2d18fdac56",
|
||||||
|
"sha256:a81e3866f99382dfe8c15a151f1ca5fde5815fde879348fe5a9884a7c092a305",
|
||||||
|
"sha256:cb10d44e6694d763fa1078a26f7f6137d69f555a78ec85dc2ef716c37447e4b2",
|
||||||
|
"sha256:d3ca6421b942f60c008f81a3541e8faf6865a28d5a9b48544b0ee4f40cac7fca"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.9.3"
|
||||||
|
},
|
||||||
|
"pycparser": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9",
|
||||||
|
"sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"
|
||||||
|
],
|
||||||
|
"version": "==2.21"
|
||||||
|
},
|
||||||
|
"pyjwt": {
|
||||||
|
"extras": [
|
||||||
|
"crypto"
|
||||||
|
],
|
||||||
|
"hashes": [
|
||||||
|
"sha256:b888b4d56f06f6dcd777210c334e69c737be74755d3e5e9ee3fe67dc18a0ee41",
|
||||||
|
"sha256:e0c4bb8d9f0af0c7f5b1ec4c5036309617d03d56932877f2f7a0beeb5318322f"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==2.3.0"
|
||||||
|
},
|
||||||
|
"pyopenssl": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:660b1b1425aac4a1bea1d94168a85d99f0b3144c869dd4390d27629d0087f1bf",
|
||||||
|
"sha256:ea252b38c87425b64116f808355e8da644ef9b07e429398bfece610f893ee2e0"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==22.0.0"
|
||||||
|
},
|
||||||
|
"pyparsing": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea",
|
||||||
|
"sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==3.0.7"
|
||||||
|
},
|
||||||
|
"python-crontab": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1e35ed7a3cdc3100545b43e196d34754e6551e7f95e4caebbe0e1c0ca41c2f1b"
|
||||||
|
],
|
||||||
|
"version": "==2.6.0"
|
||||||
|
},
|
||||||
|
"python-dateutil": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
|
||||||
|
"sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
|
"version": "==2.8.2"
|
||||||
|
},
|
||||||
|
"python-dotenv": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:32b2bdc1873fd3a3c346da1c6db83d0053c3c62f28f1f38516070c4c8971b1d3",
|
||||||
|
"sha256:a5de49a31e953b45ff2d2fd434bbc2670e8db5273606c1e737cc6b93eff3655f"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.5'",
|
||||||
|
"version": "==0.19.2"
|
||||||
|
},
|
||||||
|
"python3-openid": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:33fbf6928f401e0b790151ed2b5290b02545e8775f982485205a066f874aaeaf",
|
||||||
|
"sha256:6626f771e0417486701e0b4daff762e7212e820ca5b29fcc0d05f6f8736dfa6b"
|
||||||
|
],
|
||||||
|
"version": "==3.2.0"
|
||||||
|
},
|
||||||
|
"pytz": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c",
|
||||||
|
"sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"
|
||||||
|
],
|
||||||
|
"version": "==2021.3"
|
||||||
|
},
|
||||||
|
"pyyaml": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293",
|
||||||
|
"sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b",
|
||||||
|
"sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57",
|
||||||
|
"sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b",
|
||||||
|
"sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4",
|
||||||
|
"sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07",
|
||||||
|
"sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba",
|
||||||
|
"sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9",
|
||||||
|
"sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287",
|
||||||
|
"sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513",
|
||||||
|
"sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0",
|
||||||
|
"sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0",
|
||||||
|
"sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92",
|
||||||
|
"sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f",
|
||||||
|
"sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2",
|
||||||
|
"sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc",
|
||||||
|
"sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c",
|
||||||
|
"sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86",
|
||||||
|
"sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4",
|
||||||
|
"sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c",
|
||||||
|
"sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34",
|
||||||
|
"sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b",
|
||||||
|
"sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c",
|
||||||
|
"sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb",
|
||||||
|
"sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737",
|
||||||
|
"sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3",
|
||||||
|
"sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d",
|
||||||
|
"sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53",
|
||||||
|
"sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78",
|
||||||
|
"sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803",
|
||||||
|
"sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a",
|
||||||
|
"sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174",
|
||||||
|
"sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==6.0"
|
||||||
|
},
|
||||||
|
"rcssmin": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0a6aae7e119509445bf7aa6da6ca0f285cc198273c20f470ad999ff83bbadcf9",
|
||||||
|
"sha256:1512223b6a687bb747e4e531187bd49a56ed71287e7ead9529cbaa1ca4718a0a",
|
||||||
|
"sha256:1d7c2719d014e4e4df4e33b75ae8067c7e246cf470eaec8585e06e2efac7586c",
|
||||||
|
"sha256:2211a5c91ea14a5937b57904c9121f8bfef20987825e55368143da7d25446e3b",
|
||||||
|
"sha256:27fc400627fd3d328b7fe95af2a01f5d0af6b5af39731af5d071826a1f08e362",
|
||||||
|
"sha256:30f5522285065cae0164d20068377d84b5d10b414156115f8729b034d0ea5e8b",
|
||||||
|
"sha256:32ccaebbbd4d56eab08cf26aed36f5d33389b9d1d3ca1fecf53eb6ab77760ddf",
|
||||||
|
"sha256:352dd3a78eb914bb1cb269ac2b66b3154f2490a52ab605558c681de3fb5194d2",
|
||||||
|
"sha256:37f1242e34ca273ed2c26cf778854e18dd11b31c6bfca60e23fce146c84667c1",
|
||||||
|
"sha256:49807735f26f59404194f1e6f93254b6d5b6f7748c2a954f4470a86a40ff4c13",
|
||||||
|
"sha256:506e33ab4c47051f7deae35b6d8dbb4a5c025f016e90a830929a1ecc7daa1682",
|
||||||
|
"sha256:6158d0d86cd611c5304d738dc3d6cfeb23864dd78ad0d83a633f443696ac5d77",
|
||||||
|
"sha256:7085d1b51dd2556f3aae03947380f6e9e1da29fb1eeadfa6766b7f105c54c9ff",
|
||||||
|
"sha256:7c44002b79f3656348196005b9522ec5e04f182b466f66d72b16be0bd03c13d8",
|
||||||
|
"sha256:7da63fee37edf204bbd86785edb4d7491642adbfd1d36fd230b7ccbbd8db1a6f",
|
||||||
|
"sha256:8b659a88850e772c84cfac4520ec223de6807875e173d8ef3248ab7f90876066",
|
||||||
|
"sha256:c28b9eb20982b45ebe6adef8bd2547e5ed314dafddfff4eba806b0f8c166cfd1",
|
||||||
|
"sha256:ddff3a41611664c7f1d9e3d8a9c1669e0e155ac0458e586ffa834dc5953e7d9f",
|
||||||
|
"sha256:f1a37bbd36b050813673e62ae6464467548628690bf4d48a938170e121e8616e",
|
||||||
|
"sha256:f31c82d06ba2dbf33c20db9550157e80bb0c4cbd24575c098f0831d1d2e3c5df"
|
||||||
|
],
|
||||||
|
"version": "==1.1.0"
|
||||||
|
},
|
||||||
|
"redis": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:04629f8e42be942c4f7d1812f2094568f04c612865ad19ad3ace3005da70631a",
|
||||||
|
"sha256:1d9a0cdf89fdd93f84261733e24f55a7bbd413a9b219fdaf56e3e728ca9a2306"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==4.1.4"
|
||||||
|
},
|
||||||
|
"requests": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61",
|
||||||
|
"sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
|
||||||
|
"version": "==2.27.1"
|
||||||
|
},
|
||||||
|
"requests-oauthlib": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5",
|
||||||
|
"sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
|
"version": "==1.3.1"
|
||||||
|
},
|
||||||
|
"rjsmin": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:05efa485dfddb6418e3b86d8862463aa15641a61f6ae05e7e6de8f116ee77c69",
|
||||||
|
"sha256:1622fbb6c6a8daaf77da13cc83356539bfe79c1440f9664b02c7f7b150b9a18e",
|
||||||
|
"sha256:1c93b29fd725e61718299ffe57de93ff32d71b313eaabbfcc7bd32ddb82831d5",
|
||||||
|
"sha256:2ed83aca637186bafdc894b4b7fc3657e2d74014ccca7d3d69122c1e82675216",
|
||||||
|
"sha256:38a4474ed52e1575fb9da983ec8657faecd8ab3738508d36e04f87769411fd3d",
|
||||||
|
"sha256:3b14f4c2933ec194eb816b71a0854ce461b6419a3d852bf360344731ab28c0a6",
|
||||||
|
"sha256:40e7211a25d9a11ac9ff50446e41268c978555676828af86fa1866615823bfff",
|
||||||
|
"sha256:41c7c3910f7b8816e37366b293e576ddecf696c5f2197d53cf2c1526ac336646",
|
||||||
|
"sha256:4387a00777faddf853eebdece9f2e56ebaf243c3f24676a9de6a20c5d4f3d731",
|
||||||
|
"sha256:54fc30519365841b27556ccc1cb94c5b4413c384ff6d467442fddba66e2e325a",
|
||||||
|
"sha256:6c395ffc130332cca744f081ed5efd5699038dcb7a5d30c3ff4bc6adb5b30a62",
|
||||||
|
"sha256:6c529feb6c400984452494c52dd9fdf59185afeacca2afc5174a28ab37751a1b",
|
||||||
|
"sha256:86c4da7285ddafe6888cb262da563570f28e4a31146b5164a7a6947b1222196b",
|
||||||
|
"sha256:8944a8a55ac825b8e5ec29f341ecb7574697691ef416506885898d2f780fb4ca",
|
||||||
|
"sha256:993935654c1311280e69665367d7e6ff694ac9e1609168cf51cae8c0307df0db",
|
||||||
|
"sha256:99e5597a812b60058baa1457387dc79cca7d273b2a700dc98bfd20d43d60711d",
|
||||||
|
"sha256:b6a7c8c8d19e154334f640954e43e57283e87bb4a2f6e23295db14eea8e9fc1d",
|
||||||
|
"sha256:c81229ffe5b0a0d5b3b5d5e6d0431f182572de9e9a077e85dbae5757db0ab75c",
|
||||||
|
"sha256:d63e193a2f932a786ae82068aa76d1d126fcdff8582094caff9e5e66c4dcc124",
|
||||||
|
"sha256:e18fe1a610fb105273bb369f61c2b0bd9e66a3f0792e27e4cac44e42ace1968b"
|
||||||
|
],
|
||||||
|
"version": "==1.2.0"
|
||||||
|
},
|
||||||
|
"setuptools": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:22c7348c6d2976a52632c67f7ab0cdf40147db7789f9aed18734643fe9cf3373",
|
||||||
|
"sha256:4ce92f1e1f8f01233ee9952c04f6b81d1e02939d6e1b488428154974a4d0783e"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==59.6.0"
|
||||||
|
},
|
||||||
|
"six": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
|
||||||
|
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
|
"version": "==1.16.0"
|
||||||
|
},
|
||||||
|
"sqlparse": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae",
|
||||||
|
"sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.5'",
|
||||||
|
"version": "==0.4.2"
|
||||||
|
},
|
||||||
|
"sympy": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2009368e862cd29f1b568dc6572786371a2faa1cd8eb4d313e11a90195d6ee36",
|
||||||
|
"sha256:6cf85a5cfe8fff69553e745b05128de6fc8de8f291965c63871c79701dc6efc9"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==1.10"
|
||||||
|
},
|
||||||
|
"urllib3": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed",
|
||||||
|
"sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_full_version < '4.0.0'",
|
||||||
|
"version": "==1.26.8"
|
||||||
|
},
|
||||||
|
"vine": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30",
|
||||||
|
"sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==5.0.0"
|
||||||
|
},
|
||||||
|
"wcwidth": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784",
|
||||||
|
"sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"
|
||||||
|
],
|
||||||
|
"version": "==0.2.5"
|
||||||
|
},
|
||||||
|
"wrapt": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:00108411e0f34c52ce16f81f1d308a571df7784932cc7491d1e94be2ee93374b",
|
||||||
|
"sha256:01f799def9b96a8ec1ef6b9c1bbaf2bbc859b87545efbecc4a78faea13d0e3a0",
|
||||||
|
"sha256:09d16ae7a13cff43660155383a2372b4aa09109c7127aa3f24c3cf99b891c330",
|
||||||
|
"sha256:14e7e2c5f5fca67e9a6d5f753d21f138398cad2b1159913ec9e9a67745f09ba3",
|
||||||
|
"sha256:167e4793dc987f77fd476862d32fa404d42b71f6a85d3b38cbce711dba5e6b68",
|
||||||
|
"sha256:1807054aa7b61ad8d8103b3b30c9764de2e9d0c0978e9d3fc337e4e74bf25faa",
|
||||||
|
"sha256:1f83e9c21cd5275991076b2ba1cd35418af3504667affb4745b48937e214bafe",
|
||||||
|
"sha256:21b1106bff6ece8cb203ef45b4f5778d7226c941c83aaaa1e1f0f4f32cc148cd",
|
||||||
|
"sha256:22626dca56fd7f55a0733e604f1027277eb0f4f3d95ff28f15d27ac25a45f71b",
|
||||||
|
"sha256:23f96134a3aa24cc50614920cc087e22f87439053d886e474638c68c8d15dc80",
|
||||||
|
"sha256:2498762814dd7dd2a1d0248eda2afbc3dd9c11537bc8200a4b21789b6df6cd38",
|
||||||
|
"sha256:28c659878f684365d53cf59dc9a1929ea2eecd7ac65da762be8b1ba193f7e84f",
|
||||||
|
"sha256:2eca15d6b947cfff51ed76b2d60fd172c6ecd418ddab1c5126032d27f74bc350",
|
||||||
|
"sha256:354d9fc6b1e44750e2a67b4b108841f5f5ea08853453ecbf44c81fdc2e0d50bd",
|
||||||
|
"sha256:36a76a7527df8583112b24adc01748cd51a2d14e905b337a6fefa8b96fc708fb",
|
||||||
|
"sha256:3a0a4ca02752ced5f37498827e49c414d694ad7cf451ee850e3ff160f2bee9d3",
|
||||||
|
"sha256:3a71dbd792cc7a3d772ef8cd08d3048593f13d6f40a11f3427c000cf0a5b36a0",
|
||||||
|
"sha256:3a88254881e8a8c4784ecc9cb2249ff757fd94b911d5df9a5984961b96113fff",
|
||||||
|
"sha256:47045ed35481e857918ae78b54891fac0c1d197f22c95778e66302668309336c",
|
||||||
|
"sha256:4775a574e9d84e0212f5b18886cace049a42e13e12009bb0491562a48bb2b758",
|
||||||
|
"sha256:493da1f8b1bb8a623c16552fb4a1e164c0200447eb83d3f68b44315ead3f9036",
|
||||||
|
"sha256:4b847029e2d5e11fd536c9ac3136ddc3f54bc9488a75ef7d040a3900406a91eb",
|
||||||
|
"sha256:59d7d92cee84a547d91267f0fea381c363121d70fe90b12cd88241bd9b0e1763",
|
||||||
|
"sha256:5a0898a640559dec00f3614ffb11d97a2666ee9a2a6bad1259c9facd01a1d4d9",
|
||||||
|
"sha256:5a9a1889cc01ed2ed5f34574c90745fab1dd06ec2eee663e8ebeefe363e8efd7",
|
||||||
|
"sha256:5b835b86bd5a1bdbe257d610eecab07bf685b1af2a7563093e0e69180c1d4af1",
|
||||||
|
"sha256:5f24ca7953f2643d59a9c87d6e272d8adddd4a53bb62b9208f36db408d7aafc7",
|
||||||
|
"sha256:61e1a064906ccba038aa3c4a5a82f6199749efbbb3cef0804ae5c37f550eded0",
|
||||||
|
"sha256:65bf3eb34721bf18b5a021a1ad7aa05947a1767d1aa272b725728014475ea7d5",
|
||||||
|
"sha256:6807bcee549a8cb2f38f73f469703a1d8d5d990815c3004f21ddb68a567385ce",
|
||||||
|
"sha256:68aeefac31c1f73949662ba8affaf9950b9938b712fb9d428fa2a07e40ee57f8",
|
||||||
|
"sha256:6915682f9a9bc4cf2908e83caf5895a685da1fbd20b6d485dafb8e218a338279",
|
||||||
|
"sha256:6d9810d4f697d58fd66039ab959e6d37e63ab377008ef1d63904df25956c7db0",
|
||||||
|
"sha256:729d5e96566f44fccac6c4447ec2332636b4fe273f03da128fff8d5559782b06",
|
||||||
|
"sha256:748df39ed634851350efa87690c2237a678ed794fe9ede3f0d79f071ee042561",
|
||||||
|
"sha256:763a73ab377390e2af26042f685a26787c402390f682443727b847e9496e4a2a",
|
||||||
|
"sha256:8323a43bd9c91f62bb7d4be74cc9ff10090e7ef820e27bfe8815c57e68261311",
|
||||||
|
"sha256:8529b07b49b2d89d6917cfa157d3ea1dfb4d319d51e23030664a827fe5fd2131",
|
||||||
|
"sha256:87fa943e8bbe40c8c1ba4086971a6fefbf75e9991217c55ed1bcb2f1985bd3d4",
|
||||||
|
"sha256:88236b90dda77f0394f878324cfbae05ae6fde8a84d548cfe73a75278d760291",
|
||||||
|
"sha256:891c353e95bb11abb548ca95c8b98050f3620a7378332eb90d6acdef35b401d4",
|
||||||
|
"sha256:89ba3d548ee1e6291a20f3c7380c92f71e358ce8b9e48161401e087e0bc740f8",
|
||||||
|
"sha256:8c6be72eac3c14baa473620e04f74186c5d8f45d80f8f2b4eda6e1d18af808e8",
|
||||||
|
"sha256:9a242871b3d8eecc56d350e5e03ea1854de47b17f040446da0e47dc3e0b9ad4d",
|
||||||
|
"sha256:9a3ff5fb015f6feb78340143584d9f8a0b91b6293d6b5cf4295b3e95d179b88c",
|
||||||
|
"sha256:9a5a544861b21e0e7575b6023adebe7a8c6321127bb1d238eb40d99803a0e8bd",
|
||||||
|
"sha256:9d57677238a0c5411c76097b8b93bdebb02eb845814c90f0b01727527a179e4d",
|
||||||
|
"sha256:9d8c68c4145041b4eeae96239802cfdfd9ef927754a5be3f50505f09f309d8c6",
|
||||||
|
"sha256:9d9fcd06c952efa4b6b95f3d788a819b7f33d11bea377be6b8980c95e7d10775",
|
||||||
|
"sha256:a0057b5435a65b933cbf5d859cd4956624df37b8bf0917c71756e4b3d9958b9e",
|
||||||
|
"sha256:a65bffd24409454b889af33b6c49d0d9bcd1a219b972fba975ac935f17bdf627",
|
||||||
|
"sha256:b0ed6ad6c9640671689c2dbe6244680fe8b897c08fd1fab2228429b66c518e5e",
|
||||||
|
"sha256:b21650fa6907e523869e0396c5bd591cc326e5c1dd594dcdccac089561cacfb8",
|
||||||
|
"sha256:b3f7e671fb19734c872566e57ce7fc235fa953d7c181bb4ef138e17d607dc8a1",
|
||||||
|
"sha256:b77159d9862374da213f741af0c361720200ab7ad21b9f12556e0eb95912cd48",
|
||||||
|
"sha256:bb36fbb48b22985d13a6b496ea5fb9bb2a076fea943831643836c9f6febbcfdc",
|
||||||
|
"sha256:d066ffc5ed0be00cd0352c95800a519cf9e4b5dd34a028d301bdc7177c72daf3",
|
||||||
|
"sha256:d332eecf307fca852d02b63f35a7872de32d5ba8b4ec32da82f45df986b39ff6",
|
||||||
|
"sha256:d808a5a5411982a09fef6b49aac62986274ab050e9d3e9817ad65b2791ed1425",
|
||||||
|
"sha256:d9bdfa74d369256e4218000a629978590fd7cb6cf6893251dad13d051090436d",
|
||||||
|
"sha256:db6a0ddc1282ceb9032e41853e659c9b638789be38e5b8ad7498caac00231c23",
|
||||||
|
"sha256:debaf04f813ada978d7d16c7dfa16f3c9c2ec9adf4656efdc4defdf841fc2f0c",
|
||||||
|
"sha256:f0408e2dbad9e82b4c960274214af533f856a199c9274bd4aff55d4634dedc33",
|
||||||
|
"sha256:f2f3bc7cd9c9fcd39143f11342eb5963317bd54ecc98e3650ca22704b69d9653"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||||
|
"version": "==1.14.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"develop": {
|
||||||
|
"asgiref": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2f8abc20f7248433085eda803936d98992f1343ddb022065779f37c5da0181d0",
|
||||||
|
"sha256:88d59c13d634dcffe0510be048210188edd79aeccb6a6c9028cdad6f31d730a9"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==3.5.0"
|
||||||
|
},
|
||||||
|
"async-generator": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b",
|
||||||
|
"sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.5'",
|
||||||
|
"version": "==1.10"
|
||||||
|
},
|
||||||
|
"attrs": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4",
|
||||||
|
"sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||||
|
"version": "==21.4.0"
|
||||||
|
},
|
||||||
|
"certifi": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872",
|
||||||
|
"sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"
|
||||||
|
],
|
||||||
|
"version": "==2021.10.8"
|
||||||
|
},
|
||||||
|
"cffi": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3",
|
||||||
|
"sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2",
|
||||||
|
"sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636",
|
||||||
|
"sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20",
|
||||||
|
"sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728",
|
||||||
|
"sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27",
|
||||||
|
"sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66",
|
||||||
|
"sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443",
|
||||||
|
"sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0",
|
||||||
|
"sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7",
|
||||||
|
"sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39",
|
||||||
|
"sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605",
|
||||||
|
"sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a",
|
||||||
|
"sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37",
|
||||||
|
"sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029",
|
||||||
|
"sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139",
|
||||||
|
"sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc",
|
||||||
|
"sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df",
|
||||||
|
"sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14",
|
||||||
|
"sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880",
|
||||||
|
"sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2",
|
||||||
|
"sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a",
|
||||||
|
"sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e",
|
||||||
|
"sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474",
|
||||||
|
"sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024",
|
||||||
|
"sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8",
|
||||||
|
"sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0",
|
||||||
|
"sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e",
|
||||||
|
"sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a",
|
||||||
|
"sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e",
|
||||||
|
"sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032",
|
||||||
|
"sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6",
|
||||||
|
"sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e",
|
||||||
|
"sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b",
|
||||||
|
"sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e",
|
||||||
|
"sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954",
|
||||||
|
"sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962",
|
||||||
|
"sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c",
|
||||||
|
"sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4",
|
||||||
|
"sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55",
|
||||||
|
"sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962",
|
||||||
|
"sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023",
|
||||||
|
"sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c",
|
||||||
|
"sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6",
|
||||||
|
"sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8",
|
||||||
|
"sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382",
|
||||||
|
"sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7",
|
||||||
|
"sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc",
|
||||||
|
"sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997",
|
||||||
|
"sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"
|
||||||
|
],
|
||||||
|
"version": "==1.15.0"
|
||||||
|
},
|
||||||
|
"cryptography": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0a817b961b46894c5ca8a66b599c745b9a3d9f822725221f0e0fe49dc043a3a3",
|
||||||
|
"sha256:2d87cdcb378d3cfed944dac30596da1968f88fb96d7fc34fdae30a99054b2e31",
|
||||||
|
"sha256:30ee1eb3ebe1644d1c3f183d115a8c04e4e603ed6ce8e394ed39eea4a98469ac",
|
||||||
|
"sha256:391432971a66cfaf94b21c24ab465a4cc3e8bf4a939c1ca5c3e3a6e0abebdbcf",
|
||||||
|
"sha256:39bdf8e70eee6b1c7b289ec6e5d84d49a6bfa11f8b8646b5b3dfe41219153316",
|
||||||
|
"sha256:4caa4b893d8fad33cf1964d3e51842cd78ba87401ab1d2e44556826df849a8ca",
|
||||||
|
"sha256:53e5c1dc3d7a953de055d77bef2ff607ceef7a2aac0353b5d630ab67f7423638",
|
||||||
|
"sha256:596f3cd67e1b950bc372c33f1a28a0692080625592ea6392987dba7f09f17a94",
|
||||||
|
"sha256:5d59a9d55027a8b88fd9fd2826c4392bd487d74bf628bb9d39beecc62a644c12",
|
||||||
|
"sha256:6c0c021f35b421ebf5976abf2daacc47e235f8b6082d3396a2fe3ccd537ab173",
|
||||||
|
"sha256:73bc2d3f2444bcfeac67dd130ff2ea598ea5f20b40e36d19821b4df8c9c5037b",
|
||||||
|
"sha256:74d6c7e80609c0f4c2434b97b80c7f8fdfaa072ca4baab7e239a15d6d70ed73a",
|
||||||
|
"sha256:7be0eec337359c155df191d6ae00a5e8bbb63933883f4f5dffc439dac5348c3f",
|
||||||
|
"sha256:94ae132f0e40fe48f310bba63f477f14a43116f05ddb69d6fa31e93f05848ae2",
|
||||||
|
"sha256:bb5829d027ff82aa872d76158919045a7c1e91fbf241aec32cb07956e9ebd3c9",
|
||||||
|
"sha256:ca238ceb7ba0bdf6ce88c1b74a87bffcee5afbfa1e41e173b1ceb095b39add46",
|
||||||
|
"sha256:ca28641954f767f9822c24e927ad894d45d5a1e501767599647259cbf030b903",
|
||||||
|
"sha256:e0344c14c9cb89e76eb6a060e67980c9e35b3f36691e15e1b7a9e58a0a6c6dc3",
|
||||||
|
"sha256:ebc15b1c22e55c4d5566e3ca4db8689470a0ca2babef8e3a9ee057a8b82ce4b1",
|
||||||
|
"sha256:ec63da4e7e4a5f924b90af42eddf20b698a70e58d86a72d943857c4c6045b3ee"
|
||||||
|
],
|
||||||
|
"version": "==36.0.1"
|
||||||
|
},
|
||||||
|
"django": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:110fb58fb12eca59e072ad59fc42d771cd642dd7a2f2416582aa9da7a8ef954a",
|
||||||
|
"sha256:996495c58bff749232426c88726d8cd38d24c94d7c1d80835aafffa9bc52985a"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==4.0.2"
|
||||||
|
},
|
||||||
|
"django-debug-toolbar": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:644bbd5c428d3283aa9115722471769cac1bec189edf3a0c855fd8ff870375a9",
|
||||||
|
"sha256:6b633b6cfee24f232d73569870f19aa86c819d750e7f3e833f2344a9eb4b4409"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==3.2.4"
|
||||||
|
},
|
||||||
|
"h11": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:70813c1135087a248a4d38cc0e1a0181ffab2188141a93eaf567940c3957ff06",
|
||||||
|
"sha256:8ddd78563b633ca55346c8cd41ec0af27d3c79931828beffb46ce70a379e7442"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==0.13.0"
|
||||||
|
},
|
||||||
|
"idna": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
|
||||||
|
"sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3'",
|
||||||
|
"version": "==3.3"
|
||||||
|
},
|
||||||
|
"outcome": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:c7dd9375cfd3c12db9801d080a3b63d4b0a261aa996c4c13152380587288d958",
|
||||||
|
"sha256:e862f01d4e626e63e8f92c38d1f8d5546d3f9cce989263c521b2e7990d186967"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==1.1.0"
|
||||||
|
},
|
||||||
|
"pycparser": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9",
|
||||||
|
"sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"
|
||||||
|
],
|
||||||
|
"version": "==2.21"
|
||||||
|
},
|
||||||
|
"pyopenssl": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:660b1b1425aac4a1bea1d94168a85d99f0b3144c869dd4390d27629d0087f1bf",
|
||||||
|
"sha256:ea252b38c87425b64116f808355e8da644ef9b07e429398bfece610f893ee2e0"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==22.0.0"
|
||||||
|
},
|
||||||
|
"pysocks": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299",
|
||||||
|
"sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5",
|
||||||
|
"sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"
|
||||||
|
],
|
||||||
|
"version": "==1.7.1"
|
||||||
|
},
|
||||||
|
"selenium": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:14d28a628c831c105d38305c881c9c7847199bfd728ec84240c5e86fa1c9bd5a"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==4.1.3"
|
||||||
|
},
|
||||||
|
"sniffio": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663",
|
||||||
|
"sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.5'",
|
||||||
|
"version": "==1.2.0"
|
||||||
|
},
|
||||||
|
"sortedcontainers": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88",
|
||||||
|
"sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"
|
||||||
|
],
|
||||||
|
"version": "==2.4.0"
|
||||||
|
},
|
||||||
|
"sqlparse": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae",
|
||||||
|
"sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.5'",
|
||||||
|
"version": "==0.4.2"
|
||||||
|
},
|
||||||
|
"trio": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:670a52d3115d0e879e1ac838a4eb999af32f858163e3a704fe4839de2a676070",
|
||||||
|
"sha256:fb2d48e4eab0dfb786a472cd514aaadc71e3445b203bc300bad93daa75d77c1a"
|
||||||
|
],
|
||||||
|
"markers": "python_full_version >= '3.7.0'",
|
||||||
|
"version": "==0.20.0"
|
||||||
|
},
|
||||||
|
"trio-websocket": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:5b558f6e83cc20a37c3b61202476c5295d1addf57bd65543364e0337e37ed2bc",
|
||||||
|
"sha256:a3d34de8fac26023eee701ed1e7bf4da9a8326b61a62934ec9e53b64970fd8fe"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.5'",
|
||||||
|
"version": "==0.9.2"
|
||||||
|
},
|
||||||
|
"urllib3": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed",
|
||||||
|
"sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_full_version < '4.0.0'",
|
||||||
|
"version": "==1.26.8"
|
||||||
|
},
|
||||||
|
"wsproto": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2218cb57952d90b9fca325c0dcfb08c3bda93e8fd8070b0a17f048e2e47a521b",
|
||||||
|
"sha256:a2e56bfd5c7cd83c1369d83b5feccd6d37798b74872866e62616e0ecf111bda8"
|
||||||
|
],
|
||||||
|
"markers": "python_full_version >= '3.7.0'",
|
||||||
|
"version": "==1.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
readme.md
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# ptcoffee
|
||||||
|
|
||||||
|
## How To Start
|
||||||
|
|
||||||
|
|
||||||
|
### 1. Activate Virtualenv
|
||||||
|
|
||||||
|
`windows`
|
||||||
|
```cmd
|
||||||
|
<YOUR WORKING DIRECTORY>/venv/scripts/activate
|
||||||
|
```
|
||||||
|
>Your Current Working Directory
|
||||||
|
|
||||||
|
`Ubuntu [Debian]`
|
||||||
|
```commandline
|
||||||
|
source venv/bin/activate
|
||||||
|
```
|
||||||
|
>you can use any name instead of **venv**
|
||||||
|
|
||||||
|
### 2. Runserver
|
||||||
|
```
|
||||||
|
python3 src/manage.py runserver
|
||||||
|
```
|
||||||
|
|
||||||
|
> Built Using [django-cli](https://github.com/khan-asfi-reza/django-setup-cli)
|
||||||
23
setup.yaml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
name: ptcoffee
|
||||||
|
author: Nathan Chapman
|
||||||
|
description: E-commerce website for Port Townsend Coffee
|
||||||
|
libraries:
|
||||||
|
- celery
|
||||||
|
- whitenoise
|
||||||
|
- django-filter
|
||||||
|
- django-storages
|
||||||
|
static: true
|
||||||
|
database:
|
||||||
|
port: $DATABASE_PORT
|
||||||
|
host: $DATABASE_HOST
|
||||||
|
password: $DATABASE_PASSWORD
|
||||||
|
user: $DATABASE_USER
|
||||||
|
name: $DATABASE_NAME
|
||||||
|
engine: $DATABASE_ENGINE
|
||||||
|
cache:
|
||||||
|
location: $CACHE_LOCATION
|
||||||
|
backend: $CACHE_BACKEND
|
||||||
|
required:
|
||||||
|
- psycopg2-binary
|
||||||
|
env:
|
||||||
|
SECRET_KEY: $SECRET_KEY
|
||||||
52
src/accounts/__init__.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
STATE_CHOICES = [
|
||||||
|
('AL', 'Alabama'),
|
||||||
|
('AK', 'Alaska'),
|
||||||
|
('AZ', 'Arizona'),
|
||||||
|
('AR', 'Arkansas'),
|
||||||
|
('CA', 'California'),
|
||||||
|
('CO', 'Colorado'),
|
||||||
|
('CT', 'Connecticut'),
|
||||||
|
('DE', 'Delaware'),
|
||||||
|
('FL', 'Florida'),
|
||||||
|
('GA', 'Georgia'),
|
||||||
|
('HI', 'Hawaii'),
|
||||||
|
('ID', 'Idaho'),
|
||||||
|
('IL', 'Illinois'),
|
||||||
|
('IN', 'Indiana'),
|
||||||
|
('IA', 'Iowa'),
|
||||||
|
('KS', 'Kansas'),
|
||||||
|
('KY', 'Kentucky'),
|
||||||
|
('LA', 'Louisiana'),
|
||||||
|
('ME', 'Maine'),
|
||||||
|
('MD', 'Maryland'),
|
||||||
|
('MA', 'Massachusetts'),
|
||||||
|
('MI', 'Michigan'),
|
||||||
|
('MN', 'Minnesota'),
|
||||||
|
('MS', 'Mississippi'),
|
||||||
|
('MO', 'Missouri'),
|
||||||
|
('MT', 'Montana'),
|
||||||
|
('NE', 'Nebraska'),
|
||||||
|
('NV', 'Nevada'),
|
||||||
|
('NH', 'New Hampshire'),
|
||||||
|
('NJ', 'New Jersey'),
|
||||||
|
('NM', 'New Mexico'),
|
||||||
|
('NY', 'New York'),
|
||||||
|
('NC', 'North Carolina'),
|
||||||
|
('ND', 'North Dakota'),
|
||||||
|
('OH', 'Ohio'),
|
||||||
|
('OK', 'Oklahoma'),
|
||||||
|
('OR', 'Oregon'),
|
||||||
|
('PA', 'Pennsylvania'),
|
||||||
|
('RI', 'Rhode Island'),
|
||||||
|
('SC', 'South Carolina'),
|
||||||
|
('SD', 'South Dakota'),
|
||||||
|
('TN', 'Tennessee'),
|
||||||
|
('TX', 'Texas'),
|
||||||
|
('UT', 'Utah'),
|
||||||
|
('VT', 'Vermont'),
|
||||||
|
('VA', 'Virginia'),
|
||||||
|
('WA', 'Washington'),
|
||||||
|
('WV', 'West Virginia'),
|
||||||
|
('WI', 'Wisconsin'),
|
||||||
|
('WY', 'Wyoming'),
|
||||||
|
]
|
||||||
16
src/accounts/admin.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.admin import UserAdmin
|
||||||
|
|
||||||
|
from .models import Address, User
|
||||||
|
from .forms import AccountCreateForm, AccountUpdateForm
|
||||||
|
|
||||||
|
class UserAdmin(UserAdmin):
|
||||||
|
add_form = AccountCreateForm
|
||||||
|
form = AccountUpdateForm
|
||||||
|
model = User
|
||||||
|
list_display = ['email', 'username',]
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(Address)
|
||||||
|
admin.site.register(User, UserAdmin)
|
||||||
6
src/accounts/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AccountsConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'accounts'
|
||||||
23
src/accounts/forms.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from django import forms
|
||||||
|
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
|
||||||
|
from .models import Address, User
|
||||||
|
|
||||||
|
class AddressForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Address
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
|
class AccountCreateForm(UserCreationForm):
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = ('username', 'email')
|
||||||
|
|
||||||
|
class AccountUpdateForm(UserChangeForm):
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = [
|
||||||
|
'first_name',
|
||||||
|
'last_name',
|
||||||
|
'email',
|
||||||
|
]
|
||||||
61
src/accounts/migrations/0001_initial.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
# Generated by Django 4.0.2 on 2022-03-11 02:25
|
||||||
|
|
||||||
|
import django.contrib.auth.models
|
||||||
|
import django.contrib.auth.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('auth', '0012_alter_user_first_name_max_length'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Address',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('first_name', models.CharField(blank=True, max_length=256)),
|
||||||
|
('last_name', models.CharField(blank=True, max_length=256)),
|
||||||
|
('street_address_1', models.CharField(blank=True, max_length=256)),
|
||||||
|
('street_address_2', models.CharField(blank=True, max_length=256)),
|
||||||
|
('city', models.CharField(blank=True, max_length=256)),
|
||||||
|
('state', models.CharField(blank=True, choices=[('AL', 'Alabama'), ('AK', 'Alaska'), ('AZ', 'Arizona'), ('AR', 'Arkansas'), ('CA', 'California'), ('CO', 'Colorado'), ('CT', 'Connecticut'), ('DE', 'Delaware'), ('FL', 'Florida'), ('GA', 'Georgia'), ('HI', 'Hawaii'), ('ID', 'Idaho'), ('IL', 'Illinois'), ('IN', 'Indiana'), ('IA', 'Iowa'), ('KS', 'Kansas'), ('KY', 'Kentucky'), ('LA', 'Louisiana'), ('ME', 'Maine'), ('MD', 'Maryland'), ('MA', 'Massachusetts'), ('MI', 'Michigan'), ('MN', 'Minnesota'), ('MS', 'Mississippi'), ('MO', 'Missouri'), ('MT', 'Montana'), ('NE', 'Nebraska'), ('NV', 'Nevada'), ('NH', 'New Hampshire'), ('NJ', 'New Jersey'), ('NM', 'New Mexico'), ('NY', 'New York'), ('NC', 'North Carolina'), ('ND', 'North Dakota'), ('OH', 'Ohio'), ('OK', 'Oklahoma'), ('OR', 'Oregon'), ('PA', 'Pennsylvania'), ('RI', 'Rhode Island'), ('SC', 'South Carolina'), ('SD', 'South Dakota'), ('TN', 'Tennessee'), ('TX', 'Texas'), ('UT', 'Utah'), ('VT', 'Vermont'), ('VA', 'Virginia'), ('WA', 'Washington'), ('WV', 'West Virginia'), ('WI', 'Wisconsin'), ('WY', 'Wyoming')], max_length=2)),
|
||||||
|
('postal_code', models.CharField(blank=True, max_length=20)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='User',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||||
|
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||||
|
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||||
|
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||||
|
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||||
|
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||||
|
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
|
||||||
|
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||||
|
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||||
|
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||||
|
('addresses', models.ManyToManyField(blank=True, related_name='user_addresses', to='accounts.Address')),
|
||||||
|
('default_billing_address', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='accounts.address')),
|
||||||
|
('default_shipping_address', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='accounts.address')),
|
||||||
|
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
|
||||||
|
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'user',
|
||||||
|
'verbose_name_plural': 'users',
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
managers=[
|
||||||
|
('objects', django.contrib.auth.models.UserManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
0
src/accounts/migrations/__init__.py
Normal file
32
src/accounts/models.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
|
||||||
|
from . import STATE_CHOICES
|
||||||
|
|
||||||
|
class Address(models.Model):
|
||||||
|
first_name = models.CharField(max_length=256, blank=True)
|
||||||
|
last_name = models.CharField(max_length=256, blank=True)
|
||||||
|
street_address_1 = models.CharField(max_length=256, blank=True)
|
||||||
|
street_address_2 = models.CharField(max_length=256, blank=True)
|
||||||
|
city = models.CharField(max_length=256, blank=True)
|
||||||
|
state = models.CharField(
|
||||||
|
max_length=2,
|
||||||
|
choices=STATE_CHOICES,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
postal_code = models.CharField(max_length=20, blank=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'{self.street_address_1} — {self.city}'
|
||||||
|
|
||||||
|
class User(AbstractUser):
|
||||||
|
addresses = models.ManyToManyField(
|
||||||
|
Address, blank=True, related_name="user_addresses"
|
||||||
|
)
|
||||||
|
default_shipping_address = models.ForeignKey(
|
||||||
|
Address, related_name="+", null=True, blank=True, on_delete=models.SET_NULL
|
||||||
|
)
|
||||||
|
default_billing_address = models.ForeignKey(
|
||||||
|
Address, related_name="+", null=True, blank=True, on_delete=models.SET_NULL
|
||||||
|
)
|
||||||
24
src/accounts/tasks.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
from celery import shared_task
|
||||||
|
from celery.utils.log import get_task_logger
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.mail import EmailMessage, send_mail
|
||||||
|
|
||||||
|
from templated_email import send_templated_mail
|
||||||
|
|
||||||
|
from .models import User
|
||||||
|
|
||||||
|
logger = get_task_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
ACCOUNT_CREATED_TEMPLATE = 'accounts/account_created'
|
||||||
|
|
||||||
|
@shared_task(name='send_account_created_email')
|
||||||
|
def send_account_created_email(user):
|
||||||
|
send_templated_mail(
|
||||||
|
template_name=ACCOUNT_CREATED_TEMPLATE,
|
||||||
|
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||||
|
recipient_list=[user['email']],
|
||||||
|
context=user
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Account created email sent to {user['email']}")
|
||||||
14
src/accounts/templates/accounts/account_create.html
Executable file
@ -0,0 +1,14 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<section>
|
||||||
|
<h1>Sign up</h1>
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
|
||||||
|
<input type="submit" value="Create account">
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
13
src/accounts/templates/accounts/account_detail.html
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="panel">
|
||||||
|
<section>
|
||||||
|
<h1>{{ user.first_name }} {{ user.last_name }}</h1>
|
||||||
|
<p>{{ user.email }}</p>
|
||||||
|
<p>
|
||||||
|
<a href="{% url 'account-update' user.id %}">Update email/change password</a>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
17
src/accounts/templates/accounts/account_form.html
Executable file
@ -0,0 +1,17 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article>
|
||||||
|
<section>
|
||||||
|
<h1>Update Profile</h1>
|
||||||
|
<p><a href="{% url 'password_change' %}">Change password</a></p>
|
||||||
|
<form method="post" action="{% url 'account-update' user.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
|
||||||
|
<input type="submit" value="Save changes" class="action-button"> or
|
||||||
|
<a href="{% url 'account-detail' user.id %}">Cancel</a>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
23
src/accounts/templates/accounts/account_list.html
Executable file
@ -0,0 +1,23 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<section>
|
||||||
|
<h1>Users</h1>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<th>Username</th>
|
||||||
|
<th>Name</th>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for user in user_list %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ user.username }}</td>
|
||||||
|
<td><a href="{% url 'account-detail' user.id %}">{{user.first_name}} {{user.last_name}}</a></td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr><td>No users yet.</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
3
src/accounts/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
11
src/accounts/urls.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
from django.urls import path, include
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('', views.AccountListView.as_view(), name='account-list'),
|
||||||
|
path('<int:pk>/', include([
|
||||||
|
path('', views.AccountDetailView.as_view(), name='account-detail'),
|
||||||
|
path('update/', views.AccountUpdateView.as_view(), name='account-update'),
|
||||||
|
path('delete/', views.AccountDeleteView.as_view(), name='account-delete'),
|
||||||
|
])),
|
||||||
|
]
|
||||||
47
src/accounts/utils.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
from allauth.account.models import EmailAddress
|
||||||
|
|
||||||
|
from .models import Address, User
|
||||||
|
from .tasks import send_account_created_email
|
||||||
|
|
||||||
|
def get_or_create_customer(request, form, shipping_address):
|
||||||
|
address, a_created = Address.objects.get_or_create(
|
||||||
|
first_name = shipping_address['first_name'],
|
||||||
|
last_name = shipping_address['last_name'],
|
||||||
|
street_address_1 = shipping_address['street_address_1'],
|
||||||
|
street_address_2 = shipping_address['street_address_2'],
|
||||||
|
city = shipping_address['city'],
|
||||||
|
state = shipping_address['state'],
|
||||||
|
postal_code = shipping_address['postal_code']
|
||||||
|
)
|
||||||
|
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
user = request.user
|
||||||
|
else:
|
||||||
|
user, u_created = User.objects.get_or_create(
|
||||||
|
email=form.cleaned_data['email'],
|
||||||
|
defaults = {
|
||||||
|
'username': form.cleaned_data['email'],
|
||||||
|
'is_staff': False,
|
||||||
|
'is_active': True,
|
||||||
|
'is_superuser': False,
|
||||||
|
'first_name': form.cleaned_data['first_name'],
|
||||||
|
'last_name': form.cleaned_data['last_name'],
|
||||||
|
'default_shipping_address': address,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if u_created:
|
||||||
|
password = User.objects.make_random_password()
|
||||||
|
user.set_password(password)
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
EmailAddress.objects.create(user=user, email=user.email, primary=True, verified=False)
|
||||||
|
|
||||||
|
u = {
|
||||||
|
'full_name': user.get_full_name(),
|
||||||
|
'email': user.email,
|
||||||
|
'password': password
|
||||||
|
}
|
||||||
|
|
||||||
|
send_account_created_email.delay(u)
|
||||||
|
|
||||||
|
return user, address
|
||||||
38
src/accounts/views.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
from django.shortcuts import render, reverse, redirect
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
from django.views.generic.base import TemplateView
|
||||||
|
from django.views.generic.edit import FormView, CreateView, UpdateView, DeleteView
|
||||||
|
from django.views.generic.detail import DetailView
|
||||||
|
from django.views.generic.list import ListView
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.contrib.auth.forms import PasswordChangeForm
|
||||||
|
|
||||||
|
from .models import User
|
||||||
|
from .forms import AccountUpdateForm
|
||||||
|
|
||||||
|
|
||||||
|
class AccountListView(LoginRequiredMixin, ListView):
|
||||||
|
model = User
|
||||||
|
template_name = 'accounts/account_list.html'
|
||||||
|
|
||||||
|
class AccountDetailView(LoginRequiredMixin, DetailView):
|
||||||
|
model = User
|
||||||
|
template_name = 'accounts/account_detail.html'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
return context
|
||||||
|
|
||||||
|
class AccountUpdateView(LoginRequiredMixin, UpdateView):
|
||||||
|
model = User
|
||||||
|
form_class = AccountUpdateForm
|
||||||
|
template_name = 'accounts/account_form.html'
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
pk = self.kwargs["pk"]
|
||||||
|
return reverse('account-detail', kwargs={'pk': pk})
|
||||||
|
|
||||||
|
class AccountDeleteView(LoginRequiredMixin, DeleteView):
|
||||||
|
model = User
|
||||||
|
success_url = reverse_lazy('account-list')
|
||||||
73
src/core/__init__.py
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
class DiscountValueType:
|
||||||
|
FIXED = "fixed"
|
||||||
|
PERCENTAGE = "percentage"
|
||||||
|
|
||||||
|
CHOICES = [
|
||||||
|
(FIXED, settings.DEFAULT_CURRENCY),
|
||||||
|
(PERCENTAGE, "%"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class VoucherType:
|
||||||
|
SHIPPING = "shipping"
|
||||||
|
ENTIRE_ORDER = "entire_order"
|
||||||
|
SPECIFIC_PRODUCT = "specific_product"
|
||||||
|
|
||||||
|
CHOICES = [
|
||||||
|
(ENTIRE_ORDER, "Entire order"),
|
||||||
|
(SHIPPING, "Shipping"),
|
||||||
|
(SPECIFIC_PRODUCT, "Specific products, collections and categories"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class OrderStatus:
|
||||||
|
DRAFT = "draft" # fully editable, not finalized order created by staff users
|
||||||
|
UNFULFILLED = "unfulfilled" # order with no items marked as fulfilled
|
||||||
|
PARTIALLY_FULFILLED = (
|
||||||
|
"partially fulfilled" # order with some items marked as fulfilled
|
||||||
|
)
|
||||||
|
FULFILLED = "fulfilled" # order with all items marked as fulfilled
|
||||||
|
|
||||||
|
PARTIALLY_RETURNED = (
|
||||||
|
"partially_returned" # order with some items marked as returned
|
||||||
|
)
|
||||||
|
RETURNED = "returned" # order with all items marked as returned
|
||||||
|
CANCELED = "canceled" # permanently canceled order
|
||||||
|
|
||||||
|
CHOICES = [
|
||||||
|
(DRAFT, "Draft"),
|
||||||
|
(UNFULFILLED, "Unfulfilled"),
|
||||||
|
(PARTIALLY_FULFILLED, "Partially fulfilled"),
|
||||||
|
(PARTIALLY_RETURNED, "Partially returned"),
|
||||||
|
(RETURNED, "Returned"),
|
||||||
|
(FULFILLED, "Fulfilled"),
|
||||||
|
(CANCELED, "Canceled"),
|
||||||
|
]
|
||||||
|
|
||||||
|
class TransactionStatus:
|
||||||
|
CREATED = "CREATED" # The order was created with the specified context.
|
||||||
|
SAVED = "SAVED" # The order was saved and persisted. The order status continues to be in progress until a capture is made with final_capture = true for all purchase units within the order.
|
||||||
|
APPROVED = "APPROVED" # The customer approved the payment through the PayPal wallet or another form of guest or unbranded payment. For example, a card, bank account, or so on.
|
||||||
|
VOIDED = "VOIDED" # All purchase units in the order are voided.
|
||||||
|
COMPLETED = "COMPLETED" # The payment was authorized or the authorized payment was captured for the order.
|
||||||
|
PAYER_ACTION_REQUIRED = "PAYER_ACTION_REQUIRED" # The order requires an action from the payer (e.g. 3DS authentication). Redirect the payer to the "rel":"payer-action" HATEOAS link returned as part of the response prior to authorizing or capturing the order.
|
||||||
|
|
||||||
|
CHOICES = [
|
||||||
|
(CREATED, "Created"),
|
||||||
|
(SAVED, "Saved"),
|
||||||
|
(APPROVED, "Approved"),
|
||||||
|
(VOIDED, "Voided"),
|
||||||
|
(COMPLETED, "Completed"),
|
||||||
|
(PAYER_ACTION_REQUIRED, "Payer action required")
|
||||||
|
]
|
||||||
|
|
||||||
|
class ShippingMethodType:
|
||||||
|
PRICE_BASED = "price"
|
||||||
|
WEIGHT_BASED = "weight"
|
||||||
|
|
||||||
|
CHOICES = [
|
||||||
|
(PRICE_BASED, "Price based shipping"),
|
||||||
|
(WEIGHT_BASED, "Weight based shipping"),
|
||||||
|
]
|
||||||
19
src/core/admin.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from .models import (
|
||||||
|
Product,
|
||||||
|
ProductPhoto,
|
||||||
|
Coupon,
|
||||||
|
ShippingMethod,
|
||||||
|
Order,
|
||||||
|
Transaction,
|
||||||
|
OrderLine,
|
||||||
|
)
|
||||||
|
|
||||||
|
admin.site.register(Product)
|
||||||
|
admin.site.register(ProductPhoto)
|
||||||
|
admin.site.register(Coupon)
|
||||||
|
admin.site.register(ShippingMethod)
|
||||||
|
admin.site.register(Order)
|
||||||
|
admin.site.register(Transaction)
|
||||||
|
admin.site.register(OrderLine)
|
||||||
9
src/core/apps.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class CoreConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'core'
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
from .signals import order_created, transaction_created
|
||||||
161
src/core/fixtures/core.json
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
[{
|
||||||
|
"model": "core.product",
|
||||||
|
"pk": 1,
|
||||||
|
"fields": {
|
||||||
|
"name": "Ethiopia",
|
||||||
|
"description": "Spicy espresso reminiscent of Northern Italy, on the mild side. Perfect for espresso, and steamed milk drinks. Also, a full-bodied, earthy sweet drip or Americano. Contains organic beans from Indonesia, Africa and America.",
|
||||||
|
"sku": "23468",
|
||||||
|
"price": "13.40",
|
||||||
|
"weight": "0.453592:kg",
|
||||||
|
"visible_in_listings": true,
|
||||||
|
"created_at": "2022-02-19T20:15:36.292Z",
|
||||||
|
"updated_at": "2022-02-23T17:57:37.916Z"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"model": "core.product",
|
||||||
|
"pk": 2,
|
||||||
|
"fields": {
|
||||||
|
"name": "Sumatra",
|
||||||
|
"description": "Dark heavy-bodied roast with a lingering chocolatey taste. Organic Single origin.",
|
||||||
|
"sku": "89765",
|
||||||
|
"price": "13.40",
|
||||||
|
"weight": "0.453592:kg",
|
||||||
|
"visible_in_listings": true,
|
||||||
|
"created_at": "2022-02-19T20:15:59.741Z",
|
||||||
|
"updated_at": "2022-02-23T17:58:24.210Z"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"model": "core.product",
|
||||||
|
"pk": 3,
|
||||||
|
"fields": {
|
||||||
|
"name": "Pantomime",
|
||||||
|
"description": "Very Dark French Roast\r\nOur darkest drip. A blend of five different beans roasted two ways. Organic Africa, Indonesia, and South and Central America.",
|
||||||
|
"sku": "565656",
|
||||||
|
"price": "13.40",
|
||||||
|
"weight": "0.453592:kg",
|
||||||
|
"visible_in_listings": true,
|
||||||
|
"created_at": "2022-02-23T17:59:00.711Z",
|
||||||
|
"updated_at": "2022-02-23T17:59:00.711Z"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"model": "core.product",
|
||||||
|
"pk": 4,
|
||||||
|
"fields": {
|
||||||
|
"name": "Decaf",
|
||||||
|
"description": "French Roast (Water Processed)\r\n\r\n“I can’t believe it’s decaf!”. The best-tasting Swiss water process decaf we have developed over the past 30 years, for an unbelievable espresso or drip coffee. Organic Africa, Indonesia and South and Central America.",
|
||||||
|
"sku": "566565",
|
||||||
|
"price": "13.40",
|
||||||
|
"weight": "0.453592:kg",
|
||||||
|
"visible_in_listings": true,
|
||||||
|
"created_at": "2022-02-23T17:59:32.099Z",
|
||||||
|
"updated_at": "2022-02-23T17:59:32.099Z"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"model": "core.product",
|
||||||
|
"pk": 5,
|
||||||
|
"fields": {
|
||||||
|
"name": "Moka Java Blend",
|
||||||
|
"description": "Dark Roast\r\n\r\nA classic Moka Java style blend dark roasted with organic beans for a perfect body and sweetness with a hint of citrus.",
|
||||||
|
"sku": "56466",
|
||||||
|
"price": "13.40",
|
||||||
|
"weight": "0.453592:kg",
|
||||||
|
"visible_in_listings": true,
|
||||||
|
"created_at": "2022-02-23T18:05:41.742Z",
|
||||||
|
"updated_at": "2022-02-23T18:05:41.742Z"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"model": "core.product",
|
||||||
|
"pk": 6,
|
||||||
|
"fields": {
|
||||||
|
"name": "Loop d’ Loop",
|
||||||
|
"description": "Mild Dark Roast\r\n\r\nOur most popular blend reminiscent of Central Italy. A dark, chocolaty flavor perfect for espresso or drip. It’s dark Vienna roast properties make it ideal for steamed milk drinks. Organic Indonesia, Africa and America.",
|
||||||
|
"sku": "53264",
|
||||||
|
"price": "13.40",
|
||||||
|
"weight": "0.453592:kg",
|
||||||
|
"visible_in_listings": true,
|
||||||
|
"created_at": "2022-02-23T18:06:09.881Z",
|
||||||
|
"updated_at": "2022-02-23T18:06:09.881Z"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"model": "core.product",
|
||||||
|
"pk": 7,
|
||||||
|
"fields": {
|
||||||
|
"name": "Dante’s Tornado",
|
||||||
|
"description": "Medium Roast\r\n\r\nFull City spicy espresso roast reminiscent of Northern Italy, on the mild side. A full- bodied, earthy sweet drip or Americano. Organic Indonesia, Africa and America.",
|
||||||
|
"sku": "78945",
|
||||||
|
"price": "13.40",
|
||||||
|
"weight": "0.453592:kg",
|
||||||
|
"visible_in_listings": true,
|
||||||
|
"created_at": "2022-02-23T18:06:35.593Z",
|
||||||
|
"updated_at": "2022-02-23T18:06:35.593Z"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"model": "core.product",
|
||||||
|
"pk": 8,
|
||||||
|
"fields": {
|
||||||
|
"name": "Nicaragua",
|
||||||
|
"description": "Mild Roast\r\n\r\nOur mildest roast with sweet and fruity notes, containing organic beans from Nicaragua. Single origin.",
|
||||||
|
"sku": "12365",
|
||||||
|
"price": "13.40",
|
||||||
|
"weight": "0.453592:kg",
|
||||||
|
"visible_in_listings": true,
|
||||||
|
"created_at": "2022-02-23T18:06:57.624Z",
|
||||||
|
"updated_at": "2022-02-23T18:06:57.624Z"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"model": "core.productphoto",
|
||||||
|
"pk": 1,
|
||||||
|
"fields": {
|
||||||
|
"product": 1,
|
||||||
|
"image": "products/images/slice2.png"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"model": "core.productphoto",
|
||||||
|
"pk": 2,
|
||||||
|
"fields": {
|
||||||
|
"product": 2,
|
||||||
|
"image": "products/images/slice1.png"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"model": "core.productphoto",
|
||||||
|
"pk": 3,
|
||||||
|
"fields": {
|
||||||
|
"product": 3,
|
||||||
|
"image": "products/images/pantomime.png"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"model": "core.productphoto",
|
||||||
|
"pk": 4,
|
||||||
|
"fields": {
|
||||||
|
"product": 4,
|
||||||
|
"image": "products/images/decaf.png"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"model": "core.productphoto",
|
||||||
|
"pk": 5,
|
||||||
|
"fields": {
|
||||||
|
"product": 5,
|
||||||
|
"image": "products/images/moka_java.png"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"model": "core.productphoto",
|
||||||
|
"pk": 6,
|
||||||
|
"fields": {
|
||||||
|
"product": 6,
|
||||||
|
"image": "products/images/loop_d_loop.png"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"model": "core.productphoto",
|
||||||
|
"pk": 7,
|
||||||
|
"fields": {
|
||||||
|
"product": 7,
|
||||||
|
"image": "products/images/dantes_tornado.png"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"model": "core.productphoto",
|
||||||
|
"pk": 8,
|
||||||
|
"fields": {
|
||||||
|
"product": 8,
|
||||||
|
"image": "products/images/nicaragua.png"
|
||||||
|
}
|
||||||
|
}]
|
||||||
12
src/core/forms.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import logging
|
||||||
|
from django import forms
|
||||||
|
from django.core.mail import EmailMessage
|
||||||
|
|
||||||
|
from core.models import Order, OrderLine, ShippingMethod
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class ShippingMethodForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = ShippingMethod
|
||||||
|
fields = '__all__'
|
||||||
114
src/core/migrations/0001_initial.py
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
# Generated by Django 4.0.2 on 2022-03-11 02:25
|
||||||
|
|
||||||
|
import core.weight
|
||||||
|
from decimal import Decimal
|
||||||
|
from django.conf import settings
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
import django_measurement.models
|
||||||
|
import measurement.measures.mass
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0001_initial'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Order',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('status', models.CharField(choices=[('draft', 'Draft'), ('unfulfilled', 'Unfulfilled'), ('partially fulfilled', 'Partially fulfilled'), ('partially_returned', 'Partially returned'), ('returned', 'Returned'), ('fulfilled', 'Fulfilled'), ('canceled', 'Canceled')], default='unfulfilled', max_length=32)),
|
||||||
|
('total_net_amount', models.DecimalField(decimal_places=2, default=0, max_digits=10)),
|
||||||
|
('weight', django_measurement.models.MeasurementField(default=core.weight.zero_weight, measurement=measurement.measures.mass.Mass)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('billing_address', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='accounts.address')),
|
||||||
|
('customer', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to=settings.AUTH_USER_MODEL)),
|
||||||
|
('shipping_address', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='accounts.address')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Product',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=250)),
|
||||||
|
('description', models.TextField(blank=True)),
|
||||||
|
('sku', models.CharField(max_length=255, unique=True)),
|
||||||
|
('price', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)),
|
||||||
|
('weight', django_measurement.models.MeasurementField(blank=True, measurement=measurement.measures.mass.Mass, null=True)),
|
||||||
|
('visible_in_listings', models.BooleanField(default=False)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ShippingMethod',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100)),
|
||||||
|
('type', models.CharField(choices=[('price', 'Price based shipping'), ('weight', 'Weight based shipping')], max_length=30)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Transaction',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('status', models.CharField(blank=True, choices=[('CREATED', 'Created'), ('SAVED', 'Saved'), ('APPROVED', 'Approved'), ('VOIDED', 'Voided'), ('COMPLETED', 'Completed'), ('PAYER_ACTION_REQUIRED', 'Payer action required')], default='CREATED', max_length=32)),
|
||||||
|
('paypal_id', models.CharField(blank=True, max_length=64)),
|
||||||
|
('confirmation_email_sent', models.BooleanField(default=False)),
|
||||||
|
('order', models.OneToOneField(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.order')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ProductPhoto',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('image', models.ImageField(upload_to='products/images')),
|
||||||
|
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.product')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='OrderLine',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('quantity', models.IntegerField(validators=[django.core.validators.MinValueValidator(1)])),
|
||||||
|
('quantity_fulfilled', models.IntegerField(default=0, validators=[django.core.validators.MinValueValidator(0)])),
|
||||||
|
('customer_note', models.TextField(blank=True, default='')),
|
||||||
|
('currency', models.CharField(default='USD', max_length=3)),
|
||||||
|
('unit_price', models.DecimalField(decimal_places=2, max_digits=12)),
|
||||||
|
('tax_rate', models.DecimalField(decimal_places=2, default=Decimal('0.0'), max_digits=5)),
|
||||||
|
('order', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='lines', to='core.order')),
|
||||||
|
('product', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='order_lines', to='core.product')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='order',
|
||||||
|
name='shipping_method',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to='core.shippingmethod'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Coupon',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('type', models.CharField(choices=[('entire_order', 'Entire order'), ('shipping', 'Shipping'), ('specific_product', 'Specific products, collections and categories')], default='entire_order', max_length=20)),
|
||||||
|
('name', models.CharField(blank=True, max_length=255, null=True)),
|
||||||
|
('code', models.CharField(db_index=True, max_length=12, unique=True)),
|
||||||
|
('valid_from', models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
|
('valid_to', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('discount_value_type', models.CharField(choices=[('fixed', 'USD'), ('percentage', '%')], default='fixed', max_length=10)),
|
||||||
|
('discount_value', models.DecimalField(decimal_places=2, max_digits=12)),
|
||||||
|
('products', models.ManyToManyField(blank=True, to='core.Product')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ('code',),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
src/core/migrations/__init__.py
Normal file
221
src/core/models.py
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
import logging
|
||||||
|
from decimal import Decimal
|
||||||
|
from measurement.measures import Weight
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.db.models.functions import Coalesce
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||||
|
|
||||||
|
from django_measurement.models import MeasurementField
|
||||||
|
|
||||||
|
from accounts.models import User, Address
|
||||||
|
|
||||||
|
from . import (
|
||||||
|
DiscountValueType,
|
||||||
|
VoucherType,
|
||||||
|
TransactionStatus,
|
||||||
|
OrderStatus,
|
||||||
|
ShippingMethodType
|
||||||
|
)
|
||||||
|
from .weight import WeightUnits, zero_weight
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ProductManager(models.Manager):
|
||||||
|
def get_queryset(self):
|
||||||
|
return super().get_queryset().annotate(
|
||||||
|
num_ordered=models.Count('order_lines__quantity', distinct=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Product(models.Model):
|
||||||
|
name = models.CharField(max_length=250)
|
||||||
|
description = models.TextField(blank=True)
|
||||||
|
sku = models.CharField(max_length=255, unique=True)
|
||||||
|
price = models.DecimalField(
|
||||||
|
max_digits=settings.DEFAULT_MAX_DIGITS,
|
||||||
|
decimal_places=settings.DEFAULT_DECIMAL_PLACES,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
weight = MeasurementField(
|
||||||
|
measurement=Weight, unit_choices=WeightUnits.CHOICES, blank=True, null=True
|
||||||
|
)
|
||||||
|
|
||||||
|
visible_in_listings = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
objects = ProductManager()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class ProductPhoto(models.Model):
|
||||||
|
product = models.ForeignKey(Product, on_delete=models.CASCADE)
|
||||||
|
image = models.ImageField(upload_to='products/images')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.product.name
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Coupon(models.Model):
|
||||||
|
type = models.CharField(
|
||||||
|
max_length=20, choices=VoucherType.CHOICES, default=VoucherType.ENTIRE_ORDER
|
||||||
|
)
|
||||||
|
name = models.CharField(max_length=255, null=True, blank=True)
|
||||||
|
code = models.CharField(max_length=12, unique=True, db_index=True)
|
||||||
|
valid_from = models.DateTimeField(default=timezone.now)
|
||||||
|
valid_to = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
|
discount_value_type = models.CharField(
|
||||||
|
max_length=10,
|
||||||
|
choices=DiscountValueType.CHOICES,
|
||||||
|
default=DiscountValueType.FIXED,
|
||||||
|
)
|
||||||
|
discount_value = models.DecimalField(
|
||||||
|
max_digits=settings.DEFAULT_MAX_DIGITS,
|
||||||
|
decimal_places=settings.DEFAULT_DECIMAL_PLACES,
|
||||||
|
)
|
||||||
|
|
||||||
|
products = models.ManyToManyField(Product, blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ("code",)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_valid(self):
|
||||||
|
today = timezone.localtime(timezone.now()).date()
|
||||||
|
return True if today >= self.valid_from and today <= self.valid_to else False
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ShippingMethod(models.Model):
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
type = models.CharField(max_length=30, choices=ShippingMethodType.CHOICES)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Order(models.Model):
|
||||||
|
customer = models.ForeignKey(
|
||||||
|
User,
|
||||||
|
related_name="orders",
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
status = models.CharField(
|
||||||
|
max_length=32, default=OrderStatus.UNFULFILLED, choices=OrderStatus.CHOICES
|
||||||
|
)
|
||||||
|
billing_address = models.ForeignKey(
|
||||||
|
Address,
|
||||||
|
related_name="+",
|
||||||
|
editable=False,
|
||||||
|
null=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
)
|
||||||
|
shipping_address = models.ForeignKey(
|
||||||
|
Address,
|
||||||
|
related_name="+",
|
||||||
|
editable=False,
|
||||||
|
null=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
)
|
||||||
|
shipping_method = models.ForeignKey(
|
||||||
|
ShippingMethod,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
related_name="orders",
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
)
|
||||||
|
|
||||||
|
total_net_amount = models.DecimalField(
|
||||||
|
max_digits=10,
|
||||||
|
decimal_places=2,
|
||||||
|
default=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
weight = MeasurementField(
|
||||||
|
measurement=Weight,
|
||||||
|
unit_choices=WeightUnits.CHOICES,
|
||||||
|
default=zero_weight,
|
||||||
|
)
|
||||||
|
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True, editable=False)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
def get_total_quantity(self):
|
||||||
|
return sum([line.quantity for line in self])
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('dashboard:order-detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
class Transaction(models.Model):
|
||||||
|
status = models.CharField(
|
||||||
|
max_length=32,
|
||||||
|
blank=True,
|
||||||
|
default=TransactionStatus.CREATED,
|
||||||
|
choices=TransactionStatus.CHOICES
|
||||||
|
)
|
||||||
|
paypal_id = models.CharField(max_length=64, blank=True)
|
||||||
|
confirmation_email_sent = models.BooleanField(default=False)
|
||||||
|
order = models.OneToOneField(
|
||||||
|
Order,
|
||||||
|
editable=False,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class OrderLine(models.Model):
|
||||||
|
order = models.ForeignKey(
|
||||||
|
Order,
|
||||||
|
related_name="lines",
|
||||||
|
editable=False,
|
||||||
|
on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
product = models.ForeignKey(
|
||||||
|
Product,
|
||||||
|
related_name="order_lines",
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
quantity = models.IntegerField(validators=[MinValueValidator(1)])
|
||||||
|
quantity_fulfilled = models.IntegerField(
|
||||||
|
validators=[MinValueValidator(0)], default=0
|
||||||
|
)
|
||||||
|
customer_note = models.TextField(blank=True, default="")
|
||||||
|
|
||||||
|
currency = models.CharField(
|
||||||
|
max_length=settings.DEFAULT_CURRENCY_CODE_LENGTH,
|
||||||
|
default=settings.DEFAULT_CURRENCY,
|
||||||
|
)
|
||||||
|
|
||||||
|
unit_price = models.DecimalField(
|
||||||
|
max_digits=settings.DEFAULT_MAX_DIGITS,
|
||||||
|
decimal_places=settings.DEFAULT_DECIMAL_PLACES,
|
||||||
|
)
|
||||||
|
|
||||||
|
tax_rate = models.DecimalField(
|
||||||
|
max_digits=5, decimal_places=2, default=Decimal("0.0")
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_total(self):
|
||||||
|
return self.unit_price * self.quantity
|
||||||
|
|
||||||
|
@property
|
||||||
|
def quantity_unfulfilled(self):
|
||||||
|
return self.quantity - self.quantity_fulfilled
|
||||||
34
src/core/signals.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import logging
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
from django.db.models.signals import post_save
|
||||||
|
from django.dispatch import receiver
|
||||||
|
|
||||||
|
from . import TransactionStatus
|
||||||
|
from .models import Order, Transaction
|
||||||
|
from .tasks import send_order_confirmation_email
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@receiver(post_save, sender=Order, dispatch_uid="order_created")
|
||||||
|
def order_created(sender, instance, created, **kwargs):
|
||||||
|
if created:
|
||||||
|
logger.info("Order was created")
|
||||||
|
Transaction.objects.create(order=instance)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_save, sender=Transaction, dispatch_uid="transaction_created")
|
||||||
|
def transaction_created(sender, instance, created, **kwargs):
|
||||||
|
if created:
|
||||||
|
logger.info("Transaction was created")
|
||||||
|
|
||||||
|
elif instance.status == TransactionStatus.COMPLETED and not instance.confirmation_email_sent:
|
||||||
|
# TODO: change order to order.values()
|
||||||
|
order = {
|
||||||
|
'order_id': instance.order.pk,
|
||||||
|
'email': instance.order.customer.email,
|
||||||
|
'full_name': instance.order.customer.get_full_name()
|
||||||
|
}
|
||||||
|
send_order_confirmation_email.delay(order)
|
||||||
|
instance.confirmation_email_sent = True
|
||||||
|
instance.save()
|
||||||
26
src/core/tasks.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
from celery import shared_task
|
||||||
|
from celery.utils.log import get_task_logger
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.mail import EmailMessage, send_mail
|
||||||
|
|
||||||
|
from templated_email import send_templated_mail
|
||||||
|
|
||||||
|
from .models import Order
|
||||||
|
|
||||||
|
logger = get_task_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
CONFIRM_ORDER_TEMPLATE = 'storefront/order_confirmation'
|
||||||
|
ORDER_CANCEl_TEMPLATE = 'storefront/order_cancel'
|
||||||
|
ORDER_REFUND_TEMPLATE = 'storefront/order_refund'
|
||||||
|
|
||||||
|
@shared_task(name='send_order_confirmation_email')
|
||||||
|
def send_order_confirmation_email(order):
|
||||||
|
send_templated_mail(
|
||||||
|
template_name=CONFIRM_ORDER_TEMPLATE,
|
||||||
|
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||||
|
recipient_list=[order['email']],
|
||||||
|
context=order
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Order confirmation email sent to {order['email']}")
|
||||||
3
src/core/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
3
src/core/views.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
||||||
40
src/core/weight.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
from measurement.measures import Weight
|
||||||
|
|
||||||
|
class WeightUnits:
|
||||||
|
KILOGRAM = "kg"
|
||||||
|
POUND = "lb"
|
||||||
|
OUNCE = "oz"
|
||||||
|
GRAM = "g"
|
||||||
|
|
||||||
|
CHOICES = [
|
||||||
|
(KILOGRAM, "kg"),
|
||||||
|
(POUND, "lb"),
|
||||||
|
(OUNCE, "oz"),
|
||||||
|
(GRAM, "g"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def zero_weight():
|
||||||
|
"""Represent the zero weight value."""
|
||||||
|
return Weight(kg=0)
|
||||||
|
|
||||||
|
|
||||||
|
def convert_weight(weight: Weight, unit: str) -> Weight:
|
||||||
|
"""Covert weight to given unit and round it to 3 digits after decimal point."""
|
||||||
|
# Weight amount from the Weight instance can be retrived in serveral units
|
||||||
|
# via its properties. eg. Weight(lb=10).kg
|
||||||
|
converted_weight = getattr(weight, unit)
|
||||||
|
weight = Weight(**{unit: converted_weight})
|
||||||
|
weight.value = round(weight.value, 3)
|
||||||
|
return weight
|
||||||
|
|
||||||
|
|
||||||
|
def convert_weight_to_default_weight_unit(weight: Weight) -> Weight:
|
||||||
|
"""Weight is kept in one unit, but should be returned in site default unit."""
|
||||||
|
default_unit = get_default_weight_unit()
|
||||||
|
if weight is not None:
|
||||||
|
if weight.unit != default_unit:
|
||||||
|
weight = convert_weight(weight, default_unit)
|
||||||
|
else:
|
||||||
|
weight.value = round(weight.value, 3)
|
||||||
|
return weight
|
||||||
0
src/dashboard/__init__.py
Normal file
3
src/dashboard/admin.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
6
src/dashboard/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class DashboardConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'dashboard'
|
||||||
27
src/dashboard/forms.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import logging
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
from core.models import Order, OrderLine, ShippingMethod
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class OrderLineFulfillForm(forms.ModelForm):
|
||||||
|
# send_shipment_details_to_customer = forms.BooleanField(initial=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = OrderLine
|
||||||
|
fields = ('quantity_fulfilled',)
|
||||||
|
widgets = {
|
||||||
|
'quantity_fulfilled': forms.NumberInput(attrs = {
|
||||||
|
'min': 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['quantity_fulfilled'].widget.attrs['max'] = self.instance.quantity
|
||||||
|
|
||||||
|
OrderLineFormset = forms.inlineformset_factory(
|
||||||
|
Order, OrderLine, form=OrderLineFulfillForm,
|
||||||
|
extra=0, can_delete=False
|
||||||
|
)
|
||||||
0
src/dashboard/migrations/__init__.py
Normal file
3
src/dashboard/models.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
76
src/dashboard/templates/dashboard/customer_detail.html
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
{% extends "dashboard.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article>
|
||||||
|
<header class="object__header">
|
||||||
|
<h1><img src="{% static "images/customers.png" %}" alt=""> Customer: {{customer.get_full_name}}</h1>
|
||||||
|
<a href="{% url 'dashboard:customer-update' customer.pk %}" class="action-button">Edit</a>
|
||||||
|
</header>
|
||||||
|
<section class="object__panel">
|
||||||
|
<div class="object__item object__item--header">
|
||||||
|
<h4>Info</h4>
|
||||||
|
</div>
|
||||||
|
<div class="panel__item">
|
||||||
|
<strong>Email address</strong><br>
|
||||||
|
{{customer.email}}
|
||||||
|
</div>
|
||||||
|
<div class="panel__item">
|
||||||
|
<strong>Default shipping address</strong><br>
|
||||||
|
{% with shipping_address=customer.default_shipping_address %}
|
||||||
|
<address>
|
||||||
|
{{shipping_address.first_name}}
|
||||||
|
{{shipping_address.last_name}}<br>
|
||||||
|
{{shipping_address.street_address_1}}<br>
|
||||||
|
{% if shipping_address.street_address_2 %}
|
||||||
|
{{shipping_address.street_address_2}}<br>
|
||||||
|
{% endif %}
|
||||||
|
{{shipping_address.city}}, {{shipping_address.state}}, {{shipping_address.postal_code}}
|
||||||
|
</address>
|
||||||
|
<a href="address-update">Edit</a>
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
<div class="panel__item">
|
||||||
|
<strong>Other addresses</strong><br>
|
||||||
|
{% for address in customer.addresses.all %}
|
||||||
|
<p>
|
||||||
|
<address>
|
||||||
|
{{address.first_name}}
|
||||||
|
{{address.last_name}}<br>
|
||||||
|
{{address.street_address_1}}<br>
|
||||||
|
{% if address.street_address_2 %}
|
||||||
|
{{address.street_address_2}}<br>
|
||||||
|
{% endif %}
|
||||||
|
{{address.city}}, {{address.state}}, {{address.postal_code}}
|
||||||
|
</address>
|
||||||
|
<a href="address-update">Edit</a>
|
||||||
|
</p>
|
||||||
|
{% empty %}
|
||||||
|
<p>No other addresses.</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% with order_list=customer.orders.all %}
|
||||||
|
<section class="object__list">
|
||||||
|
<div class="object__item object__item--header" href="order-detail">
|
||||||
|
<span>Order #</span>
|
||||||
|
<span>Date</span>
|
||||||
|
<span>Status</span>
|
||||||
|
<span>Total</span>
|
||||||
|
</div>
|
||||||
|
{% for order in order_list %}
|
||||||
|
<a class="object__item" href="{% url 'dashboard:order-detail' order.pk %}">
|
||||||
|
<span>#{{order.pk}}</span>
|
||||||
|
<span>{{order.created_at|date:"D, M j Y"}}</span>
|
||||||
|
<span class="order__status--display">
|
||||||
|
<div class="status__dot order__status--{{order.status}}"></div>
|
||||||
|
{{order.get_status_display}}</span>
|
||||||
|
<span>${{order.total_net_amount}}</span>
|
||||||
|
</a>
|
||||||
|
{% empty %}
|
||||||
|
<span class="object__item">No orders</span>
|
||||||
|
{% endfor %}
|
||||||
|
</section>
|
||||||
|
{% endwith %}
|
||||||
|
</article>
|
||||||
|
{% endblock content %}
|
||||||
16
src/dashboard/templates/dashboard/customer_form.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{% extends "dashboard.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="product">
|
||||||
|
<h1>Update Customer</h1>
|
||||||
|
<section>
|
||||||
|
<form method="POST" action="{% url 'dashboard:customer-update' customer.pk %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{form.as_p}}
|
||||||
|
<p class="form__submit">
|
||||||
|
<input class="action-button" type="submit" value="Save changes"> or <a href="{% url 'dashboard:customer-detail' customer.pk %}">cancel</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
24
src/dashboard/templates/dashboard/customer_list.html
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{% extends "dashboard.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article>
|
||||||
|
<h1><img src="{% static 'images/customer.png' %}" alt=""> Customers</h1>
|
||||||
|
<section class="object__list">
|
||||||
|
<div class="object__item object__item--header" href="customer-detail">
|
||||||
|
<span>Name</span>
|
||||||
|
<span>Email</span>
|
||||||
|
<span>Orders</span>
|
||||||
|
</div>
|
||||||
|
{% for customer in user_list %}
|
||||||
|
<a class="object__item" href="{% url 'dashboard:customer-detail' customer.pk %}">
|
||||||
|
<span>{{customer.get_full_name}}</span>
|
||||||
|
<span>{{customer.email}}</span>
|
||||||
|
<span>{{customer.num_orders}}</span>
|
||||||
|
</a>
|
||||||
|
{% empty %}
|
||||||
|
<span class="object__item">No customers</span>
|
||||||
|
{% endfor %}
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
{% endblock content %}
|
||||||
25
src/dashboard/templates/dashboard/dashboard_detail.html
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{% extends "dashboard.html" %}
|
||||||
|
{% load static %}
|
||||||
|
{% load tz %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article>
|
||||||
|
<h1><img src="{% static "images/store.png" %}" alt=""> Port Townsend Coffee</h1>
|
||||||
|
<section class="store__info">
|
||||||
|
<div class="orders">
|
||||||
|
<h5>Orders</h5>
|
||||||
|
<small>Today {% now "" %}</small>
|
||||||
|
<h3>{{order_count}}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="sales">
|
||||||
|
<h5>Sales</h5>
|
||||||
|
<small>Today</small>
|
||||||
|
<h3>${{todays_sales|floatformat:2}}</h3>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<a class="store__action" href="{% url 'dashboard:order-list' %}?status=unfulfilled">{{orders_unfulfilled}} orders ready to fulfill →</a>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
82
src/dashboard/templates/dashboard/order_detail.html
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
{% extends "dashboard.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article>
|
||||||
|
<header class="object__header">
|
||||||
|
<h1><img src="{% static "images/box.png" %}" alt=""> Order #{{order.pk}}</h1>
|
||||||
|
<span class="order__status order__status--{{order.status}}">{{order.get_status_display}}</span>
|
||||||
|
</header>
|
||||||
|
<section class="object__list">
|
||||||
|
<div class="object__item object__item--header">
|
||||||
|
<span>Product</span>
|
||||||
|
<span>SKU</span>
|
||||||
|
<span>Quantity</span>
|
||||||
|
<span>Price</span>
|
||||||
|
<span>Total</span>
|
||||||
|
</div>
|
||||||
|
{% for item in order.lines.all %}
|
||||||
|
<div class="object__item">
|
||||||
|
{% with product=item.product %}
|
||||||
|
<figure class="item__figure">
|
||||||
|
<img class="product__image product__image--small" src="{{product.productphoto_set.first.image.url}}" alt="{{product.productphoto_set.first.image}}">
|
||||||
|
<figcaption><strong>{{product.name}}</strong></figcaption>
|
||||||
|
</figure>
|
||||||
|
<span>{{product.sku}}</span>
|
||||||
|
<span>{{item.quantity}}</span>
|
||||||
|
<span>${{product.price}}</span>
|
||||||
|
<span>${{item.get_total}}</span>
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<p>No items in order yet.</p>
|
||||||
|
{% endfor %}
|
||||||
|
<div class="object__item">
|
||||||
|
<a href="{% url 'dashboard:order-fulfill' order.pk %}" class="action-button order__fulfill">Fulfill →</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="object__panel">
|
||||||
|
<div class="object__item object__item--header">
|
||||||
|
<h4>Customer</h4>
|
||||||
|
</div>
|
||||||
|
{% with customer=order.customer %}
|
||||||
|
<div class="panel__item">
|
||||||
|
<p>
|
||||||
|
<strong>{{customer.get_full_name}}</strong><br>
|
||||||
|
<a href="{% url 'dashboard:customer-detail' customer.pk %}">View Profile</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="panel__item">
|
||||||
|
<strong>Email address</strong><br>
|
||||||
|
{{customer.email}}
|
||||||
|
</div>
|
||||||
|
<div class="panel__item">
|
||||||
|
<strong>Shipping address</strong><br>
|
||||||
|
{% with shipping_address=order.shipping_address %}
|
||||||
|
<address>
|
||||||
|
{{shipping_address.first_name}}
|
||||||
|
{{shipping_address.last_name}}<br>
|
||||||
|
{{shipping_address.street_address_1}}<br>
|
||||||
|
{% if shipping_address.street_address_2 %}
|
||||||
|
{{shipping_address.street_address_2}}<br>
|
||||||
|
{% endif %}
|
||||||
|
{{shipping_address.city}}, {{shipping_address.state}}, {{shipping_address.postal_code}}
|
||||||
|
</address>
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
{% endwith %}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="object__panel">
|
||||||
|
<div class="object__item object__item--header">
|
||||||
|
<h4>PayPal</h4>
|
||||||
|
</div>
|
||||||
|
<div class="panel__item">
|
||||||
|
<p>Transaction: <strong>{{order.transaction.paypal_id}}</strong><br>
|
||||||
|
Status: <strong>{{order.transaction.get_status_display}}</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
{% endblock content %}
|
||||||
46
src/dashboard/templates/dashboard/order_fulfill.html
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
{% extends "dashboard.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article>
|
||||||
|
<h1>Fulfill Order #{{order.pk}}</h1>
|
||||||
|
<section>
|
||||||
|
<form method="POST" action="">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.management_form }}
|
||||||
|
|
||||||
|
<section class="object__list">
|
||||||
|
{% for dict in form.errors %}
|
||||||
|
{% for error in dict.values %}
|
||||||
|
<div class="object__item">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
<div class="object__item object__item--header">
|
||||||
|
<span>Product</span>
|
||||||
|
<span>SKU</span>
|
||||||
|
<span>Quantity to fulfill</span>
|
||||||
|
<span>Grind</span>
|
||||||
|
</div>
|
||||||
|
{% for form in form %}
|
||||||
|
<div class="object__item">
|
||||||
|
{% with product=form.instance.product %}
|
||||||
|
{{form.id}}
|
||||||
|
<figure class="item__figure">
|
||||||
|
<img class="product__image product__image--small" src="{{product.productphoto_set.first.image.url}}" alt="{{product.productphoto_set.first.image}}">
|
||||||
|
<figcaption><strong>{{product.name}}</strong></figcaption>
|
||||||
|
</figure>
|
||||||
|
<span>{{product.sku}}</span>
|
||||||
|
<span>{{form.quantity_fulfilled}} / {{form.instance.quantity}}</span>
|
||||||
|
<span>{{form.instance.customer_note}}</span>
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<div class="object__item">
|
||||||
|
<a href="{% url 'dashboard:order-detail' order.pk %}">cancel</a> <input class="action-button order__fulfill" type="submit" value="Fulfill">
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
{% endblock content %}
|
||||||
32
src/dashboard/templates/dashboard/order_list.html
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{% extends "dashboard.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article>
|
||||||
|
<header class="object__header">
|
||||||
|
<h1><img src="{% static "images/box.png" %}" alt=""> Orders</h1>
|
||||||
|
</header>
|
||||||
|
<section class="object__list">
|
||||||
|
<div class="object__item object__item--header" href="order-detail">
|
||||||
|
<span>Order #</span>
|
||||||
|
<span>Date</span>
|
||||||
|
<span>Customer</span>
|
||||||
|
<span>Status</span>
|
||||||
|
<span>Total</span>
|
||||||
|
</div>
|
||||||
|
{% for order in order_list %}
|
||||||
|
<a class="object__item" href="{% url 'dashboard:order-detail' order.pk %}">
|
||||||
|
<span>#{{order.pk}}</span>
|
||||||
|
<span>{{order.created_at|date:"D, M j Y"}}</span>
|
||||||
|
<span>{{order.customer.get_full_name}}</span>
|
||||||
|
<span class="order__status--display">
|
||||||
|
<div class="status__dot order__status--{{order.status}}"></div>
|
||||||
|
{{order.get_status_display}}</span>
|
||||||
|
<span>${{order.total_net_amount}}</span>
|
||||||
|
</a>
|
||||||
|
{% empty %}
|
||||||
|
<span class="object__item">No orders</span>
|
||||||
|
{% endfor %}
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
{% endblock content %}
|
||||||
16
src/dashboard/templates/dashboard/product_create_form.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{% extends "dashboard.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article class="product">
|
||||||
|
<h1>Create product</h1>
|
||||||
|
<section>
|
||||||
|
<form method="POST" action="{% url 'dashboard:product-create' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{form.as_p}}
|
||||||
|
<p class="form__submit">
|
||||||
|
<input class="action-button" type="submit" value="Create product"> or <a href="{% url 'dashboard:product-list' %}">cancel</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
{% endblock %}
|
||||||
23
src/dashboard/templates/dashboard/product_detail.html
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{% extends "dashboard.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article>
|
||||||
|
<header class="object__header">
|
||||||
|
<h1><img src="{% static "images/cubes.png" %}" alt=""> Product: {{product.name}}</h1>
|
||||||
|
<a href="edit" class="action-button">Edit</a>
|
||||||
|
</header>
|
||||||
|
<section class="product__detail">
|
||||||
|
<figure class="product__figure">
|
||||||
|
<img class="" src="{{product.productphoto_set.first.image.url}}" alt="{{product.productphoto_set.first.image}}">
|
||||||
|
</figure>
|
||||||
|
<div>
|
||||||
|
<h1>{{product.name}}</h1>
|
||||||
|
<p>{{product.description}}</p>
|
||||||
|
<p>$<strong>{{product.price}}</strong></p>
|
||||||
|
<p>Visible in listings: <strong>{{product.visible_in_listings|yesno:"Yes,No"}}</strong></p>
|
||||||
|
<p>Ordered {{num_ordered}} times.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
{% endblock content %}
|
||||||
29
src/dashboard/templates/dashboard/product_list.html
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{% extends "dashboard.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article>
|
||||||
|
<header class="object__header">
|
||||||
|
<h1><img src="{% static "images/cubes.png" %}" alt=""> Catalog</h1>
|
||||||
|
<a href="{% url 'dashboard:product-create' %}" class="action-button">+ New product</a>
|
||||||
|
</header>
|
||||||
|
<section class="object__list">
|
||||||
|
<div class="object__item object__item--header">
|
||||||
|
<span></span>
|
||||||
|
<span>Name</span>
|
||||||
|
<span>Visible</span>
|
||||||
|
<span>Price</span>
|
||||||
|
</div>
|
||||||
|
{% for product in product_list %}
|
||||||
|
<a class="object__item" href="{% url 'dashboard:product-detail' product.pk %}">
|
||||||
|
<figure class="product__figure">
|
||||||
|
<img class="product__image" src="{{product.productphoto_set.first.image.url}}" alt="{{product.productphoto_set.first.image}}">
|
||||||
|
</figure>
|
||||||
|
<strong>{{product.name}}</strong>
|
||||||
|
<span>{{product.visible_in_listings|yesno:"Yes,No"}}</span>
|
||||||
|
<span>${{product.price}}</span>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
{% endblock content %}
|
||||||
3
src/dashboard/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
29
src/dashboard/urls.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
from django.urls import path, include
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('', views.DashboardHomeView.as_view(), name='home'),
|
||||||
|
|
||||||
|
path('orders/', views.OrderListView.as_view(), name='order-list'),
|
||||||
|
path('orders/<int:pk>/', include([
|
||||||
|
path('', views.OrderDetailView.as_view(), name='order-detail'),
|
||||||
|
# path('update/', views.OrderUpdateView.as_view(), name='product-update'),
|
||||||
|
# path('delete/', views.OrderDeleteView.as_view(), name='product-delete'),
|
||||||
|
path('fulfill/', views.OrderFulfillView.as_view(), name='order-fulfill'),
|
||||||
|
])),
|
||||||
|
|
||||||
|
path('products/', views.ProductListView.as_view(), name='product-list'),
|
||||||
|
path('products/new/', views.ProductCreateView.as_view(), name='product-create'),
|
||||||
|
path('<int:pk>/', include([
|
||||||
|
path('', views.ProductDetailView.as_view(), name='product-detail'),
|
||||||
|
# path('update/', views.ProductUpdateView.as_view(), name='product-update'),
|
||||||
|
# path('delete/', views.ProductDeleteView.as_view(), name='product-delete'),
|
||||||
|
])),
|
||||||
|
|
||||||
|
path('customers/', views.CustomerListView.as_view(), name='customer-list'),
|
||||||
|
path('customers/<int:pk>/', include([
|
||||||
|
path('', views.CustomerDetailView.as_view(), name='customer-detail'),
|
||||||
|
path('update/', views.CustomerUpdateView.as_view(), name='customer-update'),
|
||||||
|
# path('delete/', views.CustomerDeleteView.as_view(), name='customer-delete'),
|
||||||
|
])),
|
||||||
|
]
|
||||||
183
src/dashboard/views.py
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.shortcuts import render, reverse, redirect, get_object_or_404
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
from django.urls import reverse, reverse_lazy
|
||||||
|
from django.views.generic.base import RedirectView, TemplateView
|
||||||
|
from django.views.generic.edit import FormView, CreateView, UpdateView, DeleteView, FormMixin
|
||||||
|
from django.views.generic.detail import DetailView, SingleObjectMixin
|
||||||
|
from django.views.generic.list import ListView
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.forms import inlineformset_factory
|
||||||
|
|
||||||
|
from django.db.models import (
|
||||||
|
Exists, OuterRef, Prefetch, Subquery, Count, Sum, Avg, F, Q, Value
|
||||||
|
)
|
||||||
|
from django.db.models.functions import Coalesce
|
||||||
|
|
||||||
|
from accounts.models import User
|
||||||
|
from accounts.utils import get_or_create_customer
|
||||||
|
from accounts.forms import AddressForm
|
||||||
|
from core.models import Product, Order, OrderLine
|
||||||
|
|
||||||
|
from core import DiscountValueType, VoucherType, OrderStatus, ShippingMethodType
|
||||||
|
from .forms import OrderLineFulfillForm, OrderLineFormset
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class DashboardHomeView(TemplateView):
|
||||||
|
template_name = 'dashboard/dashboard_detail.html'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
today = timezone.localtime(timezone.now()).date()
|
||||||
|
context['order_count'] = Order.objects.filter(
|
||||||
|
created_at__date=today
|
||||||
|
).count()
|
||||||
|
context['orders_unfulfilled'] = Order.objects.filter(
|
||||||
|
status=OrderStatus.UNFULFILLED
|
||||||
|
).count()
|
||||||
|
context['todays_sales'] = Order.objects.filter(
|
||||||
|
created_at__date=today
|
||||||
|
).aggregate(total=Sum('total_net_amount'))['total'] or 0
|
||||||
|
return context
|
||||||
|
|
||||||
|
class OrderListView(ListView):
|
||||||
|
model = Order
|
||||||
|
template_name = 'dashboard/order_list.html'
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
query = self.request.GET.get('status')
|
||||||
|
order = self.request.GET.get('order')
|
||||||
|
if query:
|
||||||
|
object_list = Order.objects.filter(
|
||||||
|
status=query
|
||||||
|
).order_by(
|
||||||
|
'-created_at'
|
||||||
|
).select_related(
|
||||||
|
'customer'
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
object_list = Order.objects.order_by(
|
||||||
|
'-created_at'
|
||||||
|
).select_related(
|
||||||
|
'customer'
|
||||||
|
)
|
||||||
|
|
||||||
|
return object_list
|
||||||
|
|
||||||
|
class OrderDetailView(DetailView):
|
||||||
|
model = Order
|
||||||
|
template_name = 'dashboard/order_detail.html'
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
queryset = Order.objects.filter(
|
||||||
|
pk=self.kwargs.get(self.pk_url_kwarg)
|
||||||
|
).select_related(
|
||||||
|
'customer',
|
||||||
|
'billing_address',
|
||||||
|
'shipping_address',
|
||||||
|
'shipping_method'
|
||||||
|
).prefetch_related(
|
||||||
|
'lines__product__productphoto_set'
|
||||||
|
)
|
||||||
|
obj = queryset.get()
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class OrderFulfillView(UpdateView):
|
||||||
|
model = Order
|
||||||
|
template_name = "dashboard/order_fulfill.html"
|
||||||
|
form_class = OrderLineFormset
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
form.save()
|
||||||
|
return HttpResponseRedirect(self.get_success_url())
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse('dashboard:order-detail', kwargs={'pk': self.object.pk})
|
||||||
|
|
||||||
|
|
||||||
|
def order_fulfill(request, pk):
|
||||||
|
order = Order.objects.get(pk=pk)
|
||||||
|
OrderLineFormset = inlineformset_factory(Order, OrderLine, form=OrderLineFulfillForm, extra=0, can_delete=False)
|
||||||
|
if request.method == "POST":
|
||||||
|
formset = OrderLineFormset(request.POST, request.FILES, instance=order)
|
||||||
|
if formset.is_valid():
|
||||||
|
formset.save()
|
||||||
|
# Do something. Should generally end with a redirect. For example:
|
||||||
|
return HttpResponseRedirect(order.get_absolute_url())
|
||||||
|
|
||||||
|
else:
|
||||||
|
formset = OrderLineFormset(instance=order)
|
||||||
|
return render(request, 'dashboard/order_fulfill.html', {'formset': formset, 'order': order})
|
||||||
|
|
||||||
|
|
||||||
|
class ProductListView(ListView):
|
||||||
|
model = Product
|
||||||
|
template_name = 'dashboard/product_list.html'
|
||||||
|
|
||||||
|
# def get_queryset(self):
|
||||||
|
# object_list = Product.objects.filter(
|
||||||
|
# status=OrderStatus.UNFULFILLED
|
||||||
|
# ).select_related(
|
||||||
|
# 'customer'
|
||||||
|
# )
|
||||||
|
# return object_list
|
||||||
|
|
||||||
|
class ProductDetailView(DetailView):
|
||||||
|
model = Product
|
||||||
|
template_name = 'dashboard/product_detail.html'
|
||||||
|
|
||||||
|
class ProductCreateView(CreateView):
|
||||||
|
model = Product
|
||||||
|
template_name = "dashboard/product_create_form.html"
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
|
class CustomerListView(ListView):
|
||||||
|
model = User
|
||||||
|
template_name = 'dashboard/customer_list.html'
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
object_list = User.objects.filter(
|
||||||
|
Exists(
|
||||||
|
Order.objects.filter(customer=OuterRef('pk'))
|
||||||
|
) | Q(is_staff=False)
|
||||||
|
).prefetch_related(
|
||||||
|
'orders'
|
||||||
|
).annotate(
|
||||||
|
num_orders=Count('orders')
|
||||||
|
)
|
||||||
|
|
||||||
|
return object_list
|
||||||
|
|
||||||
|
class CustomerDetailView(DetailView):
|
||||||
|
model = User
|
||||||
|
template_name = 'dashboard/customer_detail.html'
|
||||||
|
context_object_name = 'customer'
|
||||||
|
|
||||||
|
class CustomerUpdateView(UpdateView):
|
||||||
|
model = User
|
||||||
|
template_name = 'dashboard/customer_form.html'
|
||||||
|
context_object_name = 'customer'
|
||||||
|
fields = (
|
||||||
|
'first_name',
|
||||||
|
'last_name',
|
||||||
|
'email',
|
||||||
|
'is_staff',
|
||||||
|
'addresses',
|
||||||
|
'default_shipping_address'
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse('dashboard:customer-detail', kwargs={'pk': self.object.pk})
|
||||||
|
|
||||||
22
src/manage.py
Executable file
@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run administrative tasks."""
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ptcoffee.settings')
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(
|
||||||
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
|
"forget to activate a virtual environment?"
|
||||||
|
) from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
BIN
src/media/products/images/coffee.jpg
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
src/media/products/images/coffee_02.jpg
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
src/media/products/images/coffee_05.jpg
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
src/media/products/images/dantes_tornado.png
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
src/media/products/images/decaf.png
Normal file
|
After Width: | Height: | Size: 129 KiB |
BIN
src/media/products/images/loop_d_loop.png
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
src/media/products/images/moka_java.png
Normal file
|
After Width: | Height: | Size: 123 KiB |
BIN
src/media/products/images/nicaragua.png
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
src/media/products/images/pantomime.png
Normal file
|
After Width: | Height: | Size: 123 KiB |
BIN
src/media/products/images/slice1.png
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
src/media/products/images/slice2.png
Normal file
|
After Width: | Height: | Size: 123 KiB |
3
src/ptcoffee/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from .celery import app as celery_app
|
||||||
|
|
||||||
|
__all__ = ('celery_app',)
|
||||||
11
src/ptcoffee/asgi.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
"""
|
||||||
|
https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ptcoffee.settings')
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
||||||
8
src/ptcoffee/celery.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import os
|
||||||
|
from celery import Celery
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
app = Celery('ptcoffee')
|
||||||
|
|
||||||
|
app.config_from_object('django.conf:settings', namespace='CELERY')
|
||||||
|
app.autodiscover_tasks()
|
||||||
30
src/ptcoffee/config.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
from dotenv import load_dotenv
|
||||||
|
import os
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
DEBUG = os.environ.get('DEBUG', True)
|
||||||
|
|
||||||
|
DATABASE_CONFIG = {
|
||||||
|
'ENGINE' : 'django.db.backends.postgresql',
|
||||||
|
'OPTIONS': {
|
||||||
|
'service': 'pg_service',
|
||||||
|
'passfile': '.pgpass'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SECRET_KEY = os.environ.get('SECRET_KEY', '')
|
||||||
|
CACHE_CONFIG = {
|
||||||
|
'LOCATION' : 'redis://127.0.0.1:6379',
|
||||||
|
'BACKEND' : 'django.core.cache.backends.redis.RedisCache',
|
||||||
|
}
|
||||||
|
|
||||||
|
PAYPAL_CLIENT_ID = os.environ.get('PAYPAL_CLIENT_ID', '')
|
||||||
|
PAYPAL_SECRET_ID = os.environ.get('PAYPAL_SECRET_ID', '')
|
||||||
|
|
||||||
|
ANYMAIL_CONFIG = {
|
||||||
|
'MAILGUN_API_KEY': os.environ.get('MAILGUN_API_KEY', ''),
|
||||||
|
'MAILGUN_SENDER_DOMAIN': os.environ.get('MAILGUN_SENDER_DOMAIN', '')
|
||||||
|
}
|
||||||
|
|
||||||
|
SERVER_EMAIL = os.environ.get('SERVER_EMAIL', '')
|
||||||
|
DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', '')
|
||||||
45
src/ptcoffee/middleware.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import zoneinfo
|
||||||
|
from urllib import parse
|
||||||
|
from celery.utils.log import get_task_logger
|
||||||
|
from inspect import getmodule
|
||||||
|
import dashboard.views
|
||||||
|
from django.http import Http404
|
||||||
|
|
||||||
|
|
||||||
|
logger = get_task_logger(__name__)
|
||||||
|
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
class TimezoneMiddleware:
|
||||||
|
def __init__(self, get_response):
|
||||||
|
self.get_response = get_response
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
|
tzname = request.COOKIES.get('timezone')
|
||||||
|
if tzname:
|
||||||
|
tzname = parse.unquote(tzname)
|
||||||
|
timezone.activate(zoneinfo.ZoneInfo(tzname))
|
||||||
|
else:
|
||||||
|
timezone.deactivate()
|
||||||
|
return self.get_response(request)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class RestrictStaffToAdminMiddleware:
|
||||||
|
"""
|
||||||
|
A middleware that restricts staff members access to administration panels.
|
||||||
|
"""
|
||||||
|
def __init__(self, get_response):
|
||||||
|
self.get_response = get_response
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
|
response = self.get_response(request)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def process_view(self, request, view_func, view_args, view_kwargs):
|
||||||
|
module = getmodule(view_func)
|
||||||
|
if (module is dashboard.views) and (not request.user.is_staff):
|
||||||
|
ip = request.META.get('HTTP_X_REAL_IP', request.META.get('REMOTE_ADDR'))
|
||||||
|
ua = request.META.get('HTTP_USER_AGENT')
|
||||||
|
logger.warn(f'Non-staff user "{request.user}" attempted to access admin site at "{request.get_full_path()}". UA = "{ua}", IP = "{ip}", Method = {request.method}')
|
||||||
|
raise Http404
|
||||||
218
src/ptcoffee/settings.py
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
from .config import *
|
||||||
|
|
||||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
# Add Your Required Allow Host
|
||||||
|
ALLOWED_HOSTS = ["*"]
|
||||||
|
|
||||||
|
INTERNAL_IPS = [
|
||||||
|
'127.0.0.1',
|
||||||
|
'localhost',
|
||||||
|
]
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
# Core
|
||||||
|
'django.contrib.admin',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
|
||||||
|
# 3rd Party
|
||||||
|
'django_filters',
|
||||||
|
'storages',
|
||||||
|
'debug_toolbar',
|
||||||
|
'django_celery_beat',
|
||||||
|
'django_celery_results',
|
||||||
|
'anymail',
|
||||||
|
'compressor',
|
||||||
|
'allauth',
|
||||||
|
'allauth.account',
|
||||||
|
'allauth.socialaccount',
|
||||||
|
|
||||||
|
# Local
|
||||||
|
'accounts.apps.AccountsConfig',
|
||||||
|
'core.apps.CoreConfig',
|
||||||
|
'storefront.apps.StorefrontConfig',
|
||||||
|
'dashboard.apps.DashboardConfig',
|
||||||
|
]
|
||||||
|
|
||||||
|
# Middlewares
|
||||||
|
MIDDLEWARE = [
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
'ptcoffee.middleware.TimezoneMiddleware',
|
||||||
|
'ptcoffee.middleware.RestrictStaffToAdminMiddleware',
|
||||||
|
'debug_toolbar.middleware.DebugToolbarMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'ptcoffee.urls'
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'DIRS': [BASE_DIR / 'templates'],
|
||||||
|
'APP_DIRS': True,
|
||||||
|
'OPTIONS': {
|
||||||
|
'context_processors': [
|
||||||
|
'django.template.context_processors.debug',
|
||||||
|
'django.template.context_processors.request',
|
||||||
|
'django.contrib.auth.context_processors.auth',
|
||||||
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
'storefront.context_processors.cart',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = 'ptcoffee.wsgi.application'
|
||||||
|
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': DATABASE_CONFIG
|
||||||
|
}
|
||||||
|
|
||||||
|
CACHES = {'default': CACHE_CONFIG}
|
||||||
|
|
||||||
|
SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
AUTHENTICATION_BACKENDS = (
|
||||||
|
'django.contrib.auth.backends.ModelBackend',
|
||||||
|
'allauth.account.auth_backends.AuthenticationBackend',
|
||||||
|
)
|
||||||
|
|
||||||
|
AUTH_USER_MODEL = 'accounts.User'
|
||||||
|
LOGIN_REDIRECT_URL = reverse_lazy('storefront:product-list')
|
||||||
|
|
||||||
|
ACCOUNT_EMAIL_REQUIRED = True
|
||||||
|
ACCOUNT_USERNAME_REQUIRED = False
|
||||||
|
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
|
||||||
|
ACCOUNT_SIGNUP_PASSWORD_ENTER_TWICE = True
|
||||||
|
ACCOUNT_SESSION_REMEMBER = True
|
||||||
|
ACCOUNT_AUTHENTICATION_METHOD = 'email'
|
||||||
|
ACCOUNT_UNIQUE_EMAIL = True
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/3.2/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = 'en-us'
|
||||||
|
|
||||||
|
TIME_ZONE = 'UTC'
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_L10N = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/3.2/howto/static-files/
|
||||||
|
|
||||||
|
STATIC_URL = '/static/'
|
||||||
|
STATIC_ROOT = BASE_DIR / 'public'
|
||||||
|
STATICFILES_DIRS = [BASE_DIR / 'static']
|
||||||
|
MEDIA_URL = '/images/'
|
||||||
|
MEDIA_ROOT = BASE_DIR / 'media'
|
||||||
|
|
||||||
|
STATICFILES_FINDERS = (
|
||||||
|
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||||
|
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||||
|
'compressor.finders.CompressorFinder',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Default primary key field type
|
||||||
|
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
|
||||||
|
|
||||||
|
# Email
|
||||||
|
EMAIL_BACKEND = 'anymail.backends.mailgun.EmailBackend'
|
||||||
|
|
||||||
|
ANYMAIL = ANYMAIL_CONFIG
|
||||||
|
|
||||||
|
ADMINS = (
|
||||||
|
('Nathan Chapman', 'debug@nathanjchapman.com'),
|
||||||
|
)
|
||||||
|
|
||||||
|
MANAGERS = ADMINS
|
||||||
|
|
||||||
|
TEMPLATED_EMAIL_BACKEND = 'templated_email.backends.vanilla_django.TemplateBackend'
|
||||||
|
|
||||||
|
|
||||||
|
# Site ID
|
||||||
|
SITE_ID = 1
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
LOGGING = {
|
||||||
|
'version': 1,
|
||||||
|
'disable_existing_loggers': False,
|
||||||
|
'formatters': {
|
||||||
|
'standard': {
|
||||||
|
'format': '%(asctime)s %(levelname)s %(name)s %(message)s'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'handlers': {
|
||||||
|
'console': {
|
||||||
|
'class': 'logging.StreamHandler',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'root': {
|
||||||
|
'handlers': ['console'],
|
||||||
|
'level': 'DEBUG',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
CART_SESSION_ID = 'cart'
|
||||||
|
|
||||||
|
DEFAULT_COUNTRY = 'US'
|
||||||
|
DEFAULT_CURRENCY = 'USD'
|
||||||
|
DEFAULT_DECIMAL_PLACES = 2
|
||||||
|
DEFAULT_MAX_DIGITS = 12
|
||||||
|
DEFAULT_CURRENCY_CODE_LENGTH = 3
|
||||||
|
|
||||||
|
|
||||||
|
# Celery - prefix with CELERY_
|
||||||
|
CELERY_BROKER_URL = CACHE_CONFIG['LOCATION']
|
||||||
|
CELERY_RESULT_BACKEND = CELERY_BROKER_URL
|
||||||
|
CELERY_CACHE_BACKEND = CELERY_BROKER_URL
|
||||||
|
CELERY_ACCEPT_CONTENT = ['json']
|
||||||
|
CELERY_TASK_SERIALIZER = 'json'
|
||||||
|
CELERY_RESULT_SERIALIZER = 'json'
|
||||||
|
CELERY_TASK_TRACK_STARTED = True
|
||||||
|
CELERY_TIMEZONE = 'US/Mountain'
|
||||||
17
src/ptcoffee/urls.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import debug_toolbar
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import include, path
|
||||||
|
from django.conf.urls.static import static
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('', include(('storefront.urls', 'storefront'), namespace='storefront')),
|
||||||
|
path('dashboard/', include(('dashboard.urls', 'dashboard'), namespace='dashboard')),
|
||||||
|
path('accounts/', include('allauth.urls')),
|
||||||
|
path('accounts/', include(('accounts.urls', 'accounts'), namespace='accounts')),
|
||||||
|
# path('accounts/', include('django.contrib.auth.urls')),
|
||||||
|
path('admin/', admin.site.urls),
|
||||||
|
path('__debug__/', include('debug_toolbar.urls')),
|
||||||
|
]
|
||||||
|
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||||
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
7
src/ptcoffee/wsgi.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ptcoffee.settings')
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
||||||
BIN
src/static/images/box.png
Normal file
|
After Width: | Height: | Size: 546 B |
BIN
src/static/images/coffee_banner.jpeg
Normal file
|
After Width: | Height: | Size: 350 KiB |
BIN
src/static/images/coupon.png
Normal file
|
After Width: | Height: | Size: 390 B |
BIN
src/static/images/cubes.png
Normal file
|
After Width: | Height: | Size: 698 B |
BIN
src/static/images/customer.png
Normal file
|
After Width: | Height: | Size: 673 B |
BIN
src/static/images/fair_trade_stamp.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
src/static/images/gear.png
Normal file
|
After Width: | Height: | Size: 673 B |
BIN
src/static/images/keep_calm.jpg
Normal file
|
After Width: | Height: | Size: 27 KiB |
7
src/static/images/shopping_cart.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Svg Vector Icons : http://www.onlinewebfonts.com/icon -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve">
|
||||||
|
<metadata> Svg Vector Icons : http://www.onlinewebfonts.com/icon </metadata>
|
||||||
|
<g><path d="M400.1,681.3"/><path d="M373.2,594.6h425.4c20,0,40.1-12.7,47.7-30.2l140.4-321.7c5.1-11.8,4.2-24.8-2.4-34.9c-6.4-9.7-16.9-15.3-28.9-15.3H283.2l-13-58c-6.9-31.1-37.8-55.4-70.3-55.4H30.6C19.2,79.2,10,88.4,10,99.8c0,11.4,9.2,20.6,20.6,20.6H200c13.3,0,27.3,10.8,30,23.2l131.2,586.3c-41.3,11.2-71.9,48.9-71.9,93.7c0,53.6,43.6,97.2,97.2,97.2c53.6,0,97.2-43.6,97.2-97.2c0-19.9-6-38.3-16.3-53.7h271.1c-10.3,15.4-16.3,33.9-16.3,53.7c0,53.6,43.6,97.2,97.2,97.2c53.6,0,97.2-43.6,97.2-97.2c0-53.6-43.6-97.2-97.2-97.2c-7,0-13.8,0.8-20.4,2.2H407c-1.3-0.3-2.6-0.5-3.9-0.7L373.2,594.6z M945.6,233.8L808.5,547.9c-0.9,2.1-5.9,5.5-9.9,5.5H364l-71.5-319.6H945.6z M442.5,823.6c0,30.9-25.1,56-56,56c-30.9,0-56-25.1-56-56c0-29.6,23-53.8,52.1-55.8c2.7,1.3,5.6,2,8.7,2H402C425.4,776.6,442.5,798.1,442.5,823.6 M875.5,823.6c0,30.9-25.1,56-56,56c-30.9,0-56-25.1-56-56c0-25.5,17.1-47,40.5-53.7h10c3.1,0,6.1-0.8,8.8-2C852.2,769.6,875.5,793.9,875.5,823.6"/></g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/static/images/store.png
Normal file
|
After Width: | Height: | Size: 697 B |
60
src/static/scripts/controllers/cart_controller.js
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { getCookie, setCookie } from "../lib/cookie.js"
|
||||||
|
import { Controller } from "../stimulus.js"
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static get targets() {
|
||||||
|
return [ "cartList" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
console.log(getCookie('cart'))
|
||||||
|
this.cart = this.getOrSetCart()
|
||||||
|
this.element.addEventListener('addToCart', this.addItem.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrSetCart() {
|
||||||
|
let cart = JSON.parse(getCookie('cart'))
|
||||||
|
if (cart === null) {
|
||||||
|
console.log('created cart cookie')
|
||||||
|
setCookie('cart', '{}')
|
||||||
|
cart = {}
|
||||||
|
} else {
|
||||||
|
Object.keys(cart).forEach((item_id) => {
|
||||||
|
fetch(`/${item_id}/`)
|
||||||
|
.then((response) => response.text())
|
||||||
|
.then((html) => {
|
||||||
|
this.cartListTarget.innerHTML += html;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return cart
|
||||||
|
}
|
||||||
|
|
||||||
|
decodeCartItems(cart) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
addItem(event) {
|
||||||
|
if (this.cart[event.detail.item] === undefined) {
|
||||||
|
this.cart[event.detail.item] = {'quantity': 1}
|
||||||
|
} else {
|
||||||
|
this.cart[event.detail.item]['quantity'] += 1
|
||||||
|
}
|
||||||
|
setCookie('cart', JSON.stringify(this.cart))
|
||||||
|
console.log(this.cart)
|
||||||
|
|
||||||
|
fetch(`/${event.detail.item}/`)
|
||||||
|
.then((response) => response.text())
|
||||||
|
.then((html) => {
|
||||||
|
this.cartListTarget.innerHTML += html;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeItem() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/static/scripts/controllers/cartitem_controller.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { getCookie, setCookie } from "../lib/cookie.js"
|
||||||
|
import { Controller } from "../stimulus.js"
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static get targets() {
|
||||||
|
return [ "qty" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
console.log(this.qtyTarget)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
23
src/static/scripts/controllers/product_controller.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
// import getCookie from "../get_cookie.js"
|
||||||
|
import { Controller } from "../stimulus.js"
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static values = { url: String }
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this.event = new CustomEvent('addToCart', {
|
||||||
|
detail: {
|
||||||
|
item: this.urlValue
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
addToCart(event) {
|
||||||
|
event.preventDefault()
|
||||||
|
window.dispatchEvent(this.event)
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/static/scripts/index.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Application } from "./stimulus.js"
|
||||||
|
|
||||||
|
import CartController from "./controllers/cart_controller.js"
|
||||||
|
import CartitemController from "./controllers/cartitem_controller.js"
|
||||||
|
import ProductController from "./controllers/product_controller.js"
|
||||||
|
|
||||||
|
const application = Application.start()
|
||||||
|
application.register("cart", CartController)
|
||||||
|
application.register("cartitem", CartitemController)
|
||||||
|
application.register("product", ProductController)
|
||||||
4
src/static/scripts/initializers/timezone.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { setCookie } from '../lib/cookie.js'
|
||||||
|
|
||||||
|
const { timeZone } = new Intl.DateTimeFormat().resolvedOptions()
|
||||||
|
setCookie('timezone', timeZone)
|
||||||
158
src/static/scripts/payment.js
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
import { getCookie } from "./lib/cookie.js"
|
||||||
|
|
||||||
|
let form = document.querySelector('form.order__form')
|
||||||
|
|
||||||
|
// Render the PayPal button into #paypal-button-container
|
||||||
|
paypal.Buttons({
|
||||||
|
|
||||||
|
// Call your server to set up the transaction
|
||||||
|
createOrder: function(data, actions) {
|
||||||
|
const formData = new FormData(form)
|
||||||
|
|
||||||
|
// get the csrftoken
|
||||||
|
const csrftoken = getCookie("csrftoken")
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
method: "POST",
|
||||||
|
body: new URLSearchParams(formData),
|
||||||
|
mode: "same-origin",
|
||||||
|
};
|
||||||
|
|
||||||
|
// construct a new Request passing in the csrftoken
|
||||||
|
const request = new Request('/checkout/', {
|
||||||
|
headers: { "X-CSRFToken": csrftoken },
|
||||||
|
})
|
||||||
|
|
||||||
|
return fetch(request, options)
|
||||||
|
.then(function(res) {
|
||||||
|
return res.json();
|
||||||
|
}).then(function(orderData) {
|
||||||
|
return orderData.id;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Call your server to finalize the transaction
|
||||||
|
onApprove: function(data, actions) {
|
||||||
|
const csrftoken = getCookie("csrftoken")
|
||||||
|
return fetch('/paypal/order/' + data.orderID + '/capture/', {
|
||||||
|
method: 'post',
|
||||||
|
headers: {'X-CSRFToken': csrftoken}
|
||||||
|
}).then(function(res) {
|
||||||
|
return res.json();
|
||||||
|
}).then(function(orderData) {
|
||||||
|
var errorDetail = Array.isArray(orderData.details) && orderData.details[0];
|
||||||
|
|
||||||
|
if (errorDetail && errorDetail.issue === 'INSTRUMENT_DECLINED') {
|
||||||
|
return actions.restart(); // Recoverable state, per:
|
||||||
|
// https://developer.paypal.com/docs/checkout/integration-features/funding-failure/
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorDetail) {
|
||||||
|
var msg = 'Sorry, your transaction could not be processed.';
|
||||||
|
if (errorDetail.description) msg += '\n\n' + errorDetail.description;
|
||||||
|
if (orderData.debug_id) msg += ' (' + orderData.debug_id + ')';
|
||||||
|
// Show a failure message
|
||||||
|
return actions.redirect(orderData.redirect_urls['cancel_url'])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Successful capture!
|
||||||
|
actions.redirect(orderData.redirect_urls['return_url'])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).render('#paypal-button-container');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// RETURNED DATA
|
||||||
|
// {
|
||||||
|
// "id": "1WM83684A69628456",
|
||||||
|
// "status": "COMPLETED",
|
||||||
|
// "purchase_units": [
|
||||||
|
// {
|
||||||
|
// "reference_id": "default",
|
||||||
|
// "shipping": {
|
||||||
|
// "name": {
|
||||||
|
// "full_name": "John Doe"
|
||||||
|
// },
|
||||||
|
// "address": {
|
||||||
|
// "address_line_1": "1 Main St",
|
||||||
|
// "admin_area_2": "San Jose",
|
||||||
|
// "admin_area_1": "CA",
|
||||||
|
// "postal_code": "95131",
|
||||||
|
// "country_code": "US"
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// "payments": {
|
||||||
|
// "captures": [
|
||||||
|
// {
|
||||||
|
// "id": "9AU23265T5630860D",
|
||||||
|
// "status": "COMPLETED",
|
||||||
|
// "amount": {
|
||||||
|
// "currency_code": "USD",
|
||||||
|
// "value": "13.40"
|
||||||
|
// },
|
||||||
|
// "final_capture": true,
|
||||||
|
// "seller_protection": {
|
||||||
|
// "status": "ELIGIBLE",
|
||||||
|
// "dispute_categories": [
|
||||||
|
// "ITEM_NOT_RECEIVED",
|
||||||
|
// "UNAUTHORIZED_TRANSACTION"
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
// "seller_receivable_breakdown": {
|
||||||
|
// "gross_amount": {
|
||||||
|
// "currency_code": "USD",
|
||||||
|
// "value": "13.40"
|
||||||
|
// },
|
||||||
|
// "paypal_fee": {
|
||||||
|
// "currency_code": "USD",
|
||||||
|
// "value": "0.96"
|
||||||
|
// },
|
||||||
|
// "net_amount": {
|
||||||
|
// "currency_code": "USD",
|
||||||
|
// "value": "12.44"
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// "links": [
|
||||||
|
// {
|
||||||
|
// "href": "https://api.sandbox.paypal.com/v2/payments/captures/9AU23265T5630860D",
|
||||||
|
// "rel": "self",
|
||||||
|
// "method": "GET"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "href": "https://api.sandbox.paypal.com/v2/payments/captures/9AU23265T5630860D/refund",
|
||||||
|
// "rel": "refund",
|
||||||
|
// "method": "POST"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "href": "https://api.sandbox.paypal.com/v2/checkout/orders/1WM83684A69628456",
|
||||||
|
// "rel": "up",
|
||||||
|
// "method": "GET"
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
// "create_time": "2022-02-28T15:33:07Z",
|
||||||
|
// "update_time": "2022-02-28T15:33:07Z"
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
// "payer": {
|
||||||
|
// "name": {
|
||||||
|
// "given_name": "John",
|
||||||
|
// "surname": "Doe"
|
||||||
|
// },
|
||||||
|
// "email_address": "sb-rlst914027742@personal.example.com",
|
||||||
|
// "payer_id": "G9RGHQ72CGKF6",
|
||||||
|
// "address": {
|
||||||
|
// "country_code": "US"
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// "links": [
|
||||||
|
// {
|
||||||
|
// "href": "https://api.sandbox.paypal.com/v2/checkout/orders/1WM83684A69628456",
|
||||||
|
// "rel": "self",
|
||||||
|
// "method": "GET"
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
1944
src/static/scripts/stimulus.js
Normal file
317
src/static/styles/dashboard.css
Normal file
@ -0,0 +1,317 @@
|
|||||||
|
:root {
|
||||||
|
--fg-color: #333;
|
||||||
|
--bg-color: #eff5f8;
|
||||||
|
--bg-alt-color: #bdc8d2;
|
||||||
|
--gray-color: #9d9d9d;
|
||||||
|
--yellow-color: #f8a911;
|
||||||
|
--yellow-alt-color: #f6c463;
|
||||||
|
--green-color: #13ce65;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-size: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--bg-color);
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.75;
|
||||||
|
color: var(--fg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--fg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5 {
|
||||||
|
margin: 0;
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2.488rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 2.074rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.728rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: 1.44rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 0.833rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
input[type=number] {
|
||||||
|
max-width: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.action-button,
|
||||||
|
input[type=submit],
|
||||||
|
button {
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--fg-color);
|
||||||
|
text-decoration: none;
|
||||||
|
background-color: var(--yellow-color);
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button:hover,
|
||||||
|
input[type=submit]:hover,
|
||||||
|
button:hover {
|
||||||
|
background-color: var(--yellow-alt-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
figure {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
main {
|
||||||
|
overflow: scroll;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
main article {
|
||||||
|
margin: 2rem 2rem 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.store__info {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 2rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store__info div {
|
||||||
|
background-color: white;
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store__info div h3 {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store__action {
|
||||||
|
background-color: white;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: block;
|
||||||
|
/*font-size: 1.2rem;*/
|
||||||
|
font-weight: bold;
|
||||||
|
text-decoration: none;
|
||||||
|
font-family: "Inter";
|
||||||
|
}
|
||||||
|
|
||||||
|
.store__action:hover {
|
||||||
|
background-color: var(--bg-alt-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard__main {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 7fr;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard__sidebar {
|
||||||
|
height: 100vh;
|
||||||
|
background-color: var(--bg-alt-color);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard__nav {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard__user {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard__user a,
|
||||||
|
.dashboard__nav a {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard__nav a img {
|
||||||
|
width: 24px;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard__user a:hover,
|
||||||
|
.dashboard__nav a:hover {
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.object__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.object__list,
|
||||||
|
.object__panel {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.object__item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 0.05rem solid var(--gray-color);
|
||||||
|
text-decoration: none;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel__item:last-child,
|
||||||
|
.object__item:last-child {
|
||||||
|
border-bottom: unset;
|
||||||
|
border-radius: 0 0 0.5rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.object__item:hover {
|
||||||
|
background-color: var(--bg-alt-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.object__item--header {
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: var(--bg-alt-color);
|
||||||
|
border-radius: 0.5rem 0.5rem 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel__item {
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 0.05rem solid var(--gray-color);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.product__detail {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 2fr;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product__image {
|
||||||
|
max-height: 200px;
|
||||||
|
border: 0.02rem solid var(--gray-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.order__fulfill {
|
||||||
|
grid-column: 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order__status {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order__status--display {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order__status--draft {
|
||||||
|
background-color: var(--gray-color);
|
||||||
|
}
|
||||||
|
.order__status--unfulfilled {
|
||||||
|
background-color: var(--yellow-alt-color);
|
||||||
|
}
|
||||||
|
.order__status--partially_returned {
|
||||||
|
background-color: var(--gray-color);
|
||||||
|
}
|
||||||
|
.order__status--partially_fulfilled {
|
||||||
|
background-color: var(--gray-color);
|
||||||
|
}
|
||||||
|
.order__status--returned {
|
||||||
|
background-color: var(--gray-color);
|
||||||
|
}
|
||||||
|
.order__status--fulfilled {
|
||||||
|
background-color: var(--green-color);
|
||||||
|
}
|
||||||
|
.order__status--cancelled {
|
||||||
|
background-color: var(--gray-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status__dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
min-width: 8px;
|
||||||
|
min-height: 8px;
|
||||||
|
border-radius: 100%;
|
||||||
|
margin-right: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.item__figure {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item__figure img {
|
||||||
|
height: 50px;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
424
src/static/styles/main.css
Normal file
@ -0,0 +1,424 @@
|
|||||||
|
:root {
|
||||||
|
--fg-color: #333;
|
||||||
|
--bg-color: #f9f9f9;
|
||||||
|
--gray-color: #9d9d9d;
|
||||||
|
--yellow-color: #f8a911;
|
||||||
|
--yellow-alt-color: #ffce6f;
|
||||||
|
|
||||||
|
--default-border: 2px solid var(--gray-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-size: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--bg-color);
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
max-width: 1024px;
|
||||||
|
padding: 1rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
line-height: 1.75;
|
||||||
|
color: var(--fg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--fg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5 {
|
||||||
|
margin: 0;
|
||||||
|
font-family: 'Eczar', serif;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
font-size: 2.488rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 2.074rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.728rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: 1.44rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
small, .text_small {
|
||||||
|
font-size: 0.833rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
optgroup,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
margin: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
label {
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
text-align: left;
|
||||||
|
border: var(--default-border);
|
||||||
|
padding: 0.5em;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=text],
|
||||||
|
input[type=email],
|
||||||
|
input[type=number],
|
||||||
|
input[type=password],
|
||||||
|
select[multiple=multiple],
|
||||||
|
textarea {
|
||||||
|
display: block;
|
||||||
|
text-align: left;
|
||||||
|
color: var(--fg-color);
|
||||||
|
border: var(--default-border);
|
||||||
|
padding: 0.5rem;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus,
|
||||||
|
textarea:focus {
|
||||||
|
border-color: var(--yellow-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
select[multiple=multiple] {
|
||||||
|
height: 125px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
input[type=checkbox] {
|
||||||
|
width: 1em;
|
||||||
|
vertical-align: text-top;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 6.25rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-input-placeholder {
|
||||||
|
font-style: oblique;
|
||||||
|
}
|
||||||
|
::-moz-input-placeholder {
|
||||||
|
font-style: oblique;
|
||||||
|
}
|
||||||
|
::-ms-input-placeholder {
|
||||||
|
font-style: oblique;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.action-button,
|
||||||
|
input[type=submit],
|
||||||
|
button {
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--fg-color);
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: lowercase;
|
||||||
|
font-variant: small-caps;
|
||||||
|
background-color: var(--yellow-color);
|
||||||
|
padding: 0.25rem 1rem;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button:hover {
|
||||||
|
background-color: var(--yellow-alt-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button--large {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
form input,
|
||||||
|
form select {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
figure {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product__item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 0 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product__list-item button {
|
||||||
|
grid-column: 1/3;
|
||||||
|
align-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.product__image {
|
||||||
|
/*object-fit: cover;*/
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product__form {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 3fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product__form input[type=submit] {
|
||||||
|
grid-column: 2;
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site__logo {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site__header div,
|
||||||
|
.site__header nav {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a {
|
||||||
|
text-transform: lowercase;
|
||||||
|
font-variant: small-caps;
|
||||||
|
font-weight: bold;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site__copyright {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keep_calm {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 2fr;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
.keep_calm__img {
|
||||||
|
max-width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.site__cart {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--yellow-color);
|
||||||
|
padding: 0.25rem 1rem;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
color: var(--fg-color);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.cart__icon {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart__length {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: 'Eczar';
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart__item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 5fr;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 2rem 0;
|
||||||
|
border-bottom: 0.05rem solid var(--gray-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart__total_price {
|
||||||
|
text-align: right;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
.cart__total {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item__figure img {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.product__list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product__list-item {
|
||||||
|
text-decoration: none;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product__list-item form {
|
||||||
|
grid-column: 1/3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product__list-item figure {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.order__shipping {
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.order__total {
|
||||||
|
margin: 3rem 0;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order__details {
|
||||||
|
/*margin: 3rem 0;*/
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
margin: 4rem 0 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-top: var(--default-border);
|
||||||
|
padding: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.object__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.object__list,
|
||||||
|
.object__panel {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.object__item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 0.05rem solid var(--gray-color);
|
||||||
|
text-decoration: none;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel__item:last-child,
|
||||||
|
.object__item:last-child {
|
||||||
|
border-bottom: unset;
|
||||||
|
border-radius: 0 0 0.5rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.object__item:hover {
|
||||||
|
background-color: var(--bg-alt-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.object__item--header {
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: var(--bg-alt-color);
|
||||||
|
border-radius: 0.5rem 0.5rem 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel__item {
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 0.05rem solid var(--gray-color);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.product__detail {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 2fr;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.user__emails {
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
._form_1 div {
|
||||||
|
text-align: left !important;
|
||||||
|
}
|
||||||
|
._form_1 div form {
|
||||||
|
margin: 1rem 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
0
src/static/styles/storefront.css
Normal file
0
src/storefront/__init__.py
Normal file
3
src/storefront/admin.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
6
src/storefront/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class StorefrontConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'storefront'
|
||||||
139
src/storefront/cart.py
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import logging
|
||||||
|
from decimal import Decimal
|
||||||
|
from django.conf import settings
|
||||||
|
from core.models import Product, OrderLine
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class Cart:
|
||||||
|
def __init__(self, request):
|
||||||
|
self.session = request.session
|
||||||
|
cart = self.session.get(settings.CART_SESSION_ID)
|
||||||
|
if not cart:
|
||||||
|
cart = self.session[settings.CART_SESSION_ID] = {}
|
||||||
|
self.cart = cart
|
||||||
|
|
||||||
|
def add(self, product, quantity=1, roast='', other='', customer_note='', update_quantity=False):
|
||||||
|
product_id = str(product.id)
|
||||||
|
if product_id not in self.cart:
|
||||||
|
self.cart[product_id] = {
|
||||||
|
'quantity': 0,
|
||||||
|
'roast': roast,
|
||||||
|
'other': other,
|
||||||
|
'customer_note': customer_note,
|
||||||
|
'price': str(product.price)
|
||||||
|
}
|
||||||
|
if update_quantity:
|
||||||
|
self.cart[product_id]['quantity'] = quantity
|
||||||
|
else:
|
||||||
|
self.cart[product_id]['quantity'] += quantity
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
self.session[settings.CART_SESSION_ID] = self.cart
|
||||||
|
self.session.modified = True
|
||||||
|
logger.info(f'\nCart:\n{self.cart}\n\n')
|
||||||
|
|
||||||
|
def remove(self, product):
|
||||||
|
product_id = str(product.id)
|
||||||
|
if product_id in self.cart:
|
||||||
|
del self.cart[product_id]
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
product_ids = self.cart.keys()
|
||||||
|
products = Product.objects.filter(id__in=product_ids)
|
||||||
|
for product in products:
|
||||||
|
self.cart[str(product.id)]['product'] = product
|
||||||
|
|
||||||
|
for item in self.cart.values():
|
||||||
|
item['price'] = Decimal(item['price'])
|
||||||
|
item['total_price'] = item['price'] * item['quantity']
|
||||||
|
yield item
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return sum(item['quantity'] for item in self.cart.values())
|
||||||
|
|
||||||
|
def get_total_price(self):
|
||||||
|
return sum(Decimal(item['price']) * item['quantity'] for item in self.cart.values())
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
del self.session[settings.CART_SESSION_ID]
|
||||||
|
self.session.modified = True
|
||||||
|
|
||||||
|
def get_bulk_list_and_body_data(self, order, shipping_address=None):
|
||||||
|
bulk_list = []
|
||||||
|
|
||||||
|
body_data = {
|
||||||
|
'intent': 'CAPTURE',
|
||||||
|
'purchase_units': [{
|
||||||
|
'amount': {
|
||||||
|
'currency_code': 'USD',
|
||||||
|
'value': f'{self.get_total_price()}',
|
||||||
|
'breakdown': {
|
||||||
|
# Required when including the `items` array
|
||||||
|
'item_total': {
|
||||||
|
'currency_code': 'USD',
|
||||||
|
'value': f'{self.get_total_price()}'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'items': []
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
if shipping_address:
|
||||||
|
body_data['purchase_units'][0]['shipping'] = self.process_shipping_address(shipping_address)
|
||||||
|
|
||||||
|
for item in self:
|
||||||
|
body_data['purchase_units'][0]['items'].append({
|
||||||
|
# Shows within upper-right dropdown during payment approval
|
||||||
|
'name': f'{item["product"]}',
|
||||||
|
# Item details will also be in the completed paypal.com transaction view
|
||||||
|
'description': 'Coffee',
|
||||||
|
'unit_amount': {
|
||||||
|
'currency_code': 'USD',
|
||||||
|
'value': f'{item["price"]}'
|
||||||
|
},
|
||||||
|
'quantity': f'{item["quantity"]}'
|
||||||
|
})
|
||||||
|
|
||||||
|
bulk_list.append(
|
||||||
|
OrderLine(
|
||||||
|
order=order,
|
||||||
|
product=item['product'],
|
||||||
|
customer_note=item['customer_note'],
|
||||||
|
unit_price=item['price'],
|
||||||
|
quantity=item['quantity'],
|
||||||
|
tax_rate=2,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return bulk_list, body_data
|
||||||
|
|
||||||
|
def process_shipping_address(self, address):
|
||||||
|
shipping = {
|
||||||
|
'address': {
|
||||||
|
'address_line_1': f'{address["street_address_1"]}',
|
||||||
|
'address_line_2': f'{address["street_address_2"]}',
|
||||||
|
'admin_area_2': f'{address["city"]}',
|
||||||
|
'admin_area_1': f'{address["state"]}',
|
||||||
|
'postal_code': f'{address["postal_code"]}',
|
||||||
|
'country_code': 'US'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return shipping
|
||||||
|
|
||||||
|
# @property
|
||||||
|
# def coupon(self):
|
||||||
|
# if self.coupon_id:
|
||||||
|
# return Coupon.objects.get(id=self.coupon_id)
|
||||||
|
# return None
|
||||||
|
|
||||||
|
# def get_discount(self):
|
||||||
|
# if self.coupon:
|
||||||
|
# return (self.coupon.discount / Decimal('100')) * self.get_total_price()
|
||||||
|
# return Decimal('0')
|
||||||
|
|
||||||
|
# def get_total_price_after_discount(self):
|
||||||
|
# return self.get_total_price() - self.get_discount()
|
||||||
6
src/storefront/context_processors.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from .cart import Cart
|
||||||
|
|
||||||
|
def cart(request):
|
||||||
|
return {
|
||||||
|
'cart': Cart(request)
|
||||||
|
}
|
||||||
61
src/storefront/forms.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import logging
|
||||||
|
from django import forms
|
||||||
|
from django.core.mail import EmailMessage
|
||||||
|
|
||||||
|
from core.models import Order
|
||||||
|
from accounts import STATE_CHOICES
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class AddToCartForm(forms.Form):
|
||||||
|
WHOLE = 'WHOLE'
|
||||||
|
ESPRESSO = 'ESPRESSO'
|
||||||
|
CONE_DRIP = 'CONE_DRIP'
|
||||||
|
BASKET_DRIP = 'BASKET_DRIP'
|
||||||
|
FRENCH_PRESS = 'FRENCH_PRESS'
|
||||||
|
STOVETOP_ESPRESSO = 'STOVETOP_ESPRESSO'
|
||||||
|
AEROPRESS = 'AEROPRESS'
|
||||||
|
PERCOLATOR = 'PERCOLATOR'
|
||||||
|
OTHER = 'OTHER'
|
||||||
|
ROAST_CHOICES = [
|
||||||
|
(WHOLE, 'Whole Beans'),
|
||||||
|
(ESPRESSO, 'Espresso'),
|
||||||
|
(CONE_DRIP, 'Cone Drip'),
|
||||||
|
(BASKET_DRIP, 'Basket Drip'),
|
||||||
|
(FRENCH_PRESS, 'French Press'),
|
||||||
|
(STOVETOP_ESPRESSO, 'Stovetop Espresso (Moka Pot)'),
|
||||||
|
(AEROPRESS, 'AeroPress'),
|
||||||
|
(PERCOLATOR, 'Percolator'),
|
||||||
|
(OTHER, 'Other (enter below)')
|
||||||
|
]
|
||||||
|
quantity = forms.IntegerField(min_value=1, initial=1)
|
||||||
|
roast = forms.ChoiceField(choices=ROAST_CHOICES)
|
||||||
|
other = forms.CharField(required=False)
|
||||||
|
update = forms.BooleanField(required=False, initial=False, widget=forms.HiddenInput)
|
||||||
|
|
||||||
|
|
||||||
|
class AddressForm(forms.Form):
|
||||||
|
first_name = forms.CharField()
|
||||||
|
last_name = forms.CharField()
|
||||||
|
email = forms.EmailField()
|
||||||
|
street_address_1 = forms.CharField()
|
||||||
|
street_address_2 = forms.CharField(required=False)
|
||||||
|
city = forms.CharField()
|
||||||
|
state = forms.ChoiceField(
|
||||||
|
choices=STATE_CHOICES
|
||||||
|
)
|
||||||
|
postal_code = forms.CharField()
|
||||||
|
|
||||||
|
class OrderCreateForm(forms.ModelForm):
|
||||||
|
email = forms.CharField(widget=forms.HiddenInput())
|
||||||
|
first_name = forms.CharField(widget=forms.HiddenInput())
|
||||||
|
last_name = forms.CharField(widget=forms.HiddenInput())
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Order
|
||||||
|
fields = (
|
||||||
|
'total_net_amount',
|
||||||
|
)
|
||||||
|
widgets = {
|
||||||
|
'total_net_amount': forms.HiddenInput()
|
||||||
|
}
|
||||||