diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..844422f --- /dev/null +++ b/.coveragerc @@ -0,0 +1,2 @@ +[report] +omit = src/tests/* \ No newline at end of file diff --git a/.gitignore b/.gitignore index 894a44c..d3ae56b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.vscode/ + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -80,6 +82,7 @@ celerybeat-schedule # SageMath parsed files *.sage.py +.DS_Store # Environments .env diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..201800b --- /dev/null +++ b/Pipfile @@ -0,0 +1,25 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] +pytest = "*" +pytest-watch = "*" +pytest-cov = "*" +pep8 = "*" +autopep8 = "*" + +[packages] +requests = "*" +flask = "*" +flask-sqlalchemy = "*" +psycopg2-binary = "*" +flask-migrate = "*" +python-dotenv = "*" +flask-wtf = "*" +passlib = "*" +sqlalchemy = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..b0faa07 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,423 @@ +{ + "_meta": { + "hash": { + "sha256": "ea6688de1df1739287a554400b5b03ddc3f28c51bdf5dabc2b378e3aa969872d" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "alembic": { + "hashes": [ + "sha256:505d41e01dc0c9e6d85c116d0d35dbb0a833dcb490bf483b75abeb06648864e8" + ], + "version": "==1.0.8" + }, + "certifi": { + "hashes": [ + "sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5", + "sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae" + ], + "version": "==2019.3.9" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "click": { + "hashes": [ + "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", + "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + ], + "version": "==7.0" + }, + "flask": { + "hashes": [ + "sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48", + "sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05" + ], + "index": "pypi", + "version": "==1.0.2" + }, + "flask-migrate": { + "hashes": [ + "sha256:a361578cb829681f860e4de5ed2c48886264512f0c16144e404c36ddc95ab49c", + "sha256:c24d105c5d6cc670de20f8cbfb909e04f4e04b8784d0df070005944de1f21549" + ], + "index": "pypi", + "version": "==2.4.0" + }, + "flask-sqlalchemy": { + "hashes": [ + "sha256:3bc0fac969dd8c0ace01b32060f0c729565293302f0c4269beed154b46bec50b", + "sha256:5971b9852b5888655f11db634e87725a9031e170f37c0ce7851cf83497f56e53" + ], + "index": "pypi", + "version": "==2.3.2" + }, + "flask-wtf": { + "hashes": [ + "sha256:5d14d55cfd35f613d99ee7cba0fc3fbbe63ba02f544d349158c14ca15561cc36", + "sha256:d9a9e366b32dcbb98ef17228e76be15702cd2600675668bca23f63a7947fd5ac" + ], + "index": "pypi", + "version": "==0.14.2" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "itsdangerous": { + "hashes": [ + "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", + "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" + ], + "version": "==1.1.0" + }, + "jinja2": { + "hashes": [ + "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", + "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" + ], + "version": "==2.10" + }, + "mako": { + "hashes": [ + "sha256:4e02fde57bd4abb5ec400181e4c314f56ac3e49ba4fb8b0d50bba18cb27d25ae" + ], + "version": "==1.0.7" + }, + "markupsafe": { + "hashes": [ + "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", + "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", + "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", + "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", + "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", + "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", + "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", + "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", + "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", + "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", + "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", + "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", + "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", + "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", + "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", + "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", + "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", + "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", + "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", + "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", + "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", + "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", + "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", + "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", + "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", + "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", + "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", + "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" + ], + "version": "==1.1.1" + }, + "passlib": { + "hashes": [ + "sha256:3d948f64138c25633613f303bcc471126eae67c04d5e3f6b7b8ce6242f8653e0", + "sha256:43526aea08fa32c6b6dbbbe9963c4c767285b78147b7437597f992812f69d280" + ], + "index": "pypi", + "version": "==1.7.1" + }, + "psycopg2-binary": { + "hashes": [ + "sha256:19a2d1f3567b30f6c2bb3baea23f74f69d51f0c06c2e2082d0d9c28b0733a4c2", + "sha256:2b69cf4b0fa2716fd977aa4e1fd39af6110eb47b2bb30b4e5a469d8fbecfc102", + "sha256:2e952fa17ba48cbc2dc063ddeec37d7dc4ea0ef7db0ac1eda8906365a8543f31", + "sha256:348b49dd737ff74cfb5e663e18cb069b44c64f77ec0523b5794efafbfa7df0b8", + "sha256:3d72a5fdc5f00ca85160915eb9a973cf9a0ab8148f6eda40708bf672c55ac1d1", + "sha256:4957452f7868f43f32c090dadb4188e9c74a4687323c87a882e943c2bd4780c3", + "sha256:5138cec2ee1e53a671e11cc519505eb08aaaaf390c508f25b09605763d48de4b", + "sha256:587098ca4fc46c95736459d171102336af12f0d415b3b865972a79c03f06259f", + "sha256:5b79368bcdb1da4a05f931b62760bea0955ee2c81531d8e84625df2defd3f709", + "sha256:5cf43807392247d9bc99737160da32d3fa619e0bfd85ba24d1c78db205f472a4", + "sha256:676d1a80b1eebc0cacae8dd09b2fde24213173bf65650d22b038c5ed4039f392", + "sha256:6b0211ecda389101a7d1d3df2eba0cf7ffbdd2480ca6f1d2257c7bd739e84110", + "sha256:79cde4660de6f0bb523c229763bd8ad9a93ac6760b72c369cf1213955c430934", + "sha256:7aba9786ac32c2a6d5fb446002ed936b47d5e1f10c466ef7e48f66eb9f9ebe3b", + "sha256:7c8159352244e11bdd422226aa17651110b600d175220c451a9acf795e7414e0", + "sha256:945f2eedf4fc6b2432697eb90bb98cc467de5147869e57405bfc31fa0b824741", + "sha256:96b4e902cde37a7fc6ab306b3ac089a3949e6ce3d824eeca5b19dc0bedb9f6e2", + "sha256:9a7bccb1212e63f309eb9fab47b6eaef796f59850f169a25695b248ca1bf681b", + "sha256:a3bfcac727538ec11af304b5eccadbac952d4cca1a551a29b8fe554e3ad535dc", + "sha256:b19e9f1b85c5d6136f5a0549abdc55dcbd63aba18b4f10d0d063eb65ef2c68b4", + "sha256:b664011bb14ca1f2287c17185e222f2098f7b4c857961dbcf9badb28786dbbf4", + "sha256:bde7959ef012b628868d69c474ec4920252656d0800835ed999ba5e4f57e3e2e", + "sha256:cb095a0657d792c8de9f7c9a0452385a309dfb1bbbb3357d6b1e216353ade6ca", + "sha256:d16d42a1b9772152c1fe606f679b2316551f7e1a1ce273e7f808e82a136cdb3d", + "sha256:d444b1545430ffc1e7a24ce5a9be122ccd3b135a7b7e695c5862c5aff0b11159", + "sha256:d93ccc7bf409ec0a23f2ac70977507e0b8a8d8c54e5ee46109af2f0ec9e411f3", + "sha256:df6444f952ca849016902662e1a47abf4fa0678d75f92fd9dd27f20525f809cd", + "sha256:e63850d8c52ba2b502662bf3c02603175c2397a9acc756090e444ce49508d41e", + "sha256:ec43358c105794bc2b6fd34c68d27f92bea7102393c01889e93f4b6a70975728", + "sha256:f4c6926d9c03dadce7a3b378b40d2fea912c1344ef9b29869f984fb3d2a2420b" + ], + "index": "pypi", + "version": "==2.7.7" + }, + "python-dateutil": { + "hashes": [ + "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", + "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" + ], + "version": "==2.8.0" + }, + "python-dotenv": { + "hashes": [ + "sha256:a84569d0e00d178bc5b957f7ff208bf49287cbf61857c31c258c4a91f571527b", + "sha256:c9b1ddd3cdbe75c7d462cb84674d87130f4b948f090f02c7d7144779afb99ae0" + ], + "index": "pypi", + "version": "==0.10.1" + }, + "python-editor": { + "hashes": [ + "sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d", + "sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b", + "sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8" + ], + "version": "==1.0.4" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "sqlalchemy": { + "hashes": [ + "sha256:781fb7b9d194ed3fc596b8f0dd4623ff160e3e825dd8c15472376a438c19598b" + ], + "index": "pypi", + "version": "==1.3.1" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + }, + "werkzeug": { + "hashes": [ + "sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c", + "sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b" + ], + "version": "==0.14.1" + }, + "wtforms": { + "hashes": [ + "sha256:0cdbac3e7f6878086c334aa25dc5a33869a3954e9d1e015130d65a69309b3b61", + "sha256:e3ee092c827582c50877cdbd49e9ce6d2c5c1f6561f849b3b068c1b8029626f1" + ], + "version": "==2.2.1" + } + }, + "develop": { + "argh": { + "hashes": [ + "sha256:a9b3aaa1904eeb78e32394cd46c6f37ac0fb4af6dc488daa58971bdc7d7fcaf3", + "sha256:e9535b8c84dc9571a48999094fda7f33e63c3f1b74f3e5f3ac0105a58405bb65" + ], + "version": "==0.26.2" + }, + "atomicwrites": { + "hashes": [ + "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", + "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6" + ], + "version": "==1.3.0" + }, + "attrs": { + "hashes": [ + "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", + "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" + ], + "version": "==19.1.0" + }, + "autopep8": { + "hashes": [ + "sha256:33d2b5325b7e1afb4240814fe982eea3a92ebea712869bfd08b3c0393404248c" + ], + "index": "pypi", + "version": "==1.4.3" + }, + "colorama": { + "hashes": [ + "sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", + "sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48" + ], + "version": "==0.4.1" + }, + "coverage": { + "hashes": [ + "sha256:3684fabf6b87a369017756b551cef29e505cb155ddb892a7a29277b978da88b9", + "sha256:39e088da9b284f1bd17c750ac672103779f7954ce6125fd4382134ac8d152d74", + "sha256:3c205bc11cc4fcc57b761c2da73b9b72a59f8d5ca89979afb0c1c6f9e53c7390", + "sha256:465ce53a8c0f3a7950dfb836438442f833cf6663d407f37d8c52fe7b6e56d7e8", + "sha256:48020e343fc40f72a442c8a1334284620f81295256a6b6ca6d8aa1350c763bbe", + "sha256:5296fc86ab612ec12394565c500b412a43b328b3907c0d14358950d06fd83baf", + "sha256:5f61bed2f7d9b6a9ab935150a6b23d7f84b8055524e7be7715b6513f3328138e", + "sha256:68a43a9f9f83693ce0414d17e019daee7ab3f7113a70c79a3dd4c2f704e4d741", + "sha256:6b8033d47fe22506856fe450470ccb1d8ba1ffb8463494a15cfc96392a288c09", + "sha256:7ad7536066b28863e5835e8cfeaa794b7fe352d99a8cded9f43d1161be8e9fbd", + "sha256:7bacb89ccf4bedb30b277e96e4cc68cd1369ca6841bde7b005191b54d3dd1034", + "sha256:839dc7c36501254e14331bcb98b27002aa415e4af7ea039d9009409b9d2d5420", + "sha256:8f9a95b66969cdea53ec992ecea5406c5bd99c9221f539bca1e8406b200ae98c", + "sha256:932c03d2d565f75961ba1d3cec41ddde00e162c5b46d03f7423edcb807734eab", + "sha256:988529edadc49039d205e0aa6ce049c5ccda4acb2d6c3c5c550c17e8c02c05ba", + "sha256:998d7e73548fe395eeb294495a04d38942edb66d1fa61eb70418871bc621227e", + "sha256:9de60893fb447d1e797f6bf08fdf0dbcda0c1e34c1b06c92bd3a363c0ea8c609", + "sha256:9e80d45d0c7fcee54e22771db7f1b0b126fb4a6c0a2e5afa72f66827207ff2f2", + "sha256:a545a3dfe5082dc8e8c3eb7f8a2cf4f2870902ff1860bd99b6198cfd1f9d1f49", + "sha256:a5d8f29e5ec661143621a8f4de51adfb300d7a476224156a39a392254f70687b", + "sha256:aca06bfba4759bbdb09bf52ebb15ae20268ee1f6747417837926fae990ebc41d", + "sha256:bb23b7a6fd666e551a3094ab896a57809e010059540ad20acbeec03a154224ce", + "sha256:bfd1d0ae7e292105f29d7deaa9d8f2916ed8553ab9d5f39ec65bcf5deadff3f9", + "sha256:c62ca0a38958f541a73cf86acdab020c2091631c137bd359c4f5bddde7b75fd4", + "sha256:c709d8bda72cf4cd348ccec2a4881f2c5848fd72903c185f363d361b2737f773", + "sha256:c968a6aa7e0b56ecbd28531ddf439c2ec103610d3e2bf3b75b813304f8cb7723", + "sha256:df785d8cb80539d0b55fd47183264b7002077859028dfe3070cf6359bf8b2d9c", + "sha256:f406628ca51e0ae90ae76ea8398677a921b36f0bd71aab2099dfed08abd0322f", + "sha256:f46087bbd95ebae244a0eda01a618aff11ec7a069b15a3ef8f6b520db523dcf1", + "sha256:f8019c5279eb32360ca03e9fac40a12667715546eed5c5eb59eb381f2f501260", + "sha256:fc5f4d209733750afd2714e9109816a29500718b32dd9a5db01c0cb3a019b96a" + ], + "version": "==4.5.3" + }, + "docopt": { + "hashes": [ + "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491" + ], + "version": "==0.6.2" + }, + "more-itertools": { + "hashes": [ + "sha256:0125e8f60e9e031347105eb1682cef932f5e97d7b9a1a28d9bf00c22a5daef40", + "sha256:590044e3942351a1bdb1de960b739ff4ce277960f2425ad4509446dbace8d9d1" + ], + "markers": "python_version > '2.7'", + "version": "==6.0.0" + }, + "pathtools": { + "hashes": [ + "sha256:7c35c5421a39bb82e58018febd90e3b6e5db34c5443aaaf742b3f33d4655f1c0" + ], + "version": "==0.1.2" + }, + "pep8": { + "hashes": [ + "sha256:b22cfae5db09833bb9bd7c8463b53e1a9c9b39f12e304a8d0bba729c501827ee", + "sha256:fe249b52e20498e59e0b5c5256aa52ee99fc295b26ec9eaa85776ffdb9fe6374" + ], + "index": "pypi", + "version": "==1.7.1" + }, + "pluggy": { + "hashes": [ + "sha256:19ecf9ce9db2fce065a7a0586e07cfb4ac8614fe96edf628a264b1c70116cf8f", + "sha256:84d306a647cc805219916e62aab89caa97a33a1dd8c342e87a37f91073cd4746" + ], + "version": "==0.9.0" + }, + "py": { + "hashes": [ + "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", + "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53" + ], + "version": "==1.8.0" + }, + "pycodestyle": { + "hashes": [ + "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", + "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" + ], + "version": "==2.5.0" + }, + "pytest": { + "hashes": [ + "sha256:592eaa2c33fae68c7d75aacf042efc9f77b27c08a6224a4f59beab8d9a420523", + "sha256:ad3ad5c450284819ecde191a654c09b0ec72257a2c711b9633d677c71c9850c4" + ], + "index": "pypi", + "version": "==4.3.1" + }, + "pytest-cov": { + "hashes": [ + "sha256:0ab664b25c6aa9716cbf203b17ddb301932383046082c081b9848a0edf5add33", + "sha256:230ef817450ab0699c6cc3c9c8f7a829c34674456f2ed8df1fe1d39780f7c87f" + ], + "index": "pypi", + "version": "==2.6.1" + }, + "pytest-watch": { + "hashes": [ + "sha256:06136f03d5b361718b8d0d234042f7b2f203910d8568f63df2f866b547b3d4b9" + ], + "index": "pypi", + "version": "==4.2.0" + }, + "pyyaml": { + "hashes": [ + "sha256:1adecc22f88d38052fb787d959f003811ca858b799590a5eaa70e63dca50308c", + "sha256:436bc774ecf7c103814098159fbb84c2715d25980175292c648f2da143909f95", + "sha256:460a5a4248763f6f37ea225d19d5c205677d8d525f6a83357ca622ed541830c2", + "sha256:5a22a9c84653debfbf198d02fe592c176ea548cccce47553f35f466e15cf2fd4", + "sha256:7a5d3f26b89d688db27822343dfa25c599627bc92093e788956372285c6298ad", + "sha256:9372b04a02080752d9e6f990179a4ab840227c6e2ce15b95e1278456664cf2ba", + "sha256:a5dcbebee834eaddf3fa7366316b880ff4062e4bcc9787b78c7fbb4a26ff2dd1", + "sha256:aee5bab92a176e7cd034e57f46e9df9a9862a71f8f37cad167c6fc74c65f5b4e", + "sha256:c51f642898c0bacd335fc119da60baae0824f2cde95b0330b56c0553439f0673", + "sha256:c68ea4d3ba1705da1e0d85da6684ac657912679a649e8868bd850d2c299cce13", + "sha256:e23d0cc5299223dcc37885dae624f382297717e459ea24053709675a976a3e19" + ], + "version": "==5.1" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "watchdog": { + "hashes": [ + "sha256:965f658d0732de3188211932aeb0bb457587f04f63ab4c1e33eab878e9de961d" + ], + "version": "==0.9.0" + } + } +} diff --git a/README.md b/README.md index 7eefcb2..9b40c5a 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,58 @@ # py_cafe -**Authors**: Dan Huyle, Tim Schoen, Milo Anderson +**Authors**: Dan-Huy Le, Tim Schoen, Milo Anderson -**Version**: 0.0.0 +**Version**: 1.1.0 -## Overview -py_cafe allows restauranteurs to integrate front-of-house data (customer identity, table reservations) with point-of-sale data (menu orders). Managers can view detailed reports on customer preferences & behavior; customers can accumulate reward points or participate in special offers. +## Overview: +Py_cafe allows restauranteurs to integrate front-of-house data (customer identity, table reservations) with point-of-sale data (menu orders). Managers can view detailed reports on customer preferences & behavior; customers can accumulate reward points or participate in special offers. ## Features -More to come +* Allow users to register as customers. +* As Customers users are able to make reservations and place orders. +* Customers are able update order by adding or removing items. +* As Orders are being generated users are able to see the grand-total of items in order. +* Managers are allowed to create, both customer and managers roles. +* As a Manager, you have the ability to add, remove, and update items from the menu. +* Items have the attributes of (Name, Price, Cost (cost of goods sold), and Inventory Count. +* As Orders are being placed, the inventory count reflects changes in real-time. +* As a Manager you have the ability, to run reports which include: +** Users Sales ( how much of each item a selected used has ordered) +** Item Sales(how many times each user as ordered a selected item) +** Timely Sales( All Sales detail (User, Item, Quantity, Time) with in a selected time window. -## Getting Started -We will let you know + +## Getting Started — Running local + + +1. Clone Repo to local machine. +2. Set up a Virtual Environment using PIPENV SHELL +3. Create a database and in your .env file DATABASE_URL = << your data base >> +4. Add a secret key to you .env file. SECRET_KEY = <> +5. Install dependencies using. PIPENV INSTALL +6. Run the app FLASK RUN +7. In your browser go to localhost:5000 +8. In order to set up first manager role go to localhost:5000/user/manager + + +## Tools Used: + * Vue.js + * flask + * sqlalchemy + * wtforms + * SQL + * postgres + +## Deployment: +* Deployed on AWS - ES2 (server) +* AWS - RDS (database) +* guincorn -- Python Web Server Gateway Interface HTTP server +* nginx -- web server which can also be used as a reverse proxy, load balancer. + +## Credits + + * CSS Skeleton V2.04 was used Copyright 2014, Dave Gamache, Free to use under MIT license. + * CSS Inspired by bitsofco.de/holy-grail-layout-css-grid + + + diff --git a/migrations/README b/migrations/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 0000000..f8ed480 --- /dev/null +++ b/migrations/alembic.ini @@ -0,0 +1,45 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000..169d487 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,95 @@ +from __future__ import with_statement + +import logging +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +from flask import current_app +config.set_main_option('sqlalchemy.url', + current_app.config.get('SQLALCHEMY_DATABASE_URI')) +target_metadata = current_app.extensions['migrate'].db.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=target_metadata, literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + connectable = engine_from_config( + config.get_section(config.config_ini_section), + prefix='sqlalchemy.', + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=target_metadata, + process_revision_directives=process_revision_directives, + **current_app.extensions['migrate'].configure_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/43afb694bcba_.py b/migrations/versions/43afb694bcba_.py new file mode 100644 index 0000000..d72b4bb --- /dev/null +++ b/migrations/versions/43afb694bcba_.py @@ -0,0 +1,94 @@ +"""empty message + +Revision ID: 43afb694bcba +Revises: +Create Date: 2019-03-21 17:27:23.180765 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '43afb694bcba' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('items', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=256), nullable=False), + sa.Column('price', sa.Numeric(precision=10, scale=2), nullable=True), + sa.Column('cog', sa.Numeric(precision=10, scale=2), nullable=True), + sa.Column('inventory_count', sa.Integer(), nullable=True), + sa.Column('active', sa.Boolean(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('users', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=256), nullable=True), + sa.Column('email', sa.String(length=256), nullable=True), + sa.Column('password', sa.String(length=256), nullable=True), + sa.Column('type', sa.String(length=64), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('customers', + sa.Column('phone', sa.String(length=32), nullable=True), + sa.Column('id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['id'], ['users.id'], onupdate='CASCADE', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('employees', + sa.Column('pay_rate', sa.Numeric(precision=5, scale=2), nullable=True), + sa.Column('id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['id'], ['users.id'], onupdate='CASCADE', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('managers', + sa.Column('id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['id'], ['users.id'], onupdate='CASCADE', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('orders', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('date_created', sa.DateTime(), nullable=True), + sa.Column('cust_id', sa.Integer(), nullable=True), + sa.Column('empl_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['cust_id'], ['customers.id'], ), + sa.ForeignKeyConstraint(['empl_id'], ['employees.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('reservations', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('cust_id', sa.Integer(), nullable=True), + sa.Column('date', sa.String(length=32), nullable=True), + sa.Column('time', sa.String(length=32), nullable=True), + sa.Column('party', sa.String(length=32), nullable=True), + sa.ForeignKeyConstraint(['cust_id'], ['customers.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('order_items', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('order_id', sa.Integer(), nullable=False), + sa.Column('item_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['item_id'], ['items.id'], ), + sa.ForeignKeyConstraint(['order_id'], ['orders.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('order_items') + op.drop_table('reservations') + op.drop_table('orders') + op.drop_table('managers') + op.drop_table('employees') + op.drop_table('customers') + op.drop_table('users') + op.drop_table('items') + # ### end Alembic commands ### diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ee23a35 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,24 @@ +alembic==1.0.8 +certifi==2019.3.9 +chardet==3.0.4 +Click==7.0 +Flask==1.0.2 +Flask-Migrate==2.4.0 +Flask-SQLAlchemy==2.3.2 +Flask-WTF==0.14.2 +idna==2.8 +itsdangerous==1.1.0 +Jinja2==2.10 +Mako==1.0.7 +MarkupSafe==1.1.1 +passlib==1.7.1 +psycopg2-binary==2.7.7 +python-dateutil==2.8.0 +python-dotenv==0.10.1 +python-editor==1.0.4 +requests==2.21.0 +six==1.12.0 +SQLAlchemy==1.3.1 +urllib3==1.24.1 +Werkzeug==0.14.1 +WTForms==2.2.1 diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..08f2425 --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,19 @@ +from flask import Flask +import os + +basedir = os.path.abspath(os.path.dirname(__file__)) + +app = Flask( + __name__, + static_url_path='', + static_folder='static', + instance_relative_config=True +) + +app.config.from_mapping( + SECRET_KEY=os.environ.get('SECRET_KEY'), + SQLALCHEMY_DATABASE_URI=os.environ.get('DATABASE_URL'), + SQLALCHEMY_TRACK_MODIFICATIONS=False +) + +from . import auth, routes, models, exceptions diff --git a/src/auth.py b/src/auth.py new file mode 100644 index 0000000..aee52ce --- /dev/null +++ b/src/auth.py @@ -0,0 +1,100 @@ +from flask import render_template, flash, redirect, url_for, session, abort, g +from .models import db, User, Manager, Customer, Employee +from .forms import RegisterForm, AuthForm +from . import app +import functools + + +def login_required(view): + """ + restricts access to decorated route + """ + @functools.wraps(view) + def wrapped_view(**kwargs): + if g.user is None: + return redirect(url_for('.login')) + return view(**kwargs) + return wrapped_view + + +def authorization_required(view=None, roles=[]): + """ + restricts access to only certain roles + """ + if not view: + return functools.partial(authorization_required, roles=roles) + + @functools.wraps(view) + def wrapped_view(**kwargs): + if g.user is None: + return redirect(url_for('.login')) + if g.user.type not in roles: + abort(401) + return view(**kwargs) + return wrapped_view + + +@app.before_request +def load_logged_in_user(): + """ + + """ + user_id = session.get('user_id') + if user_id is None: + g.user = None + else: + g.user = User.query.get(user_id) + + +@app.route('/register', methods=['GET', 'POST']) +def register(): + """ + route handler for register + """ + form = RegisterForm() + if form.validate_on_submit(): + customer = Customer( + name=form.data['name'], + email=form.data['email'], + phone=form.data['phone'], + password=form.data['password'] + ) + db.session.add(customer) + db.session.commit() + return redirect(url_for('.home')) + return render_template('auth/register.html', form=form) + + +@app.route('/login', methods=['GET', 'POST']) +def login(): + """ + route handler for login page + """ + form = AuthForm() + if form.validate_on_submit(): + email = form.data['email'] + password = form.data['password'] + error = None + + user = User.query.filter_by(email=email).first() + if not user or not User.check_pass_hash(user, password): + error = 'Invalid username or password' + + if not error: + session.clear() + session['user_id'] = user.id + return redirect(url_for('.home')) + + flash(error) + + return render_template('auth/login.html', form=form) + + +@app.route('/logout') +@login_required +def logout(): + """ + route handler for logout + """ + session.clear() + return redirect(url_for('.login')) diff --git a/src/exceptions.py b/src/exceptions.py new file mode 100644 index 0000000..de66301 --- /dev/null +++ b/src/exceptions.py @@ -0,0 +1,20 @@ +from . import app +from flask import render_template + + +@app.errorhandler(404) +def not_found(error): + """ Custom 404 handler """ + return render_template('exceptions/404.html', error=error), 404 + + +@app.errorhandler(400) +def bad_request(error): + """ Custom 400 handler """ + return render_template('exceptions/400.html', error=error), 400 + + +@app.errorhandler(401) +def bad_request(error): + """ Custom 401 handler """ + return render_template('exceptions/401.html', error=error), 401 diff --git a/src/forms.py b/src/forms.py new file mode 100644 index 0000000..47578ac --- /dev/null +++ b/src/forms.py @@ -0,0 +1,149 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, SelectField, PasswordField, HiddenField, BooleanField + +from wtforms.validators import DataRequired, Email, Optional +from wtforms.validators import DataRequired +from wtforms.fields.html5 import DateField, TimeField + +from .models import Manager, Customer, Employee, Order, OrderItems, Item, User +from flask import g + + +class RegisterForm(FlaskForm): + """ + Registration form + """ + name = StringField('name', validators=[DataRequired()]) + email = StringField('email', validators=[DataRequired()]) + phone = StringField('phone', validators=[DataRequired()]) + password = PasswordField('password', validators=[DataRequired()]) + + +class AuthForm(FlaskForm): + """ + login authentication form + """ + email = StringField('email', validators=[DataRequired()]) + password = PasswordField('password', validators=[DataRequired()]) + + +class AddItemsForm(FlaskForm): + """ + form to add items + """ + name = StringField('name', validators=[DataRequired()]) + price = StringField('price', validators=[DataRequired()]) + cost = StringField('cost', validators=[DataRequired()]) + count = StringField('count', validators=[DataRequired()]) + + +class DateTimeForm(FlaskForm): + start_date = DateField('Start Date', format='%Y-%m-%d') + start_time = TimeField('Start Time') + end_date = DateField('End Date', format='%Y-%m-%d') + end_time = TimeField('Start Time') + + +class ReservationForm(FlaskForm): + date = DateField('date', format='%Y-%m-%d') + time = TimeField('time') + party = StringField('party', validators=[DataRequired()]) + + +class OrderForm(FlaskForm): + """ + order form + """ + item_ids = HiddenField('item_ids', validators=[DataRequired()], render_kw={ + "v-model": "orderItemIds"}) + customer = SelectField('customer', validators=[Optional()], default=None) + employee = SelectField('employee', validators=[Optional()], default=None) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.customer.choices = [('None', '')] + [(str(c.id), c.name) + for c in Customer.query.all()] + self.employee.choices = [('None', '')] + [(str(e.id), e.name) + for e in Employee.query.all()] + + +class UpdateItemsForm(FlaskForm): + """ + update items form + """ + items = SelectField('items') + price = StringField('price', validators=[DataRequired()]) + cost = StringField('cost', validators=[DataRequired()]) + count = StringField('count', validators=[DataRequired()]) + active = BooleanField('active', default=True) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.items.choices = [(str(item.id), item.name) + for item in Item.query.all()] + + +class ItemReportForm(FlaskForm): + """ + item form + """ + items = SelectField('items') + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.items.choices = [(str(item.id), item.name) + for item in Item.query.all()] + + +class DeleteForm(FlaskForm): + """ + delete item form + """ + items = SelectField('items') + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.items.choices = [(str(item.id), item.name) + for item in Item.query.filter_by(active=True).all()] + + +class DeleteUserForm(FlaskForm): + """ + delete user form + """ + users = SelectField('users') + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.users.choices = [(str(user.id), user.name) + for user in User.query.all()] + + +class ManagerForm(FlaskForm): + """ + create manager form + """ + name = StringField('name', validators=[DataRequired()]) + email = StringField('email', validators=[DataRequired()]) + password = PasswordField('password', validators=[DataRequired()]) + + +class EmployeeForm(FlaskForm): + """ + employee form + """ + name = StringField('name', validators=[DataRequired()]) + email = StringField('email', validators=[DataRequired(), Email()]) + password = PasswordField('password', validators=[DataRequired()]) + pay_rate = StringField('pay rate') + + +class EmployeeSelect(FlaskForm): + """ creates a select-option element with all employees """ + + employee = SelectField('employee') + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.employee.choices = [('None', '')] + [(str(e.id), e.name) + for e in Employee.query.all()] diff --git a/src/models.py b/src/models.py new file mode 100644 index 0000000..a1859df --- /dev/null +++ b/src/models.py @@ -0,0 +1,167 @@ +from flask_sqlalchemy import SQLAlchemy +from flask import g +from passlib.hash import sha256_crypt +from datetime import datetime as dt +from flask_migrate import Migrate +from . import app + +db = SQLAlchemy(app) +migrate = Migrate(app, db) + + +class User(db.Model): + __tablename__ = 'users' + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(256)) + email = db.Column(db.String(256)) + password = db.Column(db.String(256)) + type = db.Column(db.String(64)) + + __mapper_args__ = { + 'polymorphic_identity': 'user', + 'polymorphic_on': type + } + + def __init__(self, name, email, password): + self.name = name + self.email = email + self.password = sha256_crypt.hash(password) + + @classmethod + def check_pass_hash(cls, user, password): + if user is not None: + if sha256_crypt.verify(password, user.password): + return True + return False + + +class Manager(User): + __tablename__ = 'managers' + + id = db.Column( + db.ForeignKey('users.id', ondelete='CASCADE', onupdate='CASCADE'), + primary_key=True + ) + __mapper_args__ = {'polymorphic_identity': 'manager'} + + def __init__(self, name, email, password): + User.__init__(self, name, email, password) + + +class Customer(User): + __tablename__ = 'customers' + + phone = db.Column(db.String(32)) + id = db.Column( + db.ForeignKey('users.id', ondelete='CASCADE', onupdate='CASCADE'), + primary_key=True + ) + orders = db.relationship('Order', back_populates='customer') + reservations = db.relationship('Reservation', back_populates='customer') + __mapper_args__ = {'polymorphic_identity': 'customer'} + + def __init__(self, name, email, password, phone): + User.__init__(self, name, email, password) + self.phone = phone + + +class Employee(User): + __tablename__ = 'employees' + + pay_rate = db.Column(db.Numeric(5, 2)) + id = db.Column( + db.ForeignKey('users.id', ondelete='CASCADE', onupdate='CASCADE'), + primary_key=True + ) + orders = db.relationship('Order', back_populates='employee') + __mapper_args__ = {'polymorphic_identity': 'employee'} + + def __init__(self, name, email, password, pay_rate): + User.__init__(self, name, email, password) + self.pay_rate = pay_rate + + +class Reservation(db.Model): + __tablename__ = 'reservations' + + id = db.Column(db.Integer, primary_key=True) + cust_id = db.Column(db.ForeignKey('customers.id', ondelete='CASCADE')) + date = db.Column(db.String(32)) + time = db.Column(db.String(32)) + party = db.Column(db.String(32)) + + customer = db.relationship( + 'Customer', + back_populates='reservations' + ) + + +class Order(db.Model): + __tablename__ = 'orders' + + id = db.Column(db.Integer, primary_key=True) + date_created = db.Column(db.DateTime, default=dt.now()) + cust_id = db.Column(db.ForeignKey('customers.id')) + empl_id = db.Column(db.ForeignKey('employees.id')) + + customer = db.relationship( + 'Customer', + back_populates='orders' + ) + employee = db.relationship( + 'Employee', + back_populates='orders' + ) + items = db.relationship( + 'Item', + secondary='order_items', + back_populates='orders' + ) + + def __init__(self, items, customer, employee): + for item in items: + item.inventory_count -= 1 + db.session.commit() + self.items = items + if customer: + self.customer = customer + if employee: + self.employee = employee + + +class OrderItems(db.Model): + __tablename__ = 'order_items' + + id = db.Column(db.Integer, primary_key=True) + order_id = db.Column(db.ForeignKey('orders.id'), nullable=False) + item_id = db.Column(db.ForeignKey('items.id'), nullable=False) + + order = db.relationship( + 'Order', + backref=db.backref('order_items', cascade='all') + ) + item = db.relationship( + 'Item', + backref=db.backref('order_items') + ) + + +class Item(db.Model): + __tablename__ = 'items' + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(256), nullable=False) + price = db.Column(db.Numeric(10, 2)) + cog = db.Column(db.Numeric(10, 2)) + inventory_count = db.Column(db.Integer, default=0) + active = db.Column(db.Boolean, default=True) + + orders = db.relationship( + 'Order', + secondary='order_items', + back_populates='items' + ) + + def __repr__(self): + return ''.format(self.name) diff --git a/src/models_reports.py b/src/models_reports.py new file mode 100644 index 0000000..2461f23 --- /dev/null +++ b/src/models_reports.py @@ -0,0 +1,76 @@ +from flask_sqlalchemy import SQLAlchemy +from sqlalchemy import func +from .models import db, User, Customer, Manager, Employee, Order, OrderItems, Item + + +class CustomerOrders(): + + def __init__(self, cust_id): + self.cust_id = cust_id + + def item_totals(self, cust_id): + rtn = [] + order_ids = [] + counts = [] + order_items_list = [] + + orders = Order.query.filter_by(cust_id=cust_id).all() + for order in orders: + order_ids.append(order.id) + for i in order_ids: + orderd_items = OrderItems.query.filter_by(order_id=i).all() + for j in orderd_items: + order_items_list.append(j.item_id) + items = Item.query.all() + for item in items: + counts.append((item.name, order_items_list.count(item.id))) + return counts + + def customer_totals(self, item_id=0): + rtn = [] + SQL = """ +select users.name, count(items.id), items.name, items.active from users +inner join customers on users.id = customers.id +inner join orders on customers.id=orders.cust_id +inner join order_items on orders.id=order_items.order_id +inner join items on order_items.item_id=items.id +where items.id={} +group by users.name, items.name, items.active; """.format(item_id) + test = db.session.execute(SQL) + for row in test: + rtn.append(row) + return rtn + + def time(self, start_time, end_time): + rtn = [] + SQL = """select users.name, orders.date_created, count(items.id), items.name from users +inner join customers on users.id = customers.id +inner join orders on customers.id=orders.cust_id +inner join order_items on orders.id=order_items.order_id +inner join items on order_items.item_id=items.id +where orders.date_created > '{}' +and orders.date_created < '{}' +group by users.name, orders.date_created, items.name;""".format(start_time, end_time) + test = db.session.execute(SQL) + for row in test: + rtn.append(row) + return rtn + + +class EmployeeOrders(): + + @classmethod + def employee_items_total(cls, empl_id): + rtn = [] + SQL = """ +select items.name, count(items.id) from users +inner join employees on users.id = employees.id +inner join orders on employees.id=orders.empl_id +inner join order_items on orders.id=order_items.order_id +inner join items on order_items.item_id=items.id +where employees.id={} +group by items.name; """.format(empl_id) + test = db.session.execute(SQL) + for row in test: + rtn.append(row) + return rtn \ No newline at end of file diff --git a/src/routes.py b/src/routes.py new file mode 100644 index 0000000..fe955b5 --- /dev/null +++ b/src/routes.py @@ -0,0 +1,298 @@ +from flask import render_template, redirect, url_for, request, flash, session, g +from sqlalchemy.exc import DBAPIError, IntegrityError +from . import app +from .forms import RegisterForm, AddItemsForm, OrderForm, UpdateItemsForm +from .forms import DeleteForm, DeleteUserForm, ManagerForm, ItemReportForm +from .forms import ReservationForm, EmployeeForm, DateTimeForm, EmployeeSelect +from .models import db, User, Manager, Customer, Employee, Item, Order, Reservation +from .models_reports import CustomerOrders, EmployeeOrders +from .auth import login_required, authorization_required +import requests +import json +import os + + +@app.route('/') +def home(): + """ + route handler for home + """ + return render_template('home.html'), 200 + + +@app.route('/about') +def about(): + """ + route handler for the about page + """ + return render_template('about_us.html'), 200 + + +@app.route('/order', methods=['GET', 'POST']) +@authorization_required(roles=['customer', 'employee', 'manager']) +def order(): + """ + route handler for order + """ + form = OrderForm() + if form.validate_on_submit(): + item_ids = form.data['item_ids'].split(',') + items = [Item.query.get(i) for i in item_ids] + customer = None + if g.user.type == 'customer': + customer = Customer.query.get(g.user.id) + elif form.data['customer'] != 'None': + cust_id = form.data['customer'] + customer = Customer.query.get(cust_id) + + employee = None + if g.user.type == 'employee': + employee = Employee.query.get(g.user.id) + elif form.data['employee'] != 'None': + empl_id = form.data['employee'] + employee = Employee.query.get(empl_id) + + order = Order( + customer=customer, + employee=employee, + items=items + ) + db.session.add(order) + db.session.commit() + + items = Item.query.filter_by(active=True).all() + return render_template( + 'order.html', + items=items, + form=form + ) + + +@app.route('/item', methods=['GET']) +@authorization_required(roles=['employee', 'manager']) +def all_items(): + """ + route handler for items to display all items in database + """ + items = Item.query.filter_by(active=True).all() + return render_template('items/all_items.html', items=items) + + +@app.route('/item/add', methods=['GET', 'POST']) +@authorization_required(roles=['employee', 'manager']) +def add_items(): + """ + route handler for add items + """ + form = AddItemsForm() + if form.validate_on_submit(): + item = Item( + name=form.data['name'], + cog=form.data['cost'], + price=form.data['price'], + inventory_count=form.data['count'] + ) + db.session.add(item) + db.session.commit() + return redirect(url_for('.add_items')) + items = Item.query.filter_by(active=True).all() + return render_template('items/add_items.html', form=form, items=items) + + +@app.route('/item/delete', methods=['GET', 'POST']) # this is a DELETE +@authorization_required(roles=['employee', 'manager']) +def delete_items(): + """ + route handler for delete items + """ + form = DeleteForm() + if form.validate_on_submit(): + name = form.data['items'] + item = Item.query.filter_by(id=name).first() + item.active = False + db.session.commit() + return redirect(url_for('.all_items')) + items = Item.query.filter_by(active=True).all() + return render_template('items/delete_items.html', form=form, items=items) + + +@app.route('/item/update', methods=['GET', 'POST']) # this is a PUT +@authorization_required(roles=['employee', 'manager']) +def update_items(): + """ + route handler for update items + """ + form = UpdateItemsForm() + if form.validate_on_submit(): + item = Item.query.get(form.data['items']) + item.cog = form.data['cost'], + item.price = form.data['price'], + item.inventory_count = form.data['count'] + item.active = form.data['active'] + db.session.commit() + return redirect(url_for('.update_items')) + items = Item.query.all() + return render_template('items/update_items.html', form=form, items=items) + + +@app.route('/all_users', methods=['GET', 'POST']) +@authorization_required(roles=['manager']) +def all_users(): + """ + route handler to display all users + """ + form = DeleteUserForm() + if form.validate_on_submit(): + id = form.data['users'] + user = User.query.filter_by(id=id).first() + db.session.delete(user) + db.session.commit() + return redirect(url_for('.all_users')) + users = User.query.all() + return render_template('/user/all_users.html', users=users, form=form) + + +@app.route('/reservation', methods=['GET', 'POST']) +@authorization_required(roles=['customer', 'manager', 'employee']) +def reservation(): + """ + route handler for reservations + """ + form = ReservationForm() + if form.validate_on_submit(): + reservation = Reservation( + date=form.data['date'], + time=form.data['time'], + party=form.data['party'], + customer=Customer.query.filter_by(id=g.user.id).first() + ) + db.session.add(reservation) + db.session.commit() + return redirect(url_for('.reservation')) + if g.user.type == 'manager' or g.user.type == 'employee': + reservations = Reservation.query.all() + else: + reservations = Reservation.query.filter_by(cust_id=g.user.id).all() + return render_template('/reservations.html', form=form, reservations=reservations) + + +@app.route('/user/manager', methods=['GET', 'POST']) +# @authorization_required(roles=['manager']) +def create_manager(): + """ + route handler to create a manager role + """ + form = ManagerForm() + if form.validate_on_submit(): + manager = Manager( + name=form.data['name'], + email=form.data['email'], + password=form.data['password'] + ) + db.session.add(manager) + db.session.commit() + return redirect(url_for('.all_users')) + + return render_template('/user/create_manager.html', form=form) + + +@app.route('/user/employee', methods=['GET', 'POST']) +@authorization_required(roles=['manager']) +def create_employee(): + """ + route handler to create an employee role + """ + form = EmployeeForm() + if form.validate_on_submit(): + employee = Employee( + name=form.data['name'], + email=form.data['email'], + password=form.data['password'], + pay_rate=form.data['pay_rate'] + ) + db.session.add(employee) + db.session.commit() + return redirect(url_for('.all_users')) + + return render_template('/user/create_employee.html', form=form) + + +@app.route('/manager', methods=['GET']) +@authorization_required(roles=['employee', 'manager']) +def reports(): + """ + route handler for manager reports + """ + return render_template('/manager/report_index.html') + + +@app.route('/manager/by_customer', methods=['GET', 'POST']) +@authorization_required(roles=['employee', 'manager']) +def by_customer(): + """ + route handler for tems sold by customer report + """ + form = DeleteUserForm() + if form.validate_on_submit(): + id = form.data['users'] + report = CustomerOrders(id) + content = report.item_totals(id) + users = User.query.all() + + return render_template('/manager/by_customer.html', users=users, form=form, content=content) + + users = User.query.all() + return render_template('/manager/by_customer.html', users=users, form=form, content=None) + + +@app.route('/manager/by_time', methods=['GET', 'POST']) +def by_time(): + form = DateTimeForm() + if form.validate_on_submit(): + start_date = form.data['start_date'] + end_date = form.data['end_date'] + start_time = form.data['start_time'] + end_time = form.data['end_time'] + print('valid') + print((start_date, end_date)) + sql_start_time = (str(start_date)+' ' + str(start_time)) + sql_end_time = (str(end_date)+' ' + str(end_time)) + report = CustomerOrders(id) + content = report.time(sql_start_time, sql_end_time) + return render_template('/manager/by_time.html', form=form, content=content) + + return render_template('/manager/by_time.html', form=form, content=None) + + +@app.route('/manager/by_item', methods=['GET', 'POST']) +@authorization_required(roles=['employee', 'manager']) +def by_item(): + """ + route handler for total customer sales by item report + """ + form = ItemReportForm() + if form.validate_on_submit(): + id = form.data['items'] + report = CustomerOrders(id) + content = report.customer_totals(id) + items = Item.query.all() + + return render_template('/manager/by_item.html', items=items, form=form, content=content) + + items = Item.query.all() + return render_template('/manager/by_item.html', items=items, form=form, content=None) + + +@app.route('/manager/by_employee', methods=['GET', 'POST']) +@authorization_required(roles=['employee', 'manager']) +def by_employee(): + """ + route handler for total employee sales + """ + form = EmployeeSelect() + if form.validate_on_submit(): + empl_id = form.data['employee'] + report = EmployeeOrders.employee_items_total(empl_id) + return render_template('/manager/by_employee.html', form=form, content=report) + + return render_template('/manager/by_employee.html', form=form, content=None) \ No newline at end of file diff --git a/src/static/assets/dan.png b/src/static/assets/dan.png new file mode 100644 index 0000000..b85c9a8 Binary files /dev/null and b/src/static/assets/dan.png differ diff --git a/src/static/assets/milo.jpg b/src/static/assets/milo.jpg new file mode 100644 index 0000000..2307d6c Binary files /dev/null and b/src/static/assets/milo.jpg differ diff --git a/src/static/assets/tim.jpg b/src/static/assets/tim.jpg new file mode 100644 index 0000000..51268e3 Binary files /dev/null and b/src/static/assets/tim.jpg differ diff --git a/src/static/css/base.css b/src/static/css/base.css new file mode 100644 index 0000000..7cd558b --- /dev/null +++ b/src/static/css/base.css @@ -0,0 +1,48 @@ +header, footer { + padding: 0px 20px; +} +nav ul li { + list-style-type: none; + margin: 0px; +} +nav ul li a { + display: block; + margin: 0px; + padding: 5px; + padding-left: 20px; + border: 1px solid darkgrey; + border-top: none; + color: navy; + text-decoration: none; + font-weight: bold; +} +nav ul li a:hover { + background-color: ivory; +} +header { + border-bottom: 2px solid darkblue; +} +footer { + border-top: 2px solid darkorange; + background-color: navajowhite; + height: 30px; +} + +table th { + text-align: left; + padding: 10px 18px; +} +table tr:nth-child(odd) td { + background-color: whitesmoke; +} +table tr:nth-child(even) td { + background-color: ivory; +} +table tr td { + padding: 4px 18px; + text-align: left; + border-bottom: 1px solid #E1E1E1; +} +.container p { + margin-bottom: 1.5rem; +} \ No newline at end of file diff --git a/src/static/css/layout.css b/src/static/css/layout.css new file mode 100644 index 0000000..faac9ac --- /dev/null +++ b/src/static/css/layout.css @@ -0,0 +1,55 @@ +/* Inspired by bitsofco.de/holy-grail-layout-css-grid */ + +header { grid-area: header; } +nav { grid-area: nav; } +main { grid-area: main; } +footer { grid-area: footer; } + +body { + display: grid; + grid-template-areas: "header header" + "nav main" + "footer footer"; + grid-template-columns: 200px 1fr; + grid-template-rows: 120px 1fr 30px; + min-height: 100vh; +} + +header h5 { + float: right; +} +main { + padding-top: 14px; +} + + +@media screen and (max-width: 800px) { + body { + grid-template-areas: "header" + "nav" + "main" + "footer"; + grid-template-columns: 100%; + grid-template-rows: 100px 50px 1fr 20px; + } + h1 { + font-size: 2.8em; + } + h4 { + font-size: 1.6em; + } + h5 { + font-size: 1.2em; + } + nav ul li a { + float: left; + font-size: .9em; + padding: 6px; + } + nav #auth-nav { + margin-top: -100%; + } + nav #auth-nav a { + float: right; + } +} \ No newline at end of file diff --git a/src/static/css/module.css b/src/static/css/module.css new file mode 100644 index 0000000..2430b55 --- /dev/null +++ b/src/static/css/module.css @@ -0,0 +1,99 @@ +#reports p { + width: 60%; + font-size: 1.2em; +} +#reports p a { + float: right; + margin-bottom: 0px; +} + +/* -------- Orders page -------- */ +#orders { + display: grid; + column-gap: 40px; + grid-template-columns: 1fr 1fr; + grid-template-areas: + "items order-items" + "form order-items"; +} +#orders #items-table { + grid-area: items; +} +#orders #order-items-table { + grid-area: order-items; +} +#orders #order-form { + grid-area: form; + display: grid; + column-gap: 10px; + margin-bottom: 0px; + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: 1fr 3fr; + grid-template-areas: "cl el ." + "ci ei b"; +} +#orders #order-form label[for=customer] { + grid-area: cl; +} +#orders #order-form #customer { + grid-area: ci; +} +#orders #order-form label[for=employee] { + grid-area: el; +} +#orders #order-form #employee { + grid-area: ei; +} +#orders #order-form-btn { + grid-area: b; +} + +#items-table td.btn-add, +#order-items-table td.btn-remove { + cursor: pointer; + font-weight: bold; + padding: 4px 0px; + width: 40px; + font-size: 1.4em; + text-align: center; + background-color: springgreen; +} +#order-items-table td.btn-remove { + background-color: orangered; +} +a.button { + display: inline-block; + height: 38px; + padding: 0 30px; + color: #555; + text-align: center; + font-size: 11px; + font-weight: 600; + line-height: 38px; + letter-spacing: .1rem; + text-transform: uppercase; + text-decoration: none; + white-space: nowrap; + background-color: transparent; + border-radius: 4px; + border: 1px solid #bbb; + cursor: pointer; + box-sizing: border-box; +} + + +/* -------- About Us page -------- */ +#about h5 { + font-size: 1.4em; +} +#about h4 { + margin-bottom: 0px; +} +#about img { + width:200px; + margin: 0px 20px 30px 0px; + float: left; +} +#about div { + clear: both; +} \ No newline at end of file diff --git a/src/static/css/normalize.css b/src/static/css/normalize.css new file mode 100644 index 0000000..6f97c5e --- /dev/null +++ b/src/static/css/normalize.css @@ -0,0 +1,428 @@ +/*! normalize.css v3.0.2 | MIT License | git.io/normalize */ + +/** + * 1. Set default font family to sans-serif. + * 2. Prevent iOS text size adjust after orientation change, without disabling + * user zoom. + */ + + html { + font-family: sans-serif; /* 1 */ + -ms-text-size-adjust: 100%; /* 2 */ + -webkit-text-size-adjust: 100%; /* 2 */ + } + + /** + * Remove default margin. + */ + + body { + margin: 0; + } + + /* HTML5 display definitions + ========================================================================== */ + + /** + * Correct `block` display not defined for any HTML5 element in IE 8/9. + * Correct `block` display not defined for `details` or `summary` in IE 10/11 + * and Firefox. + * Correct `block` display not defined for `main` in IE 11. + */ + + article, + aside, + details, + figcaption, + figure, + footer, + header, + hgroup, + main, + menu, + nav, + section, + summary { + display: block; + } + + /** + * 1. Correct `inline-block` display not defined in IE 8/9. + * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. + */ + + audio, + canvas, + progress, + video { + display: inline-block; /* 1 */ + vertical-align: baseline; /* 2 */ + } + + /** + * Prevent modern browsers from displaying `audio` without controls. + * Remove excess height in iOS 5 devices. + */ + + audio:not([controls]) { + display: none; + height: 0; + } + + /** + * Address `[hidden]` styling not present in IE 8/9/10. + * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. + */ + + [hidden], + template { + display: none; + } + + /* Links + ========================================================================== */ + + /** + * Remove the gray background color from active links in IE 10. + */ + + a { + background-color: transparent; + } + + /** + * Improve readability when focused and also mouse hovered in all browsers. + */ + + a:active, + a:hover { + outline: 0; + } + + /* Text-level semantics + ========================================================================== */ + + /** + * Address styling not present in IE 8/9/10/11, Safari, and Chrome. + */ + + abbr[title] { + border-bottom: 1px dotted; + } + + /** + * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. + */ + + b, + strong { + font-weight: bold; + } + + /** + * Address styling not present in Safari and Chrome. + */ + + dfn { + font-style: italic; + } + + /** + * Address variable `h1` font-size and margin within `section` and `article` + * contexts in Firefox 4+, Safari, and Chrome. + */ + + h1 { + font-size: 2em; + margin: 0.67em 0; + } + + /** + * Address styling not present in IE 8/9. + */ + + mark { + background: #ff0; + color: #000; + } + + /** + * Address inconsistent and variable font size in all browsers. + */ + + small { + font-size: 80%; + } + + /** + * Prevent `sub` and `sup` affecting `line-height` in all browsers. + */ + + sub, + sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; + } + + sup { + top: -0.5em; + } + + sub { + bottom: -0.25em; + } + + /* Embedded content + ========================================================================== */ + + /** + * Remove border when inside `a` element in IE 8/9/10. + */ + + img { + border: 0; + } + + /** + * Correct overflow not hidden in IE 9/10/11. + */ + + svg:not(:root) { + overflow: hidden; + } + + /* Grouping content + ========================================================================== */ + + /** + * Address margin not present in IE 8/9 and Safari. + */ + + figure { + margin: 1em 40px; + } + + /** + * Address differences between Firefox and other browsers. + */ + + hr { + -moz-box-sizing: content-box; + box-sizing: content-box; + height: 0; + } + + /** + * Contain overflow in all browsers. + */ + + pre { + overflow: auto; + } + + /** + * Address odd `em`-unit font size rendering in all browsers. + */ + + code, + kbd, + pre, + samp { + font-family: monospace, monospace; + font-size: 1em; + } + + /* Forms + ========================================================================== */ + + /** + * Known limitation: by default, Chrome and Safari on OS X allow very limited + * styling of `select`, unless a `border` property is set. + */ + + /** + * 1. Correct color not being inherited. + * Known issue: affects color of disabled elements. + * 2. Correct font properties not being inherited. + * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. + */ + + button, + input, + optgroup, + select, + textarea { + color: inherit; /* 1 */ + font: inherit; /* 2 */ + margin: 0; /* 3 */ + } + + /** + * Address `overflow` set to `hidden` in IE 8/9/10/11. + */ + + button { + overflow: visible; + } + + /** + * Address inconsistent `text-transform` inheritance for `button` and `select`. + * All other form control elements do not inherit `text-transform` values. + * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. + * Correct `select` style inheritance in Firefox. + */ + + button, + select { + text-transform: none; + } + + /** + * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` + * and `video` controls. + * 2. Correct inability to style clickable `input` types in iOS. + * 3. Improve usability and consistency of cursor style between image-type + * `input` and others. + */ + + button, + html input[type="button"], /* 1 */ + input[type="reset"], + input[type="submit"] { + -webkit-appearance: button; /* 2 */ + cursor: pointer; /* 3 */ + } + + /** + * Re-set default cursor for disabled elements. + */ + + button[disabled], + html input[disabled] { + cursor: default; + } + + /** + * Remove inner padding and border in Firefox 4+. + */ + + button::-moz-focus-inner, + input::-moz-focus-inner { + border: 0; + padding: 0; + } + + /** + * Address Firefox 4+ setting `line-height` on `input` using `!important` in + * the UA stylesheet. + */ + + input { + line-height: normal; + } + + /** + * It's recommended that you don't attempt to style these elements. + * Firefox's implementation doesn't respect box-sizing, padding, or width. + * + * 1. Address box sizing set to `content-box` in IE 8/9/10. + * 2. Remove excess padding in IE 8/9/10. + */ + + input[type="checkbox"], + input[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ + } + + /** + * Fix the cursor style for Chrome's increment/decrement buttons. For certain + * `font-size` values of the `input`, it causes the cursor style of the + * decrement button to change from `default` to `text`. + */ + + input[type="number"]::-webkit-inner-spin-button, + input[type="number"]::-webkit-outer-spin-button { + height: auto; + } + + /** + * 1. Address `appearance` set to `searchfield` in Safari and Chrome. + * 2. Address `box-sizing` set to `border-box` in Safari and Chrome + * (include `-moz` to future-proof). + */ + + input[type="search"] { + -webkit-appearance: textfield; /* 1 */ + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; /* 2 */ + box-sizing: content-box; + } + + /** + * Remove inner padding and search cancel button in Safari and Chrome on OS X. + * Safari (but not Chrome) clips the cancel button when the search input has + * padding (and `textfield` appearance). + */ + + input[type="search"]::-webkit-search-cancel-button, + input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; + } + + /** + * Define consistent border, margin, and padding. + */ + + fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; + } + + /** + * 1. Correct `color` not being inherited in IE 8/9/10/11. + * 2. Remove padding so people aren't caught out if they zero out fieldsets. + */ + + legend { + border: 0; /* 1 */ + padding: 0; /* 2 */ + } + + /** + * Remove default vertical scrollbar in IE 8/9/10/11. + */ + + textarea { + overflow: auto; + } + + /** + * Don't inherit the `font-weight` (applied by a rule above). + * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. + */ + + optgroup { + font-weight: bold; + } + + /* Tables + ========================================================================== */ + + /** + * Remove most spacing between table cells. + */ + + table { + border-collapse: collapse; + border-spacing: 0; + } + + td, + th { + padding: 0; + } + \ No newline at end of file diff --git a/src/static/css/skeleton.css b/src/static/css/skeleton.css new file mode 100644 index 0000000..d1f0e75 --- /dev/null +++ b/src/static/css/skeleton.css @@ -0,0 +1,404 @@ +/* +* Skeleton V2.0.4 +* Copyright 2014, Dave Gamache +* www.getskeleton.com +* Free to use under the MIT license. +* http://www.opensource.org/licenses/mit-license.php +* 12/29/2014 +*/ + + +/* Table of contents +–––––––––––––––––––––––––––––––––––––––––––––––––– +- Grid +- Base Styles +- Typography +- Links +- Buttons +- Forms +- Lists +- Code +- Tables +- Spacing +- Utilities +- Clearing +- Media Queries +*/ + + +/* Grid +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +.container { + position: relative; + width: 100%; + max-width: 1200px; + margin: 0 auto; + padding: 0 20px; + box-sizing: border-box; } + .column, + .columns { + width: 100%; + float: left; + box-sizing: border-box; } + + /* For devices larger than 400px */ + @media (min-width: 400px) { + .container { + width: 85%; + padding: 0; } + } + + /* For devices larger than 550px */ + @media (min-width: 550px) { + .container { + width: 80%; } + .column, + .columns { + margin-left: 4%; } + .column:first-child, + .columns:first-child { + margin-left: 0; } + + .one.column, + .one.columns { width: 4.66666666667%; } + .two.columns { width: 13.3333333333%; } + .three.columns { width: 22%; } + .four.columns { width: 30.6666666667%; } + .five.columns { width: 39.3333333333%; } + .six.columns { width: 48%; } + .seven.columns { width: 56.6666666667%; } + .eight.columns { width: 65.3333333333%; } + .nine.columns { width: 74.0%; } + .ten.columns { width: 82.6666666667%; } + .eleven.columns { width: 91.3333333333%; } + .twelve.columns { width: 100%; margin-left: 0; } + + .one-third.column { width: 30.6666666667%; } + .two-thirds.column { width: 65.3333333333%; } + + .one-half.column { width: 48%; } + + /* Offsets */ + .offset-by-one.column, + .offset-by-one.columns { margin-left: 8.66666666667%; } + .offset-by-two.column, + .offset-by-two.columns { margin-left: 17.3333333333%; } + .offset-by-three.column, + .offset-by-three.columns { margin-left: 26%; } + .offset-by-four.column, + .offset-by-four.columns { margin-left: 34.6666666667%; } + .offset-by-five.column, + .offset-by-five.columns { margin-left: 43.3333333333%; } + .offset-by-six.column, + .offset-by-six.columns { margin-left: 52%; } + .offset-by-seven.column, + .offset-by-seven.columns { margin-left: 60.6666666667%; } + .offset-by-eight.column, + .offset-by-eight.columns { margin-left: 69.3333333333%; } + .offset-by-nine.column, + .offset-by-nine.columns { margin-left: 78.0%; } + .offset-by-ten.column, + .offset-by-ten.columns { margin-left: 86.6666666667%; } + .offset-by-eleven.column, + .offset-by-eleven.columns { margin-left: 95.3333333333%; } + + .offset-by-one-third.column, + .offset-by-one-third.columns { margin-left: 34.6666666667%; } + .offset-by-two-thirds.column, + .offset-by-two-thirds.columns { margin-left: 69.3333333333%; } + + .offset-by-one-half.column, + .offset-by-one-half.columns { margin-left: 52%; } + + } + + + /* Base Styles + –––––––––––––––––––––––––––––––––––––––––––––––––– */ + /* NOTE + html is set to 62.5% so that all the REM measurements throughout Skeleton + are based on 10px sizing. So basically 1.5rem = 15px :) */ + html { + font-size: 62.5%; } + body { + font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */ + line-height: 1.6; + font-weight: 400; + font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; + color: #222; } + + + /* Typography + –––––––––––––––––––––––––––––––––––––––––––––––––– */ + h1, h2, h3, h4, h5, h6 { + margin-top: 0; + margin-bottom: 2rem; + font-weight: 300; } + h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;} + h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; } + h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; } + h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; } + h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; } + h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; } + + /* Larger than phablet */ + @media (min-width: 550px) { + h1 { font-size: 5.0rem; } + h2 { font-size: 4.2rem; } + h3 { font-size: 3.6rem; } + h4 { font-size: 3.0rem; } + h5 { font-size: 2.4rem; } + h6 { font-size: 1.5rem; } + } + + p { + margin-top: 0; } + + + /* Links + –––––––––––––––––––––––––––––––––––––––––––––––––– */ + a { + color: #1EAEDB; } + a:hover { + color: #0FA0CE; } + + + /* Buttons + –––––––––––––––––––––––––––––––––––––––––––––––––– */ + .button, + button, + input[type="submit"], + input[type="reset"], + input[type="button"] { + display: inline-block; + height: 38px; + padding: 0 30px; + color: #555; + text-align: center; + font-size: 11px; + font-weight: 600; + line-height: 38px; + letter-spacing: .1rem; + text-transform: uppercase; + text-decoration: none; + white-space: nowrap; + background-color: transparent; + border-radius: 4px; + border: 1px solid #bbb; + cursor: pointer; + box-sizing: border-box; } + .button:hover, + button:hover, + input[type="submit"]:hover, + input[type="reset"]:hover, + input[type="button"]:hover, + .button:focus, + button:focus, + input[type="submit"]:focus, + input[type="reset"]:focus, + input[type="button"]:focus { + color: #333; + border-color: #888; + outline: 0; } + .button.button-primary, + button.button-primary, + input[type="submit"].button-primary, + input[type="reset"].button-primary, + input[type="button"].button-primary { + color: #FFF; + background-color: #33C3F0; + border-color: #33C3F0; } + .button.button-primary:hover, + button.button-primary:hover, + input[type="submit"].button-primary:hover, + input[type="reset"].button-primary:hover, + input[type="button"].button-primary:hover, + .button.button-primary:focus, + button.button-primary:focus, + input[type="submit"].button-primary:focus, + input[type="reset"].button-primary:focus, + input[type="button"].button-primary:focus { + color: #FFF; + background-color: #1EAEDB; + border-color: #1EAEDB; } + + + /* Forms + –––––––––––––––––––––––––––––––––––––––––––––––––– */ + input[type="email"], + input[type="number"], + input[type="search"], + input[type="text"], + input[type="tel"], + input[type="url"], + input[type="password"], + textarea, + select { + height: 38px; + padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ + background-color: #fff; + border: 1px solid #D1D1D1; + border-radius: 4px; + box-shadow: none; + box-sizing: border-box; } + /* Removes awkward default styles on some inputs for iOS */ + input[type="email"], + input[type="number"], + input[type="search"], + input[type="text"], + input[type="tel"], + input[type="url"], + input[type="password"], + textarea { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; } + textarea { + min-height: 65px; + padding-top: 6px; + padding-bottom: 6px; } + input[type="email"]:focus, + input[type="number"]:focus, + input[type="search"]:focus, + input[type="text"]:focus, + input[type="tel"]:focus, + input[type="url"]:focus, + input[type="password"]:focus, + textarea:focus, + select:focus { + border: 1px solid #33C3F0; + outline: 0; } + label, + legend { + display: block; + margin-bottom: .5rem; + font-weight: 600; } + fieldset { + padding: 0; + border-width: 0; } + input[type="checkbox"], + input[type="radio"] { + display: inline; } + label > .label-body { + display: inline-block; + margin-left: .5rem; + font-weight: normal; } + + + /* Lists + –––––––––––––––––––––––––––––––––––––––––––––––––– */ + ul { + list-style: circle inside; } + ol { + list-style: decimal inside; } + ol, ul { + padding-left: 0; + margin-top: 0; } + ul ul, + ul ol, + ol ol, + ol ul { + margin: 1.5rem 0 1.5rem 3rem; + font-size: 90%; } + li { + margin-bottom: 1rem; } + + + /* Code + –––––––––––––––––––––––––––––––––––––––––––––––––– */ + code { + padding: .2rem .5rem; + margin: 0 .2rem; + font-size: 90%; + white-space: nowrap; + background: #F1F1F1; + border: 1px solid #E1E1E1; + border-radius: 4px; } + pre > code { + display: block; + padding: 1rem 1.5rem; + white-space: pre; } + + + /* Spacing + –––––––––––––––––––––––––––––––––––––––––––––––––– */ + button, + .button { + margin-bottom: 1rem; } + input, + textarea, + select, + fieldset { + margin-bottom: 1.5rem; } + pre, + blockquote, + dl, + figure, + table, + p, + ul, + ol, + form { + margin-bottom: 2.5rem; } + + + /* Utilities + –––––––––––––––––––––––––––––––––––––––––––––––––– */ + .u-full-width { + width: 100%; + box-sizing: border-box; } + .u-max-full-width { + max-width: 100%; + box-sizing: border-box; } + .u-pull-right { + float: right; } + .u-pull-left { + float: left; } + + + /* Misc + –––––––––––––––––––––––––––––––––––––––––––––––––– */ + hr { + margin-top: 3rem; + margin-bottom: 3.5rem; + border-width: 0; + border-top: 1px solid #E1E1E1; } + + + /* Clearing + –––––––––––––––––––––––––––––––––––––––––––––––––– */ + + /* Self Clearing Goodness */ + .container:after, + .row:after, + .u-cf { + content: ""; + display: table; + clear: both; } + + + /* Media Queries + –––––––––––––––––––––––––––––––––––––––––––––––––– */ + /* + Note: The best way to structure the use of media queries is to create the queries + near the relevant code. For example, if you wanted to change the styles for buttons + on small devices, paste the mobile query code up in the buttons section and style it + there. + */ + + + /* Larger than mobile */ + @media (min-width: 400px) {} + + /* Larger than phablet (also point when grid becomes active) */ + @media (min-width: 550px) {} + + /* Larger than tablet */ + @media (min-width: 750px) {} + + /* Larger than desktop */ + @media (min-width: 1000px) {} + + /* Larger than Desktop HD */ + @media (min-width: 1200px) {} + \ No newline at end of file diff --git a/src/static/js/app.js b/src/static/js/app.js new file mode 100644 index 0000000..cfdc3ea --- /dev/null +++ b/src/static/js/app.js @@ -0,0 +1,40 @@ +const vm = new Vue({ + el: '#orders', + data: { + orderItems: [], + orderItemIds: [], + totalPrice: 0 + }, + methods: { + updateTotalPrice: function() { + this.totalPrice = this.orderItems.reduce((acc, curr) => { + return acc + parseFloat(curr.price); + }, 0); + this.totalPrice = Number((this.totalPrice).toFixed(2)); + }, + addToOrder: function(event) { + const item_id = event.target.dataset.id; + const item_name = event.target.dataset.name; + const item_price = event.target.dataset.price; + const item = { + id: item_id, + name: item_name, + price: item_price + } + this.orderItems.push(item); + this.orderItemIds.push(item_id); + this.updateTotalPrice(); + }, + removeFromOrder: function(event) { + const item_id = event.target.dataset.id; + const i = this.orderItems.findIndex((element) => { + return element.id === item_id; + }); + this.orderItems.splice(i, 1); + const j = this.orderItemIds.indexOf(item_id); + if (j > -1) this.orderItemIds.splice(j, 1); + this.updateTotalPrice(); + } + }, + delimiters: ['[[',']]'] +}) diff --git a/src/templates/about_us.html b/src/templates/about_us.html new file mode 100644 index 0000000..c4ccec2 --- /dev/null +++ b/src/templates/about_us.html @@ -0,0 +1,30 @@ +{% extends 'base.html' %} + +{% block title%} +About Us +{% endblock title%} + +{% block content %} +
+
+

Tim Schoen

+ +
Software Developer
+

Passionate, dedicated, team player.

+

Software Devloper that utilizes a creative, entrepreneurial mindset with the ability for everyday learning. Excited about data analysis to help businesses grow.

+
+
+

Milo Anderson

+ +
Software Developer
+

I have a background in music, journalism, and social sciences, and a philosophical commitment to pragmatism. Coding gives me a way to make the world better, while experiencing the fun and camaeraderie that comes from solving tough problems as part of a team.

+
+
+

Dan Le

+ +
Software Developer
+

Python developer with a geology background, experience in leadership and quality assurance. Passionate and interested about Data Analytics.

+
+
+{% endblock content %} + diff --git a/src/templates/auth/login.html b/src/templates/auth/login.html new file mode 100644 index 0000000..15deb49 --- /dev/null +++ b/src/templates/auth/login.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} + +{% block title %} +Login +{% endblock title %} + + +{% block content %} +
+

