├── .gitignore ├── NextSteps.md ├── Pipfile ├── Pipfile.lock ├── Procfile ├── README.md ├── flasklanding.sublime-project ├── flasklanding.sublime-workspace ├── landing ├── __init__.py ├── db.sqlite3 ├── home │ ├── __init__.py │ ├── forms.py │ ├── models.py │ └── views.py ├── jobs │ ├── __init__.py │ └── views.py ├── profiles │ ├── __init__.py │ └── views.py ├── templates │ ├── base.html │ ├── home.html │ ├── items │ │ ├── delete.html │ │ ├── detail.html │ │ ├── form.html │ │ └── list.html │ ├── navbar.html │ ├── profiles_detail.html │ ├── profiles_list.html │ └── success.html └── views.py ├── migrations ├── README ├── alembic.ini ├── env.py ├── script.py.mako └── versions │ ├── 0bc39a029870_.py │ └── d64db672b5ce_.py └── wsgi.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | parse_git_log.py 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | # db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # SageMath parsed files 85 | *.sage.py 86 | 87 | # Environments 88 | .env 89 | .venv 90 | env/ 91 | venv/ 92 | ENV/ 93 | env.bak/ 94 | venv.bak/ 95 | 96 | # Spyder project settings 97 | .spyderproject 98 | .spyproject 99 | 100 | # Rope project settings 101 | .ropeproject 102 | 103 | # mkdocs documentation 104 | /site 105 | 106 | # mypy 107 | .mypy_cache/ 108 | 109 | -------------------------------------------------------------------------------- /NextSteps.md: -------------------------------------------------------------------------------- 1 | # Next Steps 2 | 3 | 4 | 1. Add a production database on heroku (postgresql) as well as local (use the same) 5 | 6 | 2. Add static files via AWS S3 cfe.sh/courses/ dive into aws 7 | 8 | 3. Auto redirect to HTTPs 9 | 10 | 4. Add users and user sessions 11 | 12 | 5. Celery for task management and task queues 13 | 14 | 6. Rest API that integrates with React.js -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | flask = "==1.0.2" 10 | sqlalchemy = "*" 11 | flask-sqlalchemy = "*" 12 | flask-migrate = "*" 13 | gunicorn = "*" 14 | flask-wtf = "*" 15 | 16 | [requires] 17 | python_version = "3.6" 18 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "46d7ad68313acbcaf704e4a1c2bf186243c430bd22372467f20537ee0e4e7b55" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.6" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "alembic": { 20 | "hashes": [ 21 | "sha256:505d41e01dc0c9e6d85c116d0d35dbb0a833dcb490bf483b75abeb06648864e8" 22 | ], 23 | "version": "==1.0.8" 24 | }, 25 | "click": { 26 | "hashes": [ 27 | "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", 28 | "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" 29 | ], 30 | "version": "==7.0" 31 | }, 32 | "flask": { 33 | "hashes": [ 34 | "sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48", 35 | "sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05" 36 | ], 37 | "index": "pypi", 38 | "version": "==1.0.2" 39 | }, 40 | "flask-migrate": { 41 | "hashes": [ 42 | "sha256:a361578cb829681f860e4de5ed2c48886264512f0c16144e404c36ddc95ab49c", 43 | "sha256:c24d105c5d6cc670de20f8cbfb909e04f4e04b8784d0df070005944de1f21549" 44 | ], 45 | "index": "pypi", 46 | "version": "==2.4.0" 47 | }, 48 | "flask-sqlalchemy": { 49 | "hashes": [ 50 | "sha256:3bc0fac969dd8c0ace01b32060f0c729565293302f0c4269beed154b46bec50b", 51 | "sha256:5971b9852b5888655f11db634e87725a9031e170f37c0ce7851cf83497f56e53" 52 | ], 53 | "index": "pypi", 54 | "version": "==2.3.2" 55 | }, 56 | "flask-wtf": { 57 | "hashes": [ 58 | "sha256:5d14d55cfd35f613d99ee7cba0fc3fbbe63ba02f544d349158c14ca15561cc36", 59 | "sha256:d9a9e366b32dcbb98ef17228e76be15702cd2600675668bca23f63a7947fd5ac" 60 | ], 61 | "index": "pypi", 62 | "version": "==0.14.2" 63 | }, 64 | "gunicorn": { 65 | "hashes": [ 66 | "sha256:aa8e0b40b4157b36a5df5e599f45c9c76d6af43845ba3b3b0efe2c70473c2471", 67 | "sha256:fa2662097c66f920f53f70621c6c58ca4a3c4d3434205e608e121b5b3b71f4f3" 68 | ], 69 | "index": "pypi", 70 | "version": "==19.9.0" 71 | }, 72 | "itsdangerous": { 73 | "hashes": [ 74 | "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", 75 | "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" 76 | ], 77 | "version": "==1.1.0" 78 | }, 79 | "jinja2": { 80 | "hashes": [ 81 | "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", 82 | "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" 83 | ], 84 | "version": "==2.10" 85 | }, 86 | "mako": { 87 | "hashes": [ 88 | "sha256:4e02fde57bd4abb5ec400181e4c314f56ac3e49ba4fb8b0d50bba18cb27d25ae" 89 | ], 90 | "version": "==1.0.7" 91 | }, 92 | "markupsafe": { 93 | "hashes": [ 94 | "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", 95 | "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", 96 | "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", 97 | "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", 98 | "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", 99 | "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", 100 | "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", 101 | "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", 102 | "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", 103 | "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", 104 | "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", 105 | "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", 106 | "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", 107 | "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", 108 | "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", 109 | "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", 110 | "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", 111 | "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", 112 | "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", 113 | "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", 114 | "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", 115 | "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", 116 | "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", 117 | "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", 118 | "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", 119 | "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", 120 | "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", 121 | "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" 122 | ], 123 | "version": "==1.1.1" 124 | }, 125 | "python-dateutil": { 126 | "hashes": [ 127 | "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", 128 | "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" 129 | ], 130 | "version": "==2.8.0" 131 | }, 132 | "python-editor": { 133 | "hashes": [ 134 | "sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d", 135 | "sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b", 136 | "sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8" 137 | ], 138 | "version": "==1.0.4" 139 | }, 140 | "six": { 141 | "hashes": [ 142 | "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", 143 | "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" 144 | ], 145 | "version": "==1.12.0" 146 | }, 147 | "sqlalchemy": { 148 | "hashes": [ 149 | "sha256:781fb7b9d194ed3fc596b8f0dd4623ff160e3e825dd8c15472376a438c19598b" 150 | ], 151 | "index": "pypi", 152 | "version": "==1.3.1" 153 | }, 154 | "werkzeug": { 155 | "hashes": [ 156 | "sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c", 157 | "sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b" 158 | ], 159 | "version": "==0.14.1" 160 | }, 161 | "wtforms": { 162 | "hashes": [ 163 | "sha256:0cdbac3e7f6878086c334aa25dc5a33869a3954e9d1e015130d65a69309b3b61", 164 | "sha256:e3ee092c827582c50877cdbd49e9ce6d2c5c1f6561f849b3b068c1b8029626f1" 165 | ], 166 | "version": "==2.2.1" 167 | } 168 | }, 169 | "develop": {} 170 | } 171 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | # procfile 2 | web: gunicorn landing:app --workers 3 -t 60 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Flask Landing Project](https://static.codingforentrepreneurs.com/media/projects/flask-landing/images/share/Flask_Landing.jpg)](https://www.codingforentrepreneurs.com/projects/flask-landing/) 2 | 3 | # Flask Landing 4 | Learn to build a landing page with the very popular Python Flask Microframework. 5 | 6 | 7 | #### Lesson Code 8 | This is what the code looks like at the end of the lesson. It's made for your reference so you can see a history of changes. 9 | 10 | 1. Welcome & Requirements _no code_ 11 | 12 | 2. Walkthrough _no code_ 13 | 14 | 3. Create a Virtual Environment _no code_ 15 | 16 | 4. Sublime Text & Reactivate Virtual Environment 17 | 18 | [5 - Hello World with Flask](../../tree/e0b1bae98738c6ab93ccf913eedacbf62dbc0b23/) 19 | 20 | [6 - WSGI for Local Dev](../../tree/00fb9c3c51033e54d6e732dd9db65ef181a37acf/) 21 | 22 | [7 - URL Routing](../../tree/673af3942b11d1d44605806c3fac26a00d511b4f/) 23 | 24 | [8 - Flask App as Package](../../tree/18fbffadea20e83513da3847f6b23c48c8f44a1f/) 25 | 26 | [9 - Modules Driving One Purpose](../../tree/bf9019d97cb55e73b59d34a9be251e7fbe4ca0aa/) 27 | 28 | [10 - Render HTML Template](../../tree/c6737068a09f5358ef72d086d0b6f761c70b3aeb/) 29 | 30 | [11 - Render Template Variables](../../tree/fb1b4cdaf6c7f08b4b7f75a15eda2c43448e5d65/) 31 | 32 | [12 - Keep Logic in Views not Templates](../../tree/73db8b26456ecc8e871032044201a60956107772/) 33 | 34 | [13 - Jinja Templates & Base HTML](../../tree/eafb22b8625bc3ca45d6fdf7d2f7f1243d66671a/) 35 | 36 | [14 - Render JSON](../../tree/408e5d924f56dcfb905f7ccfe61ff71bc63e8f96/) 37 | 38 | [15 - Landing Page Form](../../tree/d88a0cce5df34b566f1880db658f13386c326856/) 39 | 40 | [16 - POST Method and Request Data](../../tree/1989f28a78360583ef3b640c0dd867cff29fbff6/) 41 | 42 | [17 - Validation & CSRF](../../tree/3ecb4a0d2758c540f0b6b098067059adf0d0bb6e/) 43 | 44 | [18 - Python eval](../../tree/d7b13b28124af76148fa9342ef6114b68abec01e/) 45 | 46 | [19 - Adding CSRF Protection to Flask](../../tree/ff80d1549787d2a1f13661941e71ee15b46eb3c9/) 47 | 48 | [20 - A Form with Flask WTF](../../tree/7ee56db3e7633048cf5d0aa2520435e3a2d03ba1/) 49 | 50 | [21 - Basic Form Validation with wtforms](../../tree/f5bf3c1fbbf6a05897c309efe3e909d83307f1de/) 51 | 52 | [22 - Email & Custom Validation with wtforms](../../tree/028b783848ed6e8f108557e2162874dedc90d33a/) 53 | 54 | [23 - Render the Flask WTF Form in Template](../../tree/8de49fcad740e8c064e2d03b16b1c09ea5e3c672/) 55 | 56 | [24 - Input Class and Placeholder in Flask WTF Forms](../../tree/7b726a6020a6afd9665a3f664a1e9feca35de57c/) 57 | 58 | [25 - Setup a Database.](../../tree/2e3407349bf1c2856f5185fff4f4066bf12e3e3f/) 59 | 60 | [26 - Your First SQLAlchemy Model](../../tree/22aef7ac1b85c8b91ae6302231a428c92bf43cec/) 61 | 62 | [27 - Save to Database](../../tree/80f45ba6f6b91e63de0e651791dd56aebc65ac0d/) 63 | 64 | [28 - Track Modification Error](../../tree/f85ab67a848a1e933e90dbd6f4f1adad5dad734b/) 65 | 66 | 29 - Database Actions in Flask Shell _no code_ 67 | 68 | [30 - Convenience Methods for CRUD](../../tree/40bef5eed2c5f26597ab8d8cfc27ae222bf06ab1/) 69 | 70 | [31 - Saving Data to a Model via a Form](../../tree/67ff9521c1fa1b59f8ee24f187edf0eb93fca2f1/) 71 | 72 | [32 - Validation Form against Database with Model Lookup](../../tree/4b18028a0400202eda2348e1813e271ced21b0cd/) 73 | 74 | [33 - Database Object to Detail View](../../tree/66f835df39c0b1ce1c686ad4870262c7dc24f457/) 75 | 76 | [34 - First or 404](../../tree/6bd31ec341f0a393bd99b38b301ed50d459a584b/) 77 | 78 | [35 - Update View and Prefill Form Data](../../tree/be5f10208f1c570c76dedda7029c3069fc7b5e52/) 79 | 80 | [36 - Save Updates in Update View](../../tree/a4c49e8609a32912a1023228030fd7e6613c1ca2/) 81 | 82 | [37 - Exclude Items from SQLAlchemy Query](../../tree/3089738bc2bb51654544c18c4c564fba1d774999/) 83 | 84 | [38 - Delete & Confirm](../../tree/cfca3cae6dee29c28ea215a96a48cc0ca805befb/) 85 | 86 | [39 - List View and Redirects](../../tree/4facdfad3716f9cdb587fe3b0c98ea765c162b7d/) 87 | 88 | [40 - Navbar with Include](../../tree/a599b78bbec3f92e342c92fe77fc9c63d4c603ef/) 89 | 90 | [41 - Dynamic URL Paths with url_for](../../tree/670fd920a08d91c620d83715a33e45549482cb91/) 91 | 92 | [42 - url_for with Arguments](../../tree/dde3a97252e2cf9a9da421d3a445271f27d7de15/) 93 | 94 | [43 - Signals](../../tree/83c9e403f648f6d9802efbef098c93aa09e15ffa/) 95 | 96 | [44 - Change Models with Flask Migrate](../../tree/1855bfed2d305b5a04d22cf3e6fb690da75a14f4/) 97 | 98 | [45 - Fresh Migrate and DB](../../tree/e52416b57d029d08cc093358a9278e39e6f99860/) 99 | 100 | [46 - Migrate Again](../../tree/da69781001544f584e5f154776280a20d35596f6/) 101 | 102 | [47 - Password Protect a View](../../tree/fa5dd457296e6867c11891bd9d27f8bcb214ff02) 103 | 104 | [48 - Prep Landing Page for Production](../../tree/4bbadfb22c79d715fd392eec1243068b709a029b) 105 | 106 | [49 - Gunicorn Server](../../tree/02172e4ca126acf87e3454d5260ce9df8c125a3f) 107 | 108 | [50 - Heroku and Live App](../../tree/0abc3be684d974e7a94e07944ae57cbee0d64f20) 109 | 110 | [51 - Making Changes for Production](../../tree/4e86345148d84b0ea4513a67be2575bc7d85325f) 111 | 112 | 52 - Custom Domain 113 | ```console 114 | heroku domains:add 115 | ``` 116 | 117 | 53 - Adding HTTPs using [ACM](https://devcenter.heroku.com/articles/automated-certificate-management) 118 | ```console 119 | heroku ps:resize web=hobby 120 | 121 | heroku certs:auto:enable 122 | ``` 123 | 124 | [54 - Suggested Next Steps](../../tree/45d1da542aeea47e8554df306747d844a2a42279) 125 | -------------------------------------------------------------------------------- /flasklanding.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "path": "." 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /flasklanding.sublime-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "auto_complete": 3 | { 4 | "selected_items": 5 | [ 6 | [ 7 | "pr", 8 | "presigned_data" 9 | ], 10 | [ 11 | "post", 12 | "post_data" 13 | ], 14 | [ 15 | "ra", 16 | "raw_filename" 17 | ], 18 | [ 19 | "de", 20 | "detail" 21 | ], 22 | [ 23 | "file", 24 | "filetype" 25 | ], 26 | [ 27 | "fi", 28 | "filetype" 29 | ], 30 | [ 31 | "csr", 32 | "csrf_exempt" 33 | ], 34 | [ 35 | "BA", 36 | "BASE_DIR" 37 | ], 38 | [ 39 | "get_", 40 | "get_file_ext" 41 | ], 42 | [ 43 | "for", 44 | "force_download" 45 | ], 46 | [ 47 | "s3", 48 | "s3_session" 49 | ], 50 | [ 51 | "se", 52 | "session" 53 | ] 54 | ] 55 | }, 56 | "buffers": 57 | [ 58 | { 59 | "contents": "# Flask Landing\n\n\n### 1. Create virtual environment\nThis keeps all requirements (aka code versions) for this project consistent so your other/new python projects don't break.\n\nThree options:\n\n1. Python3 Built -in Virtual Environment\n```\npython3 -m venv .\n```\n\n2. `virtualenv` package\n```\nvirtualenv -p python3.6 .\n```\n\n3. `pipenv` package (our choice)\n```\npipenv shell --python 3.6 .\n```\n\n\n\n\n\n\n\n### 2. Hello World with Flask.\nAs per the [docs](http://flask.pocoo.org/), it looks like this:\n\n```python\nfrom flask import Flask\napp = Flask(__name__)\n\n@app.route(\"/\")\ndef hello():\n return \"Hello World!\"\n```\n\n\n```python\nfrom flask import Flask\nlanding_app = Flask(__name__)\n\n@landing_app.route(\"/\")\ndef hello():\n return \"Hello World!\"\n```\n\n\n\n", 60 | "settings": 61 | { 62 | "buffer_size": 744, 63 | "line_ending": "Unix", 64 | "name": "# Flask Landing" 65 | } 66 | }, 67 | { 68 | "settings": 69 | { 70 | "buffer_size": 0, 71 | "line_ending": "Unix" 72 | } 73 | } 74 | ], 75 | "build_system": "", 76 | "build_system_choices": 77 | [ 78 | ], 79 | "build_varint": "", 80 | "command_palette": 81 | { 82 | "height": 0.0, 83 | "last_filter": "", 84 | "selected_items": 85 | [ 86 | ], 87 | "width": 0.0 88 | }, 89 | "console": 90 | { 91 | "height": 0.0, 92 | "history": 93 | [ 94 | ] 95 | }, 96 | "distraction_free": 97 | { 98 | "menu_visible": true, 99 | "show_minimap": false, 100 | "show_open_files": false, 101 | "show_tabs": false, 102 | "side_bar_visible": false, 103 | "status_bar_visible": false 104 | }, 105 | "expanded_folders": 106 | [ 107 | "/Users/cfe/Dev/flasklanding" 108 | ], 109 | "file_history": 110 | [ 111 | "/Users/cfe/Dev/cfedirect/README.md", 112 | "/Users/cfe/Dev/cfedirect/src/cfehome/settings/base.py", 113 | "/Users/cfe/Dev/cfedirect/src/cfehome/settings/local.py", 114 | "/Users/cfe/Dev/cfedirect/src/cfehome/settings/production.py", 115 | "/Users/cfe/Dev/cfedirect/src/files/serializers.py", 116 | "/Users/cfe/Dev/cfedirect/.gitignore", 117 | "/Users/cfe/Dev/cfedirect/src/files/models.py", 118 | "/Users/cfe/Dev/cfedirect/src/files/views.py", 119 | "/Users/cfe/Dev/cfedirect/src/cfehome/urls.py", 120 | "/Users/cfe/Dev/cfedirect/src/templates/base.html", 121 | "/Users/cfe/Dev/cfedirect/src/templates/upload.html", 122 | "/Users/cfe/Dev/cfedirect/src/cfehome/aws/utils.py", 123 | "/Users/cfe/Dev/cfedirect/src/files/admin.py", 124 | "/Users/cfe/Dev/cfedirect/src/cfehome/aws/conf.py", 125 | "/Users/cfe/Dev/cfedirect/src/cfehome/aws/test_aws_conf.py", 126 | "/Users/cfe/Dev/cfedirect/src/cfehome/aws/test_aws_config.py", 127 | "/Users/cfe/Dev/cfehome/src/cfehome/aws/storages.py", 128 | "/Users/cfe/Dev/cfehome/src/cfehome/settings/base.py", 129 | "/Users/cfe/Dev/cfehome/src/cfehome/aws/__init__.py", 130 | "/Volumes/CFE_2018/CFE Projects/Current/Coding with macOS/Todo/To do.md", 131 | "/Volumes/CFE_2018/CFE Projects/Current/coding with macOS/To do.md" 132 | ], 133 | "find": 134 | { 135 | "height": 64.0 136 | }, 137 | "find_in_files": 138 | { 139 | "height": 0.0, 140 | "where_history": 141 | [ 142 | ] 143 | }, 144 | "find_state": 145 | { 146 | "case_sensitive": false, 147 | "find_history": 148 | [ 149 | ], 150 | "highlight": true, 151 | "in_selection": false, 152 | "preserve_case": false, 153 | "regex": false, 154 | "replace_history": 155 | [ 156 | ], 157 | "reverse": false, 158 | "show_context": true, 159 | "use_buffer2": true, 160 | "whole_word": false, 161 | "wrap": true 162 | }, 163 | "groups": 164 | [ 165 | { 166 | "selected": 1, 167 | "sheets": 168 | [ 169 | { 170 | "buffer": 0, 171 | "semi_transient": false, 172 | "settings": 173 | { 174 | "buffer_size": 744, 175 | "regions": 176 | { 177 | }, 178 | "selection": 179 | [ 180 | [ 181 | 547, 182 | 547 183 | ] 184 | ], 185 | "settings": 186 | { 187 | "auto_name": "# Flask Landing", 188 | "syntax": "Packages/Text/Plain text.tmLanguage" 189 | }, 190 | "translation.x": 0.0, 191 | "translation.y": 1260.0, 192 | "zoom_level": 1.0 193 | }, 194 | "stack_index": 1, 195 | "type": "text" 196 | }, 197 | { 198 | "buffer": 1, 199 | "semi_transient": false, 200 | "settings": 201 | { 202 | "buffer_size": 0, 203 | "regions": 204 | { 205 | }, 206 | "selection": 207 | [ 208 | [ 209 | 0, 210 | 0 211 | ] 212 | ], 213 | "settings": 214 | { 215 | "syntax": "Packages/Text/Plain text.tmLanguage" 216 | }, 217 | "translation.x": 0.0, 218 | "translation.y": 0.0, 219 | "zoom_level": 1.0 220 | }, 221 | "stack_index": 0, 222 | "type": "text" 223 | } 224 | ] 225 | } 226 | ], 227 | "incremental_find": 228 | { 229 | "height": 52.0 230 | }, 231 | "input": 232 | { 233 | "height": 64.0 234 | }, 235 | "layout": 236 | { 237 | "cells": 238 | [ 239 | [ 240 | 0, 241 | 0, 242 | 1, 243 | 1 244 | ] 245 | ], 246 | "cols": 247 | [ 248 | 0.0, 249 | 1.0 250 | ], 251 | "rows": 252 | [ 253 | 0.0, 254 | 1.0 255 | ] 256 | }, 257 | "menu_visible": true, 258 | "output.find_results": 259 | { 260 | "height": 0.0 261 | }, 262 | "pinned_build_system": "", 263 | "project": "flasklanding.sublime-project", 264 | "replace": 265 | { 266 | "height": 100.0 267 | }, 268 | "save_all_on_build": true, 269 | "select_file": 270 | { 271 | "height": 0.0, 272 | "last_filter": "", 273 | "selected_items": 274 | [ 275 | ], 276 | "width": 0.0 277 | }, 278 | "select_project": 279 | { 280 | "height": 0.0, 281 | "last_filter": "", 282 | "selected_items": 283 | [ 284 | ], 285 | "width": 0.0 286 | }, 287 | "select_symbol": 288 | { 289 | "height": 0.0, 290 | "last_filter": "", 291 | "selected_items": 292 | [ 293 | ], 294 | "width": 0.0 295 | }, 296 | "selected_group": 0, 297 | "settings": 298 | { 299 | }, 300 | "show_minimap": true, 301 | "show_open_files": true, 302 | "show_tabs": true, 303 | "side_bar_visible": true, 304 | "side_bar_width": 244.0, 305 | "status_bar_visible": true, 306 | "template_settings": 307 | { 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /landing/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Flask 3 | from flask_wtf.csrf import CSRFProtect 4 | from flask_sqlalchemy import SQLAlchemy 5 | from flask_migrate import Migrate 6 | 7 | 8 | app = Flask(__name__) 9 | csrf = CSRFProtect(app) 10 | 11 | # don't share in production 12 | # secret key 13 | # wtf csrf secret key 14 | 15 | db_path = os.path.join(os.path.dirname(__file__), 'db.sqlite3') 16 | db_uri = 'sqlite:///{}'.format(db_path) 17 | 18 | app.config.update(dict( 19 | SECRET_KEY="4b332#@@1!74-006b-4889-8#@#-67b#@#b90ddffd", 20 | WTF_CSRF_SECRET_KEY='8207bece0139#4980!af20a681e3bf56f3', 21 | SQLALCHEMY_DATABASE_URI=db_uri, 22 | SQLALCHEMY_TRACK_MODIFICATIONS=True, 23 | )) 24 | 25 | db = SQLAlchemy(app) 26 | migrate = Migrate(app, db) 27 | 28 | 29 | from .views import * 30 | from .home.views import * 31 | from .jobs.views import * 32 | from .profiles.views import * 33 | -------------------------------------------------------------------------------- /landing/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Flask-Landing/7e81b1c7eccdb76d04741100c2945af71c44b356/landing/db.sqlite3 -------------------------------------------------------------------------------- /landing/home/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Flask-Landing/7e81b1c7eccdb76d04741100c2945af71c44b356/landing/home/__init__.py -------------------------------------------------------------------------------- /landing/home/forms.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import StringField, PasswordField, validators, ValidationError, HiddenField 3 | from sqlalchemy import not_ 4 | 5 | from .models import EmailSignup 6 | 7 | class ViewItemsForm(FlaskForm): 8 | password = PasswordField('Password', validators=[ 9 | validators.DataRequired( 10 | message='You full name is required' 11 | ) 12 | ]) 13 | 14 | def validate_password(self, field): 15 | if field.data != 'e3@#@#3c9-06c8-4c30-8a12-08fDFA:1688f5': 16 | raise ValidationError("You do not have permission to view this.") 17 | 18 | 19 | class LandingForm(FlaskForm): 20 | id = HiddenField('id') 21 | full_name = StringField('Full name', 22 | render_kw={"class": "form-control", 23 | "placeholder": "Full name"}, 24 | validators=[ 25 | validators.DataRequired( 26 | message='You full name is required' 27 | ) 28 | ]) 29 | email = StringField('Email', 30 | render_kw={"class": "form-control", 31 | "placeholder": "Your email"}, 32 | validators=[ 33 | validators.DataRequired( 34 | message='You email is required' 35 | ), 36 | validators.Email() 37 | ]) 38 | 39 | def validate_email(self, field): 40 | _id = self.data.get('id', -1) # -1 is not a valid id 41 | if field.data.endswith(".edu"): 42 | raise ValidationError('You cannot use a school email address.') 43 | not_query = not_(EmailSignup.id == _id) 44 | obj = EmailSignup.query.filter_by(email=field.data).filter(not_query).first() 45 | if obj is not None: 46 | msg = 'This email has already been added.' 47 | raise ValidationError(msg) 48 | -------------------------------------------------------------------------------- /landing/home/models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from landing import db 3 | 4 | from sqlalchemy import event 5 | 6 | class EmailSignup(db.Model): 7 | id = db.Column(db.Integer, primary_key=True) # primary_key 8 | full_name = db.Column(db.String(120), nullable=True) 9 | email = db.Column(db.String(120), unique=True, nullable=False) 10 | timestamp = db.Column(db.DateTime, index=True, default=datetime.datetime.utcnow) 11 | updated = db.Column(db.DateTime, index=True, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow) 12 | 13 | 14 | def save(self, commit=True): 15 | # create and update 16 | if commit: 17 | instance = self 18 | if not instance.id: 19 | db.session.add(instance) 20 | try: 21 | db.session.commit() 22 | except Exception as e: 23 | print("Exception occured\n", e, '\n') 24 | db.session.rollback() 25 | return False 26 | return True 27 | return False 28 | 29 | def delete(self, commit=True): 30 | # delete 31 | if self.id: 32 | db.session.delete(self) 33 | try: 34 | db.session.commit() 35 | except Exception as e: 36 | print("Exception occured\n", e, '\n') 37 | db.session.rollback() 38 | return False 39 | return True 40 | return False 41 | 42 | 43 | 44 | 45 | 46 | # https://docs.sqlalchemy.org/en/latest/orm/events.html 47 | 48 | 49 | # before_insert 50 | # after_insert 51 | 52 | # before_update 53 | # after_update 54 | 55 | # before_delete 56 | # after_delete 57 | 58 | @event.listens_for(EmailSignup, 'before_update') 59 | def email_signup_pre_update_signal(mapper, connection, target): 60 | pass 61 | # target = instance 62 | # target.full_name = target.full_name + " working..." 63 | # if target.slug is None: 64 | # target.slug = full_name.lower().replace(" ", "-") 65 | 66 | @event.listens_for(EmailSignup, 'after_update') 67 | def email_signup_post_update_signal(mapper, connection, target): 68 | # target = instance 69 | assert target.id is not None 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /landing/home/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | from flask import render_template, request, redirect, url_for 3 | from landing import app 4 | 5 | from .forms import LandingForm, ViewItemsForm 6 | from .models import EmailSignup 7 | 8 | @app.route("/", methods=['GET', 'POST']) 9 | def home(): 10 | form = LandingForm() 11 | if form.validate_on_submit(): 12 | data = { 13 | "full_name": form.full_name.data, 14 | "email": form.email.data 15 | } 16 | obj = EmailSignup.query.filter_by(email=form.email.data).first() 17 | if obj is None: 18 | obj = EmailSignup(**data) #(full_name=, email=) 19 | obj.save() 20 | #form = LandingForm() 21 | #return render_template('home.html', form=form)\ 22 | return redirect('/success') 23 | # send email -> via flask and smtp 24 | # create txt doc 25 | # create csv doc 26 | # send webhook -> zapier -> google docs 27 | # save into database -> sqlite -> mysql / postgresql 28 | return render_template('home.html', form=form) 29 | 30 | ''' 31 | create email obj instance via form 32 | saved in database 33 | in database, we have objects with ids in models.py 34 | ids == primary keys 35 | primary keys to lookup in database 36 | use dynamic url routing to lookup object in database 37 | ''' 38 | @app.route("/item/", methods=['GET', 'POST']) 39 | def item_list(): 40 | form = ViewItemsForm() 41 | if form.validate_on_submit(): 42 | object_list = EmailSignup.query.all() 43 | return render_template("items/list.html", form=None, object_list=object_list) 44 | return render_template("items/list.html", form=form, object_list=[]) 45 | 46 | @app.route("/success/", methods=['GET']) 47 | def success_view(): 48 | return render_template("success.html") 49 | 50 | 51 | # @app.route("/item/", methods=['GET']) 52 | # def item_list_redirect(): 53 | # redirect_url = url_for('item_list') 54 | # return redirect(redirect_url) 55 | 56 | 57 | # @app.route("/items//", methods=['GET']) 58 | # def item_detail_redirect(id): 59 | # redirect_url = url_for('item_detail', id=id) 60 | # return redirect(redirect_url) 61 | 62 | 63 | 64 | # @app.route("/item//", methods=['GET']) 65 | # def item_detail(id): 66 | # # instance = EmailSignup.query.get(id) 67 | # instance = EmailSignup.query.filter_by(id=id).first_or_404() 68 | # return render_template('items/detail.html', instance=instance) 69 | 70 | 71 | # @app.route("/item//update/", methods=['GET', 'POST']) 72 | # def item_update(id): 73 | # # instance = EmailSignup.query.get(id) 74 | # instance = EmailSignup.query.filter_by(id=id).first_or_404() 75 | # # instance.full_name -> form.full_name 76 | # # instance.email -> form.email 77 | # form = LandingForm(obj=instance) 78 | # if form.validate_on_submit(): 79 | # full_name = form.full_name.data 80 | # email = form.email.data 81 | # instance.full_name = full_name 82 | # instance.email = email 83 | # instance.save() 84 | # redirect_url = url_for('item_detail', id=instance.id) 85 | # return redirect(redirect_url) 86 | # return render_template('items/form.html', instance=instance, form=form) 87 | 88 | 89 | 90 | 91 | # @app.route("/item//delete/", methods=['GET', 'POST']) 92 | # def item_delete(id): 93 | # # instance = EmailSignup.query.get(id) 94 | # instance = EmailSignup.query.filter_by(id=id).first_or_404() 95 | # # form 96 | # if request.method == "POST": 97 | # instance.delete() 98 | # return redirect("/") 99 | # return render_template('items/delete.html', instance=instance) 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /landing/jobs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Flask-Landing/7e81b1c7eccdb76d04741100c2945af71c44b356/landing/jobs/__init__.py -------------------------------------------------------------------------------- /landing/jobs/views.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify 2 | from landing import app 3 | 4 | # json 5 | @app.route("/jobs/") 6 | def jobs_api(): 7 | data = {'job_id': 123, 'tasks': [12, 13,21312, 3121]} 8 | return jsonify(data) 9 | 10 | 11 | @app.route("/jobs//") 12 | def jobs(job_id): 13 | data = {'job_id': job_id, 'tasks': [12, 13,21312, 3121]} 14 | return jsonify(data) -------------------------------------------------------------------------------- /landing/profiles/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Flask-Landing/7e81b1c7eccdb76d04741100c2945af71c44b356/landing/profiles/__init__.py -------------------------------------------------------------------------------- /landing/profiles/views.py: -------------------------------------------------------------------------------- 1 | from flask import render_template 2 | from landing import app 3 | 4 | @app.route("/users//") 5 | def profiles_detail(username): 6 | context = {"user": username} 7 | if username == 'jmitchel3': 8 | context["right_user_msg"] = "Yeah that's right" 9 | return render_template('profiles_detail.html', context=context) 10 | 11 | 12 | @app.route("/users/") 13 | def profiles_list(): 14 | return render_template('profiles_list.html') -------------------------------------------------------------------------------- /landing/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {% block title %}{% endblock title %} Flask Landing 12 | 13 | 14 | 15 |
16 | {% block content %}asdfaasdfads{% endblock content %} 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /landing/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 |

