├── .dockerignore ├── .editorconfig ├── .envs └── .local │ ├── .django │ └── .postgres ├── .gitattributes ├── .gitignore ├── .pre-commit-config.yaml ├── .pylintrc ├── .readthedocs.yml ├── CONTRIBUTORS.txt ├── LICENSE ├── README.md ├── compose ├── local │ ├── django │ │ ├── Dockerfile │ │ └── start │ └── docs │ │ ├── Dockerfile │ │ └── start └── production │ ├── aws │ ├── Dockerfile │ └── maintenance │ │ ├── download │ │ └── upload │ ├── django │ ├── Dockerfile │ ├── entrypoint │ └── start │ ├── postgres │ ├── Dockerfile │ └── maintenance │ │ ├── _sourced │ │ ├── constants.sh │ │ ├── countdown.sh │ │ ├── messages.sh │ │ └── yes_no.sh │ │ ├── backup │ │ ├── backups │ │ └── restore │ └── traefik │ ├── Dockerfile │ └── traefik.yml ├── config ├── __init__.py ├── api_router.py ├── settings │ ├── __init__.py │ ├── base.py │ ├── local.py │ ├── production.py │ └── test.py ├── urls.py └── wsgi.py ├── django_react_paypal ├── __init__.py ├── conftest.py ├── contrib │ ├── __init__.py │ └── sites │ │ ├── __init__.py │ │ └── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_alter_domain_unique.py │ │ ├── 0003_set_site_domain_and_name.py │ │ ├── 0004_alter_options_ordering_domain.py │ │ └── __init__.py ├── frontend │ ├── .gitignore │ ├── README.md │ ├── compose │ │ └── local │ │ │ └── Dockerfile │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ ├── src │ │ ├── App.test.tsx │ │ ├── App.tsx │ │ ├── components │ │ │ └── Payment.tsx │ │ ├── index.css │ │ ├── index.tsx │ │ ├── react-app-env.d.ts │ │ ├── reportWebVitals.ts │ │ └── setupTests.ts │ └── tsconfig.json ├── payments │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ └── __init__.py │ ├── models.py │ ├── tests.py │ └── views.py ├── static │ ├── css │ │ └── project.css │ ├── fonts │ │ └── .gitkeep │ ├── images │ │ └── favicons │ │ │ └── favicon.ico │ ├── js │ │ └── project.js │ └── sass │ │ ├── custom_bootstrap_vars.scss │ │ └── project.scss ├── templates │ ├── 403.html │ ├── 404.html │ ├── 500.html │ ├── account │ │ ├── account_inactive.html │ │ ├── base.html │ │ ├── email.html │ │ ├── email_confirm.html │ │ ├── login.html │ │ ├── logout.html │ │ ├── password_change.html │ │ ├── password_reset.html │ │ ├── password_reset_done.html │ │ ├── password_reset_from_key.html │ │ ├── password_reset_from_key_done.html │ │ ├── password_set.html │ │ ├── signup.html │ │ ├── signup_closed.html │ │ ├── verification_sent.html │ │ └── verified_email_required.html │ ├── base.html │ ├── pages │ │ ├── about.html │ │ └── home.html │ └── users │ │ ├── user_detail.html │ │ └── user_form.html ├── users │ ├── __init__.py │ ├── adapters.py │ ├── admin.py │ ├── api │ │ ├── serializers.py │ │ └── views.py │ ├── apps.py │ ├── forms.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── tests │ │ ├── __init__.py │ │ ├── factories.py │ │ ├── test_admin.py │ │ ├── test_drf_urls.py │ │ ├── test_drf_views.py │ │ ├── test_forms.py │ │ ├── test_models.py │ │ ├── test_urls.py │ │ └── test_views.py │ ├── urls.py │ └── views.py └── utils │ ├── __init__.py │ ├── context_processors.py │ └── storages.py ├── docs ├── Makefile ├── __init__.py ├── conf.py ├── howto.rst ├── index.rst ├── make.bat └── users.rst ├── local.yml ├── locale └── README.rst ├── manage.py ├── merge_production_dotenvs_in_dotenv.py ├── production.yml ├── pytest.ini ├── requirements ├── base.txt ├── local.txt └── production.txt └── setup.cfg /.dockerignore: -------------------------------------------------------------------------------- 1 | .editorconfig 2 | .gitattributes 3 | .github 4 | .gitignore 5 | .gitlab-ci.yml 6 | .idea 7 | .pre-commit-config.yaml 8 | .readthedocs.yml 9 | .travis.yml 10 | venv 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{py,rst,ini}] 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [*.py] 16 | line_length = 88 17 | known_first_party = django_react_paypal,config 18 | multi_line_output = 3 19 | default_section = THIRDPARTY 20 | recursive = true 21 | skip = venv/ 22 | skip_glob = **/migrations/*.py 23 | include_trailing_comma = true 24 | force_grid_wrap = 0 25 | use_parentheses = true 26 | 27 | [*.{html,css,scss,json,yml}] 28 | indent_style = space 29 | indent_size = 2 30 | 31 | [*.md] 32 | trim_trailing_whitespace = false 33 | 34 | [Makefile] 35 | indent_style = tab 36 | 37 | [nginx.conf] 38 | indent_style = space 39 | indent_size = 2 40 | -------------------------------------------------------------------------------- /.envs/.local/.django: -------------------------------------------------------------------------------- 1 | # General 2 | # ------------------------------------------------------------------------------ 3 | USE_DOCKER=yes 4 | IPYTHONDIR=/app/.ipython 5 | 6 | PAYPAL_CLIENT_ID= 7 | PAYPAL_CLIENT_SECRET= 8 | PAYPAL_WEBHOOK_ID= -------------------------------------------------------------------------------- /.envs/.local/.postgres: -------------------------------------------------------------------------------- 1 | # PostgreSQL 2 | # ------------------------------------------------------------------------------ 3 | POSTGRES_HOST=postgres 4 | POSTGRES_PORT=5432 5 | POSTGRES_DB=django_react_paypal 6 | POSTGRES_USER=zVrQKfAwckrooKTHHZHQNGbmJhmDtNps 7 | POSTGRES_PASSWORD=5k8P5nS87LNWzeVk9Nr4SdEroqO4tU2MR6QRFM8VZKmtK6m8CLiY62DMoKYKI9uU 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Python template 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | staticfiles/ 55 | 56 | # Sphinx documentation 57 | docs/_build/ 58 | 59 | # PyBuilder 60 | target/ 61 | 62 | # pyenv 63 | .python-version 64 | 65 | 66 | 67 | # Environments 68 | .venv 69 | venv/ 70 | ENV/ 71 | 72 | # Rope project settings 73 | .ropeproject 74 | 75 | # mkdocs documentation 76 | /site 77 | 78 | # mypy 79 | .mypy_cache/ 80 | 81 | 82 | ### Node template 83 | # Logs 84 | logs 85 | *.log 86 | npm-debug.log* 87 | yarn-debug.log* 88 | yarn-error.log* 89 | 90 | # Runtime data 91 | pids 92 | *.pid 93 | *.seed 94 | *.pid.lock 95 | 96 | # Directory for instrumented libs generated by jscoverage/JSCover 97 | lib-cov 98 | 99 | # Coverage directory used by tools like istanbul 100 | coverage 101 | 102 | # nyc test coverage 103 | .nyc_output 104 | 105 | # Bower dependency directory (https://bower.io/) 106 | bower_components 107 | 108 | # node-waf configuration 109 | .lock-wscript 110 | 111 | # Compiled binary addons (http://nodejs.org/api/addons.html) 112 | build/Release 113 | 114 | # Dependency directories 115 | node_modules/ 116 | jspm_packages/ 117 | 118 | # Typescript v1 declaration files 119 | typings/ 120 | 121 | # Optional npm cache directory 122 | .npm 123 | 124 | # Optional eslint cache 125 | .eslintcache 126 | 127 | # Optional REPL history 128 | .node_repl_history 129 | 130 | # Output of 'npm pack' 131 | *.tgz 132 | 133 | # Yarn Integrity file 134 | .yarn-integrity 135 | 136 | 137 | ### Linux template 138 | *~ 139 | 140 | # temporary files which can be created if a process still has a handle open of a deleted file 141 | .fuse_hidden* 142 | 143 | # KDE directory preferences 144 | .directory 145 | 146 | # Linux trash folder which might appear on any partition or disk 147 | .Trash-* 148 | 149 | # .nfs files are created when an open file is removed but is still being accessed 150 | .nfs* 151 | 152 | 153 | ### VisualStudioCode template 154 | .vscode/* 155 | !.vscode/settings.json 156 | !.vscode/tasks.json 157 | !.vscode/launch.json 158 | !.vscode/extensions.json 159 | *.code-workspace 160 | 161 | # Local History for Visual Studio Code 162 | .history/ 163 | 164 | 165 | 166 | 167 | 168 | ### Windows template 169 | # Windows thumbnail cache files 170 | Thumbs.db 171 | ehthumbs.db 172 | ehthumbs_vista.db 173 | 174 | # Dump file 175 | *.stackdump 176 | 177 | # Folder config file 178 | Desktop.ini 179 | 180 | # Recycle Bin used on file shares 181 | $RECYCLE.BIN/ 182 | 183 | # Windows Installer files 184 | *.cab 185 | *.msi 186 | *.msm 187 | *.msp 188 | 189 | # Windows shortcuts 190 | *.lnk 191 | 192 | 193 | ### macOS template 194 | # General 195 | *.DS_Store 196 | .AppleDouble 197 | .LSOverride 198 | 199 | # Icon must end with two \r 200 | Icon 201 | 202 | # Thumbnails 203 | ._* 204 | 205 | # Files that might appear in the root of a volume 206 | .DocumentRevisions-V100 207 | .fseventsd 208 | .Spotlight-V100 209 | .TemporaryItems 210 | .Trashes 211 | .VolumeIcon.icns 212 | .com.apple.timemachine.donotpresent 213 | 214 | # Directories potentially created on remote AFP share 215 | .AppleDB 216 | .AppleDesktop 217 | Network Trash Folder 218 | Temporary Items 219 | .apdisk 220 | 221 | 222 | ### SublimeText template 223 | # Cache files for Sublime Text 224 | *.tmlanguage.cache 225 | *.tmPreferences.cache 226 | *.stTheme.cache 227 | 228 | # Workspace files are user-specific 229 | *.sublime-workspace 230 | 231 | # Project files should be checked into the repository, unless a significant 232 | # proportion of contributors will probably not be using Sublime Text 233 | # *.sublime-project 234 | 235 | # SFTP configuration file 236 | sftp-config.json 237 | 238 | # Package control specific files 239 | Package Control.last-run 240 | Package Control.ca-list 241 | Package Control.ca-bundle 242 | Package Control.system-ca-bundle 243 | Package Control.cache/ 244 | Package Control.ca-certs/ 245 | Package Control.merged-ca-bundle 246 | Package Control.user-ca-bundle 247 | oscrypto-ca-bundle.crt 248 | bh_unicode_properties.cache 249 | 250 | # Sublime-github package stores a github token in this file 251 | # https://packagecontrol.io/packages/sublime-github 252 | GitHub.sublime-settings 253 | 254 | 255 | ### Vim template 256 | # Swap 257 | [._]*.s[a-v][a-z] 258 | [._]*.sw[a-p] 259 | [._]s[a-v][a-z] 260 | [._]sw[a-p] 261 | 262 | # Session 263 | Session.vim 264 | 265 | # Temporary 266 | .netrwhist 267 | 268 | # Auto-generated tag files 269 | tags 270 | 271 | ### Project template 272 | 273 | django_react_paypal/media/ 274 | 275 | .pytest_cache/ 276 | 277 | 278 | .ipython/ 279 | .env 280 | .envs/* 281 | !.envs/.local/ 282 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: 'docs|node_modules|migrations|.git|.tox' 2 | default_stages: [commit] 3 | fail_fast: true 4 | 5 | repos: 6 | - repo: https://github.com/pre-commit/pre-commit-hooks 7 | rev: v4.0.1 8 | hooks: 9 | - id: trailing-whitespace 10 | - id: end-of-file-fixer 11 | - id: check-yaml 12 | 13 | - repo: https://github.com/psf/black 14 | rev: 21.7b0 15 | hooks: 16 | - id: black 17 | 18 | - repo: https://github.com/timothycrosley/isort 19 | rev: 5.9.3 20 | hooks: 21 | - id: isort 22 | 23 | - repo: https://gitlab.com/pycqa/flake8 24 | rev: 3.9.2 25 | hooks: 26 | - id: flake8 27 | args: ['--config=setup.cfg'] 28 | additional_dependencies: [flake8-isort] 29 | 30 | 31 | # sets up .pre-commit-ci.yaml to ensure pre-commit dependencies stay up to date 32 | ci: 33 | autoupdate_schedule: weekly 34 | skip: [] 35 | submodules: false 36 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | load-plugins=pylint_django 3 | 4 | [FORMAT] 5 | max-line-length=120 6 | 7 | [MESSAGES CONTROL] 8 | disable=missing-docstring,invalid-name 9 | 10 | [DESIGN] 11 | max-parents=13 12 | 13 | [TYPECHECK] 14 | generated-members=REQUEST,acl_users,aq_parent,"[a-zA-Z]+_set{1,2}",save,delete 15 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | sphinx: 4 | configuration: docs/conf.py 5 | 6 | python: 7 | version: 3.9 8 | install: 9 | - requirements: requirements/local.txt 10 | -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | Matthew Freire 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | Copyright (c) 2021, Matthew Freire 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

3 | 4 | JustDjango 5 | 6 |

7 |

8 | The Definitive Django Learning Platform. 9 |

10 |

11 | 12 | # Django React PayPal Payments 13 | 14 | This is a tutorial for integrating PayPal with Django and React. 15 | 16 | This project was bootstrapped with [Cookiecutter Django](https://github.com/pydanny/cookiecutter-django). 17 | 18 | ## Running the Project 19 | 20 | **Make sure you add your environment variables** 21 | 22 | ### Option 1: Docker 23 | 24 | You can run the project [with Docker](https://cookiecutter-django.readthedocs.io/en/latest/developing-locally-docker.html): 25 | 26 | ```bash 27 | docker-compose -f local.yml build 28 | docker-compose -f local.yml up 29 | ``` 30 | 31 | ### Option 2: Locally 32 | 33 | Install and run everything manually. You will need to create a Postgres database locally as well. 34 | 35 | ### Django Backend 36 | 37 | ```bash 38 | virtualenv venv 39 | source venv/bin/activate 40 | pip install -r requirements/local.txt 41 | python manage.py migrate 42 | python manage.py runserver 43 | ``` 44 | 45 | ### React Frontend 46 | 47 | ```bash 48 | npm i 49 | npm run start 50 | ``` 51 | 52 | ## For deploying 53 | 54 | ```bash 55 | npm run build 56 | ``` 57 | 58 | --- 59 | 60 |
61 | 62 | Other places you can find us:
63 | 64 | YouTube 65 | Twitter 66 | 67 |
68 | -------------------------------------------------------------------------------- /compose/local/django/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PYTHON_VERSION=3.9-slim-buster 2 | 3 | # define an alias for the specfic python version used in this file. 4 | FROM python:${PYTHON_VERSION} as python 5 | 6 | # Python build stage 7 | FROM python as python-build-stage 8 | 9 | ARG BUILD_ENVIRONMENT=local 10 | 11 | # Install apt packages 12 | RUN apt-get update && apt-get install --no-install-recommends -y \ 13 | # dependencies for building Python packages 14 | build-essential \ 15 | # psycopg2 dependencies 16 | libpq-dev 17 | 18 | # Requirements are installed here to ensure they will be cached. 19 | COPY ./requirements . 20 | 21 | # Create Python Dependency and Sub-Dependency Wheels. 22 | RUN pip wheel --wheel-dir /usr/src/app/wheels \ 23 | -r ${BUILD_ENVIRONMENT}.txt 24 | 25 | 26 | # Python 'run' stage 27 | FROM python as python-run-stage 28 | 29 | ARG BUILD_ENVIRONMENT=local 30 | ARG APP_HOME=/app 31 | 32 | ENV PYTHONUNBUFFERED 1 33 | ENV PYTHONDONTWRITEBYTECODE 1 34 | ENV BUILD_ENV ${BUILD_ENVIRONMENT} 35 | 36 | WORKDIR ${APP_HOME} 37 | 38 | # Install required system dependencies 39 | RUN apt-get update && apt-get install --no-install-recommends -y \ 40 | # psycopg2 dependencies 41 | libpq-dev \ 42 | # Translations dependencies 43 | gettext \ 44 | # cleaning up unused files 45 | && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ 46 | && rm -rf /var/lib/apt/lists/* 47 | 48 | # All absolute dir copies ignore workdir instruction. All relative dir copies are wrt to the workdir instruction 49 | # copy python dependency wheels from python-build-stage 50 | COPY --from=python-build-stage /usr/src/app/wheels /wheels/ 51 | 52 | # use wheels to install python dependencies 53 | RUN pip install --no-cache-dir --no-index --find-links=/wheels/ /wheels/* \ 54 | && rm -rf /wheels/ 55 | 56 | COPY ./compose/production/django/entrypoint /entrypoint 57 | RUN sed -i 's/\r$//g' /entrypoint 58 | RUN chmod +x /entrypoint 59 | 60 | COPY ./compose/local/django/start /start 61 | RUN sed -i 's/\r$//g' /start 62 | RUN chmod +x /start 63 | 64 | 65 | 66 | # copy application code to WORKDIR 67 | COPY . ${APP_HOME} 68 | 69 | ENTRYPOINT ["/entrypoint"] 70 | -------------------------------------------------------------------------------- /compose/local/django/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | 7 | 8 | python manage.py migrate 9 | python manage.py runserver_plus 0.0.0.0:8000 10 | -------------------------------------------------------------------------------- /compose/local/docs/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9-slim-buster 2 | 3 | ENV PYTHONUNBUFFERED 1 4 | ENV PYTHONDONTWRITEBYTECODE 1 5 | 6 | RUN apt-get update \ 7 | # dependencies for building Python packages 8 | && apt-get install -y build-essential \ 9 | # psycopg2 dependencies 10 | && apt-get install -y libpq-dev \ 11 | # Translations dependencies 12 | && apt-get install -y gettext \ 13 | # Uncomment below lines to enable Sphinx output to latex and pdf 14 | # && apt-get install -y texlive-latex-recommended \ 15 | # && apt-get install -y texlive-fonts-recommended \ 16 | # && apt-get install -y texlive-latex-extra \ 17 | # && apt-get install -y latexmk \ 18 | # cleaning up unused files 19 | && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ 20 | && rm -rf /var/lib/apt/lists/* 21 | 22 | # Requirements are installed here to ensure they will be cached. 23 | COPY ./requirements /requirements 24 | # All imports needed for autodoc. 25 | RUN pip install -r /requirements/local.txt -r /requirements/production.txt 26 | 27 | COPY ./compose/local/docs/start /start-docs 28 | RUN sed -i 's/\r$//g' /start-docs 29 | RUN chmod +x /start-docs 30 | 31 | WORKDIR /docs 32 | -------------------------------------------------------------------------------- /compose/local/docs/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | 7 | make livehtml 8 | -------------------------------------------------------------------------------- /compose/production/aws/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM garland/aws-cli-docker:1.15.47 2 | 3 | COPY ./compose/production/aws/maintenance /usr/local/bin/maintenance 4 | COPY ./compose/production/postgres/maintenance/_sourced /usr/local/bin/maintenance/_sourced 5 | 6 | RUN chmod +x /usr/local/bin/maintenance/* 7 | 8 | RUN mv /usr/local/bin/maintenance/* /usr/local/bin \ 9 | && rmdir /usr/local/bin/maintenance 10 | -------------------------------------------------------------------------------- /compose/production/aws/maintenance/download: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ### Download a file from your Amazon S3 bucket to the postgres /backups folder 4 | ### 5 | ### Usage: 6 | ### $ docker-compose -f production.yml run --rm awscli <1> 7 | 8 | set -o errexit 9 | set -o pipefail 10 | set -o nounset 11 | 12 | working_dir="$(dirname ${0})" 13 | source "${working_dir}/_sourced/constants.sh" 14 | source "${working_dir}/_sourced/messages.sh" 15 | 16 | export AWS_ACCESS_KEY_ID="${DJANGO_AWS_ACCESS_KEY_ID}" 17 | export AWS_SECRET_ACCESS_KEY="${DJANGO_AWS_SECRET_ACCESS_KEY}" 18 | export AWS_STORAGE_BUCKET_NAME="${DJANGO_AWS_STORAGE_BUCKET_NAME}" 19 | 20 | 21 | aws s3 cp s3://${AWS_STORAGE_BUCKET_NAME}${BACKUP_DIR_PATH}/${1} ${BACKUP_DIR_PATH}/${1} 22 | 23 | message_success "Finished downloading ${1}." 24 | -------------------------------------------------------------------------------- /compose/production/aws/maintenance/upload: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ### Upload the /backups folder to Amazon S3 4 | ### 5 | ### Usage: 6 | ### $ docker-compose -f production.yml run --rm awscli upload 7 | 8 | set -o errexit 9 | set -o pipefail 10 | set -o nounset 11 | 12 | working_dir="$(dirname ${0})" 13 | source "${working_dir}/_sourced/constants.sh" 14 | source "${working_dir}/_sourced/messages.sh" 15 | 16 | export AWS_ACCESS_KEY_ID="${DJANGO_AWS_ACCESS_KEY_ID}" 17 | export AWS_SECRET_ACCESS_KEY="${DJANGO_AWS_SECRET_ACCESS_KEY}" 18 | export AWS_STORAGE_BUCKET_NAME="${DJANGO_AWS_STORAGE_BUCKET_NAME}" 19 | 20 | 21 | message_info "Upload the backups directory to S3 bucket {$AWS_STORAGE_BUCKET_NAME}" 22 | 23 | aws s3 cp ${BACKUP_DIR_PATH} s3://${AWS_STORAGE_BUCKET_NAME}${BACKUP_DIR_PATH} --recursive 24 | 25 | message_info "Cleaning the directory ${BACKUP_DIR_PATH}" 26 | 27 | rm -rf ${BACKUP_DIR_PATH}/* 28 | 29 | message_success "Finished uploading and cleaning." 30 | -------------------------------------------------------------------------------- /compose/production/django/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PYTHON_VERSION=3.9-slim-buster 2 | 3 | 4 | 5 | # define an alias for the specfic python version used in this file. 6 | FROM python:${PYTHON_VERSION} as python 7 | 8 | # Python build stage 9 | FROM python as python-build-stage 10 | 11 | ARG BUILD_ENVIRONMENT=production 12 | 13 | # Install apt packages 14 | RUN apt-get update && apt-get install --no-install-recommends -y \ 15 | # dependencies for building Python packages 16 | build-essential \ 17 | # psycopg2 dependencies 18 | libpq-dev 19 | 20 | # Requirements are installed here to ensure they will be cached. 21 | COPY ./requirements . 22 | 23 | # Create Python Dependency and Sub-Dependency Wheels. 24 | RUN pip wheel --wheel-dir /usr/src/app/wheels \ 25 | -r ${BUILD_ENVIRONMENT}.txt 26 | 27 | 28 | # Python 'run' stage 29 | FROM python as python-run-stage 30 | 31 | ARG BUILD_ENVIRONMENT=production 32 | ARG APP_HOME=/app 33 | 34 | ENV PYTHONUNBUFFERED 1 35 | ENV PYTHONDONTWRITEBYTECODE 1 36 | ENV BUILD_ENV ${BUILD_ENVIRONMENT} 37 | 38 | WORKDIR ${APP_HOME} 39 | 40 | RUN addgroup --system django \ 41 | && adduser --system --ingroup django django 42 | 43 | 44 | # Install required system dependencies 45 | RUN apt-get update && apt-get install --no-install-recommends -y \ 46 | # psycopg2 dependencies 47 | libpq-dev \ 48 | # Translations dependencies 49 | gettext \ 50 | # cleaning up unused files 51 | && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ 52 | && rm -rf /var/lib/apt/lists/* 53 | 54 | # All absolute dir copies ignore workdir instruction. All relative dir copies are wrt to the workdir instruction 55 | # copy python dependency wheels from python-build-stage 56 | COPY --from=python-build-stage /usr/src/app/wheels /wheels/ 57 | 58 | # use wheels to install python dependencies 59 | RUN pip install --no-cache-dir --no-index --find-links=/wheels/ /wheels/* \ 60 | && rm -rf /wheels/ 61 | 62 | 63 | COPY --chown=django:django ./compose/production/django/entrypoint /entrypoint 64 | RUN sed -i 's/\r$//g' /entrypoint 65 | RUN chmod +x /entrypoint 66 | 67 | 68 | COPY --chown=django:django ./compose/production/django/start /start 69 | RUN sed -i 's/\r$//g' /start 70 | RUN chmod +x /start 71 | 72 | 73 | # copy application code to WORKDIR 74 | COPY --chown=django:django . ${APP_HOME} 75 | 76 | # make django owner of the WORKDIR directory as well. 77 | RUN chown django:django ${APP_HOME} 78 | 79 | USER django 80 | 81 | ENTRYPOINT ["/entrypoint"] 82 | -------------------------------------------------------------------------------- /compose/production/django/entrypoint: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | 7 | 8 | 9 | 10 | if [ -z "${POSTGRES_USER}" ]; then 11 | base_postgres_image_default_user='postgres' 12 | export POSTGRES_USER="${base_postgres_image_default_user}" 13 | fi 14 | export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" 15 | 16 | postgres_ready() { 17 | python << END 18 | import sys 19 | 20 | import psycopg2 21 | 22 | try: 23 | psycopg2.connect( 24 | dbname="${POSTGRES_DB}", 25 | user="${POSTGRES_USER}", 26 | password="${POSTGRES_PASSWORD}", 27 | host="${POSTGRES_HOST}", 28 | port="${POSTGRES_PORT}", 29 | ) 30 | except psycopg2.OperationalError: 31 | sys.exit(-1) 32 | sys.exit(0) 33 | 34 | END 35 | } 36 | until postgres_ready; do 37 | >&2 echo 'Waiting for PostgreSQL to become available...' 38 | sleep 1 39 | done 40 | >&2 echo 'PostgreSQL is available' 41 | 42 | exec "$@" 43 | -------------------------------------------------------------------------------- /compose/production/django/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | 7 | 8 | python /app/manage.py collectstatic --noinput 9 | 10 | 11 | /usr/local/bin/gunicorn config.wsgi --bind 0.0.0.0:5000 --chdir=/app 12 | -------------------------------------------------------------------------------- /compose/production/postgres/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres:13.2 2 | 3 | COPY ./compose/production/postgres/maintenance /usr/local/bin/maintenance 4 | RUN chmod +x /usr/local/bin/maintenance/* 5 | RUN mv /usr/local/bin/maintenance/* /usr/local/bin \ 6 | && rmdir /usr/local/bin/maintenance 7 | -------------------------------------------------------------------------------- /compose/production/postgres/maintenance/_sourced/constants.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | BACKUP_DIR_PATH='/backups' 5 | BACKUP_FILE_PREFIX='backup' 6 | -------------------------------------------------------------------------------- /compose/production/postgres/maintenance/_sourced/countdown.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | countdown() { 5 | declare desc="A simple countdown. Source: https://superuser.com/a/611582" 6 | local seconds="${1}" 7 | local d=$(($(date +%s) + "${seconds}")) 8 | while [ "$d" -ge `date +%s` ]; do 9 | echo -ne "$(date -u --date @$(($d - `date +%s`)) +%H:%M:%S)\r"; 10 | sleep 0.1 11 | done 12 | } 13 | -------------------------------------------------------------------------------- /compose/production/postgres/maintenance/_sourced/messages.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | message_newline() { 5 | echo 6 | } 7 | 8 | message_debug() 9 | { 10 | echo -e "DEBUG: ${@}" 11 | } 12 | 13 | message_welcome() 14 | { 15 | echo -e "\e[1m${@}\e[0m" 16 | } 17 | 18 | message_warning() 19 | { 20 | echo -e "\e[33mWARNING\e[0m: ${@}" 21 | } 22 | 23 | message_error() 24 | { 25 | echo -e "\e[31mERROR\e[0m: ${@}" 26 | } 27 | 28 | message_info() 29 | { 30 | echo -e "\e[37mINFO\e[0m: ${@}" 31 | } 32 | 33 | message_suggestion() 34 | { 35 | echo -e "\e[33mSUGGESTION\e[0m: ${@}" 36 | } 37 | 38 | message_success() 39 | { 40 | echo -e "\e[32mSUCCESS\e[0m: ${@}" 41 | } 42 | -------------------------------------------------------------------------------- /compose/production/postgres/maintenance/_sourced/yes_no.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | yes_no() { 5 | declare desc="Prompt for confirmation. \$\"\{1\}\": confirmation message." 6 | local arg1="${1}" 7 | 8 | local response= 9 | read -r -p "${arg1} (y/[n])? " response 10 | if [[ "${response}" =~ ^[Yy]$ ]] 11 | then 12 | exit 0 13 | else 14 | exit 1 15 | fi 16 | } 17 | -------------------------------------------------------------------------------- /compose/production/postgres/maintenance/backup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | ### Create a database backup. 5 | ### 6 | ### Usage: 7 | ### $ docker-compose -f .yml (exec |run --rm) postgres backup 8 | 9 | 10 | set -o errexit 11 | set -o pipefail 12 | set -o nounset 13 | 14 | 15 | working_dir="$(dirname ${0})" 16 | source "${working_dir}/_sourced/constants.sh" 17 | source "${working_dir}/_sourced/messages.sh" 18 | 19 | 20 | message_welcome "Backing up the '${POSTGRES_DB}' database..." 21 | 22 | 23 | if [[ "${POSTGRES_USER}" == "postgres" ]]; then 24 | message_error "Backing up as 'postgres' user is not supported. Assign 'POSTGRES_USER' env with another one and try again." 25 | exit 1 26 | fi 27 | 28 | export PGHOST="${POSTGRES_HOST}" 29 | export PGPORT="${POSTGRES_PORT}" 30 | export PGUSER="${POSTGRES_USER}" 31 | export PGPASSWORD="${POSTGRES_PASSWORD}" 32 | export PGDATABASE="${POSTGRES_DB}" 33 | 34 | backup_filename="${BACKUP_FILE_PREFIX}_$(date +'%Y_%m_%dT%H_%M_%S').sql.gz" 35 | pg_dump | gzip > "${BACKUP_DIR_PATH}/${backup_filename}" 36 | 37 | 38 | message_success "'${POSTGRES_DB}' database backup '${backup_filename}' has been created and placed in '${BACKUP_DIR_PATH}'." 39 | -------------------------------------------------------------------------------- /compose/production/postgres/maintenance/backups: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | ### View backups. 5 | ### 6 | ### Usage: 7 | ### $ docker-compose -f .yml (exec |run --rm) postgres backups 8 | 9 | 10 | set -o errexit 11 | set -o pipefail 12 | set -o nounset 13 | 14 | 15 | working_dir="$(dirname ${0})" 16 | source "${working_dir}/_sourced/constants.sh" 17 | source "${working_dir}/_sourced/messages.sh" 18 | 19 | 20 | message_welcome "These are the backups you have got:" 21 | 22 | ls -lht "${BACKUP_DIR_PATH}" 23 | -------------------------------------------------------------------------------- /compose/production/postgres/maintenance/restore: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | ### Restore database from a backup. 5 | ### 6 | ### Parameters: 7 | ### <1> filename of an existing backup. 8 | ### 9 | ### Usage: 10 | ### $ docker-compose -f .yml (exec |run --rm) postgres restore <1> 11 | 12 | 13 | set -o errexit 14 | set -o pipefail 15 | set -o nounset 16 | 17 | 18 | working_dir="$(dirname ${0})" 19 | source "${working_dir}/_sourced/constants.sh" 20 | source "${working_dir}/_sourced/messages.sh" 21 | 22 | 23 | if [[ -z ${1+x} ]]; then 24 | message_error "Backup filename is not specified yet it is a required parameter. Make sure you provide one and try again." 25 | exit 1 26 | fi 27 | backup_filename="${BACKUP_DIR_PATH}/${1}" 28 | if [[ ! -f "${backup_filename}" ]]; then 29 | message_error "No backup with the specified filename found. Check out the 'backups' maintenance script output to see if there is one and try again." 30 | exit 1 31 | fi 32 | 33 | message_welcome "Restoring the '${POSTGRES_DB}' database from the '${backup_filename}' backup..." 34 | 35 | if [[ "${POSTGRES_USER}" == "postgres" ]]; then 36 | message_error "Restoring as 'postgres' user is not supported. Assign 'POSTGRES_USER' env with another one and try again." 37 | exit 1 38 | fi 39 | 40 | export PGHOST="${POSTGRES_HOST}" 41 | export PGPORT="${POSTGRES_PORT}" 42 | export PGUSER="${POSTGRES_USER}" 43 | export PGPASSWORD="${POSTGRES_PASSWORD}" 44 | export PGDATABASE="${POSTGRES_DB}" 45 | 46 | message_info "Dropping the database..." 47 | dropdb "${PGDATABASE}" 48 | 49 | message_info "Creating a new database..." 50 | createdb --owner="${POSTGRES_USER}" 51 | 52 | message_info "Applying the backup to the new database..." 53 | gunzip -c "${backup_filename}" | psql "${POSTGRES_DB}" 54 | 55 | message_success "The '${POSTGRES_DB}' database has been restored from the '${backup_filename}' backup." 56 | -------------------------------------------------------------------------------- /compose/production/traefik/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM traefik:v2.2.11 2 | RUN mkdir -p /etc/traefik/acme \ 3 | && touch /etc/traefik/acme/acme.json \ 4 | && chmod 600 /etc/traefik/acme/acme.json 5 | COPY ./compose/production/traefik/traefik.yml /etc/traefik 6 | -------------------------------------------------------------------------------- /compose/production/traefik/traefik.yml: -------------------------------------------------------------------------------- 1 | log: 2 | level: INFO 3 | 4 | entryPoints: 5 | web: 6 | # http 7 | address: ":80" 8 | http: 9 | # https://docs.traefik.io/routing/entrypoints/#entrypoint 10 | redirections: 11 | entryPoint: 12 | to: web-secure 13 | 14 | web-secure: 15 | # https 16 | address: ":443" 17 | 18 | certificatesResolvers: 19 | letsencrypt: 20 | # https://docs.traefik.io/master/https/acme/#lets-encrypt 21 | acme: 22 | email: "matt@justdjango.com" 23 | storage: /etc/traefik/acme/acme.json 24 | # https://docs.traefik.io/master/https/acme/#httpchallenge 25 | httpChallenge: 26 | entryPoint: web 27 | 28 | http: 29 | routers: 30 | web-secure-router: 31 | rule: "Host(`example.com`) || Host(`www.example.com`)" 32 | entryPoints: 33 | - web-secure 34 | middlewares: 35 | - csrf 36 | service: django 37 | tls: 38 | # https://docs.traefik.io/master/routing/routers/#certresolver 39 | certResolver: letsencrypt 40 | 41 | middlewares: 42 | csrf: 43 | # https://docs.traefik.io/master/middlewares/headers/#hostsproxyheaders 44 | # https://docs.djangoproject.com/en/dev/ref/csrf/#ajax 45 | headers: 46 | hostsProxyHeaders: ["X-CSRFToken"] 47 | 48 | services: 49 | django: 50 | loadBalancer: 51 | servers: 52 | - url: http://django:5000 53 | 54 | providers: 55 | # https://docs.traefik.io/master/providers/file/ 56 | file: 57 | filename: /etc/traefik/traefik.yml 58 | watch: true 59 | -------------------------------------------------------------------------------- /config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django_react_paypal/d3aa6a16ff0bf08d30ce79204a37d8bb7b806bd5/config/__init__.py -------------------------------------------------------------------------------- /config/api_router.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from rest_framework.routers import DefaultRouter, SimpleRouter 3 | 4 | from django_react_paypal.users.api.views import UserViewSet 5 | 6 | if settings.DEBUG: 7 | router = DefaultRouter() 8 | else: 9 | router = SimpleRouter() 10 | 11 | router.register("users", UserViewSet) 12 | 13 | 14 | app_name = "api" 15 | urlpatterns = router.urls 16 | -------------------------------------------------------------------------------- /config/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django_react_paypal/d3aa6a16ff0bf08d30ce79204a37d8bb7b806bd5/config/settings/__init__.py -------------------------------------------------------------------------------- /config/settings/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Base settings to build other settings files upon. 3 | """ 4 | from pathlib import Path 5 | 6 | import environ 7 | 8 | ROOT_DIR = Path(__file__).resolve(strict=True).parent.parent.parent 9 | # django_react_paypal/ 10 | APPS_DIR = ROOT_DIR / "django_react_paypal" 11 | env = environ.Env() 12 | 13 | READ_DOT_ENV_FILE = env.bool("DJANGO_READ_DOT_ENV_FILE", default=False) 14 | if READ_DOT_ENV_FILE: 15 | # OS environment variables take precedence over variables from .env 16 | env.read_env(str(ROOT_DIR / ".env")) 17 | 18 | # GENERAL 19 | # ------------------------------------------------------------------------------ 20 | # https://docs.djangoproject.com/en/dev/ref/settings/#debug 21 | DEBUG = env.bool("DJANGO_DEBUG", False) 22 | # Local time zone. Choices are 23 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 24 | # though not all of them may be available with every OS. 25 | # In Windows, this must be set to your system time zone. 26 | TIME_ZONE = "UTC" 27 | # https://docs.djangoproject.com/en/dev/ref/settings/#language-code 28 | LANGUAGE_CODE = "en-us" 29 | # https://docs.djangoproject.com/en/dev/ref/settings/#site-id 30 | SITE_ID = 1 31 | # https://docs.djangoproject.com/en/dev/ref/settings/#use-i18n 32 | USE_I18N = True 33 | # https://docs.djangoproject.com/en/dev/ref/settings/#use-l10n 34 | USE_L10N = True 35 | # https://docs.djangoproject.com/en/dev/ref/settings/#use-tz 36 | USE_TZ = True 37 | # https://docs.djangoproject.com/en/dev/ref/settings/#locale-paths 38 | LOCALE_PATHS = [str(ROOT_DIR / "locale")] 39 | 40 | # DATABASES 41 | # ------------------------------------------------------------------------------ 42 | # https://docs.djangoproject.com/en/dev/ref/settings/#databases 43 | DATABASES = {"default": env.db("DATABASE_URL")} 44 | DATABASES["default"]["ATOMIC_REQUESTS"] = True 45 | 46 | # URLS 47 | # ------------------------------------------------------------------------------ 48 | # https://docs.djangoproject.com/en/dev/ref/settings/#root-urlconf 49 | ROOT_URLCONF = "config.urls" 50 | # https://docs.djangoproject.com/en/dev/ref/settings/#wsgi-application 51 | WSGI_APPLICATION = "config.wsgi.application" 52 | 53 | # APPS 54 | # ------------------------------------------------------------------------------ 55 | DJANGO_APPS = [ 56 | "django.contrib.auth", 57 | "django.contrib.contenttypes", 58 | "django.contrib.sessions", 59 | "django.contrib.sites", 60 | "django.contrib.messages", 61 | "django.contrib.staticfiles", 62 | # "django.contrib.humanize", # Handy template tags 63 | "django.contrib.admin", 64 | "django.forms", 65 | ] 66 | THIRD_PARTY_APPS = [ 67 | "crispy_forms", 68 | "allauth", 69 | "allauth.account", 70 | "allauth.socialaccount", 71 | "rest_framework", 72 | "rest_framework.authtoken", 73 | "corsheaders" 74 | ] 75 | 76 | LOCAL_APPS = [ 77 | "django_react_paypal.users.apps.UsersConfig", 78 | "django_react_paypal.payments.apps.PaymentsConfig", 79 | ] 80 | # https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps 81 | INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS 82 | 83 | # MIGRATIONS 84 | # ------------------------------------------------------------------------------ 85 | # https://docs.djangoproject.com/en/dev/ref/settings/#migration-modules 86 | MIGRATION_MODULES = {"sites": "django_react_paypal.contrib.sites.migrations"} 87 | 88 | # AUTHENTICATION 89 | # ------------------------------------------------------------------------------ 90 | # https://docs.djangoproject.com/en/dev/ref/settings/#authentication-backends 91 | AUTHENTICATION_BACKENDS = [ 92 | "django.contrib.auth.backends.ModelBackend", 93 | "allauth.account.auth_backends.AuthenticationBackend", 94 | ] 95 | # https://docs.djangoproject.com/en/dev/ref/settings/#auth-user-model 96 | AUTH_USER_MODEL = "users.User" 97 | # https://docs.djangoproject.com/en/dev/ref/settings/#login-redirect-url 98 | LOGIN_REDIRECT_URL = "users:redirect" 99 | # https://docs.djangoproject.com/en/dev/ref/settings/#login-url 100 | LOGIN_URL = "account_login" 101 | 102 | # PASSWORDS 103 | # ------------------------------------------------------------------------------ 104 | # https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers 105 | PASSWORD_HASHERS = [ 106 | # https://docs.djangoproject.com/en/dev/topics/auth/passwords/#using-argon2-with-django 107 | "django.contrib.auth.hashers.Argon2PasswordHasher", 108 | "django.contrib.auth.hashers.PBKDF2PasswordHasher", 109 | "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher", 110 | "django.contrib.auth.hashers.BCryptSHA256PasswordHasher", 111 | ] 112 | # https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators 113 | AUTH_PASSWORD_VALIDATORS = [ 114 | { 115 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" 116 | }, 117 | {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, 118 | {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, 119 | {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, 120 | ] 121 | 122 | # MIDDLEWARE 123 | # ------------------------------------------------------------------------------ 124 | # https://docs.djangoproject.com/en/dev/ref/settings/#middleware 125 | MIDDLEWARE = [ 126 | "django.middleware.security.SecurityMiddleware", 127 | "corsheaders.middleware.CorsMiddleware", 128 | "django.contrib.sessions.middleware.SessionMiddleware", 129 | "django.middleware.locale.LocaleMiddleware", 130 | "django.middleware.common.CommonMiddleware", 131 | "django.middleware.csrf.CsrfViewMiddleware", 132 | "django.contrib.auth.middleware.AuthenticationMiddleware", 133 | "django.contrib.messages.middleware.MessageMiddleware", 134 | "django.middleware.common.BrokenLinkEmailsMiddleware", 135 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 136 | ] 137 | 138 | # STATIC 139 | # ------------------------------------------------------------------------------ 140 | # https://docs.djangoproject.com/en/dev/ref/settings/#static-root 141 | STATIC_ROOT = str(ROOT_DIR / "staticfiles") 142 | # https://docs.djangoproject.com/en/dev/ref/settings/#static-url 143 | STATIC_URL = "/static/" 144 | # https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS 145 | STATICFILES_DIRS = [str(APPS_DIR / "static")] 146 | # https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders 147 | STATICFILES_FINDERS = [ 148 | "django.contrib.staticfiles.finders.FileSystemFinder", 149 | "django.contrib.staticfiles.finders.AppDirectoriesFinder", 150 | ] 151 | 152 | # MEDIA 153 | # ------------------------------------------------------------------------------ 154 | # https://docs.djangoproject.com/en/dev/ref/settings/#media-root 155 | MEDIA_ROOT = str(APPS_DIR / "media") 156 | # https://docs.djangoproject.com/en/dev/ref/settings/#media-url 157 | MEDIA_URL = "/media/" 158 | 159 | # TEMPLATES 160 | # ------------------------------------------------------------------------------ 161 | # https://docs.djangoproject.com/en/dev/ref/settings/#templates 162 | TEMPLATES = [ 163 | { 164 | # https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-TEMPLATES-BACKEND 165 | "BACKEND": "django.template.backends.django.DjangoTemplates", 166 | # https://docs.djangoproject.com/en/dev/ref/settings/#template-dirs 167 | "DIRS": [str(APPS_DIR / "templates")], 168 | "OPTIONS": { 169 | # https://docs.djangoproject.com/en/dev/ref/settings/#template-loaders 170 | # https://docs.djangoproject.com/en/dev/ref/templates/api/#loader-types 171 | "loaders": [ 172 | "django.template.loaders.filesystem.Loader", 173 | "django.template.loaders.app_directories.Loader", 174 | ], 175 | # https://docs.djangoproject.com/en/dev/ref/settings/#template-context-processors 176 | "context_processors": [ 177 | "django.template.context_processors.debug", 178 | "django.template.context_processors.request", 179 | "django.contrib.auth.context_processors.auth", 180 | "django.template.context_processors.i18n", 181 | "django.template.context_processors.media", 182 | "django.template.context_processors.static", 183 | "django.template.context_processors.tz", 184 | "django.contrib.messages.context_processors.messages", 185 | "django_react_paypal.utils.context_processors.settings_context", 186 | ], 187 | }, 188 | } 189 | ] 190 | 191 | # https://docs.djangoproject.com/en/dev/ref/settings/#form-renderer 192 | FORM_RENDERER = "django.forms.renderers.TemplatesSetting" 193 | 194 | # http://django-crispy-forms.readthedocs.io/en/latest/install.html#template-packs 195 | CRISPY_TEMPLATE_PACK = "bootstrap4" 196 | 197 | # FIXTURES 198 | # ------------------------------------------------------------------------------ 199 | # https://docs.djangoproject.com/en/dev/ref/settings/#fixture-dirs 200 | FIXTURE_DIRS = (str(APPS_DIR / "fixtures"),) 201 | 202 | # SECURITY 203 | # ------------------------------------------------------------------------------ 204 | # https://docs.djangoproject.com/en/dev/ref/settings/#session-cookie-httponly 205 | SESSION_COOKIE_HTTPONLY = True 206 | # https://docs.djangoproject.com/en/dev/ref/settings/#csrf-cookie-httponly 207 | CSRF_COOKIE_HTTPONLY = True 208 | # https://docs.djangoproject.com/en/dev/ref/settings/#secure-browser-xss-filter 209 | SECURE_BROWSER_XSS_FILTER = True 210 | # https://docs.djangoproject.com/en/dev/ref/settings/#x-frame-options 211 | X_FRAME_OPTIONS = "DENY" 212 | 213 | # EMAIL 214 | # ------------------------------------------------------------------------------ 215 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend 216 | EMAIL_BACKEND = env( 217 | "DJANGO_EMAIL_BACKEND", 218 | default="django.core.mail.backends.smtp.EmailBackend", 219 | ) 220 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-timeout 221 | EMAIL_TIMEOUT = 5 222 | 223 | # ADMIN 224 | # ------------------------------------------------------------------------------ 225 | # Django Admin URL. 226 | ADMIN_URL = "admin/" 227 | # https://docs.djangoproject.com/en/dev/ref/settings/#admins 228 | ADMINS = [("""Matthew Freire""", "matt@justdjango.com")] 229 | # https://docs.djangoproject.com/en/dev/ref/settings/#managers 230 | MANAGERS = ADMINS 231 | 232 | # LOGGING 233 | # ------------------------------------------------------------------------------ 234 | # https://docs.djangoproject.com/en/dev/ref/settings/#logging 235 | # See https://docs.djangoproject.com/en/dev/topics/logging for 236 | # more details on how to customize your logging configuration. 237 | LOGGING = { 238 | "version": 1, 239 | "disable_existing_loggers": False, 240 | "formatters": { 241 | "verbose": { 242 | "format": "%(levelname)s %(asctime)s %(module)s " 243 | "%(process)d %(thread)d %(message)s" 244 | } 245 | }, 246 | "handlers": { 247 | "console": { 248 | "level": "DEBUG", 249 | "class": "logging.StreamHandler", 250 | "formatter": "verbose", 251 | } 252 | }, 253 | "root": {"level": "INFO", "handlers": ["console"]}, 254 | } 255 | 256 | 257 | # django-allauth 258 | # ------------------------------------------------------------------------------ 259 | ACCOUNT_ALLOW_REGISTRATION = env.bool("DJANGO_ACCOUNT_ALLOW_REGISTRATION", True) 260 | # https://django-allauth.readthedocs.io/en/latest/configuration.html 261 | ACCOUNT_AUTHENTICATION_METHOD = "username" 262 | # https://django-allauth.readthedocs.io/en/latest/configuration.html 263 | ACCOUNT_EMAIL_REQUIRED = True 264 | # https://django-allauth.readthedocs.io/en/latest/configuration.html 265 | ACCOUNT_EMAIL_VERIFICATION = "mandatory" 266 | # https://django-allauth.readthedocs.io/en/latest/configuration.html 267 | ACCOUNT_ADAPTER = "django_react_paypal.users.adapters.AccountAdapter" 268 | # https://django-allauth.readthedocs.io/en/latest/configuration.html 269 | SOCIALACCOUNT_ADAPTER = "django_react_paypal.users.adapters.SocialAccountAdapter" 270 | 271 | # django-rest-framework 272 | # ------------------------------------------------------------------------------- 273 | # django-rest-framework - https://www.django-rest-framework.org/api-guide/settings/ 274 | REST_FRAMEWORK = { 275 | "DEFAULT_AUTHENTICATION_CLASSES": ( 276 | "rest_framework.authentication.SessionAuthentication", 277 | "rest_framework.authentication.TokenAuthentication", 278 | ), 279 | "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",), 280 | } 281 | 282 | # django-cors-headers - https://github.com/adamchainz/django-cors-headers#setup 283 | CORS_URLS_REGEX = r"^/api/.*$" 284 | # Your stuff... 285 | # ------------------------------------------------------------------------------ 286 | 287 | PAYPAL_CLIENT_ID = env("PAYPAL_CLIENT_ID") 288 | PAYPAL_CLIENT_SECRET = env("PAYPAL_CLIENT_SECRET") 289 | PAYPAL_WEBHOOK_ID = env("PAYPAL_WEBHOOK_ID") 290 | -------------------------------------------------------------------------------- /config/settings/local.py: -------------------------------------------------------------------------------- 1 | from .base import * # noqa 2 | from .base import env 3 | 4 | # GENERAL 5 | # ------------------------------------------------------------------------------ 6 | # https://docs.djangoproject.com/en/dev/ref/settings/#debug 7 | DEBUG = True 8 | # https://docs.djangoproject.com/en/dev/ref/settings/#secret-key 9 | SECRET_KEY = env( 10 | "DJANGO_SECRET_KEY", 11 | default="u03Bb8BdjgFCJmxbpTaNh0It8OIFwXAVj2iJguhH6jl9wqnqCS8dwz8mirmFYkxR", 12 | ) 13 | # https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts 14 | ALLOWED_HOSTS = ["localhost", "0.0.0.0", "127.0.0.1", "6aa0c5404650.ngrok.io"] 15 | 16 | # CACHES 17 | # ------------------------------------------------------------------------------ 18 | # https://docs.djangoproject.com/en/dev/ref/settings/#caches 19 | CACHES = { 20 | "default": { 21 | "BACKEND": "django.core.cache.backends.locmem.LocMemCache", 22 | "LOCATION": "", 23 | } 24 | } 25 | 26 | # EMAIL 27 | # ------------------------------------------------------------------------------ 28 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend 29 | EMAIL_BACKEND = env( 30 | "DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.console.EmailBackend" 31 | ) 32 | 33 | # django-debug-toolbar 34 | # ------------------------------------------------------------------------------ 35 | # https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#prerequisites 36 | INSTALLED_APPS += ["debug_toolbar"] # noqa F405 37 | # https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#middleware 38 | MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"] # noqa F405 39 | # https://django-debug-toolbar.readthedocs.io/en/latest/configuration.html#debug-toolbar-config 40 | DEBUG_TOOLBAR_CONFIG = { 41 | "DISABLE_PANELS": ["debug_toolbar.panels.redirects.RedirectsPanel"], 42 | "SHOW_TEMPLATE_CONTEXT": True, 43 | } 44 | # https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#internal-ips 45 | INTERNAL_IPS = ["127.0.0.1", "10.0.2.2"] 46 | if env("USE_DOCKER") == "yes": 47 | import socket 48 | 49 | hostname, _, ips = socket.gethostbyname_ex(socket.gethostname()) 50 | INTERNAL_IPS += [".".join(ip.split(".")[:-1] + ["1"]) for ip in ips] 51 | 52 | # django-extensions 53 | # ------------------------------------------------------------------------------ 54 | # https://django-extensions.readthedocs.io/en/latest/installation_instructions.html#configuration 55 | INSTALLED_APPS += ["django_extensions"] # noqa F405 56 | 57 | # Your stuff... 58 | # ------------------------------------------------------------------------------ 59 | 60 | PAYPAL_MODE = "sandbox" 61 | -------------------------------------------------------------------------------- /config/settings/production.py: -------------------------------------------------------------------------------- 1 | from .base import * # noqa 2 | from .base import env 3 | 4 | # GENERAL 5 | # ------------------------------------------------------------------------------ 6 | # https://docs.djangoproject.com/en/dev/ref/settings/#secret-key 7 | SECRET_KEY = env("DJANGO_SECRET_KEY") 8 | # https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts 9 | ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["example.com"]) 10 | 11 | # DATABASES 12 | # ------------------------------------------------------------------------------ 13 | DATABASES["default"] = env.db("DATABASE_URL") # noqa F405 14 | DATABASES["default"]["ATOMIC_REQUESTS"] = True # noqa F405 15 | DATABASES["default"]["CONN_MAX_AGE"] = env.int("CONN_MAX_AGE", default=60) # noqa F405 16 | 17 | # CACHES 18 | # ------------------------------------------------------------------------------ 19 | CACHES = { 20 | "default": { 21 | "BACKEND": "django_redis.cache.RedisCache", 22 | "LOCATION": env("REDIS_URL"), 23 | "OPTIONS": { 24 | "CLIENT_CLASS": "django_redis.client.DefaultClient", 25 | # Mimicing memcache behavior. 26 | # https://github.com/jazzband/django-redis#memcached-exceptions-behavior 27 | "IGNORE_EXCEPTIONS": True, 28 | }, 29 | } 30 | } 31 | 32 | # SECURITY 33 | # ------------------------------------------------------------------------------ 34 | # https://docs.djangoproject.com/en/dev/ref/settings/#secure-proxy-ssl-header 35 | SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") 36 | # https://docs.djangoproject.com/en/dev/ref/settings/#secure-ssl-redirect 37 | SECURE_SSL_REDIRECT = env.bool("DJANGO_SECURE_SSL_REDIRECT", default=True) 38 | # https://docs.djangoproject.com/en/dev/ref/settings/#session-cookie-secure 39 | SESSION_COOKIE_SECURE = True 40 | # https://docs.djangoproject.com/en/dev/ref/settings/#csrf-cookie-secure 41 | CSRF_COOKIE_SECURE = True 42 | # https://docs.djangoproject.com/en/dev/topics/security/#ssl-https 43 | # https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-seconds 44 | # TODO: set this to 60 seconds first and then to 518400 once you prove the former works 45 | SECURE_HSTS_SECONDS = 60 46 | # https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-include-subdomains 47 | SECURE_HSTS_INCLUDE_SUBDOMAINS = env.bool( 48 | "DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS", default=True 49 | ) 50 | # https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-preload 51 | SECURE_HSTS_PRELOAD = env.bool("DJANGO_SECURE_HSTS_PRELOAD", default=True) 52 | # https://docs.djangoproject.com/en/dev/ref/middleware/#x-content-type-options-nosniff 53 | SECURE_CONTENT_TYPE_NOSNIFF = env.bool( 54 | "DJANGO_SECURE_CONTENT_TYPE_NOSNIFF", default=True 55 | ) 56 | 57 | # STORAGES 58 | # ------------------------------------------------------------------------------ 59 | # https://django-storages.readthedocs.io/en/latest/#installation 60 | INSTALLED_APPS += ["storages"] # noqa F405 61 | # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings 62 | AWS_ACCESS_KEY_ID = env("DJANGO_AWS_ACCESS_KEY_ID") 63 | # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings 64 | AWS_SECRET_ACCESS_KEY = env("DJANGO_AWS_SECRET_ACCESS_KEY") 65 | # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings 66 | AWS_STORAGE_BUCKET_NAME = env("DJANGO_AWS_STORAGE_BUCKET_NAME") 67 | # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings 68 | AWS_QUERYSTRING_AUTH = False 69 | # DO NOT change these unless you know what you're doing. 70 | _AWS_EXPIRY = 60 * 60 * 24 * 7 71 | # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings 72 | AWS_S3_OBJECT_PARAMETERS = { 73 | "CacheControl": f"max-age={_AWS_EXPIRY}, s-maxage={_AWS_EXPIRY}, must-revalidate" 74 | } 75 | # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings 76 | AWS_S3_REGION_NAME = env("DJANGO_AWS_S3_REGION_NAME", default=None) 77 | # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#cloudfront 78 | AWS_S3_CUSTOM_DOMAIN = env("DJANGO_AWS_S3_CUSTOM_DOMAIN", default=None) 79 | aws_s3_domain = AWS_S3_CUSTOM_DOMAIN or f"{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com" 80 | # STATIC 81 | # ------------------------ 82 | STATICFILES_STORAGE = "django_react_paypal.utils.storages.StaticRootS3Boto3Storage" 83 | COLLECTFAST_STRATEGY = "collectfast.strategies.boto3.Boto3Strategy" 84 | STATIC_URL = f"https://{aws_s3_domain}/static/" 85 | # MEDIA 86 | # ------------------------------------------------------------------------------ 87 | DEFAULT_FILE_STORAGE = "django_react_paypal.utils.storages.MediaRootS3Boto3Storage" 88 | MEDIA_URL = f"https://{aws_s3_domain}/media/" 89 | 90 | # TEMPLATES 91 | # ------------------------------------------------------------------------------ 92 | # https://docs.djangoproject.com/en/dev/ref/settings/#templates 93 | TEMPLATES[-1]["OPTIONS"]["loaders"] = [ # type: ignore[index] # noqa F405 94 | ( 95 | "django.template.loaders.cached.Loader", 96 | [ 97 | "django.template.loaders.filesystem.Loader", 98 | "django.template.loaders.app_directories.Loader", 99 | ], 100 | ) 101 | ] 102 | 103 | # EMAIL 104 | # ------------------------------------------------------------------------------ 105 | # https://docs.djangoproject.com/en/dev/ref/settings/#default-from-email 106 | DEFAULT_FROM_EMAIL = env( 107 | "DJANGO_DEFAULT_FROM_EMAIL", default="Django React PayPal " 108 | ) 109 | # https://docs.djangoproject.com/en/dev/ref/settings/#server-email 110 | SERVER_EMAIL = env("DJANGO_SERVER_EMAIL", default=DEFAULT_FROM_EMAIL) 111 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-subject-prefix 112 | EMAIL_SUBJECT_PREFIX = env( 113 | "DJANGO_EMAIL_SUBJECT_PREFIX", 114 | default="[Django React PayPal]", 115 | ) 116 | 117 | # ADMIN 118 | # ------------------------------------------------------------------------------ 119 | # Django Admin URL regex. 120 | ADMIN_URL = env("DJANGO_ADMIN_URL") 121 | 122 | # Anymail 123 | # ------------------------------------------------------------------------------ 124 | # https://anymail.readthedocs.io/en/stable/installation/#installing-anymail 125 | INSTALLED_APPS += ["anymail"] # noqa F405 126 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend 127 | # https://anymail.readthedocs.io/en/stable/installation/#anymail-settings-reference 128 | # https://anymail.readthedocs.io/en/stable/esps/mailgun/ 129 | EMAIL_BACKEND = "anymail.backends.mailgun.EmailBackend" 130 | ANYMAIL = { 131 | "MAILGUN_API_KEY": env("MAILGUN_API_KEY"), 132 | "MAILGUN_SENDER_DOMAIN": env("MAILGUN_DOMAIN"), 133 | "MAILGUN_API_URL": env("MAILGUN_API_URL", default="https://api.mailgun.net/v3"), 134 | } 135 | 136 | # Collectfast 137 | # ------------------------------------------------------------------------------ 138 | # https://github.com/antonagestam/collectfast#installation 139 | INSTALLED_APPS = ["collectfast"] + INSTALLED_APPS # noqa F405 140 | 141 | # LOGGING 142 | # ------------------------------------------------------------------------------ 143 | # https://docs.djangoproject.com/en/dev/ref/settings/#logging 144 | # See https://docs.djangoproject.com/en/dev/topics/logging for 145 | # more details on how to customize your logging configuration. 146 | # A sample logging configuration. The only tangible logging 147 | # performed by this configuration is to send an email to 148 | # the site admins on every HTTP 500 error when DEBUG=False. 149 | LOGGING = { 150 | "version": 1, 151 | "disable_existing_loggers": False, 152 | "filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}}, 153 | "formatters": { 154 | "verbose": { 155 | "format": "%(levelname)s %(asctime)s %(module)s " 156 | "%(process)d %(thread)d %(message)s" 157 | } 158 | }, 159 | "handlers": { 160 | "mail_admins": { 161 | "level": "ERROR", 162 | "filters": ["require_debug_false"], 163 | "class": "django.utils.log.AdminEmailHandler", 164 | }, 165 | "console": { 166 | "level": "DEBUG", 167 | "class": "logging.StreamHandler", 168 | "formatter": "verbose", 169 | }, 170 | }, 171 | "root": {"level": "INFO", "handlers": ["console"]}, 172 | "loggers": { 173 | "django.request": { 174 | "handlers": ["mail_admins"], 175 | "level": "ERROR", 176 | "propagate": True, 177 | }, 178 | "django.security.DisallowedHost": { 179 | "level": "ERROR", 180 | "handlers": ["console", "mail_admins"], 181 | "propagate": True, 182 | }, 183 | }, 184 | } 185 | 186 | # Your stuff... 187 | # ------------------------------------------------------------------------------ 188 | 189 | PAYPAL_MODE = "live" -------------------------------------------------------------------------------- /config/settings/test.py: -------------------------------------------------------------------------------- 1 | """ 2 | With these settings, tests run faster. 3 | """ 4 | 5 | from .base import * # noqa 6 | from .base import env 7 | 8 | # GENERAL 9 | # ------------------------------------------------------------------------------ 10 | # https://docs.djangoproject.com/en/dev/ref/settings/#secret-key 11 | SECRET_KEY = env( 12 | "DJANGO_SECRET_KEY", 13 | default="DJji1cuTAlod6cBW98Ed7p2KTO2KjULrA37LaxQAkjlqt6ROarQ8FaJ8DalClmwX", 14 | ) 15 | # https://docs.djangoproject.com/en/dev/ref/settings/#test-runner 16 | TEST_RUNNER = "django.test.runner.DiscoverRunner" 17 | 18 | # PASSWORDS 19 | # ------------------------------------------------------------------------------ 20 | # https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers 21 | PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"] 22 | 23 | # TEMPLATES 24 | # ------------------------------------------------------------------------------ 25 | TEMPLATES[-1]["OPTIONS"]["loaders"] = [ # type: ignore[index] # noqa F405 26 | ( 27 | "django.template.loaders.cached.Loader", 28 | [ 29 | "django.template.loaders.filesystem.Loader", 30 | "django.template.loaders.app_directories.Loader", 31 | ], 32 | ) 33 | ] 34 | 35 | # EMAIL 36 | # ------------------------------------------------------------------------------ 37 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend 38 | EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" 39 | 40 | # Your stuff... 41 | # ------------------------------------------------------------------------------ 42 | -------------------------------------------------------------------------------- /config/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.conf.urls.static import static 3 | from django.contrib import admin 4 | from django.urls import include, path 5 | from django.views import defaults as default_views 6 | from django.views.generic import TemplateView 7 | from rest_framework.authtoken.views import obtain_auth_token 8 | from django_react_paypal.payments.views import ProcessWebhookView 9 | 10 | urlpatterns = [ 11 | path("", TemplateView.as_view(template_name="pages/home.html"), name="home"), 12 | path( 13 | "about/", TemplateView.as_view(template_name="pages/about.html"), name="about" 14 | ), 15 | # Django Admin, use {% url 'admin:index' %} 16 | path(settings.ADMIN_URL, admin.site.urls), 17 | # User management 18 | path("users/", include("django_react_paypal.users.urls", namespace="users")), 19 | path("accounts/", include("allauth.urls")), 20 | path("webhooks/paypal/", ProcessWebhookView.as_view()) 21 | 22 | # Your stuff: custom urls includes go here 23 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 24 | 25 | # API URLS 26 | urlpatterns += [ 27 | # API base url 28 | path("api/", include("config.api_router")), 29 | # DRF auth token 30 | path("auth-token/", obtain_auth_token), 31 | ] 32 | 33 | if settings.DEBUG: 34 | # This allows the error pages to be debugged during development, just visit 35 | # these url in browser to see how these error pages look like. 36 | urlpatterns += [ 37 | path( 38 | "400/", 39 | default_views.bad_request, 40 | kwargs={"exception": Exception("Bad Request!")}, 41 | ), 42 | path( 43 | "403/", 44 | default_views.permission_denied, 45 | kwargs={"exception": Exception("Permission Denied")}, 46 | ), 47 | path( 48 | "404/", 49 | default_views.page_not_found, 50 | kwargs={"exception": Exception("Page not Found")}, 51 | ), 52 | path("500/", default_views.server_error), 53 | ] 54 | if "debug_toolbar" in settings.INSTALLED_APPS: 55 | import debug_toolbar 56 | 57 | urlpatterns = [path("__debug__/", include(debug_toolbar.urls))] + urlpatterns 58 | -------------------------------------------------------------------------------- /config/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for Django React PayPal project. 3 | 4 | This module contains the WSGI application used by Django's development server 5 | and any production WSGI deployments. It should expose a module-level variable 6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 7 | this application via the ``WSGI_APPLICATION`` setting. 8 | 9 | Usually you will have the standard Django WSGI application here, but it also 10 | might make sense to replace the whole Django WSGI application with a custom one 11 | that later delegates to the Django one. For example, you could introduce WSGI 12 | middleware here, or combine a Django application with an application of another 13 | framework. 14 | 15 | """ 16 | import os 17 | import sys 18 | from pathlib import Path 19 | 20 | from django.core.wsgi import get_wsgi_application 21 | 22 | # This allows easy placement of apps within the interior 23 | # django_react_paypal directory. 24 | ROOT_DIR = Path(__file__).resolve(strict=True).parent.parent 25 | sys.path.append(str(ROOT_DIR / "django_react_paypal")) 26 | # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks 27 | # if running multiple sites in the same mod_wsgi process. To fix this, use 28 | # mod_wsgi daemon mode with each site in its own daemon process, or use 29 | # os.environ["DJANGO_SETTINGS_MODULE"] = "config.settings.production" 30 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production") 31 | 32 | # This application object is used by any WSGI server configured to use this 33 | # file. This includes Django's development server, if the WSGI_APPLICATION 34 | # setting points here. 35 | application = get_wsgi_application() 36 | # Apply WSGI middleware here. 37 | # from helloworld.wsgi import HelloWorldApplication 38 | # application = HelloWorldApplication(application) 39 | -------------------------------------------------------------------------------- /django_react_paypal/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.0" 2 | __version_info__ = tuple( 3 | [ 4 | int(num) if num.isdigit() else num 5 | for num in __version__.replace("-", ".", 1).split(".") 6 | ] 7 | ) 8 | -------------------------------------------------------------------------------- /django_react_paypal/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from django_react_paypal.users.models import User 4 | from django_react_paypal.users.tests.factories import UserFactory 5 | 6 | 7 | @pytest.fixture(autouse=True) 8 | def media_storage(settings, tmpdir): 9 | settings.MEDIA_ROOT = tmpdir.strpath 10 | 11 | 12 | @pytest.fixture 13 | def user() -> User: 14 | return UserFactory() 15 | -------------------------------------------------------------------------------- /django_react_paypal/contrib/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | To understand why this file is here, please read: 3 | 4 | http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django 5 | """ 6 | -------------------------------------------------------------------------------- /django_react_paypal/contrib/sites/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | To understand why this file is here, please read: 3 | 4 | http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django 5 | """ 6 | -------------------------------------------------------------------------------- /django_react_paypal/contrib/sites/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | import django.contrib.sites.models 2 | from django.contrib.sites.models import _simple_domain_name_validator 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [] 9 | 10 | operations = [ 11 | migrations.CreateModel( 12 | name="Site", 13 | fields=[ 14 | ( 15 | "id", 16 | models.AutoField( 17 | verbose_name="ID", 18 | serialize=False, 19 | auto_created=True, 20 | primary_key=True, 21 | ), 22 | ), 23 | ( 24 | "domain", 25 | models.CharField( 26 | max_length=100, 27 | verbose_name="domain name", 28 | validators=[_simple_domain_name_validator], 29 | ), 30 | ), 31 | ("name", models.CharField(max_length=50, verbose_name="display name")), 32 | ], 33 | options={ 34 | "ordering": ("domain",), 35 | "db_table": "django_site", 36 | "verbose_name": "site", 37 | "verbose_name_plural": "sites", 38 | }, 39 | bases=(models.Model,), 40 | managers=[("objects", django.contrib.sites.models.SiteManager())], 41 | ) 42 | ] 43 | -------------------------------------------------------------------------------- /django_react_paypal/contrib/sites/migrations/0002_alter_domain_unique.py: -------------------------------------------------------------------------------- 1 | import django.contrib.sites.models 2 | from django.db import migrations, models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [("sites", "0001_initial")] 8 | 9 | operations = [ 10 | migrations.AlterField( 11 | model_name="site", 12 | name="domain", 13 | field=models.CharField( 14 | max_length=100, 15 | unique=True, 16 | validators=[django.contrib.sites.models._simple_domain_name_validator], 17 | verbose_name="domain name", 18 | ), 19 | ) 20 | ] 21 | -------------------------------------------------------------------------------- /django_react_paypal/contrib/sites/migrations/0003_set_site_domain_and_name.py: -------------------------------------------------------------------------------- 1 | """ 2 | To understand why this file is here, please read: 3 | 4 | http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django 5 | """ 6 | from django.conf import settings 7 | from django.db import migrations 8 | 9 | 10 | def update_site_forward(apps, schema_editor): 11 | """Set site domain and name.""" 12 | Site = apps.get_model("sites", "Site") 13 | Site.objects.update_or_create( 14 | id=settings.SITE_ID, 15 | defaults={ 16 | "domain": "example.com", 17 | "name": "Django React PayPal", 18 | }, 19 | ) 20 | 21 | 22 | def update_site_backward(apps, schema_editor): 23 | """Revert site domain and name to default.""" 24 | Site = apps.get_model("sites", "Site") 25 | Site.objects.update_or_create( 26 | id=settings.SITE_ID, defaults={"domain": "example.com", "name": "example.com"} 27 | ) 28 | 29 | 30 | class Migration(migrations.Migration): 31 | 32 | dependencies = [("sites", "0002_alter_domain_unique")] 33 | 34 | operations = [migrations.RunPython(update_site_forward, update_site_backward)] 35 | -------------------------------------------------------------------------------- /django_react_paypal/contrib/sites/migrations/0004_alter_options_ordering_domain.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.7 on 2021-02-04 14:49 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('sites', '0003_set_site_domain_and_name'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='site', 15 | options={'ordering': ['domain'], 'verbose_name': 'site', 'verbose_name_plural': 'sites'}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /django_react_paypal/contrib/sites/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | To understand why this file is here, please read: 3 | 4 | http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django 5 | """ 6 | -------------------------------------------------------------------------------- /django_react_paypal/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /django_react_paypal/frontend/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | 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. 37 | 38 | 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. 39 | 40 | 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. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /django_react_paypal/frontend/compose/local/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14-alpine AS development 2 | ENV NODE_ENV development 3 | 4 | WORKDIR /app 5 | 6 | # Cache and Install dependencies - very important step 7 | COPY package.json . 8 | COPY package-lock.json . 9 | 10 | RUN npm install 11 | 12 | COPY . . 13 | 14 | EXPOSE 3000 15 | 16 | CMD [ "npm", "run", "start" ] -------------------------------------------------------------------------------- /django_react_paypal/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@paypal/react-paypal-js": "^7.2.1", 7 | "@testing-library/jest-dom": "^5.14.1", 8 | "@testing-library/react": "^11.2.7", 9 | "@testing-library/user-event": "^12.8.3", 10 | "@types/jest": "^26.0.24", 11 | "@types/node": "^12.20.18", 12 | "@types/react": "^17.0.15", 13 | "@types/react-dom": "^17.0.9", 14 | "react": "^17.0.2", 15 | "react-dom": "^17.0.2", 16 | "react-hot-toast": "^2.1.0", 17 | "react-scripts": "4.0.3", 18 | "typescript": "^4.3.5", 19 | "web-vitals": "^1.1.2" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test", 25 | "eject": "react-scripts eject" 26 | }, 27 | "eslintConfig": { 28 | "extends": [ 29 | "react-app", 30 | "react-app/jest" 31 | ] 32 | }, 33 | "browserslist": { 34 | "production": [ 35 | ">0.2%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 1 chrome version", 41 | "last 1 firefox version", 42 | "last 1 safari version" 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /django_react_paypal/frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django_react_paypal/d3aa6a16ff0bf08d30ce79204a37d8bb7b806bd5/django_react_paypal/frontend/public/favicon.ico -------------------------------------------------------------------------------- /django_react_paypal/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 25 | 27 | React App 28 | 29 | 30 | 31 | 32 |
33 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /django_react_paypal/frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django_react_paypal/d3aa6a16ff0bf08d30ce79204a37d8bb7b806bd5/django_react_paypal/frontend/public/logo192.png -------------------------------------------------------------------------------- /django_react_paypal/frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django_react_paypal/d3aa6a16ff0bf08d30ce79204a37d8bb7b806bd5/django_react_paypal/frontend/public/logo512.png -------------------------------------------------------------------------------- /django_react_paypal/frontend/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 | -------------------------------------------------------------------------------- /django_react_paypal/frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /django_react_paypal/frontend/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /django_react_paypal/frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { PayPalScriptProvider } from "@paypal/react-paypal-js"; 2 | import { Toaster } from "react-hot-toast"; 3 | import { Payment } from './components/Payment' 4 | 5 | export default function App() { 6 | return ( 7 | 8 | 9 | 10 | 11 | ); 12 | } -------------------------------------------------------------------------------- /django_react_paypal/frontend/src/components/Payment.tsx: -------------------------------------------------------------------------------- 1 | import { PayPalButtons } from "@paypal/react-paypal-js"; 2 | import toast from "react-hot-toast"; 3 | 4 | 5 | export function Payment() { 6 | 7 | return ( 8 |
9 | Airtable product 10 |
11 |

Airtable Product

12 |

$10.00

13 |

Some information about the product

14 | { 17 | return actions.order.create({ 18 | purchase_units: [ 19 | { 20 | amount: { 21 | value: "10.00" 22 | }, 23 | custom_id: "e-book-1234" // the name or slug of the thing you're selling 24 | }, 25 | ], 26 | }); 27 | }} 28 | onApprove={(data, actions) => { 29 | return actions.order.capture().then(function (details) { 30 | toast.success('Payment completed. Thank you, ' + details.payer.name.given_name) 31 | }); 32 | }} 33 | onCancel={() => toast( 34 | "You cancelled the payment. Try again by clicking the PayPal button", { 35 | duration: 6000, 36 | })} 37 | onError={(err) => { 38 | toast.error( 39 | "There was an error processing your payment. If this error please contact support.", { 40 | duration: 6000, 41 | }); 42 | }} 43 | /> 44 |
45 |
46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /django_react_paypal/frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #fdf1ec; 3 | } 4 | 5 | h1, img { 6 | margin: 0; 7 | padding: 0; 8 | } 9 | 10 | .card { 11 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); 12 | max-width: 300px; 13 | margin: auto; 14 | text-align: center; 15 | font-family: arial; 16 | } 17 | 18 | .card h1, p { 19 | color: #474747; 20 | } 21 | 22 | .card-details { 23 | background-color: #ffffff; 24 | padding-bottom: 10px; 25 | padding-left: 5px; 26 | padding-right: 5px; 27 | } 28 | 29 | .price { 30 | color: grey; 31 | font-size: 22px; 32 | } 33 | 34 | -------------------------------------------------------------------------------- /django_react_paypal/frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /django_react_paypal/frontend/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare namespace NodeJS { 4 | export interface ProcessEnv { 5 | REACT_APP_PAYPAL_CLIENT_ID: string; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /django_react_paypal/frontend/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /django_react_paypal/frontend/src/setupTests.ts: -------------------------------------------------------------------------------- 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'; 6 | -------------------------------------------------------------------------------- /django_react_paypal/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /django_react_paypal/payments/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django_react_paypal/d3aa6a16ff0bf08d30ce79204a37d8bb7b806bd5/django_react_paypal/payments/__init__.py -------------------------------------------------------------------------------- /django_react_paypal/payments/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /django_react_paypal/payments/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PaymentsConfig(AppConfig): 5 | name = "django_react_paypal.payments" 6 | -------------------------------------------------------------------------------- /django_react_paypal/payments/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django_react_paypal/d3aa6a16ff0bf08d30ce79204a37d8bb7b806bd5/django_react_paypal/payments/migrations/__init__.py -------------------------------------------------------------------------------- /django_react_paypal/payments/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | -------------------------------------------------------------------------------- /django_react_paypal/payments/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /django_react_paypal/payments/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.conf import settings 4 | from django.core.mail import send_mail 5 | from django.http import HttpResponse, HttpResponseBadRequest 6 | from django.utils.decorators import method_decorator 7 | from django.views.decorators.csrf import csrf_exempt 8 | from django.views.generic import View 9 | 10 | from paypalrestsdk import notifications 11 | 12 | 13 | @method_decorator(csrf_exempt, name="dispatch") 14 | class ProcessWebhookView(View): 15 | def post(self, request): 16 | if "HTTP_PAYPAL_TRANSMISSION_ID" not in request.META: 17 | return HttpResponseBadRequest() 18 | 19 | auth_algo = request.META['HTTP_PAYPAL_AUTH_ALGO'] 20 | cert_url = request.META['HTTP_PAYPAL_CERT_URL'] 21 | transmission_id = request.META['HTTP_PAYPAL_TRANSMISSION_ID'] 22 | transmission_sig = request.META['HTTP_PAYPAL_TRANSMISSION_SIG'] 23 | transmission_time = request.META['HTTP_PAYPAL_TRANSMISSION_TIME'] 24 | webhook_id = settings.PAYPAL_WEBHOOK_ID 25 | event_body = request.body.decode(request.encoding or "utf-8") 26 | 27 | valid = notifications.WebhookEvent.verify( 28 | transmission_id=transmission_id, 29 | timestamp=transmission_time, 30 | webhook_id=webhook_id, 31 | event_body=event_body, 32 | cert_url=cert_url, 33 | actual_sig=transmission_sig, 34 | auth_algo=auth_algo, 35 | ) 36 | 37 | if not valid: 38 | return HttpResponseBadRequest() 39 | 40 | webhook_event = json.loads(event_body) 41 | 42 | event_type = webhook_event["event_type"] 43 | 44 | CHECKOUT_ORDER_APPROVED = "CHECKOUT.ORDER.APPROVED" 45 | 46 | if event_type == CHECKOUT_ORDER_APPROVED: 47 | customer_email = webhook_event["resource"]["payer"]["email_address"] 48 | product_link = "https://learn.justdjango.com" 49 | send_mail( 50 | subject="Your access", 51 | message=f"Thank you for purchasing my product. Here is the link: {product_link}", 52 | from_email="your@email.com", 53 | recipient_list=[customer_email] 54 | ) 55 | 56 | # Accessing purchased items when selling multiple products 57 | # webhook_event["resource"]["purchase_units"][0]["custom_id"] # 'e-book-1234' 58 | 59 | return HttpResponse() 60 | -------------------------------------------------------------------------------- /django_react_paypal/static/css/project.css: -------------------------------------------------------------------------------- 1 | /* These styles are generated from project.scss. */ 2 | 3 | .alert-debug { 4 | color: black; 5 | background-color: white; 6 | border-color: #d6e9c6; 7 | } 8 | 9 | .alert-error { 10 | color: #b94a48; 11 | background-color: #f2dede; 12 | border-color: #eed3d7; 13 | } 14 | -------------------------------------------------------------------------------- /django_react_paypal/static/fonts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django_react_paypal/d3aa6a16ff0bf08d30ce79204a37d8bb7b806bd5/django_react_paypal/static/fonts/.gitkeep -------------------------------------------------------------------------------- /django_react_paypal/static/images/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django_react_paypal/d3aa6a16ff0bf08d30ce79204a37d8bb7b806bd5/django_react_paypal/static/images/favicons/favicon.ico -------------------------------------------------------------------------------- /django_react_paypal/static/js/project.js: -------------------------------------------------------------------------------- 1 | /* Project specific Javascript goes here. */ 2 | -------------------------------------------------------------------------------- /django_react_paypal/static/sass/custom_bootstrap_vars.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django_react_paypal/d3aa6a16ff0bf08d30ce79204a37d8bb7b806bd5/django_react_paypal/static/sass/custom_bootstrap_vars.scss -------------------------------------------------------------------------------- /django_react_paypal/static/sass/project.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | // project specific CSS goes here 6 | 7 | //////////////////////////////// 8 | //Variables// 9 | //////////////////////////////// 10 | 11 | // Alert colors 12 | 13 | $white: #fff; 14 | $mint-green: #d6e9c6; 15 | $black: #000; 16 | $pink: #f2dede; 17 | $dark-pink: #eed3d7; 18 | $red: #b94a48; 19 | 20 | //////////////////////////////// 21 | //Alerts// 22 | //////////////////////////////// 23 | 24 | // bootstrap alert CSS, translated to the django-standard levels of 25 | // debug, info, success, warning, error 26 | 27 | .alert-debug { 28 | background-color: $white; 29 | border-color: $mint-green; 30 | color: $black; 31 | } 32 | 33 | .alert-error { 34 | background-color: $pink; 35 | border-color: $dark-pink; 36 | color: $red; 37 | } 38 | -------------------------------------------------------------------------------- /django_react_paypal/templates/403.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Forbidden (403){% endblock %} 4 | 5 | {% block content %} 6 |

Forbidden (403)

7 | 8 |

{% if exception %}{{ exception }}{% else %}You're not allowed to access this page.{% endif %}

9 | {% endblock content %} 10 | -------------------------------------------------------------------------------- /django_react_paypal/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Page not found{% endblock %} 4 | 5 | {% block content %} 6 |

Page not found

7 | 8 |

{% if exception %}{{ exception }}{% else %}This is not the page you were looking for.{% endif %}

9 | {% endblock content %} 10 | -------------------------------------------------------------------------------- /django_react_paypal/templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Server Error{% endblock %} 4 | 5 | {% block content %} 6 |

Ooops!!! 500

7 | 8 |

Looks like something went wrong!

9 | 10 |

We track these errors automatically, but if the problem persists feel free to contact us. In the meantime, try refreshing.

11 | {% endblock content %} 12 | -------------------------------------------------------------------------------- /django_react_paypal/templates/account/account_inactive.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %}{% translate "Account Inactive" %}{% endblock %} 6 | 7 | {% block inner %} 8 |