Login:

+
+ {{ form.hidden_tag() }} + {{ form.email.label }}{{ form.email (size = 32)}} + {{ form.password.label }}{{ form.password (size = 32)}} + +
+
+{% endblock content %} \ No newline at end of file diff --git a/src/templates/auth/register.html b/src/templates/auth/register.html new file mode 100644 index 0000000..e2deb7f --- /dev/null +++ b/src/templates/auth/register.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} + +{% block title %} +Register +{% endblock title %} + +{% block content %} +
+

Customer Registration

+
+ {{ form.hidden_tag() }} + {{ form.name.label }}{{ form.name(size=36)}} + {{ form.phone.label }}{{ form.phone(size=36)}} + {{ form.email.label }}{{ form.email(size=36)}} + {{ form.password.label }}{{ form.password(size=36)}} + +
+
+{% endblock content %} \ No newline at end of file diff --git a/src/templates/base.html b/src/templates/base.html new file mode 100644 index 0000000..8d809f3 --- /dev/null +++ b/src/templates/base.html @@ -0,0 +1,58 @@ + + + + + + + + + + + + {% block title %} + {% endblock title %} + + +
+

Py Café

+ {% if g.user %} +
Welcome, {{ g.user['name'] }}
+ {% endif %} +
+ + +
+ {% for message in get_flashed_messages() %} +
{{message}}
+ {% endfor %} + {% block content %} + {% endblock content %} +
+
+ + \ No newline at end of file diff --git a/src/templates/exceptions/400.html b/src/templates/exceptions/400.html new file mode 100644 index 0000000..8c65f6b --- /dev/null +++ b/src/templates/exceptions/400.html @@ -0,0 +1,9 @@ +{% extends 'base.html' %} + +{% block title%} +400 - Bad Request +{% endblock title%} + +{% block content %} +