Join us

6 | Flask Landing Project 7 | 8 |
9 |
10 | {{ form.csrf_token }} 11 | 12 |
13 | 14 | {{ form.full_name }} 15 | {% if form.full_name.errors %} 16 |
17 | {% for error in form.full_name.errors %} 18 | {{error}} 19 | {% endfor %} 20 |
21 | {% endif %} 22 |
23 | 24 |
25 | 26 | {{ form.email }} 27 | {% if form.email.errors %} 28 |
29 | {% for error in form.email.errors %} 30 | {{error}} 31 | {% endfor %} 32 |
33 | {% endif %} 34 |
35 | 36 | 37 | 38 |
39 |
40 |
41 | 42 | {% endblock %} -------------------------------------------------------------------------------- /landing/templates/items/delete.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 |

Delete Instance #{{ instance.id }}?

6 | 7 |
8 | 9 | 10 | 11 |
12 |
13 | 14 | {% endblock %} -------------------------------------------------------------------------------- /landing/templates/items/detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 |

{{ instance.id }} {{ instance.full_name }} {{ instance.email }}

6 | 7 |
8 | 9 | {% endblock content %} -------------------------------------------------------------------------------- /landing/templates/items/form.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 |

Update Instance #{{ instance.id }}

6 | 7 |
8 | {{ form.csrf_token }} 9 | 10 |
11 | 12 | {{ form.full_name }} 13 | {% if form.full_name.errors %} 14 |
15 | {% for error in form.full_name.errors %} 16 | {{error}} 17 | {% endfor %} 18 |
19 | {% endif %} 20 |
21 | 22 |
23 | 24 | {{ form.email }} 25 | {% if form.email.errors %} 26 |
27 | {% for error in form.email.errors %} 28 | {{error}} 29 | {% endfor %} 30 |
31 | {% endif %} 32 |
33 | 34 | 35 | 36 |
37 |
38 | 39 | {% endblock %} -------------------------------------------------------------------------------- /landing/templates/items/list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 | 6 | {% if form %} 7 |
8 | {{ form.csrf_token }} 9 | 10 |
11 | 12 | {{ form.password }} 13 | {% if form.password.errors %} 14 |
15 | {% for error in form.password.errors %} 16 | {{error}} 17 | {% endfor %} 18 |
19 | {% endif %} 20 |
21 | 22 | 23 | 24 | 25 |
26 | {% endif %} 27 | 28 | 29 | {% for instance in object_list %} 30 |