{% translate "Account Inactive" %}

9 | 10 |

{% translate "This account is inactive." %}

11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /django_react_paypal/templates/account/base.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}{% block head_title %}{% endblock head_title %}{% endblock title %} 3 | 4 | {% block content %} 5 |
6 |
7 | {% block inner %}{% endblock %} 8 |
9 |
10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /django_react_paypal/templates/account/email.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "account/base.html" %} 3 | 4 | {% load i18n %} 5 | {% load crispy_forms_tags %} 6 | 7 | {% block head_title %}{% translate "Account" %}{% endblock %} 8 | 9 | {% block inner %} 10 |

{% translate "E-mail Addresses" %}

11 | 12 | {% if user.emailaddress_set.all %} 13 |

{% translate 'The following e-mail addresses are associated with your account:' %}

14 | 15 | 44 | 45 | {% else %} 46 |

{% translate 'Warning:'%} {% translate "You currently do not have any e-mail address set up. You should really add an e-mail address so you can receive notifications, reset your password, etc." %}

47 | 48 | {% endif %} 49 | 50 | 51 |

{% translate "Add E-mail Address" %}

52 | 53 |
54 | {% csrf_token %} 55 | {{ form|crispy }} 56 | 57 |
58 | 59 | {% endblock %} 60 | 61 | 62 | {% block inline_javascript %} 63 | {{ block.super }} 64 | 81 | {% endblock %} 82 | -------------------------------------------------------------------------------- /django_react_paypal/templates/account/email_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load account %} 5 | 6 | {% block head_title %}{% translate "Confirm E-mail Address" %}{% endblock %} 7 | 8 | 9 | {% block inner %} 10 |