400 - The request could not be understood by the server

+{% endblock content %} \ No newline at end of file diff --git a/src/templates/exceptions/401.html b/src/templates/exceptions/401.html new file mode 100644 index 0000000..e7a6530 --- /dev/null +++ b/src/templates/exceptions/401.html @@ -0,0 +1,9 @@ +{% extends 'base.html' %} + +{% block title%} +401 - Unauthorized +{% endblock title%} + +{% block content %} +

401 - You aren't authorized to view this part of the site

+{% endblock content %} \ No newline at end of file diff --git a/src/templates/exceptions/404.html b/src/templates/exceptions/404.html new file mode 100644 index 0000000..9a9d618 --- /dev/null +++ b/src/templates/exceptions/404.html @@ -0,0 +1,9 @@ +{% extends 'base.html' %} + +{% block title%} +404 - Not Found +{% endblock title%} + +{% block content %} +

404 - The server could not find what you're looking for

+{% endblock content %} \ No newline at end of file diff --git a/src/templates/home.html b/src/templates/home.html new file mode 100644 index 0000000..3a547fb --- /dev/null +++ b/src/templates/home.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} + +{% block title%} +Home +{% endblock title%} + +{% block content %} +
+

Home Page

+
+{% endblock content %} + diff --git a/src/templates/items/add_items.html b/src/templates/items/add_items.html new file mode 100644 index 0000000..443f7ce --- /dev/null +++ b/src/templates/items/add_items.html @@ -0,0 +1,43 @@ +{% extends "base.html" %} + +{% block title %} +Add Items +{% endblock title %} + + +{% block content %} +
+

