Initial commit

This commit is contained in:
Nathan Chapman 2022-03-10 20:24:46 -07:00
commit 7df5e92720
135 changed files with 8180 additions and 0 deletions

37
.editorconfig Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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',
]

View 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()),
],
),
]

View File

32
src/accounts/models.py Normal file
View 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
View 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']}")

View 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 %}

View 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 %}

View 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 %}

View 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
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

11
src/accounts/urls.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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 cant believe its 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. Its 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": "Dantes 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
View 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__'

View 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',),
},
),
]

View File

221
src/core/models.py Normal file
View 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
View 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
View 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
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
src/core/views.py Normal file
View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

40
src/core/weight.py Normal file
View 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

View File

3
src/dashboard/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
src/dashboard/apps.py Normal file
View 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
View 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
)

View File

3
src/dashboard/models.py Normal file
View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

View 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 %}

View 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 %}

View 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 %}

View 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 &rarr;</a>
</section>
</article>
{% endblock %}

View 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 &rarr;</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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

29
src/dashboard/urls.py Normal file
View 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
View 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
View 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()

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

3
src/ptcoffee/__init__.py Normal file
View File

@ -0,0 +1,3 @@
from .celery import app as celery_app
__all__ = ('celery_app',)

11
src/ptcoffee/asgi.py Normal file
View 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
View 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
View 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', '')

View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 546 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 B

BIN
src/static/images/cubes.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 698 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 673 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

BIN
src/static/images/gear.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 673 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 697 B

View 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() {
}
}

View 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)
}
}

View 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)
}
}

View 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)

View File

@ -0,0 +1,4 @@
import { setCookie } from '../lib/cookie.js'
const { timeZone } = new Intl.DateTimeFormat().resolvedOptions()
setCookie('timezone', timeZone)

View 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"
// }
// ]
// }

File diff suppressed because it is too large Load Diff

View 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
View 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;
}

View File

View File

3
src/storefront/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
src/storefront/apps.py Normal file
View 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
View 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()

View File

@ -0,0 +1,6 @@
from .cart import Cart
def cart(request):
return {
'cart': Cart(request)
}

61
src/storefront/forms.py Normal file
View 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()
}

Some files were not shown because too many files have changed in this diff Show More