{% translate "Confirm E-mail Address" %}

11 | 12 | {% if confirmation %} 13 | 14 | {% user_display confirmation.email_address.user as user_display %} 15 | 16 |

{% blocktranslate with confirmation.email_address.email as email %}Please confirm that {{ email }} is an e-mail address for user {{ user_display }}.{% endblocktranslate %}

17 | 18 |
19 | {% csrf_token %} 20 | 21 |
22 | 23 | {% else %} 24 | 25 | {% url 'account_email' as email_url %} 26 | 27 |

{% blocktranslate %}This e-mail confirmation link expired or is invalid. Please issue a new e-mail confirmation request.{% endblocktranslate %}

28 | 29 | {% endif %} 30 | 31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /django_react_paypal/templates/account/login.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load account socialaccount %} 5 | {% load crispy_forms_tags %} 6 | 7 | {% block head_title %}{% translate "Sign In" %}{% endblock %} 8 | 9 | {% block inner %} 10 | 11 |

{% translate "Sign In" %}

12 | 13 | {% get_providers as socialaccount_providers %} 14 | 15 | {% if socialaccount_providers %} 16 |

{% blocktranslate with site.name as site_name %}Please sign in with one 17 | of your existing third party accounts. Or, sign up 18 | for a {{ site_name }} account and sign in below:{% endblocktranslate %}