All Items

+ + + + + + + + + + + {% for item in items %} + + + + + + + {% endfor %} + +
ItemPriceCost of GoodInventory
{{ item.name }} {{ item.price }} {{ item.cog }} {{ item.inventory_count }}
+ +

Add Item

+
+ {{ form.hidden_tag() }} + {{ form.name.label }}{{ form.name(size = 32)}} + {{ form.cost.label }}{{ form.cost (size = 32)}} + {{ form.price.label }}{{ form.price (size = 32)}} + {{ form.count.label }}{{ form.count (size = 32)}} + +
+
+ +{% endblock content %} \ No newline at end of file diff --git a/src/templates/items/all_items.html b/src/templates/items/all_items.html new file mode 100644 index 0000000..7042ce5 --- /dev/null +++ b/src/templates/items/all_items.html @@ -0,0 +1,36 @@ +{% extends "base.html" %} + +{% block title %} +All Items +{% endblock title %} + + +{% block content %} +
+

All Items

+ + + + + + + + + + + {% for item in items %} + + + + + + + {% endfor %} + +
ItemPriceCost of GoodInventory
{{ item.name }} {{ item.price }} {{ item.cog }} {{ item.inventory_count }}
+ Add Items + Update Items + Delete Items +
+ +{% endblock content %} \ No newline at end of file diff --git a/src/templates/items/delete_items.html b/src/templates/items/delete_items.html new file mode 100644 index 0000000..082b697 --- /dev/null +++ b/src/templates/items/delete_items.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} + +{% block title %} +Delete Items +{% endblock title %} + + +{% block content %} +
+

