├── .dockerignore ├── .env.example ├── .flaskenv ├── .gitignore ├── .vscode └── settings.json ├── Dockerfile ├── Pipfile ├── Pipfile.lock ├── README.md ├── app ├── __init__.py ├── api │ ├── auth_routes.py │ ├── comment_routes.py │ ├── post_reaction_routes.py │ ├── post_routes.py │ └── user_routes.py ├── aws_helpers.py ├── config.py ├── forms │ ├── __init__.py │ ├── login_form.py │ ├── post_form.py │ └── signup_form.py ├── models │ ├── __init__.py │ ├── comment.py │ ├── db.py │ ├── post.py │ ├── post_reactions.py │ └── user.py └── seeds │ ├── __init__.py │ ├── post.py │ └── users.py ├── assets └── images │ ├── Like-Or-Hate-Actions.gif │ └── Like-Or-Hate.png ├── dev-requirements.txt ├── migrations ├── README ├── alembic.ini ├── env.py ├── script.py.mako └── versions │ ├── 20201120_150602_create_users_table.py │ ├── 20210621_133159_test_migration.py │ ├── 20210621_162239_changed_caption_to_new_caption.py │ ├── 20210623_091822_post_reactions.py │ ├── 20210623_124208_.py │ ├── 20210623_141801_testing_removal_of_post_reactions.py │ ├── 20210623_141909_re_adding_the_post_reactions_migration_.py │ ├── 20210623_142843_removed_unnecessary_columns_moving_to_a_.py │ ├── 20210624_164710_fix_migration.py │ └── 20210815_211921_cascading_deletes_for_posts_with_.py ├── react-app ├── .env.example ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── favicon.backup.ico │ ├── favicon.ico │ └── index.html └── src │ ├── App.js │ ├── components │ ├── 404Page │ │ ├── Page404.js │ │ ├── Page404.module.css │ │ └── index.js │ ├── Comment │ │ ├── Comment.js │ │ └── Comment.module.css │ ├── CommentBody │ │ ├── CommentBody.js │ │ ├── CommentBody.module.css │ │ └── index.js │ ├── CommentReaction │ │ ├── CommentReaction.js │ │ ├── comment-reaction.css │ │ └── index.js │ ├── EditCommentForm │ │ └── EditCommentForm.js │ ├── Footer │ │ ├── Footer.js │ │ ├── Footer.module.css │ │ └── index.js │ ├── NavBar │ │ ├── NavBar.js │ │ ├── index.js │ │ └── navbar.css │ ├── Post │ │ ├── Post.js │ │ ├── index.js │ │ └── post.css │ ├── PostForm │ │ ├── PostForm.js │ │ ├── index.js │ │ └── post-form.css │ ├── PostReaction │ │ ├── PostReaction.js │ │ ├── index.js │ │ └── post-reaction.css │ ├── PostShow │ │ ├── PostShow.js │ │ ├── index.js │ │ └── post-show.css │ ├── SplashPage │ │ ├── ScreenShot.png │ │ ├── SplashPage.js │ │ ├── SplashPage.module.css │ │ └── index.js │ ├── User │ │ ├── User.js │ │ ├── index.js │ │ └── user.css │ ├── UserPost │ │ ├── UserPost.js │ │ ├── index.js │ │ └── user-post.css │ └── auth │ │ ├── DemoUser.js │ │ ├── LoginForm.js │ │ ├── LogoutButton.js │ │ ├── ProtectedRoute.js │ │ ├── SignUpForm.js │ │ ├── loginForm.css │ │ └── signUpForm.css │ ├── index.css │ ├── index.js │ └── store │ ├── comment.js │ ├── index.js │ ├── post.js │ ├── session.js │ └── user.js └── requirements.txt /.dockerignore: -------------------------------------------------------------------------------- 1 | react-app/node_modules 2 | .venv 3 | Pipfile 4 | Pipfile.lock 5 | .env 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | FLASK_APP=app 2 | FLASK_ENV=development 3 | SECRET_KEY=lkasjdf09ajsdkfljalsiorj12n3490re9485309irefvn,u90818734902139489230 4 | DATABASE_URL=postgresql://starter_app_dev:password@localhost/starter_app 5 | S3_BUCKET= 6 | S3_KEY= 7 | S3_SECRET= 8 | -------------------------------------------------------------------------------- /.flaskenv: -------------------------------------------------------------------------------- 1 | FLASK_APP=app -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | __pycache__/ 3 | *.py[cod] 4 | .venv 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "/usr/local/bin/python3", 3 | "python.linting.pylintEnabled": false, 4 | "python.linting.enabled": false, 5 | "python.linting.pycodestyleEnabled": false, 6 | "python.linting.flake8Enabled": true 7 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12 AS build-stage 2 | 3 | WORKDIR /react-app 4 | COPY react-app/. . 5 | 6 | # You have to set this because it should be set during build time. 7 | ENV REACT_APP_BASE_URL=https://like-or-hate.herokuapp.com/ 8 | 9 | # Build our React App 10 | RUN npm install 11 | RUN npm run build 12 | 13 | FROM python:3.8 14 | 15 | # Setup Flask environment 16 | ENV FLASK_APP=app 17 | ENV FLASK_ENV=production 18 | ENV SQLALCHEMY_ECHO=True 19 | 20 | EXPOSE 8000 21 | 22 | WORKDIR /var/www 23 | COPY . . 24 | COPY --from=build-stage /react-app/build/* app/static/ 25 | 26 | # Install Python Dependencies 27 | RUN pip install -r requirements.txt 28 | RUN pip install psycopg2 29 | 30 | # Run flask environment 31 | CMD gunicorn app:app 32 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | click = "==7.1.2" 8 | gunicorn = "==20.0.4" 9 | itsdangerous = "==1.1.0" 10 | python-dotenv = "==0.14.0" 11 | six = "==1.15.0" 12 | Flask = "==1.1.2" 13 | Flask-Cors = "==3.0.8" 14 | Flask-SQLAlchemy = "==2.4.4" 15 | Flask-WTF = "==0.14.3" 16 | Jinja2 = "==2.11.2" 17 | MarkupSafe = "==1.1.1" 18 | SQLAlchemy = "==1.3.19" 19 | Werkzeug = "==1.0.1" 20 | WTForms = "==2.3.3" 21 | Flask-JWT-Extended = "==3.24.1" 22 | email-validator = "==1.1.3" 23 | Flask-Migrate = "==2.5.3" 24 | Flask-Login = "==0.5.0" 25 | alembic = "==1.4.3" 26 | python-dateutil = "==2.8.1" 27 | python-editor = "==1.0.4" 28 | Mako = "==1.1.3" 29 | PyJWT = "==1.7.1" 30 | boto3 = "==1.18.17" 31 | botocore = "==1.21.17" 32 | dnspython = "==2.1.0" 33 | idna = "==3.2" 34 | jmespath = "==0.10.0" 35 | psycopg2-binary = "==2.8.6" 36 | s3transfer = "==0.5.0" 37 | urllib3 = "==1.26.6" 38 | 39 | [dev-packages] 40 | autopep8 = "*" 41 | pylint = "*" 42 | psycopg2-binary = "==2.8.6" 43 | 44 | [requires] 45 | python_version = "3.9" 46 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "ccb078ec3af815db7af449ed71543ec214bd4c755d291768a0fee2bd137c9a18" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.9" 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:4e02ed2aa796bd179965041afa092c55b51fb077de19d61835673cc80672c01c", 22 | "sha256:5334f32314fb2a56d86b4c4dd1ae34b08c03cae4cb888bc699942104d66bc245" 23 | ], 24 | "index": "pypi", 25 | "version": "==1.4.3" 26 | }, 27 | "boto3": { 28 | "hashes": [ 29 | "sha256:5e5f60ece9b73d48f668bef56ddcde716f013b48a62fdf9c5eac9512a5981136", 30 | "sha256:69a5ebbd5fda6742d20fd536cd9b2927f2eaa8dde84ad529fe816231afcf9c68" 31 | ], 32 | "index": "pypi", 33 | "version": "==1.18.17" 34 | }, 35 | "botocore": { 36 | "hashes": [ 37 | "sha256:5b665142bdb2c30fc86b15bc48dd8b74c9cac69dc3e20b6d8f79cb60ff368797", 38 | "sha256:a0d64369857d86b3a6d01b0c5933671c2394584311ce3af702271ba221b09afa" 39 | ], 40 | "index": "pypi", 41 | "version": "==1.21.17" 42 | }, 43 | "click": { 44 | "hashes": [ 45 | "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", 46 | "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" 47 | ], 48 | "index": "pypi", 49 | "version": "==7.1.2" 50 | }, 51 | "dnspython": { 52 | "hashes": [ 53 | "sha256:95d12f6ef0317118d2a1a6fc49aac65ffec7eb8087474158f42f26a639135216", 54 | "sha256:e4a87f0b573201a0f3727fa18a516b055fd1107e0e5477cded4a2de497df1dd4" 55 | ], 56 | "index": "pypi", 57 | "version": "==2.1.0" 58 | }, 59 | "email-validator": { 60 | "hashes": [ 61 | "sha256:5675c8ceb7106a37e40e2698a57c056756bf3f272cfa8682a4f87ebd95d8440b", 62 | "sha256:aa237a65f6f4da067119b7df3f13e89c25c051327b2b5b66dc075f33d62480d7" 63 | ], 64 | "index": "pypi", 65 | "version": "==1.1.3" 66 | }, 67 | "flask": { 68 | "hashes": [ 69 | "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060", 70 | "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557" 71 | ], 72 | "index": "pypi", 73 | "version": "==1.1.2" 74 | }, 75 | "flask-cors": { 76 | "hashes": [ 77 | "sha256:72170423eb4612f0847318afff8c247b38bd516b7737adfc10d1c2cdbb382d16", 78 | "sha256:f4d97201660e6bbcff2d89d082b5b6d31abee04b1b3003ee073a6fd25ad1d69a" 79 | ], 80 | "index": "pypi", 81 | "version": "==3.0.8" 82 | }, 83 | "flask-jwt-extended": { 84 | "hashes": [ 85 | "sha256:0aa8ee6fa7eb3be9314e39dd199ac8e19389a95371f9d54e155c7aa635e319dd" 86 | ], 87 | "index": "pypi", 88 | "version": "==3.24.1" 89 | }, 90 | "flask-login": { 91 | "hashes": [ 92 | "sha256:6d33aef15b5bcead780acc339464aae8a6e28f13c90d8b1cf9de8b549d1c0b4b", 93 | "sha256:7451b5001e17837ba58945aead261ba425fdf7b4f0448777e597ddab39f4fba0" 94 | ], 95 | "index": "pypi", 96 | "version": "==0.5.0" 97 | }, 98 | "flask-migrate": { 99 | "hashes": [ 100 | "sha256:4dc4a5cce8cbbb06b8dc963fd86cf8136bd7d875aabe2d840302ea739b243732", 101 | "sha256:a69d508c2e09d289f6e55a417b3b8c7bfe70e640f53d2d9deb0d056a384f37ee" 102 | ], 103 | "index": "pypi", 104 | "version": "==2.5.3" 105 | }, 106 | "flask-sqlalchemy": { 107 | "hashes": [ 108 | "sha256:05b31d2034dd3f2a685cbbae4cfc4ed906b2a733cff7964ada450fd5e462b84e", 109 | "sha256:bfc7150eaf809b1c283879302f04c42791136060c6eeb12c0c6674fb1291fae5" 110 | ], 111 | "index": "pypi", 112 | "version": "==2.4.4" 113 | }, 114 | "flask-wtf": { 115 | "hashes": [ 116 | "sha256:57b3faf6fe5d6168bda0c36b0df1d05770f8e205e18332d0376ddb954d17aef2", 117 | "sha256:d417e3a0008b5ba583da1763e4db0f55a1269d9dd91dcc3eb3c026d3c5dbd720" 118 | ], 119 | "index": "pypi", 120 | "version": "==0.14.3" 121 | }, 122 | "gunicorn": { 123 | "hashes": [ 124 | "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626", 125 | "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c" 126 | ], 127 | "index": "pypi", 128 | "version": "==20.0.4" 129 | }, 130 | "idna": { 131 | "hashes": [ 132 | "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", 133 | "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3" 134 | ], 135 | "index": "pypi", 136 | "version": "==3.2" 137 | }, 138 | "itsdangerous": { 139 | "hashes": [ 140 | "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", 141 | "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" 142 | ], 143 | "index": "pypi", 144 | "version": "==1.1.0" 145 | }, 146 | "jinja2": { 147 | "hashes": [ 148 | "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0", 149 | "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035" 150 | ], 151 | "index": "pypi", 152 | "version": "==2.11.2" 153 | }, 154 | "jmespath": { 155 | "hashes": [ 156 | "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9", 157 | "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f" 158 | ], 159 | "index": "pypi", 160 | "version": "==0.10.0" 161 | }, 162 | "mako": { 163 | "hashes": [ 164 | "sha256:8195c8c1400ceb53496064314c6736719c6f25e7479cd24c77be3d9361cddc27", 165 | "sha256:93729a258e4ff0747c876bd9e20df1b9758028946e976324ccd2d68245c7b6a9" 166 | ], 167 | "index": "pypi", 168 | "version": "==1.1.3" 169 | }, 170 | "markupsafe": { 171 | "hashes": [ 172 | "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", 173 | "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", 174 | "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", 175 | "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", 176 | "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", 177 | "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f", 178 | "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39", 179 | "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", 180 | "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", 181 | "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014", 182 | "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f", 183 | "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", 184 | "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", 185 | "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", 186 | "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", 187 | "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", 188 | "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", 189 | "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", 190 | "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", 191 | "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85", 192 | "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1", 193 | "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", 194 | "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", 195 | "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", 196 | "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850", 197 | "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0", 198 | "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", 199 | "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", 200 | "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb", 201 | "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", 202 | "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", 203 | "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", 204 | "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1", 205 | "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2", 206 | "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", 207 | "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", 208 | "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", 209 | "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7", 210 | "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", 211 | "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8", 212 | "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", 213 | "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193", 214 | "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", 215 | "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b", 216 | "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", 217 | "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", 218 | "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5", 219 | "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c", 220 | "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032", 221 | "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", 222 | "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be", 223 | "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621" 224 | ], 225 | "index": "pypi", 226 | "version": "==1.1.1" 227 | }, 228 | "psycopg2-binary": { 229 | "hashes": [ 230 | "sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c", 231 | "sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67", 232 | "sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0", 233 | "sha256:15978a1fbd225583dd8cdaf37e67ccc278b5abecb4caf6b2d6b8e2b948e953f6", 234 | "sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db", 235 | "sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94", 236 | "sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52", 237 | "sha256:6422f2ff0919fd720195f64ffd8f924c1395d30f9a495f31e2392c2efafb5056", 238 | "sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b", 239 | "sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd", 240 | "sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550", 241 | "sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679", 242 | "sha256:89705f45ce07b2dfa806ee84439ec67c5d9a0ef20154e0e475e2b2ed392a5b83", 243 | "sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77", 244 | "sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2", 245 | "sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77", 246 | "sha256:a0eb43a07386c3f1f1ebb4dc7aafb13f67188eab896e7397aa1ee95a9c884eb2", 247 | "sha256:aaa4213c862f0ef00022751161df35804127b78adf4a2755b9f991a507e425fd", 248 | "sha256:ac0c682111fbf404525dfc0f18a8b5f11be52657d4f96e9fcb75daf4f3984859", 249 | "sha256:ad20d2eb875aaa1ea6d0f2916949f5c08a19c74d05b16ce6ebf6d24f2c9f75d1", 250 | "sha256:b4afc542c0ac0db720cf516dd20c0846f71c248d2b3d21013aa0d4ef9c71ca25", 251 | "sha256:b8a3715b3c4e604bcc94c90a825cd7f5635417453b253499664f784fc4da0152", 252 | "sha256:ba28584e6bca48c59eecbf7efb1576ca214b47f05194646b081717fa628dfddf", 253 | "sha256:ba381aec3a5dc29634f20692349d73f2d21f17653bda1decf0b52b11d694541f", 254 | "sha256:bd1be66dde2b82f80afb9459fc618216753f67109b859a361cf7def5c7968729", 255 | "sha256:c2507d796fca339c8fb03216364cca68d87e037c1f774977c8fc377627d01c71", 256 | "sha256:cec7e622ebc545dbb4564e483dd20e4e404da17ae07e06f3e780b2dacd5cee66", 257 | "sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4", 258 | "sha256:d1b4ab59e02d9008efe10ceabd0b31e79519da6fb67f7d8e8977118832d0f449", 259 | "sha256:d5227b229005a696cc67676e24c214740efd90b148de5733419ac9aaba3773da", 260 | "sha256:e1f57aa70d3f7cc6947fd88636a481638263ba04a742b4a37dd25c373e41491a", 261 | "sha256:e74a55f6bad0e7d3968399deb50f61f4db1926acf4a6d83beaaa7df986f48b1c", 262 | "sha256:e82aba2188b9ba309fd8e271702bd0d0fc9148ae3150532bbb474f4590039ffb", 263 | "sha256:ee69dad2c7155756ad114c02db06002f4cded41132cc51378e57aad79cc8e4f4", 264 | "sha256:f5ab93a2cb2d8338b1674be43b442a7f544a0971da062a5da774ed40587f18f5" 265 | ], 266 | "index": "pypi", 267 | "version": "==2.8.6" 268 | }, 269 | "pyjwt": { 270 | "hashes": [ 271 | "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e", 272 | "sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96" 273 | ], 274 | "index": "pypi", 275 | "version": "==1.7.1" 276 | }, 277 | "python-dateutil": { 278 | "hashes": [ 279 | "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", 280 | "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" 281 | ], 282 | "index": "pypi", 283 | "version": "==2.8.1" 284 | }, 285 | "python-dotenv": { 286 | "hashes": [ 287 | "sha256:8c10c99a1b25d9a68058a1ad6f90381a62ba68230ca93966882a4dbc3bc9c33d", 288 | "sha256:c10863aee750ad720f4f43436565e4c1698798d763b63234fb5021b6c616e423" 289 | ], 290 | "index": "pypi", 291 | "version": "==0.14.0" 292 | }, 293 | "python-editor": { 294 | "hashes": [ 295 | "sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d", 296 | "sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b", 297 | "sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8", 298 | "sha256:c3da2053dbab6b29c94e43c486ff67206eafbe7eb52dbec7390b5e2fb05aac77", 299 | "sha256:ea87e17f6ec459e780e4221f295411462e0d0810858e055fc514684350a2f522" 300 | ], 301 | "index": "pypi", 302 | "version": "==1.0.4" 303 | }, 304 | "s3transfer": { 305 | "hashes": [ 306 | "sha256:50ed823e1dc5868ad40c8dc92072f757aa0e653a192845c94a3b676f4a62da4c", 307 | "sha256:9c1dc369814391a6bda20ebbf4b70a0f34630592c9aa520856bf384916af2803" 308 | ], 309 | "index": "pypi", 310 | "version": "==0.5.0" 311 | }, 312 | "six": { 313 | "hashes": [ 314 | "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", 315 | "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" 316 | ], 317 | "index": "pypi", 318 | "version": "==1.15.0" 319 | }, 320 | "sqlalchemy": { 321 | "hashes": [ 322 | "sha256:072766c3bd09294d716b2d114d46ffc5ccf8ea0b714a4e1c48253014b771c6bb", 323 | "sha256:107d4af989831d7b091e382d192955679ec07a9209996bf8090f1f539ffc5804", 324 | "sha256:15c0bcd3c14f4086701c33a9e87e2c7ceb3bcb4a246cd88ec54a49cf2a5bd1a6", 325 | "sha256:26c5ca9d09f0e21b8671a32f7d83caad5be1f6ff45eef5ec2f6fd0db85fc5dc0", 326 | "sha256:276936d41111a501cf4a1a0543e25449108d87e9f8c94714f7660eaea89ae5fe", 327 | "sha256:3292a28344922415f939ee7f4fc0c186f3d5a0bf02192ceabd4f1129d71b08de", 328 | "sha256:33d29ae8f1dc7c75b191bb6833f55a19c932514b9b5ce8c3ab9bc3047da5db36", 329 | "sha256:3bba2e9fbedb0511769780fe1d63007081008c5c2d7d715e91858c94dbaa260e", 330 | "sha256:465c999ef30b1c7525f81330184121521418a67189053bcf585824d833c05b66", 331 | "sha256:51064ee7938526bab92acd049d41a1dc797422256086b39c08bafeffb9d304c6", 332 | "sha256:5a49e8473b1ab1228302ed27365ea0fadd4bf44bc0f9e73fe38e10fdd3d6b4fc", 333 | "sha256:618db68745682f64cedc96ca93707805d1f3a031747b5a0d8e150cfd5055ae4d", 334 | "sha256:6547b27698b5b3bbfc5210233bd9523de849b2bb8a0329cd754c9308fc8a05ce", 335 | "sha256:6557af9e0d23f46b8cd56f8af08eaac72d2e3c632ac8d5cf4e20215a8dca7cea", 336 | "sha256:73a40d4fcd35fdedce07b5885905753d5d4edf413fbe53544dd871f27d48bd4f", 337 | "sha256:8280f9dae4adb5889ce0bb3ec6a541bf05434db5f9ab7673078c00713d148365", 338 | "sha256:83469ad15262402b0e0974e612546bc0b05f379b5aa9072ebf66d0f8fef16bea", 339 | "sha256:860d0fe234922fd5552b7f807fbb039e3e7ca58c18c8d38aa0d0a95ddf4f6c23", 340 | "sha256:883c9fb62cebd1e7126dd683222b3b919657590c3e2db33bdc50ebbad53e0338", 341 | "sha256:8afcb6f4064d234a43fea108859942d9795c4060ed0fbd9082b0f280181a15c1", 342 | "sha256:96f51489ac187f4bab588cf51f9ff2d40b6d170ac9a4270ffaed535c8404256b", 343 | "sha256:9e865835e36dfbb1873b65e722ea627c096c11b05f796831e3a9b542926e979e", 344 | "sha256:aa0554495fe06172b550098909be8db79b5accdf6ffb59611900bea345df5eba", 345 | "sha256:b595e71c51657f9ee3235db8b53d0b57c09eee74dfb5b77edff0e46d2218dc02", 346 | "sha256:b6ff91356354b7ff3bd208adcf875056d3d886ed7cef90c571aef2ab8a554b12", 347 | "sha256:b70bad2f1a5bd3460746c3fb3ab69e4e0eb5f59d977a23f9b66e5bdc74d97b86", 348 | "sha256:c7adb1f69a80573698c2def5ead584138ca00fff4ad9785a4b0b2bf927ba308d", 349 | "sha256:c898b3ebcc9eae7b36bd0b4bbbafce2d8076680f6868bcbacee2d39a7a9726a7", 350 | "sha256:e49947d583fe4d29af528677e4f0aa21f5e535ca2ae69c48270ebebd0d8843c0", 351 | "sha256:eb1d71643e4154398b02e88a42fc8b29db8c44ce4134cf0f4474bfc5cb5d4dac", 352 | "sha256:f2e8a9c0c8813a468aa659a01af6592f71cd30237ec27c4cc0683f089f90dcfc", 353 | "sha256:fe7fe11019fc3e6600819775a7d55abc5446dda07e9795f5954fdbf8a49e1c37" 354 | ], 355 | "index": "pypi", 356 | "version": "==1.3.19" 357 | }, 358 | "urllib3": { 359 | "hashes": [ 360 | "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4", 361 | "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f" 362 | ], 363 | "index": "pypi", 364 | "version": "==1.26.6" 365 | }, 366 | "werkzeug": { 367 | "hashes": [ 368 | "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43", 369 | "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c" 370 | ], 371 | "index": "pypi", 372 | "version": "==1.0.1" 373 | }, 374 | "wtforms": { 375 | "hashes": [ 376 | "sha256:7b504fc724d0d1d4d5d5c114e778ec88c37ea53144683e084215eed5155ada4c", 377 | "sha256:81195de0ac94fbc8368abbaf9197b88c4f3ffd6c2719b5bf5fc9da744f3d829c" 378 | ], 379 | "index": "pypi", 380 | "version": "==2.3.3" 381 | } 382 | }, 383 | "develop": { 384 | "astroid": { 385 | "hashes": [ 386 | "sha256:3975a0bd5373bdce166e60c851cfcbaf21ee96de80ec518c1f4cb3e94c3fb334", 387 | "sha256:ab7f36e8a78b8e54a62028ba6beef7561db4cdb6f2a5009ecc44a6f42b5697ef" 388 | ], 389 | "markers": "python_version ~= '3.6'", 390 | "version": "==2.6.6" 391 | }, 392 | "autopep8": { 393 | "hashes": [ 394 | "sha256:276ced7e9e3cb22e5d7c14748384a5cf5d9002257c0ed50c0e075b68011bb6d0", 395 | "sha256:aa213493c30dcdac99537249ee65b24af0b2c29f2e83cd8b3f68760441ed0db9" 396 | ], 397 | "index": "pypi", 398 | "version": "==1.5.7" 399 | }, 400 | "isort": { 401 | "hashes": [ 402 | "sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899", 403 | "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2" 404 | ], 405 | "markers": "python_version < '4.0' and python_full_version >= '3.6.1'", 406 | "version": "==5.9.3" 407 | }, 408 | "lazy-object-proxy": { 409 | "hashes": [ 410 | "sha256:17e0967ba374fc24141738c69736da90e94419338fd4c7c7bef01ee26b339653", 411 | "sha256:1fee665d2638491f4d6e55bd483e15ef21f6c8c2095f235fef72601021e64f61", 412 | "sha256:22ddd618cefe54305df49e4c069fa65715be4ad0e78e8d252a33debf00f6ede2", 413 | "sha256:24a5045889cc2729033b3e604d496c2b6f588c754f7a62027ad4437a7ecc4837", 414 | "sha256:410283732af311b51b837894fa2f24f2c0039aa7f220135192b38fcc42bd43d3", 415 | "sha256:4732c765372bd78a2d6b2150a6e99d00a78ec963375f236979c0626b97ed8e43", 416 | "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726", 417 | "sha256:4f60460e9f1eb632584c9685bccea152f4ac2130e299784dbaf9fae9f49891b3", 418 | "sha256:5743a5ab42ae40caa8421b320ebf3a998f89c85cdc8376d6b2e00bd12bd1b587", 419 | "sha256:85fb7608121fd5621cc4377a8961d0b32ccf84a7285b4f1d21988b2eae2868e8", 420 | "sha256:9698110e36e2df951c7c36b6729e96429c9c32b3331989ef19976592c5f3c77a", 421 | "sha256:9d397bf41caad3f489e10774667310d73cb9c4258e9aed94b9ec734b34b495fd", 422 | "sha256:b579f8acbf2bdd9ea200b1d5dea36abd93cabf56cf626ab9c744a432e15c815f", 423 | "sha256:b865b01a2e7f96db0c5d12cfea590f98d8c5ba64ad222300d93ce6ff9138bcad", 424 | "sha256:bf34e368e8dd976423396555078def5cfc3039ebc6fc06d1ae2c5a65eebbcde4", 425 | "sha256:c6938967f8528b3668622a9ed3b31d145fab161a32f5891ea7b84f6b790be05b", 426 | "sha256:d1c2676e3d840852a2de7c7d5d76407c772927addff8d742b9808fe0afccebdf", 427 | "sha256:d7124f52f3bd259f510651450e18e0fd081ed82f3c08541dffc7b94b883aa981", 428 | "sha256:d900d949b707778696fdf01036f58c9876a0d8bfe116e8d220cfd4b15f14e741", 429 | "sha256:ebfd274dcd5133e0afae738e6d9da4323c3eb021b3e13052d8cbd0e457b1256e", 430 | "sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93", 431 | "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b" 432 | ], 433 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", 434 | "version": "==1.6.0" 435 | }, 436 | "mccabe": { 437 | "hashes": [ 438 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 439 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 440 | ], 441 | "version": "==0.6.1" 442 | }, 443 | "psycopg2-binary": { 444 | "hashes": [ 445 | "sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c", 446 | "sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67", 447 | "sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0", 448 | "sha256:15978a1fbd225583dd8cdaf37e67ccc278b5abecb4caf6b2d6b8e2b948e953f6", 449 | "sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db", 450 | "sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94", 451 | "sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52", 452 | "sha256:6422f2ff0919fd720195f64ffd8f924c1395d30f9a495f31e2392c2efafb5056", 453 | "sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b", 454 | "sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd", 455 | "sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550", 456 | "sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679", 457 | "sha256:89705f45ce07b2dfa806ee84439ec67c5d9a0ef20154e0e475e2b2ed392a5b83", 458 | "sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77", 459 | "sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2", 460 | "sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77", 461 | "sha256:a0eb43a07386c3f1f1ebb4dc7aafb13f67188eab896e7397aa1ee95a9c884eb2", 462 | "sha256:aaa4213c862f0ef00022751161df35804127b78adf4a2755b9f991a507e425fd", 463 | "sha256:ac0c682111fbf404525dfc0f18a8b5f11be52657d4f96e9fcb75daf4f3984859", 464 | "sha256:ad20d2eb875aaa1ea6d0f2916949f5c08a19c74d05b16ce6ebf6d24f2c9f75d1", 465 | "sha256:b4afc542c0ac0db720cf516dd20c0846f71c248d2b3d21013aa0d4ef9c71ca25", 466 | "sha256:b8a3715b3c4e604bcc94c90a825cd7f5635417453b253499664f784fc4da0152", 467 | "sha256:ba28584e6bca48c59eecbf7efb1576ca214b47f05194646b081717fa628dfddf", 468 | "sha256:ba381aec3a5dc29634f20692349d73f2d21f17653bda1decf0b52b11d694541f", 469 | "sha256:bd1be66dde2b82f80afb9459fc618216753f67109b859a361cf7def5c7968729", 470 | "sha256:c2507d796fca339c8fb03216364cca68d87e037c1f774977c8fc377627d01c71", 471 | "sha256:cec7e622ebc545dbb4564e483dd20e4e404da17ae07e06f3e780b2dacd5cee66", 472 | "sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4", 473 | "sha256:d1b4ab59e02d9008efe10ceabd0b31e79519da6fb67f7d8e8977118832d0f449", 474 | "sha256:d5227b229005a696cc67676e24c214740efd90b148de5733419ac9aaba3773da", 475 | "sha256:e1f57aa70d3f7cc6947fd88636a481638263ba04a742b4a37dd25c373e41491a", 476 | "sha256:e74a55f6bad0e7d3968399deb50f61f4db1926acf4a6d83beaaa7df986f48b1c", 477 | "sha256:e82aba2188b9ba309fd8e271702bd0d0fc9148ae3150532bbb474f4590039ffb", 478 | "sha256:ee69dad2c7155756ad114c02db06002f4cded41132cc51378e57aad79cc8e4f4", 479 | "sha256:f5ab93a2cb2d8338b1674be43b442a7f544a0971da062a5da774ed40587f18f5" 480 | ], 481 | "index": "pypi", 482 | "version": "==2.8.6" 483 | }, 484 | "pycodestyle": { 485 | "hashes": [ 486 | "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068", 487 | "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef" 488 | ], 489 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 490 | "version": "==2.7.0" 491 | }, 492 | "pylint": { 493 | "hashes": [ 494 | "sha256:2e1a0eb2e8ab41d6b5dbada87f066492bb1557b12b76c47c2ee8aa8a11186594", 495 | "sha256:8b838c8983ee1904b2de66cce9d0b96649a91901350e956d78f289c3bc87b48e" 496 | ], 497 | "index": "pypi", 498 | "version": "==2.9.6" 499 | }, 500 | "toml": { 501 | "hashes": [ 502 | "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", 503 | "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" 504 | ], 505 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", 506 | "version": "==0.10.2" 507 | }, 508 | "wrapt": { 509 | "hashes": [ 510 | "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" 511 | ], 512 | "version": "==1.12.1" 513 | } 514 | } 515 | } 516 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Like-Or-Hate 2 | [![Feature overview](./assets/images/Like-Or-Hate-Actions.gif)](https://like-or-hate.herokuapp.com/) 3 | ***Click on image for live link :^)*** 4 | 5 | Like-Or-Hate is an Instagram clone created by Benjamin Ash, Ananya Hans, Nathan Mount, and Christian Pettet. 6 | 7 | All users can browse a selection of user posts, comment on posts, and up/downvote posts. In Like-Or-Hate, voting has consequences, and if more users hate a post than like it, the post will be deleted! 8 | 9 | Any user can create a post, and upload any image to the website which will in-turn be hosted on an AWS (Amazon Web Services) S3 (Simple Storage Solution) bucket to offload server load. 10 | 11 | ## Database Diagram 12 | ![like-or-hate](./assets/images/Like-Or-Hate.png) 13 | 14 | ## Technologies and Packages Used 15 | ### Backend 16 | * Amazon Web Services (AWS) Simple Storage Solution (S3) 17 | * Python 18 | * Flask 19 | * WTForms 20 | * SQLAlchemy 21 | * Alembic 22 | * PostgreSQL 23 | * Heroku 24 | 25 | ### Frontend 26 | * HTML5 27 | * CSS3 28 | * JavaScript 29 | * React 30 | * Redux 31 | 32 | ## Current Features 33 | ### Posts 34 | * Any registered user can view all posts by the website's users. 35 | * AWS S3 is used to handle image hosting to reduce website load. 36 | * Registered users can upload an image and attach a caption to their post. 37 | * Post authors can edit their image's captions or delete their post. 38 | ### Comments 39 | * Registered users can view all comments for a post. 40 | * Registered users can comment on any post. 41 | * Comment authors can edit or delete their comments. 42 | ### Post Reactions 43 | * Registered users can like or hate a post. If there are more hates than likes, the post is democratically deleted! 44 | 45 | ## Challenges 46 | ### Personnel Shortage 47 | Shortly after feature completion, the development team for Like-Or-Hate was torn by deferral and successfully landing a development position. The members who finished and polished Like-Or-Hate did so without their lost partners' inputs and worked on a codebase that was less than ideally created. 48 | ### Implementing Likes and Hates 49 | Initially, likes and hates were tracked through an extension of the user model with a voted boolean for each post. An easier solution was found by creating a Post Reactions table to track each users' post reactions. The logic for finding if a user had reacted to a post was easier, and the solution to find the number of likes and hates with a SQL query was elegant and efficient. 50 | ```python 51 | try: 52 | post_reaction = PostReaction.query.filter_by(post_id=post_id,user_id=current_user.id,).one() 53 | except: 54 | post_reaction = PostReaction(user_id=current_user.id, post_id=post_id, 55 | reaction=reaction) 56 | db.session.add(post_reaction) 57 | db.session.commit() 58 | 59 | if str(post_reaction.reaction) != reaction: 60 | post_reaction.reaction = reaction 61 | db.session.commit() 62 | 63 | count_likes = PostReaction.query.filter_by(post_id=post_id, reaction=True).count() 64 | count_hates = PostReaction.query.filter_by(post_id=post_id, reaction=False).count() 65 | ``` 66 | 67 | ## Future Features 68 | * Implement comment reactions with democratic deletion. 69 | * Implement web sockets to chat with other users. 70 | * Implement a follows and tags feature to personalize a user's feed. 71 | * Personalized views for posts based on user and post locations. 72 | 73 | ## Getting started 74 | 75 | 1. Clone this repository (only this branch) 76 | 77 | ```bash 78 | git clone https://github.com/composerben/flask-group-project.git 79 | ``` 80 | 81 | 2. Install dependencies 82 | 83 | ```bash 84 | pipenv install --dev -r dev-requirements.txt && pipenv install -r requirements.txt 85 | ``` 86 | 87 | 3. Create a **.env** file based on the **.env.example** with proper settings for your 88 | development environment 89 | 4. Setup your PostgreSQL user, password and database and make sure it matches your **.env** file 90 | 91 | 5. Activate your pipenv, migrate your database, seed your database, and run the Flask/backend app. After starting the backend server, keep the terminal running in the background. 92 | 93 | ```bash 94 | pipenv shell 95 | flask db upgrade 96 | flask seed all 97 | flask run 98 | ``` 99 | 100 | 6. To run the React App in development, start a new terminal instance, navigate to the correct directory, install all dependencies, and start react. 101 | ```bash 102 | cd react-app/ 103 | npm install 104 | npm start 105 | ``` 106 | 107 | *** 108 | *IMPORTANT!* 109 | If you add any python dependencies to your pipfiles, you'll need to regenerate your requirements.txt before deployment. 110 | You can do this by running: 111 | 112 | ```bash 113 | pipenv lock -r > requirements.txt 114 | ``` 115 | 116 | *ALSO IMPORTANT!* 117 | psycopg2-binary MUST remain a dev dependency because you can't install it on apline-linux. 118 | There is a layer in the Dockerfile that will install psycopg2 (not binary) for us. 119 | *** 120 | 121 | ## Deploying to Heroku 122 | 123 | 1. Create a new project on Heroku 124 | 2. Under Resources click "Find more add-ons" and add the add on called "Heroku Postgres" 125 | 3. Run 126 | 127 | ```bash 128 | heroku login 129 | ``` 130 | 131 | 4. Login to the heroku container registry 132 | 133 | ```bash 134 | heroku container:login 135 | ``` 136 | 137 | 5. Update the `REACT_APP_BASE_URL` variable in the Dockerfile. 138 | This should be the full URL of your Heroku app: i.e. "https://flask-react-app.herokuapp.com" 139 | 6. [NON-M1 USERS] If not using an M1 Mac, continue with this step otherwise, skip ahead to the next step. Push your docker container to heroku from the root directory of your project. 140 | This will build the dockerfile and push the image to your heroku container registry 141 | 142 | ```bash 143 | heroku container:push web -a {NAME_OF_HEROKU_APP} 144 | ``` 145 | 6. [M1 USERS] If using an M1 Mac, continue with this step, otherwise, skip to the next step. Build image with linux platform for heroku servers. Tag your app with the url for your apps registry. Use docker to push the image to the Heroku container registry. 146 | ```bash 147 | docker buildx build --platform linux/amd64 -t {NAME_OF_HEROKU_APP} . 148 | docker tag {NAME_OF_HEROKU_APP} registry.heroku.com/{NAME_OF_HEROKU_APP}/web 149 | docker push registry.heroku.com/{NAME_OF_HEROKU_APP}/web 150 | ``` 151 | 152 | 7. Release your docker container to heroku 153 | 154 | ```bash 155 | heroku container:release web -a {NAME_OF_HEROKU_APP} 156 | ``` 157 | 158 | 8. set up your database: 159 | 160 | ```bash 161 | heroku run -a {NAME_OF_HEROKU_APP} flask db upgrade 162 | heroku run -a {NAME_OF_HEROKU_APP} flask seed all 163 | ``` 164 | 165 | 9. Under Settings find "Config Vars" and add any additional/secret .env variables. 166 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Flask, render_template, request, session, redirect 3 | from flask_cors import CORS 4 | from flask_migrate import Migrate 5 | from flask_wtf.csrf import CSRFProtect, generate_csrf 6 | from flask_login import LoginManager 7 | 8 | from .models import db, User, Post 9 | from .api.user_routes import user_routes 10 | from .api.auth_routes import auth_routes 11 | from .api.post_routes import post_routes 12 | from .api.post_reaction_routes import post_reaction_routes 13 | from .api.comment_routes import comment_routes 14 | 15 | from .seeds import seed_commands 16 | 17 | from .config import Config 18 | 19 | app = Flask(__name__) 20 | 21 | # Setup login manager 22 | login = LoginManager(app) 23 | login.login_view = 'auth.unauthorized' 24 | 25 | 26 | @login.user_loader 27 | def load_user(id): 28 | return User.query.get(int(id)) 29 | 30 | 31 | # Tell flask about our seed commands 32 | app.cli.add_command(seed_commands) 33 | 34 | app.config.from_object(Config) 35 | app.register_blueprint(user_routes, url_prefix='/api/users') 36 | app.register_blueprint(auth_routes, url_prefix='/api/auth') 37 | app.register_blueprint(post_routes, url_prefix='/api/posts') 38 | app.register_blueprint(post_reaction_routes, url_prefix='/api/post_reaction') 39 | app.register_blueprint(comment_routes, url_prefix='/api/comments') 40 | db.init_app(app) 41 | Migrate(app, db) 42 | 43 | # Application Security 44 | CORS(app) 45 | 46 | # Since we are deploying with Docker and Flask, 47 | # we won't be using a buildpack when we deploy to Heroku. 48 | # Therefore, we need to make sure that in production any 49 | # request made over http is redirected to https. 50 | # Well......... 51 | 52 | 53 | @app.before_request 54 | def https_redirect(): 55 | if os.environ.get('FLASK_ENV') == 'production': 56 | if request.headers.get('X-Forwarded-Proto') == 'http': 57 | url = request.url.replace('http://', 'https://', 1) 58 | code = 301 59 | return redirect(url, code=code) 60 | 61 | 62 | @app.after_request 63 | def inject_csrf_token(response): 64 | response.set_cookie('csrf_token', 65 | generate_csrf(), 66 | secure=True if os.environ.get( 67 | 'FLASK_ENV') == 'production' else False, 68 | samesite='Strict' if os.environ.get( 69 | 'FLASK_ENV') == 'production' else None, 70 | httponly=True) 71 | return response 72 | 73 | 74 | @app.route('/', defaults={'path': ''}) 75 | @app.route('/') 76 | def react_root(path): 77 | if path == 'favicon.ico': 78 | return app.send_static_file('favicon.ico') 79 | return app.send_static_file('index.html') 80 | -------------------------------------------------------------------------------- /app/api/auth_routes.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, request 2 | from app.models import User, db 3 | from app.forms import LoginForm 4 | from app.forms import SignUpForm 5 | from flask_login import current_user, login_user, logout_user, login_required 6 | 7 | auth_routes = Blueprint('auth', __name__) 8 | 9 | 10 | def validation_errors_to_error_messages(validation_errors): 11 | """ 12 | Simple function that turns the WTForms validation errors into a simple list 13 | """ 14 | errorMessages = [] 15 | for field in validation_errors: 16 | for error in validation_errors[field]: 17 | errorMessages.append(f"{field} : {error}") 18 | return errorMessages 19 | 20 | 21 | @auth_routes.route('/') 22 | def authenticate(): 23 | """ 24 | Authenticates a user. 25 | """ 26 | if current_user.is_authenticated: 27 | return current_user.to_dict() 28 | return {'errors': ['Unauthorized']} 29 | 30 | 31 | # POST /api/auth/login 32 | @auth_routes.route('/login', methods=['POST']) 33 | def login(): 34 | """ 35 | Logs a user in 36 | """ 37 | form = LoginForm() 38 | # Get the csrf_token from the request cookie and put it into the 39 | # form manually to validate_on_submit can be used 40 | form['csrf_token'].data = request.cookies['csrf_token'] 41 | if form.validate_on_submit(): 42 | # Add the user to the session, we are logged in! 43 | user = User.query.filter(User.email == form.data['email']).first() 44 | login_user(user) 45 | return user.to_dict() 46 | return {'errors': validation_errors_to_error_messages(form.errors)}, 401 47 | 48 | 49 | @auth_routes.route('/logout') 50 | def logout(): 51 | """ 52 | Logs a user out 53 | """ 54 | logout_user() 55 | return {'message': 'User logged out'} 56 | 57 | 58 | @auth_routes.route('/signup', methods=['POST']) 59 | def sign_up(): 60 | """ 61 | Creates a new user and logs them in 62 | """ 63 | form = SignUpForm() 64 | form['csrf_token'].data = request.cookies['csrf_token'] 65 | if form.validate_on_submit(): 66 | user = User( 67 | username=form.data['username'], 68 | email=form.data['email'], 69 | password=form.data['password'] 70 | ) 71 | db.session.add(user) 72 | db.session.commit() 73 | login_user(user) 74 | return user.to_dict() 75 | return {'errors': validation_errors_to_error_messages(form.errors)}, 401 76 | 77 | 78 | @auth_routes.route('/unauthorized') 79 | def unauthorized(): 80 | """ 81 | Returns unauthorized JSON when flask-login authentication fails 82 | """ 83 | return {'errors': ['Unauthorized']}, 401 84 | -------------------------------------------------------------------------------- /app/api/comment_routes.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, request, session 2 | from flask_login import login_required, current_user 3 | from app.models import db, Comment 4 | 5 | comment_routes = Blueprint("comments", __name__) 6 | 7 | 8 | # GET /api/comments 9 | @comment_routes.route("") 10 | @login_required 11 | def comments(): 12 | comments = Comment.query.all() 13 | return {"comments": [comment.to_dict() for comment in comments]} 14 | 15 | # DELETE /api/comments/:id 16 | 17 | 18 | @comment_routes.route("/", methods=["DELETE"]) 19 | @login_required 20 | def delete_comments(id): 21 | comment = Comment.query.get(id) 22 | db.session.delete(comment) 23 | db.session.commit() 24 | return comment.to_dict() 25 | 26 | 27 | # PATCH /api/comments/:id 28 | @comment_routes.route("/", methods=["PATCH"]) 29 | @login_required 30 | def edit_comment(id): 31 | comment = Comment.query.get(id) 32 | request_body = request.get_json() 33 | comment.body = request_body 34 | db.session.commit() 35 | return {'comment': comment.to_dict()} 36 | 37 | # POST /api/comments 38 | 39 | 40 | @comment_routes.route("", methods=["POST"]) 41 | @login_required 42 | def post_comment(): 43 | request_body = request.get_json() 44 | comment = Comment(user_id=current_user.id, 45 | post_id=request_body['postId'], body=request_body['body']) 46 | db.session.add(comment) 47 | db.session.commit() 48 | return {'comment': comment.to_dict()} 49 | -------------------------------------------------------------------------------- /app/api/post_reaction_routes.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, request, session 2 | from flask_login import login_required, current_user 3 | from app.models import db, PostReaction 4 | 5 | post_reaction_routes = Blueprint('post_reaction', __name__) 6 | 7 | 8 | @post_reaction_routes.route('//', 9 | methods=['POST']) 10 | @login_required 11 | def postLike(post_id, reaction): 12 | if reaction == "True": 13 | reaction = True 14 | else: 15 | reaction = False 16 | 17 | # diff = {'likes': 0, 'hates': 0} 18 | # check DB if a postreaction exists for user_id and post_id 19 | try: 20 | post_reaction = PostReaction.query.filter_by(post_id=post_id, 21 | user_id=current_user.id, 22 | ).one() 23 | except: 24 | post_reaction = PostReaction(user_id=current_user.id, post_id=post_id, 25 | reaction=reaction) 26 | db.session.add(post_reaction) 27 | db.session.commit() 28 | 29 | if str(post_reaction.reaction) != reaction: 30 | post_reaction.reaction = reaction 31 | db.session.commit() 32 | 33 | count_likes = PostReaction.query.filter_by(post_id=post_id, 34 | reaction=True).count() 35 | count_hates = PostReaction.query.filter_by(post_id=post_id, 36 | reaction=False).count() 37 | return { 38 | 'post_reaction': post_reaction.to_dict(), 39 | 'count_likes': count_likes, 40 | 'count_hates': count_hates, 41 | } 42 | -------------------------------------------------------------------------------- /app/api/post_routes.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, request, session 2 | from flask_login import login_required, current_user 3 | from app.models import db, Post 4 | from app.forms import PostForm 5 | from app.aws_helpers import ( 6 | upload_file_to_s3, allowed_file, get_unique_filename) 7 | 8 | post_routes = Blueprint("posts", __name__) 9 | 10 | 11 | # GET /api/posts 12 | @post_routes.route("") 13 | @login_required 14 | def posts(): 15 | posts = Post.query.all() 16 | return {"posts": [post.to_dict() for post in posts]} 17 | 18 | 19 | # POST /api/posts 20 | @post_routes.route("", methods=["POST"]) 21 | @login_required 22 | def post_posts(): 23 | if "image" not in request.files: 24 | return {"errors": "image required"}, 400 25 | 26 | image = request.files["image"] 27 | 28 | if not allowed_file(image.filename): 29 | return {"errors": "file type not permitted"}, 400 30 | 31 | image.filename = get_unique_filename(image.filename) 32 | 33 | upload = upload_file_to_s3(image) 34 | 35 | if "url" not in upload: 36 | # if no URL, error when uploading 37 | return upload, 400 38 | 39 | url = upload["url"] 40 | 41 | form = PostForm() 42 | form['csrf_token'].data = request.cookies['csrf_token'] 43 | if form.validate_on_submit(): 44 | post = Post( 45 | user_id=current_user.id, 46 | image_src=url, 47 | caption=form.data['caption'], 48 | ) 49 | db.session.add(post) 50 | db.session.commit() 51 | return post.to_dict() 52 | 53 | return {"errors": form.errors} 54 | 55 | 56 | # DELETE /api/posts/:id 57 | @post_routes.route("/", methods=["DELETE"]) 58 | @login_required 59 | def delete_posts(id): 60 | post = Post.query.get(id) 61 | db.session.delete(post) 62 | db.session.commit() 63 | return {'message': 'success'} 64 | 65 | 66 | # PATCH /api/posts/:id 67 | @post_routes.route("/", methods=["PATCH"]) 68 | @login_required 69 | def edit_post(id): 70 | post = Post.query.get(id) 71 | request_body = request.get_json() 72 | post.caption = request_body 73 | db.session.commit() 74 | return {'post': post.to_dict()} 75 | -------------------------------------------------------------------------------- /app/api/user_routes.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify 2 | from flask_login import login_required 3 | from app.models import User, Post 4 | 5 | user_routes = Blueprint('users', __name__) 6 | 7 | 8 | @user_routes.route('/') 9 | @login_required 10 | def users(): 11 | users = User.query.all() 12 | return {"users": [user.to_dict() for user in users]} 13 | 14 | 15 | @user_routes.route('/') 16 | @login_required 17 | def user(id): 18 | user = User.query.get_or_404(id) 19 | return user.to_dict() 20 | 21 | 22 | @user_routes.route('//posts') 23 | @login_required 24 | def userPosts(id): 25 | posts = Post.query.filter(Post.user_id == id).all() 26 | return {"posts": [post.to_dict() for post in posts]} 27 | -------------------------------------------------------------------------------- /app/aws_helpers.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import botocore 3 | import os 4 | import uuid 5 | 6 | BUCKET_NAME = os.environ.get("S3_BUCKET") 7 | S3_LOCATION = f"https://{BUCKET_NAME}.s3.amazonaws.com/" 8 | ALLOWED_EXTENSIONS = {"pdf", "png", "jpg", "jpeg", "gif"} 9 | 10 | s3 = boto3.client( 11 | "s3", 12 | aws_access_key_id=os.environ.get("S3_KEY"), 13 | aws_secret_access_key=os.environ.get("S3_SECRET") 14 | ) 15 | 16 | 17 | def allowed_file(filename): 18 | return "." in filename and \ 19 | filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS 20 | 21 | 22 | def get_unique_filename(filename): 23 | ext = filename.rsplit(".", 1)[1].lower() 24 | unique_filename = uuid.uuid4().hex 25 | return f"{unique_filename}.{ext}" 26 | 27 | 28 | def upload_file_to_s3(file, acl="public-read"): 29 | try: 30 | s3.upload_fileobj( 31 | file, 32 | BUCKET_NAME, 33 | file.filename, 34 | ExtraArgs={ 35 | "ACL": acl, 36 | "ContentType": file.content_type 37 | } 38 | ) 39 | except Exception as e: 40 | # in case the our s3 upload fails 41 | return {"errors": str(e)} 42 | 43 | return {"url": f"{S3_LOCATION}{file.filename}"} 44 | -------------------------------------------------------------------------------- /app/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | class Config: 4 | SECRET_KEY=os.environ.get('SECRET_KEY') 5 | SQLALCHEMY_TRACK_MODIFICATIONS=False 6 | SQLALCHEMY_DATABASE_URI=os.environ.get('DATABASE_URL') 7 | SQLALCHEMY_ECHO=True -------------------------------------------------------------------------------- /app/forms/__init__.py: -------------------------------------------------------------------------------- 1 | from .login_form import LoginForm 2 | from .signup_form import SignUpForm 3 | from .post_form import PostForm 4 | -------------------------------------------------------------------------------- /app/forms/login_form.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import StringField 3 | from wtforms.validators import DataRequired, Email, ValidationError 4 | from app.models import User 5 | 6 | 7 | def user_exists(form, field): 8 | email = field.data 9 | user = User.query.filter(User.email == email).first() 10 | if not user: 11 | raise ValidationError("Email provided not found.") 12 | 13 | 14 | def password_matches(form, field): 15 | password = field.data 16 | email = form.data['email'] 17 | user = User.query.filter(User.email == email).first() 18 | if not user: 19 | raise ValidationError("No such user exists.") 20 | if not user.check_password(password): 21 | raise ValidationError("Password was incorrect.") 22 | 23 | 24 | class LoginForm(FlaskForm): 25 | email = StringField('email', validators=[DataRequired(), user_exists]) 26 | password = StringField('password', validators=[ 27 | DataRequired(), password_matches]) 28 | -------------------------------------------------------------------------------- /app/forms/post_form.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import StringField, TextAreaField 3 | from wtforms.validators import DataRequired 4 | from app.models import Post 5 | 6 | 7 | class PostForm(FlaskForm): 8 | image = StringField("image", validators=[DataRequired()]) 9 | caption = TextAreaField("caption") 10 | -------------------------------------------------------------------------------- /app/forms/signup_form.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import StringField 3 | from wtforms.validators import DataRequired, Email, ValidationError 4 | from app.models import User 5 | 6 | 7 | def user_exists(form, field): 8 | email = field.data 9 | user = User.query.filter(User.email == email).first() 10 | if user: 11 | raise ValidationError("User is already registered.") 12 | 13 | 14 | class SignUpForm(FlaskForm): 15 | username = StringField('username', validators=[DataRequired()]) 16 | email = StringField('email', validators=[DataRequired(), user_exists]) 17 | password = StringField('password', validators=[DataRequired()]) 18 | -------------------------------------------------------------------------------- /app/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .db import db 2 | from .user import User 3 | from .post import Post 4 | from .post_reactions import PostReaction 5 | from .comment import Comment 6 | -------------------------------------------------------------------------------- /app/models/comment.py: -------------------------------------------------------------------------------- 1 | from .db import db 2 | 3 | #add commentReactions Later 4 | class Comment(db.Model): 5 | __tablename__ = 'comments' 6 | 7 | id = db.Column(db.Integer, primary_key=True) 8 | user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) 9 | post_id = db.Column(db.Integer, db.ForeignKey('posts.id', ondelete="CASCADE"), nullable=False) 10 | body = db.Column(db.String(1000), nullable=False) 11 | # come back to add a GPS location/coords 12 | 13 | user = db.relationship("User", back_populates="comment") 14 | post = db.relationship("Post", back_populates="comment") 15 | 16 | def to_dict(self): 17 | return { 18 | "id": self.id, 19 | "user_id": self.user_id, 20 | "post_id": self.post_id, 21 | "body": self.body, 22 | } 23 | -------------------------------------------------------------------------------- /app/models/db.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | db = SQLAlchemy() 3 | -------------------------------------------------------------------------------- /app/models/post.py: -------------------------------------------------------------------------------- 1 | from .db import db 2 | 3 | #add commentReactions Later 4 | class Post(db.Model): 5 | __tablename__ = 'posts' 6 | 7 | id = db.Column(db.Integer, primary_key=True) 8 | user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) 9 | image_src = db.Column(db.String(255), nullable=False, unique=True) 10 | caption = db.Column(db.String(255), nullable=True) 11 | # come back to add a GPS location/coords 12 | 13 | user = db.relationship("User", back_populates="post") 14 | comment = db.relationship( 15 | "Comment", back_populates="post", passive_deletes=True, cascade="all,delete-orphan") 16 | 17 | reactions = db.relationship( 18 | "PostReaction", back_populates="post", passive_deletes=True, cascade="all,delete-orphan") 19 | 20 | def get_reactions(self): 21 | """ 22 | Gets all reactions, returns in dictionary. 23 | """ 24 | return { 25 | 'likes': len([reaction.reaction for reaction in self.reactions if reaction.reaction is True]), 26 | 'hates': len([reaction.reaction for reaction in self.reactions if reaction.reaction is False]), 27 | } 28 | def get_comments(self): 29 | return [comment.to_dict() for comment in self.comment] 30 | 31 | def to_dict(self): 32 | return { 33 | "id": self.id, 34 | "user_id": self.user_id, 35 | "image_src": self.image_src, 36 | "caption": self.caption, 37 | "likes": self.get_reactions()["likes"], 38 | "hates": self.get_reactions()["hates"], 39 | "comment": self.get_comments() 40 | } 41 | -------------------------------------------------------------------------------- /app/models/post_reactions.py: -------------------------------------------------------------------------------- 1 | from .db import db 2 | 3 | 4 | class PostReaction(db.Model): 5 | __tablename__ = "post_reactions" 6 | 7 | user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False, 8 | primary_key=True) 9 | post_id = db.Column(db.Integer, db.ForeignKey('posts.id', ondelete="CASCADE"), nullable=False, 10 | primary_key=True) 11 | reaction = db.Column(db.Boolean, nullable=True) 12 | 13 | user = db.relationship("User", back_populates="post_reaction") 14 | post = db.relationship("Post", back_populates="reactions") 15 | 16 | def to_dict(self): 17 | return { 18 | "user_id": self.user_id, 19 | "post_id": self.post_id, 20 | "reaction": self.reaction 21 | } 22 | -------------------------------------------------------------------------------- /app/models/user.py: -------------------------------------------------------------------------------- 1 | from .db import db 2 | from werkzeug.security import generate_password_hash, check_password_hash 3 | from flask_login import UserMixin 4 | 5 | #add commentReactions Later 6 | class User(db.Model, UserMixin): 7 | __tablename__ = 'users' 8 | 9 | id = db.Column(db.Integer, primary_key=True) 10 | username = db.Column(db.String(40), nullable=False, unique=True) 11 | email = db.Column(db.String(255), nullable=False, unique=True) 12 | hashed_password = db.Column(db.String(255), nullable=False) 13 | 14 | @property 15 | def password(self): 16 | return self.hashed_password 17 | 18 | @password.setter 19 | def password(self, password): 20 | self.hashed_password = generate_password_hash(password) 21 | 22 | def check_password(self, password): 23 | return check_password_hash(self.password, password) 24 | 25 | post = db.relationship("Post", back_populates="user") 26 | post_reaction = db.relationship("PostReaction", back_populates="user") 27 | 28 | comment = db.relationship("Comment", back_populates="user") 29 | 30 | def to_dict(self): 31 | return { 32 | "id": self.id, 33 | "username": self.username, 34 | "email": self.email 35 | } 36 | -------------------------------------------------------------------------------- /app/seeds/__init__.py: -------------------------------------------------------------------------------- 1 | from flask.cli import AppGroup 2 | from .users import seed_users, undo_users 3 | from .post import seed_posts, undo_posts 4 | 5 | # Creates a seed group to hold our commands 6 | # So we can type `flask seed --help` 7 | seed_commands = AppGroup('seed') 8 | 9 | # Creates the `flask seed all` command 10 | 11 | 12 | @seed_commands.command('all') 13 | def seed(): 14 | seed_users() 15 | seed_posts() 16 | # Add other seed functions here 17 | 18 | 19 | # Creates the `flask seed undo` command 20 | 21 | 22 | @seed_commands.command('undo') 23 | def undo(): 24 | undo_users() 25 | undo_posts() 26 | # Add other undo functions here 27 | -------------------------------------------------------------------------------- /app/seeds/post.py: -------------------------------------------------------------------------------- 1 | from app.models import db, User, Post 2 | 3 | 4 | def seed_posts(): 5 | seed1 = Post(user_id="1", image_src="https://d.newsweek.com/en/full/520858/supermoon-moon-smartphone-photo-picture.jpg", 6 | caption="Wow! Can't believe this picture is real!") 7 | seed2 = Post(user_id="3", image_src="https://www.planetware.com/wpimages/2019/10/switzerland-in-pictures-most-beautiful-places-matterhorn.jpg", 8 | caption="One day I will climb you...one day...#themountainsarecalling") 9 | seed3 = Post(user_id="2", image_src="https://static.independent.co.uk/2021/04/05/12/SWNS_SOCIETY_PHOTOS_03.jpg?width=982&height=726&auto=webp&quality=75", 10 | caption="Who put this on my profile? AND WHY CAN'T I DELETE IT??? #thisisntfunny") 11 | seed4 = Post(user_id="10", image_src="https://eturbonews.com/wp-content/uploads/2020/10/itlay.jpeg", 12 | caption="Now that the world is opening up again I can't wait to get back to traveling and living my best life! Italy is definitely at the top of my list of places to go back to; I just can't get ENOUGH of Venice! #vinoplease") 13 | seed5 = Post(user_id="10", image_src="https://www.traveloffpath.com/wp-content/uploads/2021/05/WTTC-Gives-Seven-More-Countries-Safe-Travel-Stamp.jpg", 14 | caption="Throwback to my trip to Thailand a few years ago! Can't believe how long it's been (and can't believe they'll allow me back in after all those shenanigans!) #mumstheword") 15 | seed6 = Post(user_id="3", image_src="https://www.state.gov/wp-content/uploads/2018/11/Iceland-2109x1406.jpg", 16 | caption="Hands down one of the most stunning places on Earth. The weather is crazy, the nature is wild, the people are friendly, and the drinks aren't mild. #howdoimovehere") 17 | seed7 = Post(user_id="3", image_src="https://www.tehaleh.com/media/8070156/mt-rainier-paradise-1.png", 18 | caption="I swear I don't only post about mountains....okay, so what if I do?? They're better than people! #pnw4lyfe") 19 | seed8 = Post(user_id="4", image_src="https://upload.wikimedia.org/wikipedia/commons/b/b7/New_Zealand_vs_South_Africa_2006_Tri_Nations_Line_Out.JPG", 20 | caption="Such a shame Rugby isn't more well-followed in the States, but so glad to see the rise of a few teams here and there! Here's hoping it catches on at a national level! #rugbyisbetterthanfootball") 21 | seed9 = Post(user_id="5", image_src="https://images-na.ssl-images-amazon.com/images/I/81AlnMS8z7L._AC_SL1500_.jpg", 22 | caption="WHO WANTS TO PLAY WITH ME (probably no one cause you know I'd win) #mymetagameistoostrongforyou") 23 | seed10 = Post(user_id="6", image_src="https://centerforworldmusic.org/wp-content/uploads/2016/05/Hardanger-fiddle-1200x-618px.jpg", 24 | caption="My latest addition to my (already-too-large) collection of instruments, and I am IN LOVE...now if I could just learn to play it.... #mywalletisaheadofmybrain") 25 | seed11 = Post(user_id="7", image_src="https://cdn.wccftech.com/wp-content/uploads/2019/07/The-Witcher-3-UHD-Trees-2060x1159.jpeg", 26 | caption="Made a new mod to improve the trees in Witcher 3. When was the last time I saw a real tree, you ask?...yeah, maybe don't ask. #bluelightistheonlylight") 27 | seed12 = Post(user_id="8", image_src="https://gymcrafter.com/wp-content/uploads/2018/06/Squat-Rack-large.jpg", 28 | caption="New PR, PRIMAL. SWOLE. #getit") 29 | seed13 = Post(user_id="9", image_src="https://images.fineartamerica.com/images/artworkimages/mediumlarge/1/pirate-ship-sunset-daniel-eskridge.jpg", 30 | caption="Yo ho, yo ho... #itsthelifeforme") 31 | seed14 = Post(user_id="10", image_src="https://cdn.britannica.com/85/117385-050-9D7D5132/Copenhagen-Den.jpg", 32 | caption="Copenhagen has been on my bucket list for so long, and I'm so excited to have FINALLY booked a ticket to go in December. Can't wait to eat at Noma!!! #blessed") 33 | seed15 = Post(user_id="1", image_src="http://b50ym1n8ryw31pmkr4671ui1c64-wpengine.netdna-ssl.com/wp-content/blogs.dir/11/files/2015/06/ShootingStar_Donated_JordanReed.jpg", 34 | caption="Also can't believe this is real!") 35 | seed16 = Post(user_id="1", image_src="https://www.thephotoargus.com/wp-content/uploads/2018/02/longexposure07.jpg", 36 | caption="Wait...is ANYthing real? (I'm not real either, I'm just a demo)") 37 | seed17 = Post(user_id="4", image_src="https://primalpeak.com/wp-content/uploads/2019/09/image3-1.jpg", 38 | caption="The 2nd worst sport, but I at least like Ronaldo's thoughts on Coca-Cola #drinkwater #footballisbetterthansoccer") 39 | seed18 = Post(user_id="7", image_src="https://www.dexerto.com/wp-content/uploads/2021/06/15/things-you-may-have-missed-in-the-legend-of-zelda-breath-of-the-wild-2-e3-2021-trailer.jpg", 40 | caption="BOTW2 HYPE TRAIN #zombieganonisbestganon") 41 | seed19 = Post(user_id="5", image_src="https://techcrunch.com/wp-content/uploads/2015/04/codecode.jpg", 42 | caption="EatSleepCodeRepeat EatSleepCodeRepeat EatSleepCodeRepeat EatSleepCodeRepeat EatSleepCodeRepeat EatSleepCodeRepeat EatSleepCodeRepeat EatSleepCodeRepeat EatSleepCodeRepeat EatSleepCodeRepeat EatSleepCodeRepeat PassOutGoOutsideSmashComputerCry") 43 | seed20 = Post(user_id="8", image_src="https://hips.hearstapps.com/hmg-prod.s3.amazonaws.com/images/basic-exercises-are-best-royalty-free-image-541285800-1561581654.jpg", 44 | caption="What even is crossfit? My back just hurts looking at this. #pullupsfromadeadhangorbust") 45 | seed21 = Post(user_id="6", image_src="https://www.wearegreenbay.com/wp-content/uploads/sites/70/2018/03/RT_MG_027420copy_1521600871224.jpg_37874469_ver1.0.jpg?w=2560&h=1440&crop=1", 46 | caption="Great view of the first concert at Carnegie post-pandemic. God it's good to be back!") 47 | seed22 = Post(user_id="9", image_src="https://www.cbc.ca/kidscbc2/content/the_feed/_848/newpiratesheader.jpg", 48 | caption="I would call this cosplay, but this is honestly how I go to work in the morning. #drinkupmehearties") 49 | 50 | db.session.add(seed1) 51 | db.session.add(seed2) 52 | db.session.add(seed3) 53 | db.session.add(seed4) 54 | db.session.add(seed5) 55 | db.session.add(seed6) 56 | db.session.add(seed7) 57 | db.session.add(seed8) 58 | db.session.add(seed9) 59 | db.session.add(seed10) 60 | db.session.add(seed11) 61 | db.session.add(seed12) 62 | db.session.add(seed13) 63 | db.session.add(seed14) 64 | db.session.add(seed15) 65 | db.session.add(seed16) 66 | db.session.add(seed17) 67 | db.session.add(seed18) 68 | db.session.add(seed19) 69 | db.session.add(seed20) 70 | db.session.add(seed21) 71 | db.session.add(seed22) 72 | 73 | db.session.commit() 74 | 75 | 76 | def undo_posts(): 77 | db.session.execute('TRUNCATE posts RESTART IDENTITY CASCADE;') 78 | db.session.commit() 79 | -------------------------------------------------------------------------------- /app/seeds/users.py: -------------------------------------------------------------------------------- 1 | from app.models import db, User 2 | 3 | # Adds a demo user, you can add other users here if you want 4 | 5 | 6 | def seed_users(): 7 | 8 | demo = User(username='Demo', email='demo@aa.io', 9 | password='password') 10 | demoBen = User(username='Ben', email='demoBen@aa.io', 11 | password='password') 12 | natureFan = User(username="NatureFan", 13 | email="naturefan@gmail.com", password="password") 14 | sportsFan = User(username="SportsFan", 15 | email="sportsfan@gmail.com", password="password") 16 | superNerd = User(username="SuperNerd", 17 | email="supernerd@gmail.com", password="password") 18 | musicIsLife = User(username="MusicIsLife", 19 | email="musicislife@gmail.com", password="password") 20 | gamer = User(username="gamer", email="gamer@gmail.com", 21 | password="password") 22 | gymRat = User(username="GymRat", email="gymrat@gmail.com", 23 | password="password") 24 | piratesLife = User(username="PiratesLife", 25 | email="pirateslife@gmail.com", password="password") 26 | travelBug = User(username="TravelBug", 27 | email="travelbug@gmail.com", password="password") 28 | 29 | db.session.add(demo) 30 | db.session.add(demoBen) 31 | db.session.add(natureFan) 32 | db.session.add(sportsFan) 33 | db.session.add(superNerd) 34 | db.session.add(musicIsLife) 35 | db.session.add(gamer) 36 | db.session.add(gymRat) 37 | db.session.add(piratesLife) 38 | db.session.add(travelBug) 39 | 40 | db.session.commit() 41 | 42 | # Uses a raw SQL query to TRUNCATE the users table. 43 | # SQLAlchemy doesn't have a built in function to do this 44 | # TRUNCATE Removes all the data from the table, and resets 45 | # the auto incrementing primary key 46 | 47 | 48 | def undo_users(): 49 | db.session.execute('TRUNCATE users RESTART IDENTITY CASCADE;') 50 | db.session.commit() 51 | -------------------------------------------------------------------------------- /assets/images/Like-Or-Hate-Actions.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composerben/flask-group-project/f6b14c3602b26e9585bb92355eb537d26b26200a/assets/images/Like-Or-Hate-Actions.gif -------------------------------------------------------------------------------- /assets/images/Like-Or-Hate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composerben/flask-group-project/f6b14c3602b26e9585bb92355eb537d26b26200a/assets/images/Like-Or-Hate.png -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | psycopg2-binary==2.8.6 2 | -------------------------------------------------------------------------------- /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 = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d%%(second).2d_%%(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( 26 | 'sqlalchemy.url', 27 | str(current_app.extensions['migrate'].db.engine.url).replace('%', '%%')) 28 | target_metadata = current_app.extensions['migrate'].db.metadata 29 | 30 | # other values from the config, defined by the needs of env.py, 31 | # can be acquired: 32 | # my_important_option = config.get_main_option("my_important_option") 33 | # ... etc. 34 | 35 | 36 | def run_migrations_offline(): 37 | """Run migrations in 'offline' mode. 38 | 39 | This configures the context with just a URL 40 | and not an Engine, though an Engine is acceptable 41 | here as well. By skipping the Engine creation 42 | we don't even need a DBAPI to be available. 43 | 44 | Calls to context.execute() here emit the given string to the 45 | script output. 46 | 47 | """ 48 | url = config.get_main_option("sqlalchemy.url") 49 | context.configure( 50 | url=url, target_metadata=target_metadata, literal_binds=True 51 | ) 52 | 53 | with context.begin_transaction(): 54 | context.run_migrations() 55 | 56 | 57 | def run_migrations_online(): 58 | """Run migrations in 'online' mode. 59 | 60 | In this scenario we need to create an Engine 61 | and associate a connection with the context. 62 | 63 | """ 64 | 65 | # this callback is used to prevent an auto-migration from being generated 66 | # when there are no changes to the schema 67 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html 68 | def process_revision_directives(context, revision, directives): 69 | if getattr(config.cmd_opts, 'autogenerate', False): 70 | script = directives[0] 71 | if script.upgrade_ops.is_empty(): 72 | directives[:] = [] 73 | logger.info('No changes in schema detected.') 74 | 75 | connectable = engine_from_config( 76 | config.get_section(config.config_ini_section), 77 | prefix='sqlalchemy.', 78 | poolclass=pool.NullPool, 79 | ) 80 | 81 | with connectable.connect() as connection: 82 | context.configure( 83 | connection=connection, 84 | target_metadata=target_metadata, 85 | process_revision_directives=process_revision_directives, 86 | **current_app.extensions['migrate'].configure_args 87 | ) 88 | 89 | with context.begin_transaction(): 90 | context.run_migrations() 91 | 92 | 93 | if context.is_offline_mode(): 94 | run_migrations_offline() 95 | else: 96 | run_migrations_online() 97 | -------------------------------------------------------------------------------- /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/20201120_150602_create_users_table.py: -------------------------------------------------------------------------------- 1 | """create_users_table 2 | 3 | Revision ID: ffdc0a98111c 4 | Revises: 5 | Create Date: 2020-11-20 15:06:02.230689 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'ffdc0a98111c' 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('users', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('username', sa.String(length=40), nullable=False), 24 | sa.Column('email', sa.String(length=255), nullable=False), 25 | sa.Column('hashed_password', sa.String(length=255), nullable=False), 26 | sa.PrimaryKeyConstraint('id'), 27 | sa.UniqueConstraint('email'), 28 | sa.UniqueConstraint('username') 29 | ) 30 | # ### end Alembic commands ###qqqqqqqqq 31 | 32 | 33 | def downgrade(): 34 | # ### commands auto generated by Alembic - please adjust! ### 35 | op.drop_table('users') 36 | # ### end Alembic commands ### 37 | -------------------------------------------------------------------------------- /migrations/versions/20210621_133159_test_migration.py: -------------------------------------------------------------------------------- 1 | """test migration 2 | 3 | Revision ID: f8f2b3884340 4 | Revises: ffdc0a98111c 5 | Create Date: 2021-06-21 13:31:59.139357 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'f8f2b3884340' 14 | down_revision = 'ffdc0a98111c' 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('posts', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('user_id', sa.Integer(), nullable=False), 24 | sa.Column('image_src', sa.String(length=255), nullable=False), 25 | sa.Column('num_of_likes', sa.Integer(), nullable=True), 26 | sa.Column('num_of_hates', sa.Integer(), nullable=True), 27 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), 28 | sa.PrimaryKeyConstraint('id'), 29 | sa.UniqueConstraint('image_src') 30 | ) 31 | # ### end Alembic commands ### 32 | 33 | 34 | def downgrade(): 35 | # ### commands auto generated by Alembic - please adjust! ### 36 | op.drop_table('posts') 37 | # ### end Alembic commands ### 38 | -------------------------------------------------------------------------------- /migrations/versions/20210621_162239_changed_caption_to_new_caption.py: -------------------------------------------------------------------------------- 1 | """changed caption to newcaption 2 | 3 | Revision ID: 843def12575b 4 | Revises: f8f2b3884340 5 | Create Date: 2021-06-21 16:22:39.979758 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '843def12575b' 14 | down_revision = 'f8f2b3884340' 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('posts', sa.Column('caption', sa.String(length=255), nullable=True)) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_column('posts', 'caption') 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /migrations/versions/20210623_091822_post_reactions.py: -------------------------------------------------------------------------------- 1 | """post_reactions 2 | 3 | Revision ID: e8d1f27e6a3a 4 | Revises: 843def12575b 5 | Create Date: 2021-06-23 09:18:22.335356 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'e8d1f27e6a3a' 14 | down_revision = '843def12575b' 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('post_reactions', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('user_id', sa.Integer(), nullable=False), 24 | sa.Column('post_id', sa.Integer(), nullable=False), 25 | sa.ForeignKeyConstraint(['post_id'], ['posts.id'], ), 26 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), 27 | sa.PrimaryKeyConstraint('id') 28 | ) 29 | # ### end Alembic commands ### 30 | 31 | 32 | def downgrade(): 33 | # ### commands auto generated by Alembic - please adjust! ### 34 | op.drop_table('post_reactions') 35 | # ### end Alembic commands ### 36 | -------------------------------------------------------------------------------- /migrations/versions/20210623_124208_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 02f37635144e 4 | Revises: e8d1f27e6a3a 5 | Create Date: 2021-06-23 12:42:08.090501 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '02f37635144e' 14 | down_revision = 'e8d1f27e6a3a' 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('post_reactions', sa.Column('_reaction', sa.Boolean(), nullable=True)) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_column('post_reactions', '_reaction') 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /migrations/versions/20210623_141801_testing_removal_of_post_reactions.py: -------------------------------------------------------------------------------- 1 | """testing removal of post_reactions 2 | 3 | Revision ID: 1c2eab9014ec 4 | Revises: 02f37635144e 5 | Create Date: 2021-06-23 14:18:01.564901 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '1c2eab9014ec' 14 | down_revision = '02f37635144e' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.drop_table('post_reactions') 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.create_table('post_reactions', 28 | sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), 29 | sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), 30 | sa.Column('post_id', sa.INTEGER(), autoincrement=False, nullable=False), 31 | sa.Column('_reaction', sa.BOOLEAN(), autoincrement=False, nullable=True), 32 | sa.ForeignKeyConstraint(['post_id'], ['posts.id'], name='post_reactions_post_id_fkey'), 33 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], name='post_reactions_user_id_fkey'), 34 | sa.PrimaryKeyConstraint('id', name='post_reactions_pkey') 35 | ) 36 | # ### end Alembic commands ### 37 | -------------------------------------------------------------------------------- /migrations/versions/20210623_141909_re_adding_the_post_reactions_migration_.py: -------------------------------------------------------------------------------- 1 | """re-adding the post_reactions migration/model 2 | 3 | Revision ID: 63bafc06e66d 4 | Revises: 1c2eab9014ec 5 | Create Date: 2021-06-23 14:19:09.251400 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '63bafc06e66d' 14 | down_revision = '1c2eab9014ec' 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('post_reactions', 22 | sa.Column('user_id', sa.Integer(), nullable=False), 23 | sa.Column('post_id', sa.Integer(), nullable=False), 24 | sa.Column('_reaction', sa.Boolean(), nullable=True), 25 | sa.ForeignKeyConstraint(['post_id'], ['posts.id'], ), 26 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), 27 | sa.PrimaryKeyConstraint('user_id', 'post_id') 28 | ) 29 | # ### end Alembic commands ### 30 | 31 | 32 | def downgrade(): 33 | # ### commands auto generated by Alembic - please adjust! ### 34 | op.drop_table('post_reactions') 35 | # ### end Alembic commands ### 36 | -------------------------------------------------------------------------------- /migrations/versions/20210623_142843_removed_unnecessary_columns_moving_to_a_.py: -------------------------------------------------------------------------------- 1 | """Removed unnecessary columns, moving to a SQL query. 2 | 3 | Revision ID: e4338c095afb 4 | Revises: 63bafc06e66d 5 | Create Date: 2021-06-23 14:28:43.770753 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'e4338c095afb' 14 | down_revision = '63bafc06e66d' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.drop_column('posts', 'num_of_likes') 22 | op.drop_column('posts', 'num_of_hates') 23 | # ### end Alembic commands ### 24 | 25 | 26 | def downgrade(): 27 | # ### commands auto generated by Alembic - please adjust! ### 28 | op.add_column('posts', sa.Column('num_of_hates', sa.INTEGER(), autoincrement=False, nullable=True)) 29 | op.add_column('posts', sa.Column('num_of_likes', sa.INTEGER(), autoincrement=False, nullable=True)) 30 | # ### end Alembic commands ### 31 | -------------------------------------------------------------------------------- /migrations/versions/20210624_164710_fix_migration.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 391b24b33343 4 | Revises: e4338c095afb 5 | Create Date: 2021-06-24 16:47:10.434392 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '391b24b33343' 14 | down_revision = 'e4338c095afb' 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('comments', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('user_id', sa.Integer(), nullable=False), 24 | sa.Column('post_id', sa.Integer(), nullable=False), 25 | sa.Column('body', sa.String(length=1000), nullable=False), 26 | sa.ForeignKeyConstraint(['post_id'], ['posts.id'], ), 27 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), 28 | sa.PrimaryKeyConstraint('id') 29 | ) 30 | op.add_column('post_reactions', sa.Column('reaction', sa.Boolean(), nullable=True)) 31 | op.drop_constraint('post_reactions_post_id_fkey', 'post_reactions', type_='foreignkey') 32 | op.create_foreign_key(None, 'post_reactions', 'posts', ['post_id'], ['id'], ondelete='CASCADE') 33 | op.drop_column('post_reactions', '_reaction') 34 | # ### end Alembic commands ### 35 | 36 | 37 | def downgrade(): 38 | # ### commands auto generated by Alembic - please adjust! ### 39 | op.add_column('post_reactions', sa.Column('_reaction', sa.BOOLEAN(), autoincrement=False, nullable=True)) 40 | op.drop_constraint(None, 'post_reactions', type_='foreignkey') 41 | op.create_foreign_key('post_reactions_post_id_fkey', 'post_reactions', 'posts', ['post_id'], ['id']) 42 | op.drop_column('post_reactions', 'reaction') 43 | op.drop_table('comments') 44 | # ### end Alembic commands ### 45 | -------------------------------------------------------------------------------- /migrations/versions/20210815_211921_cascading_deletes_for_posts_with_.py: -------------------------------------------------------------------------------- 1 | """Cascading deletes for posts with comments. 2 | 3 | Revision ID: 3f2406ea56f7 4 | Revises: 391b24b33343 5 | Create Date: 2021-08-15 21:19:21.763411 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '3f2406ea56f7' 14 | down_revision = '391b24b33343' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.drop_constraint('comments_post_id_fkey', 'comments', type_='foreignkey') 22 | op.create_foreign_key(None, 'comments', 'posts', ['post_id'], ['id'], ondelete='CASCADE') 23 | # ### end Alembic commands ### 24 | 25 | 26 | def downgrade(): 27 | # ### commands auto generated by Alembic - please adjust! ### 28 | op.drop_constraint(None, 'comments', type_='foreignkey') 29 | op.create_foreign_key('comments_post_id_fkey', 'comments', 'posts', ['post_id'], ['id']) 30 | # ### end Alembic commands ### 31 | -------------------------------------------------------------------------------- /react-app/.env.example: -------------------------------------------------------------------------------- 1 | REACT_APP_BASE_URL=http://localhost:5000 2 | -------------------------------------------------------------------------------- /react-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /react-app/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | Your React App will live here. While is development, run this application from this location using `npm start`. 4 | 5 | 6 | No environment variables are needed to run this application in development, but be sure to set the REACT_APP_BASE_URL environment variable in heroku! 7 | 8 | This app will be automatically built when you deploy to heroku, please see the `heroku-postbuild` script in your `express.js` applications `package.json` to see how this works. 9 | -------------------------------------------------------------------------------- /react-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.3.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "http-proxy-middleware": "^1.0.5", 10 | "react": "^17.0.0", 11 | "react-dom": "^17.0.0", 12 | "react-redux": "^7.2.4", 13 | "react-router-dom": "^5.2.0", 14 | "react-scripts": "3.4.3", 15 | "redux": "^4.1.0", 16 | "redux-logger": "^3.0.6", 17 | "redux-thunk": "^2.3.0" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test", 23 | "eject": "react-scripts eject" 24 | }, 25 | "eslintConfig": { 26 | "extends": "react-app" 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | }, 40 | "proxy": "http://localhost:5000" 41 | } 42 | -------------------------------------------------------------------------------- /react-app/public/favicon.backup.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composerben/flask-group-project/f6b14c3602b26e9585bb92355eb537d26b26200a/react-app/public/favicon.backup.ico -------------------------------------------------------------------------------- /react-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composerben/flask-group-project/f6b14c3602b26e9585bb92355eb537d26b26200a/react-app/public/favicon.ico -------------------------------------------------------------------------------- /react-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 23 | Like OR Hate 24 | 25 | 26 |
27 | 28 | 29 | -------------------------------------------------------------------------------- /react-app/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { BrowserRouter, Redirect, Route, Switch } from "react-router-dom"; 3 | import LoginForm from "./components/auth/LoginForm"; 4 | import SignUpForm from "./components/auth/SignUpForm"; 5 | import NavBar from "./components/NavBar/NavBar"; 6 | import ProtectedRoute from "./components/auth/ProtectedRoute"; 7 | import PostShow from "./components/PostShow/PostShow"; 8 | import User from "./components/User/User"; 9 | import PostForm from "./components/PostForm/PostForm"; 10 | import Footer from "./components/Footer"; 11 | import SplashPage from "./components/SplashPage"; 12 | import { authenticate } from "./store/session"; 13 | import { useDispatch } from "react-redux"; 14 | import Page404 from "./components/404Page"; 15 | 16 | function App() { 17 | const dispatch = useDispatch(); 18 | const [loaded, setLoaded] = useState(false); 19 | 20 | useEffect(() => { 21 | (async () => { 22 | await dispatch(authenticate()); 23 | setLoaded(true); 24 | })(); 25 | }, [dispatch]); 26 | 27 | if (!loaded) { 28 | return null; 29 | } 30 | 31 | return ( 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 |