{{ instance.id }}, {{ instance.full_name }} {{ instance.email }} {{ instance.timestamp }}

31 | {% endfor %} 32 | 33 |
34 | 35 | {% endblock content %} -------------------------------------------------------------------------------- /landing/templates/navbar.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /landing/templates/profiles_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} User {% endblock title %} 4 | 5 | {% block content %} 6 | 7 | 8 |

Hello there {{ context.user }}

9 | 10 | {{ context.right_user_msg }} 11 | 12 | 13 | {% endblock content %} -------------------------------------------------------------------------------- /landing/templates/profiles_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} User {% endblock title %} 4 | 5 | {% block content %} 6 | 7 |

Hello there profiles

8 | 9 | {% endblock content %} -------------------------------------------------------------------------------- /landing/templates/success.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 | {% include 'navbar.html' %} 5 |
6 |

Success! Thanks for joining

7 |
8 | {% endblock content %} -------------------------------------------------------------------------------- /landing/views.py: -------------------------------------------------------------------------------- 1 | from landing import app 2 | 3 | 4 | 5 | @app.route("/contact-us/") 6 | def contact_us(): 7 | return "

Contact Us

" 8 | 9 | @app.route("/about-us/") 10 | def about_us(): 11 | return "

About Us

" 12 | 13 | 14 | -------------------------------------------------------------------------------- /migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [handler_console] 38 | class = StreamHandler 39 | args = (sys.stderr,) 40 | level = NOTSET 41 | formatter = generic 42 | 43 | [formatter_generic] 44 | format = %(levelname)-5.5s [%(name)s] %(message)s 45 | datefmt = %H:%M:%S 46 | -------------------------------------------------------------------------------- /migrations/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | import logging 4 | from logging.config import fileConfig 5 | 6 | from sqlalchemy import engine_from_config 7 | from sqlalchemy import pool 8 | 9 | from alembic import context 10 | 11 | # this is the Alembic Config object, which provides 12 | # access to the values within the .ini file in use. 13 | config = context.config 14 | 15 | # Interpret the config file for Python logging. 16 | # This line sets up loggers basically. 17 | fileConfig(config.config_file_name) 18 | logger = logging.getLogger('alembic.env') 19 | 20 | # add your model's MetaData object here 21 | # for 'autogenerate' support 22 | # from myapp import mymodel 23 | # target_metadata = mymodel.Base.metadata 24 | from flask import current_app 25 | config.set_main_option('sqlalchemy.url', 26 | current_app.config.get('SQLALCHEMY_DATABASE_URI')) 27 | target_metadata = current_app.extensions['migrate'].db.metadata 28 | 29 | # other values from the config, defined by the needs of env.py, 30 | # can be acquired: 31 | # my_important_option = config.get_main_option("my_important_option") 32 | # ... etc. 33 | 34 | 35 | def run_migrations_offline(): 36 | """Run migrations in 'offline' mode. 37 | 38 | This configures the context with just a URL 39 | and not an Engine, though an Engine is acceptable 40 | here as well. By skipping the Engine creation 41 | we don't even need a DBAPI to be available. 42 | 43 | Calls to context.execute() here emit the given string to the 44 | script output. 45 | 46 | """ 47 | url = config.get_main_option("sqlalchemy.url") 48 | context.configure( 49 | url=url, target_metadata=target_metadata, literal_binds=True 50 | ) 51 | 52 | with context.begin_transaction(): 53 | context.run_migrations() 54 | 55 | 56 | def run_migrations_online(): 57 | """Run migrations in 'online' mode. 58 | 59 | In this scenario we need to create an Engine 60 | and associate a connection with the context. 61 | 62 | """ 63 | 64 | # this callback is used to prevent an auto-migration from being generated 65 | # when there are no changes to the schema 66 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html 67 | def process_revision_directives(context, revision, directives): 68 | if getattr(config.cmd_opts, 'autogenerate', False): 69 | script = directives[0] 70 | if script.upgrade_ops.is_empty(): 71 | directives[:] = [] 72 | logger.info('No changes in schema detected.') 73 | 74 | connectable = engine_from_config( 75 | config.get_section(config.config_ini_section), 76 | prefix='sqlalchemy.', 77 | poolclass=pool.NullPool, 78 | ) 79 | 80 | with connectable.connect() as connection: 81 | context.configure( 82 | connection=connection, 83 | target_metadata=target_metadata, 84 | process_revision_directives=process_revision_directives, 85 | **current_app.extensions['migrate'].configure_args 86 | ) 87 | 88 | with context.begin_transaction(): 89 | context.run_migrations() 90 | 91 | 92 | if context.is_offline_mode(): 93 | run_migrations_offline() 94 | else: 95 | run_migrations_online() 96 | -------------------------------------------------------------------------------- /migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /migrations/versions/0bc39a029870_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 0bc39a029870 4 | Revises: 5 | Create Date: 2019-03-11 15:46:36.144388 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '0bc39a029870' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('email_signup', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('full_name', sa.String(length=120), nullable=True), 24 | sa.Column('email', sa.String(length=120), nullable=False), 25 | sa.PrimaryKeyConstraint('id'), 26 | sa.UniqueConstraint('email') 27 | ) 28 | # ### end Alembic commands ### 29 | 30 | 31 | def downgrade(): 32 | # ### commands auto generated by Alembic - please adjust! ### 33 | op.drop_table('email_signup') 34 | # ### end Alembic commands ### 35 | -------------------------------------------------------------------------------- /migrations/versions/d64db672b5ce_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: d64db672b5ce 4 | Revises: 0bc39a029870 5 | Create Date: 2019-03-11 15:46:47.427682 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'd64db672b5ce' 14 | down_revision = '0bc39a029870' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('email_signup', sa.Column('timestamp', sa.DateTime(), nullable=True)) 22 | op.add_column('email_signup', sa.Column('updated', sa.DateTime(), nullable=True)) 23 | op.create_index(op.f('ix_email_signup_timestamp'), 'email_signup', ['timestamp'], unique=False) 24 | op.create_index(op.f('ix_email_signup_updated'), 'email_signup', ['updated'], unique=False) 25 | # ### end Alembic commands ### 26 | 27 | 28 | def downgrade(): 29 | # ### commands auto generated by Alembic - please adjust! ### 30 | op.drop_index(op.f('ix_email_signup_updated'), table_name='email_signup') 31 | op.drop_index(op.f('ix_email_signup_timestamp'), table_name='email_signup') 32 | op.drop_column('email_signup', 'updated') 33 | op.drop_column('email_signup', 'timestamp') 34 | # ### end Alembic commands ### 35 | -------------------------------------------------------------------------------- /wsgi.py: -------------------------------------------------------------------------------- 1 | from landing import app as application 2 | 3 | if __name__ == '__main__': 4 | application.run() --------------------------------------------------------------------------------