All Items

+ + + + + + + + + + + {% for item in items %} + + + + + + + {% endfor %} + +
ItemPriceCost of GoodInventory
{{ item.name }} {{ item.price }} {{ item.cog }} {{ item.inventory_count }}
+ +

Delete Item

+
+ {{ form.hidden_tag() }} + {{ form.items.label }}{{ form.items()}} + +
+
+{% endblock content %} \ No newline at end of file diff --git a/src/templates/items/update_items.html b/src/templates/items/update_items.html new file mode 100644 index 0000000..bd01c53 --- /dev/null +++ b/src/templates/items/update_items.html @@ -0,0 +1,46 @@ +{% extends "base.html" %} + +{% block title %} +Update Items +{% endblock title %} + + +{% block content %} +
+

All Items

+ + + + + + + + + + + + {% for item in items %} + + + + + + + + {% endfor %} + +
ItemPriceCost of GoodInventoryActive?
{{ item.name }} {{ item.price }} {{ item.cog }} {{ item.inventory_count }} {{ item.active }}
+ +

Update Item

+
+ {{ form.hidden_tag() }} + {{ form.items.label }}{{ form.items()}} + {{ form.active.label }}{{ form.active()}} + {{ form.cost.label }}{{ form.cost (size = 32)}} + {{ form.price.label }}{{ form.price (size = 32)}} + {{ form.count.label }}{{ form.count (size = 32)}} + +
+
+ +{% endblock content %} \ No newline at end of file diff --git a/src/templates/manager/by_customer.html b/src/templates/manager/by_customer.html new file mode 100644 index 0000000..e2966a7 --- /dev/null +++ b/src/templates/manager/by_customer.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} + +{% block title %} +Reports +{% endblock title %} + + +{% block content %} +
+ {% if content != None %} + + + + + + + + + {% for item in content %} + + + + + {% endfor %} + +
NameNumber Sold
{{ item[0] }} {{ item[1] }}
+ {% endif %} +