19 | 20 |
21 | 22 |
    23 | {% include "socialaccount/snippets/provider_list.html" with process="login" %} 24 |
25 | 26 | 27 | 28 |
29 | 30 | {% include "socialaccount/snippets/login_extra.html" %} 31 | 32 | {% else %} 33 |

{% blocktranslate %}If you have not created an account yet, then please 34 | sign up first.{% endblocktranslate %}

35 | {% endif %} 36 | 37 | 46 | 47 | {% endblock %} 48 | -------------------------------------------------------------------------------- /django_react_paypal/templates/account/logout.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %}{% translate "Sign Out" %}{% endblock %} 6 | 7 | {% block inner %} 8 |

{% translate "Sign Out" %}

9 | 10 |

{% translate 'Are you sure you want to sign out?' %}

11 | 12 |
13 | {% csrf_token %} 14 | {% if redirect_field_value %} 15 | 16 | {% endif %} 17 | 18 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /django_react_paypal/templates/account/password_change.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load crispy_forms_tags %} 5 | 6 | {% block head_title %}{% translate "Change Password" %}{% endblock %} 7 | 8 | {% block inner %} 9 |

{% translate "Change Password" %}

10 | 11 |
12 | {% csrf_token %} 13 | {{ form|crispy }} 14 | 15 |
16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /django_react_paypal/templates/account/password_reset.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load account %} 5 | {% load crispy_forms_tags %} 6 | 7 | {% block head_title %}{% translate "Password Reset" %}{% endblock %} 8 | 9 | {% block inner %} 10 | 11 |

