├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .prettierrc ├── Pipfile ├── Pipfile.lock ├── README.md ├── leadmanager ├── accounts │ ├── __init__.py │ ├── admin.py │ ├── api.py │ ├── apps.py │ ├── migrations │ │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── frontend │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ └── __init__.py │ ├── models.py │ ├── src │ │ ├── actions │ │ │ ├── auth.js │ │ │ ├── leads.js │ │ │ ├── messages.js │ │ │ └── types.js │ │ ├── components │ │ │ ├── App.js │ │ │ ├── accounts │ │ │ │ ├── Login.js │ │ │ │ └── Register.js │ │ │ ├── common │ │ │ │ └── PrivateRoute.js │ │ │ ├── layout │ │ │ │ ├── Alerts.js │ │ │ │ └── Header.js │ │ │ └── leads │ │ │ │ ├── Dashboard.js │ │ │ │ ├── Form.js │ │ │ │ └── Leads.js │ │ ├── index.js │ │ ├── reducers │ │ │ ├── auth.js │ │ │ ├── errors.js │ │ │ ├── index.js │ │ │ ├── leads.js │ │ │ └── messages.js │ │ └── store.js │ ├── static │ │ └── frontend │ │ │ └── main.js │ ├── templates │ │ └── frontend │ │ │ └── index.html │ ├── tests.py │ ├── urls.py │ └── views.py ├── leadmanager │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── leads │ ├── __init__.py │ ├── admin.py │ ├── api.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_lead_owner.py │ │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── tests.py │ ├── urls.py │ └── views.py └── manage.py ├── package-lock.json ├── package.json └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": ["transform-class-properties"] 4 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | max_line_length = 100 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | max_line_length = 0 15 | trim_trailing_whitespace = false 16 | 17 | [{Makefile, **.mk}] 18 | # Use tabs for indentation (Makefiles require tabs) 19 | indent_style = tab 20 | 21 | [*.scss] 22 | indent_size = 2 23 | indent_style = space -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "parserOptions": { 4 | "sourceType": "module", 5 | "allowImportExportEverywhere": false, 6 | "codeFrame": false 7 | }, 8 | "extends": [ 9 | "airbnb", 10 | "prettier" 11 | ], 12 | "env": { 13 | "browser": true, 14 | "jest": true 15 | }, 16 | "rules": { 17 | "max-len": [ 18 | "error", 19 | { 20 | "code": 100 21 | } 22 | ], 23 | "prefer-promise-reject-errors": [ 24 | "off" 25 | ], 26 | "react/jsx-filename-extension": [ 27 | "off" 28 | ], 29 | "react/prop-types": [ 30 | "warn" 31 | ], 32 | "no-return-assign": [ 33 | "off" 34 | ] 35 | } 36 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | ### Django ### 4 | *.log 5 | *.pot 6 | *.pyc 7 | __pycache__/ 8 | local_settings.py 9 | db.sqlite3 10 | media 11 | 12 | # If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/ 13 | # in your Git repository. Update and uncomment the following line accordingly. 14 | # /staticfiles/ 15 | 16 | ### Django.Python Stack ### 17 | # Byte-compiled / optimized / DLL files 18 | *.py[cod] 19 | *$py.class 20 | 21 | # C extensions 22 | *.so 23 | 24 | # Distribution / packaging 25 | .Python 26 | build/ 27 | develop-eggs/ 28 | dist/ 29 | downloads/ 30 | eggs/ 31 | .eggs/ 32 | lib/ 33 | lib64/ 34 | parts/ 35 | sdist/ 36 | var/ 37 | wheels/ 38 | share/python-wheels/ 39 | *.egg-info/ 40 | .installed.cfg 41 | *.egg 42 | MANIFEST 43 | 44 | # PyInstaller 45 | # Usually these files are written by a python script from a template 46 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 47 | *.manifest 48 | *.spec 49 | 50 | # Installer logs 51 | pip-log.txt 52 | pip-delete-this-directory.txt 53 | 54 | # Unit test / coverage reports 55 | htmlcov/ 56 | .tox/ 57 | .nox/ 58 | .coverage 59 | .coverage.* 60 | .cache 61 | nosetests.xml 62 | coverage.xml 63 | *.cover 64 | .hypothesis/ 65 | .pytest_cache/ 66 | 67 | # Translations 68 | *.mo 69 | 70 | # Django stuff: 71 | 72 | # Flask stuff: 73 | instance/ 74 | .webassets-cache 75 | 76 | # Scrapy stuff: 77 | .scrapy 78 | 79 | # Sphinx documentation 80 | docs/_build/ 81 | 82 | # PyBuilder 83 | target/ 84 | 85 | # Jupyter Notebook 86 | .ipynb_checkpoints 87 | 88 | # IPython 89 | profile_default/ 90 | ipython_config.py 91 | 92 | # pyenv 93 | .python-version 94 | 95 | # celery beat schedule file 96 | celerybeat-schedule 97 | 98 | # SageMath parsed files 99 | *.sage.py 100 | 101 | # Environments 102 | .env 103 | .venv 104 | env/ 105 | venv/ 106 | ENV/ 107 | env.bak/ 108 | venv.bak/ 109 | 110 | # Spyder project settings 111 | .spyderproject 112 | .spyproject 113 | 114 | # Rope project settings 115 | .ropeproject 116 | 117 | # mkdocs documentation 118 | /site 119 | 120 | # mypy 121 | .mypy_cache/ 122 | .dmypy.json 123 | dmypy.json 124 | 125 | # Pyre type checker 126 | .pyre/ 127 | 128 | .DS_STORE 129 | scripts/flow/*/.flowconfig 130 | *~ 131 | .grunt 132 | _SpecRunner.html 133 | __benchmarks__ 134 | remote-repo/ 135 | coverage/ 136 | .module-cache 137 | fixtures/dom/public/react-dom.js 138 | fixtures/dom/public/react.js 139 | test/the-files-to-test.generated.js 140 | *.log* 141 | chrome-user-data 142 | *.sublime-project 143 | *.sublime-workspace 144 | .idea 145 | *.iml 146 | .vscode 147 | *.swp 148 | *.swo 149 | /.pnp 150 | .pnp.js 151 | 152 | .env.local 153 | .env.development.local 154 | .env.test.local 155 | .env.production.local 156 | 157 | npm-debug.log* 158 | yarn-debug.log* 159 | yarn-error.log* 160 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "trailingComma": "all", 4 | "tabWidth": 2, 5 | "semi": true, 6 | "singleQuote": true 7 | } -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | django = "*" 10 | djangorestframework = "*" 11 | django-rest-knox = "*" 12 | psycopg2 = "*" 13 | 14 | [requires] 15 | python_version = "3.8" 16 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "5477e1b3f85e9e8b4c13530def4e58170bb975212a1d692d4fe760615cc7f839" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.8" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "asgiref": { 20 | "hashes": [ 21 | "sha256:a5098bc870b80e7b872bff60bb363c7f2c2c89078759f6c47b53ff8c525a152e", 22 | "sha256:cd88907ecaec59d78e4ac00ea665b03e571cb37e3a0e37b3702af1a9e86c365a" 23 | ], 24 | "version": "==3.3.0" 25 | }, 26 | "cffi": { 27 | "hashes": [ 28 | "sha256:005f2bfe11b6745d726dbb07ace4d53f057de66e336ff92d61b8c7e9c8f4777d", 29 | "sha256:09e96138280241bd355cd585148dec04dbbedb4f46128f340d696eaafc82dd7b", 30 | "sha256:0b1ad452cc824665ddc682400b62c9e4f5b64736a2ba99110712fdee5f2505c4", 31 | "sha256:0ef488305fdce2580c8b2708f22d7785ae222d9825d3094ab073e22e93dfe51f", 32 | "sha256:15f351bed09897fbda218e4db5a3d5c06328862f6198d4fb385f3e14e19decb3", 33 | "sha256:22399ff4870fb4c7ef19fff6eeb20a8bbf15571913c181c78cb361024d574579", 34 | "sha256:23e5d2040367322824605bc29ae8ee9175200b92cb5483ac7d466927a9b3d537", 35 | "sha256:2791f68edc5749024b4722500e86303a10d342527e1e3bcac47f35fbd25b764e", 36 | "sha256:2f9674623ca39c9ebe38afa3da402e9326c245f0f5ceff0623dccdac15023e05", 37 | "sha256:3363e77a6176afb8823b6e06db78c46dbc4c7813b00a41300a4873b6ba63b171", 38 | "sha256:33c6cdc071ba5cd6d96769c8969a0531be2d08c2628a0143a10a7dcffa9719ca", 39 | "sha256:3b8eaf915ddc0709779889c472e553f0d3e8b7bdf62dab764c8921b09bf94522", 40 | "sha256:3cb3e1b9ec43256c4e0f8d2837267a70b0e1ca8c4f456685508ae6106b1f504c", 41 | "sha256:3eeeb0405fd145e714f7633a5173318bd88d8bbfc3dd0a5751f8c4f70ae629bc", 42 | "sha256:44f60519595eaca110f248e5017363d751b12782a6f2bd6a7041cba275215f5d", 43 | "sha256:4d7c26bfc1ea9f92084a1d75e11999e97b62d63128bcc90c3624d07813c52808", 44 | "sha256:529c4ed2e10437c205f38f3691a68be66c39197d01062618c55f74294a4a4828", 45 | "sha256:6642f15ad963b5092d65aed022d033c77763515fdc07095208f15d3563003869", 46 | "sha256:85ba797e1de5b48aa5a8427b6ba62cf69607c18c5d4eb747604b7302f1ec382d", 47 | "sha256:8f0f1e499e4000c4c347a124fa6a27d37608ced4fe9f7d45070563b7c4c370c9", 48 | "sha256:a624fae282e81ad2e4871bdb767e2c914d0539708c0f078b5b355258293c98b0", 49 | "sha256:b0358e6fefc74a16f745afa366acc89f979040e0cbc4eec55ab26ad1f6a9bfbc", 50 | "sha256:bbd2f4dfee1079f76943767fce837ade3087b578aeb9f69aec7857d5bf25db15", 51 | "sha256:bf39a9e19ce7298f1bd6a9758fa99707e9e5b1ebe5e90f2c3913a47bc548747c", 52 | "sha256:c11579638288e53fc94ad60022ff1b67865363e730ee41ad5e6f0a17188b327a", 53 | "sha256:c150eaa3dadbb2b5339675b88d4573c1be3cb6f2c33a6c83387e10cc0bf05bd3", 54 | "sha256:c53af463f4a40de78c58b8b2710ade243c81cbca641e34debf3396a9640d6ec1", 55 | "sha256:cb763ceceae04803adcc4e2d80d611ef201c73da32d8f2722e9d0ab0c7f10768", 56 | "sha256:cc75f58cdaf043fe6a7a6c04b3b5a0e694c6a9e24050967747251fb80d7bce0d", 57 | "sha256:d80998ed59176e8cba74028762fbd9b9153b9afc71ea118e63bbf5d4d0f9552b", 58 | "sha256:de31b5164d44ef4943db155b3e8e17929707cac1e5bd2f363e67a56e3af4af6e", 59 | "sha256:e66399cf0fc07de4dce4f588fc25bfe84a6d1285cc544e67987d22663393926d", 60 | "sha256:f0620511387790860b249b9241c2f13c3a80e21a73e0b861a2df24e9d6f56730", 61 | "sha256:f4eae045e6ab2bb54ca279733fe4eb85f1effda392666308250714e01907f394", 62 | "sha256:f92cdecb618e5fa4658aeb97d5eb3d2f47aa94ac6477c6daf0f306c5a3b9e6b1", 63 | "sha256:f92f789e4f9241cd262ad7a555ca2c648a98178a953af117ef7fad46aa1d5591" 64 | ], 65 | "version": "==1.14.3" 66 | }, 67 | "cryptography": { 68 | "hashes": [ 69 | "sha256:22f8251f68953553af4f9c11ec5f191198bc96cff9f0ac5dd5ff94daede0ee6d", 70 | "sha256:284e275e3c099a80831f9898fb5c9559120d27675c3521278faba54e584a7832", 71 | "sha256:3e17d02941c0f169c5b877597ca8be895fca0e5e3eb882526a74aa4804380a98", 72 | "sha256:52a47e60953679eea0b4d490ca3c241fb1b166a7b161847ef4667dfd49e7699d", 73 | "sha256:57b8c1ed13b8aa386cabbfde3be175d7b155682470b0e259fecfe53850967f8a", 74 | "sha256:6a8f64ed096d13f92d1f601a92d9fd1f1025dc73a2ca1ced46dcf5e0d4930943", 75 | "sha256:6e8a3c7c45101a7eeee93102500e1b08f2307c717ff553fcb3c1127efc9b6917", 76 | "sha256:7ef41304bf978f33cfb6f43ca13bb0faac0c99cda33693aa20ad4f5e34e8cb8f", 77 | "sha256:87c2fffd61e934bc0e2c927c3764c20b22d7f5f7f812ee1a477de4c89b044ca6", 78 | "sha256:88069392cd9a1e68d2cfd5c3a2b0d72a44ef3b24b8977a4f7956e9e3c4c9477a", 79 | "sha256:8a0866891326d3badb17c5fd3e02c926b635e8923fa271b4813cd4d972a57ff3", 80 | "sha256:8f0fd8b0751d75c4483c534b209e39e918f0d14232c0d8a2a76e687f64ced831", 81 | "sha256:9a07e6d255053674506091d63ab4270a119e9fc83462c7ab1dbcb495b76307af", 82 | "sha256:9a8580c9afcdcddabbd064c0a74f337af74ff4529cdf3a12fa2e9782d677a2e5", 83 | "sha256:bd80bc156d3729b38cb227a5a76532aef693b7ac9e395eea8063ee50ceed46a5", 84 | "sha256:d1cbc3426e6150583b22b517ef3720036d7e3152d428c864ff0f3fcad2b97591", 85 | "sha256:e15ac84dcdb89f92424cbaca4b0b34e211e7ce3ee7b0ec0e4f3c55cee65fae5a", 86 | "sha256:e4789b84f8dedf190148441f7c5bfe7244782d9cbb194a36e17b91e7d3e1cca9", 87 | "sha256:f01c9116bfb3ad2831e125a73dcd957d173d6ddca7701528eff1e7d97972872c", 88 | "sha256:f0e3986f6cce007216b23c490f093f35ce2068f3c244051e559f647f6731b7ae", 89 | "sha256:f2aa3f8ba9e2e3fd49bd3de743b976ab192fbf0eb0348cebde5d2a9de0090a9f", 90 | "sha256:fb70a4cedd69dc52396ee114416a3656e011fb0311fca55eb55c7be6ed9c8aef" 91 | ], 92 | "index": "pypi", 93 | "version": "==3.2" 94 | }, 95 | "django": { 96 | "hashes": [ 97 | "sha256:642d8eceab321ca743ae71e0f985ff8fdca59f07aab3a9fb362c617d23e33a76", 98 | "sha256:d4666c2edefa38c5ede0ec1655424c56dc47ceb04b6d8d62a7eac09db89545c1" 99 | ], 100 | "index": "pypi", 101 | "version": "==3.0.5" 102 | }, 103 | "django-rest-knox": { 104 | "hashes": [ 105 | "sha256:4a57b05b04fcccc41fcc969ad0e3f180467d7045b45003b58f5a437d6d3370d4", 106 | "sha256:e4ac93e6d7dd63af5b58ff19d7f197788a874e932464d77db5630f185365eac0", 107 | "sha256:f7dac2f7a6ece7c1bd331bdb01dc3d16e0b340f3d1001a7285cc13b171d539a3" 108 | ], 109 | "index": "pypi", 110 | "version": "==4.1.0" 111 | }, 112 | "djangorestframework": { 113 | "hashes": [ 114 | "sha256:05809fc66e1c997fd9a32ea5730d9f4ba28b109b9da71fccfa5ff241201fd0a4", 115 | "sha256:e782087823c47a26826ee5b6fa0c542968219263fb3976ec3c31edab23a4001f" 116 | ], 117 | "index": "pypi", 118 | "version": "==3.11.0" 119 | }, 120 | "psycopg2": { 121 | "hashes": [ 122 | "sha256:4212ca404c4445dc5746c0d68db27d2cbfb87b523fe233dc84ecd24062e35677", 123 | "sha256:47fc642bf6f427805daf52d6e52619fe0637648fe27017062d898f3bf891419d", 124 | "sha256:72772181d9bad1fa349792a1e7384dde56742c14af2b9986013eb94a240f005b", 125 | "sha256:8396be6e5ff844282d4d49b81631772f80dabae5658d432202faf101f5283b7c", 126 | "sha256:893c11064b347b24ecdd277a094413e1954f8a4e8cdaf7ffbe7ca3db87c103f0", 127 | "sha256:92a07dfd4d7c325dd177548c4134052d4842222833576c8391aab6f74038fc3f", 128 | "sha256:965c4c93e33e6984d8031f74e51227bd755376a9df6993774fd5b6fb3288b1f4", 129 | "sha256:9ab75e0b2820880ae24b7136c4d230383e07db014456a476d096591172569c38", 130 | "sha256:b0845e3bdd4aa18dc2f9b6fb78fbd3d9d371ad167fd6d1b7ad01c0a6cdad4fc6", 131 | "sha256:dca2d7203f0dfce8ea4b3efd668f8ea65cd2b35112638e488a4c12594015f67b", 132 | "sha256:ed686e5926929887e2c7ae0a700e32c6129abb798b4ad2b846e933de21508151", 133 | "sha256:ef6df7e14698e79c59c7ee7cf94cd62e5b869db369ed4b1b8f7b729ea825712a", 134 | "sha256:f898e5cc0a662a9e12bde6f931263a1bbd350cfb18e1d5336a12927851825bb6" 135 | ], 136 | "index": "pypi", 137 | "version": "==2.8.4" 138 | }, 139 | "pycparser": { 140 | "hashes": [ 141 | "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", 142 | "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" 143 | ], 144 | "version": "==2.20" 145 | }, 146 | "pytz": { 147 | "hashes": [ 148 | "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed", 149 | "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048" 150 | ], 151 | "version": "==2020.1" 152 | }, 153 | "six": { 154 | "hashes": [ 155 | "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", 156 | "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" 157 | ], 158 | "version": "==1.15.0" 159 | }, 160 | "sqlparse": { 161 | "hashes": [ 162 | "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0", 163 | "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8" 164 | ], 165 | "version": "==0.4.1" 166 | } 167 | }, 168 | "develop": {} 169 | } 170 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lead Manager 2 | 3 | > Full stack Django/React/Redux app that uses token based authentication with Knox. 4 | 5 | ## Quick Start 6 | 7 | ```bash 8 | # Install dependencies 9 | npm install 10 | 11 | # Serve API on localhost:8000 12 | python leadmanager/manage.py runserver 13 | 14 | # Run webpack (from root) 15 | npm run dev 16 | 17 | # Build for production 18 | npm run build 19 | ``` 20 | -------------------------------------------------------------------------------- /leadmanager/accounts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bradtraversy/lead_manager_react_django/db45e4f6fc05a3481e7b8a0223e0aab0355a84b6/leadmanager/accounts/__init__.py -------------------------------------------------------------------------------- /leadmanager/accounts/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /leadmanager/accounts/api.py: -------------------------------------------------------------------------------- 1 | from rest_framework import generics, permissions 2 | from rest_framework.response import Response 3 | from knox.models import AuthToken 4 | from .serializers import UserSerializer, RegisterSerializer, LoginSerializer 5 | 6 | # Register API 7 | class RegisterAPI(generics.GenericAPIView): 8 | serializer_class = RegisterSerializer 9 | 10 | def post(self, request, *args, **kwargs): 11 | serializer = self.get_serializer(data=request.data) 12 | serializer.is_valid(raise_exception=True) 13 | user = serializer.save() 14 | return Response({ 15 | "user": UserSerializer(user, context=self.get_serializer_context()).data, 16 | "token": AuthToken.objects.create(user)[1] 17 | }) 18 | 19 | # Login API 20 | class LoginAPI(generics.GenericAPIView): 21 | serializer_class = LoginSerializer 22 | 23 | def post(self, request, *args, **kwargs): 24 | serializer = self.get_serializer(data=request.data) 25 | serializer.is_valid(raise_exception=True) 26 | user = serializer.validated_data 27 | _, token = AuthToken.objects.create(user) 28 | return Response({ 29 | "user": UserSerializer(user, context=self.get_serializer_context()).data, 30 | "token": token 31 | }) 32 | 33 | # Get User API 34 | class UserAPI(generics.RetrieveAPIView): 35 | permission_classes = [ 36 | permissions.IsAuthenticated, 37 | ] 38 | serializer_class = UserSerializer 39 | 40 | def get_object(self): 41 | return self.request.user 42 | -------------------------------------------------------------------------------- /leadmanager/accounts/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AccountsConfig(AppConfig): 5 | name = 'accounts' 6 | -------------------------------------------------------------------------------- /leadmanager/accounts/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bradtraversy/lead_manager_react_django/db45e4f6fc05a3481e7b8a0223e0aab0355a84b6/leadmanager/accounts/migrations/__init__.py -------------------------------------------------------------------------------- /leadmanager/accounts/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /leadmanager/accounts/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from django.contrib.auth.models import User 3 | from django.contrib.auth import authenticate 4 | 5 | # User Serializer 6 | class UserSerializer(serializers.ModelSerializer): 7 | class Meta: 8 | model = User 9 | fields = ('id', 'username', 'email') 10 | 11 | # Register Serializer 12 | class RegisterSerializer(serializers.ModelSerializer): 13 | class Meta: 14 | model = User 15 | fields = ('id', 'username', 'email', 'password') 16 | extra_kwargs = {'password': {'write_only': True}} 17 | 18 | def create(self, validated_data): 19 | user = User.objects.create_user(validated_data['username'], validated_data['email'], validated_data['password']) 20 | 21 | return user 22 | 23 | # Login Serializer 24 | class LoginSerializer(serializers.Serializer): 25 | username = serializers.CharField() 26 | password = serializers.CharField() 27 | 28 | def validate(self, data): 29 | user = authenticate(**data) 30 | if user and user.is_active: 31 | return user 32 | raise serializers.ValidationError("Incorrect Credentials") -------------------------------------------------------------------------------- /leadmanager/accounts/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /leadmanager/accounts/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | from .api import RegisterAPI, LoginAPI, UserAPI 3 | from knox import views as knox_views 4 | 5 | urlpatterns = [ 6 | path('api/auth', include('knox.urls')), 7 | path('api/auth/register', RegisterAPI.as_view()), 8 | path('api/auth/login', LoginAPI.as_view()), 9 | path('api/auth/user', UserAPI.as_view()), 10 | path('api/auth/logout', knox_views.LogoutView.as_view(), name='knox_logout') 11 | ] -------------------------------------------------------------------------------- /leadmanager/accounts/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /leadmanager/frontend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bradtraversy/lead_manager_react_django/db45e4f6fc05a3481e7b8a0223e0aab0355a84b6/leadmanager/frontend/__init__.py -------------------------------------------------------------------------------- /leadmanager/frontend/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /leadmanager/frontend/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class FrontendConfig(AppConfig): 5 | name = 'frontend' 6 | -------------------------------------------------------------------------------- /leadmanager/frontend/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bradtraversy/lead_manager_react_django/db45e4f6fc05a3481e7b8a0223e0aab0355a84b6/leadmanager/frontend/migrations/__init__.py -------------------------------------------------------------------------------- /leadmanager/frontend/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /leadmanager/frontend/src/actions/auth.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { returnErrors } from './messages'; 3 | 4 | import { 5 | USER_LOADED, 6 | USER_LOADING, 7 | AUTH_ERROR, 8 | LOGIN_SUCCESS, 9 | LOGIN_FAIL, 10 | LOGOUT_SUCCESS, 11 | REGISTER_SUCCESS, 12 | REGISTER_FAIL, 13 | } from './types'; 14 | 15 | // CHECK TOKEN & LOAD USER 16 | export const loadUser = () => (dispatch, getState) => { 17 | // User Loading 18 | dispatch({ type: USER_LOADING }); 19 | 20 | axios 21 | .get('/api/auth/user', tokenConfig(getState)) 22 | .then((res) => { 23 | dispatch({ 24 | type: USER_LOADED, 25 | payload: res.data, 26 | }); 27 | }) 28 | .catch((err) => { 29 | dispatch(returnErrors(err.response.data, err.response.status)); 30 | dispatch({ 31 | type: AUTH_ERROR, 32 | }); 33 | }); 34 | }; 35 | 36 | // LOGIN USER 37 | export const login = (username, password) => (dispatch) => { 38 | // Headers 39 | const config = { 40 | headers: { 41 | 'Content-Type': 'application/json', 42 | }, 43 | }; 44 | 45 | // Request Body 46 | const body = JSON.stringify({ username, password }); 47 | 48 | axios 49 | .post('/api/auth/login', body, config) 50 | .then((res) => { 51 | dispatch({ 52 | type: LOGIN_SUCCESS, 53 | payload: res.data, 54 | }); 55 | }) 56 | .catch((err) => { 57 | dispatch(returnErrors(err.response.data, err.response.status)); 58 | dispatch({ 59 | type: LOGIN_FAIL, 60 | }); 61 | }); 62 | }; 63 | 64 | // REGISTER USER 65 | export const register = ({ username, password, email }) => (dispatch) => { 66 | // Headers 67 | const config = { 68 | headers: { 69 | 'Content-Type': 'application/json', 70 | }, 71 | }; 72 | 73 | // Request Body 74 | const body = JSON.stringify({ username, email, password }); 75 | 76 | axios 77 | .post('/api/auth/register', body, config) 78 | .then((res) => { 79 | dispatch({ 80 | type: REGISTER_SUCCESS, 81 | payload: res.data, 82 | }); 83 | }) 84 | .catch((err) => { 85 | dispatch(returnErrors(err.response.data, err.response.status)); 86 | dispatch({ 87 | type: REGISTER_FAIL, 88 | }); 89 | }); 90 | }; 91 | 92 | // LOGOUT USER 93 | export const logout = () => (dispatch, getState) => { 94 | axios 95 | .post('/api/auth/logout/', null, tokenConfig(getState)) 96 | .then((res) => { 97 | dispatch({ type: 'CLEAR_LEADS' }); 98 | dispatch({ 99 | type: LOGOUT_SUCCESS, 100 | }); 101 | }) 102 | .catch((err) => { 103 | dispatch(returnErrors(err.response.data, err.response.status)); 104 | }); 105 | }; 106 | 107 | // Setup config with token - helper function 108 | export const tokenConfig = (getState) => { 109 | // Get token from state 110 | const token = getState().auth.token; 111 | 112 | // Headers 113 | const config = { 114 | headers: { 115 | 'Content-Type': 'application/json', 116 | }, 117 | }; 118 | 119 | // If token, add to headers config 120 | if (token) { 121 | config.headers['Authorization'] = `Token ${token}`; 122 | } 123 | 124 | return config; 125 | }; 126 | -------------------------------------------------------------------------------- /leadmanager/frontend/src/actions/leads.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { createMessage, returnErrors } from './messages'; 3 | import { tokenConfig } from './auth'; 4 | 5 | import { GET_LEADS, DELETE_LEAD, ADD_LEAD } from './types'; 6 | 7 | // GET LEADS 8 | export const getLeads = () => (dispatch, getState) => { 9 | axios 10 | .get('/api/leads/', tokenConfig(getState)) 11 | .then((res) => { 12 | dispatch({ 13 | type: GET_LEADS, 14 | payload: res.data, 15 | }); 16 | }) 17 | .catch((err) => dispatch(returnErrors(err.response.data, err.response.status))); 18 | }; 19 | 20 | // DELETE LEAD 21 | export const deleteLead = (id) => (dispatch, getState) => { 22 | axios 23 | .delete(`/api/leads/${id}/`, tokenConfig(getState)) 24 | .then((res) => { 25 | dispatch(createMessage({ deleteLead: 'Lead Deleted' })); 26 | dispatch({ 27 | type: DELETE_LEAD, 28 | payload: id, 29 | }); 30 | }) 31 | .catch((err) => console.log(err)); 32 | }; 33 | 34 | // ADD LEAD 35 | export const addLead = (lead) => (dispatch, getState) => { 36 | axios 37 | .post('/api/leads/', lead, tokenConfig(getState)) 38 | .then((res) => { 39 | dispatch(createMessage({ addLead: 'Lead Added' })); 40 | dispatch({ 41 | type: ADD_LEAD, 42 | payload: res.data, 43 | }); 44 | }) 45 | .catch((err) => dispatch(returnErrors(err.response.data, err.response.status))); 46 | }; 47 | -------------------------------------------------------------------------------- /leadmanager/frontend/src/actions/messages.js: -------------------------------------------------------------------------------- 1 | import { CREATE_MESSAGE, GET_ERRORS } from './types'; 2 | 3 | // CREATE MESSAGE 4 | export const createMessage = (msg) => { 5 | return { 6 | type: CREATE_MESSAGE, 7 | payload: msg, 8 | }; 9 | }; 10 | 11 | // RETURN ERRORS 12 | export const returnErrors = (msg, status) => { 13 | return { 14 | type: GET_ERRORS, 15 | payload: { msg, status }, 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /leadmanager/frontend/src/actions/types.js: -------------------------------------------------------------------------------- 1 | export const GET_LEADS = 'GET_LEADS'; 2 | export const DELETE_LEAD = 'DELETE_LEAD'; 3 | export const ADD_LEAD = 'ADD_LEAD'; 4 | export const GET_ERRORS = 'GET_ERRORS'; 5 | export const CREATE_MESSAGE = 'CREATE_MESSAGE'; 6 | export const USER_LOADING = 'USER_LOADING'; 7 | export const USER_LOADED = 'USER_LOADED'; 8 | export const AUTH_ERROR = 'AUTH_ERROR'; 9 | export const LOGIN_SUCCESS = 'LOGIN_SUCCESS'; 10 | export const LOGIN_FAIL = 'LOGIN_FAIL'; 11 | export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS'; 12 | export const REGISTER_SUCCESS = 'REGISTER_SUCCESS'; 13 | export const REGISTER_FAIL = 'REGISTER_FAIL'; 14 | export const CLEAR_LEADS = 'CLEAR_LEADS'; 15 | -------------------------------------------------------------------------------- /leadmanager/frontend/src/components/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { HashRouter as Router, Route, Switch, Redirect } from 'react-router-dom'; 4 | 5 | import { Provider as AlertProvider } from 'react-alert'; 6 | import AlertTemplate from 'react-alert-template-basic'; 7 | 8 | import Header from './layout/Header'; 9 | import Dashboard from './leads/Dashboard'; 10 | import Alerts from './layout/Alerts'; 11 | import Login from './accounts/Login'; 12 | import Register from './accounts/Register'; 13 | import PrivateRoute from './common/PrivateRoute'; 14 | 15 | import { Provider } from 'react-redux'; 16 | import store from '../store'; 17 | import { loadUser } from '../actions/auth'; 18 | 19 | // Alert Options 20 | const alertOptions = { 21 | timeout: 3000, 22 | position: 'top center', 23 | }; 24 | 25 | class App extends Component { 26 | componentDidMount() { 27 | store.dispatch(loadUser()); 28 | } 29 | 30 | render() { 31 | return ( 32 | 33 | 34 | 35 | 36 |
37 | 38 |
39 | 40 | 41 | 42 | 43 | 44 |
45 | 46 | 47 | 48 | 49 | ); 50 | } 51 | } 52 | 53 | ReactDOM.render(, document.getElementById('app')); 54 | -------------------------------------------------------------------------------- /leadmanager/frontend/src/components/accounts/Login.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link, Redirect } from 'react-router-dom'; 3 | import { connect } from 'react-redux'; 4 | import PropTypes from 'prop-types'; 5 | import { login } from '../../actions/auth'; 6 | 7 | export class Login extends Component { 8 | state = { 9 | username: '', 10 | password: '', 11 | }; 12 | 13 | static propTypes = { 14 | login: PropTypes.func.isRequired, 15 | isAuthenticated: PropTypes.bool, 16 | }; 17 | 18 | onSubmit = (e) => { 19 | e.preventDefault(); 20 | this.props.login(this.state.username, this.state.password); 21 | }; 22 | 23 | onChange = (e) => this.setState({ [e.target.name]: e.target.value }); 24 | 25 | render() { 26 | if (this.props.isAuthenticated) { 27 | return ; 28 | } 29 | const { username, password } = this.state; 30 | return ( 31 |
32 |
33 |

Login

34 |
35 |
36 | 37 | 44 |
45 | 46 |
47 | 48 | 55 |
56 | 57 |
58 | 61 |
62 |

63 | Don't have an account? Register 64 |

65 |
66 |
67 |
68 | ); 69 | } 70 | } 71 | 72 | const mapStateToProps = (state) => ({ 73 | isAuthenticated: state.auth.isAuthenticated, 74 | }); 75 | 76 | export default connect(mapStateToProps, { login })(Login); 77 | -------------------------------------------------------------------------------- /leadmanager/frontend/src/components/accounts/Register.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link, Redirect } from 'react-router-dom'; 3 | import { connect } from 'react-redux'; 4 | import PropTypes from 'prop-types'; 5 | import { register } from '../../actions/auth'; 6 | import { createMessage } from '../../actions/messages'; 7 | 8 | export class Register extends Component { 9 | state = { 10 | username: '', 11 | email: '', 12 | password: '', 13 | password2: '', 14 | }; 15 | 16 | static propTypes = { 17 | register: PropTypes.func.isRequired, 18 | isAuthenticated: PropTypes.bool, 19 | }; 20 | 21 | onSubmit = (e) => { 22 | e.preventDefault(); 23 | const { username, email, password, password2 } = this.state; 24 | if (password !== password2) { 25 | this.props.createMessage({ passwordNotMatch: 'Passwords do not match' }); 26 | } else { 27 | const newUser = { 28 | username, 29 | password, 30 | email, 31 | }; 32 | this.props.register(newUser); 33 | } 34 | }; 35 | 36 | onChange = (e) => this.setState({ [e.target.name]: e.target.value }); 37 | 38 | render() { 39 | if (this.props.isAuthenticated) { 40 | return ; 41 | } 42 | const { username, email, password, password2 } = this.state; 43 | return ( 44 |
45 |
46 |

Register

47 |
48 |
49 | 50 | 57 |
58 |
59 | 60 | 67 |
68 |
69 | 70 | 77 |
78 |
79 | 80 | 87 |
88 |
89 | 92 |
93 |

94 | Already have an account? Login 95 |

96 |
97 |
98 |
99 | ); 100 | } 101 | } 102 | 103 | const mapStateToProps = (state) => ({ 104 | isAuthenticated: state.auth.isAuthenticated, 105 | }); 106 | 107 | export default connect(mapStateToProps, { register, createMessage })(Register); 108 | -------------------------------------------------------------------------------- /leadmanager/frontend/src/components/common/PrivateRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Redirect } from 'react-router-dom'; 3 | import { connect } from 'react-redux'; 4 | import PropTypes from 'prop-types'; 5 | 6 | const PrivateRoute = ({ component: Component, auth, ...rest }) => ( 7 | { 10 | if (auth.isLoading) { 11 | return

Loading...

; 12 | } else if (!auth.isAuthenticated) { 13 | return ; 14 | } else { 15 | return ; 16 | } 17 | }} 18 | /> 19 | ); 20 | 21 | const mapStateToProps = (state) => ({ 22 | auth: state.auth, 23 | }); 24 | 25 | export default connect(mapStateToProps)(PrivateRoute); 26 | -------------------------------------------------------------------------------- /leadmanager/frontend/src/components/layout/Alerts.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import { withAlert } from 'react-alert'; 3 | import { connect } from 'react-redux'; 4 | import PropTypes from 'prop-types'; 5 | 6 | export class Alerts extends Component { 7 | static propTypes = { 8 | error: PropTypes.object.isRequired, 9 | message: PropTypes.object.isRequired, 10 | }; 11 | 12 | componentDidUpdate(prevProps) { 13 | const { error, alert, message } = this.props; 14 | if (error !== prevProps.error) { 15 | if (error.msg.name) alert.error(`Name: ${error.msg.name.join()}`); 16 | if (error.msg.email) alert.error(`Email: ${error.msg.email.join()}`); 17 | if (error.msg.message) alert.error(`Message: ${error.msg.message.join()}`); 18 | if (error.msg.non_field_errors) alert.error(error.msg.non_field_errors.join()); 19 | if (error.msg.username) alert.error(error.msg.username.join()); 20 | } 21 | 22 | if (message !== prevProps.message) { 23 | if (message.deleteLead) alert.success(message.deleteLead); 24 | if (message.addLead) alert.success(message.addLead); 25 | if (message.passwordNotMatch) alert.error(message.passwordNotMatch); 26 | } 27 | } 28 | 29 | render() { 30 | return ; 31 | } 32 | } 33 | 34 | const mapStateToProps = (state) => ({ 35 | error: state.errors, 36 | message: state.messages, 37 | }); 38 | 39 | export default connect(mapStateToProps)(withAlert()(Alerts)); 40 | -------------------------------------------------------------------------------- /leadmanager/frontend/src/components/layout/Header.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { connect } from 'react-redux'; 4 | import PropTypes from 'prop-types'; 5 | import { logout } from '../../actions/auth'; 6 | 7 | export class Header extends Component { 8 | static propTypes = { 9 | auth: PropTypes.object.isRequired, 10 | logout: PropTypes.func.isRequired, 11 | }; 12 | 13 | render() { 14 | const { isAuthenticated, user } = this.props.auth; 15 | 16 | const authLinks = ( 17 |
    18 | 19 | {user ? `Welcome ${user.username}` : ''} 20 | 21 |
  • 22 | 25 |
  • 26 |
27 | ); 28 | 29 | const guestLinks = ( 30 |
    31 |
  • 32 | 33 | Register 34 | 35 |
  • 36 |
  • 37 | 38 | Login 39 | 40 |
  • 41 |
42 | ); 43 | 44 | return ( 45 | 66 | ); 67 | } 68 | } 69 | 70 | const mapStateToProps = (state) => ({ 71 | auth: state.auth, 72 | }); 73 | 74 | export default connect(mapStateToProps, { logout })(Header); 75 | -------------------------------------------------------------------------------- /leadmanager/frontend/src/components/leads/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import Form from './Form'; 3 | import Leads from './Leads'; 4 | 5 | export default function Dashboard() { 6 | return ( 7 | 8 |
9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /leadmanager/frontend/src/components/leads/Form.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import PropTypes from 'prop-types'; 4 | import { addLead } from '../../actions/leads'; 5 | 6 | export class Form extends Component { 7 | state = { 8 | name: '', 9 | email: '', 10 | message: '', 11 | }; 12 | 13 | static propTypes = { 14 | addLead: PropTypes.func.isRequired, 15 | }; 16 | 17 | onChange = (e) => this.setState({ [e.target.name]: e.target.value }); 18 | 19 | onSubmit = (e) => { 20 | e.preventDefault(); 21 | const { name, email, message } = this.state; 22 | const lead = { name, email, message }; 23 | this.props.addLead(lead); 24 | this.setState({ 25 | name: '', 26 | email: '', 27 | message: '', 28 | }); 29 | }; 30 | 31 | render() { 32 | const { name, email, message } = this.state; 33 | return ( 34 |
35 |

Add Lead

36 | 37 |
38 | 39 | 46 |
47 |
48 | 49 | 56 |
57 |
58 | 59 |