Select User

+ + +
+ +{% endblock content %} \ No newline at end of file diff --git a/src/templates/manager/by_employee.html b/src/templates/manager/by_employee.html new file mode 100644 index 0000000..9068589 --- /dev/null +++ b/src/templates/manager/by_employee.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} + +{% block title %} +Reports +{% endblock title %} + +{% block content %} +
+ {% if content != None %} + + + + + + + + + {% for item in content %} + + + + + {% endfor %} + +
NameNumber Sold
{{ item[0] }} {{ item[1] }}
+ {% endif %} +

Select Employee

+ + +
+{% endblock content %} \ No newline at end of file diff --git a/src/templates/manager/by_item.html b/src/templates/manager/by_item.html new file mode 100644 index 0000000..1b45466 --- /dev/null +++ b/src/templates/manager/by_item.html @@ -0,0 +1,40 @@ +{% extends "base.html" %} + +{% block title %} +Report By Items +{% endblock title %} + + +{% block content %} +
+ {% if content != None %} + + + + + + + + + + + {% for item in content %} + + + + + + + {% endfor %} + +
NameItemNumber SoldActive?
{{ item[0] }}{{ item[2] }}{{ item[1] }}{{ item[3] }}
+ {% endif %} +

Select Item

+ +
+ +{% endblock content %} \ No newline at end of file diff --git a/src/templates/manager/by_time.html b/src/templates/manager/by_time.html new file mode 100644 index 0000000..ff188ea --- /dev/null +++ b/src/templates/manager/by_time.html @@ -0,0 +1,46 @@ +{% extends "base.html" %} + +{% block title %} +Report By Time +{% endblock title %} + + +{% block content %} +
+ {% if content != None %} + + + + + + + + + + + + {% for item in content %} + + + + + + + + {% endfor %} + +
UserDate/TimeQuantityItem
{{ item[0] }}{{ item[1] }}{{ item[2] }}{{ item[3] }}
+ {% endif %} + +