{% translate "Password Reset" %}

12 | {% if user.is_authenticated %} 13 | {% include "account/snippets/already_logged_in.html" %} 14 | {% endif %} 15 | 16 |

{% translate "Forgotten your password? Enter your e-mail address below, and we'll send you an e-mail allowing you to reset it." %}

17 | 18 |
19 | {% csrf_token %} 20 | {{ form|crispy }} 21 | 22 |
23 | 24 |

{% blocktranslate %}Please contact us if you have any trouble resetting your password.{% endblocktranslate %}

25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /django_react_paypal/templates/account/password_reset_done.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load account %} 5 | 6 | {% block head_title %}{% translate "Password Reset" %}{% endblock %} 7 | 8 | {% block inner %} 9 |

{% translate "Password Reset" %}

10 | 11 | {% if user.is_authenticated %} 12 | {% include "account/snippets/already_logged_in.html" %} 13 | {% endif %} 14 | 15 |

{% blocktranslate %}We have sent you an e-mail. Please contact us if you do not receive it within a few minutes.{% endblocktranslate %}

16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /django_react_paypal/templates/account/password_reset_from_key.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load crispy_forms_tags %} 5 | {% block head_title %}{% translate "Change Password" %}{% endblock %} 6 | 7 | {% block inner %} 8 |

