├── .gitignore ├── .vscode └── settings.json ├── Pipfile ├── Pipfile.lock ├── React-notes.md ├── Readme.md ├── accounts ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ └── __init__.py ├── models.py ├── tests.py └── views.py ├── db.sqlite3 ├── manage.py ├── profiles ├── __init__.py ├── admin.py ├── api │ ├── __init__.py │ ├── urls.py │ └── views.py ├── apps.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20200108_2100.py │ └── __init__.py ├── models.py ├── serializers.py ├── tests.py ├── urls.py └── views.py ├── static ├── css │ ├── main.50519c1c.chunk.css │ └── main.50519c1c.chunk.css.map ├── js │ ├── 2.437034a0.chunk.js │ ├── 2.437034a0.chunk.js.LICENSE │ ├── 2.437034a0.chunk.js.map │ ├── main.f30308ad.chunk.js │ ├── main.f30308ad.chunk.js.map │ ├── runtime-main.76e46f0d.js │ └── runtime-main.76e46f0d.js.map └── media │ └── logo.5d5d9eef.svg ├── templates ├── accounts │ └── auth.html ├── base.html ├── components │ ├── footer.html │ ├── form.html │ └── navbar.html ├── pages │ ├── feed.html │ └── home.html ├── profiles │ ├── detail.html │ └── form.html ├── react.html ├── react │ ├── base_embed.html │ ├── css.html │ └── js.html ├── react_via_dj.html └── tweets │ ├── detail.html │ └── list.html ├── todo.md ├── tweetme.code-workspace ├── tweetme2-web ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt └── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ ├── lookup │ ├── components.js │ └── index.js │ ├── profiles │ ├── badge.js │ ├── components.js │ ├── index.js │ ├── lookup.js │ └── utils.js │ ├── serviceWorker.js │ ├── setupTests.js │ └── tweets │ ├── buttons.js │ ├── components.js │ ├── create.js │ ├── detail.js │ ├── feed.js │ ├── index.js │ ├── list.js │ └── lookup.js ├── tweetme2 ├── __init__.py ├── rest_api │ ├── __init__.py │ └── dev.py ├── settings.py ├── urls.py └── wsgi.py └── tweets ├── __init__.py ├── admin.py ├── api ├── __init__.py ├── urls.py └── views.py ├── apps.py ├── forms.py ├── migrations ├── 0001_initial.py ├── 0002_auto_20191215_2304.py ├── 0003_tweet_user.py ├── 0004_auto_20191217_1953.py ├── 0005_tweet_parent.py ├── 0006_auto_20200108_2040.py └── __init__.py ├── models.py ├── serializers.py ├── tests.py └── views.py /.gitignore: -------------------------------------------------------------------------------- 1 | static-root/ 2 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | /node_modules 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | 27 | node_modules/ 28 | # local project items 29 | parse.py 30 | 31 | # Byte-compiled / optimized / DLL files 32 | __pycache__/ 33 | *.py[cod] 34 | *$py.class 35 | 36 | # C extensions 37 | *.so 38 | 39 | # Distribution / packaging 40 | .Python 41 | build/ 42 | develop-eggs/ 43 | dist/ 44 | downloads/ 45 | eggs/ 46 | .eggs/ 47 | lib/ 48 | lib64/ 49 | parts/ 50 | sdist/ 51 | var/ 52 | wheels/ 53 | pip-wheel-metadata/ 54 | share/python-wheels/ 55 | *.egg-info/ 56 | .installed.cfg 57 | *.egg 58 | MANIFEST 59 | 60 | # PyInstaller 61 | # Usually these files are written by a python script from a template 62 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 63 | *.manifest 64 | *.spec 65 | 66 | # Installer logs 67 | pip-log.txt 68 | pip-delete-this-directory.txt 69 | 70 | # Unit test / coverage reports 71 | htmlcov/ 72 | .tox/ 73 | .nox/ 74 | .coverage 75 | .coverage.* 76 | .cache 77 | nosetests.xml 78 | coverage.xml 79 | *.cover 80 | *.py,cover 81 | .hypothesis/ 82 | .pytest_cache/ 83 | 84 | # Translations 85 | *.mo 86 | *.pot 87 | 88 | # Django stuff: 89 | *.log 90 | local_settings.py 91 | db.sqlite3 92 | db.sqlite3-journal 93 | 94 | # Flask stuff: 95 | instance/ 96 | .webassets-cache 97 | 98 | # Scrapy stuff: 99 | .scrapy 100 | 101 | # Sphinx documentation 102 | docs/_build/ 103 | 104 | # PyBuilder 105 | target/ 106 | 107 | # Jupyter Notebook 108 | .ipynb_checkpoints 109 | 110 | # IPython 111 | profile_default/ 112 | ipython_config.py 113 | 114 | # pyenv 115 | .python-version 116 | 117 | # pipenv 118 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 119 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 120 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 121 | # install all needed dependencies. 122 | #Pipfile.lock 123 | 124 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 125 | __pypackages__/ 126 | 127 | # Celery stuff 128 | celerybeat-schedule 129 | celerybeat.pid 130 | 131 | # SageMath parsed files 132 | *.sage.py 133 | 134 | # Environments 135 | .env 136 | .venv 137 | env/ 138 | venv/ 139 | ENV/ 140 | env.bak/ 141 | venv.bak/ 142 | 143 | # Spyder project settings 144 | .spyderproject 145 | .spyproject 146 | 147 | # Rope project settings 148 | .ropeproject 149 | 150 | # mkdocs documentation 151 | /site 152 | 153 | # mypy 154 | .mypy_cache/ 155 | .dmypy.json 156 | dmypy.json 157 | 158 | # Pyre type checker 159 | .pyre/ -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "/Users/cfe/.local/share/virtualenvs/tweetme2-LFH35Wvl/bin/python" 3 | } -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [packages] 7 | django = "==2.2" 8 | djangorestframework = "*" 9 | django-cors-headers = "*" 10 | 11 | [requires] 12 | python_version = "3.6" 13 | 14 | [dev-packages] 15 | pylint = "*" 16 | GitPython = "*" 17 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "14653c595cd0f28b74035c0291e8de22de3e8370bbfbf69a2d401cda1828a8fc" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.6" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "django": { 20 | "hashes": [ 21 | "sha256:7c3543e4fb070d14e10926189a7fcf42ba919263b7473dceaefce34d54e8a119", 22 | "sha256:a2814bffd1f007805b19194eb0b9a331933b82bd5da1c3ba3d7b7ba16e06dc4b" 23 | ], 24 | "index": "pypi", 25 | "version": "==2.2" 26 | }, 27 | "django-cors-headers": { 28 | "hashes": [ 29 | "sha256:84933651fbbde8f2bc084bef2f077b79db1ec1389432f21dd661eaae6b3d6a95", 30 | "sha256:a8b2772582e8025412f4d4b54b617d8b707076ffd53a2b961bd24f10ec207a7c" 31 | ], 32 | "index": "pypi", 33 | "version": "==3.2.0" 34 | }, 35 | "djangorestframework": { 36 | "hashes": [ 37 | "sha256:05809fc66e1c997fd9a32ea5730d9f4ba28b109b9da71fccfa5ff241201fd0a4", 38 | "sha256:e782087823c47a26826ee5b6fa0c542968219263fb3976ec3c31edab23a4001f" 39 | ], 40 | "index": "pypi", 41 | "version": "==3.11.0" 42 | }, 43 | "pytz": { 44 | "hashes": [ 45 | "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d", 46 | "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be" 47 | ], 48 | "version": "==2019.3" 49 | }, 50 | "sqlparse": { 51 | "hashes": [ 52 | "sha256:40afe6b8d4b1117e7dff5504d7a8ce07d9a1b15aeeade8a2d10f130a834f8177", 53 | "sha256:7c3dca29c022744e95b547e867cee89f4fce4373f3549ccd8797d8eb52cdb873" 54 | ], 55 | "version": "==0.3.0" 56 | } 57 | }, 58 | "develop": { 59 | "astroid": { 60 | "hashes": [ 61 | "sha256:71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a", 62 | "sha256:840947ebfa8b58f318d42301cf8c0a20fd794a33b61cc4638e28e9e61ba32f42" 63 | ], 64 | "version": "==2.3.3" 65 | }, 66 | "gitdb2": { 67 | "hashes": [ 68 | "sha256:1b6df1433567a51a4a9c1a5a0de977aa351a405cc56d7d35f3388bad1f630350", 69 | "sha256:96bbb507d765a7f51eb802554a9cfe194a174582f772e0d89f4e87288c288b7b" 70 | ], 71 | "version": "==2.0.6" 72 | }, 73 | "gitpython": { 74 | "hashes": [ 75 | "sha256:9c2398ffc3dcb3c40b27324b316f08a4f93ad646d5a6328cafbb871aa79f5e42", 76 | "sha256:c155c6a2653593ccb300462f6ef533583a913e17857cfef8fc617c246b6dc245" 77 | ], 78 | "index": "pypi", 79 | "version": "==3.0.5" 80 | }, 81 | "isort": { 82 | "hashes": [ 83 | "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", 84 | "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" 85 | ], 86 | "version": "==4.3.21" 87 | }, 88 | "lazy-object-proxy": { 89 | "hashes": [ 90 | "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d", 91 | "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449", 92 | "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08", 93 | "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a", 94 | "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50", 95 | "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd", 96 | "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239", 97 | "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb", 98 | "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea", 99 | "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e", 100 | "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156", 101 | "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142", 102 | "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442", 103 | "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62", 104 | "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db", 105 | "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531", 106 | "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383", 107 | "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a", 108 | "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357", 109 | "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", 110 | "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" 111 | ], 112 | "version": "==1.4.3" 113 | }, 114 | "mccabe": { 115 | "hashes": [ 116 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 117 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 118 | ], 119 | "version": "==0.6.1" 120 | }, 121 | "pylint": { 122 | "hashes": [ 123 | "sha256:3db5468ad013380e987410a8d6956226963aed94ecb5f9d3a28acca6d9ac36cd", 124 | "sha256:886e6afc935ea2590b462664b161ca9a5e40168ea99e5300935f6591ad467df4" 125 | ], 126 | "index": "pypi", 127 | "version": "==2.4.4" 128 | }, 129 | "six": { 130 | "hashes": [ 131 | "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", 132 | "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" 133 | ], 134 | "version": "==1.13.0" 135 | }, 136 | "smmap2": { 137 | "hashes": [ 138 | "sha256:0555a7bf4df71d1ef4218e4807bbf9b201f910174e6e08af2e138d4e517b4dde", 139 | "sha256:29a9ffa0497e7f2be94ca0ed1ca1aa3cd4cf25a1f6b4f5f87f74b46ed91d609a" 140 | ], 141 | "version": "==2.0.5" 142 | }, 143 | "typed-ast": { 144 | "hashes": [ 145 | "sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161", 146 | "sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", 147 | "sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", 148 | "sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", 149 | "sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", 150 | "sha256:48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47", 151 | "sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", 152 | "sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", 153 | "sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", 154 | "sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", 155 | "sha256:7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2", 156 | "sha256:838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e", 157 | "sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", 158 | "sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", 159 | "sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", 160 | "sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", 161 | "sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", 162 | "sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", 163 | "sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66", 164 | "sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12" 165 | ], 166 | "markers": "implementation_name == 'cpython' and python_version < '3.8'", 167 | "version": "==1.4.0" 168 | }, 169 | "wrapt": { 170 | "hashes": [ 171 | "sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1" 172 | ], 173 | "version": "==1.11.2" 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /React-notes.md: -------------------------------------------------------------------------------- 1 | # Option 1 2 | `npx create-react-app ` 3 | > If fails, try: `npm -g uninstall create-react-app` then `npx create-react-app ` 4 | 5 | # Option 2 6 | `npm init react-app ` 7 | 8 | # Option 3 9 | `package.json`: 10 | ``` 11 | { 12 | "name": "your-app-name", 13 | "version": "0.1.0", 14 | "private": true, 15 | "dependencies": { 16 | "@testing-library/jest-dom": "^4.2.4", 17 | "@testing-library/react": "^9.4.0", 18 | "@testing-library/user-event": "^7.2.1", 19 | "react": "^16.12.0", 20 | "react-dom": "^16.12.0", 21 | "react-scripts": "3.3.0" 22 | }, 23 | "scripts": { 24 | "start": "react-scripts start", 25 | "build": "react-scripts build", 26 | "test": "react-scripts test", 27 | "eject": "react-scripts eject" 28 | }, 29 | "eslintConfig": { 30 | "extends": "react-app" 31 | }, 32 | "browserslist": { 33 | "production": [ 34 | ">0.2%", 35 | "not dead", 36 | "not op_mini all" 37 | ], 38 | "development": [ 39 | "last 1 chrome version", 40 | "last 1 firefox version", 41 | "last 1 safari version" 42 | ] 43 | } 44 | } 45 | ``` 46 | Then `npm install` 47 | 48 | # Option 4 (using yarm) 49 | 50 | ``` 51 | yarn create react-app my-app 52 | ``` 53 | 54 | 55 | ``` -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Tweetme 2 2 | Build a twitter-like app in Django, Bootstrap, Javascript, & React.js. Step-by-Step. 3 | 4 | [![Tweetme 2 Logo](https://static.codingforentrepreneurs.com/media/projects/tweetme-2/images/share/Tweetme2_share.jpg)](https://cfe.sh/projects/tweetme-2) 5 | 6 | ### Lesson Code 7 | _Lessons 1-5: no significant code added_ 8 | 9 | [6 - Updated VS Code Config](../../tree/c118bac532475dc16052c0ce5dce2d264d5c333a/) 10 | 11 | [7 - Our Roadmap](../../tree/c51618fea2bf4e7af336d1377d31b5ce4c376217/) 12 | 13 | [8 - The Tweets Model](../../tree/84a3ef90feeefa9e99264e832b0c73e4ded950a6/) 14 | 15 | [9 - Store Data from Django Model](../../tree/c08ef2bb515709610161838223b0b16fc0fc4cf3/) 16 | 17 | [10 - Intro to URL Routing and Dynamic Routing](../../tree/ef2e008e2a2b66130ec6e37a7b349f62ade71607/) 18 | 19 | [11 - Handling Dynamic Routing](../../tree/9b7d4ce7cdc049de4d77716599a3a9fed3fe7e2d/) 20 | 21 | [12 - Dynamic View into REST API Endpoint](../../tree/8823ffa71dc72a46ea5f51f2c4032d5101990929/) 22 | 23 | [13 - Our First Template](../../tree/cd504b8dd2b91fcf5ffa8de5a1a99cba2abad379/) 24 | 25 | [14 - Bootstrap & Django Templates](../../tree/6a9a5ec09ae1ed41bf84c37b5b667abe335e23a3/) 26 | 27 | [15 - Tweet List View](../../tree/f52194e263c6600b43b11dfcb10ad686b155dd92/) 28 | 29 | [16 - Dynamic Load Tweets via JavaScript](../../tree/935e371eca7e2926827d60107dd4d954f37fda03/) 30 | 31 | [17 - Replace HTML Content with JavaScript](../../tree/a3ed3ae82854a5fccae2970559a40e5cd260cadd/) 32 | 33 | [18 - Tweets to HTML via JavaScript](../../tree/30166801d0c48b38d91d95d9ae772ec2aa0c2682/) 34 | 35 | [19 - Format Tweet Method](../../tree/714f3c80aed3fb491f9dd894927e673b231114ae/) 36 | 37 | [20 - Like Button Rendering](../../tree/8124fe9de95400a65652d14a9c39def1dd9f2bdf/) 38 | 39 | [21 - Rapid Implement of Bootstrap Theme](../../tree/7335b554e18a1d1c0999447410dffcc6a92e2266/) 40 | 41 | [22 - Tweet Create Form](../../tree/4a56201848d5475b0ec8d4c59b22e307592eccae/) 42 | 43 | [23 - Tweet Form by Hand](../../tree/0a7553c2294fee17c1d1e84b92f27e532540d026/) 44 | 45 | [24 - Successful Form Redirect](../../tree/2e2f8c609e4815cba43468c8879823db0159c50f/) 46 | 47 | [25 - Safe URL Redirect](../../tree/83c9b9e47ec1afa96a7164701851427ecf61d50d/) 48 | 49 | [26 - Prevent Form Submit via JavaScript](../../tree/76cd131d96e7ef3690a62543fc4c23c75e63d4c6/) 50 | 51 | [27 - Sending Form Data via pure JavaScript](../../tree/76cd131d96e7ef3690a62543fc4c23c75e63d4c6/) 52 | 53 | [28 - Handling Ajax Requests](../../tree/99a8891b976d6acdb82a5a2b70676e9d23bd7e6f/) 54 | 55 | [29 - Serialize Django Model Object](../../tree/ebff9f8fe77005de4c1f1934d5818befd5e39c64/) 56 | 57 | [30 - Append New Tweet & Reorder](../../tree/c088d8b548a7c981f32a0cfd7151cff4a11fd528/) 58 | 59 | [31 - Handling Form Errors](../../tree/4b764cbc57f533e25b5fa973342da550a1e3ab91/) 60 | 61 | [32 - Rendering the Error Message via Vanilla JavaScript](../../tree/493d324165b47bc7b965916ab3233dc674991aee/) 62 | 63 | [33 - Users & Tweets](../../tree/bca1089afdd332ec077c896069af032d86957ab0/) 64 | 65 | [34 - Django Admin](../../tree/e4f6e285b029722925df1d58265a127d10224214/) 66 | 67 | [35 - Associate Authenticated User to Object](../../tree/b553fc641b4e40722aa632caa4aa5bad7e63879f/) 68 | 69 | [36 - Permissions & Roadmap](../../tree/c5e98a47214dc439290c32c8eb5315855cacb7bf/) 70 | 71 | [37 - Install Django Rest Framework](../../tree/f0b6d26180c5a8c86e2e30df7a2c0f3a585c4472/) 72 | 73 | [38 - Django Forms to Django Rest Framework Serializer](../../tree/773bd786a3bf114104e14b9cde54c4a5a1bf1cdf/) 74 | 75 | [39 - Django Views to Django Rest Framework Views](../../tree/c1fdd108faf2e8494c3f9125334dbfb3ea0479d2/) 76 | 77 | [40 - Permissions and Authentication Classes Decorators for DRF APIs](../../tree/62b13fef8bc0360c16180bc0a2ba91e26b91756d/) 78 | 79 | [41 - Delete Tweet API View](../../tree/fd97a4487f54ecc2f204f99fd08053f4133374a0/) 80 | 81 | [42 - Adding a Like Field](../../tree/0e72e11fdc4b9237465df622f8f4d6e2266e761b/) 82 | 83 | [43 - Understanding Setting ManyToMayFields](../../tree/d7dcc835547cd7946913fcd702e1cfdbb81c5b43/) 84 | 85 | [44 - Tweet Action View](../../tree/a801e54e56ec89c78be3fd675e1391d322fdc37d/) 86 | 87 | [45 - JavaScript Tweet Action Handler](../../tree/df5d9f6c4521ea0b3bab964d0ffb8ad93aa44b08/) 88 | 89 | [46 - CSRF & Client Side Action Buttons](../../tree/2902c65cb2072dbd66dce4ce89b56f1997127d7b/) 90 | 91 | [47 - Retweeting Logic](../../tree/8ad028e5aec09bfc74d5309d943f2cb2bc8fd7f3/) 92 | 93 | [48 - Two Types of Serializers](../../tree/5ae049e476bbcc12ed711bd508bdd1db35385a58/) 94 | 95 | [49 - Internal App Urls](../../tree/94d7f9c9e4304a32da36213ddfec00390f8a10eb/) 96 | 97 | [50 - Setting up Tests in Django](../../tree/863f1e597a9f2b9a6eab386cc55def479a9cc521/) 98 | 99 | _51 - Verify or Install Nodejs- no code_ 100 | 101 | [52 - Create React App](../../tree/64af9c04b3138b8a65c059d2c448306dcfdb4f14/) 102 | 103 | [54 - Ajax lookup via XHR in Reactjs](../../tree/a324816b849c8488a5f442c213b43e988c267bcc/) 104 | 105 | [55 - Handling CORS and Invalid HOST_HEADER in Django](../../tree/28584e8be0455a8e6bb36a8236046843add92479/) 106 | 107 | [56 - Functional Components in React](../../tree/56e80ff0608360bf1083d39aab9d82e4a051a83f/) 108 | 109 | [57 - Reactjs Action Btn](../../tree/3d5c281e3b03bd99763efa137a74585438b3aaaf/) 110 | 111 | [58 - Using JavaScript Modules](../../tree/bc892166397e5bc09a575c721c23d4fad3882469/) 112 | 113 | [59 - Improved Action Btn](../../tree/17406366b1a5b1d360e25603868ee7b60920553d/) 114 | 115 | [60 - Understanding setState Hook](../../tree/3bc00466c918241db25420d3c222e4bf693ac185/) 116 | 117 | [61 - Handling a Form in React](../../tree/49553ed3e14b60ffa9453a545293812e85b64e9a/) 118 | 119 | [62 - Pass from Parent Component to Child with useEffect](../../tree/7a6310b5c82a3651b86450725c9b9502999f9ef6/) 120 | 121 | [63 - Adjust the React Render Process](../../tree/168382442ca1efeb90067c9c2ebd92a90027bb90/) 122 | 123 | [64 - React Rendered by Django](../../tree/7f33ea9f9eab6c07345db535ff02d81b9c471871/) 124 | 125 | [65 - Render React App via Any Django Template](../../tree/2763cd207d10ba25737bc9c5fd45f2a76b332e4e/) 126 | 127 | [66 - A Better XHR Lookup](../../tree/6dfdf9913f7389235bc0ca81c0deec6f90c76f04/) 128 | 129 | [67 - Create Tweet in React](../../tree/864b60b7ca7d7de211d6efd1de631146d068a010/) 130 | 131 | [68 - Dev Authentication](../../tree/944cbcb5ad4c535eacf51aa6645f816e72f30edc/) 132 | 133 | [69 - Handling New Tweet](../../tree/cd3a15c77d4343a11f65485132e88951929af393/) 134 | 135 | [70 - API Methods in React](../../tree/5133bd4a589f180e3332a02de3a435b2418b4f0d/) 136 | 137 | [71 - Tweet Action Btn](../../tree/8cab91c9fc6e678aa04cf3141110ebf56a7d913e/) 138 | 139 | [72 - Rendering the ReTweet](../../tree/987ded4a6a4e1a1f4ab47e38d085e9ce325571c2/) 140 | 141 | [73 - Improving the Tweet Action Button](../../tree/8f89a740e4db66cbd82879b26f08a6600318cb5a/) 142 | 143 | [74 - Prepending Dynamic Retweets](../../tree/3fd141a06ebcdc9b27bd1ad779b1942771a04588/) 144 | 145 | [75 - Set Data Props on ReactDOM Render](../../tree/82eb8cf2e88e5a1aa99947103a5a10f666a36c2e/) 146 | 147 | [76 - Limit List View by Username](../../tree/c5306bb9cacac68268a1ac5bd4eb28f5610e2767/) 148 | 149 | [77 - Rendering Limited Tweet List by Username](../../tree/5c7b86ad592bff6106826f4bfeb4f9120f5b63ca/) 150 | 151 | [78 - Tweets Module Clean Up](../../tree/33af1a02d078540a14a74384b347313de2333f24/) 152 | 153 | [79 - Lookup & Render & Embed Individual Tweets](../../tree/eb194546aa4202e6f1fbb46a69fecdc9e30e1311/) 154 | 155 | [80 - Linking Individual Tweets](../../tree/55d983e16241705e01128164d351249cfb4267c8/) 156 | 157 | [81 - Build and Use On Django](../../tree/9b524ac3a9e42139666fe10fb548833df8e5be5c/) 158 | 159 | [82 - Clean Up API Urls and Views](../../tree/ac6fa11b6084f5e82fe8c7675f1858ab2e3fcb6e/) 160 | 161 | [83 - Login Required Redirect](../../tree/e4761aa629c02c665be78b6f82c9fb26a4219d7a/) 162 | 163 | [84 - Authentication & Registration](../../tree/b2b410fd524924eb73b8491a4785a5fdfc8f281f/) 164 | 165 | [85 - User Profiles](../../tree/889c7cbf37b43a1320c6231186ab412225a46f7c/) 166 | 167 | [86 - Handling Profile Does Not Exist](../../tree/061c715c81acbfc705c50a09c9fd7860353d335e/) 168 | 169 | [87 - Signals to Create Profile Objects](../../tree/b331593f11aa5d792f5cc1a4916bb829a12196b4/) 170 | 171 | [88 - Save 2 Models in 1 Form and 1 View](../../tree/4a46592de493f19f0e2d1a8cb12ff154a028d805/) 172 | 173 | [89 - ManyToManyField and Reverse Relations](../../tree/c52b4ca2b0b4d09bb34b5ba06b57440937eeb845/) 174 | 175 | [90 - Followers and Following](../../tree/749ba392ebacf799305d4f017dc99fe69462ffc7/) 176 | 177 | [91 - Follow Button Logic and Endpoint](../../tree/bcb308d5c132a34defc521231f6315a26317b14a/) 178 | 179 | [92 - Profile Following Unit Tests](../../tree/eec3476bd41768d4899125b17ebd5a8d144e2c55/) 180 | 181 | [93 - User Feed including Following](../../tree/2b76f4e9834dcd21474b286bdec4248343ecc306/) 182 | 183 | [94 - More Effecient Backend Lookups and Custom Model Managers](../../tree/0b1f4b907bad5d4bdfb525804edefeeb626bf79b/) 184 | 185 | [95 - More Efficient List Views with Pagination](../../tree/7c89ad7c014a7b431dba55b8986f7cc71d0c8439/) 186 | 187 | [96 - User Profile Serializer](../../tree/d7fd9517d591f91082374ead775991e94ec4b290/) 188 | 189 | [97 - Handling our New List View Response](../../tree/2a6f0106c1877df5d39512aa161889e17b9a502b/) 190 | 191 | [98 - Handling Pagination in React](../../tree/0c39d7474d64966682bb642dce2befd56de17dd6/) 192 | 193 | [99 - Display User Within Tweet](../../tree/877884675610faee2335df06c4c1cb67df5644e7/) 194 | 195 | [100 - Display Tweet User Details](../../tree/161ff2d8a7236c20d8b9addbcc578150de09651b/) 196 | 197 | [101 - Feed View Component](../../tree/f5e1eb899c6bd2d4f7d89b12f3d741e2e57777ed/) 198 | 199 | [102 - Build for the Feed](../../tree/116a480560e10021bb33299c4e0fcdcc4d560752/) 200 | 201 | [103 - User Profile API Detail](../../tree/72db184c5a6b9338eecba1c2df630812fa269faa/) 202 | 203 | [104 - Passing the Request to Serializers](../../tree/aa9379e967c4d8feae5e577df743327a707456a5/) 204 | 205 | [105 - Render Profile Badge Component](../../tree/cbc23c43d7eeec86c5bd0c0e0242f824bdc95b1f/) 206 | 207 | [106 - The Follow Button](../../tree/214190819624697713aab6cc9b5da3fc70e2e8b3/) 208 | 209 | [107 - Removing Redundant Profile View](../../tree/5ebfac71d174b4869c367c79095df554fa0ec97a/) 210 | 211 | [108 - Display Follower Count with Numeraljs](../../tree/62f930450531e07039a650401cbb3aac852bdc37/) 212 | 213 | [109 - FInal Build](../../tree/7ce03e49253bed9e4a55f5ab985f92b047fa736c/) 214 | 215 | **110 - Thank you and next steps** _no code_ 216 | 217 | Next steps: 218 | - Large File Uploads for Images ~ [Dive into AWS](https://cfe.sh/courses/aws) 219 | - Notifications 220 | - Direct Messages / Private Inboxes ~ [Chat x Channels](https://cfe.sh/courses/chat-channels-react) 221 | - Explore -> parse & filter for hashtags 222 | 223 | 224 | -------------------------------------------------------------------------------- /accounts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Tweetme-2/50a6e18b77cb0333cda8c6e1f82649728771c6dc/accounts/__init__.py -------------------------------------------------------------------------------- /accounts/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /accounts/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AccountsConfig(AppConfig): 5 | name = 'accounts' 6 | -------------------------------------------------------------------------------- /accounts/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Tweetme-2/50a6e18b77cb0333cda8c6e1f82649728771c6dc/accounts/migrations/__init__.py -------------------------------------------------------------------------------- /accounts/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /accounts/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /accounts/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, redirect 2 | from django.contrib.auth import login, logout, authenticate 3 | from django.contrib.auth.forms import AuthenticationForm, UserCreationForm 4 | # Create your views here. 5 | 6 | # Function based views to Class Based Views 7 | 8 | def login_view(request, *args, **kwargs): 9 | form = AuthenticationForm(request, data=request.POST or None) 10 | if form.is_valid(): 11 | user_ = form.get_user() 12 | login(request, user_) 13 | return redirect("/") 14 | context = { 15 | "form": form, 16 | "btn_label": "Login", 17 | "title": "Login" 18 | } 19 | return render(request, "accounts/auth.html", context) 20 | 21 | def logout_view(request, *args, **kwargs): 22 | if request.method == "POST": 23 | logout(request) 24 | return redirect("/login") 25 | context = { 26 | "form": None, 27 | "description": "Are you sure you want to logout?", 28 | "btn_label": "Click to Confirm", 29 | "title": "Logout" 30 | } 31 | return render(request, "accounts/auth.html", context) 32 | 33 | 34 | def register_view(request, *args, **kwargs): 35 | form = UserCreationForm(request.POST or None) 36 | if form.is_valid(): 37 | user = form.save(commit=True) 38 | user.set_password(form.cleaned_data.get("password1")) 39 | # send a confirmation email to verify their account 40 | login(request, user) 41 | return redirect("/") 42 | context = { 43 | "form": form, 44 | "btn_label": "Register", 45 | "title": "Register" 46 | } 47 | return render(request, "accounts/auth.html", context) -------------------------------------------------------------------------------- /db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Tweetme-2/50a6e18b77cb0333cda8c6e1f82649728771c6dc/db.sqlite3 -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tweetme2.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /profiles/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Tweetme-2/50a6e18b77cb0333cda8c6e1f82649728771c6dc/profiles/__init__.py -------------------------------------------------------------------------------- /profiles/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Profile 4 | 5 | 6 | admin.site.register(Profile) -------------------------------------------------------------------------------- /profiles/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Tweetme-2/50a6e18b77cb0333cda8c6e1f82649728771c6dc/profiles/api/__init__.py -------------------------------------------------------------------------------- /profiles/api/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import ( 4 | profile_detail_api_view, 5 | ) 6 | ''' 7 | CLIENT 8 | Base ENDPOINT /api/profiles/ 9 | ''' 10 | urlpatterns = [ 11 | path('/', profile_detail_api_view), 12 | path('/follow', profile_detail_api_view), 13 | ] 14 | -------------------------------------------------------------------------------- /profiles/api/views.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from django.conf import settings 4 | from django.contrib.auth import get_user_model 5 | from django.http import HttpResponse, Http404, JsonResponse 6 | from django.shortcuts import render, redirect 7 | from django.utils.http import is_safe_url 8 | 9 | from rest_framework.authentication import SessionAuthentication 10 | from rest_framework.decorators import api_view, authentication_classes, permission_classes 11 | from rest_framework.permissions import IsAuthenticated 12 | from rest_framework.response import Response 13 | from ..models import Profile 14 | from ..serializers import PublicProfileSerializer 15 | 16 | User = get_user_model() 17 | ALLOWED_HOSTS = settings.ALLOWED_HOSTS 18 | 19 | # @api_view(['GET']) 20 | # @permission_classes([IsAuthenticated]) 21 | # def user_profile_detail_view(request, username, *args, **kwargs): 22 | # current_user = request.user 23 | # to_follow_user = ?? 24 | # return Response({}, status=200) 25 | 26 | @api_view(['GET', 'POST']) 27 | def profile_detail_api_view(request, username, *args, **kwargs): 28 | # get the profile for the passed username 29 | qs = Profile.objects.filter(user__username=username) 30 | if not qs.exists(): 31 | return Response({"detail": "User not found"}, status=404) 32 | profile_obj = qs.first() 33 | data = request.data or {} 34 | if request.method == "POST": 35 | me = request.user 36 | action = data.get("action") 37 | if profile_obj.user != me: 38 | if action == "follow": 39 | profile_obj.followers.add(me) 40 | elif action == "unfollow": 41 | profile_obj.followers.remove(me) 42 | else: 43 | pass 44 | serializer = PublicProfileSerializer(instance=profile_obj, context={"request": request}) 45 | return Response(serializer.data, status=200) 46 | 47 | 48 | # @api_view(['GET', 'POST']) 49 | # @permission_classes([IsAuthenticated]) 50 | # def user_follow_view(request, username, *args, **kwargs): 51 | # me = request.user 52 | # other_user_qs = User.objects.filter(username=username) 53 | # if me.username == username: 54 | # my_followers = me.profile.followers.all() 55 | # return Response({"count": my_followers.count()}, status=200) 56 | # if not other_user_qs.exists(): 57 | # return Response({}, status=404) 58 | # other = other_user_qs.first() 59 | # profile = other.profile 60 | # data = request.data or {} 61 | # action = data.get("action") 62 | # if action == "follow": 63 | # profile.followers.add(me) 64 | # elif action == "unfollow": 65 | # profile.followers.remove(me) 66 | # else: 67 | # pass 68 | # data = PublicProfileSerializer(instance=profile, context={"request": request}) 69 | # return Response(data.data, status=200) 70 | -------------------------------------------------------------------------------- /profiles/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ProfilesConfig(AppConfig): 5 | name = 'profiles' 6 | -------------------------------------------------------------------------------- /profiles/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.auth import get_user_model 3 | 4 | from .models import Profile 5 | 6 | User = get_user_model() 7 | 8 | 9 | class UserProfileForm(forms.ModelForm): 10 | location = forms.CharField(required=False) 11 | bio = forms.CharField(required=False) 12 | class Meta: 13 | model = User 14 | fields = ['first_name', 'last_name', 'email'] 15 | 16 | 17 | class ProfileForm(forms.ModelForm): 18 | first_name = forms.CharField(required=False) 19 | last_name = forms.CharField(required=False) 20 | email = forms.CharField(required=False) 21 | class Meta: 22 | model = Profile 23 | fields = ['location', 'bio'] 24 | 25 | 26 | class ProfileBasicForm(forms.Form): 27 | first_name = forms.CharField(required=False) 28 | last_name = forms.CharField(required=False) 29 | email_address = forms.CharField(required=False) 30 | location = forms.CharField(required=False) 31 | bio = forms.CharField(required=False) -------------------------------------------------------------------------------- /profiles/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2020-01-06 23:20 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Profile', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('location', models.CharField(blank=True, max_length=220, null=True)), 22 | ('bio', models.TextField(blank=True, null=True)), 23 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 24 | ], 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /profiles/migrations/0002_auto_20200108_2100.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2020-01-08 21:00 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | import django.utils.timezone 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 13 | ('profiles', '0001_initial'), 14 | ] 15 | 16 | operations = [ 17 | migrations.AddField( 18 | model_name='profile', 19 | name='followers', 20 | field=models.ManyToManyField(blank=True, related_name='following', to=settings.AUTH_USER_MODEL), 21 | ), 22 | migrations.AddField( 23 | model_name='profile', 24 | name='timestamp', 25 | field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), 26 | preserve_default=False, 27 | ), 28 | migrations.AddField( 29 | model_name='profile', 30 | name='updated', 31 | field=models.DateTimeField(auto_now=True), 32 | ), 33 | migrations.CreateModel( 34 | name='FollowerRelation', 35 | fields=[ 36 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 37 | ('timestamp', models.DateTimeField(auto_now_add=True)), 38 | ('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='profiles.Profile')), 39 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 40 | ], 41 | ), 42 | ] 43 | -------------------------------------------------------------------------------- /profiles/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Tweetme-2/50a6e18b77cb0333cda8c6e1f82649728771c6dc/profiles/migrations/__init__.py -------------------------------------------------------------------------------- /profiles/models.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.db import models 3 | from django.db.models.signals import post_save 4 | 5 | User = settings.AUTH_USER_MODEL 6 | 7 | class FollowerRelation(models.Model): 8 | user = models.ForeignKey(User, on_delete=models.CASCADE) 9 | profile = models.ForeignKey("Profile", on_delete=models.CASCADE) 10 | timestamp = models.DateTimeField(auto_now_add=True) 11 | 12 | class Profile(models.Model): 13 | user = models.OneToOneField(User, on_delete=models.CASCADE) 14 | location = models.CharField(max_length=220, null=True, blank=True) 15 | bio = models.TextField(blank=True, null=True) 16 | timestamp = models.DateTimeField(auto_now_add=True) 17 | updated = models.DateTimeField(auto_now=True) 18 | followers = models.ManyToManyField(User, related_name='following', blank=True) 19 | """ 20 | project_obj = Profile.objects.first() 21 | project_obj.followers.all() -> All users following this profile 22 | user.following.all() -> All user profiles I follow 23 | """ 24 | def user_did_save(sender, instance, created, *args, **kwargs): 25 | if created: 26 | Profile.objects.get_or_create(user=instance) 27 | 28 | post_save.connect(user_did_save, sender=User) 29 | 30 | # after the user logs in -> verify profile -------------------------------------------------------------------------------- /profiles/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from .models import Profile 4 | 5 | class PublicProfileSerializer(serializers.ModelSerializer): 6 | first_name = serializers.SerializerMethodField(read_only=True) 7 | last_name = serializers.SerializerMethodField(read_only=True) 8 | is_following = serializers.SerializerMethodField(read_only=True) 9 | username = serializers.SerializerMethodField(read_only=True) 10 | follower_count = serializers.SerializerMethodField(read_only=True) 11 | following_count = serializers.SerializerMethodField(read_only=True) 12 | class Meta: 13 | model = Profile 14 | fields = [ 15 | "first_name", 16 | "last_name", 17 | "id", 18 | "bio", 19 | "location", 20 | "follower_count", 21 | "following_count", 22 | "is_following", 23 | "username", 24 | ] 25 | 26 | def get_is_following(self, obj): 27 | # request??? 28 | is_following = False 29 | context = self.context 30 | request = context.get("request") 31 | if request: 32 | user = request.user 33 | is_following = user in obj.followers.all() 34 | return is_following 35 | 36 | def get_first_name(self, obj): 37 | return obj.user.first_name 38 | 39 | def get_last_name(self, obj): 40 | return obj.user.last_name 41 | 42 | def get_username(self, obj): 43 | return obj.user.username 44 | 45 | def get_following_count(self, obj): 46 | return obj.user.following.count() 47 | 48 | def get_follower_count(self, obj): 49 | return obj.followers.count() -------------------------------------------------------------------------------- /profiles/tests.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.test import TestCase 3 | 4 | from rest_framework.test import APIClient 5 | 6 | from .models import Profile 7 | 8 | 9 | User = get_user_model() 10 | 11 | class ProfileTestCase(TestCase): 12 | def setUp(self): 13 | self.user = User.objects.create_user(username='cfe', password='somepassword') 14 | self.userb = User.objects.create_user(username='cfe-2', password='somepassword2') 15 | 16 | def get_client(self): 17 | client = APIClient() 18 | client.login(username=self.user.username, password='somepassword') 19 | return client 20 | 21 | def test_profile_created_via_signal(self): 22 | qs = Profile.objects.all() 23 | self.assertEqual(qs.count(), 2) 24 | 25 | def test_following(self): 26 | first = self.user 27 | second = self.userb 28 | first.profile.followers.add(second) # added a follower 29 | second_user_following_whom = second.following.all() 30 | qs = second_user_following_whom.filter(user=first) # from a user, check other user is being followed. 31 | first_user_following_no_one = first.following.all() # check new user has is not following anyone 32 | self.assertTrue(qs.exists()) 33 | self.assertFalse(first_user_following_no_one.exists()) 34 | 35 | def test_follow_api_endpoint(self): 36 | client = self.get_client() 37 | response = client.post( 38 | f"/api/profiles/{self.userb.username}/follow", 39 | {"action": "follow"} 40 | ) 41 | r_data = response.json() 42 | count = r_data.get("count") 43 | self.assertEqual(count, 1) 44 | 45 | def test_unfollow_api_endpoint(self): 46 | first = self.user 47 | second = self.userb 48 | first.profile.followers.add(second) 49 | client = self.get_client() 50 | response = client.post( 51 | f"/api/profiles/{self.userb.username}/follow", 52 | {"action": "unfollow"} 53 | ) 54 | r_data = response.json() 55 | count = r_data.get("count") 56 | self.assertEqual(count, 0) 57 | 58 | def test_cannot_follow_api_endpoint(self): 59 | client = self.get_client() 60 | response = client.post( 61 | f"/api/profiles/{self.user.username}/follow", 62 | {"action": "follow"} 63 | ) 64 | r_data = response.json() 65 | count = r_data.get("count") 66 | self.assertEqual(count, 0) -------------------------------------------------------------------------------- /profiles/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | 4 | from .views import profile_detail_view, profile_update_view 5 | 6 | urlpatterns = [ 7 | path('edit', profile_update_view), 8 | path('', profile_detail_view), 9 | ] 10 | -------------------------------------------------------------------------------- /profiles/views.py: -------------------------------------------------------------------------------- 1 | from django.http import Http404 2 | from django.shortcuts import render, redirect 3 | 4 | from .forms import ProfileForm 5 | from .models import Profile 6 | 7 | 8 | 9 | def profile_update_view(request, *args, **kwargs): 10 | if not request.user.is_authenticated: # is_authenticated() 11 | return redirect("/login?next=/profile/update") 12 | user = request.user 13 | user_data = { 14 | "first_name": user.first_name, 15 | "last_name": user.last_name, 16 | "email": user.email 17 | } 18 | my_profile = user.profile 19 | form = ProfileForm(request.POST or None, instance=my_profile, initial=user_data) 20 | if form.is_valid(): 21 | profile_obj = form.save(commit=False) 22 | first_name = form.cleaned_data.get('first_name') 23 | last_name = form.cleaned_data.get('last_name') 24 | email = form.cleaned_data.get('email') 25 | user.first_name = first_name 26 | user.last_name = last_name 27 | user.email = email 28 | user.save() 29 | profile_obj.save() 30 | context = { 31 | "form": form, 32 | "btn_label": "Save", 33 | "title": "Update Profile" 34 | } 35 | return render(request, "profiles/form.html", context) 36 | 37 | 38 | 39 | def profile_detail_view(request, username, *args, **kwargs): 40 | # get the profile for the passed username 41 | qs = Profile.objects.filter(user__username=username) 42 | if not qs.exists(): 43 | raise Http404 44 | profile_obj = qs.first() 45 | is_following = False 46 | if request.user.is_authenticated: 47 | user = request.user 48 | is_following = user in profile_obj.followers.all() 49 | # is_following = profile_obj in user.following.all() 50 | context = { 51 | "username": username, 52 | "profile": profile_obj, 53 | "is_following": is_following 54 | } 55 | return render(request, "profiles/detail.html", context) -------------------------------------------------------------------------------- /static/css/main.50519c1c.chunk.css: -------------------------------------------------------------------------------- 1 | body{margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}.pointer{cursor:pointer!important}.App{text-align:center}.App-logo{height:40vmin;pointer-events:none}@media (prefers-reduced-motion:no-preference){.App-logo{-webkit-animation:App-logo-spin 20s linear infinite;animation:App-logo-spin 20s linear infinite}}.App-header{background-color:#282c34;min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;font-size:calc(10px + 2vmin);color:#fff}.App-link{color:#61dafb}@-webkit-keyframes App-logo-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes App-logo-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}} 2 | /*# sourceMappingURL=main.50519c1c.chunk.css.map */ -------------------------------------------------------------------------------- /static/css/main.50519c1c.chunk.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["index.css","App.css"],"names":[],"mappings":"AAAA,KACE,QAAS,CACT,mIAEY,CACZ,kCAAmC,CACnC,iCACF,CAEA,KACE,uEAEF,CAEA,SACE,wBACF,CChBA,KACE,iBACF,CAEA,UACE,aAAc,CACd,mBACF,CAEA,8CACE,UACE,mDAA4C,CAA5C,2CACF,CACF,CAEA,YACE,wBAAyB,CACzB,gBAAiB,CACjB,YAAa,CACb,qBAAsB,CACtB,kBAAmB,CACnB,sBAAuB,CACvB,4BAA6B,CAC7B,UACF,CAEA,UACE,aACF,CAEA,iCACE,GACE,8BAAuB,CAAvB,sBACF,CACA,GACE,+BAAyB,CAAzB,uBACF,CACF,CAPA,yBACE,GACE,8BAAuB,CAAvB,sBACF,CACA,GACE,+BAAyB,CAAzB,uBACF,CACF","file":"main.50519c1c.chunk.css","sourcesContent":["body {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\n 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\n sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',\n monospace;\n}\n\n.pointer {\n cursor: pointer !important;\n}",".App {\n text-align: center;\n}\n\n.App-logo {\n height: 40vmin;\n pointer-events: none;\n}\n\n@media (prefers-reduced-motion: no-preference) {\n .App-logo {\n animation: App-logo-spin infinite 20s linear;\n }\n}\n\n.App-header {\n background-color: #282c34;\n min-height: 100vh;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n font-size: calc(10px + 2vmin);\n color: white;\n}\n\n.App-link {\n color: #61dafb;\n}\n\n@keyframes App-logo-spin {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(360deg);\n }\n}\n"]} -------------------------------------------------------------------------------- /static/js/2.437034a0.chunk.js.LICENSE: -------------------------------------------------------------------------------- 1 | /* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | 7 | /*! @preserve 8 | * numeral.js 9 | * version : 2.0.6 10 | * author : Adam Draper 11 | * license : MIT 12 | * http://adamwdraper.github.com/Numeral-js/ 13 | */ 14 | 15 | /** @license React v16.12.0 16 | * react.production.min.js 17 | * 18 | * Copyright (c) Facebook, Inc. and its affiliates. 19 | * 20 | * This source code is licensed under the MIT license found in the 21 | * LICENSE file in the root directory of this source tree. 22 | */ 23 | 24 | /** @license React v16.12.0 25 | * react-dom.production.min.js 26 | * 27 | * Copyright (c) Facebook, Inc. and its affiliates. 28 | * 29 | * This source code is licensed under the MIT license found in the 30 | * LICENSE file in the root directory of this source tree. 31 | */ 32 | 33 | /** @license React v0.18.0 34 | * scheduler.production.min.js 35 | * 36 | * Copyright (c) Facebook, Inc. and its affiliates. 37 | * 38 | * This source code is licensed under the MIT license found in the 39 | * LICENSE file in the root directory of this source tree. 40 | */ 41 | -------------------------------------------------------------------------------- /static/js/main.f30308ad.chunk.js: -------------------------------------------------------------------------------- 1 | (this["webpackJsonptweetme2-web"]=this["webpackJsonptweetme2-web"]||[]).push([[0],[,,,,,function(e,t,n){e.exports=n.p+"static/media/logo.5d5d9eef.svg"},,,function(e,t,n){e.exports=n(15)},,,,,function(e,t,n){},function(e,t,n){},function(e,t,n){"use strict";n.r(t);var a=n(0),c=n.n(a),r=n(3),l=n.n(r),o=(n(13),n(5)),i=n.n(o);n(14);function s(e,t,n,a){var c;a&&(c=JSON.stringify(a));var r=new XMLHttpRequest,l="http://localhost:8000/api".concat(t);r.responseType="json";var o=function(e){var t=null;if(document.cookie&&""!==document.cookie)for(var n=document.cookie.split(";"),a=0;a{\n console.log(response, status)\n if ((status === 200 || status === 201) && didPerformAction){\n didPerformAction(response, status)\n }\n }\n const handleClick = (event) => {\n event.preventDefault()\n apiTweetAction(tweet.id, action.type, handleActionBackendEvent)\n \n }\n const display = action.type === 'like' ? `${likes} ${actionDisplay}` : actionDisplay\n return \n }","import React from 'react'\nimport {apiTweetCreate} from './lookup'\n\n\nexport function TweetCreate(props){\n const textAreaRef = React.createRef()\n const {didTweet} = props\n const handleBackendUpdate = (response, status) =>{\n if (status === 201){\n didTweet(response)\n } else {\n console.log(response)\n alert(\"An error occured please try again\")\n }\n }\n\n const handleSubmit = (event) => {\n event.preventDefault()\n const newVal = textAreaRef.current.value\n // backend api request\n apiTweetCreate(newVal, handleBackendUpdate)\n textAreaRef.current.value = ''\n }\n return
\n
\n \n \n
\n
\n}","import React from 'react'\n\n\n\nexport function UserLink (props) {\n const {username} = props\n const handleUserLink = (event) => {\n window.location.href= `/profiles/${username}`\n }\n return \n {props.children}\n \n}\n\n\nexport function UserDisplay(props){\n const {user, includeFullName, hideLink} = props\n const nameDisplay = includeFullName === true ? `${user.first_name} ${user.last_name} ` : null\n return \n {nameDisplay}\n {hideLink === true ? `@${user.username}` : @{user.username}}\n \n}\n\nexport function UserPicture (props) {\n const {user, hideLink} = props\n const userIdSpan = \n {user.username[0]}\n\n return hideLink === true ? userIdSpan : {userIdSpan}\n}\n","import React from 'react'\nimport numeral from 'numeral'\n\n\nexport function DisplayCount(props) {\n return {numeral(props.children).format(\"0a\")}\n }\n ","import React, {useEffect, useState} from 'react'\n\nimport {UserDisplay, UserPicture} from './components'\nimport {apiProfileDetail, apiProfileFollowToggle} from './lookup'\n\nimport {DisplayCount} from './utils'\n\nfunction ProfileBadge(props) {\n const {user, didFollowToggle, profileLoading} = props\n let currentVerb = (user && user.is_following) ? \"Unfollow\" : \"Follow\"\n currentVerb = profileLoading ? \"Loading...\" : currentVerb\n const handleFollowToggle = (event) => {\n event.preventDefault()\n if (didFollowToggle && !profileLoading) {\n didFollowToggle(currentVerb)\n }\n }\n return user ?
\n \n

\n

{user.follower_count} {user.follower_count === 1 ? \"follower\" : \"followers\"}

\n

{user.following_count} following

\n

{user.location}

\n

{user.bio}

\n \n
: null\n}\n\nexport function ProfileBadgeComponent (props) {\n const {username} = props\n // lookup\n const [didLookup, setDidLookup] = useState(false)\n const [profile, setProfile] = useState(null)\n const [profileLoading, setProfileLoading] = useState(false)\n const handleBackendLookup = (response, status) => {\n if (status === 200) {\n setProfile(response)\n }\n }\n useEffect(()=>{\n if (didLookup === false){\n apiProfileDetail(username, handleBackendLookup)\n setDidLookup(true)\n }\n }, [username, didLookup, setDidLookup])\n\n const handleNewFollow = (actionVerb) => {\n apiProfileFollowToggle(username, actionVerb, (response, status)=>{\n // console.log(response, status)\n if (status===200) {\n setProfile(response)\n // apiProfileDetail(username, handleBackendLookup)\n }\n setProfileLoading(false)\n })\n setProfileLoading(true)\n \n }\n return didLookup === false ? \"Loading...\" : profile ? : null\n}","import {backendLookup} from '../lookup'\n\nexport function apiProfileDetail(username, callback) {\n backendLookup(\"GET\", `/profiles/${username}/`, callback)\n}\n\n\nexport function apiProfileFollowToggle(username, action, callback) {\n const data = {action: `${action && action}`.toLowerCase()}\n backendLookup(\"POST\", `/profiles/${username}/follow`, callback, data)\n}","\nimport React, {useState} from 'react'\n\nimport {ActionBtn} from './buttons'\n\nimport {\n UserDisplay,\n UserPicture\n} from '../profiles'\n\nexport function ParentTweet(props){\n const {tweet} = props\n return tweet.parent ? : null\n }\n export function Tweet(props) {\n const {tweet, didRetweet, hideActions, isRetweet, retweeter} = props\n const [actionTweet, setActionTweet] = useState(props.tweet ? props.tweet : null)\n let className = props.className ? props.className : 'col-10 mx-auto col-md-6'\n className = isRetweet === true ? `${className} p-2 border rounded` : className\n const path = window.location.pathname\n const match = path.match(/(?\\d+)/)\n const urlTweetId = match ? match.groups.tweetid : -1\n const isDetail = `${tweet.id}` === `${urlTweetId}`\n \n const handleLink = (event) => {\n event.preventDefault()\n window.location.href = `/${tweet.id}`\n }\n const handlePerformAction = (newActionTweet, status) => {\n if (status === 200){\n setActionTweet(newActionTweet)\n } else if (status === 201) {\n if (didRetweet){\n didRetweet(newActionTweet)\n }\n }\n \n }\n \n return
\n {isRetweet === true &&
\n Retweet via \n
}\n
\n \n
\n \n
\n
\n
\n \n

\n \n

\n

{tweet.content}

\n \n \n
\n
\n {(actionTweet && hideActions !== true) && \n \n \n \n \n }\n {isDetail === true ? null : }\n
\n
\n
\n
\n }\n ","import React, {useEffect, useState} from 'react'\n\nimport {apiTweetFeed} from './lookup'\n\nimport {Tweet} from './detail'\n\nexport function FeedList(props) {\n const [tweetsInit, setTweetsInit] = useState([])\n const [tweets, setTweets] = useState([])\n const [nextUrl, setNextUrl] = useState(null)\n const [tweetsDidSet, setTweetsDidSet] = useState(false)\n useEffect(()=>{\n const final = [...props.newTweets].concat(tweetsInit)\n if (final.length !== tweets.length) {\n setTweets(final)\n }\n }, [props.newTweets, tweets, tweetsInit])\n\n useEffect(() => {\n if (tweetsDidSet === false){\n const handleTweetListLookup = (response, status) => {\n if (status === 200){\n setNextUrl(response.next)\n setTweetsInit(response.results)\n setTweetsDidSet(true)\n }\n }\n apiTweetFeed(handleTweetListLookup)\n }\n }, [tweetsInit, tweetsDidSet, setTweetsDidSet, props.username])\n\n\n const handleDidRetweet = (newTweet) => {\n const updateTweetsInit = [...tweetsInit]\n updateTweetsInit.unshift(newTweet)\n setTweetsInit(updateTweetsInit)\n const updateFinalTweets = [...tweets]\n updateFinalTweets.unshift(tweets)\n setTweets(updateFinalTweets)\n }\n const handleLoadNext = (event) => {\n event.preventDefault()\n if (nextUrl !== null) {\n const handleLoadNextResponse = (response, status) =>{\n if (status === 200){\n setNextUrl(response.next)\n const newTweets = [...tweets].concat(response.results)\n setTweetsInit(newTweets)\n setTweets(newTweets)\n }\n }\n apiTweetFeed(handleLoadNextResponse, nextUrl)\n }\n }\n\n return {tweets.map((item, index)=>{\n return \n })}\n {nextUrl !== null && }\n \n }\n\n\n","import React, {useEffect, useState} from 'react'\n\nimport {apiTweetList} from './lookup'\n\nimport {Tweet} from './detail'\n\nexport function TweetsList(props) {\n const [tweetsInit, setTweetsInit] = useState([])\n const [tweets, setTweets] = useState([])\n const [nextUrl, setNextUrl] = useState(null)\n const [tweetsDidSet, setTweetsDidSet] = useState(false)\n useEffect(()=>{\n const final = [...props.newTweets].concat(tweetsInit)\n if (final.length !== tweets.length) {\n setTweets(final)\n }\n }, [props.newTweets, tweets, tweetsInit])\n\n useEffect(() => {\n if (tweetsDidSet === false){\n const handleTweetListLookup = (response, status) => {\n if (status === 200){\n setNextUrl(response.next)\n setTweetsInit(response.results)\n setTweetsDidSet(true)\n } else {\n alert(\"There was an error\")\n }\n }\n apiTweetList(props.username, handleTweetListLookup)\n }\n }, [tweetsInit, tweetsDidSet, setTweetsDidSet, props.username])\n\n\n const handleDidRetweet = (newTweet) => {\n const updateTweetsInit = [...tweetsInit]\n updateTweetsInit.unshift(newTweet)\n setTweetsInit(updateTweetsInit)\n const updateFinalTweets = [...tweets]\n updateFinalTweets.unshift(tweets)\n setTweets(updateFinalTweets)\n }\n const handleLoadNext = (event) => {\n event.preventDefault()\n if (nextUrl !== null) {\n const handleLoadNextResponse = (response, status) =>{\n if (status === 200){\n setNextUrl(response.next)\n const newTweets = [...tweets].concat(response.results)\n setTweetsInit(newTweets)\n setTweets(newTweets)\n } else {\n alert(\"There was an error\")\n }\n }\n apiTweetList(props.username, handleLoadNextResponse, nextUrl)\n }\n }\n\n return {tweets.map((item, index)=>{\n return \n })}\n {nextUrl !== null && }\n \n }\n\n\n","import React, {useEffect, useState} from 'react'\n\nimport {TweetCreate} from './create'\nimport {Tweet} from './detail'\nimport {apiTweetDetail} from './lookup'\nimport {FeedList} from './feed'\nimport {TweetsList} from './list'\n\nexport function FeedComponent(props) {\n const [newTweets, setNewTweets] = useState([])\n const canTweet = props.canTweet === \"false\" ? false : true\n const handleNewTweet = (newTweet) =>{\n let tempNewTweets = [...newTweets]\n tempNewTweets.unshift(newTweet)\n setNewTweets(tempNewTweets)\n }\n return
\n {canTweet === true && }\n \n
\n}\n\nexport function TweetsComponent(props) {\n const [newTweets, setNewTweets] = useState([])\n const canTweet = props.canTweet === \"false\" ? false : true\n const handleNewTweet = (newTweet) =>{\n let tempNewTweets = [...newTweets]\n tempNewTweets.unshift(newTweet)\n setNewTweets(tempNewTweets)\n }\n return
\n {canTweet === true && }\n \n
\n}\n\n\nexport function TweetDetailComponent(props){\n const {tweetId} = props\n const [didLookup, setDidLookup] = useState(false)\n const [tweet, setTweet] = useState(null)\n\n const handleBackendLookup = (response, status) => {\n if (status === 200) {\n setTweet(response)\n } else {\n alert(\"There was an error finding your tweet.\")\n }\n }\n useEffect(()=>{\n if (didLookup === false){\n\n apiTweetDetail(tweetId, handleBackendLookup)\n setDidLookup(true)\n }\n }, [tweetId, didLookup, setDidLookup])\n\n return tweet === null ? null : \n }","import React from 'react';\nimport logo from './logo.svg';\nimport './App.css';\n\nimport {TweetsComponent} from './tweets'\n\n\nfunction App() {\n \n return (\n
\n
\n \"logo\"\n

\n Edit src/App.js and save to reload.\n

\n
\n \n
\n \n Learn React\n \n
\n
\n );\n}\n\nexport default App;\n","// This optional code is used to register a service worker.\n// register() is not called by default.\n\n// This lets the app load faster on subsequent visits in production, and gives\n// it offline capabilities. However, it also means that developers (and users)\n// will only see deployed updates on subsequent visits to a page, after all the\n// existing tabs open on the page have been closed, since previously cached\n// resources are updated in the background.\n\n// To learn more about the benefits of this model and instructions on how to\n// opt-in, read https://bit.ly/CRA-PWA\n\nconst isLocalhost = Boolean(\n window.location.hostname === 'localhost' ||\n // [::1] is the IPv6 localhost address.\n window.location.hostname === '[::1]' ||\n // 127.0.0.0/8 are considered localhost for IPv4.\n window.location.hostname.match(\n /^127(?:\\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/\n )\n);\n\nexport function register(config) {\n if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {\n // The URL constructor is available in all browsers that support SW.\n const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);\n if (publicUrl.origin !== window.location.origin) {\n // Our service worker won't work if PUBLIC_URL is on a different origin\n // from what our page is served on. This might happen if a CDN is used to\n // serve assets; see https://github.com/facebook/create-react-app/issues/2374\n return;\n }\n\n window.addEventListener('load', () => {\n const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;\n\n if (isLocalhost) {\n // This is running on localhost. Let's check if a service worker still exists or not.\n checkValidServiceWorker(swUrl, config);\n\n // Add some additional logging to localhost, pointing developers to the\n // service worker/PWA documentation.\n navigator.serviceWorker.ready.then(() => {\n console.log(\n 'This web app is being served cache-first by a service ' +\n 'worker. To learn more, visit https://bit.ly/CRA-PWA'\n );\n });\n } else {\n // Is not localhost. Just register service worker\n registerValidSW(swUrl, config);\n }\n });\n }\n}\n\nfunction registerValidSW(swUrl, config) {\n navigator.serviceWorker\n .register(swUrl)\n .then(registration => {\n registration.onupdatefound = () => {\n const installingWorker = registration.installing;\n if (installingWorker == null) {\n return;\n }\n installingWorker.onstatechange = () => {\n if (installingWorker.state === 'installed') {\n if (navigator.serviceWorker.controller) {\n // At this point, the updated precached content has been fetched,\n // but the previous service worker will still serve the older\n // content until all client tabs are closed.\n console.log(\n 'New content is available and will be used when all ' +\n 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'\n );\n\n // Execute callback\n if (config && config.onUpdate) {\n config.onUpdate(registration);\n }\n } else {\n // At this point, everything has been precached.\n // It's the perfect time to display a\n // \"Content is cached for offline use.\" message.\n console.log('Content is cached for offline use.');\n\n // Execute callback\n if (config && config.onSuccess) {\n config.onSuccess(registration);\n }\n }\n }\n };\n };\n })\n .catch(error => {\n console.error('Error during service worker registration:', error);\n });\n}\n\nfunction checkValidServiceWorker(swUrl, config) {\n // Check if the service worker can be found. If it can't reload the page.\n fetch(swUrl, {\n headers: { 'Service-Worker': 'script' }\n })\n .then(response => {\n // Ensure service worker exists, and that we really are getting a JS file.\n const contentType = response.headers.get('content-type');\n if (\n response.status === 404 ||\n (contentType != null && contentType.indexOf('javascript') === -1)\n ) {\n // No service worker found. Probably a different app. Reload the page.\n navigator.serviceWorker.ready.then(registration => {\n registration.unregister().then(() => {\n window.location.reload();\n });\n });\n } else {\n // Service worker found. Proceed as normal.\n registerValidSW(swUrl, config);\n }\n })\n .catch(() => {\n console.log(\n 'No internet connection found. App is running in offline mode.'\n );\n });\n}\n\nexport function unregister() {\n if ('serviceWorker' in navigator) {\n navigator.serviceWorker.ready.then(registration => {\n registration.unregister();\n });\n }\n}\n","import React from 'react';\nimport ReactDOM from 'react-dom';\nimport './index.css';\nimport App from './App';\nimport {ProfileBadgeComponent} from './profiles'\nimport {FeedComponent, TweetsComponent, TweetDetailComponent} from './tweets'\nimport * as serviceWorker from './serviceWorker';\n\nconst appEl = document.getElementById('root')\nif (appEl) {\n ReactDOM.render(, appEl);\n}\nconst e = React.createElement\nconst tweetsEl = document.getElementById(\"tweetme-2\")\nif (tweetsEl) {\n ReactDOM.render(\n e(TweetsComponent, tweetsEl.dataset), tweetsEl);\n}\n\nconst tweetFeedEl = document.getElementById(\"tweetme-2-feed\")\nif (tweetFeedEl) {\n ReactDOM.render(\n e(FeedComponent, tweetFeedEl.dataset), tweetFeedEl);\n}\n\nconst tweetDetailElements = document.querySelectorAll(\".tweetme-2-detail\")\n\ntweetDetailElements.forEach(container=> {\n ReactDOM.render(\n e(TweetDetailComponent, container.dataset), \n container);\n})\n\nconst userProfileBadgeElements = document.querySelectorAll(\".tweetme-2-profile-badge\")\n\nuserProfileBadgeElements.forEach(container=> {\n ReactDOM.render(\n e(ProfileBadgeComponent, container.dataset), \n container);\n})\n// If you want your app to work offline and load faster, you can change\n// unregister() to register() below. Note this comes with some pitfalls.\n// Learn more about service workers: https://bit.ly/CRA-PWA\nserviceWorker.unregister();\n"],"sourceRoot":""} -------------------------------------------------------------------------------- /static/js/runtime-main.76e46f0d.js: -------------------------------------------------------------------------------- 1 | !function(e){function t(t){for(var n,l,f=t[0],i=t[1],a=t[2],c=0,s=[];c 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /templates/accounts/auth.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block content %} 5 | 6 |
7 |

{{ title }}

8 | {% if description %} 9 |

{{ description }}

10 | {% endif %} 11 | {% include "components/form.html" with form=form btn_label=btn_label %} 12 | 13 |
14 | 15 | {% endblock content %} -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {% include 'react/css.html' %} 11 | 16 | Tweetme 2 {% block head_title %}{% endblock head_title%} 17 | 18 | 19 | 20 | {% include "components/navbar.html" %} 21 |
22 | {% block content %} 23 | {% endblock content %} 24 |
25 | {% include 'react/base_embed.html' %} 26 | {% include 'react/js.html' %} 27 | 28 | -------------------------------------------------------------------------------- /templates/components/footer.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Tweetme-2/50a6e18b77cb0333cda8c6e1f82649728771c6dc/templates/components/footer.html -------------------------------------------------------------------------------- /templates/components/form.html: -------------------------------------------------------------------------------- 1 |
{% csrf_token %} 2 | {{ form.as_p }} 3 | 5 |
-------------------------------------------------------------------------------- /templates/components/navbar.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/pages/feed.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 | 5 | {% if request.user.is_authenticated %} 6 |
7 | {% else %} 8 |
9 | {% endif %} 10 | 11 | {% endblock content %} -------------------------------------------------------------------------------- /templates/pages/home.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block head_title %} 4 | this is amazing!!!! 5 | {% endblock head_title%} 6 | 7 | 8 | {% block content %} 9 | 10 | 11 |
12 | 13 | 14 | 15 |
16 |
17 |

Welcome to Tweetme 2

18 |
19 |
20 | 21 |
22 |
23 |
24 | {% csrf_token %} 25 |
26 | 27 | 28 | 29 |
30 |
31 |
32 | 33 | 34 | 35 |
36 | Loading... 37 |
38 | 39 | 207 | {% endblock content %} -------------------------------------------------------------------------------- /templates/profiles/detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 | 5 |
6 | 7 |
10 | 11 | {% endblock content %} -------------------------------------------------------------------------------- /templates/profiles/form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block content %} 5 | 6 |
7 |

{{ title }}

8 | {% if description %} 9 |

{{ description }}

10 | {% endif %} 11 | {% include "components/form.html" with form=form btn_label=btn_label %} 12 | 13 |
14 | 15 | {% endblock content %} -------------------------------------------------------------------------------- /templates/react.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | CFE App
-------------------------------------------------------------------------------- /templates/react/base_embed.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/react/css.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/react/js.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /templates/react_via_dj.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 | 5 |
6 | 7 | {% endblock content %} -------------------------------------------------------------------------------- /templates/tweets/detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
8 | 9 | {% endblock content %} -------------------------------------------------------------------------------- /templates/tweets/list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 | 5 |
6 | 7 | {% endblock content %} -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | 1. Tweets 2 | -> User Permissions 3 | -> Creating 4 | -> Text 5 | -> Image -> Media Storage Server 6 | -> Delete 7 | -> Retweeting 8 | -> Read only serializer 9 | -> Create only serializer 10 | -> Liking or Unliking 11 | 12 | 2. Users 13 | -> Register 14 | -> Login 15 | -> Logout 16 | -> Profile 17 | -> Image? 18 | -> Text? 19 | -> Follow Button 20 | -> Feed 21 | -> User's feed only? 22 | -> User + who they follow? 23 | 24 | 3. Following / Followers 25 | 26 | 27 | Next Steps: 28 | - Large File Uploads for Images ~ Dive into AWS 29 | - Notifications 30 | - Direct Messages / Private Inboxes ~ Chat x Channels 31 | - Explore -> parse & filter for hashtags 32 | -------------------------------------------------------------------------------- /tweetme.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": { 8 | "python.pythonPath": "/Users/cfe/.local/share/virtualenvs/tweetme2-LFH35Wvl/bin/python" 9 | } 10 | } -------------------------------------------------------------------------------- /tweetme2-web/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `npm run build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /tweetme2-web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tweetme2-web", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.4.0", 8 | "@testing-library/user-event": "^7.2.1", 9 | "numeral": "^2.0.6", 10 | "react": "^16.12.0", 11 | "react-dom": "^16.12.0", 12 | "react-scripts": "3.3.0" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test" 18 | }, 19 | "eslintConfig": { 20 | "extends": "react-app" 21 | }, 22 | "browserslist": { 23 | "production": [ 24 | ">0.2%", 25 | "not dead", 26 | "not op_mini all" 27 | ], 28 | "development": [ 29 | "last 1 chrome version", 30 | "last 1 firefox version", 31 | "last 1 safari version" 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tweetme2-web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Tweetme-2/50a6e18b77cb0333cda8c6e1f82649728771c6dc/tweetme2-web/public/favicon.ico -------------------------------------------------------------------------------- /tweetme2-web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | 28 | 29 | CFE App 30 | 31 | 32 | 33 |
34 |
35 |
36 | 37 |
38 | 48 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /tweetme2-web/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Tweetme-2/50a6e18b77cb0333cda8c6e1f82649728771c6dc/tweetme2-web/public/logo192.png -------------------------------------------------------------------------------- /tweetme2-web/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Tweetme-2/50a6e18b77cb0333cda8c6e1f82649728771c6dc/tweetme2-web/public/logo512.png -------------------------------------------------------------------------------- /tweetme2-web/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /tweetme2-web/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /tweetme2-web/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tweetme2-web/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import logo from './logo.svg'; 3 | import './App.css'; 4 | 5 | import {TweetsComponent} from './tweets' 6 | 7 | 8 | function App() { 9 | 10 | return ( 11 |
12 |
13 | logo 14 |

15 | Edit src/App.js and save to reload. 16 |

17 |
18 | 19 |
20 | 26 | Learn React 27 | 28 |
29 |
30 | ); 31 | } 32 | 33 | export default App; 34 | -------------------------------------------------------------------------------- /tweetme2-web/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /tweetme2-web/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | 15 | .pointer { 16 | cursor: pointer !important; 17 | } -------------------------------------------------------------------------------- /tweetme2-web/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import {ProfileBadgeComponent} from './profiles' 6 | import {FeedComponent, TweetsComponent, TweetDetailComponent} from './tweets' 7 | import * as serviceWorker from './serviceWorker'; 8 | 9 | const appEl = document.getElementById('root') 10 | if (appEl) { 11 | ReactDOM.render(, appEl); 12 | } 13 | const e = React.createElement 14 | const tweetsEl = document.getElementById("tweetme-2") 15 | if (tweetsEl) { 16 | ReactDOM.render( 17 | e(TweetsComponent, tweetsEl.dataset), tweetsEl); 18 | } 19 | 20 | const tweetFeedEl = document.getElementById("tweetme-2-feed") 21 | if (tweetFeedEl) { 22 | ReactDOM.render( 23 | e(FeedComponent, tweetFeedEl.dataset), tweetFeedEl); 24 | } 25 | 26 | const tweetDetailElements = document.querySelectorAll(".tweetme-2-detail") 27 | 28 | tweetDetailElements.forEach(container=> { 29 | ReactDOM.render( 30 | e(TweetDetailComponent, container.dataset), 31 | container); 32 | }) 33 | 34 | const userProfileBadgeElements = document.querySelectorAll(".tweetme-2-profile-badge") 35 | 36 | userProfileBadgeElements.forEach(container=> { 37 | ReactDOM.render( 38 | e(ProfileBadgeComponent, container.dataset), 39 | container); 40 | }) 41 | // If you want your app to work offline and load faster, you can change 42 | // unregister() to register() below. Note this comes with some pitfalls. 43 | // Learn more about service workers: https://bit.ly/CRA-PWA 44 | serviceWorker.unregister(); 45 | -------------------------------------------------------------------------------- /tweetme2-web/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tweetme2-web/src/lookup/components.js: -------------------------------------------------------------------------------- 1 | function getCookie(name) { 2 | var cookieValue = null; 3 | if (document.cookie && document.cookie !== '') { 4 | var cookies = document.cookie.split(';'); 5 | for (var i = 0; i < cookies.length; i++) { 6 | var cookie = cookies[i].trim(); 7 | // Does this cookie string begin with the name we want? 8 | if (cookie.substring(0, name.length + 1) === (name + '=')) { 9 | cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); 10 | break; 11 | } 12 | } 13 | } 14 | return cookieValue; 15 | } 16 | 17 | export function backendLookup(method, endpoint, callback, data) { 18 | let jsonData; 19 | if (data){ 20 | jsonData = JSON.stringify(data) 21 | } 22 | const xhr = new XMLHttpRequest() 23 | const url = `http://localhost:8000/api${endpoint}` 24 | xhr.responseType = "json" 25 | const csrftoken = getCookie('csrftoken'); 26 | xhr.open(method, url) 27 | xhr.setRequestHeader("Content-Type", "application/json") 28 | 29 | if (csrftoken){ 30 | // xhr.setRequestHeader("HTTP_X_REQUESTED_WITH", "XMLHttpRequest") 31 | xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest") 32 | xhr.setRequestHeader("X-CSRFToken", csrftoken) 33 | } 34 | 35 | xhr.onload = function() { 36 | if (xhr.status === 403) { 37 | const detail = xhr.response.detail 38 | if (detail === "Authentication credentials were not provided."){ 39 | if (window.location.href.indexOf("login") === -1) { 40 | window.location.href = "/login?showLoginRequired=true" 41 | } 42 | } 43 | } 44 | callback(xhr.response, xhr.status) 45 | } 46 | xhr.onerror = function (e) { 47 | callback({"message": "The request was an error"}, 400) 48 | } 49 | xhr.send(jsonData) 50 | } 51 | 52 | -------------------------------------------------------------------------------- /tweetme2-web/src/lookup/index.js: -------------------------------------------------------------------------------- 1 | import {backendLookup} from './components' 2 | 3 | export { 4 | backendLookup 5 | } -------------------------------------------------------------------------------- /tweetme2-web/src/profiles/badge.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react' 2 | 3 | import {UserDisplay, UserPicture} from './components' 4 | import {apiProfileDetail, apiProfileFollowToggle} from './lookup' 5 | 6 | import {DisplayCount} from './utils' 7 | 8 | function ProfileBadge(props) { 9 | const {user, didFollowToggle, profileLoading} = props 10 | let currentVerb = (user && user.is_following) ? "Unfollow" : "Follow" 11 | currentVerb = profileLoading ? "Loading..." : currentVerb 12 | const handleFollowToggle = (event) => { 13 | event.preventDefault() 14 | if (didFollowToggle && !profileLoading) { 15 | didFollowToggle(currentVerb) 16 | } 17 | } 18 | return user ?
19 | 20 |

21 |

{user.follower_count} {user.follower_count === 1 ? "follower" : "followers"}

22 |

{user.following_count} following

23 |

{user.location}

24 |

{user.bio}

25 | 26 |
: null 27 | } 28 | 29 | export function ProfileBadgeComponent (props) { 30 | const {username} = props 31 | // lookup 32 | const [didLookup, setDidLookup] = useState(false) 33 | const [profile, setProfile] = useState(null) 34 | const [profileLoading, setProfileLoading] = useState(false) 35 | const handleBackendLookup = (response, status) => { 36 | if (status === 200) { 37 | setProfile(response) 38 | } 39 | } 40 | useEffect(()=>{ 41 | if (didLookup === false){ 42 | apiProfileDetail(username, handleBackendLookup) 43 | setDidLookup(true) 44 | } 45 | }, [username, didLookup, setDidLookup]) 46 | 47 | const handleNewFollow = (actionVerb) => { 48 | apiProfileFollowToggle(username, actionVerb, (response, status)=>{ 49 | // console.log(response, status) 50 | if (status===200) { 51 | setProfile(response) 52 | // apiProfileDetail(username, handleBackendLookup) 53 | } 54 | setProfileLoading(false) 55 | }) 56 | setProfileLoading(true) 57 | 58 | } 59 | return didLookup === false ? "Loading..." : profile ? : null 60 | } -------------------------------------------------------------------------------- /tweetme2-web/src/profiles/components.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | 4 | 5 | export function UserLink (props) { 6 | const {username} = props 7 | const handleUserLink = (event) => { 8 | window.location.href= `/profiles/${username}` 9 | } 10 | return 11 | {props.children} 12 | 13 | } 14 | 15 | 16 | export function UserDisplay(props){ 17 | const {user, includeFullName, hideLink} = props 18 | const nameDisplay = includeFullName === true ? `${user.first_name} ${user.last_name} ` : null 19 | return 20 | {nameDisplay} 21 | {hideLink === true ? `@${user.username}` : @{user.username}} 22 | 23 | } 24 | 25 | export function UserPicture (props) { 26 | const {user, hideLink} = props 27 | const userIdSpan = 28 | {user.username[0]} 29 | 30 | return hideLink === true ? userIdSpan : {userIdSpan} 31 | } 32 | -------------------------------------------------------------------------------- /tweetme2-web/src/profiles/index.js: -------------------------------------------------------------------------------- 1 | import {UserPicture, UserDisplay, UserLink} from './components' 2 | import {ProfileBadgeComponent} from './badge' 3 | export { 4 | ProfileBadgeComponent, 5 | UserPicture, 6 | UserDisplay, UserLink 7 | } -------------------------------------------------------------------------------- /tweetme2-web/src/profiles/lookup.js: -------------------------------------------------------------------------------- 1 | import {backendLookup} from '../lookup' 2 | 3 | export function apiProfileDetail(username, callback) { 4 | backendLookup("GET", `/profiles/${username}/`, callback) 5 | } 6 | 7 | 8 | export function apiProfileFollowToggle(username, action, callback) { 9 | const data = {action: `${action && action}`.toLowerCase()} 10 | backendLookup("POST", `/profiles/${username}/follow`, callback, data) 11 | } -------------------------------------------------------------------------------- /tweetme2-web/src/profiles/utils.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import numeral from 'numeral' 3 | 4 | 5 | export function DisplayCount(props) { 6 | return {numeral(props.children).format("0a")} 7 | } 8 | -------------------------------------------------------------------------------- /tweetme2-web/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' } 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready.then(registration => { 134 | registration.unregister(); 135 | }); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /tweetme2-web/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /tweetme2-web/src/tweets/buttons.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import {apiTweetAction} from './lookup' 4 | 5 | export function ActionBtn(props) { 6 | const {tweet, action, didPerformAction} = props 7 | const likes = tweet.likes ? tweet.likes : 0 8 | const className = props.className ? props.className : 'btn btn-primary btn-sm' 9 | const actionDisplay = action.display ? action.display : 'Action' 10 | 11 | const handleActionBackendEvent = (response, status) =>{ 12 | console.log(response, status) 13 | if ((status === 200 || status === 201) && didPerformAction){ 14 | didPerformAction(response, status) 15 | } 16 | } 17 | const handleClick = (event) => { 18 | event.preventDefault() 19 | apiTweetAction(tweet.id, action.type, handleActionBackendEvent) 20 | 21 | } 22 | const display = action.type === 'like' ? `${likes} ${actionDisplay}` : actionDisplay 23 | return 24 | } -------------------------------------------------------------------------------- /tweetme2-web/src/tweets/components.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react' 2 | 3 | import {TweetCreate} from './create' 4 | import {Tweet} from './detail' 5 | import {apiTweetDetail} from './lookup' 6 | import {FeedList} from './feed' 7 | import {TweetsList} from './list' 8 | 9 | export function FeedComponent(props) { 10 | const [newTweets, setNewTweets] = useState([]) 11 | const canTweet = props.canTweet === "false" ? false : true 12 | const handleNewTweet = (newTweet) =>{ 13 | let tempNewTweets = [...newTweets] 14 | tempNewTweets.unshift(newTweet) 15 | setNewTweets(tempNewTweets) 16 | } 17 | return
18 | {canTweet === true && } 19 | 20 |
21 | } 22 | 23 | export function TweetsComponent(props) { 24 | const [newTweets, setNewTweets] = useState([]) 25 | const canTweet = props.canTweet === "false" ? false : true 26 | const handleNewTweet = (newTweet) =>{ 27 | let tempNewTweets = [...newTweets] 28 | tempNewTweets.unshift(newTweet) 29 | setNewTweets(tempNewTweets) 30 | } 31 | return
32 | {canTweet === true && } 33 | 34 |
35 | } 36 | 37 | 38 | export function TweetDetailComponent(props){ 39 | const {tweetId} = props 40 | const [didLookup, setDidLookup] = useState(false) 41 | const [tweet, setTweet] = useState(null) 42 | 43 | const handleBackendLookup = (response, status) => { 44 | if (status === 200) { 45 | setTweet(response) 46 | } else { 47 | alert("There was an error finding your tweet.") 48 | } 49 | } 50 | useEffect(()=>{ 51 | if (didLookup === false){ 52 | 53 | apiTweetDetail(tweetId, handleBackendLookup) 54 | setDidLookup(true) 55 | } 56 | }, [tweetId, didLookup, setDidLookup]) 57 | 58 | return tweet === null ? null : 59 | } -------------------------------------------------------------------------------- /tweetme2-web/src/tweets/create.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {apiTweetCreate} from './lookup' 3 | 4 | 5 | export function TweetCreate(props){ 6 | const textAreaRef = React.createRef() 7 | const {didTweet} = props 8 | const handleBackendUpdate = (response, status) =>{ 9 | if (status === 201){ 10 | didTweet(response) 11 | } else { 12 | console.log(response) 13 | alert("An error occured please try again") 14 | } 15 | } 16 | 17 | const handleSubmit = (event) => { 18 | event.preventDefault() 19 | const newVal = textAreaRef.current.value 20 | // backend api request 21 | apiTweetCreate(newVal, handleBackendUpdate) 22 | textAreaRef.current.value = '' 23 | } 24 | return
25 |
26 | 29 | 30 |
31 |
32 | } -------------------------------------------------------------------------------- /tweetme2-web/src/tweets/detail.js: -------------------------------------------------------------------------------- 1 | 2 | import React, {useState} from 'react' 3 | 4 | import {ActionBtn} from './buttons' 5 | 6 | import { 7 | UserDisplay, 8 | UserPicture 9 | } from '../profiles' 10 | 11 | export function ParentTweet(props){ 12 | const {tweet} = props 13 | return tweet.parent ? : null 14 | } 15 | export function Tweet(props) { 16 | const {tweet, didRetweet, hideActions, isRetweet, retweeter} = props 17 | const [actionTweet, setActionTweet] = useState(props.tweet ? props.tweet : null) 18 | let className = props.className ? props.className : 'col-10 mx-auto col-md-6' 19 | className = isRetweet === true ? `${className} p-2 border rounded` : className 20 | const path = window.location.pathname 21 | const match = path.match(/(?\d+)/) 22 | const urlTweetId = match ? match.groups.tweetid : -1 23 | const isDetail = `${tweet.id}` === `${urlTweetId}` 24 | 25 | const handleLink = (event) => { 26 | event.preventDefault() 27 | window.location.href = `/${tweet.id}` 28 | } 29 | const handlePerformAction = (newActionTweet, status) => { 30 | if (status === 200){ 31 | setActionTweet(newActionTweet) 32 | } else if (status === 201) { 33 | if (didRetweet){ 34 | didRetweet(newActionTweet) 35 | } 36 | } 37 | 38 | } 39 | 40 | return
41 | {isRetweet === true &&
42 | Retweet via 43 |
} 44 |
45 | 46 |
47 | 48 |
49 |
50 |
51 | 52 |

53 | 54 |

55 |

{tweet.content}

56 | 57 | 58 |
59 |
60 | {(actionTweet && hideActions !== true) && 61 | 62 | 63 | 64 | 65 | } 66 | {isDetail === true ? null : } 67 |
68 |
69 |
70 |
71 | } 72 | -------------------------------------------------------------------------------- /tweetme2-web/src/tweets/feed.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react' 2 | 3 | import {apiTweetFeed} from './lookup' 4 | 5 | import {Tweet} from './detail' 6 | 7 | export function FeedList(props) { 8 | const [tweetsInit, setTweetsInit] = useState([]) 9 | const [tweets, setTweets] = useState([]) 10 | const [nextUrl, setNextUrl] = useState(null) 11 | const [tweetsDidSet, setTweetsDidSet] = useState(false) 12 | useEffect(()=>{ 13 | const final = [...props.newTweets].concat(tweetsInit) 14 | if (final.length !== tweets.length) { 15 | setTweets(final) 16 | } 17 | }, [props.newTweets, tweets, tweetsInit]) 18 | 19 | useEffect(() => { 20 | if (tweetsDidSet === false){ 21 | const handleTweetListLookup = (response, status) => { 22 | if (status === 200){ 23 | setNextUrl(response.next) 24 | setTweetsInit(response.results) 25 | setTweetsDidSet(true) 26 | } 27 | } 28 | apiTweetFeed(handleTweetListLookup) 29 | } 30 | }, [tweetsInit, tweetsDidSet, setTweetsDidSet, props.username]) 31 | 32 | 33 | const handleDidRetweet = (newTweet) => { 34 | const updateTweetsInit = [...tweetsInit] 35 | updateTweetsInit.unshift(newTweet) 36 | setTweetsInit(updateTweetsInit) 37 | const updateFinalTweets = [...tweets] 38 | updateFinalTweets.unshift(tweets) 39 | setTweets(updateFinalTweets) 40 | } 41 | const handleLoadNext = (event) => { 42 | event.preventDefault() 43 | if (nextUrl !== null) { 44 | const handleLoadNextResponse = (response, status) =>{ 45 | if (status === 200){ 46 | setNextUrl(response.next) 47 | const newTweets = [...tweets].concat(response.results) 48 | setTweetsInit(newTweets) 49 | setTweets(newTweets) 50 | } 51 | } 52 | apiTweetFeed(handleLoadNextResponse, nextUrl) 53 | } 54 | } 55 | 56 | return {tweets.map((item, index)=>{ 57 | return 62 | })} 63 | {nextUrl !== null && } 64 | 65 | } 66 | 67 | 68 | -------------------------------------------------------------------------------- /tweetme2-web/src/tweets/index.js: -------------------------------------------------------------------------------- 1 | import {ActionBtn} from './buttons' 2 | 3 | import {FeedComponent, TweetsComponent, TweetDetailComponent} from './components' 4 | import {TweetCreate} from './create' 5 | import {Tweet} from './detail' 6 | import {TweetsList} from './list' 7 | 8 | 9 | export { 10 | ActionBtn, 11 | FeedComponent, 12 | Tweet, 13 | TweetCreate, 14 | TweetsComponent, 15 | TweetDetailComponent, 16 | TweetsList, 17 | 18 | } -------------------------------------------------------------------------------- /tweetme2-web/src/tweets/list.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react' 2 | 3 | import {apiTweetList} from './lookup' 4 | 5 | import {Tweet} from './detail' 6 | 7 | export function TweetsList(props) { 8 | const [tweetsInit, setTweetsInit] = useState([]) 9 | const [tweets, setTweets] = useState([]) 10 | const [nextUrl, setNextUrl] = useState(null) 11 | const [tweetsDidSet, setTweetsDidSet] = useState(false) 12 | useEffect(()=>{ 13 | const final = [...props.newTweets].concat(tweetsInit) 14 | if (final.length !== tweets.length) { 15 | setTweets(final) 16 | } 17 | }, [props.newTweets, tweets, tweetsInit]) 18 | 19 | useEffect(() => { 20 | if (tweetsDidSet === false){ 21 | const handleTweetListLookup = (response, status) => { 22 | if (status === 200){ 23 | setNextUrl(response.next) 24 | setTweetsInit(response.results) 25 | setTweetsDidSet(true) 26 | } else { 27 | alert("There was an error") 28 | } 29 | } 30 | apiTweetList(props.username, handleTweetListLookup) 31 | } 32 | }, [tweetsInit, tweetsDidSet, setTweetsDidSet, props.username]) 33 | 34 | 35 | const handleDidRetweet = (newTweet) => { 36 | const updateTweetsInit = [...tweetsInit] 37 | updateTweetsInit.unshift(newTweet) 38 | setTweetsInit(updateTweetsInit) 39 | const updateFinalTweets = [...tweets] 40 | updateFinalTweets.unshift(tweets) 41 | setTweets(updateFinalTweets) 42 | } 43 | const handleLoadNext = (event) => { 44 | event.preventDefault() 45 | if (nextUrl !== null) { 46 | const handleLoadNextResponse = (response, status) =>{ 47 | if (status === 200){ 48 | setNextUrl(response.next) 49 | const newTweets = [...tweets].concat(response.results) 50 | setTweetsInit(newTweets) 51 | setTweets(newTweets) 52 | } else { 53 | alert("There was an error") 54 | } 55 | } 56 | apiTweetList(props.username, handleLoadNextResponse, nextUrl) 57 | } 58 | } 59 | 60 | return {tweets.map((item, index)=>{ 61 | return 66 | })} 67 | {nextUrl !== null && } 68 | 69 | } 70 | 71 | 72 | -------------------------------------------------------------------------------- /tweetme2-web/src/tweets/lookup.js: -------------------------------------------------------------------------------- 1 | import {backendLookup} from '../lookup' 2 | 3 | export function apiTweetCreate(newTweet, callback){ 4 | backendLookup("POST", "/tweets/create/", callback, {content: newTweet}) 5 | } 6 | 7 | export function apiTweetAction(tweetId, action, callback){ 8 | const data = {id: tweetId, action: action} 9 | backendLookup("POST", "/tweets/action/", callback, data) 10 | } 11 | 12 | export function apiTweetDetail(tweetId, callback) { 13 | backendLookup("GET", `/tweets/${tweetId}/`, callback) 14 | } 15 | 16 | export function apiTweetFeed(callback, nextUrl) { 17 | let endpoint = "/tweets/feed/" 18 | if (nextUrl !== null && nextUrl !== undefined) { 19 | endpoint = nextUrl.replace("http://localhost:8000/api", "") 20 | } 21 | backendLookup("GET", endpoint, callback) 22 | } 23 | 24 | 25 | export function apiTweetList(username, callback, nextUrl) { 26 | let endpoint = "/tweets/" 27 | if (username){ 28 | endpoint = `/tweets/?username=${username}` 29 | } 30 | if (nextUrl !== null && nextUrl !== undefined) { 31 | endpoint = nextUrl.replace("http://localhost:8000/api", "") 32 | } 33 | backendLookup("GET", endpoint, callback) 34 | } -------------------------------------------------------------------------------- /tweetme2/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Tweetme-2/50a6e18b77cb0333cda8c6e1f82649728771c6dc/tweetme2/__init__.py -------------------------------------------------------------------------------- /tweetme2/rest_api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Tweetme-2/50a6e18b77cb0333cda8c6e1f82649728771c6dc/tweetme2/rest_api/__init__.py -------------------------------------------------------------------------------- /tweetme2/rest_api/dev.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from rest_framework import authentication 3 | 4 | 5 | User = get_user_model() 6 | 7 | class DevAuthentication(authentication.BasicAuthentication): 8 | def authenticate(self, request): 9 | qs = User.objects.filter(id=2) 10 | user = qs.order_by("?").first() 11 | return (user, None) -------------------------------------------------------------------------------- /tweetme2/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for tweetme2 project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.2/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | # remember on same level as manage.py 17 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 18 | 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = '#)=h)_wc*k%f=wk+!$x0t%1wx7*_50$a1%*75s$og(8$27$ju1' 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = True 28 | 29 | ALLOWED_HOSTS = ['127.0.0.1', '.cfe.sh', 'localhost'] 30 | LOGIN_URL = "/login" 31 | 32 | MAX_TWEET_LENGTH = 240 33 | TWEET_ACTION_OPTIONS = ["like", "unlike", "retweet"] 34 | 35 | # Application definition 36 | 37 | INSTALLED_APPS = [ 38 | 'django.contrib.admin', 39 | 'django.contrib.auth', 40 | 'django.contrib.contenttypes', 41 | 'django.contrib.sessions', 42 | 'django.contrib.messages', 43 | 'django.contrib.staticfiles', 44 | # third-party 45 | 'corsheaders', 46 | 'rest_framework', 47 | # internal 48 | 'accounts', 49 | 'profiles', 50 | 'tweets', 51 | ] 52 | 53 | MIDDLEWARE = [ 54 | 'django.middleware.security.SecurityMiddleware', 55 | 'django.contrib.sessions.middleware.SessionMiddleware', 56 | 'corsheaders.middleware.CorsMiddleware', 57 | 'django.middleware.common.CommonMiddleware', 58 | 'django.middleware.csrf.CsrfViewMiddleware', 59 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 60 | 'django.contrib.messages.middleware.MessageMiddleware', 61 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 62 | ] 63 | 64 | ROOT_URLCONF = 'tweetme2.urls' 65 | 66 | TEMPLATES = [ 67 | { 68 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 69 | 'DIRS': [os.path.join(BASE_DIR, "templates")], 70 | 'APP_DIRS': True, 71 | 'OPTIONS': { 72 | 'context_processors': [ 73 | 'django.template.context_processors.debug', 74 | 'django.template.context_processors.request', 75 | 'django.contrib.auth.context_processors.auth', 76 | 'django.contrib.messages.context_processors.messages', 77 | ], 78 | }, 79 | }, 80 | ] 81 | 82 | WSGI_APPLICATION = 'tweetme2.wsgi.application' 83 | 84 | 85 | # Database 86 | # https://docs.djangoproject.com/en/2.2/ref/settings/#databases 87 | 88 | DATABASES = { 89 | 'default': { 90 | 'ENGINE': 'django.db.backends.sqlite3', 91 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 92 | } 93 | } 94 | 95 | 96 | # Password validation 97 | # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators 98 | 99 | AUTH_PASSWORD_VALIDATORS = [ 100 | { 101 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 102 | }, 103 | { 104 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 105 | }, 106 | { 107 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 108 | }, 109 | { 110 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 111 | }, 112 | ] 113 | 114 | 115 | # Internationalization 116 | # https://docs.djangoproject.com/en/2.2/topics/i18n/ 117 | 118 | LANGUAGE_CODE = 'en-us' 119 | 120 | TIME_ZONE = 'UTC' 121 | 122 | USE_I18N = True 123 | 124 | USE_L10N = True 125 | 126 | USE_TZ = True 127 | 128 | 129 | # Static files (CSS, JavaScript, Images) 130 | # https://docs.djangoproject.com/en/2.2/howto/static-files/ 131 | 132 | STATIC_URL = '/static/' 133 | 134 | STATICFILES_DIRS = [ 135 | os.path.join(BASE_DIR, "static"), 136 | ] 137 | STATIC_ROOT = os.path.join(BASE_DIR, "static-root") 138 | 139 | 140 | CORS_ORIGIN_ALLOW_ALL = True # any website has access to my api 141 | CORS_URLS_REGEX = r'^/api/.*$' 142 | 143 | 144 | DEFAULT_RENDERER_CLASSES = [ 145 | 'rest_framework.renderers.JSONRenderer', 146 | ] 147 | 148 | DEFAULT_AUTHENTICATION_CLASSES = [ 149 | 'rest_framework.authentication.SessionAuthentication' 150 | ] 151 | if DEBUG: 152 | DEFAULT_RENDERER_CLASSES += [ 153 | 'rest_framework.renderers.BrowsableAPIRenderer', 154 | ] 155 | # DEFAULT_AUTHENTICATION_CLASSES += [ 156 | # 'tweetme2.rest_api.dev.DevAuthentication' 157 | # ] 158 | REST_FRAMEWORK = { 159 | 160 | 'DEFAULT_AUTHENTICATION_CLASSES': DEFAULT_AUTHENTICATION_CLASSES, 161 | 'DEFAULT_RENDERER_CLASSES': DEFAULT_RENDERER_CLASSES 162 | } -------------------------------------------------------------------------------- /tweetme2/urls.py: -------------------------------------------------------------------------------- 1 | """tweetme2 URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.2/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.conf import settings 17 | from django.conf.urls.static import static 18 | from django.contrib import admin 19 | from django.urls import path, re_path, include # url() 20 | from django.views.generic import TemplateView 21 | 22 | from accounts.views import ( 23 | login_view, 24 | logout_view, 25 | register_view, 26 | ) 27 | 28 | from tweets.views import ( 29 | home_view, 30 | tweets_list_view, 31 | tweets_detail_view, 32 | ) 33 | 34 | urlpatterns = [ 35 | path('', home_view), 36 | path('admin/', admin.site.urls), 37 | path('global/', tweets_list_view), 38 | path('login/', login_view), 39 | path('logout/', logout_view), 40 | path('register/', register_view), 41 | path('', tweets_detail_view), 42 | re_path(r'profiles?/', include('profiles.urls')), 43 | path('api/tweets/', include('tweets.api.urls')), 44 | re_path(r'api/profiles?/', include('profiles.api.urls')), 45 | ] 46 | 47 | if settings.DEBUG: 48 | urlpatterns += static(settings.STATIC_URL, 49 | document_root=settings.STATIC_ROOT) -------------------------------------------------------------------------------- /tweetme2/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for tweetme2 project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tweetme2.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /tweets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Tweetme-2/50a6e18b77cb0333cda8c6e1f82649728771c6dc/tweets/__init__.py -------------------------------------------------------------------------------- /tweets/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | from .models import Tweet, TweetLike 5 | 6 | 7 | class TweetLikeAdmin(admin.TabularInline): 8 | model = TweetLike 9 | 10 | class TweetAdmin(admin.ModelAdmin): 11 | inlines = [TweetLikeAdmin] 12 | list_display = ['__str__', 'user'] 13 | search_fields = ['content', 'user__username', 'user__email'] 14 | class Meta: 15 | model = Tweet 16 | 17 | admin.site.register(Tweet, TweetAdmin) 18 | 19 | 20 | -------------------------------------------------------------------------------- /tweets/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Tweetme-2/50a6e18b77cb0333cda8c6e1f82649728771c6dc/tweets/api/__init__.py -------------------------------------------------------------------------------- /tweets/api/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import ( 4 | tweet_action_view, 5 | tweet_delete_view, 6 | tweet_detail_view, 7 | tweet_feed_view, 8 | tweet_list_view, 9 | tweet_create_view, 10 | ) 11 | ''' 12 | CLIENT 13 | Base ENDPOINT /api/tweets/ 14 | ''' 15 | urlpatterns = [ 16 | path('', tweet_list_view), 17 | path('feed/', tweet_feed_view), 18 | path('action/', tweet_action_view), 19 | path('create/', tweet_create_view), 20 | path('/', tweet_detail_view), 21 | path('/delete/', tweet_delete_view), 22 | ] 23 | -------------------------------------------------------------------------------- /tweets/api/views.py: -------------------------------------------------------------------------------- 1 | import random 2 | from django.conf import settings 3 | from django.http import HttpResponse, Http404, JsonResponse 4 | from django.shortcuts import render, redirect 5 | from django.utils.http import is_safe_url 6 | 7 | from rest_framework.authentication import SessionAuthentication 8 | from rest_framework.decorators import api_view, authentication_classes, permission_classes 9 | from rest_framework.pagination import PageNumberPagination 10 | from rest_framework.permissions import IsAuthenticated 11 | from rest_framework.response import Response 12 | from ..forms import TweetForm 13 | from ..models import Tweet 14 | from ..serializers import ( 15 | TweetSerializer, 16 | TweetActionSerializer, 17 | TweetCreateSerializer 18 | ) 19 | 20 | ALLOWED_HOSTS = settings.ALLOWED_HOSTS 21 | 22 | @api_view(['POST']) # http method the client == POST 23 | # @authentication_classes([SessionAuthentication, MyCustomAuth]) 24 | @permission_classes([IsAuthenticated]) # REST API course 25 | def tweet_create_view(request, *args, **kwargs): 26 | serializer = TweetCreateSerializer(data=request.data) 27 | if serializer.is_valid(raise_exception=True): 28 | serializer.save(user=request.user) 29 | return Response(serializer.data, status=201) 30 | return Response({}, status=400) 31 | 32 | @api_view(['GET']) 33 | def tweet_detail_view(request, tweet_id, *args, **kwargs): 34 | qs = Tweet.objects.filter(id=tweet_id) 35 | if not qs.exists(): 36 | return Response({}, status=404) 37 | obj = qs.first() 38 | serializer = TweetSerializer(obj) 39 | return Response(serializer.data, status=200) 40 | 41 | @api_view(['DELETE', 'POST']) 42 | @permission_classes([IsAuthenticated]) 43 | def tweet_delete_view(request, tweet_id, *args, **kwargs): 44 | qs = Tweet.objects.filter(id=tweet_id) 45 | if not qs.exists(): 46 | return Response({}, status=404) 47 | qs = qs.filter(user=request.user) 48 | if not qs.exists(): 49 | return Response({"message": "You cannot delete this tweet"}, status=401) 50 | obj = qs.first() 51 | obj.delete() 52 | return Response({"message": "Tweet removed"}, status=200) 53 | 54 | @api_view(['POST']) 55 | @permission_classes([IsAuthenticated]) 56 | def tweet_action_view(request, *args, **kwargs): 57 | ''' 58 | id is required. 59 | Action options are: like, unlike, retweet 60 | ''' 61 | serializer = TweetActionSerializer(data=request.data) 62 | if serializer.is_valid(raise_exception=True): 63 | data = serializer.validated_data 64 | tweet_id = data.get("id") 65 | action = data.get("action") 66 | content = data.get("content") 67 | qs = Tweet.objects.filter(id=tweet_id) 68 | if not qs.exists(): 69 | return Response({}, status=404) 70 | obj = qs.first() 71 | if action == "like": 72 | obj.likes.add(request.user) 73 | serializer = TweetSerializer(obj) 74 | return Response(serializer.data, status=200) 75 | elif action == "unlike": 76 | obj.likes.remove(request.user) 77 | serializer = TweetSerializer(obj) 78 | return Response(serializer.data, status=200) 79 | elif action == "retweet": 80 | new_tweet = Tweet.objects.create( 81 | user=request.user, 82 | parent=obj, 83 | content=content, 84 | ) 85 | serializer = TweetSerializer(new_tweet) 86 | return Response(serializer.data, status=201) 87 | return Response({}, status=200) 88 | 89 | 90 | def get_paginated_queryset_response(qs, request): 91 | paginator = PageNumberPagination() 92 | paginator.page_size = 20 93 | paginated_qs = paginator.paginate_queryset(qs, request) 94 | serializer = TweetSerializer(paginated_qs, many=True, context={"request": request}) 95 | return paginator.get_paginated_response(serializer.data) # Response( serializer.data, status=200) 96 | 97 | 98 | @api_view(['GET']) 99 | @permission_classes([IsAuthenticated]) 100 | def tweet_feed_view(request, *args, **kwargs): 101 | user = request.user 102 | qs = Tweet.objects.feed(user) 103 | return get_paginated_queryset_response(qs, request) 104 | 105 | @api_view(['GET']) 106 | def tweet_list_view(request, *args, **kwargs): 107 | qs = Tweet.objects.all() 108 | username = request.GET.get('username') # ?username=Justin 109 | if username != None: 110 | qs = qs.by_username(username) 111 | return get_paginated_queryset_response(qs, request) 112 | 113 | 114 | 115 | def tweet_create_view_pure_django(request, *args, **kwargs): 116 | ''' 117 | REST API Create View -> DRF 118 | ''' 119 | user = request.user 120 | if not request.user.is_authenticated: 121 | user = None 122 | if request.is_ajax(): 123 | return JsonResponse({}, status=401) 124 | return redirect(settings.LOGIN_URL) 125 | form = TweetForm(request.POST or None) 126 | next_url = request.POST.get("next") or None 127 | if form.is_valid(): 128 | obj = form.save(commit=False) 129 | # do other form related logic 130 | obj.user = user 131 | obj.save() 132 | if request.is_ajax(): 133 | return JsonResponse(obj.serialize(), status=201) # 201 == created items 134 | if next_url != None and is_safe_url(next_url, ALLOWED_HOSTS): 135 | return redirect(next_url) 136 | form = TweetForm() 137 | if form.errors: 138 | if request.is_ajax(): 139 | return JsonResponse(form.errors, status=400) 140 | return render(request, 'components/form.html', context={"form": form}) 141 | 142 | 143 | def tweet_list_view_pure_django(request, *args, **kwargs): 144 | """ 145 | REST API VIEW 146 | Consume by JavaScript or Swift/Java/iOS/Andriod 147 | return json data 148 | """ 149 | qs = Tweet.objects.all() 150 | tweets_list = [x.serialize() for x in qs] 151 | data = { 152 | "isUser": False, 153 | "response": tweets_list 154 | } 155 | return JsonResponse(data) 156 | 157 | 158 | def tweet_detail_view_pure_django(request, tweet_id, *args, **kwargs): 159 | """ 160 | REST API VIEW 161 | Consume by JavaScript or Swift/Java/iOS/Andriod 162 | return json data 163 | """ 164 | data = { 165 | "id": tweet_id, 166 | } 167 | status = 200 168 | try: 169 | obj = Tweet.objects.get(id=tweet_id) 170 | data['content'] = obj.content 171 | except: 172 | data['message'] = "Not found" 173 | status = 404 174 | return JsonResponse(data, status=status) # json.dumps content_type='application/json' -------------------------------------------------------------------------------- /tweets/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TweetsConfig(AppConfig): 5 | name = 'tweets' 6 | -------------------------------------------------------------------------------- /tweets/forms.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django import forms 3 | 4 | from .models import Tweet 5 | 6 | MAX_TWEET_LENGTH = settings.MAX_TWEET_LENGTH 7 | 8 | class TweetForm(forms.ModelForm): 9 | class Meta: 10 | model = Tweet 11 | fields = ['content'] 12 | 13 | def clean_content(self): 14 | content = self.cleaned_data.get("content") 15 | if len(content) > MAX_TWEET_LENGTH: 16 | raise forms.ValidationError("This tweet is too long") 17 | return content -------------------------------------------------------------------------------- /tweets/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2019-12-14 00:03 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Tweet', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('content', models.TextField(blank=True, null=True)), 19 | ('image', models.FileField(blank=True, null=True, upload_to='images/')), 20 | ], 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /tweets/migrations/0002_auto_20191215_2304.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2019-12-15 23:04 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('tweets', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='tweet', 15 | options={'ordering': ['-id']}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /tweets/migrations/0003_tweet_user.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2019-12-16 22:58 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('tweets', '0002_auto_20191215_2304'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='tweet', 18 | name='user', 19 | field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), 20 | preserve_default=False, 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /tweets/migrations/0004_auto_20191217_1953.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2019-12-17 19:53 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | import django.utils.timezone 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 13 | ('tweets', '0003_tweet_user'), 14 | ] 15 | 16 | operations = [ 17 | migrations.AddField( 18 | model_name='tweet', 19 | name='timestamp', 20 | field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), 21 | preserve_default=False, 22 | ), 23 | migrations.CreateModel( 24 | name='TweetLike', 25 | fields=[ 26 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 27 | ('timestamp', models.DateTimeField(auto_now_add=True)), 28 | ('tweet', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tweets.Tweet')), 29 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 30 | ], 31 | ), 32 | migrations.AddField( 33 | model_name='tweet', 34 | name='likes', 35 | field=models.ManyToManyField(blank=True, related_name='tweet_user', through='tweets.TweetLike', to=settings.AUTH_USER_MODEL), 36 | ), 37 | ] 38 | -------------------------------------------------------------------------------- /tweets/migrations/0005_tweet_parent.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2019-12-17 23:08 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('tweets', '0004_auto_20191217_1953'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='tweet', 16 | name='parent', 17 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='tweets.Tweet'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /tweets/migrations/0006_auto_20200108_2040.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2020-01-08 20:40 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('tweets', '0005_tweet_parent'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='tweet', 17 | name='user', 18 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tweets', to=settings.AUTH_USER_MODEL), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /tweets/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Tweetme-2/50a6e18b77cb0333cda8c6e1f82649728771c6dc/tweets/migrations/__init__.py -------------------------------------------------------------------------------- /tweets/models.py: -------------------------------------------------------------------------------- 1 | import random 2 | from django.conf import settings 3 | from django.db import models 4 | from django.db.models import Q 5 | 6 | User = settings.AUTH_USER_MODEL 7 | 8 | class TweetLike(models.Model): 9 | user = models.ForeignKey(User, on_delete=models.CASCADE) 10 | tweet = models.ForeignKey("Tweet", on_delete=models.CASCADE) 11 | timestamp = models.DateTimeField(auto_now_add=True) 12 | 13 | class TweetQuerySet(models.QuerySet): 14 | def by_username(self, username): 15 | return self.filter(user__username__iexact=username) 16 | 17 | def feed(self, user): 18 | profiles_exist = user.following.exists() 19 | followed_users_id = [] 20 | if profiles_exist: 21 | followed_users_id = user.following.values_list("user__id", flat=True) # [x.user.id for x in profiles] 22 | return self.filter( 23 | Q(user__id__in=followed_users_id) | 24 | Q(user=user) 25 | ).distinct().order_by("-timestamp") 26 | 27 | class TweetManager(models.Manager): 28 | def get_queryset(self, *args, **kwargs): 29 | return TweetQuerySet(self.model, using=self._db) 30 | 31 | def feed(self, user): 32 | return self.get_queryset().feed(user) 33 | 34 | class Tweet(models.Model): 35 | # Maps to SQL data 36 | # id = models.AutoField(primary_key=True) 37 | parent = models.ForeignKey("self", null=True, on_delete=models.SET_NULL) 38 | user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="tweets") # many users can many tweets 39 | likes = models.ManyToManyField(User, related_name='tweet_user', blank=True, through=TweetLike) 40 | content = models.TextField(blank=True, null=True) 41 | image = models.FileField(upload_to='images/', blank=True, null=True) 42 | timestamp = models.DateTimeField(auto_now_add=True) 43 | 44 | objects = TweetManager() 45 | # def __str__(self): 46 | # return self.content 47 | 48 | class Meta: 49 | ordering = ['-id'] 50 | 51 | @property 52 | def is_retweet(self): 53 | return self.parent != None 54 | 55 | def serialize(self): 56 | ''' 57 | Feel free to delete! 58 | ''' 59 | return { 60 | "id": self.id, 61 | "content": self.content, 62 | "likes": random.randint(0, 200) 63 | } -------------------------------------------------------------------------------- /tweets/serializers.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from rest_framework import serializers 3 | from profiles.serializers import PublicProfileSerializer 4 | from .models import Tweet 5 | 6 | MAX_TWEET_LENGTH = settings.MAX_TWEET_LENGTH 7 | TWEET_ACTION_OPTIONS = settings.TWEET_ACTION_OPTIONS 8 | 9 | class TweetActionSerializer(serializers.Serializer): 10 | id = serializers.IntegerField() 11 | action = serializers.CharField() 12 | content = serializers.CharField(allow_blank=True, required=False) 13 | 14 | def validate_action(self, value): 15 | value = value.lower().strip() # "Like " -> "like" 16 | if not value in TWEET_ACTION_OPTIONS: 17 | raise serializers.ValidationError("This is not a valid action for tweets") 18 | return value 19 | 20 | 21 | class TweetCreateSerializer(serializers.ModelSerializer): 22 | user = PublicProfileSerializer(source='user.profile', read_only=True) # serializers.SerializerMethodField(read_only=True) 23 | likes = serializers.SerializerMethodField(read_only=True) 24 | 25 | class Meta: 26 | model = Tweet 27 | fields = ['user', 'id', 'content', 'likes', 'timestamp'] 28 | 29 | def get_likes(self, obj): 30 | return obj.likes.count() 31 | 32 | def validate_content(self, value): 33 | if len(value) > MAX_TWEET_LENGTH: 34 | raise serializers.ValidationError("This tweet is too long") 35 | return value 36 | 37 | # def get_user(self, obj): 38 | # return obj.user.id 39 | 40 | 41 | class TweetSerializer(serializers.ModelSerializer): 42 | user = PublicProfileSerializer(source='user.profile', read_only=True) 43 | likes = serializers.SerializerMethodField(read_only=True) 44 | parent = TweetCreateSerializer(read_only=True) 45 | class Meta: 46 | model = Tweet 47 | fields = [ 48 | 'user', 49 | 'id', 50 | 'content', 51 | 'likes', 52 | 'is_retweet', 53 | 'parent', 54 | 'timestamp'] 55 | 56 | def get_likes(self, obj): 57 | return obj.likes.count() 58 | -------------------------------------------------------------------------------- /tweets/tests.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.test import TestCase 3 | 4 | from rest_framework.test import APIClient 5 | 6 | from .models import Tweet 7 | # Create your tests here. 8 | User = get_user_model() 9 | 10 | class TweetTestCase(TestCase): 11 | def setUp(self): 12 | self.user = User.objects.create_user(username='cfe', password='somepassword') 13 | self.userb = User.objects.create_user(username='cfe-2', password='somepassword2') 14 | Tweet.objects.create(content="my first tweet", 15 | user=self.user) 16 | Tweet.objects.create(content="my first tweet", 17 | user=self.user) 18 | Tweet.objects.create(content="my first tweet", 19 | user=self.userb) 20 | self.currentCount = Tweet.objects.all().count() 21 | 22 | def test_tweet_created(self): 23 | tweet_obj = Tweet.objects.create(content="my second tweet", 24 | user=self.user) 25 | self.assertEqual(tweet_obj.id, 4) 26 | self.assertEqual(tweet_obj.user, self.user) 27 | 28 | def get_client(self): 29 | client = APIClient() 30 | client.login(username=self.user.username, password='somepassword') 31 | return client 32 | 33 | def test_tweet_list(self): 34 | client = self.get_client() 35 | response = client.get("/api/tweets/") 36 | self.assertEqual(response.status_code, 200) 37 | self.assertEqual(len(response.json()), 1) 38 | 39 | def test_tweet_list(self): 40 | client = self.get_client() 41 | response = client.get("/api/tweets/") 42 | self.assertEqual(response.status_code, 200) 43 | self.assertEqual(len(response.json()), 3) 44 | 45 | def test_tweets_related_name(self): 46 | user = self.user 47 | self.assertEqual(user.tweets.count(), 2) 48 | 49 | def test_action_like(self): 50 | client = self.get_client() 51 | response = client.post("/api/tweets/action/", 52 | {"id": 1, "action": "like"}) 53 | like_count = response.json().get("likes") 54 | user = self.user 55 | my_like_instances_count = user.tweetlike_set.count() 56 | my_related_likes = user.tweet_user.count() 57 | self.assertEqual(response.status_code, 200) 58 | self.assertEqual(like_count, 1) 59 | self.assertEqual(my_like_instances_count, 1) 60 | self.assertEqual(my_like_instances_count, my_related_likes) 61 | 62 | def test_action_unlike(self): 63 | client = self.get_client() 64 | response = client.post("/api/tweets/action/", 65 | {"id": 2, "action": "like"}) 66 | self.assertEqual(response.status_code, 200) 67 | response = client.post("/api/tweets/action/", 68 | {"id": 2, "action": "unlike"}) 69 | self.assertEqual(response.status_code, 200) 70 | like_count = response.json().get("likes") 71 | self.assertEqual(like_count, 0) 72 | 73 | def test_action_retweet(self): 74 | client = self.get_client() 75 | response = client.post("/api/tweets/action/", 76 | {"id": 2, "action": "retweet"}) 77 | self.assertEqual(response.status_code, 201) 78 | data = response.json() 79 | new_tweet_id = data.get("id") 80 | self.assertNotEqual(2, new_tweet_id) 81 | self.assertEqual(self.currentCount + 1, new_tweet_id) 82 | 83 | def test_tweet_create_api_view(self): 84 | request_data = {"content": "This is my test tweet"} 85 | client = self.get_client() 86 | response = client.post("/api/tweets/create/", request_data) 87 | self.assertEqual(response.status_code, 201) 88 | response_data = response.json() 89 | new_tweet_id = response_data.get("id") 90 | self.assertEqual(self.currentCount + 1, new_tweet_id) 91 | 92 | def test_tweet_detail_api_view(self): 93 | client = self.get_client() 94 | response = client.get("/api/tweets/1/") 95 | self.assertEqual(response.status_code, 200) 96 | data = response.json() 97 | _id = data.get("id") 98 | self.assertEqual(_id, 1) 99 | 100 | def test_tweet_delete_api_view(self): 101 | client = self.get_client() 102 | response = client.delete("/api/tweets/1/delete/") 103 | self.assertEqual(response.status_code, 200) 104 | client = self.get_client() 105 | response = client.delete("/api/tweets/1/delete/") 106 | self.assertEqual(response.status_code, 404) 107 | response_incorrect_owner = client.delete("/api/tweets/3/delete/") 108 | self.assertEqual(response_incorrect_owner.status_code, 401) -------------------------------------------------------------------------------- /tweets/views.py: -------------------------------------------------------------------------------- 1 | import random 2 | from django.conf import settings 3 | from django.http import HttpResponse, Http404, JsonResponse 4 | from django.shortcuts import render, redirect 5 | from django.utils.http import is_safe_url 6 | 7 | 8 | ALLOWED_HOSTS = settings.ALLOWED_HOSTS 9 | 10 | # Create your views here. 11 | def home_view(request, *args, **kwargs): 12 | return render(request, "pages/feed.html") 13 | 14 | def tweets_list_view(request, *args, **kwargs): 15 | return render(request, "tweets/list.html") 16 | 17 | def tweets_detail_view(request, tweet_id, *args, **kwargs): 18 | return render(request, "tweets/detail.html", context={"tweet_id": tweet_id}) 19 | --------------------------------------------------------------------------------