Merge branch 'release/0.6.0'
This commit is contained in:
commit
d6eeb50fa1
1
Pipfile
1
Pipfile
@ -20,6 +20,7 @@ paypal-checkout-serversdk = "*"
|
||||
Pillow = "*"
|
||||
redis = "*"
|
||||
psycopg2 = "*"
|
||||
usps-api = "*"
|
||||
|
||||
[dev-packages]
|
||||
django-debug-toolbar = "*"
|
||||
|
||||
200
Pipfile.lock
generated
200
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "f7ca5d3f9e367e3324d6fd9af8c1022cff36b1a999eb7afa232ac1af8404c62c"
|
||||
"sha256": "0087f9e4fd44233bc6329f7a844a96ae4001ee9d173323e7a715c7295844163c"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@ -32,6 +32,14 @@
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==3.5.0"
|
||||
},
|
||||
"async-timeout": {
|
||||
"hashes": [
|
||||
"sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15",
|
||||
"sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==4.0.2"
|
||||
},
|
||||
"billiard": {
|
||||
"hashes": [
|
||||
"sha256:299de5a8da28a783d51b197d496bef4f1595dd023a93a4f59dde1886ae905547",
|
||||
@ -44,11 +52,11 @@
|
||||
"redis"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:8aacd02fc23a02760686d63dde1eb0daa9f594e735e73ea8fb15c2ff15cb608c",
|
||||
"sha256:e2cd41667ad97d4f6a2f4672d1c6a6ebada194c619253058b5f23704aaadaa82"
|
||||
"sha256:d1398cadf30f576266b34370e28e880306ec55f7a4b6307549b0ae9c15663481",
|
||||
"sha256:da31f8eae7607b1582e5ee2d3f2d6f58450585afd23379491e3d9229d08102d0"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==5.2.3"
|
||||
"version": "==5.2.6"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
@ -201,10 +209,10 @@
|
||||
},
|
||||
"django-allauth": {
|
||||
"hashes": [
|
||||
"sha256:f5fbb67376177c6a9276516dde98bcb01ac4160a5a27f7b340914dd521d04f12"
|
||||
"sha256:ee3a174e249771caeb1d037e64b2704dd3c56cfec44f2058fae2214b224d35e8"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.49.0"
|
||||
"version": "==0.50.0"
|
||||
},
|
||||
"django-anymail": {
|
||||
"extras": [
|
||||
@ -321,6 +329,73 @@
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==5.2.4"
|
||||
},
|
||||
"lxml": {
|
||||
"hashes": [
|
||||
"sha256:078306d19a33920004addeb5f4630781aaeabb6a8d01398045fcde085091a169",
|
||||
"sha256:0c1978ff1fd81ed9dcbba4f91cf09faf1f8082c9d72eb122e92294716c605428",
|
||||
"sha256:1010042bfcac2b2dc6098260a2ed022968dbdfaf285fc65a3acf8e4eb1ffd1bc",
|
||||
"sha256:1d650812b52d98679ed6c6b3b55cbb8fe5a5460a0aef29aeb08dc0b44577df85",
|
||||
"sha256:20b8a746a026017acf07da39fdb10aa80ad9877046c9182442bf80c84a1c4696",
|
||||
"sha256:2403a6d6fb61c285969b71f4a3527873fe93fd0abe0832d858a17fe68c8fa507",
|
||||
"sha256:24f5c5ae618395ed871b3d8ebfcbb36e3f1091fd847bf54c4de623f9107942f3",
|
||||
"sha256:28d1af847786f68bec57961f31221125c29d6f52d9187c01cd34dc14e2b29430",
|
||||
"sha256:31499847fc5f73ee17dbe1b8e24c6dafc4e8d5b48803d17d22988976b0171f03",
|
||||
"sha256:31ba2cbc64516dcdd6c24418daa7abff989ddf3ba6d3ea6f6ce6f2ed6e754ec9",
|
||||
"sha256:330bff92c26d4aee79c5bc4d9967858bdbe73fdbdbacb5daf623a03a914fe05b",
|
||||
"sha256:5045ee1ccd45a89c4daec1160217d363fcd23811e26734688007c26f28c9e9e7",
|
||||
"sha256:52cbf2ff155b19dc4d4100f7442f6a697938bf4493f8d3b0c51d45568d5666b5",
|
||||
"sha256:530f278849031b0eb12f46cca0e5db01cfe5177ab13bd6878c6e739319bae654",
|
||||
"sha256:545bd39c9481f2e3f2727c78c169425efbfb3fbba6e7db4f46a80ebb249819ca",
|
||||
"sha256:5804e04feb4e61babf3911c2a974a5b86f66ee227cc5006230b00ac6d285b3a9",
|
||||
"sha256:5a58d0b12f5053e270510bf12f753a76aaf3d74c453c00942ed7d2c804ca845c",
|
||||
"sha256:5f148b0c6133fb928503cfcdfdba395010f997aa44bcf6474fcdd0c5398d9b63",
|
||||
"sha256:5f7d7d9afc7b293147e2d506a4596641d60181a35279ef3aa5778d0d9d9123fe",
|
||||
"sha256:60d2f60bd5a2a979df28ab309352cdcf8181bda0cca4529769a945f09aba06f9",
|
||||
"sha256:6259b511b0f2527e6d55ad87acc1c07b3cbffc3d5e050d7e7bcfa151b8202df9",
|
||||
"sha256:6268e27873a3d191849204d00d03f65c0e343b3bcb518a6eaae05677c95621d1",
|
||||
"sha256:627e79894770783c129cc5e89b947e52aa26e8e0557c7e205368a809da4b7939",
|
||||
"sha256:62f93eac69ec0f4be98d1b96f4d6b964855b8255c345c17ff12c20b93f247b68",
|
||||
"sha256:6d6483b1229470e1d8835e52e0ff3c6973b9b97b24cd1c116dca90b57a2cc613",
|
||||
"sha256:6f7b82934c08e28a2d537d870293236b1000d94d0b4583825ab9649aef7ddf63",
|
||||
"sha256:6fe4ef4402df0250b75ba876c3795510d782def5c1e63890bde02d622570d39e",
|
||||
"sha256:719544565c2937c21a6f76d520e6e52b726d132815adb3447ccffbe9f44203c4",
|
||||
"sha256:730766072fd5dcb219dd2b95c4c49752a54f00157f322bc6d71f7d2a31fecd79",
|
||||
"sha256:74eb65ec61e3c7c019d7169387d1b6ffcfea1b9ec5894d116a9a903636e4a0b1",
|
||||
"sha256:7993232bd4044392c47779a3c7e8889fea6883be46281d45a81451acfd704d7e",
|
||||
"sha256:80bbaddf2baab7e6de4bc47405e34948e694a9efe0861c61cdc23aa774fcb141",
|
||||
"sha256:86545e351e879d0b72b620db6a3b96346921fa87b3d366d6c074e5a9a0b8dadb",
|
||||
"sha256:891dc8f522d7059ff0024cd3ae79fd224752676447f9c678f2a5c14b84d9a939",
|
||||
"sha256:8a31f24e2a0b6317f33aafbb2f0895c0bce772980ae60c2c640d82caac49628a",
|
||||
"sha256:8b99ec73073b37f9ebe8caf399001848fced9c08064effdbfc4da2b5a8d07b93",
|
||||
"sha256:986b7a96228c9b4942ec420eff37556c5777bfba6758edcb95421e4a614b57f9",
|
||||
"sha256:a1547ff4b8a833511eeaceacbcd17b043214fcdb385148f9c1bc5556ca9623e2",
|
||||
"sha256:a2bfc7e2a0601b475477c954bf167dee6d0f55cb167e3f3e7cefad906e7759f6",
|
||||
"sha256:a3c5f1a719aa11866ffc530d54ad965063a8cbbecae6515acbd5f0fae8f48eaa",
|
||||
"sha256:a9f1c3489736ff8e1c7652e9dc39f80cff820f23624f23d9eab6e122ac99b150",
|
||||
"sha256:aa0cf4922da7a3c905d000b35065df6184c0dc1d866dd3b86fd961905bbad2ea",
|
||||
"sha256:ad4332a532e2d5acb231a2e5d33f943750091ee435daffca3fec0a53224e7e33",
|
||||
"sha256:b2582b238e1658c4061ebe1b4df53c435190d22457642377fd0cb30685cdfb76",
|
||||
"sha256:b6fc2e2fb6f532cf48b5fed57567ef286addcef38c28874458a41b7837a57807",
|
||||
"sha256:b92d40121dcbd74831b690a75533da703750f7041b4bf951befc657c37e5695a",
|
||||
"sha256:bbab6faf6568484707acc052f4dfc3802bdb0cafe079383fbaa23f1cdae9ecd4",
|
||||
"sha256:c0b88ed1ae66777a798dc54f627e32d3b81c8009967c63993c450ee4cbcbec15",
|
||||
"sha256:ce13d6291a5f47c1c8dbd375baa78551053bc6b5e5c0e9bb8e39c0a8359fd52f",
|
||||
"sha256:db3535733f59e5605a88a706824dfcb9bd06725e709ecb017e165fc1d6e7d429",
|
||||
"sha256:dd10383f1d6b7edf247d0960a3db274c07e96cf3a3fc7c41c8448f93eac3fb1c",
|
||||
"sha256:e01f9531ba5420838c801c21c1b0f45dbc9607cb22ea2cf132844453bec863a5",
|
||||
"sha256:e11527dc23d5ef44d76fef11213215c34f36af1608074561fcc561d983aeb870",
|
||||
"sha256:e1ab2fac607842ac36864e358c42feb0960ae62c34aa4caaf12ada0a1fb5d99b",
|
||||
"sha256:e1fd7d2fe11f1cb63d3336d147c852f6d07de0d0020d704c6031b46a30b02ca8",
|
||||
"sha256:e9f84ed9f4d50b74fbc77298ee5c870f67cb7e91dcdc1a6915cb1ff6a317476c",
|
||||
"sha256:ec4b4e75fc68da9dc0ed73dcdb431c25c57775383fec325d23a770a64e7ebc87",
|
||||
"sha256:f10ce66fcdeb3543df51d423ede7e238be98412232fca5daec3e54bcd16b8da0",
|
||||
"sha256:f63f62fc60e6228a4ca9abae28228f35e1bd3ce675013d1dfb828688d50c6e23",
|
||||
"sha256:fa56bb08b3dd8eac3a8c5b7d075c94e74f755fd9d8a04543ae8d37b1612dd170",
|
||||
"sha256:fa9b7c450be85bfc6cd39f6df8c5b8cbd76b5d6fc1f69efec80203f9894b885f"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==4.8.0"
|
||||
},
|
||||
"measurement": {
|
||||
"hashes": [
|
||||
"sha256:352b20f7f0e553236af7c5ed48d091a51cf26061c1a063f46b31706ff7c0d57a"
|
||||
@ -368,52 +443,55 @@
|
||||
},
|
||||
"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"
|
||||
"sha256:01ce45deec9df310cbbee11104bae1a2a43308dd9c317f99235b6d3080ddd66e",
|
||||
"sha256:0c51cb9edac8a5abd069fd0758ac0a8bfe52c261ee0e330f363548aca6893595",
|
||||
"sha256:17869489de2fce6c36690a0c721bd3db176194af5f39249c1ac56d0bb0fcc512",
|
||||
"sha256:21dee8466b42912335151d24c1665fcf44dc2ee47e021d233a40c3ca5adae59c",
|
||||
"sha256:25023a6209a4d7c42154073144608c9a71d3512b648a2f5d4465182cb93d3477",
|
||||
"sha256:255c9d69754a4c90b0ee484967fc8818c7ff8311c6dddcc43a4340e10cd1636a",
|
||||
"sha256:35be4a9f65441d9982240e6966c1eaa1c654c4e5e931eaf580130409e31804d4",
|
||||
"sha256:3f42364485bfdab19c1373b5cd62f7c5ab7cc052e19644862ec8f15bb8af289e",
|
||||
"sha256:3fddcdb619ba04491e8f771636583a7cc5a5051cd193ff1aa1ee8616d2a692c5",
|
||||
"sha256:463acf531f5d0925ca55904fa668bb3461c3ef6bc779e1d6d8a488092bdee378",
|
||||
"sha256:4fe29a070de394e449fd88ebe1624d1e2d7ddeed4c12e0b31624561b58948d9a",
|
||||
"sha256:55dd1cf09a1fd7c7b78425967aacae9b0d70125f7d3ab973fadc7b5abc3de652",
|
||||
"sha256:5a3ecc026ea0e14d0ad7cd990ea7f48bfcb3eb4271034657dc9d06933c6629a7",
|
||||
"sha256:5cfca31ab4c13552a0f354c87fbd7f162a4fafd25e6b521bba93a57fe6a3700a",
|
||||
"sha256:66822d01e82506a19407d1afc104c3fcea3b81d5eb11485e593ad6b8492f995a",
|
||||
"sha256:69e5ddc609230d4408277af135c5b5c8fe7a54b2bdb8ad7c5100b86b3aab04c6",
|
||||
"sha256:6b6d4050b208c8ff886fd3db6690bf04f9a48749d78b41b7a5bf24c236ab0165",
|
||||
"sha256:7a053bd4d65a3294b153bdd7724dce864a1d548416a5ef61f6d03bf149205160",
|
||||
"sha256:82283af99c1c3a5ba1da44c67296d5aad19f11c535b551a5ae55328a317ce331",
|
||||
"sha256:8782189c796eff29dbb37dd87afa4ad4d40fc90b2742704f94812851b725964b",
|
||||
"sha256:8d79c6f468215d1a8415aa53d9868a6b40c4682165b8cb62a221b1baa47db458",
|
||||
"sha256:97bda660702a856c2c9e12ec26fc6d187631ddfd896ff685814ab21ef0597033",
|
||||
"sha256:a325ac71914c5c043fa50441b36606e64a10cd262de12f7a179620f579752ff8",
|
||||
"sha256:a336a4f74baf67e26f3acc4d61c913e378e931817cd1e2ef4dfb79d3e051b481",
|
||||
"sha256:a598d8830f6ef5501002ae85c7dbfcd9c27cc4efc02a1989369303ba85573e58",
|
||||
"sha256:a5eaf3b42df2bcda61c53a742ee2c6e63f777d0e085bbc6b2ab7ed57deb13db7",
|
||||
"sha256:aea7ce61328e15943d7b9eaca87e81f7c62ff90f669116f857262e9da4057ba3",
|
||||
"sha256:af79d3fde1fc2e33561166d62e3b63f0cc3e47b5a3a2e5fea40d4917754734ea",
|
||||
"sha256:c24f718f9dd73bb2b31a6201e6db5ea4a61fdd1d1c200f43ee585fc6dcd21b34",
|
||||
"sha256:c5b0ff59785d93b3437c3703e3c64c178aabada51dea2a7f2c5eccf1bcf565a3",
|
||||
"sha256:c7110ec1701b0bf8df569a7592a196c9d07c764a0a74f65471ea56816f10e2c8",
|
||||
"sha256:c870193cce4b76713a2b29be5d8327c8ccbe0d4a49bc22968aa1e680930f5581",
|
||||
"sha256:c9efef876c21788366ea1f50ecb39d5d6f65febe25ad1d4c0b8dff98843ac244",
|
||||
"sha256:de344bcf6e2463bb25179d74d6e7989e375f906bcec8cb86edb8b12acbc7dfef",
|
||||
"sha256:eb1b89b11256b5b6cad5e7593f9061ac4624f7651f7a8eb4dfa37caa1dfaa4d0",
|
||||
"sha256:ed742214068efa95e9844c2d9129e209ed63f61baa4d54dbf4cf8b5e2d30ccf2",
|
||||
"sha256:f401ed2bbb155e1ade150ccc63db1a4f6c1909d3d378f7d1235a44e90d75fb97",
|
||||
"sha256:fb89397013cf302f282f0fc998bb7abf11d49dcff72c8ecb320f76ea6e2c5717"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==9.0.1"
|
||||
"version": "==9.1.0"
|
||||
},
|
||||
"prompt-toolkit": {
|
||||
"hashes": [
|
||||
"sha256:30129d870dcb0b3b6a53efdc9d0a83ea96162ffd28ffe077e94215b233dc670c",
|
||||
"sha256:9f1cd16b1e86c2968f2519d7fb31dd9d669916f515612c269d14e9ed52b51650"
|
||||
"sha256:62291dad495e665fca0bda814e342c69952086afb0f4094d0893d357e5c78752",
|
||||
"sha256:bd640f60e8cecd74f0dc249713d433ace2ddc62b65ee07f96d358e0b152b6ea7"
|
||||
],
|
||||
"markers": "python_full_version >= '3.6.2'",
|
||||
"version": "==3.0.28"
|
||||
"version": "==3.0.29"
|
||||
},
|
||||
"psycopg2": {
|
||||
"hashes": [
|
||||
@ -568,11 +646,11 @@
|
||||
},
|
||||
"redis": {
|
||||
"hashes": [
|
||||
"sha256:04629f8e42be942c4f7d1812f2094568f04c612865ad19ad3ace3005da70631a",
|
||||
"sha256:1d9a0cdf89fdd93f84261733e24f55a7bbd413a9b219fdaf56e3e728ca9a2306"
|
||||
"sha256:0107dc8e98a4f1d1d4aa00100e044287f77121a1e6d2085545c4b7fa94a7a27f",
|
||||
"sha256:4e95f4ec5f49e636efcf20061a5a9110c20852f607cfca6865c07aaa8a739ee2"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.1.4"
|
||||
"version": "==4.2.2"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
@ -615,14 +693,6 @@
|
||||
],
|
||||
"version": "==1.2.0"
|
||||
},
|
||||
"setuptools": {
|
||||
"hashes": [
|
||||
"sha256:22c7348c6d2976a52632c67f7ab0cdf40147db7789f9aed18734643fe9cf3373",
|
||||
"sha256:4ce92f1e1f8f01233ee9952c04f6b81d1e02939d6e1b488428154974a4d0783e"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==59.6.0"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
|
||||
@ -655,6 +725,13 @@
|
||||
"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.9"
|
||||
},
|
||||
"usps-api": {
|
||||
"hashes": [
|
||||
"sha256:20c2f19e02dde3eac01b794d6a4c1bcad03ab681d67a8579d090dbda5e5247a3"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.5"
|
||||
},
|
||||
"vine": {
|
||||
"hashes": [
|
||||
"sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30",
|
||||
@ -739,6 +816,13 @@
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==1.14.0"
|
||||
},
|
||||
"xmltodict": {
|
||||
"hashes": [
|
||||
"sha256:50d8c638ed7ecb88d90561beedbf720c9b4e851a9fa6c47ebd64e99d166d8a21",
|
||||
"sha256:8bbcb45cc982f48b2ca8fe7e7827c5d792f217ecf1792626f808bf41c3b86051"
|
||||
],
|
||||
"version": "==0.12.0"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
@ -951,7 +1035,7 @@
|
||||
"sha256:670a52d3115d0e879e1ac838a4eb999af32f858163e3a704fe4839de2a676070",
|
||||
"sha256:fb2d48e4eab0dfb786a472cd514aaadc71e3445b203bc300bad93daa75d77c1a"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"markers": "python_full_version >= '3.7.0'",
|
||||
"version": "==0.20.0"
|
||||
},
|
||||
"trio-websocket": {
|
||||
@ -975,7 +1059,7 @@
|
||||
"sha256:2218cb57952d90b9fca325c0dcfb08c3bda93e8fd8070b0a17f048e2e47a521b",
|
||||
"sha256:a2e56bfd5c7cd83c1369d83b5feccd6d37798b74872866e62616e0ecf111bda8"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"markers": "python_full_version >= '3.7.0'",
|
||||
"version": "==1.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,15 @@ from .models import Address, User
|
||||
class AddressForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Address
|
||||
fields = '__all__'
|
||||
fields = (
|
||||
'first_name',
|
||||
'last_name',
|
||||
'street_address_1',
|
||||
'street_address_2',
|
||||
'city',
|
||||
'state',
|
||||
'postal_code',
|
||||
)
|
||||
|
||||
|
||||
class AccountCreateForm(UserCreationForm):
|
||||
@ -16,8 +24,24 @@ class AccountCreateForm(UserCreationForm):
|
||||
class AccountUpdateForm(UserChangeForm):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = [
|
||||
fields = (
|
||||
'first_name',
|
||||
'last_name',
|
||||
'email',
|
||||
]
|
||||
'default_shipping_address',
|
||||
'addresses',
|
||||
)
|
||||
|
||||
class CustomerUpdateForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['default_shipping_address'].queryset = kwargs['instance'].addresses
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = (
|
||||
'first_name',
|
||||
'last_name',
|
||||
'email',
|
||||
'default_shipping_address',
|
||||
)
|
||||
|
||||
@ -6,4 +6,9 @@ class CoreConfig(AppConfig):
|
||||
name = 'core'
|
||||
|
||||
def ready(self):
|
||||
from .signals import order_created, transaction_created, order_line_post_save
|
||||
from .signals import (
|
||||
order_created,
|
||||
transaction_created,
|
||||
order_line_post_save,
|
||||
trackingnumber_postsave
|
||||
)
|
||||
|
||||
@ -0,0 +1,36 @@
|
||||
# Generated by Django 4.0.2 on 2022-03-23 16:25
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='shippingmethod',
|
||||
name='price',
|
||||
field=models.DecimalField(decimal_places=2, default=0, max_digits=12),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='order',
|
||||
name='status',
|
||||
field=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),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TrackingNumber',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('tracking_id', models.CharField(max_length=256)),
|
||||
('order', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='tracking_numbers', to='core.order')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Tracking Number',
|
||||
'verbose_name_plural': 'Tracking Numbers',
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,25 @@
|
||||
# Generated by Django 4.0.2 on 2022-03-23 17:04
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0002_shippingmethod_price_alter_order_status_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='trackingnumber',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trackingnumber',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
]
|
||||
19
src/core/migrations/0004_order_coupon.py
Normal file
19
src/core/migrations/0004_order_coupon.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 4.0.2 on 2022-03-23 21:30
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0003_trackingnumber_created_at_trackingnumber_updated_at'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
name='coupon',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to='core.coupon'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,22 @@
|
||||
# Generated by Django 4.0.2 on 2022-03-28 17:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0004_order_coupon'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='product',
|
||||
options={'ordering': ['sorting', 'name']},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='product',
|
||||
name='sorting',
|
||||
field=models.PositiveIntegerField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@ -49,6 +49,7 @@ class Product(models.Model):
|
||||
)
|
||||
|
||||
visible_in_listings = models.BooleanField(default=False)
|
||||
sorting = models.PositiveIntegerField(blank=True, null=True)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
@ -61,6 +62,9 @@ class Product(models.Model):
|
||||
def get_absolute_url(self):
|
||||
return reverse('dashboard:product-detail', kwargs={'pk': self.pk})
|
||||
|
||||
class Meta:
|
||||
ordering = ['sorting', 'name']
|
||||
|
||||
|
||||
class ProductPhoto(models.Model):
|
||||
product = models.ForeignKey(Product, on_delete=models.CASCADE)
|
||||
@ -69,6 +73,12 @@ class ProductPhoto(models.Model):
|
||||
def __str__(self):
|
||||
return self.product.name
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
storage, path = self.image.storage, self.image.path
|
||||
|
||||
super(ProductPhoto, self).delete(*args, **kwargs)
|
||||
storage.delete(path)
|
||||
|
||||
# def save(self, *args, **kwargs):
|
||||
# super().save(*args, **kwargs)
|
||||
|
||||
@ -105,17 +115,31 @@ class Coupon(models.Model):
|
||||
class Meta:
|
||||
ordering = ("code",)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def is_valid(self):
|
||||
today = timezone.localtime(timezone.now()).date()
|
||||
today = timezone.localtime(timezone.now())
|
||||
return True if today >= self.valid_from and today <= self.valid_to else False
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dashboard:coupon-detail', kwargs={'pk': self.pk})
|
||||
|
||||
|
||||
|
||||
|
||||
class ShippingMethod(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
type = models.CharField(max_length=30, choices=ShippingMethodType.CHOICES)
|
||||
price = models.DecimalField(
|
||||
max_digits=settings.DEFAULT_MAX_DIGITS,
|
||||
decimal_places=settings.DEFAULT_DECIMAL_PLACES,
|
||||
default=0,
|
||||
)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dashboard:shipmeth-detail', kwargs={'pk': self.pk})
|
||||
|
||||
|
||||
class OrderManager(models.Manager):
|
||||
@ -167,6 +191,13 @@ class Order(models.Model):
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
|
||||
coupon = models.ForeignKey(
|
||||
Coupon,
|
||||
related_name='orders',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True
|
||||
)
|
||||
|
||||
total_net_amount = models.DecimalField(
|
||||
max_digits=10,
|
||||
decimal_places=2,
|
||||
@ -188,9 +219,23 @@ class Order(models.Model):
|
||||
def get_total_quantity(self):
|
||||
return sum([line.quantity for line in self])
|
||||
|
||||
def get_discount(self):
|
||||
if self.coupon:
|
||||
if self.coupon.discount_value_type == DiscountValueType.FIXED:
|
||||
return self.coupon.discount_value
|
||||
elif self.coupon.discount_value_type == DiscountValueType.PERCENTAGE:
|
||||
return (self.coupon.discount_value / Decimal('100')) * self.total_net_amount
|
||||
return Decimal('0')
|
||||
|
||||
def get_total_price_after_discount(self):
|
||||
return round(self.total_net_amount - self.get_discount(), 2)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dashboard:order-detail', kwargs={'pk': self.pk})
|
||||
|
||||
class Meta:
|
||||
ordering = ('-created_at',)
|
||||
|
||||
|
||||
|
||||
class Transaction(models.Model):
|
||||
@ -251,3 +296,24 @@ class OrderLine(models.Model):
|
||||
@property
|
||||
def quantity_unfulfilled(self):
|
||||
return self.quantity - self.quantity_fulfilled
|
||||
|
||||
|
||||
class TrackingNumber(models.Model):
|
||||
order = models.ForeignKey(
|
||||
Order,
|
||||
related_name="tracking_numbers",
|
||||
editable=False,
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
tracking_id = models.CharField(max_length=256)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True, editable=False)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Tracking Number'
|
||||
verbose_name_plural = 'Tracking Numbers'
|
||||
|
||||
def __str__(self):
|
||||
return self.tracking_id
|
||||
|
||||
|
||||
@ -6,8 +6,11 @@ from django.dispatch import receiver
|
||||
from django.db import models
|
||||
|
||||
from . import OrderStatus, TransactionStatus
|
||||
from .models import Order, OrderLine, Transaction
|
||||
from .tasks import send_order_confirmation_email
|
||||
from .models import Order, OrderLine, Transaction, TrackingNumber
|
||||
from .tasks import (
|
||||
send_order_confirmation_email,
|
||||
send_order_shipped_email
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -34,6 +37,20 @@ def transaction_created(sender, instance, created, **kwargs):
|
||||
instance.confirmation_email_sent = True
|
||||
instance.save()
|
||||
|
||||
@receiver(post_save, sender=TrackingNumber, dispatch_uid="trackingnumber_postsave")
|
||||
def trackingnumber_postsave(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
logger.info("TrackingNumber was created")
|
||||
|
||||
data = {
|
||||
'order_id': instance.order.pk,
|
||||
'email': instance.order.customer.email,
|
||||
'full_name': instance.order.customer.get_full_name(),
|
||||
'tracking_id': instance.tracking_id
|
||||
}
|
||||
send_order_shipped_email.delay(data)
|
||||
|
||||
|
||||
def get_order_status(total_quantity_fulfilled, total_quantity_ordered):
|
||||
if total_quantity_fulfilled >= total_quantity_ordered:
|
||||
return OrderStatus.FULFILLED
|
||||
|
||||
@ -11,6 +11,7 @@ logger = get_task_logger(__name__)
|
||||
|
||||
|
||||
CONFIRM_ORDER_TEMPLATE = 'storefront/order_confirmation'
|
||||
SHIP_ORDER_TEMPLATE = 'storefront/order_shipped'
|
||||
ORDER_CANCEl_TEMPLATE = 'storefront/order_cancel'
|
||||
ORDER_REFUND_TEMPLATE = 'storefront/order_refund'
|
||||
|
||||
@ -24,3 +25,14 @@ def send_order_confirmation_email(order):
|
||||
)
|
||||
|
||||
logger.info(f"Order confirmation email sent to {order['email']}")
|
||||
|
||||
@shared_task(name='send_order_shipped_email')
|
||||
def send_order_shipped_email(data):
|
||||
send_templated_mail(
|
||||
template_name=SHIP_ORDER_TEMPLATE,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
recipient_list=[data['email']],
|
||||
context=data
|
||||
)
|
||||
|
||||
logger.info(f"Order shipped email sent to {data['email']}")
|
||||
|
||||
@ -1,10 +1,33 @@
|
||||
import logging
|
||||
from django import forms
|
||||
|
||||
from core.models import Order, OrderLine, ShippingMethod
|
||||
from core.models import Order, OrderLine, ShippingMethod, TrackingNumber, Coupon, ProductPhoto
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class CouponForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Coupon
|
||||
fields = (
|
||||
'type',
|
||||
'name',
|
||||
'code',
|
||||
'valid_from',
|
||||
'valid_to',
|
||||
'discount_value_type',
|
||||
'discount_value',
|
||||
'products',
|
||||
)
|
||||
widgets = {
|
||||
'valid_from': forms.DateInput(attrs = {
|
||||
'type': 'date'
|
||||
}),
|
||||
'valid_to': forms.DateInput(attrs = {
|
||||
'type': 'date'
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
class OrderLineFulfillForm(forms.ModelForm):
|
||||
# send_shipment_details_to_customer = forms.BooleanField(initial=True)
|
||||
|
||||
@ -25,3 +48,22 @@ OrderLineFormset = forms.inlineformset_factory(
|
||||
Order, OrderLine, form=OrderLineFulfillForm,
|
||||
extra=0, can_delete=False
|
||||
)
|
||||
|
||||
|
||||
class OrderTrackingForm(forms.ModelForm):
|
||||
# send_shipment_details_to_customer = forms.BooleanField(initial=True)
|
||||
|
||||
class Meta:
|
||||
model = TrackingNumber
|
||||
fields = ('tracking_id',)
|
||||
|
||||
OrderTrackingFormset = forms.inlineformset_factory(
|
||||
Order, TrackingNumber, form=OrderTrackingForm,
|
||||
extra=1, can_delete=False
|
||||
)
|
||||
|
||||
|
||||
class ProductPhotoForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = ProductPhoto
|
||||
fields = ('image',)
|
||||
|
||||
36
src/dashboard/templates/dashboard/config.html
Normal file
36
src/dashboard/templates/dashboard/config.html
Normal file
@ -0,0 +1,36 @@
|
||||
{% extends "dashboard.html" %}
|
||||
{% load static %}
|
||||
{% load tz %}
|
||||
|
||||
{% block content %}
|
||||
<article>
|
||||
<header class="object__header">
|
||||
<h1><img src="{% static 'images/gear.png' %}" alt=""> Site configuration</h1>
|
||||
</header>
|
||||
|
||||
<section class="object__panel">
|
||||
<div class="object__item panel__header panel__header--flex">
|
||||
<h4>Shipping methods</h4>
|
||||
<a href="{% url 'dashboard:shipmeth-create' %}" class="action-button order__fulfill">+ New method</a>
|
||||
</div>
|
||||
<div class="panel__item">
|
||||
{% for method in shipping_method_list %}
|
||||
<p>
|
||||
<a href="{% url 'dashboard:shipmeth-detail' method.pk %}">{{method.name}} | {{method.type}} | {{method.price}}</a>
|
||||
</p>
|
||||
{% empty %}
|
||||
<p>No shipping methods yet.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="object__panel">
|
||||
<div class="object__item panel__header panel__header--flex">
|
||||
<h4>Staff</h4>
|
||||
<a href="" class="action-button order__fulfill">+ New staff</a>
|
||||
</div>
|
||||
<div class="panel__item">
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
{% endblock %}
|
||||
19
src/dashboard/templates/dashboard/coupon_confirm_delete.html
Normal file
19
src/dashboard/templates/dashboard/coupon_confirm_delete.html
Normal file
@ -0,0 +1,19 @@
|
||||
{% extends "dashboard.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<article>
|
||||
<header class="object__header">
|
||||
<h1><img src="{% static 'images/coupon.png' %}" alt=""> Coupon</h1>
|
||||
</header>
|
||||
<section class="coupon__detail object__panel">
|
||||
<form method="post" class="panel__item">{% csrf_token %}
|
||||
<p>Are you sure you want to delete "{{ object }}"?</p>
|
||||
{{ form.as_p }}
|
||||
<p>
|
||||
<input class="action-button action-button--warning" type="submit" value="Confirm"> or <a href="{% url 'dashboard:coupon-detail' coupon.pk %}">cancel</a>
|
||||
</p>
|
||||
</form>
|
||||
</section>
|
||||
</article>
|
||||
{% endblock content %}
|
||||
18
src/dashboard/templates/dashboard/coupon_create_form.html
Normal file
18
src/dashboard/templates/dashboard/coupon_create_form.html
Normal file
@ -0,0 +1,18 @@
|
||||
{% extends "dashboard.html" %}
|
||||
|
||||
{% block content %}
|
||||
<article class="product">
|
||||
<header class="object__header">
|
||||
<h1>Create coupon</h1>
|
||||
</header>
|
||||
<section class="object__panel">
|
||||
<form class="panel__item" method="POST" action="{% url 'dashboard:coupon-create' %}">
|
||||
{% csrf_token %}
|
||||
{{form.as_p}}
|
||||
<p class="form__submit">
|
||||
<input class="action-button" type="submit" value="Create coupon"> or <a href="{% url 'dashboard:coupon-list' %}">cancel</a>
|
||||
</p>
|
||||
</form>
|
||||
</section>
|
||||
</article>
|
||||
{% endblock %}
|
||||
23
src/dashboard/templates/dashboard/coupon_detail.html
Normal file
23
src/dashboard/templates/dashboard/coupon_detail.html
Normal file
@ -0,0 +1,23 @@
|
||||
{% extends "dashboard.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<article>
|
||||
<header class="object__header">
|
||||
<h1><img src="{% static 'images/coupon.png' %}" alt=""> {{ coupon.name }}</h1>
|
||||
<div class="object__menu">
|
||||
<a href="{% url 'dashboard:coupon-delete' coupon.pk %}" class="action-button action-button--warning">Delete</a>
|
||||
<a href="{% url 'dashboard:coupon-update' coupon.pk %}" class="action-button">Edit</a>
|
||||
</div>
|
||||
</header>
|
||||
<section class="product__detail object__panel">
|
||||
<div>
|
||||
<p>{{ coupon.get_type_display }}</p>
|
||||
<p>{{ coupon.code }}</p>
|
||||
<p>{{ coupon.valid_from }}</p>
|
||||
<p>{{ coupon.valid_to }}</p>
|
||||
<p>{{ coupon.discount_value }} {{ coupon.get_discount_value_type_display }}</p>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
{% endblock content %}
|
||||
18
src/dashboard/templates/dashboard/coupon_form.html
Normal file
18
src/dashboard/templates/dashboard/coupon_form.html
Normal file
@ -0,0 +1,18 @@
|
||||
{% extends "dashboard.html" %}
|
||||
|
||||
{% block content %}
|
||||
<article class="product">
|
||||
<header class="object__header">
|
||||
<h1>Update Coupon</h1>
|
||||
</header>
|
||||
<section class="object__panel">
|
||||
<form class="panel__item" method="POST" action="{% url 'dashboard:coupon-update' coupon.pk %}">
|
||||
{% csrf_token %}
|
||||
{{form.as_p}}
|
||||
<p class="form__submit">
|
||||
<input class="action-button" type="submit" value="Save changes"> or <a href="{% url 'dashboard:coupon-detail' coupon.pk %}">cancel</a>
|
||||
</p>
|
||||
</form>
|
||||
</section>
|
||||
</article>
|
||||
{% endblock %}
|
||||
33
src/dashboard/templates/dashboard/coupon_list.html
Normal file
33
src/dashboard/templates/dashboard/coupon_list.html
Normal file
@ -0,0 +1,33 @@
|
||||
{% extends "dashboard.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<article>
|
||||
<header class="object__header">
|
||||
<h1><img src="{% static 'images/coupon.png' %}" alt=""> Coupons</h1>
|
||||
<div class="object__menu">
|
||||
<a href="{% url 'dashboard:coupon-create' %}" class="action-button order__fulfill">+ New coupon</a>
|
||||
</div>
|
||||
</header>
|
||||
<section class="object__list">
|
||||
<div class="object__item panel__header object__item--col5" href="coupon-detail">
|
||||
<span>Name</span>
|
||||
<span>Code</span>
|
||||
<span>Starts</span>
|
||||
<span>Ends</span>
|
||||
<span>Value</span>
|
||||
</div>
|
||||
{% for coupon in coupon_list %}
|
||||
<a class="object__item object__item--link object__item--col5" href="{% url 'dashboard:coupon-detail' coupon.pk %}">
|
||||
<span>{{ coupon.name }}</span>
|
||||
<span>{{ coupon.code }}</span>
|
||||
<span>{{ coupon.valid_from|date:"SHORT_DATE_FORMAT" }}</span>
|
||||
<span>{{ coupon.valid_to|date:"SHORT_DATE_FORMAT" }}</span>
|
||||
<span>{{ coupon.discount_value }} {{ coupon.get_discount_value_type_display }}</span>
|
||||
</a>
|
||||
{% empty %}
|
||||
<span class="object__item">No coupons</span>
|
||||
{% endfor %}
|
||||
</section>
|
||||
</article>
|
||||
{% endblock content %}
|
||||
@ -5,10 +5,12 @@
|
||||
<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>
|
||||
<div class="object__menu">
|
||||
<a href="{% url 'dashboard:customer-update' customer.pk %}" class="action-button">Edit</a>
|
||||
</div>
|
||||
</header>
|
||||
<section class="object__panel">
|
||||
<div class="object__item object__item--header">
|
||||
<div class="object__item panel__header">
|
||||
<h4>Info</h4>
|
||||
</div>
|
||||
<div class="panel__item">
|
||||
@ -52,14 +54,14 @@
|
||||
</section>
|
||||
{% with order_list=customer.orders.all %}
|
||||
<section class="object__list">
|
||||
<div class="object__item object__item--header" href="order-detail">
|
||||
<div class="object__item panel__header object__item--col4" 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 %}">
|
||||
<a class="object__item object__item--col4 object__item--link" 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">
|
||||
|
||||
@ -2,9 +2,12 @@
|
||||
|
||||
{% block content %}
|
||||
<article class="product">
|
||||
<h1>Update Customer</h1>
|
||||
<section>
|
||||
<form method="POST" action="{% url 'dashboard:customer-update' customer.pk %}">
|
||||
<p><a href="{% url 'storefront:customer-detail' customer.pk %}">← Back</a></p>
|
||||
<header class="object__header">
|
||||
<h1>Update Customer</h1>
|
||||
</header>
|
||||
<section class="object__panel">
|
||||
<form class="panel__item" method="POST" action="{% url 'dashboard:customer-update' customer.pk %}">
|
||||
{% csrf_token %}
|
||||
{{form.as_p}}
|
||||
<p class="form__submit">
|
||||
|
||||
@ -3,15 +3,17 @@
|
||||
|
||||
{% block content %}
|
||||
<article>
|
||||
<h1><img src="{% static 'images/customer.png' %}" alt=""> Customers</h1>
|
||||
<header class="object__header">
|
||||
<h1><img src="{% static 'images/customer.png' %}" alt=""> Customers</h1>
|
||||
</header>
|
||||
<section class="object__list">
|
||||
<div class="object__item object__item--header" href="customer-detail">
|
||||
<div class="object__item panel__header object__item--col3" 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 %}">
|
||||
<a class="object__item object__item--link object__item--col3" href="{% url 'dashboard:customer-detail' customer.pk %}">
|
||||
<span>{{customer.get_full_name}}</span>
|
||||
<span>{{customer.email}}</span>
|
||||
<span>{{customer.num_orders}}</span>
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
{% block content %}
|
||||
<article>
|
||||
<header class="object__header">
|
||||
<h1><img src="{% static "images/box.png" %}" alt=""> Order #{{order.pk}}</h1>
|
||||
<h1><img src="{% static 'images/box.png' %}" alt=""> Order #{{order.pk}}</h1>
|
||||
<div class="object__menu">
|
||||
<div class="dropdown">
|
||||
<span class="dropdown__menu">Options ↓</span>
|
||||
@ -17,19 +17,19 @@
|
||||
</div>
|
||||
</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>
|
||||
<div class="object__item panel__header object__item--col5">
|
||||
<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">
|
||||
<div class="object__item object__item--col5">
|
||||
{% 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>
|
||||
<figcaption><strong>{{product.name}}</strong><br>Grind: {{item.customer_note}}</figcaption>
|
||||
</figure>
|
||||
<span>{{product.sku}}</span>
|
||||
<span>{{item.quantity}}</span>
|
||||
@ -40,22 +40,33 @@
|
||||
{% empty %}
|
||||
<p>No items in order yet.</p>
|
||||
{% endfor %}
|
||||
<div class="object__item">
|
||||
<a href="{% url 'dashboard:order-fulfill' order.pk %}" class="action-button order__fulfill">Fulfill →</a>
|
||||
</div>
|
||||
<div class="object__item object__item--col5">
|
||||
<a href="{% url 'dashboard:order-fulfill' order.pk %}" class="action-button order__fulfill">Fulfill →</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="object__panel">
|
||||
<div class="object__item object__item--header">
|
||||
<div class="object__item panel__header object__item--col5">
|
||||
<h4>Shipping</h4>
|
||||
<a href="{% url 'dashboard:order-ship' order.pk %}" class="action-button order__fulfill">Ship order →</a>
|
||||
</div>
|
||||
<div class="panel__item">
|
||||
<a href="{% url 'dashboard:order-fulfill' order.pk %}" class="action-button order__fulfill">Ship order →</a>
|
||||
</div>
|
||||
{% for number in order.tracking_numbers.all %}
|
||||
<div class="panel__item">
|
||||
<p>
|
||||
<strong>Shipment</strong><br>
|
||||
Date: {{number.created_at|date:"SHORT_DATE_FORMAT" }}<br>
|
||||
Tracking number: {{number.tracking_id}}
|
||||
</p>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="panel__item">
|
||||
<p>No tracking information.</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
|
||||
<section class="object__panel">
|
||||
<div class="object__item object__item--header">
|
||||
<div class="object__item panel__header">
|
||||
<h4>Customer</h4>
|
||||
</div>
|
||||
{% with customer=order.customer %}
|
||||
@ -87,7 +98,22 @@
|
||||
</section>
|
||||
|
||||
<section class="object__panel">
|
||||
<div class="object__item object__item--header">
|
||||
<div class="object__item panel__header">
|
||||
<h4>Payment</h4>
|
||||
</div>
|
||||
<div class="panel__item">
|
||||
<p>
|
||||
<span>Subtotal: {{order.total_net_amount}}</span><br>
|
||||
{% if order.coupon %}
|
||||
<span>Discount: {{order.coupon.discount_value}} {{order.coupon.get_discount_value_type_display}}</span><br>
|
||||
{% endif %}
|
||||
<span>Total: {{order.get_total_price_after_discount}}</span>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="object__panel">
|
||||
<div class="object__item panel__header">
|
||||
<h4>Transaction</h4>
|
||||
</div>
|
||||
<div class="panel__item">
|
||||
|
||||
@ -16,14 +16,14 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
<div class="object__item object__item--header">
|
||||
<div class="object__item panel__header object__item--col4">
|
||||
<span>Product</span>
|
||||
<span>SKU</span>
|
||||
<span>Quantity to fulfill</span>
|
||||
<span>Grind</span>
|
||||
</div>
|
||||
{% for form in form %}
|
||||
<div class="object__item">
|
||||
<div class="object__item object__item--col4">
|
||||
{% with product=form.instance.product %}
|
||||
{{form.id}}
|
||||
<figure class="item__figure">
|
||||
@ -36,7 +36,7 @@
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="object__item">
|
||||
<div class="object__item object__item--col5">
|
||||
<a href="{% url 'dashboard:order-detail' order.pk %}">cancel</a> <input class="action-button order__fulfill" type="submit" value="Fulfill">
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
<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">
|
||||
<div class="object__item panel__header object__item--col5" href="order-detail">
|
||||
<span>Order #</span>
|
||||
<span>Date</span>
|
||||
<span>Customer</span>
|
||||
@ -15,7 +15,7 @@
|
||||
<span>Total</span>
|
||||
</div>
|
||||
{% for order in order_list %}
|
||||
<a class="object__item" href="{% url 'dashboard:order-detail' order.pk %}">
|
||||
<a class="object__item object__item--link object__item--col5" 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>
|
||||
|
||||
36
src/dashboard/templates/dashboard/order_tracking_form.html
Normal file
36
src/dashboard/templates/dashboard/order_tracking_form.html
Normal file
@ -0,0 +1,36 @@
|
||||
{% extends "dashboard.html" %}
|
||||
|
||||
{% block content %}
|
||||
<article>
|
||||
<header class="object__header">
|
||||
<h1>Ship Order #{{order.pk}}</h1>
|
||||
</header>
|
||||
<section class="object__panel">
|
||||
<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 panel__header panel__header--flex">
|
||||
<span>Product</span>
|
||||
</div>
|
||||
{% for formitem in form %}
|
||||
<div class="object__item">
|
||||
{{formitem}}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="object__item object__item--col5">
|
||||
<a href="{% url 'dashboard:order-detail' order.pk %}">cancel</a> <input class="action-button order__fulfill" type="submit" value="Ship order and send tracking info to customer">
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
</section>
|
||||
</article>
|
||||
{% endblock content %}
|
||||
18
src/dashboard/templates/dashboard/prodphoto_create_form.html
Normal file
18
src/dashboard/templates/dashboard/prodphoto_create_form.html
Normal file
@ -0,0 +1,18 @@
|
||||
{% extends "dashboard.html" %}
|
||||
|
||||
{% block content %}
|
||||
<article class="product">
|
||||
<header class="object__header">
|
||||
<h1>Add photo</h1>
|
||||
</header>
|
||||
<section class="object__panel">
|
||||
<form class="panel__item" enctype="multipart/form-data" method="POST" action="{% url 'dashboard:prodphoto-create' product.pk %}">
|
||||
{% csrf_token %}
|
||||
{{form.as_p}}
|
||||
<p class="form__submit">
|
||||
<input class="action-button" type="submit" value="Add photo"> or <a href="{% url 'dashboard:product-detail' product.pk %}">cancel</a>
|
||||
</p>
|
||||
</form>
|
||||
</section>
|
||||
</article>
|
||||
{% endblock %}
|
||||
@ -0,0 +1,22 @@
|
||||
{% extends "dashboard.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<article>
|
||||
<header class="object__header">
|
||||
<h1><img src="{% static 'images/cubes.png' %}" alt=""> Product</h1>
|
||||
</header>
|
||||
<section class="product__detail object__panel">
|
||||
<figure class="product__figure">
|
||||
<img class="" src="{{product.productphoto_set.first.image.url}}" alt="{{product.productphoto_set.first.image}}">
|
||||
</figure>
|
||||
<form method="post">{% csrf_token %}
|
||||
<p>Are you sure you want to delete "{{ object }}"?</p>
|
||||
{{ form.as_p }}
|
||||
<p>
|
||||
<input class="action-button action-button--warning" type="submit" value="Confirm"> or <a href="{% url 'dashboard:product-detail' product.pk %}">cancel</a>
|
||||
</p>
|
||||
</form>
|
||||
</section>
|
||||
</article>
|
||||
{% endblock content %}
|
||||
@ -2,9 +2,11 @@
|
||||
|
||||
{% block content %}
|
||||
<article class="product">
|
||||
<h1>Create product</h1>
|
||||
<section>
|
||||
<form method="POST" action="{% url 'dashboard:product-create' %}">
|
||||
<header class="object__header">
|
||||
<h1>Create product</h1>
|
||||
</header>
|
||||
<section class="object__panel">
|
||||
<form class="panel__item" method="POST" action="{% url 'dashboard:product-create' %}">
|
||||
{% csrf_token %}
|
||||
{{form.as_p}}
|
||||
<p class="form__submit">
|
||||
|
||||
@ -5,7 +5,10 @@
|
||||
<article>
|
||||
<header class="object__header">
|
||||
<h1><img src="{% static "images/cubes.png" %}" alt=""> Product</h1>
|
||||
<a href="{% url 'dashboard:product-update' product.pk %}" class="action-button">Edit</a>
|
||||
<div class="object__menu">
|
||||
<a href="{% url 'dashboard:product-delete' product.pk %}" class="action-button action-button--warning">Delete</a>
|
||||
<a href="{% url 'dashboard:product-update' product.pk %}" class="action-button">Edit</a>
|
||||
</div>
|
||||
</header>
|
||||
<section class="product__detail object__panel">
|
||||
<figure class="product__figure">
|
||||
@ -17,7 +20,26 @@
|
||||
<p>$<strong>{{product.price}}</strong></p>
|
||||
<p>{{product.weight.oz}} oz</p>
|
||||
<p>Visible in listings: <strong>{{product.visible_in_listings|yesno:"Yes,No"}}</strong></p>
|
||||
<p>Ordered {{product.num_ordered|default_if_none:"0"}} time{{ product.num_ordered|pluralize }}.</p>
|
||||
<p>Ordered {{product.num_ordered|default_if_none:"0"}} time{{ product.num_ordered|default_if_none:"0"|pluralize }}.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="object__panel">
|
||||
<div class="object__item panel__header panel__header--flex">
|
||||
<h4>Photos</h4>
|
||||
<a href="{% url 'dashboard:prodphoto-create' product.pk %}" class="action-button order__fulfill">+ Upload new photo</a>
|
||||
</div>
|
||||
<div class="panel__item gallery">
|
||||
{% for photo in product.productphoto_set.all %}
|
||||
<figure class="gallery__item">
|
||||
<img src="{{ photo.image.url }}" alt="">
|
||||
<figcaption>
|
||||
<form action="{% url 'dashboard:prodphoto-delete' product.pk photo.pk %}" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="submit" class="action-button action-button--warning" value="Delete photo">
|
||||
</form>
|
||||
</figcaption>
|
||||
</figure>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
@ -8,14 +8,14 @@
|
||||
<a href="{% url 'dashboard:product-create' %}" class="action-button">+ New product</a>
|
||||
</header>
|
||||
<section class="object__list">
|
||||
<div class="object__item object__item--header">
|
||||
<div class="object__item panel__header object__item--col4">
|
||||
<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 %}">
|
||||
<a class="object__item object__item--link object__item--col4" 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>
|
||||
|
||||
16
src/dashboard/templates/dashboard/shipmeth_create_form.html
Normal file
16
src/dashboard/templates/dashboard/shipmeth_create_form.html
Normal file
@ -0,0 +1,16 @@
|
||||
{% extends "dashboard.html" %}
|
||||
|
||||
{% block content %}
|
||||
<article>
|
||||
<h1>Create Shipping Method</h1>
|
||||
<section>
|
||||
<form method="POST" action="{% url 'dashboard:shipmeth-create' %}">
|
||||
{% csrf_token %}
|
||||
{{form.as_p}}
|
||||
<p class="form__submit">
|
||||
<input class="action-button" type="submit" value="Create method"> or <a href="{% url 'dashboard:config' %}">cancel</a>
|
||||
</p>
|
||||
</form>
|
||||
</section>
|
||||
</article>
|
||||
{% endblock %}
|
||||
21
src/dashboard/templates/dashboard/shipmeth_detail.html
Normal file
21
src/dashboard/templates/dashboard/shipmeth_detail.html
Normal file
@ -0,0 +1,21 @@
|
||||
{% extends "dashboard.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<article>
|
||||
<header class="object__header">
|
||||
<h1><img src="{% static 'images/gear.png' %}" alt=""> Shipping Method</h1>
|
||||
<div class="object__menu">
|
||||
<a href="" class="action-button action-button--warning">Delete</a>
|
||||
<a href="" class="action-button">Edit</a>
|
||||
</div>
|
||||
</header>
|
||||
<section class="product__detail object__panel">
|
||||
<div>
|
||||
<h1>{{shippingmethod.name}}</h1>
|
||||
<p>{{shippingmethod.get_type_display}}</p>
|
||||
<p>$<strong>{{shippingmethod.price}}</strong></p>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
{% endblock content %}
|
||||
@ -3,6 +3,20 @@ from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.DashboardHomeView.as_view(), name='home'),
|
||||
path('config/', views.DashboardConfigView.as_view(), name='config'),
|
||||
|
||||
path('shipping-methods/new/', views.ShippingMethodCreateView.as_view(), name='shipmeth-create'),
|
||||
path('shipping-methods/<int:pk>/', include([
|
||||
path('', views.ShippingMethodDetailView.as_view(), name='shipmeth-detail'),
|
||||
])),
|
||||
|
||||
path('coupons/', views.CouponListView.as_view(), name='coupon-list'),
|
||||
path('coupons/new/', views.CouponCreateView.as_view(), name='coupon-create'),
|
||||
path('coupons/<int:pk>/', include([
|
||||
path('', views.CouponDetailView.as_view(), name='coupon-detail'),
|
||||
path('update/', views.CouponUpdateView.as_view(), name='coupon-update'),
|
||||
path('delete/', views.CouponDeleteView.as_view(), name='coupon-delete'),
|
||||
])),
|
||||
|
||||
path('orders/', views.OrderListView.as_view(), name='order-list'),
|
||||
path('orders/<int:pk>/', include([
|
||||
@ -10,14 +24,20 @@ urlpatterns = [
|
||||
# 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('ship/', views.OrderTrackingView.as_view(), name='order-ship'),
|
||||
])),
|
||||
|
||||
path('products/', views.ProductListView.as_view(), name='product-list'),
|
||||
path('products/new/', views.ProductCreateView.as_view(), name='product-create'),
|
||||
path('<int:pk>/', include([
|
||||
path('products/<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('delete/', views.ProductDeleteView.as_view(), name='product-delete'),
|
||||
|
||||
path('photos/new/', views.ProductPhotoCreateView.as_view(), name='prodphoto-create'),
|
||||
path('photos/<int:photo_pk>/', include([
|
||||
path('delete/', views.ProductPhotoDeleteView.as_view(), name='prodphoto-delete'),
|
||||
])),
|
||||
])),
|
||||
|
||||
path('customers/', views.CustomerListView.as_view(), name='customer-list'),
|
||||
|
||||
@ -12,6 +12,8 @@ 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.contrib import messages
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
|
||||
from django.db.models import (
|
||||
Exists, OuterRef, Prefetch, Subquery, Count, Sum, Avg, F, Q, Value
|
||||
@ -21,14 +23,23 @@ 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.models import (
|
||||
Product,
|
||||
ProductPhoto,
|
||||
Order,
|
||||
OrderLine,
|
||||
ShippingMethod,
|
||||
Transaction,
|
||||
TrackingNumber,
|
||||
Coupon
|
||||
)
|
||||
|
||||
from core import DiscountValueType, VoucherType, OrderStatus, ShippingMethodType
|
||||
from .forms import OrderLineFulfillForm, OrderLineFormset
|
||||
from .forms import OrderLineFulfillForm, OrderLineFormset, OrderTrackingFormset, CouponForm, ProductPhotoForm
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class DashboardHomeView(TemplateView):
|
||||
class DashboardHomeView(LoginRequiredMixin, TemplateView):
|
||||
template_name = 'dashboard/dashboard_detail.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
@ -45,16 +56,70 @@ class DashboardHomeView(TemplateView):
|
||||
).aggregate(total=Sum('total_net_amount'))['total']
|
||||
return context
|
||||
|
||||
class OrderListView(ListView):
|
||||
class DashboardConfigView(TemplateView):
|
||||
template_name = 'dashboard/config.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
today = timezone.localtime(timezone.now()).date()
|
||||
|
||||
context['shipping_method_list'] = ShippingMethod.objects.all()
|
||||
|
||||
return context
|
||||
|
||||
|
||||
|
||||
|
||||
class ShippingMethodCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
|
||||
model = ShippingMethod
|
||||
template_name = 'dashboard/shipmeth_create_form.html'
|
||||
fields = '__all__'
|
||||
success_message = '%(name)s created.'
|
||||
|
||||
class ShippingMethodDetailView(LoginRequiredMixin, DetailView):
|
||||
model = ShippingMethod
|
||||
template_name = 'dashboard/shipmeth_detail.html'
|
||||
|
||||
|
||||
|
||||
class CouponListView(LoginRequiredMixin, ListView):
|
||||
model = Coupon
|
||||
template_name = 'dashboard/coupon_list.html'
|
||||
|
||||
class CouponCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
|
||||
model = Coupon
|
||||
template_name = 'dashboard/coupon_create_form.html'
|
||||
form_class = CouponForm
|
||||
success_message = '%(name)s created.'
|
||||
|
||||
class CouponDetailView(LoginRequiredMixin, DetailView):
|
||||
model = Coupon
|
||||
template_name = 'dashboard/coupon_detail.html'
|
||||
|
||||
class CouponUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
|
||||
model = Coupon
|
||||
template_name = 'dashboard/coupon_form.html'
|
||||
success_message = '%(name)s saved.'
|
||||
form_class = CouponForm
|
||||
|
||||
class CouponDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
|
||||
model = Coupon
|
||||
template_name = 'dashboard/coupon_confirm_delete.html'
|
||||
success_url = reverse_lazy('dashboard:coupon-list')
|
||||
success_message = 'Coupon deleted.'
|
||||
|
||||
|
||||
|
||||
class OrderListView(LoginRequiredMixin, 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:
|
||||
if query == 'unfulfilled':
|
||||
object_list = Order.objects.filter(
|
||||
status=query
|
||||
Q(status=OrderStatus.UNFULFILLED) |
|
||||
Q(status=OrderStatus.PARTIALLY_FULFILLED)
|
||||
).order_by(
|
||||
'-created_at'
|
||||
).select_related(
|
||||
@ -70,7 +135,7 @@ class OrderListView(ListView):
|
||||
|
||||
return object_list
|
||||
|
||||
class OrderDetailView(DetailView):
|
||||
class OrderDetailView(LoginRequiredMixin, DetailView):
|
||||
model = Order
|
||||
template_name = 'dashboard/order_detail.html'
|
||||
|
||||
@ -93,10 +158,24 @@ class OrderDetailView(DetailView):
|
||||
return context
|
||||
|
||||
|
||||
class OrderFulfillView(UpdateView):
|
||||
class OrderFulfillView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
|
||||
model = Order
|
||||
template_name = "dashboard/order_fulfill.html"
|
||||
form_class = OrderLineFormset
|
||||
success_message = "Order saved."
|
||||
|
||||
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})
|
||||
|
||||
class OrderTrackingView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
|
||||
model = Order
|
||||
template_name = "dashboard/order_tracking_form.html"
|
||||
form_class = OrderTrackingFormset
|
||||
success_message = "Order saved."
|
||||
|
||||
def form_valid(self, form):
|
||||
form.save()
|
||||
@ -106,9 +185,10 @@ class OrderFulfillView(UpdateView):
|
||||
return reverse('dashboard:order-detail', kwargs={'pk': self.object.pk})
|
||||
|
||||
|
||||
class ProductListView(ListView):
|
||||
class ProductListView(LoginRequiredMixin, ListView):
|
||||
model = Product
|
||||
template_name = 'dashboard/product_list.html'
|
||||
ordering = 'sorting'
|
||||
|
||||
# def get_queryset(self):
|
||||
# object_list = Product.objects.filter(
|
||||
@ -118,22 +198,63 @@ class ProductListView(ListView):
|
||||
# )
|
||||
# return object_list
|
||||
|
||||
class ProductDetailView(DetailView):
|
||||
class ProductDetailView(LoginRequiredMixin, DetailView):
|
||||
model = Product
|
||||
template_name = 'dashboard/product_detail.html'
|
||||
|
||||
class ProductUpdateView(UpdateView):
|
||||
class ProductUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
|
||||
model = Product
|
||||
template_name = 'dashboard/product_update_form.html'
|
||||
fields = '__all__'
|
||||
success_message = '%(name)s saved.'
|
||||
|
||||
class ProductCreateView(CreateView):
|
||||
class ProductCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
|
||||
model = Product
|
||||
template_name = "dashboard/product_create_form.html"
|
||||
template_name = 'dashboard/product_create_form.html'
|
||||
fields = '__all__'
|
||||
success_message = '%(name)s created.'
|
||||
|
||||
class ProductDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
|
||||
model = Product
|
||||
template_name = 'dashboard/product_confirm_delete.html'
|
||||
success_url = reverse_lazy('dashboard:product-list')
|
||||
success_message = 'Product deleted.'
|
||||
|
||||
|
||||
class CustomerListView(ListView):
|
||||
class ProductPhotoCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
|
||||
model = ProductPhoto
|
||||
pk_url_kwarg = 'photo_pk'
|
||||
template_name = 'dashboard/prodphoto_create_form.html'
|
||||
form_class = ProductPhotoForm
|
||||
success_message = 'Photo added.'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['product'] = Product.objects.get(pk=self.kwargs['pk'])
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.product = Product.objects.get(pk=self.kwargs['pk'])
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('dashboard:product-detail', kwargs={'pk': self.kwargs['pk']})
|
||||
|
||||
class ProductPhotoDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
|
||||
model = ProductPhoto
|
||||
pk_url_kwarg = 'photo_pk'
|
||||
template_name = 'dashboard/prodphoto_confirm_delete.html'
|
||||
success_message = 'Photo deleted.'
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('dashboard:product-detail', kwargs={'pk': self.kwargs['pk']})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class CustomerListView(LoginRequiredMixin, ListView):
|
||||
model = User
|
||||
template_name = 'dashboard/customer_list.html'
|
||||
|
||||
@ -150,15 +271,16 @@ class CustomerListView(ListView):
|
||||
|
||||
return object_list
|
||||
|
||||
class CustomerDetailView(DetailView):
|
||||
class CustomerDetailView(LoginRequiredMixin, DetailView):
|
||||
model = User
|
||||
template_name = 'dashboard/customer_detail.html'
|
||||
context_object_name = 'customer'
|
||||
|
||||
class CustomerUpdateView(UpdateView):
|
||||
class CustomerUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
|
||||
model = User
|
||||
template_name = 'dashboard/customer_form.html'
|
||||
context_object_name = 'customer'
|
||||
success_message = 'Customer saved.'
|
||||
fields = (
|
||||
'first_name',
|
||||
'last_name',
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 41 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 55 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 29 KiB |
@ -28,6 +28,7 @@ ANYMAIL_CONFIG = {
|
||||
|
||||
SERVER_EMAIL = os.environ.get('SERVER_EMAIL', '')
|
||||
DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', '')
|
||||
DEFAULT_CONTACT_EMAIL = os.environ.get('DEFAULT_CONTACT_EMAIL', '')
|
||||
|
||||
SECURE_HSTS_SECONDS = os.environ.get('SECURE_HSTS_SECONDS', 3600)
|
||||
SECURE_SSL_REDIRECT = os.environ.get('SECURE_SSL_REDIRECT', 'False') == 'True'
|
||||
|
||||
@ -9,6 +9,8 @@ BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
# Add Your Required Allow Host
|
||||
if DEBUG == False:
|
||||
ALLOWED_HOSTS = ['ptcoffee-dev.windmillapps.org']
|
||||
else:
|
||||
ALLOWED_HOSTS = ['192.168.68.106', '127.0.0.1', 'localhost']
|
||||
|
||||
INTERNAL_IPS = [
|
||||
'127.0.0.1',
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 43 KiB |
1
src/static/images/site_logo.svg
Normal file
1
src/static/images/site_logo.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 36 KiB |
@ -16,9 +16,9 @@ export function getCookie(name) {
|
||||
|
||||
const twentyYears = 20 * 365 * 24 * 60 * 60 * 1000
|
||||
|
||||
export function setCookie(name, value) {
|
||||
export function setCookie(name, value, expiration=twentyYears) {
|
||||
const body = [ name, value ].map(encodeURIComponent).join("=")
|
||||
const expires = new Date(Date.now() + twentyYears).toUTCString()
|
||||
const expires = new Date(Date.now() + expiration).toUTCString()
|
||||
const cookie = `${body}; domain=; path=/; SameSite=Lax; expires=${expires}`
|
||||
document.cookie = cookie
|
||||
}
|
||||
|
||||
37
src/static/scripts/index.js
Normal file
37
src/static/scripts/index.js
Normal file
@ -0,0 +1,37 @@
|
||||
import { getCookie, setCookie } from "./cookie.js"
|
||||
|
||||
// Get the modal
|
||||
const modal = document.querySelector(".modal-menu");
|
||||
|
||||
// Get the <span> element that closes the modal
|
||||
const closeBtn = document.querySelector(".close-modal");
|
||||
|
||||
const oneDay = 1 * 24 * 60 * 60 * 1000
|
||||
|
||||
// When the user clicks on <span> (x), close the modal
|
||||
closeBtn.addEventListener("click", event => {
|
||||
modal.style.display = "none";
|
||||
setCookie('newsletter-modal', 'true', oneDay)
|
||||
})
|
||||
|
||||
const scrollFunction = () => {
|
||||
let modalDismissed = getCookie('newsletter-modal')
|
||||
console.log(modalDismissed)
|
||||
if (modalDismissed != 'true') {
|
||||
if (document.body.scrollTop > 600 || document.documentElement.scrollTop > 600) {
|
||||
modal.style.display = "block";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.onscroll = () => {
|
||||
scrollFunction();
|
||||
};
|
||||
|
||||
// When the user clicks anywhere outside of the modal, close it
|
||||
window.addEventListener("click", event => {
|
||||
if (event.target == modal) {
|
||||
modal.style.display = "none";
|
||||
}
|
||||
setCookie('newsletter-modal', 'true', oneDay)
|
||||
});
|
||||
@ -1,6 +1,6 @@
|
||||
import { getCookie } from "./cookie.js"
|
||||
|
||||
let form = document.querySelector('form.order__form')
|
||||
let form = document.querySelector('form')
|
||||
|
||||
// Render the PayPal button into #paypal-button-container
|
||||
paypal.Buttons({
|
||||
|
||||
14
src/static/scripts/product_form.js
Normal file
14
src/static/scripts/product_form.js
Normal file
@ -0,0 +1,14 @@
|
||||
const form = document.querySelector('form')
|
||||
const purchaseTypeInput = form.querySelector('[name=purchase_type]')
|
||||
const scheduleInput = form.querySelector('[name=schedule]')
|
||||
|
||||
scheduleInput.parentElement.style.display = 'none'
|
||||
|
||||
purchaseTypeInput.addEventListener('change', event => {
|
||||
if (event.target.value === 'Subscribe') {
|
||||
scheduleInput.parentElement.style.display = 'block'
|
||||
} else if (event.target.value === 'One-time purchase') {
|
||||
scheduleInput.parentElement.style.display = 'none'
|
||||
}
|
||||
|
||||
})
|
||||
12
src/static/scripts/product_gallery.js
Normal file
12
src/static/scripts/product_gallery.js
Normal file
@ -0,0 +1,12 @@
|
||||
const gallery = document.querySelectorAll('.gallery__thumbnail')
|
||||
const productImage = document.querySelector('.product__image')
|
||||
let currentImg = document.querySelector('.gallery__thumbnail--focus')
|
||||
|
||||
gallery.forEach(image => {
|
||||
image.addEventListener('mouseover', event => {
|
||||
currentImg.classList.remove('gallery__thumbnail--focus')
|
||||
event.target.classList.add('gallery__thumbnail--focus')
|
||||
currentImg = event.target
|
||||
productImage.src = currentImg.src
|
||||
})
|
||||
})
|
||||
@ -9,6 +9,7 @@
|
||||
--red-color: #ff4d44;
|
||||
|
||||
--default-border: 2px solid var(--gray-color);
|
||||
--default-shadow: 0 1rem 3rem var(--gray-color);
|
||||
}
|
||||
|
||||
html {
|
||||
@ -71,6 +72,7 @@ label {
|
||||
input[type=text],
|
||||
input[type=email],
|
||||
input[type=number],
|
||||
input[type=date],
|
||||
input[type=password],
|
||||
select[multiple=multiple],
|
||||
textarea {
|
||||
@ -144,7 +146,7 @@ button:hover {
|
||||
}
|
||||
|
||||
.action-button--warning {
|
||||
background-color: var(--red-color);
|
||||
background-color: var(--red-color) !important;
|
||||
}
|
||||
|
||||
.action-link {
|
||||
@ -261,6 +263,41 @@ main article {
|
||||
}
|
||||
|
||||
|
||||
.site__messages {
|
||||
text-align: left;
|
||||
white-space: normal;
|
||||
background-color: var(--fg-color);
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: var(--default-shadow);
|
||||
|
||||
margin: 1rem;
|
||||
padding: 0.5rem 1rem;
|
||||
|
||||
position: fixed;
|
||||
|
||||
left: auto;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
top: auto;
|
||||
z-index: 990;
|
||||
}
|
||||
|
||||
.messages__message.debug {
|
||||
color: white;
|
||||
}
|
||||
.messages__message.info {
|
||||
color: white;
|
||||
}
|
||||
.messages__message.success {
|
||||
color: var(--green-color);
|
||||
}
|
||||
.messages__message.warning {
|
||||
color: var(--yellow-color);
|
||||
}
|
||||
.messages__message.error {
|
||||
color: var(--red-color);
|
||||
}
|
||||
|
||||
|
||||
.object__header {
|
||||
display: flex;
|
||||
@ -278,12 +315,33 @@ main article {
|
||||
|
||||
.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;
|
||||
justify-items: start;
|
||||
}
|
||||
|
||||
.object__item--col3 {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.object__item--col5 {
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
}
|
||||
|
||||
.object__item--col4 {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
.object__item--col8 {
|
||||
grid-template-columns: repeat(8, 1fr);
|
||||
}
|
||||
|
||||
.panel__header--flex {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.panel__item:last-child,
|
||||
@ -292,11 +350,11 @@ main article {
|
||||
border-radius: 0 0 0.5rem 0.5rem;
|
||||
}
|
||||
|
||||
.object__item:hover {
|
||||
.object__item--link:hover {
|
||||
background-color: var(--bg-alt-color);
|
||||
}
|
||||
|
||||
.object__item--header {
|
||||
.panel__header {
|
||||
font-weight: bold;
|
||||
background-color: var(--bg-alt-color);
|
||||
border-radius: 0.5rem 0.5rem 0 0;
|
||||
@ -328,6 +386,10 @@ main article {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.object__menu > a:not(:last-child) {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.order__fulfill {
|
||||
grid-column: 8;
|
||||
}
|
||||
@ -356,7 +418,7 @@ main article {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 0 3rem var(--gray-color);
|
||||
box-shadow: var(--default-shadow);
|
||||
}
|
||||
|
||||
.dropdown__child a {
|
||||
@ -457,3 +519,24 @@ main article {
|
||||
height: 50px;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.gallery {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 3rem;
|
||||
|
||||
}
|
||||
|
||||
.gallery__item {
|
||||
}
|
||||
|
||||
.gallery__item,
|
||||
.gallery__item img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.gallery__item img {
|
||||
border: var(--default-border);
|
||||
object-fit: cover;
|
||||
aspect-ratio: 1/1;
|
||||
}
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
:root {
|
||||
--fg-color: #333;
|
||||
--fg-color: #34201A;
|
||||
--bg-color: #f5f5f5;
|
||||
--bg-alt-color: #eee5d3;
|
||||
--gray-color: #9d9d9d;
|
||||
--yellow-color: #f8a911;
|
||||
--yellow-alt-color: #ffce6f;
|
||||
--yellow-dark-color: #b27606;
|
||||
|
||||
--default-border: 2px solid var(--gray-color);
|
||||
}
|
||||
@ -13,17 +15,22 @@ html {
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg-color);
|
||||
background-color: var(--bg-color);
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-weight: 400;
|
||||
max-width: 1024px;
|
||||
padding: 1rem;
|
||||
padding: 0;
|
||||
margin: 0 auto;
|
||||
line-height: 1.75;
|
||||
color: var(--fg-color);
|
||||
}
|
||||
|
||||
|
||||
/* ==========================================================================
|
||||
Typography
|
||||
========================================================================== */
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@ -33,9 +40,9 @@ a {
|
||||
|
||||
h1, h2, h3, h4, h5 {
|
||||
margin: 0;
|
||||
font-family: 'Eczar', serif;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@ -59,82 +66,102 @@ h5 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
small, .text_small {
|
||||
small, .text-small {
|
||||
font-size: 0.833rem;
|
||||
}
|
||||
|
||||
label {
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
blockquote q {
|
||||
/*font-weight: bold;*/
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
blockquote cite {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
color: inherit;
|
||||
/* ==========================================================================
|
||||
Tables
|
||||
========================================================================== */
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
font: inherit;
|
||||
margin: 0;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid var(--gray-color);
|
||||
}
|
||||
|
||||
input {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
label {
|
||||
td,
|
||||
th {
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
font-weight: 700;
|
||||
font-size: 0.85em;
|
||||
padding: 2px 10px;
|
||||
border-bottom: 1px solid;
|
||||
border-color: var(--gray-color);
|
||||
}
|
||||
|
||||
select {
|
||||
text-align: left;
|
||||
border: var(--default-border);
|
||||
padding: 0.5em;
|
||||
outline: 0;
|
||||
th {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
table a {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
|
||||
/* ==========================================================================
|
||||
Forms
|
||||
========================================================================== */
|
||||
input[type=text],
|
||||
input[type=email],
|
||||
input[type=number],
|
||||
input[type=date],
|
||||
input[type=password],
|
||||
select[multiple=multiple],
|
||||
select,
|
||||
textarea {
|
||||
display: block;
|
||||
text-align: left;
|
||||
color: var(--fg-color);
|
||||
border: var(--default-border);
|
||||
padding: 0.5rem;
|
||||
outline: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
textarea:focus {
|
||||
border-color: var(--yellow-color);
|
||||
}
|
||||
input:focus,
|
||||
textarea:focus {
|
||||
border-color: var(--yellow-color);
|
||||
}
|
||||
|
||||
select[multiple=multiple] {
|
||||
height: 125px;
|
||||
height: 8rem;
|
||||
}
|
||||
|
||||
|
||||
input[type=radio],
|
||||
input[type=checkbox] {
|
||||
width: 1em;
|
||||
vertical-align: text-top;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
input[type=radio] + label,
|
||||
input[type=checkbox] + label {
|
||||
display: inline-block;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: 6.25rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
::-webkit-input-placeholder {
|
||||
@ -148,36 +175,36 @@ textarea {
|
||||
}
|
||||
|
||||
|
||||
.action-button,
|
||||
button,
|
||||
input[type=submit],
|
||||
button {
|
||||
.action-button {
|
||||
font-family: inherit;
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
color: var(--fg-color);
|
||||
font-size: 1.5rem;
|
||||
text-decoration: none;
|
||||
text-transform: lowercase;
|
||||
font-variant: small-caps;
|
||||
white-space: nowrap;
|
||||
|
||||
color: var(--fg-color);
|
||||
background-color: var(--yellow-color);
|
||||
|
||||
padding: 0.25rem 1rem;
|
||||
border-radius: 0.2rem;
|
||||
border: none;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button,
|
||||
input[type=submit]:hover,
|
||||
.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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -188,237 +215,515 @@ figure {
|
||||
}
|
||||
img {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.product__item {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 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;
|
||||
|
||||
/* ==========================================================================
|
||||
Base Layout
|
||||
========================================================================== */
|
||||
.site__header > nav,
|
||||
main > article,
|
||||
footer > section {
|
||||
max-width: 1024px;
|
||||
padding: 1rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.product__form input[type=submit] {
|
||||
grid-column: 2;
|
||||
justify-self: end;
|
||||
|
||||
/* ==========================================================================
|
||||
Modal
|
||||
========================================================================== */
|
||||
.modal-menu {
|
||||
display: none;
|
||||
position: fixed; /* Stay in place */
|
||||
z-index: 1; /* Sit on top */
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%; /* Full width */
|
||||
height: 100%; /* Full height */
|
||||
overflow: auto; /* Enable scroll if needed */
|
||||
background-color: rgb(0,0,0); /* Fallback color */
|
||||
background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
|
||||
}
|
||||
|
||||
.site__logo {
|
||||
text-decoration: none;
|
||||
/* Modal Content/Box */
|
||||
.modal-menu__content {
|
||||
background-color: var(--bg-color);
|
||||
margin: 25vh auto;
|
||||
padding: 20px;
|
||||
border: var(--default-border);
|
||||
max-width: 40rem;
|
||||
}
|
||||
|
||||
.site__header div,
|
||||
.site__header nav {
|
||||
|
||||
.modal-menu__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
nav a {
|
||||
text-transform: lowercase;
|
||||
font-variant: small-caps;
|
||||
/* The Close Button */
|
||||
.close-modal {
|
||||
color: #aaa;
|
||||
font-size: 2rem;
|
||||
line-height: 0;
|
||||
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;
|
||||
.close-modal:hover,
|
||||
.close-modal:focus {
|
||||
color: var(--fg-color);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
/* ==========================================================================
|
||||
Site
|
||||
========================================================================== */
|
||||
.site__ft-stamp {
|
||||
max-width: 6rem;
|
||||
}
|
||||
|
||||
/* Site Header
|
||||
========================================================================== */
|
||||
.site__header {
|
||||
background-color: var(--bg-alt-color);
|
||||
}
|
||||
|
||||
.site__logo > img {
|
||||
height: 4rem;
|
||||
}
|
||||
|
||||
/* Site Nav
|
||||
========================================================================== */
|
||||
|
||||
.site__nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.site__logo,
|
||||
.nav__main,
|
||||
.nav__account {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
.site__logo,
|
||||
.nav__main,
|
||||
.nav__account {
|
||||
margin-right: 0;
|
||||
}
|
||||
.site__nav {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 0.5fr 0.5fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
.site__logo {
|
||||
grid-column: 1;
|
||||
}
|
||||
.nav__main {
|
||||
grid-column: 1/4;
|
||||
grid-row: 2;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.nav__account {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
.site__cart {
|
||||
grid-column: 3;
|
||||
grid-row: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 400px) {
|
||||
.site__nav {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
.site__logo {
|
||||
grid-column: span 2;
|
||||
justify-self: center;
|
||||
}
|
||||
.nav__main {
|
||||
grid-column: 1/3;
|
||||
grid-row: 3;
|
||||
}
|
||||
.nav__account {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
}
|
||||
.site__cart {
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.nav__list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.nav__menu {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav__link {
|
||||
text-transform: lowercase;
|
||||
font-variant: small-caps;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nav__list li:not(:last-child) {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.nav__list.menu {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.nav__dropdown {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: var(--bg-color);
|
||||
border: var(--default-border);
|
||||
padding: 0.5rem;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 400px) {
|
||||
.nav__dropdown {
|
||||
left: 0;
|
||||
right: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.nav__menu:hover .nav__dropdown {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Site Cart
|
||||
========================================================================== */
|
||||
.site__cart {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
justify-self: end;
|
||||
|
||||
font-family: inherit;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
text-transform: lowercase;
|
||||
font-variant: small-caps;
|
||||
|
||||
color: var(--fg-color);
|
||||
background-color: var(--yellow-color);
|
||||
padding: 0.25rem 1rem;
|
||||
border-radius: 0.2rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cart__count {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.cart__icon {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.cart__length {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
font-family: 'Inter', sans-serif;
|
||||
|
||||
/* ==========================================================================
|
||||
Articles
|
||||
========================================================================== */
|
||||
article > header {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.cart__item {
|
||||
.article__header--with-action {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
article + article {
|
||||
margin-top: 8rem;
|
||||
}
|
||||
|
||||
/* Product reviews
|
||||
========================================================================== */
|
||||
.review__list {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 5fr;
|
||||
gap: 1rem;
|
||||
padding: 2rem 0;
|
||||
border-bottom: 0.05rem solid var(--gray-color);
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 4rem;
|
||||
}
|
||||
|
||||
.cart__total_price {
|
||||
.review__list-short {
|
||||
margin: inherit auto;
|
||||
max-width: 64ch;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.review__item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.review__list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Products
|
||||
========================================================================== */
|
||||
.product__list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 4rem;
|
||||
}
|
||||
|
||||
.product__item {
|
||||
text-decoration: none;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.product__item h3 {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
.product__list {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 6rem;
|
||||
}
|
||||
.product__figure {
|
||||
grid-row: span 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.product__item {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.product__figure {
|
||||
grid-row: 1;
|
||||
justify-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Product Detail
|
||||
========================================================================== */
|
||||
.product__detail {
|
||||
display: grid;
|
||||
grid-template-columns: 0.25fr 2fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.gallery__thumbnail {
|
||||
border: var(--default-border);
|
||||
border-color: var(--bg-color);
|
||||
cursor: pointer;
|
||||
object-fit: cover;
|
||||
aspect-ratio: 1/1;
|
||||
}
|
||||
|
||||
.gallery__image {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.gallery__thumbnail--focus {
|
||||
border-color: var(--yellow-color);
|
||||
}
|
||||
|
||||
.product__form input,
|
||||
.product__form select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 700px) {
|
||||
.product__detail {
|
||||
grid-template-columns: 0.25fr 2fr;
|
||||
}
|
||||
|
||||
.product__info {
|
||||
grid-column: span 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 700px) {
|
||||
.product__detail {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 4rem 1fr auto;
|
||||
}
|
||||
.product__gallery {
|
||||
display: flex;
|
||||
}
|
||||
.gallery__thumbnail {
|
||||
max-height: 4rem;
|
||||
}
|
||||
.gallery__thumbnail:not(:last-child) {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
.product__image {
|
||||
max-height: 16rem;
|
||||
}
|
||||
.product__info {
|
||||
grid-column: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Shopping Cart
|
||||
========================================================================== */
|
||||
.cart__list {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.cart__item {
|
||||
padding: 1rem 0;
|
||||
border-bottom: var(--default-border);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 3fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.cart__table-wrapper {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.cart__summary {
|
||||
text-align: right;
|
||||
}
|
||||
.cart__totals {
|
||||
width: unset;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
.cart__total {
|
||||
|
||||
.cart__totals th,
|
||||
.cart__totals td {
|
||||
border: none;
|
||||
}
|
||||
.cart__totals th:first-child,
|
||||
.cart__totals td:first-child {
|
||||
text-align: right;
|
||||
|
||||
}
|
||||
|
||||
.cart__proceed {
|
||||
text-align: right;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.item__figure img {
|
||||
vertical-align: middle;
|
||||
.cart__proceed > .action-button {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.item__image {
|
||||
max-height: 12rem;
|
||||
}
|
||||
|
||||
.item__price {
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.item__form p,
|
||||
.coupon__form p {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.coupon__form p {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.item__form input[type=number] {
|
||||
max-width: 6rem;
|
||||
}
|
||||
|
||||
.coupon__form input[type=text] {
|
||||
max-width: 8rem;
|
||||
}
|
||||
|
||||
:is(.item__form, .coupon__form) label,
|
||||
:is(.item__form, .coupon__form) input:not(:last-child) {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
.cart__item {
|
||||
grid-template-columns: 2fr 1fr;
|
||||
}
|
||||
|
||||
.cart__proceed {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.item__info {
|
||||
grid-column: span 2;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.item__price {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.product__list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8rem;
|
||||
|
||||
/* Checkout / Shipping Address
|
||||
========================================================================== */
|
||||
.checkout__address-form input,
|
||||
.checkout__address-form select {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 24rem;
|
||||
}
|
||||
|
||||
.product__list-item {
|
||||
text-decoration: none;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
.checkout__address {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.product__list-item form {
|
||||
grid-column: 1/3;
|
||||
}
|
||||
|
||||
.product__list-item figure {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
#paypal-button-container {
|
||||
max-width: 24rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
|
||||
.order__shipping {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
|
||||
.order__total {
|
||||
margin: 3rem 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.order__details {
|
||||
/*margin: 3rem 0;*/
|
||||
|
||||
}
|
||||
/* ==========================================================================
|
||||
Footer
|
||||
========================================================================== */
|
||||
|
||||
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 {
|
||||
padding: 1rem 0;
|
||||
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;
|
||||
footer > section {
|
||||
text-align: center;
|
||||
|
||||
}
|
||||
|
||||
349
src/static/styles/normalize.css
vendored
Normal file
349
src/static/styles/normalize.css
vendored
Normal file
@ -0,0 +1,349 @@
|
||||
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
|
||||
|
||||
/* Document
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Correct the line height in all browsers.
|
||||
* 2. Prevent adjustments of font size after orientation changes in iOS.
|
||||
*/
|
||||
|
||||
html {
|
||||
line-height: 1.15; /* 1 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
}
|
||||
|
||||
/* Sections
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the margin in all browsers.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the `main` element consistently in IE.
|
||||
*/
|
||||
|
||||
main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the font size and margin on `h1` elements within `section` and
|
||||
* `article` contexts in Chrome, Firefox, and Safari.
|
||||
*/
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
|
||||
/* Grouping content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in Firefox.
|
||||
* 2. Show the overflow in Edge and IE.
|
||||
*/
|
||||
|
||||
hr {
|
||||
box-sizing: content-box; /* 1 */
|
||||
height: 0; /* 1 */
|
||||
overflow: visible; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
pre {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/* Text-level semantics
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the gray background on active links in IE 10.
|
||||
*/
|
||||
|
||||
a {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Remove the bottom border in Chrome 57-
|
||||
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
|
||||
*/
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: none; /* 1 */
|
||||
text-decoration: underline; /* 2 */
|
||||
text-decoration: underline dotted; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font weight in Chrome, Edge, and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent `sub` and `sup` elements from affecting the line height in
|
||||
* all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/* Embedded content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the border on images inside links in IE 10.
|
||||
*/
|
||||
|
||||
img {
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
/* Forms
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Change the font styles in all browsers.
|
||||
* 2. Remove the margin in Firefox and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit; /* 1 */
|
||||
font-size: 100%; /* 1 */
|
||||
line-height: 1.15; /* 1 */
|
||||
margin: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the overflow in IE.
|
||||
* 1. Show the overflow in Edge.
|
||||
*/
|
||||
|
||||
button,
|
||||
input { /* 1 */
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inheritance of text transform in Edge, Firefox, and IE.
|
||||
* 1. Remove the inheritance of text transform in Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select { /* 1 */
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the inability to style clickable types in iOS and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
[type="button"],
|
||||
[type="reset"],
|
||||
[type="submit"] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner border and padding in Firefox.
|
||||
*/
|
||||
|
||||
button::-moz-focus-inner,
|
||||
[type="button"]::-moz-focus-inner,
|
||||
[type="reset"]::-moz-focus-inner,
|
||||
[type="submit"]::-moz-focus-inner {
|
||||
border-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the focus styles unset by the previous rule.
|
||||
*/
|
||||
|
||||
button:-moz-focusring,
|
||||
[type="button"]:-moz-focusring,
|
||||
[type="reset"]:-moz-focusring,
|
||||
[type="submit"]:-moz-focusring {
|
||||
outline: 1px dotted ButtonText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the padding in Firefox.
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
padding: 0.35em 0.75em 0.625em;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the text wrapping in Edge and IE.
|
||||
* 2. Correct the color inheritance from `fieldset` elements in IE.
|
||||
* 3. Remove the padding so developers are not caught out when they zero out
|
||||
* `fieldset` elements in all browsers.
|
||||
*/
|
||||
|
||||
legend {
|
||||
box-sizing: border-box; /* 1 */
|
||||
color: inherit; /* 2 */
|
||||
display: table; /* 1 */
|
||||
max-width: 100%; /* 1 */
|
||||
padding: 0; /* 3 */
|
||||
white-space: normal; /* 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
|
||||
*/
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the default vertical scrollbar in IE 10+.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in IE 10.
|
||||
* 2. Remove the padding in IE 10.
|
||||
*/
|
||||
|
||||
[type="checkbox"],
|
||||
[type="radio"] {
|
||||
box-sizing: border-box; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the cursor style of increment and decrement buttons in Chrome.
|
||||
*/
|
||||
|
||||
[type="number"]::-webkit-inner-spin-button,
|
||||
[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the odd appearance in Chrome and Safari.
|
||||
* 2. Correct the outline style in Safari.
|
||||
*/
|
||||
|
||||
[type="search"] {
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
outline-offset: -2px; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner padding in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inability to style clickable types in iOS and Safari.
|
||||
* 2. Change font properties to `inherit` in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
}
|
||||
|
||||
/* Interactive
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* Add the correct display in Edge, IE 10+, and Firefox.
|
||||
*/
|
||||
|
||||
details {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/*
|
||||
* Add the correct display in all browsers.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/* Misc
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 10+.
|
||||
*/
|
||||
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 10.
|
||||
*/
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
@ -1,31 +1,37 @@
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
from django.conf import settings
|
||||
from core.models import Product, OrderLine
|
||||
from core.models import Product, OrderLine, Coupon
|
||||
from .payments import CreateOrder
|
||||
|
||||
from core import (
|
||||
DiscountValueType,
|
||||
VoucherType,
|
||||
TransactionStatus,
|
||||
OrderStatus,
|
||||
ShippingMethodType
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class Cart:
|
||||
def __init__(self, request):
|
||||
self.session = request.session
|
||||
self.coupon_code = self.session.get('coupon_code')
|
||||
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='', update_quantity=False):
|
||||
def add(self, product, quantity=1, grind='', update_quantity=False):
|
||||
product_id = str(product.id)
|
||||
if product_id not in self.cart:
|
||||
self.cart[product_id] = {
|
||||
'quantity': 0,
|
||||
'roast': roast,
|
||||
'grind': grind,
|
||||
'price': str(product.price)
|
||||
}
|
||||
elif product_id in self.cart:
|
||||
self.cart[product_id].update({
|
||||
'roast': roast,
|
||||
})
|
||||
|
||||
if update_quantity:
|
||||
self.cart[product_id]['quantity'] = quantity
|
||||
else:
|
||||
@ -62,14 +68,16 @@ class Cart:
|
||||
|
||||
def clear(self):
|
||||
del self.session[settings.CART_SESSION_ID]
|
||||
del self.session['coupon_code']
|
||||
self.session.modified = True
|
||||
|
||||
def build_order_params(self):
|
||||
return \
|
||||
{
|
||||
'items': self,
|
||||
'total_price': f'{self.get_total_price()}',
|
||||
'total_price': f'{self.get_total_price_after_discount()}',
|
||||
'item_total': f'{self.get_total_price()}',
|
||||
'discount': f'{self.get_discount()}',
|
||||
'shipping_price': '0',
|
||||
'tax_total': '0',
|
||||
'shipping_method': 'US POSTAL SERVICE',
|
||||
@ -86,7 +94,7 @@ class Cart:
|
||||
bulk_list = [OrderLine(
|
||||
order=order,
|
||||
product=item['product'],
|
||||
customer_note=item['roast'],
|
||||
customer_note=item['grind'],
|
||||
unit_price=item['price'],
|
||||
quantity=item['quantity'],
|
||||
tax_rate=2,
|
||||
@ -105,16 +113,19 @@ class Cart:
|
||||
'country_code': 'US'
|
||||
}
|
||||
|
||||
# @property
|
||||
# def coupon(self):
|
||||
# if self.coupon_id:
|
||||
# return Coupon.objects.get(id=self.coupon_id)
|
||||
# return None
|
||||
@property
|
||||
def coupon(self):
|
||||
if self.coupon_code:
|
||||
return Coupon.objects.get(code=self.coupon_code)
|
||||
return None
|
||||
|
||||
# def get_discount(self):
|
||||
# if self.coupon:
|
||||
# return (self.coupon.discount / Decimal('100')) * self.get_total_price()
|
||||
# return Decimal('0')
|
||||
def get_discount(self):
|
||||
if self.coupon:
|
||||
if self.coupon.discount_value_type == DiscountValueType.FIXED:
|
||||
return round(self.coupon.discount_value, 2)
|
||||
elif self.coupon.discount_value_type == DiscountValueType.PERCENTAGE:
|
||||
return round((self.coupon.discount_value / Decimal('100')) * self.get_total_price(), 2)
|
||||
return Decimal('0')
|
||||
|
||||
# def get_total_price_after_discount(self):
|
||||
# return self.get_total_price() - self.get_discount()
|
||||
def get_total_price_after_discount(self):
|
||||
return round(self.get_total_price() - self.get_discount(), 2)
|
||||
|
||||
@ -5,6 +5,8 @@ from django.core.mail import EmailMessage
|
||||
from core.models import Order
|
||||
from accounts import STATE_CHOICES
|
||||
|
||||
from .tasks import contact_form_email
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class AddToCartForm(forms.Form):
|
||||
@ -17,7 +19,7 @@ class AddToCartForm(forms.Form):
|
||||
AEROPRESS = 'AeroPress'
|
||||
PERCOLATOR = 'Percolator'
|
||||
OTHER = 'Other'
|
||||
ROAST_CHOICES = [
|
||||
GRIND_CHOICES = [
|
||||
(WHOLE, 'Whole Beans'),
|
||||
(ESPRESSO, 'Espresso'),
|
||||
(CONE_DRIP, 'Cone Drip'),
|
||||
@ -28,9 +30,49 @@ class AddToCartForm(forms.Form):
|
||||
(PERCOLATOR, 'Percolator'),
|
||||
(OTHER, 'Other (enter below)')
|
||||
]
|
||||
|
||||
grind = forms.ChoiceField(choices=GRIND_CHOICES)
|
||||
quantity = forms.IntegerField(min_value=1, max_value=20, initial=1)
|
||||
|
||||
class UpdateCartItemForm(forms.Form):
|
||||
quantity = forms.IntegerField(min_value=1, max_value=20, initial=1)
|
||||
update = forms.BooleanField(required=False, initial=True, widget=forms.HiddenInput)
|
||||
|
||||
|
||||
class AddToSubscriptionForm(forms.Form):
|
||||
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'
|
||||
GRIND_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)')
|
||||
]
|
||||
|
||||
SEVEN_DAYS = 7
|
||||
FOURTEEN_DAYS = 14
|
||||
THIRTY_DAYS = 30
|
||||
SCHEDULE_CHOICES = [
|
||||
(SEVEN_DAYS, 'Every 7 days'),
|
||||
(FOURTEEN_DAYS, 'Every 14 days'),
|
||||
(THIRTY_DAYS, 'Every 30 days'),
|
||||
]
|
||||
|
||||
quantity = forms.IntegerField(min_value=1, initial=1)
|
||||
roast = forms.ChoiceField(choices=ROAST_CHOICES)
|
||||
update = forms.BooleanField(required=False, initial=False, widget=forms.HiddenInput)
|
||||
grind = forms.ChoiceField(choices=GRIND_CHOICES)
|
||||
schedule = forms.ChoiceField(choices=SCHEDULE_CHOICES)
|
||||
|
||||
|
||||
class AddressForm(forms.Form):
|
||||
@ -53,8 +95,43 @@ class OrderCreateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Order
|
||||
fields = (
|
||||
'coupon',
|
||||
'total_net_amount',
|
||||
)
|
||||
widgets = {
|
||||
'coupon': forms.HiddenInput(),
|
||||
'total_net_amount': forms.HiddenInput()
|
||||
}
|
||||
|
||||
class CouponApplyForm(forms.Form):
|
||||
code = forms.CharField(label='Coupon code')
|
||||
|
||||
class ContactForm(forms.Form):
|
||||
GOOGLE = 'Google Search'
|
||||
SHOP = 'The coffee shop'
|
||||
WOM = 'Word of mouth'
|
||||
PRODUCT = 'Coffee Bag'
|
||||
STORE = 'Store'
|
||||
OTHER = 'Other'
|
||||
|
||||
REFERAL_CHOICES = [
|
||||
(GOOGLE, 'Google Search'),
|
||||
(SHOP, 'Better Living Through Coffee coffee shop'),
|
||||
(WOM, 'Friend/Relative'),
|
||||
(PRODUCT, 'Our Coffee Bag'),
|
||||
(STORE, 'PT Food Coop/other store'),
|
||||
(OTHER, 'Other (please describe in the Message section below'),
|
||||
]
|
||||
|
||||
first_name = forms.CharField()
|
||||
last_name = forms.CharField()
|
||||
email_address = forms.EmailField()
|
||||
referal = forms.ChoiceField(
|
||||
label='How did you find our website?',
|
||||
choices=REFERAL_CHOICES
|
||||
)
|
||||
subject = forms.CharField()
|
||||
message = forms.CharField(widget=forms.Textarea)
|
||||
|
||||
def send_email(self):
|
||||
contact_form_email.delay(self.cleaned_data)
|
||||
|
||||
@ -112,10 +112,10 @@ class CreateOrder(PayPalClient):
|
||||
"currency_code": "USD",
|
||||
"value": params['tax_total']
|
||||
},
|
||||
# "shipping_discount": {
|
||||
# "currency_code": "USD",
|
||||
# "value": "10"
|
||||
# }
|
||||
"discount": {
|
||||
"currency_code": "USD",
|
||||
"value": params['discount']
|
||||
}
|
||||
}
|
||||
},
|
||||
"items": processed_items,
|
||||
|
||||
@ -1,26 +1,22 @@
|
||||
# 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 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 templated_email import send_templated_mail
|
||||
|
||||
# from core.models import Order
|
||||
|
||||
# logger = get_task_logger(__name__)
|
||||
logger = get_task_logger(__name__)
|
||||
|
||||
|
||||
# CONFIRM_ORDER_TEMPLATE = 'storefront/order_confirmation'
|
||||
# ORDER_CANCEl_TEMPLATE = 'storefront/order_cancel'
|
||||
# ORDER_REFUND_TEMPLATE = 'storefront/order_refund'
|
||||
COTACT_FORM_TEMPLATE = 'storefront/contact_form'
|
||||
|
||||
# @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
|
||||
# )
|
||||
@shared_task(name='contact_form_email')
|
||||
def contact_form_email(formdata):
|
||||
send_templated_mail(
|
||||
template_name=COTACT_FORM_TEMPLATE,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
recipient_list=[settings.DEFAULT_CONTACT_EMAIL],
|
||||
context=formdata
|
||||
)
|
||||
|
||||
# logger.info(f"Order confirmation email sent to {order['email']}")
|
||||
logger.info(f"Contact form email sent from {formdata['email_address']}")
|
||||
|
||||
@ -3,10 +3,14 @@
|
||||
|
||||
{% block content %}
|
||||
<article>
|
||||
<h1>About PT Coffee</h1>
|
||||
<figure>
|
||||
<img src="{% static 'images/coffee_banner.jpeg' %}" alt="">
|
||||
</figure>
|
||||
<header>
|
||||
<h1>About PT Coffee</h1>
|
||||
</header>
|
||||
<section>
|
||||
<figure>
|
||||
<img src="{% static 'images/coffee_banner.jpeg' %}" alt="">
|
||||
</figure>
|
||||
</section>
|
||||
<section>
|
||||
<h2>We love coffee!</h2>
|
||||
<p><strong>If you’ve found Port Townsend Coffee Roasting Co., you probably love coffee so much that you seek out the best tasting, Certified Fair Trade Organic coffees available.</strong></p>
|
||||
@ -27,7 +31,7 @@
|
||||
<p>We roast our coffee much more slowly than most roasters, especially coffee roasters in the Pacific Northwest. The slower roasting process allows for greater bean development. The bean is evenly roasted right to the center and our process mutes some of the acidic compounds, which smooths out the flavor.</p>
|
||||
<p>Port Townsend Coffee is known for roasts that are darker than others available in the Northwest. Due to our roasting process, which emphasizes patience with the beans as well as air flow adjustments, the darker roasts are smooth and syrupy. Dark roasts from other companies can taste bitter, or slightly burnt due to the size and speed of the roast.</p>
|
||||
<p>We have been buying beans from the same brokers for many years. They understand the flavor profiles we prefer and seek to accommodate our needs.</p>
|
||||
<div class="column__layout">
|
||||
<div>
|
||||
<h5>Fair Trade and Organic</h5>
|
||||
<p>We pay a steep premium for these beans, which are typically from smaller farms that are organized into co-ops. These farms take pride in their coffees, as the farmers make a living wage and their families are able to live in a healthier, more secure environment than farmers who grow a conventional coffee crop. The quality of our coffee is consistent, in part due to the quality of organic and fair trade beans.</p>
|
||||
<h5>Freshness and Storage</h5>
|
||||
|
||||
19
src/storefront/templates/storefront/address_form.html
Normal file
19
src/storefront/templates/storefront/address_form.html
Normal file
@ -0,0 +1,19 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<article>
|
||||
<header>
|
||||
<p><a href="{% url 'storefront:customer-detail' user.pk %}">← Back</a></p>
|
||||
<h1>Update Address</h1>
|
||||
</header>
|
||||
<section>
|
||||
<form method="post" action="{% url 'storefront:address-update' customer.pk address.pk %}">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<p>
|
||||
<input type="submit" value="Save changes">
|
||||
</p>
|
||||
</form>
|
||||
</section>
|
||||
</article>
|
||||
{% endblock %}
|
||||
@ -2,36 +2,73 @@
|
||||
|
||||
{% block content %}
|
||||
<article>
|
||||
<h2>Shopping Cart</h2>
|
||||
<section>
|
||||
<header>
|
||||
<h1>Shopping Cart</h1>
|
||||
</header>
|
||||
<section class="cart__list">
|
||||
{% for item in cart %}
|
||||
<div class="cart__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}}">
|
||||
<img class="item__image" src="{{product.productphoto_set.first.image.url}}" alt="{{product.productphoto_set.first.image}}">
|
||||
</figure>
|
||||
<div class="item__details">
|
||||
<p>{{product.name}}<br> ${{item.price}}</p>
|
||||
<div class="item__info">
|
||||
<h4>{{product.name}}</h4>
|
||||
<p><strong>Grind</strong>: {{item.grind}}</p>
|
||||
<form class="item__form" action="{% url 'storefront:cart-update' product.pk %}" method="POST">
|
||||
{% csrf_token %}
|
||||
<p>
|
||||
{{ item.update_quantity_form }}
|
||||
<input type="submit" value="Update">
|
||||
</p>
|
||||
</form>
|
||||
<p>
|
||||
<a href="{% url 'storefront:cart-remove' product.pk %}">Remove from cart</a>
|
||||
</p>
|
||||
<form class="product__form" action="{% url 'storefront:cart-add' product.pk %}" method="POST">
|
||||
{% csrf_token %}
|
||||
{{ item.update_quantity_form }}
|
||||
<input type="submit" value="Update">
|
||||
</form>
|
||||
</div>
|
||||
<div class="item__price">
|
||||
<p><strong>${{item.price}}</strong></p>
|
||||
</div>
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% empty %}
|
||||
<p>No items in cart yet.</p>
|
||||
<div>
|
||||
<p>No items in cart yet.</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
<section>
|
||||
<p class="cart__total_price">Cart total: <strong>${{cart.get_total_price}}</strong></p>
|
||||
<p class="cart__total">
|
||||
<a href="{% url 'storefront:product-list' %}">Continue Shopping</a> or <a class="action-button action-button--large" href="{% url 'storefront:checkout-address' %}">Proceed to Checkout</a>
|
||||
</p>
|
||||
<section class="cart__summary">
|
||||
<h4>Cart Totals</h4>
|
||||
<div>
|
||||
<form class="coupon__form" action="{% url 'storefront:coupon-apply' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<p>
|
||||
{{ coupon_apply_form }}
|
||||
<input type="submit" value="Apply" class="action-button">
|
||||
</p>
|
||||
</form>
|
||||
<div class="cart__table-wrapper">
|
||||
<table class="cart__totals">
|
||||
<tr>
|
||||
<td>Subtotal</td>
|
||||
<td>${{cart.get_total_price|floatformat:"2"}}</td>
|
||||
</tr>
|
||||
{% if cart.coupon %}
|
||||
<tr>
|
||||
<td>Coupon</td>
|
||||
<td>{{cart.coupon.discount_value}} {{cart.coupon.get_discount_value_type_display}}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th>Total</th>
|
||||
<td><strong>${{cart.get_total_price_after_discount|floatformat:"2"}}</strong></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<p class="cart__proceed">
|
||||
<a href="{% url 'storefront:product-list' %}">Continue Shopping</a>{% if cart|length > 0 %} or <a class="action-button" href="{% url 'storefront:checkout-address' %}">Proceed to Checkout</a>{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
{% endblock %}
|
||||
|
||||
@ -3,16 +3,18 @@
|
||||
|
||||
{% block content %}
|
||||
<article>
|
||||
<h2>Checkout</h2>
|
||||
<section class="order__details">
|
||||
<div class="order__shipping">
|
||||
<h3>Shipping Address</h3>
|
||||
<form action="" method="POST" class="address__form">
|
||||
{% csrf_token %}
|
||||
{{form.as_p}}
|
||||
<header>
|
||||
<h1>Checkout</h1>
|
||||
</header>
|
||||
<section>
|
||||
<h3>Shipping Address</h3>
|
||||
<form class="checkout__address-form" action="" method="POST">
|
||||
{% csrf_token %}
|
||||
{{form.as_p}}
|
||||
<p>
|
||||
<input type="submit" value="Continue to Payment">
|
||||
</form>
|
||||
</div>
|
||||
</p>
|
||||
</form>
|
||||
</section>
|
||||
</article>
|
||||
{% endblock %}
|
||||
|
||||
@ -1,17 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<article>
|
||||
<h2>Checkout</h2>
|
||||
<section class="order__details">
|
||||
<div class="order__shipping">
|
||||
<h3>Shipping Method</h3>
|
||||
<form action="post" class="address__form">
|
||||
{{form.as_p}}
|
||||
<input type="submit" value="Review order">
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
{% endblock %}
|
||||
18
src/storefront/templates/storefront/contact_form.html
Normal file
18
src/storefront/templates/storefront/contact_form.html
Normal file
@ -0,0 +1,18 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<article>
|
||||
<header>
|
||||
<h1>Contact us</h1>
|
||||
</header>
|
||||
<section>
|
||||
<form action="{% url 'storefront:contact' %}" method="post">
|
||||
{% csrf_token %}
|
||||
{{form.as_p}}
|
||||
<p>
|
||||
<input type="submit" value="Send message">
|
||||
</p>
|
||||
</form>
|
||||
</section>
|
||||
</article>
|
||||
{% endblock %}
|
||||
@ -3,21 +3,19 @@
|
||||
|
||||
{% block content %}
|
||||
<article>
|
||||
<header class="object__header">
|
||||
<header class="article__header--with-action">
|
||||
<h1>{{customer.get_full_name}}</h1>
|
||||
<a href="{% url 'storefront:customer-update' customer.pk %}" class="action-button">Edit profile</a>
|
||||
</header>
|
||||
<section class="object__panel">
|
||||
<div class="object__item object__item--header">
|
||||
<h4>Info</h4>
|
||||
</div>
|
||||
<div class="panel__item">
|
||||
<section>
|
||||
<h4>Info</h4>
|
||||
<p>
|
||||
<strong>Email address</strong><br>
|
||||
{{customer.email}}<br>
|
||||
<a href="{% url 'account_email' %}">Manage</a>
|
||||
</div>
|
||||
<div class="panel__item">
|
||||
<strong>Default shipping address</strong><br>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Default shipping address</strong>
|
||||
{% with shipping_address=customer.default_shipping_address %}
|
||||
<address>
|
||||
{{shipping_address.first_name}}
|
||||
@ -28,11 +26,10 @@
|
||||
{% 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>
|
||||
</p>
|
||||
<div>
|
||||
<p><strong>All addresses</strong></p>
|
||||
{% for address in customer.addresses.all %}
|
||||
<p>
|
||||
<address>
|
||||
@ -44,34 +41,38 @@
|
||||
{% endif %}
|
||||
{{address.city}}, {{address.state}}, {{address.postal_code}}
|
||||
</address>
|
||||
<a href="address-update">Edit</a>
|
||||
<a href="{% url 'storefront:address-update' customer.pk address.pk %}">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="">
|
||||
<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 %}
|
||||
{% with order_list=customer.orders.all %}
|
||||
<section>
|
||||
<h3>Your orders</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Order #</th>
|
||||
<th>Date</th>
|
||||
<th colspan="2">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for order in order_list %}
|
||||
<tr>
|
||||
<td>#{{order.pk}}</td>
|
||||
<td>{{order.created_at|date:"M j, Y"}}</td>
|
||||
<td>${{order.total_net_amount}}</td>
|
||||
<td><a href="{% url 'storefront:order-detail' customer.pk order.pk %}">See details →</a></td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<span class="object__item">No orders</span>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
{% endwith %}
|
||||
</article>
|
||||
{% endblock content %}
|
||||
|
||||
@ -1,14 +1,17 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<article class="product">
|
||||
<h1>Update your profile</h1>
|
||||
<article>
|
||||
<header>
|
||||
<p><a href="{% url 'storefront:customer-detail' customer.pk %}">← Back</a></p>
|
||||
<h1>Update your profile</h1>
|
||||
</header>
|
||||
<section>
|
||||
<form method="POST" action="{% url 'storefront: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 'storefront:customer-detail' customer.pk %}">cancel</a>
|
||||
<p>
|
||||
<input type="submit" value="Save changes">
|
||||
</p>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
63
src/storefront/templates/storefront/order_detail.html
Normal file
63
src/storefront/templates/storefront/order_detail.html
Normal file
@ -0,0 +1,63 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<article>
|
||||
<p><a href="{% url 'storefront:customer-detail' customer.pk %}">← Back</a></p>
|
||||
<header>
|
||||
<h1>Order #{{order.pk}}</h1>
|
||||
<h3>Placed on {{order.created_at|date:"M j, Y"}}</h3>
|
||||
</header>
|
||||
<section>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">Product</th>
|
||||
<th>Quantity</th>
|
||||
<th>Price</th>
|
||||
<th>Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in order.lines.all %}
|
||||
<tr>
|
||||
{% with product=item.product %}
|
||||
<td>
|
||||
<img src="{{product.productphoto_set.first.image.url}}" alt="{{product.productphoto_set.first.image}}">
|
||||
</td>
|
||||
<td><strong>{{product.name}}</strong></td>
|
||||
<td>{{item.quantity}}</td>
|
||||
<td>${{product.price}}</td>
|
||||
<td>${{item.get_total}}</td>
|
||||
{% endwith %}
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5">No items in order</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h4>Payment</h4>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Subtotal</td>
|
||||
<td>${{order.total_net_amount}}</td>
|
||||
</tr>
|
||||
{% if order.coupon %}
|
||||
<tr>
|
||||
<td>Discount</td>
|
||||
<td>{{order.coupon.discount_value}} {{order.coupon.get_discount_value_type_display}}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th>Total</th>
|
||||
<td><strong>${{order.get_total_price_after_discount}}</strong></td>
|
||||
</tr>
|
||||
</table>
|
||||
</section>
|
||||
</article>
|
||||
{% endblock content %}
|
||||
@ -2,53 +2,73 @@
|
||||
{% load static %}
|
||||
|
||||
{% block head %}
|
||||
<script defer src="https://www.paypal.com/sdk/js?client-id=AVELl5svEDTPo3FRNViyjWnqODfFsAHbKKdm5BlwpKCwZCVxTraFF9Ax3N1xlkoEWSpOj7AI_T9xYMRu¤cy=USD"></script>
|
||||
<script defer src="https://www.paypal.com/sdk/js?client-id={{PAYPAL_CLIENT_ID}}¤cy=USD"></script>
|
||||
<script type="module" defer src="{% static 'scripts/payment.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<article>
|
||||
<h2>Checkout</h2>
|
||||
<section class="order__details">
|
||||
<div class="order__shipping">
|
||||
<h3>Shipping Address</h3>
|
||||
<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 class="action-button" href="{% url 'storefront:checkout-address' %}">edit</a>
|
||||
</div>
|
||||
<div class="order__list">
|
||||
<h3>Cart Summary</h3>
|
||||
{% for item in cart %}
|
||||
<div class="cart__item">
|
||||
{% with product=item.product %}
|
||||
<figure class="item__figure">
|
||||
{{item.quantity}} x
|
||||
<img class="product__image product__image--small" src="{{product.productphoto_set.first.image.url}}" alt="{{product.productphoto_set.first.image}}">
|
||||
</figure>
|
||||
<div class="item__details">
|
||||
<p>{{product.name}}<br> ${{item.price}}</p>
|
||||
<p><strong>Grind options</strong>: {{item.customer_note}}</p>
|
||||
</div>
|
||||
{% endwith %}
|
||||
<header>
|
||||
<h1>Checkout</h1>
|
||||
</header>
|
||||
<section class="checkout__address">
|
||||
<h3>Shipping address</h3>
|
||||
<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 class="action-button" href="{% url 'storefront:checkout-address' %}">Change</a>
|
||||
</section>
|
||||
<section class="cart__list">
|
||||
<h3>Review items</h3>
|
||||
{% for item in cart %}
|
||||
<div class="cart__item">
|
||||
{% with product=item.product %}
|
||||
<figure>
|
||||
<img src="{{product.productphoto_set.first.image.url}}" alt="{{product.productphoto_set.first.image}}">
|
||||
</figure>
|
||||
<div>
|
||||
<h4>{{product.name}}</h4>
|
||||
<p><strong>Grind options</strong>: {{item.grind}}</p>
|
||||
<p><strong>Quantity</strong>: {{item.quantity}}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="item__price">
|
||||
<p><strong>${{item.price}}</strong></p>
|
||||
</div>
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="order__total">
|
||||
<form action="" method="POST" class="order__form">
|
||||
{% csrf_token %}
|
||||
{{form.as_p}}
|
||||
{# <input type="submit" value="Place order"> #}
|
||||
</form>
|
||||
<h4>Total: ${{cart.get_total_price}}</h4>
|
||||
<div id="paypal-button-container"></div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
<section class="cart__summary">
|
||||
<h3>Order summary</h3>
|
||||
<form action="" method="POST">
|
||||
{% csrf_token %}
|
||||
{{form.as_p}}
|
||||
</form>
|
||||
<div class="cart__table-wrapper">
|
||||
<table class="cart__totals">
|
||||
<tr>
|
||||
<td>Subtotal</td>
|
||||
<td>${{cart.get_total_price|floatformat:"2"}}</td>
|
||||
</tr>
|
||||
{% if cart.coupon %}
|
||||
<tr>
|
||||
<td>Coupon</td>
|
||||
<td>{{cart.coupon.discount_value}} {{cart.coupon.get_discount_value_type_display}}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th>Total</th>
|
||||
<td><strong>${{cart.get_total_price_after_discount|floatformat:"2"}}</strong></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div id="paypal-button-container"></div>
|
||||
</section>
|
||||
</article>
|
||||
{% endblock %}
|
||||
|
||||
@ -1,24 +1,33 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block head %}
|
||||
<script defer src="{% static 'scripts/product_gallery.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<article class="product__item">
|
||||
<figure class="product__figure">
|
||||
<img class="product__image" src="{{product.productphoto_set.first.image.url}}" alt="{{product.productphoto_set.first.image}}">
|
||||
<article class="product__detail">
|
||||
<section class="product__gallery">
|
||||
{% for photo in product.productphoto_set.all %}
|
||||
<img class="gallery__thumbnail {% if forloop.first %}gallery__thumbnail--focus{% endif %}" src="{{ photo.image.url }}" alt="{{ photo }}">
|
||||
{% endfor %}
|
||||
</section>
|
||||
<figure class="gallery__image">
|
||||
<img class="product__image" src="{{ product.productphoto_set.first.image.url }}" alt="{{ product.name }}">
|
||||
</figure>
|
||||
<section>
|
||||
<section class="product__info">
|
||||
<h1>{{product.name}}</h1>
|
||||
<p>{{product.description}}</p>
|
||||
<p class="site__ft-stamp"><img class="fair_trade--small" src="{% static 'images/fair_trade_stamp.png' %}" alt=""></p>
|
||||
<p>$<strong>{{product.price}}</strong></p>
|
||||
<p>{{product.weight.oz}} oz</p>
|
||||
<form method="post" action="{% url 'storefront:cart-add' product.pk %}">
|
||||
<form class="product__form" method="post" action="{% url 'storefront:cart-add' product.pk %}">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<p>
|
||||
<input type="submit" value="Add to cart" class="action-button">
|
||||
<input type="submit" value="Add to cart">
|
||||
</p>
|
||||
</form>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@ -2,24 +2,54 @@
|
||||
|
||||
{% block content %}
|
||||
<article>
|
||||
<header>
|
||||
<h1>Coffee</h1>
|
||||
</header>
|
||||
<section class="product__list">
|
||||
{% for product in product_list %}
|
||||
<a href="{% url 'storefront:product-detail' product.pk %}" class="product__list-item">
|
||||
<a class="product__item" href="{% url 'storefront: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>
|
||||
<div>
|
||||
<h3>{{ product.name }}</h3><br>
|
||||
<h3>$<strong>{{product.price}}</strong></h3>
|
||||
<p>{{product.description}}</p>
|
||||
<h3>{{ product.name }}</h3>
|
||||
<p>{{product.description|truncatewords:20}}</p>
|
||||
<p>$<strong>{{product.price}}</strong> | {{product.weight.oz}} oz</p>
|
||||
</div>
|
||||
<button class="action-button">View product</button>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</section>
|
||||
</article>
|
||||
<article class="review__list-short">
|
||||
<header>
|
||||
<h2>What people are saying</h2>
|
||||
</header>
|
||||
<section>
|
||||
<blockquote class="review__item">
|
||||
<q>Really good coffee. That's all there is to say. Supposedly the pour over coffee is the best way to go. It is definitely nothing like a Starbucks, and in this case, that's a very good thing!</q>
|
||||
<cite>-Mark Nickel</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>They have a passion for their coffee and chai. What makes them interesting is the intensity of flavor and not the extreme sweetness that other coffee shops focus on. The mocha had the velvety bitterness of true cocoa and the chai was more reminiscent of walking through an Indian spice store. These are drinks that should be sipped while sitting quietly at one of their picnic tables looking out at the water.</q>
|
||||
<cite>-Natasha Hughes</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Morning started with a Caffè Arancia and a scone. Couldn't have gotten anything better. Superb flavor for the coffee, a delight to drink.</q>
|
||||
<cite>-Mr Ty</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Warm and welcoming place with the best Americano I’ve ever had.</q>
|
||||
<cite>-Mathew Metcalfe</cite>
|
||||
</blockquote>
|
||||
</section>
|
||||
<section>
|
||||
<p class="review__item">
|
||||
<a href="{% url 'storefront:reviews' %}" class="action-button">Read more →</a>
|
||||
</p>
|
||||
</section>
|
||||
</article>
|
||||
{% endblock %}
|
||||
|
||||
{% block footer %}
|
||||
<div class="_form_1"></div><script src="https://bltc999.activehosted.com/f/embed.php?id=1" type="text/javascript" charset="utf-8"></script>
|
||||
{% endblock footer %}
|
||||
|
||||
771
src/storefront/templates/storefront/reviews.html
Normal file
771
src/storefront/templates/storefront/reviews.html
Normal file
@ -0,0 +1,771 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<article>
|
||||
<header>
|
||||
<h2>Reviews</h2>
|
||||
</header>
|
||||
<section class="review__list">
|
||||
<blockquote class="review__item">
|
||||
<q>Really good coffee. That's all there is to say. Supposedly the pour over coffee is the best way to go. It is definitely nothing like a Starbucks, and in this case, that's a very good thing!</q>
|
||||
<cite>-Mark Nickel</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>They have a passion for their coffee and chai. What makes them interesting is the intensity of flavor and not the extreme sweetness that other coffee shops focus on. The mocha had the velvety bitterness of true cocoa and the chai was more reminiscent of walking through an Indian spice store. These are drinks that should be sipped while sitting quietly at one of their picnic tables looking out at the water.</q>
|
||||
<cite>-Natasha Hughes</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Morning started with a Caffè Arancia and a scone. Couldn't have gotten anything better. Superb flavor for the coffee, a delight to drink.</q>
|
||||
<cite>-Mr Ty</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Warm and welcoming place with the best Americano I’ve ever had.</q>
|
||||
<cite>-Mathew Metcalfe</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Go to this coffee shop if you want great coffee and a pleasant atmosphere. The caramel latte I got handed had a great consistency, perfect amount of foam, and excellent taste. The staff know how to make good coffee.</q>
|
||||
<cite>-Nate Sullivan</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>The absolute best coffee house in town! Don't worry about the line, the wait is well worth it.</q>
|
||||
<cite>-Brian</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>This is my first stop every time I go through town. The coffee is amazing! The food is amazing! The people are amazing! Seriously wish we had a sister shop over in Seattle.</q>
|
||||
<cite>-Steve Cragar</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>We've tried four different coffee shops in Port Townsend and this one is simply the best of those we've visited. Sometimes, there's a bit of a line but that's an indication of a popular spot and it's well worth the wait. Definitely would recommend you try them if you've never been.</q>
|
||||
<cite>-William Jones</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>If I did nothing else in Port Townsend except visit this coffee shop, it would have been worth the trip. By far one of the best lattes I've ever had, even with non-dairy milk</q>
|
||||
<cite>-Keera Lindenberg</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>The espresso was so good, we kept coming back and bought a bag of beans.</q>
|
||||
<cite>-Brett Melton</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>This coffee shop is BY FAR the best coffee shop I have ever been to - and I have been to coffee shops around the world. Inexpensive, organic and absolutely delicious espresso drinks</q>
|
||||
<cite>-karolina anderson</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>I don’t get out to Port Townsend often but every time I do I make a point to stop at Better Living Through Coffee. Their Caffè Arancia is one of the best coffee beverages I’ve ever had.</q>
|
||||
<cite>-Brenton Woodrow</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>This was the best cup of coffee I've ever had. I go for Americano every time. It is truly about the coffee.</q>
|
||||
<cite>-Jeri Baird</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Stopped by on my way to oyster run last year and it was the best coffee experience ive ever had. It was an amazing cup of drip coffee</q>
|
||||
<cite>-james williams</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>One of those hidden local gems that serve the best coffee</q>
|
||||
<cite>-michael peterson</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>This place has the best iced coffee I have ever had.</q>
|
||||
<cite>-Troy Goracke</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>One of the best coffee shops in the country!</q>
|
||||
<cite>-Brian Rohr</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>The x4 shot iced coffee will forever leave me chasing the dragon. It was richly coffee flavored with out being bitter ... unmatched the world over. I'll be back every time I visit Washington!</q>
|
||||
<cite>-Conner Morlang</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>When visiting Port Townsend, this is THE place to get coffee. The coffee is outstanding and offered in a variety of ways.</q>
|
||||
<cite>-kathleen b</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Amazing coffee at prices lower than their competitors.</q>
|
||||
<cite>-Monique Ermine</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Just the best dang pour over coffee</q>
|
||||
<cite>-Jonquil Elise Dreadful</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>The best coffee very tasty.</q>
|
||||
<cite>-Audy Vasquez-Ramirez</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Outstanding Coffee service and ambiance.</q>
|
||||
<cite>-Mimi Magoo</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>A... MAZ...ING! Dantes coffee pour over is stellar.</q>
|
||||
<cite>-Jeremy Bryant</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Excellent coffee and service. I drove 90 miles for Sofia's Sumatra.</q>
|
||||
<cite>-Ian Roberts</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Best coffee in Port Townsend.</q>
|
||||
<cite>-Anna Stenwick</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Great view and outside seating area. Plus the coffee was amazing and top notch service.</q>
|
||||
<cite>-Laura McKinney</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Delicious coffee. Fresh, local, organic ingredients. So nice to sit and enjoy the waterfront view.</q>
|
||||
<cite>-R_ V_B</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>I had one of the best mugs of pour over coffee I've ever tasted.</q>
|
||||
<cite>-Ken Hoekema</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>A must stop in Port Townsend!! The coffee drink was amazing</q>
|
||||
<cite>-Patty Glenn</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Really great cup of espresso!</q>
|
||||
<cite>-Vicky G</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>They really make good coffee. Quality beans for sure.</q>
|
||||
<cite>-Jason Everett</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Pour over specialty here. 10/10 on coffee</q>
|
||||
<cite>-Olive Cattau</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Seriously good coffee, comfortable welcoming atmosphere</q>
|
||||
<cite>-Eric Mager</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Excellent coffee delivered by friendly people in a cool environment</q>
|
||||
<cite>-Mr T</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>This has ended up the place we end up going to every trip to Port Townsend. Awesome coffees and drinks,</q>
|
||||
<cite>-Sean McNulty</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Beautiful view, amazing coffee and the friendliest staff! Highly recommended</q>
|
||||
<cite>-SCH Travel</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Very hip, warm atmosphere, great views and wonderful coffee.</q>
|
||||
<cite>-Toke Beard</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Funky, friendly atmosphere and a great cup of coffee, freshly dripped right into your cup.</q>
|
||||
<cite>-Mike Cutcliff</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Delicious coffee. Love this place!</q>
|
||||
<cite>-Jenny Mitchell</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Best orange espresso (Arancia)! Also, a calm atmosphere and competent staff.</q>
|
||||
<cite>-Deanne McCausland</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Great service, awesome coffee.. low key always a favorite..</q>
|
||||
<cite>-E Quigley</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Amazing place..the coffee drinks are so delicious. Great staff.</q>
|
||||
<cite>-Angela Dawn</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>They are very friendly and make the best cafe arancia ever. Yumm</q>
|
||||
<cite>-Vicki Moler</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>An absolutely delightful spot to visit. Nice views and a casual unpretentious vibe,</q>
|
||||
<cite>-Kevin Chung</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Great place! Great coffee, staff and amazing location.</q>
|
||||
<cite>-Juanita Sheppard</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Great coffee and amazing view.</q>
|
||||
<cite>-Graham Magnuson</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>This is a unique, albeit a bit quirky place. Creative coffee drinks.</q>
|
||||
<cite>-Artak Kalantarian</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>This place is my daily grind awesome staff awesome owner most incredible of you in a coffee shop I've ever had</q>
|
||||
<cite>-mugen andeson</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Great coffee and service.</q>
|
||||
<cite>-Jeannette Heffley</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Great atmosphere! Nice people! AWESOME coffee and food!</q>
|
||||
<cite>-N R</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Best coffee/espresso on the peninsula.</q>
|
||||
<cite>-Rachel Rutledge</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Awesome view and atmosphere. Good coffee as well.</q>
|
||||
<cite>-Andrew Law</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Best coffee by a mile, good service. Ambiance : Gemuetlich.</q>
|
||||
<cite>-Hans Knigge</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>I was just there with my girlfriend on a rainy day. We got the best hot chocolate we have ever had.</q>
|
||||
<cite>-Will Hale</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Grab you Cappuccino, go outside and enjoy the waves on the beach. Life is good.</q>
|
||||
<cite>-Bill Crane</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Beautiful setting great coffee fabulous service I will be back</q>
|
||||
<cite>-Karen Karadimov</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Amazing coffee and food, friendly service and chill atmosphere</q>
|
||||
<cite>-Muldair Moore</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Nice staff and friendly atmosphere and good coffee too.</q>
|
||||
<cite>-Chainarong Patana-anake</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Amazing views, great coffee, and a large local following</q>
|
||||
<cite>-Bradford Billings</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Great coffee. Good service. Relaxing environment.</q>
|
||||
<cite>-Bob Phillips</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Great coffee, food and views</q>
|
||||
<cite>-Chris Langston</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Best coffee ever! Waterfront, great location.</q>
|
||||
<cite>-Susan Willis</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Such care, great food, killin' views & the coffee zings. Good.</q>
|
||||
<cite>-Scott Doran</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Great service!! Great staff!! Very good coffee!!</q>
|
||||
<cite>-casey reuck</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>The coffee goes well with the view. Makes sense why it's so popular.</q>
|
||||
<cite>-Dabin Park</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Great coffee. Excellent food. Beautiful views</q>
|
||||
<cite>-James Reynolds</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Excellent espresso super service</q>
|
||||
<cite>-Sharon Jack</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Good coffee and good prices. Starbucks ain't got nothing on BLTC.</q>
|
||||
<cite>-Garrett Massey</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Oh the coffee is simply the best.</q>
|
||||
<cite>-Stephen Smith</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Great coffee!</q>
|
||||
<cite>-Jessica Dugal</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>They make the best salted Caramel lattes in the world.</q>
|
||||
<cite>-Margaret Barbo</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>What not to love...the view, the organic way of Caffeine, the friendly fun atmosphere…</q>
|
||||
<cite>-Dr. Joseph Adam</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Great coffee. Loved it</q>
|
||||
<cite>-Rapunzel</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Such good coffee</q>
|
||||
<cite>-Peggy Baker</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Great local coffee shop, awesome coffee</q>
|
||||
<cite>-Parley Wells</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Best Sumatra dark drip to go. Great people.</q>
|
||||
<cite>-Scott Tennant</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Great food, great coffee, hardworking staff</q>
|
||||
<cite>-Stevie Schmidt</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Best coffee in Port Townsend!</q>
|
||||
<cite>-Robert Maass</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Friendly place. Awesome people and great coffee!</q>
|
||||
<cite>-Brett Merkley</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>The Coffee Here Is Really Great. It's great. The coffee</q>
|
||||
<cite>-John Buday</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Superb quad-shot espresso drinks and pastries straight from the angels!</q>
|
||||
<cite>-Bradley Smull</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>A comfortable and relaxing place with delicious coffee.</q>
|
||||
<cite>-Morgan Humphreys</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Better Living With Coffee has the best salted caramel latte</q>
|
||||
<cite>-Gilbert Barbo</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Awesome coffee and amazing ambiance ♥️</q>
|
||||
<cite>-Hamsa Abdullah</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Get the coffee Arancia. It's the best thing to happen to the planet since the polio vaccine.</q>
|
||||
<cite>-Shane Squire</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Delicious drip coffee for a reasonable price!</q>
|
||||
<cite>-Ian McLaughlan</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>I love BLTC. Great coffee and great people.</q>
|
||||
<cite>-Sea Otter</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Beautiful view, awesome coffee, ethical standards</q>
|
||||
<cite>-Charles Boyles</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Great coffee, food, people and view!</q>
|
||||
<cite>-Gabe Herbert</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Great coffee and great food</q>
|
||||
<cite>-Scott Clarkson</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Great coffee, and atmosphere!</q>
|
||||
<cite>-Kristina Koranek</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Had a fantastic wet cappuccino</q>
|
||||
<cite>-Scott Urstad</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Delicious coffee in a friendly atmosphere.</q>
|
||||
<cite>-Barbara Beers</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>The best, simply the best coffee in town.</q>
|
||||
<cite>-C Miller</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Best coffee in PT</q>
|
||||
<cite>-Peggy Baker</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Best latte I have ever had.</q>
|
||||
<cite>-Juan Haley</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Always my favorite coffee shop!</q>
|
||||
<cite>-leslie shawn</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Great location, great views, great coffee</q>
|
||||
<cite>-yr deervvitch</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Great coffee!</q>
|
||||
<cite>-Reed McGinnis</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Nice place for coffee ☕🍪🤯</q>
|
||||
<cite>-Blowing Bubbles</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Amazing quality of product and personnel.</q>
|
||||
<cite>-Tim Hartzell</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Amazing coffee great staff</q>
|
||||
<cite>-Thomas Carso</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Best coffee on the west coast.</q>
|
||||
<cite>-Mark Burchfield</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Awesome coffee!</q>
|
||||
<cite>-Karen Fry</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Nice coffee</q>
|
||||
<cite>-リーBryan</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Loved it. Good coffee.</q>
|
||||
<cite>-Hans Schweizer</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>My favorite reason to visit Port Townsend!</q>
|
||||
<cite>-Ilana Moss</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Best Espresso on the Peninsula!!</q>
|
||||
<cite>-Audra Downs</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>The best coffee in Port Townsend</q>
|
||||
<cite>-Denice Nolan</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Great coffee!</q>
|
||||
<cite>-Joe Bolton</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Fabulous coffee and staff!!</q>
|
||||
<cite>-Candice Cotterill</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Good ..No GREAT COFFEE</q>
|
||||
<cite>-David Ustick</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>I love their coffee</q>
|
||||
<cite>-Julie Canterbury</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Slice of heaven on the waterfront.</q>
|
||||
<cite>-John Franich</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>A top notch coffee house</q>
|
||||
<cite>-The dog cat reptile And plant lady</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Very cool coffee shop.</q>
|
||||
<cite>-ramsay tanham</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>I love love love the Pantomime French!</q>
|
||||
<cite>-Cynthia Koan</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Great coffee</q>
|
||||
<cite>-Eric Black</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Drip coffee done right</q>
|
||||
<cite>-Austin Ivey</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Varied, sustainable, and delicious!</q>
|
||||
<cite>-Daniel Archer</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>5 stars says it all!</q>
|
||||
<cite>-N Croston</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Yummy!</q>
|
||||
<cite>-Pearl Newt</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>The coffee is delicious.</q>
|
||||
<cite>-Destini Jewell</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Delightful coffee and place.</q>
|
||||
<cite>-Ash</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Yummy coffee</q>
|
||||
<cite>-Nichole Van Duren</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Best coffee in town</q>
|
||||
<cite>-Natalie Ainge (Kerrigan Byrne)</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>This place has it all!</q>
|
||||
<cite>-Lillian Henegar</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Lovely</q>
|
||||
<cite>-C.S. Hirst ndt</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>I just love this place. ❤️</q>
|
||||
<cite>-Kristina</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Delicious coffee!</q>
|
||||
<cite>-sopalla d</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Excellent coffee</q>
|
||||
<cite>-Tom Hunter</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>I love everything about this place.</q>
|
||||
<cite>-James Jacoby</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Amazing</q>
|
||||
<cite>-Nicole Malloy</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Always the best choice</q>
|
||||
<cite>-ANTHONY DIORIO</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Excellent cup of coffee</q>
|
||||
<cite>-Monica Buchholz</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Wow!</q>
|
||||
<cite>-Ken P</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Delicious. Adina Than. Great coffee</q>
|
||||
<cite>-Angela Fusaro</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Yum</q>
|
||||
<cite>-Jeff Royster</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Good coffee</q>
|
||||
<cite>-Richard Paul</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Chetzemoka for the win</q>
|
||||
<cite>-Ian Sanderson</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Good Coffee</q>
|
||||
<cite>-Rob Kline</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>the highlight of PT</q>
|
||||
<cite>-Jessica von Volkli</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Smart coffee shop and good coffee! They have good coffee and that’s not easy to find outside of Seattle really. Drip coffee made inventively. Many types to keep people coming back.</q>
|
||||
<cite>-Iwamisea</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Ah, Coffee! And more… Some of the best coffee in Port Townsend spoken from the lips of a long-time coffee/espresso drinker. It's fresh and strong</q>
|
||||
<cite>-James B</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Best coffee! This is a place for every coffee lover and coffee aficionado.</q>
|
||||
<cite>-FABJourney</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>As somewhat of a coffee snob, finding a good cup of joe, or a great latte, is what can turn my entire day around. The latte was OUTSTANDING.</q>
|
||||
<cite>-BEST COFFEE IN PORT TOWNSEND</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>The place to go. My only complaint is that it is too popular…</q>
|
||||
<cite>-Ryan C</cite>
|
||||
</blockquote>
|
||||
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Pure Port Townsend. Perfect and delicious locally roasted coffees</q>
|
||||
<cite>-janetzim</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Great Coffee. Love this coffee shop, very interesting spot and has a great view. The coffee is amazing! Had the Arancia (think this is how it is spelled) quite a few times and continue to crave it to this day.</q>
|
||||
<cite>-Cas M</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Great coffee and kind staff. We went to this place 4 days in a row recently while on vacation. Staff greeted us kindly and made us some of the best coffee we'd had in a long time.</q>
|
||||
<cite>-922aaronm</cite>
|
||||
</blockquote>
|
||||
|
||||
<blockquote class="review__item">
|
||||
<q>Wonderful! GREAT coffee! If you are a coffee lover you must go here.</q>
|
||||
<cite>-Maggie L</cite>
|
||||
</blockquote>
|
||||
</section>
|
||||
</article>
|
||||
{% endblock %}
|
||||
@ -3,6 +3,8 @@ from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('about/', views.AboutView.as_view(), name='about'),
|
||||
path('reviews/', views.ReviewListView.as_view(), name='reviews'),
|
||||
path('contact/', views.ContactFormView.as_view(), name='contact'),
|
||||
|
||||
path('', views.ProductListView.as_view(), name='product-list'),
|
||||
path('products/<int:pk>/', include([
|
||||
@ -11,9 +13,13 @@ urlpatterns = [
|
||||
|
||||
path('cart/', views.CartView.as_view(), name='cart-detail'),
|
||||
path('cart/<int:pk>/add/', views.CartAddProductView.as_view(), name='cart-add'),
|
||||
path('cart/<int:pk>/update/', views.CartUpdateProductView.as_view(), name='cart-update'),
|
||||
path('cart/<int:pk>/remove/', views.cart_remove_product_view, name='cart-remove'),
|
||||
|
||||
path('coupon/apply/', views.CouponApplyView.as_view(), name='coupon-apply'),
|
||||
|
||||
path('paypal/order/<slug:transaction_id>/capture/', views.paypal_order_transaction_capture, name='paypal-capture'),
|
||||
path('paypal/webhooks/', views.paypal_webhook_endpoint, name='paypal-webhook'),
|
||||
|
||||
path('checkout/address/', views.CheckoutAddressView.as_view(), name='checkout-address'),
|
||||
path('checkout/', views.OrderCreateView.as_view(), name='order-create'),
|
||||
@ -24,5 +30,9 @@ urlpatterns = [
|
||||
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'),
|
||||
|
||||
path('orders/<int:order_pk>/', views.OrderDetailView.as_view(), name='order-detail'),
|
||||
path('addresses/<int:address_pk>/update/', views.AddressUpdateView.as_view(), name='address-update'),
|
||||
])),
|
||||
|
||||
]
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
import logging
|
||||
import requests
|
||||
import json
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.shortcuts import render, reverse, redirect, get_object_or_404
|
||||
from django.urls import reverse_lazy
|
||||
from django.core.mail import EmailMessage
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.http import JsonResponse
|
||||
from django.views.generic.base import RedirectView, TemplateView
|
||||
from django.views.generic.edit import FormView, CreateView, UpdateView, DeleteView, FormMixin
|
||||
@ -11,16 +14,20 @@ 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, UserPassesTestMixin
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.http import require_POST
|
||||
|
||||
from paypalcheckoutsdk.orders import OrdersCreateRequest, OrdersCaptureRequest
|
||||
from paypalcheckoutsdk.core import PayPalHttpClient, SandboxEnvironment
|
||||
|
||||
from accounts.models import User, Address
|
||||
from accounts.utils import get_or_create_customer
|
||||
from core.models import Product, Order, Transaction, OrderLine
|
||||
from accounts.forms import AddressForm as AccountAddressForm, CustomerUpdateForm
|
||||
from core.models import Product, Order, Transaction, OrderLine, Coupon
|
||||
from core.forms import ShippingMethodForm
|
||||
|
||||
from .forms import AddToCartForm, OrderCreateForm, AddressForm
|
||||
from .forms import AddToCartForm, UpdateCartItemForm, OrderCreateForm, AddressForm, CouponApplyForm, ContactForm
|
||||
from .cart import Cart
|
||||
from .payments import CaptureOrder
|
||||
|
||||
@ -33,14 +40,13 @@ class CartView(TemplateView):
|
||||
context = super().get_context_data(**kwargs)
|
||||
cart = Cart(self.request)
|
||||
for item in cart:
|
||||
item['update_quantity_form'] = AddToCartForm(
|
||||
item['update_quantity_form'] = UpdateCartItemForm(
|
||||
initial={
|
||||
'quantity': item['quantity'],
|
||||
'roast': item['roast'],
|
||||
'update': True
|
||||
}
|
||||
)
|
||||
context['cart'] = cart
|
||||
context['coupon_apply_form'] = CouponApplyForm()
|
||||
return context
|
||||
|
||||
class CartAddProductView(SingleObjectMixin, FormView):
|
||||
@ -56,7 +62,30 @@ class CartAddProductView(SingleObjectMixin, FormView):
|
||||
if form.is_valid():
|
||||
cart.add(
|
||||
product=self.get_object(),
|
||||
roast=form.cleaned_data['roast'],
|
||||
grind=form.cleaned_data['grind'],
|
||||
quantity=form.cleaned_data['quantity']
|
||||
)
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
def form_valid(self, form):
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class CartUpdateProductView(SingleObjectMixin, FormView):
|
||||
model = Product
|
||||
form_class = UpdateCartItemForm
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('storefront:cart-detail')
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
cart = Cart(request)
|
||||
form = self.get_form()
|
||||
if form.is_valid():
|
||||
cart.add(
|
||||
product=self.get_object(),
|
||||
quantity=form.cleaned_data['quantity'],
|
||||
update_quantity=form.cleaned_data['update']
|
||||
)
|
||||
@ -75,10 +104,32 @@ def cart_remove_product_view(request, pk):
|
||||
return redirect('storefront:cart-detail')
|
||||
|
||||
|
||||
class CouponApplyView(FormView):
|
||||
template_name = 'contact.html'
|
||||
form_class = CouponApplyForm
|
||||
success_url = reverse_lazy('storefront:cart-detail')
|
||||
|
||||
def form_valid(self, form):
|
||||
today = timezone.localtime(timezone.now()).date()
|
||||
code = form.cleaned_data['code']
|
||||
try:
|
||||
coupon = Coupon.objects.get(
|
||||
code__iexact=code,
|
||||
valid_from__date__lte=today,
|
||||
valid_to__date__gte=today
|
||||
)
|
||||
if coupon.is_valid:
|
||||
self.request.session['coupon_code'] = coupon.code
|
||||
except ObjectDoesNotExist:
|
||||
self.request.session['coupon_code'] = None
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class ProductListView(FormMixin, ListView):
|
||||
model = Product
|
||||
template_name = 'storefront/product_list.html'
|
||||
form_class = AddToCartForm
|
||||
ordering = 'sorting'
|
||||
|
||||
queryset = Product.objects.filter(
|
||||
visible_in_listings=True
|
||||
@ -127,6 +178,7 @@ class OrderCreateView(CreateView):
|
||||
def get_initial(self):
|
||||
cart = Cart(self.request)
|
||||
initial = {
|
||||
'coupon': cart.coupon,
|
||||
'total_net_amount': cart.get_total_price()
|
||||
}
|
||||
|
||||
@ -151,9 +203,11 @@ class OrderCreateView(CreateView):
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['shipping_address'] = self.request.session.get('shipping_address')
|
||||
context['PAYPAL_CLIENT_ID'] = settings.PAYPAL_CLIENT_ID
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
# TODO: make order status "Draft" and then in the PP capture set status appropriately
|
||||
cart = Cart(self.request)
|
||||
shipping_address = self.request.session.get('shipping_address')
|
||||
form.instance.customer, form.instance.shipping_address = get_or_create_customer(self.request, form, shipping_address)
|
||||
@ -182,6 +236,13 @@ def paypal_order_transaction_capture(request, transaction_id):
|
||||
else:
|
||||
return JsonResponse({'details': 'invalid request'})
|
||||
|
||||
@csrf_exempt
|
||||
@require_POST
|
||||
def paypal_webhook_endpoint(request):
|
||||
data = json.loads(request.body)
|
||||
logger.info(data)
|
||||
return JsonResponse(data)
|
||||
|
||||
|
||||
class PaymentDoneView(TemplateView):
|
||||
template_name = 'storefront/payment_done.html'
|
||||
@ -190,24 +251,57 @@ class PaymentCanceledView(TemplateView):
|
||||
template_name = 'storefront/payment_canceled.html'
|
||||
|
||||
|
||||
class CustomerDetailView(DetailView):
|
||||
class CustomerDetailView(LoginRequiredMixin, DetailView):
|
||||
model = User
|
||||
template_name = 'storefront/customer_detail.html'
|
||||
context_object_name = 'customer'
|
||||
|
||||
class CustomerUpdateView(UpdateView):
|
||||
class CustomerUpdateView(LoginRequiredMixin, UpdateView):
|
||||
model = User
|
||||
template_name = 'storefront/customer_form.html'
|
||||
context_object_name = 'customer'
|
||||
fields = (
|
||||
'first_name',
|
||||
'last_name',
|
||||
'email',
|
||||
'default_shipping_address'
|
||||
)
|
||||
form_class = CustomerUpdateForm
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('storefront:customer-detail', kwargs={'pk': self.object.pk})
|
||||
|
||||
class OrderDetailView(LoginRequiredMixin, DetailView):
|
||||
model = Order
|
||||
template_name = 'storefront/order_detail.html'
|
||||
pk_url_kwarg = 'order_pk'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['customer'] = User.objects.get(pk=self.kwargs['pk'])
|
||||
return context
|
||||
|
||||
class AddressUpdateView(LoginRequiredMixin, UpdateView):
|
||||
model = Address
|
||||
pk_url_kwarg = 'address_pk'
|
||||
template_name = 'storefront/address_form.html'
|
||||
form_class = AccountAddressForm
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['customer'] = User.objects.get(pk=self.kwargs['pk'])
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('storefront:customer-detail',kwargs={'pk': self.kwargs['pk']})
|
||||
|
||||
|
||||
|
||||
class AboutView(TemplateView):
|
||||
template_name = 'storefront/about.html'
|
||||
|
||||
class ReviewListView(TemplateView):
|
||||
template_name = 'storefront/reviews.html'
|
||||
|
||||
class ContactFormView(FormView, SuccessMessageMixin):
|
||||
template_name = 'storefront/contact_form.html'
|
||||
form_class = ContactForm
|
||||
success_url = reverse_lazy('storefront:product-list')
|
||||
|
||||
def form_valid(self, form):
|
||||
form.send_email()
|
||||
return super().form_valid(form)
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
|
||||
{% block content %}
|
||||
<article>
|
||||
<p><a href="{% url 'storefront:customer-detail' user.pk %}">← Back</a></p>
|
||||
<section>
|
||||
<h1>{% trans "E-mail Addresses" %}</h1>
|
||||
</section>
|
||||
@ -16,10 +17,10 @@
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
{% for emailaddress in user.emailaddress_set.all %}
|
||||
<div class="ctrlHolder">
|
||||
<div>
|
||||
<input id="email_radio_{{forloop.counter}}" type="radio" name="email" {% if emailaddress.primary or user.emailaddress_set.count == 1 %}checked="checked"{%endif %} value="{{emailaddress.email}}"/>
|
||||
<label for="email_radio_{{forloop.counter}}" class="{% if emailaddress.primary %}primary_email{%endif%}">
|
||||
|
||||
<input id="email_radio_{{forloop.counter}}" type="radio" name="email" {% if emailaddress.primary or user.emailaddress_set.count == 1 %}checked="checked"{%endif %} value="{{emailaddress.email}}"/>
|
||||
|
||||
{{ emailaddress.email }}
|
||||
{% if emailaddress.verified %}
|
||||
|
||||
@ -14,74 +14,77 @@
|
||||
<link href="https://fonts.googleapis.com/css2?family=Eczar:wght@400;700&family=Inter:wght@100;400;700&display=swap" rel="stylesheet">
|
||||
|
||||
{% compress css %}
|
||||
<link rel="stylesheet" type="text/css" href="{% static "styles/normalize.css" %}">
|
||||
<link rel="stylesheet" type="text/css" href="{% static "styles/main.css" %}">
|
||||
{% endcompress %}
|
||||
|
||||
<script type="module" defer src="{% static 'scripts/initializers/timezone.js' %}"></script>
|
||||
<script type="module" defer src="{% static 'scripts/index.js' %}"></script>
|
||||
|
||||
{% block head %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<header class="site__header">
|
||||
<div>
|
||||
<h1><a class="site__logo" href="{% url 'storefront:product-list' %}">Port Townsend Coffee</a></h1>
|
||||
{% if user.is_authenticated %}
|
||||
<p>
|
||||
Hi <a href="{% url 'storefront:customer-detail' user.pk %}">{{user.first_name}} {{user.last_name}}</a>!
|
||||
</p>
|
||||
{% if user.is_staff %}
|
||||
<p><a href="{% url 'dashboard:home' %}">Dashboard</a></p>
|
||||
{% endif %}
|
||||
<p><a href="{% url 'account_logout' %}">Log Out</a></p>
|
||||
{% else %}
|
||||
<p>
|
||||
<a href="{% url 'account_login' %}">Log In</a> | <a href="{% url 'account_signup' %}">Sign Up</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
<div class="modal-menu">
|
||||
<div class="modal-menu__content">
|
||||
<div class="modal-menu__header">
|
||||
<h4>Sign up for our newsletter</h4>
|
||||
<span class="close-modal">×</span>
|
||||
</div>
|
||||
<div class="modal-menu__form">
|
||||
<div class="_form_1"></div>
|
||||
</div>
|
||||
</div>
|
||||
<nav>
|
||||
<a href="{% url 'storefront:product-list' %}">Shop</a>
|
||||
<a href="">Wholesale</a>
|
||||
<a href="">Subscribe</a>
|
||||
<a href="">Cafe</a>
|
||||
<a href="">Fair Trade</a>
|
||||
<a href="{% url 'storefront:about' %}">About</a>
|
||||
<a href="">Contact</a>
|
||||
</div>
|
||||
|
||||
<header class="site__header">
|
||||
<nav class="site__nav">
|
||||
<a class="site__logo" href="{% url 'storefront:product-list' %}"><img src="{% static 'images/site_logo.svg' %}" alt="Port Townsend Roasting Co."></a>
|
||||
<ul class="nav__list nav__main">
|
||||
<li><a class="nav__link" href="{% url 'storefront:product-list' %}">Shop</a></li>
|
||||
<li><a class="nav__link" href="">Cafe</a></li>
|
||||
<li><a class="nav__link" href="">Fair trade</a></li>
|
||||
<li><a class="nav__link" href="{% url 'storefront:about' %}">About</a></li>
|
||||
<li><a class="nav__link" href="{% url 'storefront:contact' %}">Contact</a></li>
|
||||
</ul>
|
||||
{% if user.is_authenticated %}
|
||||
<div class="nav__menu nav__account">
|
||||
<a class="nav__link" href="{% url 'storefront:customer-detail' user.pk %}">Account ▼</a>
|
||||
<ul class="nav__dropdown">
|
||||
{% if user.is_staff %}
|
||||
<li><a class="nav__link" href="{% url 'dashboard:home' %}">Dashboard</a></li>
|
||||
{% endif %}
|
||||
<li><a class="nav__link" href="{% url 'storefront:customer-detail' user.pk %}">Orders</a></li>
|
||||
<li><a class="nav__link" href="{% url 'account_logout' %}">Logout</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{% else %}
|
||||
<ul class="nav__list nav__account">
|
||||
<li><a class="nav__link" href="{% url 'account_login' %}">Login</a></li>
|
||||
<li>/</li>
|
||||
<li><a class="nav__link" href="{% url 'account_signup' %}">Signup</a></li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
<a class="site__cart" href="{% url 'storefront:cart-detail' %}">
|
||||
<span class="cart__length">{{cart|length}}</span>
|
||||
<span class="cart__count">{{cart|length}}</span>
|
||||
<img class="cart__icon" src="{% static 'images/shopping_cart.svg' %}" alt="Shopping cart">
|
||||
</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<aside>
|
||||
{% block aside %}
|
||||
{% endblock %}
|
||||
</aside>
|
||||
<main>
|
||||
{% block content %}
|
||||
{% endblock content %}
|
||||
</main>
|
||||
<footer>
|
||||
<div class="keep_calm">
|
||||
<figure>
|
||||
<img class="keep_calm__img" src="{% static 'images/keep_calm.jpg' %}" alt="Keep calm and drink coffee">
|
||||
</figure>
|
||||
<div>
|
||||
<h4>Problem with your order?<br>Have a question?</h4>
|
||||
<p>Please contact us, we’re happy to help you over the phone at <a href="tel:+13603855856">(360) 385-5856</a> between 8:00 am and 10:00 pm Pacific Time.</p>
|
||||
{% block footer %}
|
||||
{% endblock footer %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="site__copyright">
|
||||
<section>
|
||||
<p>
|
||||
<small>Copyright © 2016-{% now "Y" %} Better Living Food Company Inc.</small><br>
|
||||
<small>Fair Trade | Organic | Port Townsend, WA 98368</small><br><br>
|
||||
<img src="{% static 'images/fair_trade_stamp.png' %}" alt="">
|
||||
<img class="site__ft-stamp" src="{% static 'images/fair_trade_stamp.png' %}" alt="">
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</footer>
|
||||
|
||||
<script src="https://ptcoffee.activehosted.com/f/embed.php?id=1" type="text/javascript" charset="utf-8"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -41,13 +41,13 @@
|
||||
<img src="{% static 'images/customer.png' %}" alt="">
|
||||
Customers
|
||||
</a>
|
||||
<a href="">
|
||||
<a href="{% url 'dashboard:coupon-list' %}">
|
||||
<img src="{% static 'images/coupon.png' %}" alt="">
|
||||
Coupons
|
||||
</a>
|
||||
<a href="">
|
||||
<a href="{% url 'dashboard:config' %}">
|
||||
<img src="{% static 'images/gear.png' %}" alt="">
|
||||
Staff
|
||||
Config
|
||||
</a>
|
||||
</nav>
|
||||
<div class="dashboard__user">
|
||||
@ -67,8 +67,24 @@
|
||||
{% endblock content %}
|
||||
</main>
|
||||
</div>
|
||||
{% if messages %}
|
||||
<div class="site__messages">
|
||||
{% for message in messages %}
|
||||
<span class="messages__message {% if message.tags %} {{ message.tags }} {% endif %}">{{ message }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<footer>
|
||||
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
const messageEl = document.querySelector('.site__messages')
|
||||
if (messageEl) {
|
||||
setTimeout(function () {
|
||||
messageEl.style.display = 'none'
|
||||
}, 5000)
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
16
src/templates/templated_email/storefront/contact_form.email
Normal file
16
src/templates/templated_email/storefront/contact_form.email
Normal file
@ -0,0 +1,16 @@
|
||||
{% block subject %}{{subject}}{% endblock %}
|
||||
{% block plain %}
|
||||
Referred from: {{referal}}
|
||||
|
||||
From: {{first_name}} {{last_name}} {{email_address}}
|
||||
|
||||
Message: {{message}}
|
||||
{% endblock %}
|
||||
|
||||
{% block html %}
|
||||
<p><strong>Referred from</strong>:<br>{{referal}}</p>
|
||||
|
||||
<p><strong>From</strong>:<br>{{first_name}} {{last_name}} {{email_address|urlize}}</p>
|
||||
|
||||
<p><strong>Message</strong>:<br>{{message|linebreaks}}</p>
|
||||
{% endblock %}
|
||||
18
src/templates/templated_email/storefront/order_shipped.email
Normal file
18
src/templates/templated_email/storefront/order_shipped.email
Normal file
@ -0,0 +1,18 @@
|
||||
{% block subject %}Your PT Coffee order #{{order_id}} has shipped{% endblock %}
|
||||
{% block plain %}
|
||||
Great news! Your recent order #{{order_id}} has shipped
|
||||
|
||||
{{tracking_id}}
|
||||
|
||||
Thanks,
|
||||
Port Townsend Coffee
|
||||
{% endblock %}
|
||||
|
||||
{% block html %}
|
||||
<p>Great news! Your recent order #{{order_id}} has shipped</p>
|
||||
|
||||
<p>{{tracking_id}}</p>
|
||||
|
||||
<p>Thanks,<br>
|
||||
Port Townsend Coffee</p>
|
||||
{% endblock %}
|
||||
Loading…
x
Reference in New Issue
Block a user