{% if token_fail %}{% translate "Bad Token" %}{% else %}{% translate "Change Password" %}{% endif %}

9 | 10 | {% if token_fail %} 11 | {% url 'account_reset_password' as passwd_reset_url %} 12 |

{% blocktranslate %}The password reset link was invalid, possibly because it has already been used. Please request a new password reset.{% endblocktranslate %}

13 | {% else %} 14 | {% if form %} 15 |
16 | {% csrf_token %} 17 | {{ form|crispy }} 18 | 19 |
20 | {% else %} 21 |

{% translate 'Your password is now changed.' %}

22 | {% endif %} 23 | {% endif %} 24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /django_react_paypal/templates/account/password_reset_from_key_done.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% block head_title %}{% translate "Change Password" %}{% endblock %} 5 | 6 | {% block inner %} 7 |

{% translate "Change Password" %}

8 |

{% translate 'Your password is now changed.' %}

9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /django_react_paypal/templates/account/password_set.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load crispy_forms_tags %} 5 | 6 | {% block head_title %}{% translate "Set Password" %}{% endblock %} 7 | 8 | {% block inner %} 9 |

{% translate "Set Password" %}

10 | 11 |
12 | {% csrf_token %} 13 | {{ form|crispy }} 14 | 15 |
16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /django_react_paypal/templates/account/signup.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load crispy_forms_tags %} 5 | 6 | {% block head_title %}{% translate "Signup" %}{% endblock %} 7 | 8 | {% block inner %} 9 |

{% translate "Sign Up" %}

10 | 11 |

{% blocktranslate %}Already have an account? Then please sign in.{% endblocktranslate %}

12 | 13 | 21 | 22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /django_react_paypal/templates/account/signup_closed.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %}{% translate "Sign Up Closed" %}{% endblock %} 6 | 7 | {% block inner %} 8 |

{% translate "Sign Up Closed" %}

9 | 10 |

{% translate "We are sorry, but the sign up is currently closed." %}

11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /django_react_paypal/templates/account/verification_sent.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %}{% translate "Verify Your E-mail Address" %}{% endblock %} 6 | 7 | {% block inner %} 8 |

{% translate "Verify Your E-mail Address" %}

9 | 10 |

{% blocktranslate %}We have sent an e-mail to you for verification. Follow the link provided to finalize the signup process. Please contact us if you do not receive it within a few minutes.{% endblocktranslate %}

11 | 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /django_react_paypal/templates/account/verified_email_required.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %}{% translate "Verify Your E-mail Address" %}{% endblock %} 6 | 7 | {% block inner %} 8 |

{% translate "Verify Your E-mail Address" %}

9 | 10 | {% url 'account_email' as email_url %} 11 | 12 |

{% blocktranslate %}This part of the site requires us to verify that 13 | you are who you claim to be. For this purpose, we require that you 14 | verify ownership of your e-mail address. {% endblocktranslate %}

15 | 16 |

{% blocktranslate %}We have sent an e-mail to you for 17 | verification. Please click on the link inside this e-mail. Please 18 | contact us if you do not receive it within a few minutes.{% endblocktranslate %}

19 | 20 |

{% blocktranslate %}Note: you can still change your e-mail address.{% endblocktranslate %}