Select Item

+ +
+ +{% endblock content %} \ No newline at end of file diff --git a/src/templates/manager/report_index.html b/src/templates/manager/report_index.html new file mode 100644 index 0000000..f86ff2c --- /dev/null +++ b/src/templates/manager/report_index.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} + +{% block title %} +Reports +{% endblock title %} + + +{% block content %} +
+

Reports

+ +

Total number of each item sold, by customer + Go +

+ +

Total number of each item sold, by employee + Go +

+ +

Total customer sales, by item + Go +

+ +

Total sales, by time + Go +

+ +
+ +{% endblock content %} \ No newline at end of file diff --git a/src/templates/order.html b/src/templates/order.html new file mode 100644 index 0000000..a7203c1 --- /dev/null +++ b/src/templates/order.html @@ -0,0 +1,66 @@ +{% extends "base.html" %} +{% block title %} +Order +{% endblock title %} +{% block content %} +
+

Py Cafe Menu

+
+ + + + + + + + + {% for item in items %} + + + + + + {% endfor %} + +
ItemPrice
{{ item.name }} ${{ item.price }} +
+
+ {{ form.hidden_tag() }} + {{ form.item_ids }} + {% if g.user.type != 'customer' %} + {{ form.customer.label }}{{ form.customer() }} + {% endif %} + {% if g.user.type == 'manager' %} + {{ form.employee.label }}{{ form.employee() }} + {% endif %} + +
+ + + + + + + + + + + + + + + + + + +
ItemPrice
[[ item.name ]]$[[ item.price ]]-
Total Price:$[[ totalPrice ]]
+
+
+ + +{% endblock content %} \ No newline at end of file diff --git a/src/templates/reservations.html b/src/templates/reservations.html new file mode 100644 index 0000000..10fd4c3 --- /dev/null +++ b/src/templates/reservations.html @@ -0,0 +1,41 @@ +{% extends "base.html" %} + +{% block title %} +Make a Reservation +{% endblock title %} + + +{% block content %} +
+

Your Reservations

+ + + + + + + + + + {% for reservation in reservations %} + + + + + + {% endfor %} + +
DateTimeParty
{{ reservation.date }}{{ reservation.time }}{{ reservation.party }}
+ {% if form.errors%} + {{form.errors}} + {% endif %} +
+ {{ form.hidden_tag() }} + {{ form.date.label }}{{ form.date(size = 32)}} + {{ form.time.label }}{{ form.time (size = 32)}} + {{ form.party.label }}{{ form.party(size = 32)}} + +
+
+ +{% endblock content %} \ No newline at end of file diff --git a/src/templates/user/all_users.html b/src/templates/user/all_users.html new file mode 100644 index 0000000..e28a9fe --- /dev/null +++ b/src/templates/user/all_users.html @@ -0,0 +1,45 @@ +{% extends 'base.html' %} + +{% block title%} +Users +{% endblock title%} + +{% block content %} +
+

Manage Users

+ + + + + + + + + + + + {% for user in users %} + + + + + + + + {% endfor %} + +
NameEmailRolePhoneRate
{{ user.name }}{{ user.email }}{{ user.type }}{{ user.phone }}{{ user.pay_rate }}
+ + Create Customer + Create Employee + Create Manager + +

Delete User

+ +
+{% endblock content %} + diff --git a/src/templates/user/create_employee.html b/src/templates/user/create_employee.html new file mode 100644 index 0000000..55213b8 --- /dev/null +++ b/src/templates/user/create_employee.html @@ -0,0 +1,20 @@ +{% extends 'base.html' %} + +{% block title%} +Create Employee +{% endblock title%} + +{% block content %} +
+

Create Employee

+ + +
+{% endblock content %} \ No newline at end of file diff --git a/src/templates/user/create_manager.html b/src/templates/user/create_manager.html new file mode 100644 index 0000000..9bdbcd9 --- /dev/null +++ b/src/templates/user/create_manager.html @@ -0,0 +1,19 @@ +{% extends 'base.html' %} + +{% block title%} +Create Manager +{% endblock title%} + +{% block content %} +
+

Create Manager