21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /django_react_paypal/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static i18n %} 2 | 3 | 4 | 5 | 6 | {% block title %}Django React PayPal{% endblock title %} 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 18 | {% block css %} 19 | 20 | 21 | 22 | 23 | 24 | 25 | {% endblock %} 26 | 28 | {# Placed at the top of the document so pages load faster with defer #} 29 | {% block javascript %} 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | {% endblock javascript %} 41 | 42 | 43 | 44 | 45 | 46 |
47 | 83 | 84 |
85 | 86 |
87 | 88 | {% if messages %} 89 | {% for message in messages %} 90 |
{{ message }}
91 | {% endfor %} 92 | {% endif %} 93 | 94 | {% block content %} 95 |

Use this document as a way to quick start any new project.

96 | {% endblock content %} 97 | 98 |
99 | 100 | {% block modal %}{% endblock modal %} 101 | 102 | {% block inline_javascript %} 103 | {# Script tags with only code, no src (defer by default) #} 104 | {% endblock inline_javascript %} 105 | 106 | 107 | -------------------------------------------------------------------------------- /django_react_paypal/templates/pages/about.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | -------------------------------------------------------------------------------- /django_react_paypal/templates/pages/home.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | -------------------------------------------------------------------------------- /django_react_paypal/templates/users/user_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | 4 | {% block title %}User: {{ object.username }}{% endblock %} 5 | 6 | {% block content %} 7 |
8 | 9 |
10 |
11 | 12 |

{{ object.username }}

13 | {% if object.name %} 14 |

{{ object.name }}

15 | {% endif %} 16 |
17 |
18 | 19 | {% if object == request.user %} 20 | 21 |
22 | 23 |
24 | My Info 25 | E-Mail 26 | 27 |
28 | 29 |
30 | 31 | {% endif %} 32 | 33 |
34 | {% endblock content %} 35 | -------------------------------------------------------------------------------- /django_react_paypal/templates/users/user_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load crispy_forms_tags %} 3 | 4 | {% block title %}{{ user.username }}{% endblock %} 5 | 6 | {% block content %} 7 |

{{ user.username }}

8 |
9 | {% csrf_token %} 10 | {{ form|crispy }} 11 |
12 |
13 | 14 |
15 |
16 |
17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /django_react_paypal/users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django_react_paypal/d3aa6a16ff0bf08d30ce79204a37d8bb7b806bd5/django_react_paypal/users/__init__.py -------------------------------------------------------------------------------- /django_react_paypal/users/adapters.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from allauth.account.adapter import DefaultAccountAdapter 4 | from allauth.socialaccount.adapter import DefaultSocialAccountAdapter 5 | from django.conf import settings 6 | from django.http import HttpRequest 7 | 8 | 9 | class AccountAdapter(DefaultAccountAdapter): 10 | def is_open_for_signup(self, request: HttpRequest): 11 | return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True) 12 | 13 | 14 | class SocialAccountAdapter(DefaultSocialAccountAdapter): 15 | def is_open_for_signup(self, request: HttpRequest, sociallogin: Any): 16 | return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True) 17 | -------------------------------------------------------------------------------- /django_react_paypal/users/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth import admin as auth_admin 3 | from django.contrib.auth import get_user_model 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | from django_react_paypal.users.forms import UserChangeForm, UserCreationForm 7 | 8 | User = get_user_model() 9 | 10 | 11 | @admin.register(User) 12 | class UserAdmin(auth_admin.UserAdmin): 13 | 14 | form = UserChangeForm 15 | add_form = UserCreationForm 16 | fieldsets = ( 17 | (None, {"fields": ("username", "password")}), 18 | (_("Personal info"), {"fields": ("name", "email")}), 19 | ( 20 | _("Permissions"), 21 | { 22 | "fields": ( 23 | "is_active", 24 | "is_staff", 25 | "is_superuser", 26 | "groups", 27 | "user_permissions", 28 | ), 29 | }, 30 | ), 31 | (_("Important dates"), {"fields": ("last_login", "date_joined")}), 32 | ) 33 | list_display = ["username", "name", "is_superuser"] 34 | search_fields = ["name"] 35 | -------------------------------------------------------------------------------- /django_react_paypal/users/api/serializers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from rest_framework import serializers 3 | 4 | User = get_user_model() 5 | 6 | 7 | class UserSerializer(serializers.ModelSerializer): 8 | class Meta: 9 | model = User 10 | fields = ["username", "name", "url"] 11 | 12 | extra_kwargs = { 13 | "url": {"view_name": "api:user-detail", "lookup_field": "username"} 14 | } 15 | -------------------------------------------------------------------------------- /django_react_paypal/users/api/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from rest_framework import status 3 | from rest_framework.decorators import action 4 | from rest_framework.mixins import ListModelMixin, RetrieveModelMixin, UpdateModelMixin 5 | from rest_framework.response import Response 6 | from rest_framework.viewsets import GenericViewSet 7 | 8 | from .serializers import UserSerializer 9 | 10 | User = get_user_model() 11 | 12 | 13 | class UserViewSet(RetrieveModelMixin, ListModelMixin, UpdateModelMixin, GenericViewSet): 14 | serializer_class = UserSerializer 15 | queryset = User.objects.all() 16 | lookup_field = "username" 17 | 18 | def get_queryset(self, *args, **kwargs): 19 | return self.queryset.filter(id=self.request.user.id) 20 | 21 | @action(detail=False, methods=["GET"]) 22 | def me(self, request): 23 | serializer = UserSerializer(request.user, context={"request": request}) 24 | return Response(status=status.HTTP_200_OK, data=serializer.data) 25 | -------------------------------------------------------------------------------- /django_react_paypal/users/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class UsersConfig(AppConfig): 6 | name = "django_react_paypal.users" 7 | verbose_name = _("Users") 8 | 9 | def ready(self): 10 | try: 11 | import django_react_paypal.users.signals # noqa F401 12 | except ImportError: 13 | pass 14 | -------------------------------------------------------------------------------- /django_react_paypal/users/forms.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import forms as admin_forms 2 | from django.contrib.auth import get_user_model 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | User = get_user_model() 6 | 7 | 8 | class UserChangeForm(admin_forms.UserChangeForm): 9 | class Meta(admin_forms.UserChangeForm.Meta): 10 | model = User 11 | 12 | 13 | class UserCreationForm(admin_forms.UserCreationForm): 14 | class Meta(admin_forms.UserCreationForm.Meta): 15 | model = User 16 | 17 | error_messages = { 18 | "username": {"unique": _("This username has already been taken.")} 19 | } 20 | -------------------------------------------------------------------------------- /django_react_paypal/users/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | import django.contrib.auth.models 2 | import django.contrib.auth.validators 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [("auth", "0008_alter_user_username_max_length")] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name="User", 16 | fields=[ 17 | ( 18 | "id", 19 | models.AutoField( 20 | auto_created=True, 21 | primary_key=True, 22 | serialize=False, 23 | verbose_name="ID", 24 | ), 25 | ), 26 | ("password", models.CharField(max_length=128, verbose_name="password")), 27 | ( 28 | "last_login", 29 | models.DateTimeField( 30 | blank=True, null=True, verbose_name="last login" 31 | ), 32 | ), 33 | ( 34 | "is_superuser", 35 | models.BooleanField( 36 | default=False, 37 | help_text="Designates that this user has all permissions without explicitly assigning them.", 38 | verbose_name="superuser status", 39 | ), 40 | ), 41 | ( 42 | "username", 43 | models.CharField( 44 | error_messages={ 45 | "unique": "A user with that username already exists." 46 | }, 47 | help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", 48 | max_length=150, 49 | unique=True, 50 | validators=[ 51 | django.contrib.auth.validators.UnicodeUsernameValidator() 52 | ], 53 | verbose_name="username", 54 | ), 55 | ), 56 | ( 57 | "email", 58 | models.EmailField( 59 | blank=True, max_length=254, verbose_name="email address" 60 | ), 61 | ), 62 | ( 63 | "is_staff", 64 | models.BooleanField( 65 | default=False, 66 | help_text="Designates whether the user can log into this admin site.", 67 | verbose_name="staff status", 68 | ), 69 | ), 70 | ( 71 | "is_active", 72 | models.BooleanField( 73 | default=True, 74 | help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", 75 | verbose_name="active", 76 | ), 77 | ), 78 | ( 79 | "date_joined", 80 | models.DateTimeField( 81 | default=django.utils.timezone.now, verbose_name="date joined" 82 | ), 83 | ), 84 | ( 85 | "name", 86 | models.CharField( 87 | blank=True, max_length=255, verbose_name="Name of User" 88 | ), 89 | ), 90 | ( 91 | "groups", 92 | models.ManyToManyField( 93 | blank=True, 94 | help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", 95 | related_name="user_set", 96 | related_query_name="user", 97 | to="auth.Group", 98 | verbose_name="groups", 99 | ), 100 | ), 101 | ( 102 | "user_permissions", 103 | models.ManyToManyField( 104 | blank=True, 105 | help_text="Specific permissions for this user.", 106 | related_name="user_set", 107 | related_query_name="user", 108 | to="auth.Permission", 109 | verbose_name="user permissions", 110 | ), 111 | ), 112 | ], 113 | options={ 114 | "verbose_name_plural": "users", 115 | "verbose_name": "user", 116 | "abstract": False, 117 | }, 118 | managers=[("objects", django.contrib.auth.models.UserManager())], 119 | ) 120 | ] 121 | -------------------------------------------------------------------------------- /django_react_paypal/users/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django_react_paypal/d3aa6a16ff0bf08d30ce79204a37d8bb7b806bd5/django_react_paypal/users/migrations/__init__.py -------------------------------------------------------------------------------- /django_react_paypal/users/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import AbstractUser 2 | from django.db.models import CharField 3 | from django.urls import reverse 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | 7 | class User(AbstractUser): 8 | """Default user for Django React PayPal.""" 9 | 10 | #: First and last name do not cover name patterns around the globe 11 | name = CharField(_("Name of User"), blank=True, max_length=255) 12 | first_name = None # type: ignore 13 | last_name = None # type: ignore 14 | 15 | def get_absolute_url(self): 16 | """Get url for user's detail view. 17 | 18 | Returns: 19 | str: URL for user detail. 20 | 21 | """ 22 | return reverse("users:detail", kwargs={"username": self.username}) 23 | -------------------------------------------------------------------------------- /django_react_paypal/users/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django_react_paypal/d3aa6a16ff0bf08d30ce79204a37d8bb7b806bd5/django_react_paypal/users/tests/__init__.py -------------------------------------------------------------------------------- /django_react_paypal/users/tests/factories.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Sequence 2 | 3 | from django.contrib.auth import get_user_model 4 | from factory import Faker, post_generation 5 | from factory.django import DjangoModelFactory 6 | 7 | 8 | class UserFactory(DjangoModelFactory): 9 | 10 | username = Faker("user_name") 11 | email = Faker("email") 12 | name = Faker("name") 13 | 14 | @post_generation 15 | def password(self, create: bool, extracted: Sequence[Any], **kwargs): 16 | password = ( 17 | extracted 18 | if extracted 19 | else Faker( 20 | "password", 21 | length=42, 22 | special_chars=True, 23 | digits=True, 24 | upper_case=True, 25 | lower_case=True, 26 | ).evaluate(None, None, extra={"locale": None}) 27 | ) 28 | self.set_password(password) 29 | 30 | class Meta: 31 | model = get_user_model() 32 | django_get_or_create = ["username"] 33 | -------------------------------------------------------------------------------- /django_react_paypal/users/tests/test_admin.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.urls import reverse 3 | 4 | from django_react_paypal.users.models import User 5 | 6 | pytestmark = pytest.mark.django_db 7 | 8 | 9 | class TestUserAdmin: 10 | def test_changelist(self, admin_client): 11 | url = reverse("admin:users_user_changelist") 12 | response = admin_client.get(url) 13 | assert response.status_code == 200 14 | 15 | def test_search(self, admin_client): 16 | url = reverse("admin:users_user_changelist") 17 | response = admin_client.get(url, data={"q": "test"}) 18 | assert response.status_code == 200 19 | 20 | def test_add(self, admin_client): 21 | url = reverse("admin:users_user_add") 22 | response = admin_client.get(url) 23 | assert response.status_code == 200 24 | 25 | response = admin_client.post( 26 | url, 27 | data={ 28 | "username": "test", 29 | "password1": "My_R@ndom-P@ssw0rd", 30 | "password2": "My_R@ndom-P@ssw0rd", 31 | }, 32 | ) 33 | assert response.status_code == 302 34 | assert User.objects.filter(username="test").exists() 35 | 36 | def test_view_user(self, admin_client): 37 | user = User.objects.get(username="admin") 38 | url = reverse("admin:users_user_change", kwargs={"object_id": user.pk}) 39 | response = admin_client.get(url) 40 | assert response.status_code == 200 41 | -------------------------------------------------------------------------------- /django_react_paypal/users/tests/test_drf_urls.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.urls import resolve, reverse 3 | 4 | from django_react_paypal.users.models import User 5 | 6 | pytestmark = pytest.mark.django_db 7 | 8 | 9 | def test_user_detail(user: User): 10 | assert ( 11 | reverse("api:user-detail", kwargs={"username": user.username}) 12 | == f"/api/users/{user.username}/" 13 | ) 14 | assert resolve(f"/api/users/{user.username}/").view_name == "api:user-detail" 15 | 16 | 17 | def test_user_list(): 18 | assert reverse("api:user-list") == "/api/users/" 19 | assert resolve("/api/users/").view_name == "api:user-list" 20 | 21 | 22 | def test_user_me(): 23 | assert reverse("api:user-me") == "/api/users/me/" 24 | assert resolve("/api/users/me/").view_name == "api:user-me" 25 | -------------------------------------------------------------------------------- /django_react_paypal/users/tests/test_drf_views.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.test import RequestFactory 3 | 4 | from django_react_paypal.users.api.views import UserViewSet 5 | from django_react_paypal.users.models import User 6 | 7 | pytestmark = pytest.mark.django_db 8 | 9 | 10 | class TestUserViewSet: 11 | def test_get_queryset(self, user: User, rf: RequestFactory): 12 | view = UserViewSet() 13 | request = rf.get("/fake-url/") 14 | request.user = user 15 | 16 | view.request = request 17 | 18 | assert user in view.get_queryset() 19 | 20 | def test_me(self, user: User, rf: RequestFactory): 21 | view = UserViewSet() 22 | request = rf.get("/fake-url/") 23 | request.user = user 24 | 25 | view.request = request 26 | 27 | response = view.me(request) 28 | 29 | assert response.data == { 30 | "username": user.username, 31 | "name": user.name, 32 | "url": f"http://testserver/api/users/{user.username}/", 33 | } 34 | -------------------------------------------------------------------------------- /django_react_paypal/users/tests/test_forms.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for all Form Tests. 3 | """ 4 | import pytest 5 | from django.utils.translation import gettext_lazy as _ 6 | 7 | from django_react_paypal.users.forms import UserCreationForm 8 | from django_react_paypal.users.models import User 9 | 10 | pytestmark = pytest.mark.django_db 11 | 12 | 13 | class TestUserCreationForm: 14 | """ 15 | Test class for all tests related to the UserCreationForm 16 | """ 17 | 18 | def test_username_validation_error_msg(self, user: User): 19 | """ 20 | Tests UserCreation Form's unique validator functions correctly by testing: 21 | 1) A new user with an existing username cannot be added. 22 | 2) Only 1 error is raised by the UserCreation Form 23 | 3) The desired error message is raised 24 | """ 25 | 26 | # The user already exists, 27 | # hence cannot be created. 28 | form = UserCreationForm( 29 | { 30 | "username": user.username, 31 | "password1": user.password, 32 | "password2": user.password, 33 | } 34 | ) 35 | 36 | assert not form.is_valid() 37 | assert len(form.errors) == 1 38 | assert "username" in form.errors 39 | assert form.errors["username"][0] == _("This username has already been taken.") 40 | -------------------------------------------------------------------------------- /django_react_paypal/users/tests/test_models.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from django_react_paypal.users.models import User 4 | 5 | pytestmark = pytest.mark.django_db 6 | 7 | 8 | def test_user_get_absolute_url(user: User): 9 | assert user.get_absolute_url() == f"/users/{user.username}/" 10 | -------------------------------------------------------------------------------- /django_react_paypal/users/tests/test_urls.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.urls import resolve, reverse 3 | 4 | from django_react_paypal.users.models import User 5 | 6 | pytestmark = pytest.mark.django_db 7 | 8 | 9 | def test_detail(user: User): 10 | assert ( 11 | reverse("users:detail", kwargs={"username": user.username}) 12 | == f"/users/{user.username}/" 13 | ) 14 | assert resolve(f"/users/{user.username}/").view_name == "users:detail" 15 | 16 | 17 | def test_update(): 18 | assert reverse("users:update") == "/users/~update/" 19 | assert resolve("/users/~update/").view_name == "users:update" 20 | 21 | 22 | def test_redirect(): 23 | assert reverse("users:redirect") == "/users/~redirect/" 24 | assert resolve("/users/~redirect/").view_name == "users:redirect" 25 | -------------------------------------------------------------------------------- /django_react_paypal/users/tests/test_views.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.conf import settings 3 | from django.contrib import messages 4 | from django.contrib.auth.models import AnonymousUser 5 | from django.contrib.messages.middleware import MessageMiddleware 6 | from django.contrib.sessions.middleware import SessionMiddleware 7 | from django.http import HttpRequest 8 | from django.test import RequestFactory 9 | from django.urls import reverse 10 | 11 | from django_react_paypal.users.forms import UserChangeForm 12 | from django_react_paypal.users.models import User 13 | from django_react_paypal.users.tests.factories import UserFactory 14 | from django_react_paypal.users.views import ( 15 | UserRedirectView, 16 | UserUpdateView, 17 | user_detail_view, 18 | ) 19 | 20 | pytestmark = pytest.mark.django_db 21 | 22 | 23 | class TestUserUpdateView: 24 | """ 25 | TODO: 26 | extracting view initialization code as class-scoped fixture 27 | would be great if only pytest-django supported non-function-scoped 28 | fixture db access -- this is a work-in-progress for now: 29 | https://github.com/pytest-dev/pytest-django/pull/258 30 | """ 31 | 32 | def dummy_get_response(self, request: HttpRequest): 33 | return None 34 | 35 | def test_get_success_url(self, user: User, rf: RequestFactory): 36 | view = UserUpdateView() 37 | request = rf.get("/fake-url/") 38 | request.user = user 39 | 40 | view.request = request 41 | 42 | assert view.get_success_url() == f"/users/{user.username}/" 43 | 44 | def test_get_object(self, user: User, rf: RequestFactory): 45 | view = UserUpdateView() 46 | request = rf.get("/fake-url/") 47 | request.user = user 48 | 49 | view.request = request 50 | 51 | assert view.get_object() == user 52 | 53 | def test_form_valid(self, user: User, rf: RequestFactory): 54 | view = UserUpdateView() 55 | request = rf.get("/fake-url/") 56 | 57 | # Add the session/message middleware to the request 58 | SessionMiddleware(self.dummy_get_response).process_request(request) 59 | MessageMiddleware(self.dummy_get_response).process_request(request) 60 | request.user = user 61 | 62 | view.request = request 63 | 64 | # Initialize the form 65 | form = UserChangeForm() 66 | form.cleaned_data = [] 67 | view.form_valid(form) 68 | 69 | messages_sent = [m.message for m in messages.get_messages(request)] 70 | assert messages_sent == ["Information successfully updated"] 71 | 72 | 73 | class TestUserRedirectView: 74 | def test_get_redirect_url(self, user: User, rf: RequestFactory): 75 | view = UserRedirectView() 76 | request = rf.get("/fake-url") 77 | request.user = user 78 | 79 | view.request = request 80 | 81 | assert view.get_redirect_url() == f"/users/{user.username}/" 82 | 83 | 84 | class TestUserDetailView: 85 | def test_authenticated(self, user: User, rf: RequestFactory): 86 | request = rf.get("/fake-url/") 87 | request.user = UserFactory() 88 | 89 | response = user_detail_view(request, username=user.username) 90 | 91 | assert response.status_code == 200 92 | 93 | def test_not_authenticated(self, user: User, rf: RequestFactory): 94 | request = rf.get("/fake-url/") 95 | request.user = AnonymousUser() 96 | 97 | response = user_detail_view(request, username=user.username) 98 | login_url = reverse(settings.LOGIN_URL) 99 | 100 | assert response.status_code == 302 101 | assert response.url == f"{login_url}?next=/fake-url/" 102 | -------------------------------------------------------------------------------- /django_react_paypal/users/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from django_react_paypal.users.views import ( 4 | user_detail_view, 5 | user_redirect_view, 6 | user_update_view, 7 | ) 8 | 9 | app_name = "users" 10 | urlpatterns = [ 11 | path("~redirect/", view=user_redirect_view, name="redirect"), 12 | path("~update/", view=user_update_view, name="update"), 13 | path("/", view=user_detail_view, name="detail"), 14 | ] 15 | -------------------------------------------------------------------------------- /django_react_paypal/users/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.contrib.auth.mixins import LoginRequiredMixin 3 | from django.contrib.messages.views import SuccessMessageMixin 4 | from django.urls import reverse 5 | from django.utils.translation import gettext_lazy as _ 6 | from django.views.generic import DetailView, RedirectView, UpdateView 7 | 8 | User = get_user_model() 9 | 10 | 11 | class UserDetailView(LoginRequiredMixin, DetailView): 12 | 13 | model = User 14 | slug_field = "username" 15 | slug_url_kwarg = "username" 16 | 17 | 18 | user_detail_view = UserDetailView.as_view() 19 | 20 | 21 | class UserUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): 22 | 23 | model = User 24 | fields = ["name"] 25 | success_message = _("Information successfully updated") 26 | 27 | def get_success_url(self): 28 | return self.request.user.get_absolute_url() # type: ignore [union-attr] 29 | 30 | def get_object(self): 31 | return self.request.user 32 | 33 | 34 | user_update_view = UserUpdateView.as_view() 35 | 36 | 37 | class UserRedirectView(LoginRequiredMixin, RedirectView): 38 | 39 | permanent = False 40 | 41 | def get_redirect_url(self): 42 | return reverse("users:detail", kwargs={"username": self.request.user.username}) 43 | 44 | 45 | user_redirect_view = UserRedirectView.as_view() 46 | -------------------------------------------------------------------------------- /django_react_paypal/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django_react_paypal/d3aa6a16ff0bf08d30ce79204a37d8bb7b806bd5/django_react_paypal/utils/__init__.py -------------------------------------------------------------------------------- /django_react_paypal/utils/context_processors.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | 4 | def settings_context(_request): 5 | """Settings available by default to the templates context.""" 6 | # Note: we intentionally do NOT expose the entire settings 7 | # to prevent accidental leaking of sensitive information 8 | return {"DEBUG": settings.DEBUG} 9 | -------------------------------------------------------------------------------- /django_react_paypal/utils/storages.py: -------------------------------------------------------------------------------- 1 | from storages.backends.s3boto3 import S3Boto3Storage 2 | 3 | 4 | class StaticRootS3Boto3Storage(S3Boto3Storage): 5 | location = "static" 6 | default_acl = "public-read" 7 | 8 | 9 | class MediaRootS3Boto3Storage(S3Boto3Storage): 10 | location = "media" 11 | file_overwrite = False 12 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build -c . 8 | SOURCEDIR = . 9 | BUILDDIR = ./_build 10 | APP = /app 11 | 12 | .PHONY: help livehtml apidocs Makefile 13 | 14 | # Put it first so that "make" without argument is like "make help". 15 | help: 16 | @$(SPHINXBUILD) help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 17 | 18 | # Build, watch and serve docs with live reload 19 | livehtml: 20 | sphinx-autobuild -b html --host 0.0.0.0 --port 7000 --watch $(APP) -c . $(SOURCEDIR) $(BUILDDIR)/html 21 | 22 | # Outputs rst files from django application code 23 | apidocs: 24 | sphinx-apidoc -o $(SOURCEDIR)/api /app 25 | 26 | # Catch-all target: route all unknown targets to Sphinx using the new 27 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 28 | %: Makefile 29 | @$(SPHINXBUILD) -b $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 30 | -------------------------------------------------------------------------------- /docs/__init__.py: -------------------------------------------------------------------------------- 1 | # Included so that Django's startproject comment runs against the docs directory 2 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | 13 | import os 14 | import sys 15 | import django 16 | 17 | if os.getenv("READTHEDOCS", default=False) == "True": 18 | sys.path.insert(0, os.path.abspath("..")) 19 | os.environ["DJANGO_READ_DOT_ENV_FILE"] = "True" 20 | os.environ["USE_DOCKER"] = "no" 21 | else: 22 | sys.path.insert(0, os.path.abspath("/app")) 23 | os.environ["DATABASE_URL"] = "sqlite:///readthedocs.db" 24 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") 25 | django.setup() 26 | 27 | # -- Project information ----------------------------------------------------- 28 | 29 | project = "Django React PayPal" 30 | copyright = """2021, Matthew Freire""" 31 | author = "Matthew Freire" 32 | 33 | 34 | # -- General configuration --------------------------------------------------- 35 | 36 | # Add any Sphinx extension module names here, as strings. They can be 37 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 38 | # ones. 39 | extensions = [ 40 | "sphinx.ext.autodoc", 41 | "sphinx.ext.napoleon", 42 | ] 43 | 44 | # Add any paths that contain templates here, relative to this directory. 45 | # templates_path = ["_templates"] 46 | 47 | # List of patterns, relative to source directory, that match files and 48 | # directories to ignore when looking for source files. 49 | # This pattern also affects html_static_path and html_extra_path. 50 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 51 | 52 | # -- Options for HTML output ------------------------------------------------- 53 | 54 | # The theme to use for HTML and HTML Help pages. See the documentation for 55 | # a list of builtin themes. 56 | # 57 | html_theme = "alabaster" 58 | 59 | # Add any paths that contain custom static files (such as style sheets) here, 60 | # relative to this directory. They are copied after the builtin static files, 61 | # so a file named "default.css" will overwrite the builtin "default.css". 62 | # html_static_path = ["_static"] 63 | -------------------------------------------------------------------------------- /docs/howto.rst: -------------------------------------------------------------------------------- 1 | How To - Project Documentation 2 | ====================================================================== 3 | 4 | Get Started 5 | ---------------------------------------------------------------------- 6 | 7 | Documentation can be written as rst files in `django_react_paypal/docs`. 8 | 9 | 10 | To build and serve docs, use the commands:: 11 | 12 | docker-compose -f local.yml up docs 13 | 14 | 15 | 16 | Changes to files in `docs/_source` will be picked up and reloaded automatically. 17 | 18 | `Sphinx `_ is the tool used to build documentation. 19 | 20 | Docstrings to Documentation 21 | ---------------------------------------------------------------------- 22 | 23 | The sphinx extension `apidoc `_ is used to automatically document code using signatures and docstrings. 24 | 25 | Numpy or Google style docstrings will be picked up from project files and availble for documentation. See the `Napoleon `_ extension for details. 26 | 27 | For an in-use example, see the `page source <_sources/users.rst.txt>`_ for :ref:`users`. 28 | 29 | To compile all docstrings automatically into documentation source files, use the command: 30 | :: 31 | 32 | make apidocs 33 | 34 | 35 | This can be done in the docker container: 36 | :: 37 | 38 | docker run --rm docs make apidocs 39 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Django React PayPal documentation master file, created by 2 | sphinx-quickstart. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Django React PayPal's documentation! 7 | ====================================================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | howto 14 | pycharm/configuration 15 | users 16 | 17 | 18 | 19 | Indices and tables 20 | ================== 21 | 22 | * :ref:`genindex` 23 | * :ref:`modindex` 24 | * :ref:`search` 25 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | 8 | if "%SPHINXBUILD%" == "" ( 9 | set SPHINXBUILD=sphinx-build -c . 10 | ) 11 | set SOURCEDIR=_source 12 | set BUILDDIR=_build 13 | set APP=..\django_react_paypal 14 | 15 | if "%1" == "" goto help 16 | 17 | %SPHINXBUILD% >NUL 2>NUL 18 | if errorlevel 9009 ( 19 | echo. 20 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 21 | echo.installed, then set the SPHINXBUILD environment variable to point 22 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 23 | echo.may add the Sphinx directory to PATH. 24 | echo. 25 | echo.Install sphinx-autobuild for live serving. 26 | echo.If you don't have Sphinx installed, grab it from 27 | echo.http://sphinx-doc.org/ 28 | exit /b 1 29 | ) 30 | 31 | %SPHINXBUILD% -b %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 32 | goto end 33 | 34 | :livehtml 35 | sphinx-autobuild -b html --open-browser -p 7000 --watch %APP% -c . %SOURCEDIR% %BUILDDIR%/html 36 | GOTO :EOF 37 | 38 | :apidocs 39 | sphinx-apidoc -o %SOURCEDIR%/api %APP% 40 | GOTO :EOF 41 | 42 | :help 43 | %SPHINXBUILD% -b help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 44 | 45 | :end 46 | popd 47 | -------------------------------------------------------------------------------- /docs/users.rst: -------------------------------------------------------------------------------- 1 | .. _users: 2 | 3 | Users 4 | ====================================================================== 5 | 6 | Starting a new project, it’s highly recommended to set up a custom user model, 7 | even if the default User model is sufficient for you. 8 | 9 | This model behaves identically to the default user model, 10 | but you’ll be able to customize it in the future if the need arises. 11 | 12 | .. automodule:: django_react_paypal.users.models 13 | :members: 14 | :noindex: 15 | 16 | -------------------------------------------------------------------------------- /local.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | volumes: 4 | local_postgres_data: {} 5 | local_postgres_data_backups: {} 6 | 7 | services: 8 | django: 9 | build: 10 | context: . 11 | dockerfile: ./compose/local/django/Dockerfile 12 | image: django_react_paypal_local_django 13 | container_name: django_react_paypal_django 14 | depends_on: 15 | - postgres 16 | volumes: 17 | - .:/app:z 18 | env_file: 19 | - ./.envs/.local/.django 20 | - ./.envs/.local/.postgres 21 | ports: 22 | - "8000:8000" 23 | command: /start 24 | 25 | react: 26 | build: 27 | context: ./django_react_paypal/frontend 28 | dockerfile: ./compose/local/Dockerfile 29 | target: development 30 | image: django_react_paypal_local_react 31 | container_name: django_react_paypal_react 32 | volumes: 33 | - ./django_react_paypal/frontend/src:/app/src 34 | ports: 35 | - 3000:3000 36 | 37 | postgres: 38 | build: 39 | context: . 40 | dockerfile: ./compose/production/postgres/Dockerfile 41 | image: django_react_paypal_production_postgres 42 | container_name: django_react_paypal_postgres 43 | volumes: 44 | - local_postgres_data:/var/lib/postgresql/data:Z 45 | - local_postgres_data_backups:/backups:z 46 | env_file: 47 | - ./.envs/.local/.postgres 48 | 49 | docs: 50 | image: django_react_paypal_local_docs 51 | container_name: django_react_paypal_docs 52 | build: 53 | context: . 54 | dockerfile: ./compose/local/docs/Dockerfile 55 | env_file: 56 | - ./.envs/.local/.django 57 | volumes: 58 | - ./docs:/docs:z 59 | - ./config:/app/config:z 60 | - ./django_react_paypal:/app/django_react_paypal:z 61 | ports: 62 | - "7000:7000" 63 | command: /start-docs 64 | -------------------------------------------------------------------------------- /locale/README.rst: -------------------------------------------------------------------------------- 1 | Translations 2 | ============ 3 | 4 | Translations will be placed in this folder when running:: 5 | 6 | python manage.py makemessages 7 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | from pathlib import Path 5 | 6 | if __name__ == "__main__": 7 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") 8 | 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError: 12 | # The above import may fail for some other reason. Ensure that the 13 | # issue is really that Django is missing to avoid masking other 14 | # exceptions on Python 2. 15 | try: 16 | import django # noqa 17 | except ImportError: 18 | raise ImportError( 19 | "Couldn't import Django. Are you sure it's installed and " 20 | "available on your PYTHONPATH environment variable? Did you " 21 | "forget to activate a virtual environment?" 22 | ) 23 | 24 | raise 25 | 26 | # This allows easy placement of apps within the interior 27 | # django_react_paypal directory. 28 | current_path = Path(__file__).parent.resolve() 29 | sys.path.append(str(current_path / "django_react_paypal")) 30 | 31 | execute_from_command_line(sys.argv) 32 | -------------------------------------------------------------------------------- /merge_production_dotenvs_in_dotenv.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | from typing import Sequence 4 | 5 | import pytest 6 | 7 | ROOT_DIR_PATH = Path(__file__).parent.resolve() 8 | PRODUCTION_DOTENVS_DIR_PATH = ROOT_DIR_PATH / ".envs" / ".production" 9 | PRODUCTION_DOTENV_FILE_PATHS = [ 10 | PRODUCTION_DOTENVS_DIR_PATH / ".django", 11 | PRODUCTION_DOTENVS_DIR_PATH / ".postgres", 12 | ] 13 | DOTENV_FILE_PATH = ROOT_DIR_PATH / ".env" 14 | 15 | 16 | def merge( 17 | output_file_path: str, merged_file_paths: Sequence[str], append_linesep: bool = True 18 | ) -> None: 19 | with open(output_file_path, "w") as output_file: 20 | for merged_file_path in merged_file_paths: 21 | with open(merged_file_path, "r") as merged_file: 22 | merged_file_content = merged_file.read() 23 | output_file.write(merged_file_content) 24 | if append_linesep: 25 | output_file.write(os.linesep) 26 | 27 | 28 | def main(): 29 | merge(DOTENV_FILE_PATH, PRODUCTION_DOTENV_FILE_PATHS) 30 | 31 | 32 | @pytest.mark.parametrize("merged_file_count", range(3)) 33 | @pytest.mark.parametrize("append_linesep", [True, False]) 34 | def test_merge(tmpdir_factory, merged_file_count: int, append_linesep: bool): 35 | tmp_dir_path = Path(str(tmpdir_factory.getbasetemp())) 36 | 37 | output_file_path = tmp_dir_path / ".env" 38 | 39 | expected_output_file_content = "" 40 | merged_file_paths = [] 41 | for i in range(merged_file_count): 42 | merged_file_ord = i + 1 43 | 44 | merged_filename = ".service{}".format(merged_file_ord) 45 | merged_file_path = tmp_dir_path / merged_filename 46 | 47 | merged_file_content = merged_filename * merged_file_ord 48 | 49 | with open(merged_file_path, "w+") as file: 50 | file.write(merged_file_content) 51 | 52 | expected_output_file_content += merged_file_content 53 | if append_linesep: 54 | expected_output_file_content += os.linesep 55 | 56 | merged_file_paths.append(merged_file_path) 57 | 58 | merge(output_file_path, merged_file_paths, append_linesep) 59 | 60 | with open(output_file_path, "r") as output_file: 61 | actual_output_file_content = output_file.read() 62 | 63 | assert actual_output_file_content == expected_output_file_content 64 | 65 | 66 | if __name__ == "__main__": 67 | main() 68 | -------------------------------------------------------------------------------- /production.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | volumes: 4 | production_postgres_data: {} 5 | production_postgres_data_backups: {} 6 | production_traefik: {} 7 | 8 | services: 9 | django: 10 | build: 11 | context: . 12 | dockerfile: ./compose/production/django/Dockerfile 13 | image: django_react_paypal_production_django 14 | depends_on: 15 | - postgres 16 | - redis 17 | env_file: 18 | - ./.envs/.production/.django 19 | - ./.envs/.production/.postgres 20 | command: /start 21 | 22 | postgres: 23 | build: 24 | context: . 25 | dockerfile: ./compose/production/postgres/Dockerfile 26 | image: django_react_paypal_production_postgres 27 | volumes: 28 | - production_postgres_data:/var/lib/postgresql/data:Z 29 | - production_postgres_data_backups:/backups:z 30 | env_file: 31 | - ./.envs/.production/.postgres 32 | 33 | traefik: 34 | build: 35 | context: . 36 | dockerfile: ./compose/production/traefik/Dockerfile 37 | image: django_react_paypal_production_traefik 38 | depends_on: 39 | - django 40 | volumes: 41 | - production_traefik:/etc/traefik/acme:z 42 | ports: 43 | - "0.0.0.0:80:80" 44 | - "0.0.0.0:443:443" 45 | 46 | redis: 47 | image: redis:5.0 48 | 49 | awscli: 50 | build: 51 | context: . 52 | dockerfile: ./compose/production/aws/Dockerfile 53 | env_file: 54 | - ./.envs/.production/.django 55 | volumes: 56 | - production_postgres_data_backups:/backups:z 57 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --ds=config.settings.test --reuse-db 3 | python_files = tests.py test_*.py 4 | -------------------------------------------------------------------------------- /requirements/base.txt: -------------------------------------------------------------------------------- 1 | pytz==2021.1 # https://github.com/stub42/pytz 2 | python-slugify==5.0.2 # https://github.com/un33k/python-slugify 3 | Pillow==8.3.1 # https://github.com/python-pillow/Pillow 4 | argon2-cffi==20.1.0 # https://github.com/hynek/argon2_cffi 5 | redis==3.5.3 # https://github.com/andymccurdy/redis-py 6 | hiredis==2.0.0 # https://github.com/redis/hiredis-py 7 | paypalrestsdk==1.13.1 # https://github.com/paypal/PayPal-Python-SDK 8 | 9 | # Django 10 | # ------------------------------------------------------------------------------ 11 | django==3.1.13 # pyup: < 3.2 # https://www.djangoproject.com/ 12 | django-environ==0.4.5 # https://github.com/joke2k/django-environ 13 | django-model-utils==4.1.1 # https://github.com/jazzband/django-model-utils 14 | django-allauth==0.45.0 # https://github.com/pennersr/django-allauth 15 | django-crispy-forms==1.12.0 # https://github.com/django-crispy-forms/django-crispy-forms 16 | django-redis==5.0.0 # https://github.com/jazzband/django-redis 17 | # Django REST Framework 18 | djangorestframework==3.12.4 # https://github.com/encode/django-rest-framework 19 | django-cors-headers==3.7.0 # https://github.com/adamchainz/django-cors-headers 20 | -------------------------------------------------------------------------------- /requirements/local.txt: -------------------------------------------------------------------------------- 1 | -r base.txt 2 | 3 | Werkzeug==1.0.1 # https://github.com/pallets/werkzeug 4 | ipdb==0.13.9 # https://github.com/gotcha/ipdb 5 | psycopg2==2.9.1 # https://github.com/psycopg/psycopg2 6 | 7 | # Testing 8 | # ------------------------------------------------------------------------------ 9 | mypy==0.910 # https://github.com/python/mypy 10 | django-stubs==1.8.0 # https://github.com/typeddjango/django-stubs 11 | pytest==6.2.4 # https://github.com/pytest-dev/pytest 12 | pytest-sugar==0.9.4 # https://github.com/Frozenball/pytest-sugar 13 | 14 | # Documentation 15 | # ------------------------------------------------------------------------------ 16 | sphinx==4.1.2 # https://github.com/sphinx-doc/sphinx 17 | sphinx-autobuild==2021.3.14 # https://github.com/GaretJax/sphinx-autobuild 18 | 19 | # Code quality 20 | # ------------------------------------------------------------------------------ 21 | flake8==3.9.2 # https://github.com/PyCQA/flake8 22 | flake8-isort==4.0.0 # https://github.com/gforcada/flake8-isort 23 | coverage==5.5 # https://github.com/nedbat/coveragepy 24 | black==21.7b0 # https://github.com/psf/black 25 | pylint-django==2.4.4 # https://github.com/PyCQA/pylint-django 26 | pre-commit==2.13.0 # https://github.com/pre-commit/pre-commit 27 | 28 | # Django 29 | # ------------------------------------------------------------------------------ 30 | factory-boy==3.2.0 # https://github.com/FactoryBoy/factory_boy 31 | 32 | django-debug-toolbar==3.2.1 # https://github.com/jazzband/django-debug-toolbar 33 | django-extensions==3.1.3 # https://github.com/django-extensions/django-extensions 34 | django-coverage-plugin==2.0.0 # https://github.com/nedbat/django_coverage_plugin 35 | pytest-django==4.4.0 # https://github.com/pytest-dev/pytest-django 36 | -------------------------------------------------------------------------------- /requirements/production.txt: -------------------------------------------------------------------------------- 1 | # PRECAUTION: avoid production dependencies that aren't in development 2 | 3 | -r base.txt 4 | 5 | gunicorn==20.1.0 # https://github.com/benoitc/gunicorn 6 | psycopg2==2.9.1 # https://github.com/psycopg/psycopg2 7 | Collectfast==2.2.0 # https://github.com/antonagestam/collectfast 8 | 9 | # Django 10 | # ------------------------------------------------------------------------------ 11 | django-storages[boto3]==1.11.1 # https://github.com/jschneier/django-storages 12 | django-anymail[mailgun]==8.4 # https://github.com/anymail/django-anymail 13 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv 4 | 5 | [pycodestyle] 6 | max-line-length = 120 7 | exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv 8 | 9 | [mypy] 10 | python_version = 3.9 11 | check_untyped_defs = True 12 | ignore_missing_imports = True 13 | warn_unused_ignores = True 14 | warn_redundant_casts = True 15 | warn_unused_configs = True 16 | plugins = mypy_django_plugin.main 17 | 18 | [mypy.plugins.django-stubs] 19 | django_settings_module = config.settings.test 20 | 21 | [mypy-*.migrations.*] 22 | # Django migrations should not produce any errors: 23 | ignore_errors = True 24 | 25 | [coverage:run] 26 | include = django_react_paypal/* 27 | omit = *migrations*, *tests* 28 | plugins = 29 | django_coverage_plugin 30 | --------------------------------------------------------------------------------