+ + +
+{% endblock content %} \ No newline at end of file diff --git a/src/tests/conftest.py b/src/tests/conftest.py new file mode 100644 index 0000000..fd387c9 --- /dev/null +++ b/src/tests/conftest.py @@ -0,0 +1,180 @@ +from src.models import db as _db +from src.models import Manager, Customer, Employee, Order, OrderItems, Item, Reservation +from src import app as _app +import pytest +import os + + +@pytest.fixture() +def app(request): + """ Session-wide testable Flask app """ + _app.config.from_mapping( + TESTING=True, + SQLALCHEMY_DATABASE_URI=os.getenv('TEST_DATABASE_URL'), + SQLALCHEMY_TRACK_MODIFICATIONS=False, + WTF_CSRF_ENABLED=False + ) + ctx = _app.app_context() + ctx.push() + + def teardown(): + ctx.pop() + + request.addfinalizer(teardown) + return _app + + +@pytest.fixture() +def db(app, request): + """ Session-wide test DB """ + + _db.app = app + _db.create_all() + + def teardown(): + _db.drop_all() + request.addfinalizer(teardown) + return _db + + +@pytest.fixture() +def session(db, request): + """ Create new DB session for testing """ + connection = db.engine.connect() + transaction = connection.begin() + options = dict(bind=connection, binds={}) + session = db.create_scoped_session(options=options) + db.session = session + + def teardown(): + transaction.rollback() + connection.close() + session.remove() + request.addfinalizer(teardown) + + return session + + +@pytest.fixture() +def client(app, db, session): + """ Create test client """ + client = app.test_client() + ctx = app.app_context() + ctx.push() + yield client + ctx.pop() + + +@pytest.fixture() +def customer(session): + """ Create test user with customer role """ + customer = Customer( + name='Milo', + email='milo@test.com', + password='12345', + phone='123-456-7890' + ) + session.add(customer) + session.commit() + return customer + + +@pytest.fixture() +def auth_customer(client, customer): + client.post( + '/login', + data={'email': customer.email, 'password': '12345'}, + follow_redirects=True + ) + return client + + +@pytest.fixture() +def manager(session): + """ Create test user with manager role """ + manager = Manager( + name='Tim', + email='tim@test.com', + password='12345' + ) + session.add(manager) + session.commit() + return manager + + +@pytest.fixture() +def auth_manager(client, manager): + client.post( + '/login', + data={'email': manager.email, 'password': '12345'}, + follow_redirects=True + ) + return client + + +@pytest.fixture() +def employee(session): + """ Create test employee """ + employee = Employee( + name='Dan', + email='dan@test.com', + password='54321', + pay_rate=10.50 + ) + session.add(employee) + session.commit() + return employee + +@pytest.fixture() +def reservation(session): + """ create test reservation """ + reservation = Reservation( + date = '3/24/2019', + time = '11:00 PM', + party = 3 + ) + return reservation + +@pytest.fixture() +def auth_employee(client, employee): + client.post( + '/login', + data={'email': employee.email, 'password': '54321'}, + follow_redirects=True + ) + return client + + +@pytest.fixture() +def items(session): + """ Create test items """ + item2 = Item( + name='Cheeseburger', + price=8.50, + cog=5.00, + inventory_count=22 + ) + session.add(item2) + item1 = Item( + name='Biscuits and Gravy', + price=9.50, + cog=6.00, + inventory_count=12 + ) + session.add(item1) + session.commit() + return [item1, item2] + + +@pytest.fixture() +def order(session, customer, employee, items): + """ Create test order """ + order = Order( + customer=customer, + employee=employee, + items=items, + + ) + session.add(order) + session.commit() + return order diff --git a/src/tests/items_routes_test.py b/src/tests/items_routes_test.py new file mode 100644 index 0000000..d1d0bf0 --- /dev/null +++ b/src/tests/items_routes_test.py @@ -0,0 +1,81 @@ +from src.models import Order, Item + + +class TestItems(): + + def test_item_all_get(self, auth_manager, items): + """ all_items landing page """ + res = auth_manager.get('/item') + assert res.status_code == 200 + assert b'All Items' in res.data + assert b'Cheeseburger' in res.data + assert b'Biscuits and Gravy' in res.data + + def test_item_add_get(self, client, auth_manager): + """ + test add item route + """ + rv = client.get('/item/add') + assert rv.status_code == 200 + assert b'Add Items' in rv.data + + def test_item_add_post(self, auth_manager): + """ testing item creation """ + res = auth_manager.post( + '/item/add', + data={ + 'name': 'BLT', + 'cost': 4.50, + 'price': 7.95, + 'count': 12 + } + ) + items = Item.query.all() + assert len(items) == 1 + assert items[0].name == 'BLT' + assert items[0].cog == 4.5 + + def test_item_delete_get(self, client, auth_manager): + """ test delete item route """ + rv = client.get('/item/delete') + assert rv.status_code == 200 + assert b'Delete Items' in rv.data + + def test_item_delete_post(self, auth_manager, items): + """ testing item deletion """ + res = auth_manager.post( + '/item/delete', + data={'items': 1}, + follow_redirects=True + ) + items = Item.query.all() + assert len(items) == 2 + assert items[0].active is True + assert items[1].active is False + + def test_item_update_get(self, auth_manager, items): + """ test item update route """ + res = auth_manager.get('/item/update') + assert res.status_code == 200 + assert b'Update Items' in res.data + + def test_item_update_post(self, auth_manager, items): + """ test item updating """ + res = auth_manager.post( + '/item/update', + data={ + 'items': 1, + 'cost': 0.50, + 'price': 20.00, + 'count': 42 + }, + follow_redirects=True + ) + saved_items = Item.query.all() + assert len(saved_items) == 2 + assert saved_items[1].cog == 0.50 + assert saved_items[0].cog != 0.50 + assert saved_items[1].price == 20.00 + assert saved_items[0].price != 20.00 + assert saved_items[1].inventory_count == 42 + assert saved_items[0].inventory_count != 42 diff --git a/src/tests/models_test.py b/src/tests/models_test.py new file mode 100644 index 0000000..0c1d4d7 --- /dev/null +++ b/src/tests/models_test.py @@ -0,0 +1,87 @@ + +from src.models import Manager, Customer, Employee, Order, OrderItems, Item, Reservation + +from src.models_reports import CustomerOrders + + +class TestUserModels(): + """ Tests for Customer and Manager models """ + + def test_create_customer(self, customer): + assert customer + + def test_customer_data(self, customer): + assert customer.name == 'Milo' + assert customer.email == 'milo@test.com' + assert customer.password + assert customer.password != '12345' + assert customer.phone == '123-456-7890' + + def test_create_manager(self, manager): + assert manager + + def test_manager_data(self, manager): + assert manager.name == 'Tim' + assert manager.email == 'tim@test.com' + assert manager.password + assert manager.password != '12345' + + def test_create_employee(self, employee): + assert employee + + def test_employee_data(self, employee): + assert employee.name == 'Dan' + assert employee.email == 'dan@test.com' + assert employee.password + assert employee.password != '54321' + + def test_user_query(self, customer, manager, employee): + c = Customer.query.all() + m = Manager.query.all() + e = Employee.query.all() + assert c[0].name == 'Milo' + assert m[0].name == 'Tim' + assert e[0].name == 'Dan' + + +class TestItems(): + """ Tests for Item model """ + + def test_create_item(self, items): + assert items + + def test_item_data(self, items): + assert items[1].name == 'Cheeseburger' + assert items[1].price == 8.50 + assert items[1].cog == 5.00 + assert items[1].inventory_count == 22 + + def test_item_query(self, items): + i = Item.query.all() + assert i[0].name == 'Cheeseburger' + assert i[1].name == 'Biscuits and Gravy' + + +class TestOrders(): + """ Tests for Order model """ + + def test_create_order(self, customer, employee, items, order): + assert order + + def test_order_data(self, customer, employee, items, order): + assert order.customer.name == 'Milo' + assert order.employee.name == 'Dan' + assert order.items[0].name == 'Biscuits and Gravy' + assert order.items[1].name == 'Cheeseburger' + + +class TestReservations(): + """ Tests for Reservation model """ + + def test_create_reservation(self, reservation): + assert reservation + + def test_reservation_data(self, reservation): + assert reservation.date == '3/24/2019' + assert reservation.time == '11:00 PM' + assert reservation.party == 3 diff --git a/src/tests/orders_routes_test.py b/src/tests/orders_routes_test.py new file mode 100644 index 0000000..d2e8a29 --- /dev/null +++ b/src/tests/orders_routes_test.py @@ -0,0 +1,26 @@ +from src.models import Order, Item + + +class TestOrders(): + + def test_order_route_get(self, auth_customer): + """ test order route """ + res = auth_customer.get('/order') + assert res.status_code == 200 + assert b'Order' in res.data + + def test_order_route_post(self, auth_customer, items): + """ test order creation """ + res = auth_customer.post( + '/order', + data={'item_ids': '1,1,2,2,2', } + ) + orders = Order.query.all() + assert len(orders) == 1 + assert orders[0].id == 1 + assert orders[0].cust_id == 1 + items = Item.query.all() + assert items[0].name == 'Cheeseburger' + assert items[0].inventory_count == 20 + assert items[1].name == 'Biscuits and Gravy' + assert items[1].inventory_count == 9 diff --git a/src/tests/pytest.ini b/src/tests/pytest.ini new file mode 100644 index 0000000..b0e5a94 --- /dev/null +++ b/src/tests/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +filterwarnings = + ignore::DeprecationWarning \ No newline at end of file diff --git a/src/tests/reports_test.py b/src/tests/reports_test.py new file mode 100644 index 0000000..f3a144a --- /dev/null +++ b/src/tests/reports_test.py @@ -0,0 +1,28 @@ +from src.models import Manager, Customer, Employee, Order, OrderItems, Item +from src.models_reports import CustomerOrders + + +class TestReportModels(): + """ Tests for Reports """ + + def test_create_model(self): + assert CustomerOrders + + def test_created_model(self): + assert CustomerOrders(0) + + def test_sales_report(self, customer, items, order): + res = CustomerOrders(0) + assert res.item_totals(1)[0][0] == 'Biscuits and Gravy' + assert res.item_totals(1)[0][1] == 1 + assert res.item_totals(1)[1][0] == 'Cheeseburger' + assert res.item_totals(1)[1][1] == 1 + + def test_customer_totals_report(self, customer, items, order): + res = CustomerOrders(0) + # testing for Biscuits and Gravy + assert res.customer_totals(1)[0][0] == 'Milo' + assert res.customer_totals(1)[0][1] == 1 + # testing for Cheeseburger + assert res.customer_totals(2)[0][0] == 'Milo' + assert res.customer_totals(2)[0][1] == 1 diff --git a/src/tests/routes_test.py b/src/tests/routes_test.py new file mode 100644 index 0000000..5e884b5 --- /dev/null +++ b/src/tests/routes_test.py @@ -0,0 +1,57 @@ + +class TestRoutes(): + + def test_home_route(self, client): + """ + test home route + """ + rv = client.get('/') + assert rv.status_code == 200 + assert b'Home' in rv.data + + def test_about_us_route(self, client): + """ test about_us route """ + res = client.get('/about') + assert res.status_code == 200 + assert b'About Us' in res.data + assert b'Tim Schoen' in res.data + assert b'Milo Anderson' in res.data + assert b'Dan Le' in res.data + + def test_login_route(self, client): + """ + test login route + """ + rv = client.get('/login') + assert rv.status_code == 200 + assert b'Login' in rv.data + + def test_login_post(self, client): + """ testing login """ + res = client.post( + '/login', + data={ + 'email': 'milo@test.com', + 'password': '12345' + }, + follow_redirects=True + ) + assert res.status_code == 200 + assert b'Login' in res.data + + def test_register_route(self, client): + """ + test register route + """ + rv = client.get('/register') + assert rv.status_code == 200 + assert b'Register' in rv.data + + def test_reservation(self, client, auth_manager): + """ + test reservation route + """ + rv = client.get('/reservation') + assert rv.status_code == 200 + assert b'Make a Reservation' in rv.data + \ No newline at end of file diff --git a/src/tests/users_routes_test.py b/src/tests/users_routes_test.py new file mode 100644 index 0000000..b90aefd --- /dev/null +++ b/src/tests/users_routes_test.py @@ -0,0 +1,36 @@ +from src.models import User, Customer, Employee, Manager + + +class TestUsers(): + + def test_users_route_get(self, client, auth_manager): + """ test all users route """ + rv = client.get('/all_users') + assert rv.status_code == 200 + assert b'Users' in rv.data + + def test_customer_registration_get(self, auth_manager): + """ registration page load """ + res = auth_manager.get('/register') + assert res.status_code == 200 + assert b'Register' in res.data + + def test_customer_registration_post(self, auth_manager): + """ test customer creation """ + res = auth_manager.post( + '/register', + data={ + 'name': 'Chris', + 'phone': '1234567890', + 'email': 'chris@test.com', + 'password': '12345' + }, + follow_redirects=True + ) + customers = Customer.query.all() + assert res.status_code == 200 + assert len(customers) == 1 + assert customers[0].name == 'Chris' + assert customers[0].phone == '1234567890' + assert customers[0].email == 'chris@test.com' + assert customers[0].password != '12345' diff --git a/wsgi.py b/wsgi.py new file mode 100644 index 0000000..27803c4 --- /dev/null +++ b/wsgi.py @@ -0,0 +1,4 @@ +from src import app + +if __name__ == '__main__': + app.run()