├── .flake8 ├── .github └── workflows │ └── docs.yml ├── .gitignore ├── .isort.cfg ├── LICENSE.md ├── Makefile ├── README.md ├── djangorocket ├── __init__.py ├── __main__.py └── cli.py ├── docs ├── Makefile ├── changelog.rst ├── conf.py ├── development.rst ├── index.rst └── initial-project-structure.rst ├── hooks └── post_gen_project.py ├── pyproject.toml ├── requirements.txt ├── requirements ├── requirements-docs.txt ├── requirements-linting.txt └── requirements.txt ├── setup.py └── templates └── projects └── base ├── cookiecutter.json └── {{ cookiecutter.project_slug }} ├── .coveragerc ├── .env.example ├── .flake8 ├── .gitignore ├── .isort.cfg ├── Makefile ├── README.md ├── docker-compose.yml ├── pyproject.toml ├── pytest.ini ├── requirements.txt ├── requirements ├── requirements-dev.txt ├── requirements-docs.txt ├── requirements-linting.txt ├── requirements-testing.txt └── requirements.txt ├── runtime.txt └── src ├── manage.py ├── static ├── CACHE │ └── manifest.json └── img │ └── favicon │ ├── favicon.png │ └── favicon.svg ├── tailwind_theme ├── __init__.py ├── apps.py ├── static_src │ ├── .gitignore │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.js │ ├── src │ │ └── styles.css │ └── tailwind.config.js └── templates │ └── base.html ├── templates ├── base.html ├── base_settings.html ├── components │ ├── app_header.html │ ├── footer.html │ ├── header.html │ └── settings_desktop_sidebar.html └── pages │ └── index.html └── {{ cookiecutter.project_slug }} ├── __init__.py ├── asgi.py ├── auth ├── __init__.py ├── admin.py ├── apps.py ├── factories.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── templates │ └── auth │ │ ├── forms │ │ ├── login_form.html │ │ ├── register_form.html │ │ ├── update_password_form.html │ │ └── update_user_form.html │ │ └── pages │ │ ├── account_settings.html │ │ ├── email_settings.html │ │ ├── login.html │ │ ├── register.html │ │ └── security_settings.html ├── tests │ ├── __init__.py │ └── views │ │ ├── __init__.py │ │ ├── login_view │ │ ├── __init__.py │ │ └── tests.py │ │ ├── logout_view │ │ ├── __init__.py │ │ └── tests.py │ │ └── register_view │ │ ├── __init__.py │ │ └── tests.py ├── urls.py ├── utils.py └── views.py ├── billing ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── templates │ └── billing │ │ ├── forms │ │ └── update_billing_information_form.html │ │ └── pages │ │ └── billing_settings.html ├── urls.py ├── utils.py └── views.py ├── celery.py ├── context_processors.py ├── model_loaders.py ├── permissions.py ├── search ├── __init__.py ├── api_urls.py ├── api_views.py ├── apps.py ├── serializers.py └── utils.py ├── serializers.py ├── settings.py ├── urls.py ├── utils ├── __init__.py ├── apps.py ├── templatetags │ ├── __init__.py │ ├── {{ cookiecutter.project_slug }}_utils_math.py │ └── {{ cookiecutter.project_slug }}_utils_timestamp.py └── timezone │ └── __init__.py ├── views.py └── wsgi.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = 3 | .git, 4 | .gitignore, 5 | .mypy_cache, 6 | .pytest_cache, 7 | *.pot, 8 | *.py[co], 9 | __pycache__, 10 | venv, 11 | env, 12 | .env, 13 | docs, 14 | */migrations/*, 15 | manage.py 16 | max-line-length = 88 17 | max-complexity = 18 18 | select = B,C,E,F,W,T4,B9 19 | ignore = E203, E266, E501, W503, F403, F401 -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Publish documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | docs: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-python@v2 14 | - name: Install dependencies 15 | run: | 16 | pip install -r requirements/requirements-docs.txt 17 | - name: Sphinx build 18 | run: | 19 | sphinx-build docs _build 20 | - name: Deploy 21 | uses: peaceiris/actions-gh-pages@v3 22 | if: ${{ github.ref == 'refs/heads/main' }} 23 | with: 24 | publish_branch: gh-pages 25 | github_token: ${{ secrets.GITHUB_TOKEN }} 26 | publish_dir: _build/ 27 | force_orphan: true 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | /dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # PyInstrument profiles 132 | pyinstrument_profiles/ 133 | 134 | # MacOS 135 | .DS_Store 136 | 137 | /src/staticfiles/ -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | 3 | profile = black 4 | 5 | skip = 6 | .gitignore, 7 | .env, 8 | env/, 9 | docs/ 10 | templates/ -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright (c) 2023 Ernesto González 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | LIGHT_CYAN=\033[1;36m 4 | NO_COLOR=\033[0m 5 | 6 | .PHONY: docs 7 | 8 | help: 9 | @echo "lint - lint the python code" 10 | @echo "format - format the python code" 11 | @echo "linttemplates - lint the Django HTML code" 12 | @echo "formattemplates - format the Django HTML code" 13 | 14 | # Lint python code 15 | lint: 16 | @echo "${LIGHT_CYAN}Linting code...${NO_COLOR}" 17 | isort . --check-only 18 | black . --check 19 | flake8 . 20 | 21 | # Format python code 22 | format: 23 | @echo "${LIGHT_CYAN}Formatting code...${NO_COLOR}" 24 | isort . 25 | black . 26 | 27 | # Lint templates code 28 | linttemplates: 29 | @echo "${LIGHT_CYAN}Linting Django HTML code...${NO_COLOR}" 30 | djlint "{{ cookiecutter.project_slug }}/src/" --extension=html --lint 31 | 32 | # Format templates code 33 | formattemplates: 34 | @echo "${LIGHT_CYAN}Linting Django HTML code...${NO_COLOR}" 35 | djlint "{{ cookiecutter.project_slug }}/src/" --extension=html --reformat 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # djangorocket 2 | 3 | [![Django version](https://img.shields.io/badge/django-5.0.6-blue)](https://github.com/ErnestoFGonzalez/djangorocket) 4 | [![Latest Release](https://img.shields.io/github/v/release/ErnestoFGonzalez/djangorocket)](https://github.com/ErnestoFGonzalez/djangorocket/releases) 5 | [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/ErnestoFGonzalez/djangorocket/blob/main/LICENSE.md) 6 | 7 | _Django Rocket is a powerful Django SaaS boilerplate designed for indie hackers and SaaS companies that need to quickly launch their paywalled software. It leverages the [Cookiecutter](https://github.com/cookiecutter/cookiecutter) templating engine to generate a project structure with commonly used features such as authentication and billing, saving you time and effort_. 8 | 9 | For detailed information on usage and third-party integrations, please refer to the [full documentation](https://djangorocket.com). 10 | 11 | ## Features 12 | 13 | - Subscriptions 14 | - Stripe payment integration via [stripe-python](https://github.com/stripe/stripe-python) 15 | - Customizable templates with [Tailwind CSS](https://github.com/tailwindlabs/tailwindcss) (powered by [django-tailwind](https://github.com/timonweb/django-tailwind)) 16 | - Custom user model 17 | - Static file serving with [whitenoise](https://github.com/evansd/whitenoise) 18 | 19 | ## Requirements 20 | 21 | Before getting started, make sure to install the following dependencies: 22 | - [cookiecutter](https://github.com/cookiecutter/cookiecutter) 23 | - [django](https://github.com/django/django). 24 | 25 | You can easily install them using [pip](https://github.com/pypa/pip): 26 | 27 | ```bash 28 | $ pip install cookiecutter==2.1.1 django==5.0.6 29 | ``` 30 | 31 | > **_NOTE:_** Although Django Rocket works with other versions of Cookiecutter and Django, we recommend using the versions mentioned above, as they are well-tested. 32 | 33 | ## Usage 34 | 35 | To create a new Django Rocket project, simply run the following command in your terminal: 36 | 37 | ```bash 38 | $ cookiecutter gh:ErnestoFGonzalez/djangorocket --directory="templates/projects/base" 39 | ``` 40 | 41 | or using `djangorocket` as a CLI tool 42 | 43 | ```bash 44 | $ djangorocket init 45 | ``` 46 | 47 | You will be prompted to enter your project name and slug, after which the project structure will be generated. Make sure to fill out the `.env` file with the appropriate values for your project. 48 | 49 | For comprehensive coverage of features and integrations, check out the [full documentation](https://djangorocket.com). 50 | -------------------------------------------------------------------------------- /djangorocket/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ernestofgonzalez/djangorocket/5946100cdf20e588dff73665b122aed009fb1808/djangorocket/__init__.py -------------------------------------------------------------------------------- /djangorocket/__main__.py: -------------------------------------------------------------------------------- 1 | from djangorocket.cli import main 2 | 3 | if __name__ == "__main__": 4 | main(prog_name="djangorocket") 5 | -------------------------------------------------------------------------------- /djangorocket/cli.py: -------------------------------------------------------------------------------- 1 | import click 2 | from cookiecutter.main import cookiecutter 3 | 4 | 5 | @click.group() 6 | def main(): 7 | pass 8 | 9 | 10 | @main.command() 11 | def init(): 12 | """Run the cookiecutter template in the root folder.""" 13 | cookiecutter("./templates/projects/base") 14 | -------------------------------------------------------------------------------- /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 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. _changelog: 2 | 3 | ========= 4 | Changelog 5 | ========= 6 | 7 | .. _v_1_0_0a1: 8 | 9 | 1.0.0a1 (2025-04-05) 10 | --------------------------- 11 | 12 | * Added command line interface with `init` 13 | 14 | .. _v_0_6_0: 15 | 16 | 0.6.0 (2025-01-24) 17 | ------------------ 18 | 19 | * Upgrade to Django 5 (:issue:`47`) 20 | 21 | .. _v_0_5_0: 22 | 23 | 0.5.0 (2025-01-01) 24 | ------------------ 25 | 26 | * Added search with OpenSearch 27 | * Added settings for Mixpanel 28 | * Added error logging with Sentry 29 | 30 | .. _v_0_4_3: 31 | 32 | 0.4.3 (2023-08-11) 33 | ------------------ 34 | 35 | * Fix typo in namespace (:issue:`41`) 36 | 37 | .. _v_0_4_2: 38 | 39 | 0.4.2 (2023-06-07) 40 | ------------------ 41 | 42 | * Fixed the missing Sign In with Google button in the `auth/pages/login.html` template (:issue:`38`) 43 | * Fixed an incorrect assign of the Google account name to `user.email` when creating the account with Google (:issue:`38`) 44 | 45 | .. _v_0_4_1: 46 | 47 | 0.4.1 (2023-06-06) 48 | ------------------ 49 | 50 | * Fixed `createsuperuser` command (:issue:`36`) 51 | 52 | .. _v_0_4_0: 53 | 54 | 0.4.0 (2023-01-27) 55 | ------------------ 56 | 57 | * Added sign in with Google (:issue:`8`) 58 | 59 | .. _v_0_3_0: 60 | 61 | 0.3.0 (2023-01-18) 62 | ------------------ 63 | 64 | * Added a `docker-compose.yml` file to set up Postgres and Redis instances (:issue:`23`) 65 | * Changed index template to add links to documentation and source code (:issue:`23`) 66 | 67 | .. _v_0_2_0: 68 | 69 | 0.2.0 (2023-01-17) 70 | ------------------ 71 | 72 | * Added a pre-populated `.env` file (:issue:`20`) 73 | 74 | .. _v_0_1_0: 75 | 76 | 0.1.0 (2023-01-17) 77 | ------------------ 78 | 79 | * Added billing with Stripe (:issue:`6`) 80 | 81 | * Added authentication with custom user model (:issue:`4`) -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | project = 'Django Rocket' 10 | copyright = '2023-2025, Ernesto F. González' 11 | author = 'Ernesto F. González' 12 | release = '1.0.0a1' 13 | 14 | # -- General configuration --------------------------------------------------- 15 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 16 | 17 | extensions = [ 18 | 'sphinx_copybutton', 19 | 'sphinx_issues' 20 | ] 21 | 22 | templates_path = ['_templates'] 23 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 24 | 25 | 26 | 27 | # -- Options for HTML output ------------------------------------------------- 28 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 29 | 30 | html_theme = 'furo' 31 | html_static_path = ['_static'] 32 | 33 | # -- Linking Github issues -------------------------------------------------- 34 | # https://github.com/sloria/sphinx-issues 35 | 36 | issues_github_path = "ErnestoFGonzalez/djangorocket" 37 | -------------------------------------------------------------------------------- /docs/development.rst: -------------------------------------------------------------------------------- 1 | .. _development: 2 | 3 | ========================= 4 | Development 5 | ========================= 6 | 7 | After you have generated your project, there are a few things missing in your development environment to get started: 8 | 9 | * Install project requirements 10 | * Set up Docker containers (Postgres and Redis) 11 | * Configure OpenSearch DSL 12 | * Run database migrations 13 | * Set up a Stripe project and product 14 | * Set up Sign in with Google 15 | * Install Tailwind dependencies 16 | 17 | We will walk you through each step with what we consider the faster approach. If you know how to do it in another way, fell free to deviate. 18 | 19 | The first step is to install the project requirements. 20 | 21 | Install project requirements 22 | ---------------------------- 23 | 24 | Open your terminal in the project root 25 | 26 | .. code-block:: sh 27 | 28 | pip install -r requirements.txt 29 | 30 | This will install all the requirements to develop, format, lint and write docs for your project. 31 | 32 | Next up is setting up your Docker containers. 33 | 34 | Set up Docker containers 35 | ------------------------ 36 | 37 | We use Docker Compose to set up both Postgres and Redis. If you don't have Compose installed, go over to `Install Compose`_ and then come back. 38 | 39 | .. _Docker Compose: https://docs.docker.com/compose/ 40 | .. _Install Compose: https://docs.docker.com/compose/install/ 41 | 42 | Your initial project ships with a :code:`docker-compose.yml` file in the root. Here's how it looks: 43 | 44 | .. code-block:: yaml 45 | 46 | version: "3.1" 47 | 48 | services: 49 | 50 | postgres: 51 | image: postgres:latest 52 | restart: always 53 | environment: 54 | - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} 55 | - POSTGRES_DB=${POSTGRES_DB} 56 | ports: 57 | - "5432:5432" 58 | 59 | redis: 60 | image: bitnami/redis:latest 61 | restart: always 62 | environment: 63 | - ALLOW_EMPTY_PASSWORD=yes 64 | ports: 65 | - "6379:6379" 66 | 67 | Since Django Rocket also ships with a pre-populated :code:`.env` file, the :code:`POSTGRES_PASSWORD` and :code:`POSTGRES_DB` environment variables have already been set for you. All is left to do is run compose: 68 | 69 | .. code-block:: sh 70 | 71 | docker compose up 72 | 73 | Configure OpenSearch DSL 74 | ------------------------ 75 | 76 | Before running migrations, you need to either configure OpenSearch DSL with proper AWS credentials or remove it from your settings. 77 | 78 | To configure OpenSearch DSL, make sure you have the following environment variables set in your :code:`.env` file: 79 | 80 | * AWS_OPEN_SEARCH_HOST 81 | * AWS_ACCESS_KEY_ID 82 | * AWS_SECRET_ACCESS_KEY 83 | * AWS_OPEN_SEARCH_REGION_NAME 84 | 85 | Your :code:`settings.py` should include this configuration: 86 | 87 | .. code-block:: python 88 | 89 | OPENSEARCH_DSL = { 90 | "default": { 91 | "hosts": AWS_OPEN_SEARCH_HOST, 92 | "http_auth": AWSV4SignerAuth( 93 | boto3.Session( 94 | aws_access_key_id=AWS_ACCESS_KEY_ID, 95 | aws_secret_access_key=AWS_SECRET_ACCESS_KEY, 96 | ).get_credentials(), 97 | AWS_OPEN_SEARCH_REGION_NAME, 98 | "es", 99 | ), 100 | "use_ssl": True, 101 | "verify_certs": True, 102 | "connection_class": RequestsHttpConnection, 103 | "pool_maxsize": 20, 104 | }, 105 | } 106 | 107 | If you don't plan to use OpenSearch, you must: 108 | 109 | 1. Remove the :code:`OPENSEARCH_DSL` setting from your :code:`settings.py` 110 | 2. Remove :code:`'django_opensearch_dsl'` from :code:`INSTALLED_APPS` in :code:`settings.py` 111 | 112 | Run migrations 113 | -------------- 114 | 115 | Once you've properly configured or removed OpenSearch DSL, you can run the project migrations. In your terminal: 116 | 117 | .. code-block:: sh 118 | 119 | python src/manage.py migrate 120 | 121 | Notice we expect the :code:`manage.py` file to be in the :code:`src` directory. 122 | 123 | .. note:: 124 | For a detailed description of the initial project directory, see :doc:`Initial project structure `. 125 | 126 | Set up Stripe 127 | ------------- 128 | 129 | For this step, you will need a `Stripe`_ account. Once you are registered in Stripe, navigate to the `dashboard`_ and click on `Developers`_ and in the left sidebar click `API keys`_. 130 | 131 | .. _Stripe: https://stripe.com/ 132 | .. _dashboard: https://dashboard.stripe.com/dashboard 133 | .. _Developers: https://dashboard.stripe.com/test/developers 134 | .. _API keys: https://dashboard.stripe.com/test/apikeys 135 | 136 | From here, you will create a new secret key. The resulting publishable key and secret key should be stored in your :code:`.env` under the keys :code:`STRIPE_PUBLISHABLE_KEY` and :code:`STRIPE_SECRET_KEY`. 137 | 138 | Now navigate to `Webhooks`_ and add a webhook endpoint. The URL should be :code:`https:///billing/stripe/webhook/`. Make sure to replace :code:`` with your host. 139 | 140 | .. _Webhooks: https://dashboard.stripe.com/test/webhooks 141 | 142 | The final step is to create a product. Navigate to the `Products`_ tab. Click on "Add a product" and make sure you select "Recurring" under "Price". Django Rocket expects your product to be a subscription. 143 | 144 | .. _Products: https://dashboard.stripe.com/test/products?active=true 145 | 146 | Fill all the information for your product and once you are done hit save. Then collect the price id and set it in your :code:`.env` under the key :code:`STRIPE_PRICE_ID` 147 | 148 | Set up Sign in with Google 149 | -------------------------- 150 | 151 | Open the `Google Developer Console`_. If you don't have a developer account sign up for one. 152 | 153 | .. _Google Developer Console: https://console.developers.google.com 154 | 155 | `Create a new project`_ for your website. Once you have your project created navigate to `APIs & Services`_, select the `Credentials`_ tab and create a new OAuth client ID with Web application application type. Assign the resulting client id and secret to :code:`GOOGLE_OAUTH_CLIENT_ID` and :code:`GOOGLE_OAUTH_CLIENT_SECRET` respectively in your :code:`.env` file. 156 | 157 | .. _Create a new project: https://console.cloud.google.com/projectcreate 158 | .. _APIs & Services: https://console.cloud.google.com/apis/dashboard 159 | .. _Credentials: https://console.cloud.google.com/apis/credentials 160 | 161 | Add :code:`http://localhost` and :code:`http://localhost:8000` to the Authorized JavaScript origins and :code:`http://localhost:8000/login/google/` to Authorized redirect URIs and make sure to hit save. 162 | 163 | Install Tailwind dependencies 164 | ----------------------------- 165 | 166 | To Install Tailwind dependencies head over to the terminal 167 | 168 | .. code:: sh 169 | 170 | python src/manage.py tailwind install 171 | 172 | Running the project 173 | ------------------- 174 | 175 | There are two processes you need running while developing. The first one watches your styles and writes to your stylesheets to include relevant Tailwind utilities 176 | 177 | .. code:: sh 178 | 179 | python src/manage.py tailwind start 180 | 181 | The second one is your familiar Django server 182 | 183 | .. code:: sh 184 | 185 | python src/manage.py runserver 186 | 187 | That's it for setting up your development environment. -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Django Rocket 2 | ============= 3 | 4 | **A Django SaaS boilerplate** 5 | 6 | Django Rocket is an almost-ready-to-launch boilerplate framework powered by `Cookiecutter`_. 7 | 8 | .. _Cookiecutter: https://github.com/cookiecutter/cookiecutter 9 | 10 | It was initially built to allow me to launch my SaaS projects without having to copy-paste common code, so expect the design decisions and integrations to be targeted for this purpose. 11 | 12 | Requirements 13 | ------------ 14 | 15 | To get started, you need to install the dependencies 16 | 17 | * `cookiecutter`_ 18 | * `django`_ 19 | 20 | .. _cookiecutter: https://github.com/cookiecutter/cookiecutter 21 | .. _django: https://github.com/django/django 22 | 23 | You can install them via `pip`_ 24 | 25 | .. _pip: https://github.com/pypa/pip 26 | 27 | .. code-block:: sh 28 | 29 | pip install cookiecutter==2.1.1 django==5.0.6 30 | 31 | .. note:: 32 | Django Rocket works with other versions of Cookiecutter and Django, but it lacks extensive test coverage so there may be small errors. For now it's better to stick to the mentioned versions. 33 | 34 | Usage 35 | ----- 36 | 37 | To build your project with cookiecutter 38 | 39 | .. code-block:: sh 40 | 41 | cookiecutter gh:ErnestoFGonzalez/djangorocket --directory="templates/projects/base" 42 | 43 | You'll be prompted to enter some information 44 | 45 | .. code-block:: sh 46 | 47 | project_name [My Project]: 48 | project_slug [my_project]: 49 | 50 | Django Rocket is also available as a CLI tool 51 | 52 | .. code-block:: sh 53 | 54 | djangorocket init 55 | 56 | Output 57 | ------ 58 | 59 | Running this command will create a directory called :code:`my_project` inside the current folder. Inside :code:`my_project`, you'll have the initial project structure: 60 | 61 | :: 62 | 63 | my_project 64 | ├── requirements 65 | │ ├── requiremens-dev.txt 66 | │ ├── requiremens-docs.txt 67 | │ ├── requiremens-linting.txt 68 | │ ├── requiremens-testing.txt 69 | │ └── requirements.txt 70 | ├── src 71 | │ ├── my_project 72 | │ │ ├── auth 73 | │ │ ├── billing 74 | | | ├── search 75 | │ │ ├── utils 76 | │ │ ├── __init__.py 77 | │ │ ├── asgi.py 78 | │ │ ├── celery.py 79 | │ │ ├── context_processors.py 80 | │ │ ├── model_loaders.py 81 | │ │ ├── settings.py 82 | │ │ ├── urls.py 83 | │ │ ├── views.py 84 | │ │ └──wsgi.py 85 | │ ├── static 86 | │ ├── tailwind_theme 87 | │ ├── templates 88 | │ └── manage.py 89 | ├── .env 90 | ├── .env.example 91 | ├── .flake8 92 | ├── .isort.cfg 93 | ├── docker-compose.yml 94 | ├── Makefile 95 | ├── pyproject.toml 96 | ├── pytest.ini 97 | ├── requiremens.txt 98 | └── runtime.txt 99 | 100 | For a deep dive, see :doc:`Initial project structure `. 101 | 102 | Start developing 103 | ---------------- 104 | 105 | In order to get started with your development there are a few things you need to do first: 106 | 107 | * Install project requirements 108 | * Create and connect a Postgres database 109 | * Run database migrations 110 | * Create and connect a Redis instance 111 | * Set up a Stripe project and product 112 | * Set up Sign in with Google 113 | * Install Tailwind dependencies 114 | 115 | Go to :doc:`Development ` for a step-by-step guide. 116 | 117 | License 118 | ------- 119 | 120 | Django Rocket is distributed under the Apache License 2.0. You can `read the full license here`_. 121 | 122 | .. _read the full license here: https://github.com/ErnestoFGonzalez/djangorocket/blob/main/LICENSE.md 123 | 124 | .. toctree:: 125 | :hidden: 126 | 127 | self 128 | initial-project-structure 129 | development 130 | changelog 131 | 132 | 133 | -------------------------------------------------------------------------------- /docs/initial-project-structure.rst: -------------------------------------------------------------------------------- 1 | .. _initial-project-structure: 2 | 3 | ========================= 4 | Initial project structure 5 | ========================= 6 | 7 | After you generate your project, the initial project structure is 8 | 9 | :: 10 | 11 | [project_slug] 12 | ├── requirements 13 | │ ├── requiremens-dev.txt 14 | │ ├── requiremens-docs.txt 15 | │ ├── requiremens-linting.txt 16 | │ ├── requiremens-testing.txt 17 | │ └── requirements.txt 18 | ├── src 19 | │ ├── [project_slug] 20 | │ │ ├── auth 21 | │ │ ├── billing 22 | | | ├── search 23 | │ │ ├── utils 24 | │ │ ├── __init__.py 25 | │ │ ├── asgi.py 26 | │ │ ├── celery.py 27 | │ │ ├── context_processors.py 28 | │ │ ├── model_loaders.py 29 | │ │ ├── settings.py 30 | │ │ ├── urls.py 31 | │ │ ├── views.py 32 | │ │ └──wsgi.py 33 | │ ├── static 34 | │ ├── tailwind_theme 35 | │ ├── templates 36 | │ └── manage.py 37 | ├── .env 38 | ├── .env.example 39 | ├── .flake8 40 | ├── .isort.cfg 41 | ├── docker-compose.yml 42 | ├── Makefile 43 | ├── pyproject.toml 44 | ├── pytest.ini 45 | ├── requiremens.txt 46 | └── runtime.txt -------------------------------------------------------------------------------- /hooks/post_gen_project.py: -------------------------------------------------------------------------------- 1 | from uuid import uuid4 2 | 3 | from django.core.management.utils import get_random_secret_key 4 | 5 | 6 | def main(): 7 | postgres_password = uuid4().hex 8 | postgres_db = uuid4().hex 9 | 10 | with open(".env", "w") as file: 11 | file.writelines( 12 | s + "\n" 13 | for s in [ 14 | "# Django", 15 | 'SECRET_KEY="{0}"'.format(get_random_secret_key()), 16 | "DEBUG=True", 17 | "SECURE_SSL_REDIRECT=False", 18 | 'ALLOWED_HOSTS=["*"]', 19 | "CORS_ORIGIN_ALLOW_ALL=True", 20 | "CORS_ORIGIN_WHITELIST=[]", 21 | 'INTERNAL_IPS=["127.0.0.1"]', 22 | "", 23 | "# Databases", 24 | "DATABASE_URL=postgresql://postgres:{0}@localhost:5432/{1}".format( 25 | postgres_password, postgres_db 26 | ), 27 | "POSTGRES_PASSWORD={0}".format(postgres_password), 28 | "POSTGRES_DB={0}".format(postgres_db), 29 | "", 30 | "# Celery", 31 | 'CELERY_BROKER_URL="redis://localhost/"', 32 | 'CELERY_ACCEPT_CONTENT=["json"]', 33 | "", 34 | "# Google", 35 | "GOOGLE_OAUTH_CLIENT_ID=", 36 | "GOOGLE_OAUTH_CLIENT_SECRET=", 37 | "", 38 | "# Stripe", 39 | "STRIPE_PUBLISHABLE_KEY=", 40 | "STRIPE_SECRET_KEY=", 41 | "STRIPE_WEBHOOK_SECRET=", 42 | "STRIPE_PRICE_ID=", 43 | "", 44 | "# Heroku ", 45 | "PORT=8000", 46 | ] 47 | ) 48 | 49 | 50 | if __name__ == "__main__": 51 | main() 52 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 88 3 | target-version = ['py38'] 4 | include = '\.pyi?$' 5 | exclude = ''' 6 | ( 7 | /( 8 | \.eggs 9 | | \.git 10 | | \.hg 11 | | \.mypy_cache 12 | | \.tox 13 | | \.venv 14 | | _build 15 | | buck-out 16 | | build 17 | | dist 18 | | env 19 | | docs 20 | | templates 21 | )/ 22 | | manage.py 23 | ) 24 | ''' 25 | 26 | [tool.djlint] 27 | profile = "django" 28 | format_attribute_template_tags = true -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -r requirements/requirements-docs.txt 2 | -r requirements/requirements-linting.txt 3 | -r requirements/requirements.txt -------------------------------------------------------------------------------- /requirements/requirements-docs.txt: -------------------------------------------------------------------------------- 1 | sphinx==6.1.3 2 | sphinx-copybutton==0.5.1 3 | sphinx-issues==3.0.1 4 | furo==2022.12.7 -------------------------------------------------------------------------------- /requirements/requirements-linting.txt: -------------------------------------------------------------------------------- 1 | black==22.6.0 2 | flake8==4.0.1 3 | isort==5.10.1 4 | djlint==1.19.7 -------------------------------------------------------------------------------- /requirements/requirements.txt: -------------------------------------------------------------------------------- 1 | click==8.1.8 2 | cookiecutter==2.1.1 3 | django==5.0.6 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import io 3 | import os 4 | 5 | VERSION = "1.0.0a1" 6 | 7 | 8 | def get_long_description(): 9 | with io.open( 10 | os.path.join(os.path.dirname(os.path.abspath(__file__)), "README.md"), 11 | encoding="utf8", 12 | ) as fp: 13 | return fp.read() 14 | 15 | 16 | setup( 17 | name="djangorocket", 18 | description="CLI tool to add applications and UI templates to any Django website.", 19 | long_description=get_long_description(), 20 | long_description_content_type="text/markdown", 21 | author="Ernesto González", 22 | version=VERSION, 23 | license="Apache License, Version 2.0", 24 | packages=find_packages(exclude=["templates"]), 25 | install_requires=[ 26 | "click", 27 | "click-default-group>=1.2.3", 28 | "cookiecutter", 29 | ], 30 | entry_points={ 31 | 'console_scripts': [ 32 | 'djangorocket = djangorocket.cli:main', 33 | ] 34 | }, 35 | url="https://github.com/ernestofgonzalez/djangorocket", 36 | project_urls={ 37 | "Documentation": "https://djangorocket.com/", 38 | "Changelog": "https://djangorocket.com/changelog.html", 39 | "Source code": "https://github.com/ernestofgonzalez/djangorocket", 40 | "Issues": "https://github.com/ernestofgonzalez/djangorocket/issues", 41 | "CI": "https://github.com/ernestofgonzalez/djangorocket/actions", 42 | }, 43 | python_requires=">=3.10", 44 | classifiers=[ 45 | "Intended Audience :: Developers", 46 | "Topic :: Software Development :: Libraries", 47 | "License :: OSI Approved :: Apache Software License", 48 | "Programming Language :: Python :: 3.10", 49 | "Programming Language :: Python :: 3.11", 50 | "Programming Language :: Python :: 3.12", 51 | "Programming Language :: Python :: 3.13", 52 | "Framework :: Django :: 5.0", 53 | "Framework :: Django :: 5.1", 54 | "Framework :: Django :: 5.2", 55 | ], 56 | ) -------------------------------------------------------------------------------- /templates/projects/base/cookiecutter.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_name": "My Project", 3 | "project_slug": "{{ cookiecutter.project_name.lower()|replace(' ', '_')|replace('-', '_')|replace('.', '_')|trim() }}" 4 | } -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit= 3 | venv 4 | env 5 | docs 6 | */migrations/* 7 | */tests/* 8 | */__init__.py 9 | 10 | parallel=True -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/.env.example: -------------------------------------------------------------------------------- 1 | SECRET_KEY= 2 | DEBUG= 3 | SECURE_SSL_REDIRECT= 4 | ALLOWED_HOSTS= 5 | CORS_ORIGIN_ALLOW_ALL= 6 | CORS_ORIGIN_WHITELIST= 7 | INTERNAL_IPS= 8 | 9 | # Databases 10 | DATABASE_URL= 11 | POSTGRES_PASSWORD= 12 | POSTGRES_DB= 13 | 14 | # Celery 15 | CELERY_BROKER_URL= 16 | CELERY_ACCEPT_CONTENT= 17 | 18 | # AWS 19 | AWS_ACCESS_KEY_ID= 20 | AWS_SECRET_ACCESS_KEY= 21 | AWS_OPEN_SEARCH_REGION_NAME= 22 | AWS_OPEN_SEARCH_HOST= 23 | 24 | # Google 25 | GOOGLE_OAUTH_CLIENT_ID= 26 | GOOGLE_OAUTH_CLIENT_SECRET= 27 | GOOGLE_TAG_ID= 28 | 29 | # Heroku 30 | PORT= 31 | 32 | # Stripe 33 | STRIPE_PUBLISHABLE_KEY= 34 | STRIPE_SECRET_KEY= 35 | STRIPE_WEBHOOK_SECRET= 36 | STRIPE_PRICE_ID= 37 | 38 | # Mixpanel 39 | MIXPANEL_API_TOKEN= -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = 3 | .git, 4 | .gitignore, 5 | .mypy_cache, 6 | .pytest_cache, 7 | *.pot, 8 | *.py[co], 9 | __pycache__, 10 | venv, 11 | env, 12 | .env, 13 | docs, 14 | */migrations/*, 15 | src/manage.py 16 | max-line-length = 88 17 | max-complexity = 18 18 | select = B,C,E,F,W,T4,B9 19 | ignore = E203, E266, E501, W503, F403, F401 -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | /dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # PyInstrument profiles 132 | pyinstrument_profiles/ 133 | 134 | # MacOS 135 | .DS_Store 136 | 137 | # Static files 138 | /src/staticfiles/ 139 | 140 | # Keys 141 | *.pem 142 | 143 | # Logs 144 | logs 145 | *.log 146 | npm-debug.log* 147 | yarn-debug.log* 148 | yarn-error.log* 149 | lerna-debug.log* 150 | .pnpm-debug.log* 151 | 152 | # Diagnostic reports (https://nodejs.org/api/report.html) 153 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 154 | 155 | # Runtime data 156 | pids 157 | *.pid 158 | *.seed 159 | *.pid.lock 160 | 161 | # Directory for instrumented libs generated by jscoverage/JSCover 162 | lib-cov 163 | 164 | # Coverage directory used by tools like istanbul 165 | coverage 166 | *.lcov 167 | 168 | # nyc test coverage 169 | .nyc_output 170 | 171 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 172 | .grunt 173 | 174 | # Bower dependency directory (https://bower.io/) 175 | bower_components 176 | 177 | # node-waf configuration 178 | .lock-wscript 179 | 180 | # Compiled binary addons (https://nodejs.org/api/addons.html) 181 | build/Release 182 | 183 | # Dependency directories 184 | node_modules/ 185 | jspm_packages/ 186 | 187 | # Snowpack dependency directory (https://snowpack.dev/) 188 | web_modules/ 189 | 190 | # TypeScript cache 191 | *.tsbuildinfo 192 | 193 | # Optional npm cache directory 194 | .npm 195 | 196 | # Optional eslint cache 197 | .eslintcache 198 | 199 | # Optional stylelint cache 200 | .stylelintcache 201 | 202 | # Microbundle cache 203 | .rpt2_cache/ 204 | .rts2_cache_cjs/ 205 | .rts2_cache_es/ 206 | .rts2_cache_umd/ 207 | 208 | # Optional REPL history 209 | .node_repl_history 210 | 211 | # Output of 'npm pack' 212 | *.tgz 213 | 214 | # Yarn Integrity file 215 | .yarn-integrity 216 | 217 | # dotenv environment variable files 218 | .env 219 | .env.development.local 220 | .env.test.local 221 | .env.production.local 222 | .env.local 223 | 224 | # parcel-bundler cache (https://parceljs.org/) 225 | .cache 226 | .parcel-cache 227 | 228 | # Next.js build output 229 | .next 230 | out 231 | 232 | # Nuxt.js build / generate output 233 | .nuxt 234 | dist 235 | 236 | # Gatsby files 237 | .cache/ 238 | # Comment in the public line in if your project uses Gatsby and not Next.js 239 | # https://nextjs.org/blog/next-9-1#public-directory-support 240 | # public 241 | 242 | # vuepress build output 243 | .vuepress/dist 244 | 245 | # vuepress v2.x temp and cache directory 246 | .temp 247 | .cache 248 | 249 | # Docusaurus cache and generated files 250 | .docusaurus 251 | 252 | # Serverless directories 253 | .serverless/ 254 | 255 | # FuseBox cache 256 | .fusebox/ 257 | 258 | # DynamoDB Local files 259 | .dynamodb/ 260 | 261 | # TernJS port file 262 | .tern-port 263 | 264 | # Stores VSCode versions used for testing VSCode extensions 265 | .vscode-test 266 | 267 | # yarn v2 268 | .yarn/cache 269 | .yarn/unplugged 270 | .yarn/build-state.yml 271 | .yarn/install-state.gz 272 | .pnp.* -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | 3 | profile = black 4 | 5 | skip = 6 | .gitignore, 7 | .env, 8 | env/, 9 | docs/ -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/Makefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | LIGHT_CYAN=\033[1;36m 4 | NO_COLOR=\033[0m 5 | 6 | .PHONY: docs 7 | 8 | help: 9 | @echo "test - run tests quickly with the default Python" 10 | @echo "pytest - run tests with pytest" 11 | @echo "coverage - get code coverage report" 12 | @echo "lint - lint the python code" 13 | @echo "format - format the python code" 14 | @echo "linttemplates - lint the Django HTML code" 15 | @echo "formattemplates - format the Django HTML code" 16 | 17 | # Run Django tests 18 | test: 19 | @echo "${LIGHT_CYAN}Running tests...${NO_COLOR}" 20 | python3 src/manage.py test --parallel 21 | 22 | # Run tests with pytest 23 | pytest: 24 | @echo "${LIGHT_CYAN}Running tests with pytest...${NO_COLOR}" 25 | pytest --durations=1 -n 8 26 | 27 | # Get code coverage report 28 | coverage: 29 | @echo "${LIGHT_CYAN}Running tests and collecting coverage data...${NO_COLOR}" 30 | pytest --cov=. -n 8 31 | coverage combine 32 | @echo "${LIGHT_CYAN}Reporting code coverage data...${NO_COLOR}" 33 | coverage report 34 | @echo "${LIGHT_CYAN}Creating HTML report...${NO_COLOR}" 35 | coverage html 36 | @echo "${LIGHT_CYAN}Creating coverage badge...${NO_COLOR}" 37 | @rm ./coverage.svg 38 | coverage-badge -o coverage.svg 39 | 40 | # Lint python code 41 | lint: 42 | @echo "${LIGHT_CYAN}Linting code...${NO_COLOR}" 43 | isort . --check-only 44 | black . --check 45 | flake8 . 46 | 47 | # Format python code 48 | format: 49 | @echo "${LIGHT_CYAN}Formatting code...${NO_COLOR}" 50 | isort . 51 | black . 52 | 53 | # Lint templates code 54 | linttemplates: 55 | @echo "${LIGHT_CYAN}Linting Django HTML code...${NO_COLOR}" 56 | djlint src/ --extension=html --lint 57 | 58 | # Format templates code 59 | formattemplates: 60 | @echo "${LIGHT_CYAN}Linting Django HTML code...${NO_COLOR}" 61 | djlint src/ --extension=html --reformat -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ernestofgonzalez/djangorocket/5946100cdf20e588dff73665b122aed009fb1808/templates/projects/base/{{ cookiecutter.project_slug }}/README.md -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.1" 2 | 3 | services: 4 | 5 | postgres: 6 | image: postgres:latest 7 | restart: always 8 | environment: 9 | - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} 10 | - POSTGRES_DB=${POSTGRES_DB} 11 | ports: 12 | - "5432:5432" 13 | 14 | redis: 15 | image: bitnami/redis:latest 16 | restart: always 17 | environment: 18 | - ALLOW_EMPTY_PASSWORD=yes 19 | ports: 20 | - "6379:6379" -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 88 3 | target-version = ['py38'] 4 | include = '\.pyi?$' 5 | exclude = ''' 6 | ( 7 | /( 8 | \.eggs 9 | | \.git 10 | | \.hg 11 | | \.mypy_cache 12 | | \.tox 13 | | \.venv 14 | | _build 15 | | buck-out 16 | | build 17 | | dist 18 | | env 19 | | docs 20 | )/ 21 | | src/manage.py 22 | ) 23 | ''' 24 | 25 | [tool.djlint] 26 | profile = "django" 27 | exclude = "pyinstrument_profiles/" -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | pythonpath = . src 3 | DJANGO_SETTINGS_MODULE = src.{{ cookiecutter.project_slug }}.settings 4 | python_files = tests.py test_*.py *_tests.py 5 | addopts = -p no:warnings -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/requirements.txt: -------------------------------------------------------------------------------- 1 | -r requirements/requirements-dev.txt 2 | -r requirements/requirements-docs.txt 3 | -r requirements/requirements-linting.txt 4 | -r requirements/requirements-testing.txt 5 | -r requirements/requirements.txt -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/requirements/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | psycopg2-binary==2.9.3 -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/requirements/requirements-docs.txt: -------------------------------------------------------------------------------- 1 | sphinx==3.5.3 -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/requirements/requirements-linting.txt: -------------------------------------------------------------------------------- 1 | black==22.6.0 2 | flake8==4.0.1 3 | isort==5.10.1 4 | djlint==1.19.7 -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/requirements/requirements-testing.txt: -------------------------------------------------------------------------------- 1 | coverage==6.4.1 2 | coverage-badge==1.1.0 3 | pyinstrument==4.3.0 4 | pytest==7.2.0 5 | pytest-django==4.5.2 6 | pytest-xdist==2.5.0 7 | pytest-cov==3.0.0 8 | selenium==4.6.0 -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/requirements/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3==1.35.71 2 | celery==5.4.0 3 | celery[redis]==5.4.0 4 | click==8.1.3 5 | dj-database-url==0.5.0 6 | django==5.0.6 7 | django-browser-reload==1.6.0 8 | django-celery-beat==2.6.0 9 | django-compressor==4.1 10 | django-cors-headers==3.13.0 11 | django-countries==7.5 12 | django-debug-toolbar 13 | django-hijack==3.5.3 14 | django-json-widget==2.0.1 15 | django-jsonform==2.22.0 16 | django-opensearch-dsl==0.6.2 17 | django-phonenumber-field==7.0.0 18 | django-storages==1.13.1 19 | django-tailwind==3.8.0 20 | djangorestframework==3.14.0 21 | djangorestframework-api-key==3.0.0 22 | factory-boy==3.2.1 23 | google-auth==2.16.0 24 | gunicorn==20.0.4 25 | phonenumbers==8.13.0 26 | psycopg2==2.9.3 27 | python-dotenv==0.15.0 28 | sentry-sdk==2.18.0 29 | serpy==0.3.1 30 | shortuuid==1.0.11 31 | stripe==4.2.0 32 | whitenoise==6.2.0 -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.10.8 -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', '{{ cookiecutter.project_slug }}.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/static/CACHE/manifest.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ernestofgonzalez/djangorocket/5946100cdf20e588dff73665b122aed009fb1808/templates/projects/base/{{ cookiecutter.project_slug }}/src/static/CACHE/manifest.json -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/static/img/favicon/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ernestofgonzalez/djangorocket/5946100cdf20e588dff73665b122aed009fb1808/templates/projects/base/{{ cookiecutter.project_slug }}/src/static/img/favicon/favicon.png -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/static/img/favicon/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/tailwind_theme/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ernestofgonzalez/djangorocket/5946100cdf20e588dff73665b122aed009fb1808/templates/projects/base/{{ cookiecutter.project_slug }}/src/tailwind_theme/__init__.py -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/tailwind_theme/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class Tailwind_themeConfig(AppConfig): 5 | name = "tailwind_theme" 6 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/tailwind_theme/static_src/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/tailwind_theme/static_src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tailwind_theme", 3 | "version": "3.4.0", 4 | "description": "", 5 | "scripts": { 6 | "start": "npm run dev", 7 | "build": "npm run build:clean && npm run build:tailwind", 8 | "build:clean": "rimraf ../static/css/dist", 9 | "build:tailwind": "cross-env NODE_ENV=production tailwindcss --postcss -i ./src/styles.css -o ../static/css/dist/styles.css --minify", 10 | "dev": "cross-env NODE_ENV=development tailwindcss --postcss -i ./src/styles.css -o ../static/css/dist/styles.css -w", 11 | "tailwindcss": "node ./node_modules/tailwindcss/lib/cli.js" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "MIT", 16 | "dependencies": { 17 | "@tailwindcss/aspect-ratio": "^0.4.0", 18 | "@tailwindcss/forms": "^0.5.2", 19 | "@tailwindcss/line-clamp": "^0.4.0", 20 | "@tailwindcss/typography": "^0.5.2", 21 | "cross-env": "^7.0.3", 22 | "postcss": "^8.4.14", 23 | "postcss-import": "^14.1.0", 24 | "postcss-nested": "^5.0.6", 25 | "postcss-simple-vars": "^6.0.3", 26 | "rimraf": "^3.0.2", 27 | "tailwindcss": "^3.1.6" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/tailwind_theme/static_src/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | "postcss-import": {}, 4 | "postcss-simple-vars": {}, 5 | "postcss-nested": {} 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/tailwind_theme/static_src/src/styles.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer components { 6 | .btn { 7 | @apply transition ease-in-out inline-flex items-center justify-center rounded font-bold focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2; 8 | } 9 | 10 | .btn-extra-small { 11 | @apply py-1 px-2 text-xs; 12 | } 13 | 14 | .btn-small { 15 | @apply py-1 px-2 text-xs md:text-sm; 16 | } 17 | 18 | .btn-medium { 19 | @apply py-2 px-4 text-sm md:text-base; 20 | } 21 | 22 | .btn-large { 23 | @apply py-4 px-8 text-lg md:text-xl; 24 | } 25 | 26 | .btn-primary { 27 | @apply border-2 border-blue-800 bg-[#0081fb] text-white hover:text-slate-100 hover:bg-blue-700 active:bg-blue-900 active:text-white focus-visible:outline-blue-700; 28 | } 29 | 30 | .btn-white { 31 | @apply bg-white border-2 border-gray-700 text-gray-700 hover:border-green-800 hover:text-white hover:bg-green-600 active:border-green-800 active:text-white active:bg-green-900 focus-visible:outline-green-700; 32 | } 33 | 34 | .input { 35 | @apply appearance-none rounded border-2 border-gray-800 bg-gray-50 px-3 py-2 text-base text-gray-900 placeholder-gray-400 focus:border-blue-500 focus:bg-white focus:outline-none focus:ring-blue-500; 36 | } 37 | 38 | .label { 39 | @apply text-base font-medium text-gray-900; 40 | } 41 | } -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/tailwind_theme/static_src/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const defaultTheme = require('tailwindcss/defaultTheme') 2 | 3 | module.exports = { 4 | content: [ 5 | '../templates/**/*.html', 6 | '../../templates/**/*.html', 7 | '../../**/templates/**/*.html', 8 | '!../../**/node_modules', 9 | '../../**/*.js', 10 | '../../**/*.py' 11 | ], 12 | theme: { 13 | container: { 14 | padding: { 15 | DEFAULT: '1rem', 16 | md: '4rem', 17 | } 18 | }, 19 | fontSize: { 20 | xs: ['0.75rem', { lineHeight: '1rem' }], 21 | sm: ['0.875rem', { lineHeight: '1.5rem' }], 22 | base: ['1rem', { lineHeight: '1.75rem' }], 23 | lg: ['1.125rem', { lineHeight: '2rem' }], 24 | xl: ['1.25rem', { lineHeight: '2rem' }], 25 | '2xl': ['1.5rem', { lineHeight: '2rem' }], 26 | '3xl': ['2rem', { lineHeight: '2.5rem' }], 27 | '4xl': ['2.5rem', { lineHeight: '3.5rem' }], 28 | '5xl': ['3rem', { lineHeight: '3.5rem' }], 29 | '6xl': ['3.75rem', { lineHeight: '1' }], 30 | '7xl': ['4.5rem', { lineHeight: '1.1' }], 31 | '8xl': ['6rem', { lineHeight: '1' }], 32 | '9xl': ['8rem', { lineHeight: '1' }], 33 | }, 34 | extend: { 35 | borderRadius: { 36 | '4xl': '2rem', 37 | }, 38 | fontFamily: { 39 | sans: ['Inter', ...defaultTheme.fontFamily.sans], 40 | display: ['Lexend', ...defaultTheme.fontFamily.sans], 41 | }, 42 | maxWidth: { 43 | '2xl': '40rem', 44 | }, 45 | }, 46 | }, 47 | plugins: [ 48 | require('@tailwindcss/forms'), 49 | require('@tailwindcss/typography'), 50 | require('@tailwindcss/line-clamp'), 51 | require('@tailwindcss/aspect-ratio'), 52 | ], 53 | } 54 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/tailwind_theme/templates/base.html: -------------------------------------------------------------------------------- 1 | {% raw %} 2 | {% load static tailwind_tags %} 3 | 4 | 5 | 6 | Django Tailwind 7 | 8 | 9 | 10 | {% tailwind_css %} 11 | 12 | 13 |
14 |
15 |

Django + Tailwind = ❤️

16 |
17 |
18 | 19 | 20 | {% endraw %} 21 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/templates/base.html: -------------------------------------------------------------------------------- 1 | {% raw %} 2 | {% load static i18n compress tailwind_tags %} 3 | {% get_current_language as LANGUAGE_CODE %} 4 | 5 | 6 | 7 | 8 | {% block head %}{% endblock %} 9 | {% compress css %} 10 | {% tailwind_css %} 11 | {% endcompress %} 12 | 13 | {% if stripe_publishable_key != None %} 14 | 15 | {% endif %} 16 | 19 | 22 | 23 | 24 | {% block body %}{% endblock %} 25 | 26 | 27 | {% endraw %} 28 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/templates/base_settings.html: -------------------------------------------------------------------------------- 1 | {% raw %} 2 | {% extends "base.html" %} 3 | {% block head %} 4 | Settings - 5 | {% endraw %} 6 | {{ cookiecutter.project_name }} 7 | {% raw %} 8 | 9 | 10 | {% endblock %} 11 | {% block body %} 12 | {% include "components/app_header.html" %} 13 |
14 | {% include "components/settings_desktop_sidebar.html" %} 15 |
16 | {% block main %}{% endblock %} 17 |
18 |
19 | {% include "components/footer.html" %} 20 | {% endblock %} 21 | {% endraw %} 22 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/templates/components/app_header.html: -------------------------------------------------------------------------------- 1 | {% raw %} 2 |
3 |
4 | 66 |
67 |
68 | 85 | {% endraw %} 86 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/templates/components/footer.html: -------------------------------------------------------------------------------- 1 | {% raw %} 2 |
3 |
4 | Te queremos dar la mejor experiencia. 5 |

6 | Cómo tú, somos emprendedores y sabemos lo importante que es tener una herramienta fácil de usar y entender. Así que si tienes preguntas o te hace falta ayuda, entra en contacto y te respondemos en el mismo día. 7 |

8 |
9 |
10 |
11 |

Producto

12 | 22 |
23 |
24 |

Empresa

25 | 39 |
40 |
41 |

Recursos

42 | 56 |
57 |
58 |
59 | {% endraw %} 60 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/templates/components/header.html: -------------------------------------------------------------------------------- 1 | {% raw %} 2 |
3 | 29 |
30 | {% endraw %} 31 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/templates/components/settings_desktop_sidebar.html: -------------------------------------------------------------------------------- 1 | {% raw %} 2 |
3 |
4 |

Settings

5 |
6 | 126 |
127 | {% endraw %} 128 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/templates/pages/index.html: -------------------------------------------------------------------------------- 1 | {% raw %} 2 | {% extends "base.html" %} 3 | {% block head %}{% endblock %} 4 | {% block body %} 5 |
6 |

7 | Your project is live. 8 |

9 |

10 | Your rocket is almost ready to launch! 11 |

12 |
13 |
14 |
15 | Documentation 16 |
17 |
18 | Visit our documentation for instructions on first steps to make your project ready for launch. 20 |
21 |
22 |
23 |
24 | Source code 25 |
26 |
27 | Django Rocket is open source and is open to contributions from the community. Visit the repo. 29 |
30 |
31 |
32 |

33 | We're open to feature requests, so if you have any open an issue. 37 |

38 |
39 | {% endblock %} 40 | {% endraw %} 41 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/__init__.py: -------------------------------------------------------------------------------- 1 | from {{cookiecutter.project_slug}}.celery import app as celery_app 2 | 3 | __all__ = ("celery_app",) 4 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/asgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.core.asgi import get_asgi_application 4 | 5 | os.environ.setdefault( 6 | "DJANGO_SETTINGS_MODULE", "{{ cookiecutter.project_slug }}.settings" 7 | ) 8 | 9 | application = get_asgi_application() 10 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/auth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ernestofgonzalez/djangorocket/5946100cdf20e588dff73665b122aed009fb1808/templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/auth/__init__.py -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/auth/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/auth/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AuthConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "{{ cookiecutter.project_slug }}.auth" 7 | label = "{{ cookiecutter.project_slug }}_auth" 8 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/auth/factories.py: -------------------------------------------------------------------------------- 1 | from factory import Faker 2 | from factory.django import DjangoModelFactory 3 | 4 | 5 | class UserFactory(DjangoModelFactory): 6 | class Meta: 7 | model = "{{ cookiecutter.project_slug }}_auth.User" 8 | 9 | name = Faker("name") 10 | email = Faker("email") 11 | phone_number = Faker("phone_number") 12 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/auth/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.conf import settings 3 | 4 | 5 | class UpdateUserForm(forms.Form): 6 | template_name = "auth/forms/update_user_form.html" 7 | name = forms.CharField( 8 | max_length=settings.AUTH_USER_NAME_MAX_LENGTH, 9 | error_messages={"required": "You need to enter your name."}, 10 | help_text="Your full name", 11 | widget=forms.TextInput(attrs={"placeholder": "Enter your name."}), 12 | ) 13 | 14 | 15 | class UpdatePasswordForm(forms.Form): 16 | template_name = "auth/forms/update_password_form.html" 17 | password = forms.CharField( 18 | label="Current password", 19 | error_messages={ 20 | "required": "You need to enter your current password.", 21 | }, 22 | ) 23 | new_password = forms.CharField( 24 | label="New password", 25 | error_messages={ 26 | "required": "You need to enter a new password.", 27 | }, 28 | ) 29 | new_password_confirm = forms.CharField( 30 | label="Confirm new password", 31 | error_messages={ 32 | "required": "You need to confirm your new password.", 33 | }, 34 | ) 35 | 36 | def clean(self): 37 | cleaned_data = super().clean() 38 | new_password = cleaned_data.get("new_password") 39 | new_password_confirm = cleaned_data.get("new_password_confirm") 40 | 41 | if new_password != new_password_confirm: 42 | raise forms.ValidationError("The new passwords entered don't match.") 43 | 44 | 45 | class LoginForm(forms.Form): 46 | template_name = "auth/forms/login_form.html" 47 | email = forms.EmailField( 48 | label="Email address", 49 | error_messages={"required": "You need to enter your email."}, 50 | widget=forms.EmailInput(attrs={"placeholder": "Enter your email address."}), 51 | ) 52 | password = forms.CharField( 53 | label="Password", 54 | error_messages={"required": "You need to enter your password."}, 55 | widget=forms.TextInput( 56 | attrs={"placeholder": "Enter your password."}, 57 | ), 58 | ) 59 | 60 | 61 | class RegisterForm(forms.Form): 62 | template_name = "auth/forms/register_form.html" 63 | name = forms.CharField( 64 | max_length=settings.AUTH_USER_NAME_MAX_LENGTH, 65 | label="Full name", 66 | error_messages={"required": "You need to enter your name."}, 67 | widget=forms.TextInput(attrs={"placeholder": "Enter your name."}), 68 | ) 69 | email = forms.EmailField( 70 | label="Email address", 71 | error_messages={"required": "You need to enter your email address."}, 72 | widget=forms.EmailInput(attrs={"placeholder": "Enter your email address."}), 73 | ) 74 | password = forms.CharField( 75 | min_length=8, 76 | label="Password", 77 | error_messages={ 78 | "required": "You need to enter a password.", 79 | "min_length": "Your password must have at least 8 characters.", 80 | }, 81 | widget=forms.TextInput( 82 | attrs={"placeholder": "Create a password."}, 83 | ), 84 | ) 85 | 86 | terms = forms.BooleanField( 87 | widget=forms.CheckboxInput(), 88 | error_messages={ 89 | "required": "You need to accept the Terms and Conditions.", 90 | }, 91 | ) 92 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/auth/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.4 on 2023-01-11 22:51 2 | 3 | import django.contrib.auth.models 4 | import django.utils.timezone 5 | import phonenumber_field.modelfields 6 | import {{ cookiecutter.project_slug }}.utils 7 | from django.db import migrations, models 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | initial = True 13 | 14 | dependencies = [ 15 | ("auth", "0012_alter_user_first_name_max_length"), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name="User", 21 | fields=[ 22 | ( 23 | "id", 24 | models.BigAutoField( 25 | auto_created=True, 26 | primary_key=True, 27 | serialize=False, 28 | verbose_name="ID", 29 | ), 30 | ), 31 | ("password", models.CharField(max_length=128, verbose_name="password")), 32 | ( 33 | "last_login", 34 | models.DateTimeField( 35 | blank=True, null=True, verbose_name="last login" 36 | ), 37 | ), 38 | ( 39 | "is_superuser", 40 | models.BooleanField( 41 | default=False, 42 | help_text="Designates that this user has all permissions without explicitly assigning them.", 43 | verbose_name="superuser status", 44 | ), 45 | ), 46 | ( 47 | "is_staff", 48 | models.BooleanField( 49 | default=False, 50 | help_text="Designates whether the user can log into this admin site.", 51 | verbose_name="staff status", 52 | ), 53 | ), 54 | ( 55 | "is_active", 56 | models.BooleanField( 57 | default=True, 58 | help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", 59 | verbose_name="active", 60 | ), 61 | ), 62 | ( 63 | "date_joined", 64 | models.DateTimeField( 65 | default=django.utils.timezone.now, verbose_name="date joined" 66 | ), 67 | ), 68 | ( 69 | "uuid", 70 | models.CharField( 71 | default={{ cookiecutter.project_slug }}.utils.default_uuid, 72 | editable=False, 73 | max_length=255, 74 | unique=True, 75 | ), 76 | ), 77 | ("name", models.CharField(max_length=150)), 78 | ( 79 | "phone_number", 80 | phonenumber_field.modelfields.PhoneNumberField( 81 | max_length=128, null=True, region=None, unique=True 82 | ), 83 | ), 84 | ("email", models.EmailField(max_length=255, unique=True)), 85 | ("google_id", models.CharField(max_length=255, null=True)), 86 | ( 87 | "groups", 88 | models.ManyToManyField( 89 | blank=True, 90 | help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", 91 | related_name="user_set", 92 | related_query_name="user", 93 | to="auth.group", 94 | verbose_name="groups", 95 | ), 96 | ), 97 | ( 98 | "user_permissions", 99 | models.ManyToManyField( 100 | blank=True, 101 | help_text="Specific permissions for this user.", 102 | related_name="user_set", 103 | related_query_name="user", 104 | to="auth.permission", 105 | verbose_name="user permissions", 106 | ), 107 | ), 108 | ], 109 | options={ 110 | "ordering": ["-date_joined"], 111 | }, 112 | managers=[ 113 | ("objects", django.contrib.auth.models.UserManager()), 114 | ], 115 | ), 116 | ] 117 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/auth/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ernestofgonzalez/djangorocket/5946100cdf20e588dff73665b122aed009fb1808/templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/auth/migrations/__init__.py -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/auth/models.py: -------------------------------------------------------------------------------- 1 | import stripe 2 | from django.conf import settings 3 | from django.contrib.auth.models import AbstractUser, BaseUserManager 4 | from django.db import models 5 | from django.utils.functional import cached_property 6 | from phonenumber_field.modelfields import PhoneNumberField 7 | from {{cookiecutter.project_slug}}.utils import default_uuid 8 | 9 | 10 | class UserManager(BaseUserManager): 11 | def create_user(self, email, password=None, name=None, **extra_fields): 12 | if not email: 13 | raise ValueError('Enter an email address') 14 | if not name: 15 | raise ValueError('Enter a name') 16 | email = self.normalize_email(email) 17 | user = self.model(email=email, name=name, **extra_fields) 18 | user.set_password(password) 19 | user.save() 20 | return user 21 | 22 | def create_superuser(self, email, name, password): 23 | user = self.create_user(email, name=name, password=password) 24 | user.is_superuser = True 25 | user.is_staff = True 26 | user.save() 27 | return user 28 | 29 | 30 | class User(AbstractUser): 31 | uuid = models.CharField( 32 | default=default_uuid, editable=False, unique=True, max_length=255 33 | ) 34 | 35 | username = None 36 | first_name = None 37 | last_name = None 38 | 39 | name = models.CharField( 40 | null=False, blank=False, max_length=settings.AUTH_USER_NAME_MAX_LENGTH 41 | ) 42 | 43 | phone_number = PhoneNumberField(unique=True, null=True, blank=False) 44 | 45 | email = models.EmailField(max_length=255, unique=True, null=False, blank=False) 46 | 47 | google_id = models.CharField( 48 | null=True, blank=False, max_length=255, 49 | ) 50 | 51 | objects = UserManager() 52 | 53 | USERNAME_FIELD = "email" 54 | REQUIRED_FIELDS = [ 55 | "name", 56 | ] 57 | 58 | class Meta: 59 | ordering = ["-date_joined"] 60 | 61 | @cached_property 62 | def default_payment_method(self): 63 | stripe.api_key = settings.STRIPE_SECRET_KEY 64 | customer = stripe.Customer.retrieve( 65 | self.stripe_customer.stripe_customer_id, 66 | expand=["invoice_settings.default_payment_method"], 67 | ) 68 | return customer["invoice_settings"]["default_payment_method"] 69 | 70 | @cached_property 71 | def subscription(self): 72 | stripe.api_key = settings.STRIPE_SECRET_KEY 73 | subscription = stripe.Subscription.retrieve( 74 | self.stripe_customer.stripe_subscription_id, 75 | expand=["default_payment_method"], 76 | ) 77 | return subscription 78 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/auth/templates/auth/forms/login_form.html: -------------------------------------------------------------------------------- 1 | {% raw %} 2 | {% for error in form.non_field_errors %} 3 | 6 | {% endfor %} 7 |
8 |
9 | 10 | 18 |
19 | {% if form.errors.email %} 20 |
    21 | {% for error in form.errors.email %}
  • {{ error|escape }}
  • {% endfor %} 22 |
23 | {% endif %} 24 |
25 |
26 |
27 | 28 | 35 |
36 | {% if form.errors.password %} 37 |
    38 | {% for error in form.errors.password %}
  • {{ error|escape }}
  • {% endfor %} 39 |
40 | {% endif %} 41 |
42 | {% endraw %} 43 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/auth/templates/auth/forms/register_form.html: -------------------------------------------------------------------------------- 1 | {% raw %} 2 | {% for error in form.non_field_errors %} 3 | 6 | {% endfor %} 7 |
8 |
9 | 10 | 18 |
19 | {% if form.errors.name %} 20 |
    21 | {% for error in form.errors.name %}
  • {{ error|escape }}
  • {% endfor %} 22 |
23 | {% endif %} 24 |
25 |
26 |
27 | 28 | 36 |
37 | {% if form.errors.email %} 38 |
    39 | {% for error in form.errors.email %}
  • {{ error|escape }}
  • {% endfor %} 40 |
41 | {% endif %} 42 |
43 |
44 |
45 | 46 | 53 |
54 | {% if form.errors.password %} 55 |
    56 | {% for error in form.errors.password %}
  • {{ error|escape }}
  • {% endfor %} 57 |
58 | {% endif %} 59 |
60 |
61 |
62 |
63 | 69 |
70 |
71 | I agree with the Terms and Conditions 72 |
73 |
74 | {% if form.errors.terms %} 75 |
    76 | {% for error in form.errors.terms %}
  • {{ error|escape }}
  • {% endfor %} 77 |
78 | {% endif %} 79 |
80 | {% endraw %} 81 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/auth/templates/auth/forms/update_password_form.html: -------------------------------------------------------------------------------- 1 | {% raw %} 2 | {% for error in form.non_field_errors %} 3 | 6 | {% endfor %} 7 |
8 |
9 | 10 | 16 |
17 | {% if form.errors.password %} 18 |
    19 | {% for error in form.errors.password %}
  • {{ error|escape }}
  • {% endfor %} 20 |
21 | {% endif %} 22 |
23 |
24 |
25 | 26 | 32 |
33 | {% if form.errors.new_password %} 34 |
    35 | {% for error in form.errors.new_password %}
  • {{ error|escape }}
  • {% endfor %} 36 |
37 | {% endif %} 38 |
39 |
40 |
41 | 44 | 50 |
51 | {% if form.errors.new_password_confirm %} 52 |
    53 | {% for error in form.errors.new_password_confirm %}
  • {{ error|escape }}
  • {% endfor %} 54 |
55 | {% endif %} 56 |
57 | {% endraw %} 58 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/auth/templates/auth/forms/update_user_form.html: -------------------------------------------------------------------------------- 1 | {% raw %} 2 | {% for error in form.non_field_errors %} 3 | 6 | {% endfor %} 7 |
8 |
9 | 10 |

{{ form.name.field.help_text }}

11 | 17 |
18 | {% if form.errors.name %} 19 |
    20 | {% for error in form.errors.name %}
  • {{ error|escape }}
  • {% endfor %} 21 |
22 | {% endif %} 23 |
24 | {% endraw %} 25 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/auth/templates/auth/pages/account_settings.html: -------------------------------------------------------------------------------- 1 | {% raw %} 2 | {% extends "base_settings.html" %} 3 | {% block head %} 4 | Account settings - 5 | {% endraw %} 6 | {{ cookiecutter.project_name }} 7 | {% raw %} 8 | 9 | 10 | {% endblock %} 11 | {% block main %} 12 |
13 |
14 |

Account settings

15 |
16 |
21 | {% csrf_token %} 22 |
23 |
{{ form }}
24 |
25 | 28 |
29 |
30 |
31 |
32 | {% endblock %} 33 | {% endraw %} 34 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/auth/templates/auth/pages/email_settings.html: -------------------------------------------------------------------------------- 1 | {% raw %} 2 | {% extends "base_settings.html" %} 3 | {% block head %} 4 | Email settings - 5 | {% endraw %} 6 | {{ cookiecutter.project_name }} 7 | {% raw %} 8 | 9 | 10 | {% endblock %} 11 | {% block main %} 12 |
13 |
14 |

Emails

15 |
16 |
17 |

18 | {{ request.user.email }}Primary 19 |

20 |
    21 |
  • 22 | Visible in emails 23 |
  • 24 |
  • 25 | Receives notifications 26 |
  • 27 |
28 |
29 |
30 | {% endblock %} 31 | {% endraw %} 32 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/auth/templates/auth/pages/login.html: -------------------------------------------------------------------------------- 1 | {% raw %} 2 | {% extends "base.html" %} 3 | {% block head %} 4 | Login - 5 | {% endraw -%} 6 | {{ cookiecutter.project_name }}{%- raw -%} 7 | 8 | {% endblock %} 9 | {% block body %} 10 |
11 |
12 |

Welcome back

13 | 14 |
22 |
23 | 31 | 32 |
33 |
34 | or 35 |
36 |
37 | 38 |

Sign in with your email address

39 |
42 | {% csrf_token %} 43 | {{ form }} 44 |
45 | 48 |
49 |
50 |
51 |

52 | Don't have an account? Sign up 54 |

55 |
56 |
57 |
58 | 59 | {% endblock %} 60 | {% endraw %} 61 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/auth/templates/auth/pages/register.html: -------------------------------------------------------------------------------- 1 | {% raw %} 2 | {% extends "base.html" %} 3 | {% block head %} 4 | Sign up - 5 | {% endraw -%} 6 | {{ cookiecutter.project_name }}{%- raw -%} 7 | 8 | {% endblock %} 9 | {% block body %} 10 |
11 |
12 |

Register to get started.

13 |
21 |
22 | 30 | 31 |
32 |
33 | or 34 |
35 |
36 | 37 |

Sign up with your email address

38 |
41 | {% csrf_token %} 42 | {{ form }} 43 |
44 | 47 |
48 |
49 |
50 |

51 | Already have an account? Log in 53 |

54 |
55 |
56 |
57 | 58 | {% endblock %} 59 | {% endraw %} 60 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/auth/templates/auth/pages/security_settings.html: -------------------------------------------------------------------------------- 1 | {% raw %} 2 | {% extends "base_settings.html" %} 3 | {% block head %} 4 | Security settings - 5 | {% endraw %} 6 | {{ cookiecutter.project_name }} 7 | {% raw %} 8 | 9 | 10 | {% endblock %} 11 | {% block main %} 12 |
13 |
14 |

Change password

15 |
16 |
21 | {% csrf_token %} 22 |
23 |
{{ form }}
24 |
25 | I forgot my password 27 | 30 |
31 |
32 |
33 |
34 | {% endblock %} 35 | {% endraw %} 36 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/auth/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ernestofgonzalez/djangorocket/5946100cdf20e588dff73665b122aed009fb1808/templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/auth/tests/__init__.py -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/auth/tests/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ernestofgonzalez/djangorocket/5946100cdf20e588dff73665b122aed009fb1808/templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/auth/tests/views/__init__.py -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/auth/tests/views/login_view/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ernestofgonzalez/djangorocket/5946100cdf20e588dff73665b122aed009fb1808/templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/auth/tests/views/login_view/__init__.py -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/auth/tests/views/login_view/tests.py: -------------------------------------------------------------------------------- 1 | from http import HTTPStatus 2 | 3 | from django.test import TestCase 4 | from django.urls import include, path, reverse 5 | from {{cookiecutter.project_slug}}.auth.factories import UserFactory 6 | 7 | 8 | class LoginViewTests(TestCase): 9 | urlpatterns = [ 10 | path("", include("{{ cookiecutter.project_slug }}.auth.urls")), 11 | ] 12 | 13 | def test_endpoint(self): 14 | url = reverse("{{ cookiecutter.project_slug }}-auth:login") 15 | self.assertEqual(url, "/login/") 16 | 17 | def test_get_response_status_code(self): 18 | url = reverse("{{ cookiecutter.project_slug }}-auth:login") 19 | response = self.client.get(url) 20 | self.assertEqual(response.status_code, HTTPStatus.OK) 21 | 22 | def test_get_response_context(self): 23 | url = reverse("{{ cookiecutter.project_slug }}-auth:login") 24 | response = self.client.get(url) 25 | 26 | self.assertIn("form", response.context) 27 | 28 | form = response.context["form"] 29 | self.assertIn("email", form.fields) 30 | self.assertIn("password", form.fields) 31 | 32 | email_field = form.fields["email"] 33 | self.assertTrue(email_field.required) 34 | self.assertFalse(email_field.disabled) 35 | 36 | password_field = form.fields["password"] 37 | self.assertTrue(password_field.required) 38 | self.assertFalse(password_field.disabled) 39 | 40 | def test_post_invalid_email_displays_error_message(self): 41 | url = reverse("{{ cookiecutter.project_slug }}-auth:login") 42 | data = {"email": "john@example", "password": "safsdf678hg"} 43 | response = self.client.post(url, data=data, follow=True) 44 | 45 | self.assertEqual(response.status_code, HTTPStatus.OK) 46 | self.assertContains( 47 | response, 48 | "Este correo electrónico es inválido. Asegúrate de que tenga un formato como este: ana@ejemplo.com", 49 | html=True, 50 | ) 51 | 52 | def test_post_missing_password_displays_error_message(self): 53 | url = reverse("{{ cookiecutter.project_slug }}-auth:login") 54 | data = { 55 | "email": "john@example", 56 | } 57 | response = self.client.post(url, data=data, follow=True) 58 | 59 | self.assertEqual(response.status_code, HTTPStatus.OK) 60 | self.assertContains( 61 | response, "Es necesario que indiques tu password.", html=True 62 | ) 63 | 64 | def test_post_success_authenticates_request_user(self): 65 | user = UserFactory(email="john@example.com") 66 | password = "f7s8tsda87fgyfsads7f" 67 | user.set_password(password) 68 | user.save() 69 | 70 | url = reverse("{{ cookiecutter.project_slug }}-auth:login") 71 | data = {"email": user.email, "password": password} 72 | self.client.post(url, data=data, follow=True) 73 | 74 | self.assertTrue(user.is_authenticated) 75 | 76 | def test_post_success_redirects_to_index(self): 77 | user = UserFactory(email="john@example.com") 78 | password = "f7s8tsda87fgyfsads7f" 79 | user.set_password(password) 80 | user.save() 81 | 82 | url = reverse("{{ cookiecutter.project_slug }}-auth:login") 83 | data = {"email": user.email, "password": password} 84 | response = self.client.post(url, data=data, follow=False) 85 | 86 | self.assertRedirects( 87 | response, 88 | reverse("index"), 89 | status_code=HTTPStatus.FOUND, 90 | target_status_code=HTTPStatus.FOUND, 91 | fetch_redirect_response=True, 92 | ) 93 | 94 | def test_put_is_not_allowed(self): 95 | url = reverse("{{ cookiecutter.project_slug }}-auth:login") 96 | response = self.client.put(url, data={}, follow=True) 97 | 98 | self.assertEqual(response.status_code, HTTPStatus.METHOD_NOT_ALLOWED) 99 | 100 | def test_patch_is_not_allowed(self): 101 | url = reverse("{{ cookiecutter.project_slug }}-auth:login") 102 | response = self.client.patch(url, data={}, follow=True) 103 | 104 | self.assertEqual(response.status_code, HTTPStatus.METHOD_NOT_ALLOWED) 105 | 106 | def test_delete_is_not_allowed(self): 107 | url = reverse("{{ cookiecutter.project_slug }}-auth:login") 108 | response = self.client.delete(url, data={}, follow=True) 109 | 110 | self.assertEqual(response.status_code, HTTPStatus.METHOD_NOT_ALLOWED) 111 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/auth/tests/views/logout_view/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ernestofgonzalez/djangorocket/5946100cdf20e588dff73665b122aed009fb1808/templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/auth/tests/views/logout_view/__init__.py -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/auth/tests/views/logout_view/tests.py: -------------------------------------------------------------------------------- 1 | from http import HTTPStatus 2 | 3 | from django.test import TestCase 4 | from django.urls import include, path, reverse 5 | from {{cookiecutter.project_slug}}.auth.factories import UserFactory 6 | 7 | 8 | class LogoutViewTests(TestCase): 9 | urlpatterns = [ 10 | path("", include("{{ cookiecutter.project_slug }}.auth.urls")), 11 | ] 12 | 13 | def test_endpoint(self): 14 | url = reverse("{{ cookiecutter.project_slug }}-auth:logout") 15 | self.assertEqual(url, "/logout/") 16 | 17 | def test_get_response_status_code(self): 18 | user = UserFactory() 19 | self.client.force_login(user) 20 | 21 | url = reverse("{{ cookiecutter.project_slug }}-auth:logout") 22 | response = self.client.get(url) 23 | self.assertEqual(response.status_code, HTTPStatus.METHOD_NOT_ALLOWED) 24 | 25 | def test_post_success_redirects_to_index_path(self): 26 | user = UserFactory() 27 | self.client.force_login(user) 28 | 29 | url = reverse("{{ cookiecutter.project_slug }}-auth:logout") 30 | response = self.client.post(url, follow=True) 31 | 32 | self.assertRedirects( 33 | response, 34 | reverse("index"), 35 | status_code=HTTPStatus.FOUND, 36 | target_status_code=HTTPStatus.OK, 37 | fetch_redirect_response=True, 38 | ) 39 | 40 | def test_post_with_unauthenticated_request_user_redirects_to_login_page(self): 41 | url = reverse("{{ cookiecutter.project_slug }}-auth:logout") 42 | response = self.client.post(url, follow=True) 43 | 44 | self.assertRedirects( 45 | response, 46 | reverse("{{ cookiecutter.project_slug }}-auth:login"), 47 | status_code=HTTPStatus.FOUND, 48 | target_status_code=HTTPStatus.OK, 49 | fetch_redirect_response=True, 50 | ) 51 | 52 | def test_get_is_not_allowed(self): 53 | url = reverse("{{ cookiecutter.project_slug }}-auth:logout") 54 | response = self.client.get(url, data={}, follow=True) 55 | 56 | self.assertEqual(response.status_code, HTTPStatus.METHOD_NOT_ALLOWED) 57 | 58 | def test_put_is_not_allowed(self): 59 | url = reverse("{{ cookiecutter.project_slug }}-auth:logout") 60 | response = self.client.put(url, data={}, follow=True) 61 | 62 | self.assertEqual(response.status_code, HTTPStatus.METHOD_NOT_ALLOWED) 63 | 64 | def test_patch_is_not_allowed(self): 65 | url = reverse("{{ cookiecutter.project_slug }}-auth:logout") 66 | response = self.client.patch(url, data={}, follow=True) 67 | 68 | self.assertEqual(response.status_code, HTTPStatus.METHOD_NOT_ALLOWED) 69 | 70 | def test_delete_is_not_allowed(self): 71 | url = reverse("{{ cookiecutter.project_slug }}-auth:logout") 72 | response = self.client.delete(url, data={}, follow=True) 73 | 74 | self.assertEqual(response.status_code, HTTPStatus.METHOD_NOT_ALLOWED) 75 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/auth/tests/views/register_view/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ernestofgonzalez/djangorocket/5946100cdf20e588dff73665b122aed009fb1808/templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/auth/tests/views/register_view/__init__.py -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/auth/tests/views/register_view/tests.py: -------------------------------------------------------------------------------- 1 | from http import HTTPStatus 2 | 3 | from django.contrib.auth import get_user 4 | from django.test import TestCase 5 | from django.urls import include, path, reverse 6 | 7 | 8 | class RegisterViewTests(TestCase): 9 | urlpatterns = [ 10 | path("", include("{{ cookiecutter.project_slug }}.auth.urls")), 11 | ] 12 | 13 | def test_endpoint(self): 14 | url = reverse("{{ cookiecutter.project_slug }}-auth:register") 15 | self.assertEqual(url, "/register/") 16 | 17 | def test_get_response_status_code(self): 18 | url = reverse("{{ cookiecutter.project_slug }}-auth:register") 19 | response = self.client.get(url) 20 | self.assertEqual(response.status_code, HTTPStatus.OK) 21 | 22 | def test_get_response_context(self): 23 | url = reverse("{{ cookiecutter.project_slug }}-auth:register") 24 | response = self.client.get(url) 25 | 26 | self.assertIn("form", response.context) 27 | 28 | form = response.context["form"] 29 | self.assertIn("name", form.fields) 30 | self.assertIn("email", form.fields) 31 | self.assertIn("password", form.fields) 32 | self.assertIn("terms", form.fields) 33 | 34 | name_field = form.fields["name"] 35 | self.assertTrue(name_field.required) 36 | self.assertFalse(name_field.disabled) 37 | 38 | email_field = form.fields["email"] 39 | self.assertTrue(email_field.required) 40 | self.assertFalse(email_field.disabled) 41 | 42 | password_field = form.fields["password"] 43 | self.assertTrue(password_field.required) 44 | self.assertFalse(password_field.disabled) 45 | 46 | country_field = form.fields["country"] 47 | self.assertTrue(country_field.required) 48 | self.assertFalse(country_field.disabled) 49 | 50 | terms_field = form.fields["terms"] 51 | self.assertTrue(terms_field.required) 52 | self.assertFalse(terms_field.disabled) 53 | 54 | def test_post_invalid_email_displays_error_message(self): 55 | url = reverse("{{ cookiecutter.project_slug }}-auth:register") 56 | data = { 57 | "name": "Marie C", 58 | "email": "marie@example", 59 | "password": "safsdf678hg", 60 | "country": "ES", 61 | "terms": "on", 62 | } 63 | response = self.client.post(url, data=data, follow=True) 64 | 65 | self.assertEqual(response.status_code, HTTPStatus.OK) 66 | self.assertContains( 67 | response, 68 | "Este correo electrónico es inválido. Asegúrate de que tenga un formato como este: ana@ejemplo.com", 69 | html=True, 70 | ) 71 | 72 | def test_post_missing_name_displays_error_message(self): 73 | url = reverse("{{ cookiecutter.project_slug }}-auth:register") 74 | data = { 75 | "email": "john@example.com", 76 | "password": "fdsjgkhdfgs", 77 | "country": "ES", 78 | "terms": "on", 79 | } 80 | response = self.client.post(url, data=data, follow=True) 81 | 82 | self.assertEqual(response.status_code, HTTPStatus.OK) 83 | self.assertContains(response, "Es necesario que indiques tu nombre.", html=True) 84 | 85 | def test_post_missing_password_displays_error_message(self): 86 | url = reverse("{{ cookiecutter.project_slug }}-auth:register") 87 | data = { 88 | "name": "John Smith", 89 | "email": "john@example.com", 90 | "country": "ES", 91 | "terms": "on", 92 | } 93 | response = self.client.post(url, data=data, follow=True) 94 | 95 | self.assertEqual(response.status_code, HTTPStatus.OK) 96 | self.assertContains( 97 | response, "Es necesario que indiques tu password.", html=True 98 | ) 99 | 100 | def test_post_password_with_less_than_8_characters_displays_error_message(self): 101 | url = reverse("{{cookiecutter.project_slug }}-auth:register") 102 | data = { 103 | "name": "Ernesto González", 104 | "email": "ernesto@example.com", 105 | "password": "shd72!s", 106 | "country": "ES", 107 | "terms": "on", 108 | } 109 | response = self.client.post(url, data=data, follow=True) 110 | 111 | self.assertEqual(response.status_code, HTTPStatus.OK) 112 | self.assertContains( 113 | response, "Tu password debe tener al menos 8 caracteres.", html=True 114 | ) 115 | 116 | def test_post_terms_off_displays_error_message(self): 117 | url = reverse("{{ cookiecutter.project_slug }}-auth:register") 118 | data = { 119 | "name": "John Doe", 120 | "email": "john@example.com", 121 | "password": "shd72!s", 122 | "country": "ES", 123 | } 124 | response = self.client.post(url, data=data, follow=True) 125 | 126 | self.assertEqual(response.status_code, HTTPStatus.OK) 127 | self.assertContains( 128 | response, 129 | "Debes aceptar los términos y condiciones para poder empezar.", 130 | html=True, 131 | ) 132 | 133 | def test_post_success_authenticates_request_user(self): 134 | url = reverse("{{ cookiecutter.project_slug }}-auth:register") 135 | data = { 136 | "name": "John Doe", 137 | "email": "john@example.com", 138 | "password": "fdg7dsg8sdfg78", 139 | "country": "ES", 140 | "terms": "on", 141 | } 142 | self.client.post(url, data=data, follow=True) 143 | 144 | self.assertTrue(get_user(self.client).is_authenticated) 145 | 146 | def test_post_success_redirects_to_index(self): 147 | url = reverse("{{ cookiecutter.project_slug }}-auth:register") 148 | data = { 149 | "name": "John Doe", 150 | "email": "john@example.com", 151 | "password": "fdg7dsg8sdfg78", 152 | "country": "ES", 153 | "terms": "on", 154 | } 155 | response = self.client.post(url, data=data, follow=False) 156 | 157 | self.assertRedirects( 158 | response, 159 | reverse("index"), 160 | status_code=HTTPStatus.FOUND, 161 | target_status_code=HTTPStatus.FOUND, 162 | fetch_redirect_response=True, 163 | ) 164 | 165 | def test_put_is_not_allowed(self): 166 | url = reverse("{{ cookiecutter.project_slug }}-auth:register") 167 | response = self.client.put(url, data={}, follow=True) 168 | 169 | self.assertEqual(response.status_code, HTTPStatus.METHOD_NOT_ALLOWED) 170 | 171 | def test_patch_is_not_allowed(self): 172 | url = reverse("{{ cookiecutter.project_slug }}-auth:register") 173 | response = self.client.patch(url, data={}, follow=True) 174 | 175 | self.assertEqual(response.status_code, HTTPStatus.METHOD_NOT_ALLOWED) 176 | 177 | def test_delete_is_not_allowed(self): 178 | url = reverse("{{ cookiecutter.project_slug }}-auth:register") 179 | response = self.client.delete(url, data={}, follow=True) 180 | 181 | self.assertEqual(response.status_code, HTTPStatus.METHOD_NOT_ALLOWED) 182 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/auth/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from {{cookiecutter.project_slug}}.auth import views 3 | 4 | app_name = "{{ cookiecutter.project_slug }}-auth" 5 | 6 | urlpatterns = [ 7 | path("login/", views.login_view, name="login"), 8 | path("login/google/", views.signin_with_google_view, name="signin-with-google"), 9 | path("logout/", views.logout_view, name="logout"), 10 | path("register/", views.register_view, name="register"), 11 | path("settings/account/", views.account_settings_view, name="account-settings"), 12 | path("settings/email/", views.email_settings_view, name="email-settings"), 13 | path("settings/security/", views.security_settings_view, name="security-settings"), 14 | ] 15 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/auth/utils.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from google.auth.transport import requests 3 | from google.oauth2 import id_token 4 | 5 | 6 | def validate_google_id_token(request): 7 | csrf_token_cookie = request.COOKIES.get("g_csrf_token", None) 8 | if not csrf_token_cookie: 9 | raise Exception("No CSRF token in Cookie.") 10 | 11 | csrf_token_body = request.POST.get("g_csrf_token", None) 12 | if not csrf_token_body: 13 | raise Exception("No CSRF token in post body.") 14 | if csrf_token_cookie != csrf_token_body: 15 | raise Exception("Failed to verify double submit cookie.") 16 | 17 | id_token_body = request.POST.get("credential", None) 18 | try: 19 | idinfo = id_token.verify_oauth2_token( 20 | id_token_body, requests.Request(), settings.GOOGLE_OAUTH_CLIENT_ID 21 | ) 22 | except ValueError: 23 | raise Exception("Invalid token.") 24 | 25 | return idinfo 26 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/auth/views.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.auth import authenticate, get_user_model, login, logout 3 | from django.contrib.auth.decorators import login_required 4 | from django.db import IntegrityError 5 | from django.shortcuts import redirect, render 6 | from django.views.decorators.cache import never_cache 7 | from django.views.decorators.csrf import csrf_exempt 8 | from django.views.decorators.http import require_GET, require_http_methods, require_POST 9 | from {{cookiecutter.project_slug}}.auth.forms import ( 10 | LoginForm, 11 | RegisterForm, 12 | UpdatePasswordForm, 13 | UpdateUserForm, 14 | ) 15 | from {{cookiecutter.project_slug}}.auth.utils import validate_google_id_token 16 | from {{cookiecutter.project_slug}}.billing.utils import create_subscription_for_user 17 | 18 | 19 | @never_cache 20 | @require_http_methods(["GET", "POST"]) 21 | @login_required 22 | def account_settings_view(request): 23 | context = { 24 | "current_tab": "account", 25 | } 26 | 27 | form = UpdateUserForm( 28 | request.POST 29 | or { 30 | "name": request.user.name, 31 | }, 32 | initial={ 33 | "name": request.user.name, 34 | }, 35 | ) 36 | context["form"] = form 37 | 38 | if request.method == "POST": 39 | if form.is_valid(): 40 | if form.has_changed(): 41 | for attr in form.changed_data: 42 | setattr(request.user, attr, form.cleaned_data[attr]) 43 | request.user.save() 44 | 45 | return redirect("{{ cookiecutter.project_slug }}-auth:account-settings") 46 | 47 | return render(request, "auth/pages/account_settings.html", context) 48 | 49 | 50 | @never_cache 51 | @require_GET 52 | @login_required 53 | def email_settings_view(request): 54 | context = { 55 | "current_tab": "email", 56 | } 57 | 58 | return render(request, "auth/pages/email_settings.html", context) 59 | 60 | 61 | def security_settings_view(request): 62 | context = { 63 | "current_tab": "security", 64 | } 65 | 66 | form = UpdatePasswordForm(request.POST or None) 67 | context["form"] = form 68 | 69 | if request.method == "POST": 70 | if form.is_valid(): 71 | if request.user.check_password(form.cleaned_data["password"]): 72 | request.user.set_password(form.cleaned_data["new_password"]) 73 | request.user.save() 74 | 75 | return redirect("{{ cookiecutter.project_slug }}-auth:security-settings") 76 | else: 77 | form.add_error(None, "Password incorrecto.") 78 | 79 | return render(request, "auth/pages/security_settings.html", context) 80 | 81 | 82 | @never_cache 83 | @require_http_methods(["GET", "POST"]) 84 | def login_view(request): 85 | context = {} 86 | 87 | form = LoginForm(request.POST or None) 88 | context["form"] = form 89 | 90 | if request.method == "POST": 91 | if form.is_valid(): 92 | if user.has_usable_password(): 93 | user = authenticate( 94 | request, 95 | username=form.cleaned_data.get("email", None), 96 | password=form.cleaned_data.get("password", None), 97 | ) 98 | if user is not None: 99 | login(request, user) 100 | return redirect("index") 101 | else: 102 | form.add_error(None, "Incorrect email address or password.") 103 | else: 104 | form.add_error(None, "You registered using your Google Account. Please use Sign In with Google to sign in.") 105 | 106 | return render(request, "auth/pages/login.html", context) 107 | 108 | 109 | @never_cache 110 | @require_http_methods(["GET", "POST"]) 111 | def register_view(request): 112 | context = {} 113 | context["google_oauth_client_id"] = settings.GOOGLE_OAUTH_CLIENT_ID 114 | 115 | form = RegisterForm(request.POST or None) 116 | context["form"] = form 117 | 118 | if request.method == "POST": 119 | if form.is_valid(): 120 | try: 121 | user = get_user_model().objects.create( 122 | name=form.cleaned_data.get("name", None), 123 | email=form.cleaned_data.get("email", None), 124 | ) 125 | 126 | user.set_password(form.cleaned_data["password"]) 127 | user.save() 128 | 129 | create_subscription_for_user(user, settings.SUBSCRIPTION_TRIAL_PERIOD_DAYS) 130 | 131 | login(request, user) 132 | return redirect("index") 133 | except IntegrityError as e: 134 | form.add_error( 135 | None, "Este correo electrónico ya está asociado a una cuenta." 136 | ) 137 | 138 | return render(request, "auth/pages/register.html", context) 139 | 140 | 141 | @never_cache 142 | @require_POST 143 | @login_required(redirect_field_name=None) 144 | def logout_view(request): 145 | logout(request) 146 | return redirect("index") 147 | 148 | 149 | @require_POST 150 | @csrf_exempt 151 | def signin_with_google_view(request): 152 | next = request.POST.get("next", None) 153 | 154 | try: 155 | idinfo = validate_google_id_token(request) 156 | except Exception as e: 157 | if next is None: 158 | return redirect("{{cookiecutter.project_slug}}-auth:register") 159 | return redirect(next) 160 | 161 | user, created = get_user_model().objects.get_or_create( 162 | email=idinfo["email"], 163 | ) 164 | 165 | if created: 166 | user.name = idinfo["name"] 167 | user.set_unusable_password() 168 | create_subscription_for_user(user, settings.SUBSCRIPTION_TRIAL_PERIOD_DAYS) 169 | 170 | user.google_id = idinfo["sub"] 171 | user.save() 172 | 173 | login(request, user) 174 | return redirect("index") 175 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/billing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ernestofgonzalez/djangorocket/5946100cdf20e588dff73665b122aed009fb1808/templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/billing/__init__.py -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/billing/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from {{cookiecutter.project_slug}}.billing.models import StripeCustomer 3 | 4 | admin.site.register(StripeCustomer) 5 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/billing/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class BillingConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "{{ cookiecutter.project_slug }}.billing" 7 | label = "{{ cookiecutter.project_slug }}_billing" 8 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/billing/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django_countries.fields import CountryField 3 | 4 | 5 | class UpdateBillingInformationForm(forms.Form): 6 | template_name = "billing/forms/update_billing_information_form.html" 7 | name = forms.CharField( 8 | max_length=255, 9 | label="Name", 10 | error_messages={"required": "You need to enter your name."}, 11 | help_text="Your full name", 12 | widget=forms.TextInput(), 13 | required=True, 14 | ) 15 | address_line_1 = forms.CharField( 16 | max_length=255, 17 | label="Address", 18 | help_text="P.O box, company name, c/o", 19 | widget=forms.TextInput(), 20 | required=True, 21 | ) 22 | address_line_2 = forms.CharField( 23 | max_length=255, 24 | label="Address line 2", 25 | help_text="Apartment, suite, unit", 26 | widget=forms.TextInput(), 27 | ) 28 | city = forms.CharField( 29 | max_length=255, label="City", widget=forms.TextInput(), required=True 30 | ) 31 | postal_code = forms.CharField( 32 | max_length=255, 33 | label="Postal/Zip code", 34 | widget=forms.TextInput(), 35 | ) 36 | country = CountryField().formfield( 37 | label="Country", 38 | widget=forms.Select(attrs={"placeholder": "Choose your country"}), 39 | ) 40 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/billing/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.3 on 2022-12-21 15:02 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name="StripeCustomer", 19 | fields=[ 20 | ( 21 | "id", 22 | models.BigAutoField( 23 | auto_created=True, 24 | primary_key=True, 25 | serialize=False, 26 | verbose_name="ID", 27 | ), 28 | ), 29 | ("stripe_customer_id", models.CharField(max_length=255)), 30 | ("stripe_subscription_id", models.CharField(max_length=255)), 31 | ( 32 | "user", 33 | models.OneToOneField( 34 | on_delete=django.db.models.deletion.CASCADE, 35 | related_name="stripe_customer", 36 | to=settings.AUTH_USER_MODEL, 37 | ), 38 | ), 39 | ], 40 | ), 41 | ] 42 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/billing/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ernestofgonzalez/djangorocket/5946100cdf20e588dff73665b122aed009fb1808/templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/billing/migrations/__init__.py -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/billing/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class StripeCustomer(models.Model): 5 | user = models.OneToOneField( 6 | "{{ cookiecutter.project_slug }}_auth.User", 7 | related_name="stripe_customer", 8 | on_delete=models.CASCADE, 9 | ) 10 | stripe_customer_id = models.CharField(max_length=255) 11 | stripe_subscription_id = models.CharField(max_length=255) 12 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/billing/templates/billing/forms/update_billing_information_form.html: -------------------------------------------------------------------------------- 1 | {% raw %} 2 | {% for error in form.non_field_errors %} 3 | 6 | {% endfor %} 7 |
8 |
9 | 12 | 18 |
19 | {% if form.errors.name %} 20 |
    21 | {% for error in form.errors.name %}
  • {{ error|escape }}
  • {% endfor %} 22 |
23 | {% endif %} 24 |
25 |
26 |
27 | 30 |

{{ form.address_line_1.field.help_text }}

31 | 37 |
38 | {% if form.errors.address_line_1 %} 39 |
    40 | {% for error in form.errors.address_line_1 %}
  • {{ error|escape }}
  • {% endfor %} 41 |
42 | {% endif %} 43 |
44 |
45 |
46 | 47 |

{{ form.address_line_2.field.help_text }}

48 | 54 |
55 | {% if form.errors.address_line_2 %} 56 |
    57 | {% for error in form.errors.address_line_2 %}
  • {{ error|escape }}
  • {% endfor %} 58 |
59 | {% endif %} 60 |
61 |
62 |
63 | 66 | 72 |
73 | {% if form.errors.city %} 74 |
    75 | {% for error in form.errors.city %}
  • {{ error|escape }}
  • {% endfor %} 76 |
77 | {% endif %} 78 |
79 |
80 |
81 | 82 | 88 |
89 | {% if form.errors.postal_code %} 90 |
    91 | {% for error in form.errors.postal_code %}
  • {{ error|escape }}
  • {% endfor %} 92 |
93 | {% endif %} 94 |
95 |
96 | 99 |
100 | 110 |
111 | {% if form.errors.country %} 112 |
    113 | {% for error in form.errors.country %}
  • {{ error|escape }}
  • {% endfor %} 114 |
115 | {% endif %} 116 |
117 | {% endraw %} 118 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/billing/templates/billing/pages/billing_settings.html: -------------------------------------------------------------------------------- 1 | {% raw %} 2 | {% extends "base_settings.html" %} 3 | {% load 4 | {% endraw -%} 5 | {{ cookiecutter.project_slug }}{%- raw -%}_utils_math %} 6 | {% load 7 | {% endraw -%} 8 | {{ cookiecutter.project_slug }}{%- raw -%}_utils_timestamp %} 9 | {% block head %} 10 | Billing and plans - 11 | {% endraw -%} 12 | {{ cookiecutter.project_name }}{%- raw -%} 13 | 14 | {% endblock %} 15 | {% block main %} 16 |
17 |
18 |

Billing and plans

19 |
20 |
21 |
22 |

Plans

23 |
24 |
25 |
26 |
27 |

Your current plan

28 |
29 |
30 |

31 | {% if request.user.subscription.status == "trialing" %} 32 | Free trial 33 | {% else %} 34 | {{ request.user.subscription.items.data.0.price.unit_amount|div:100|floatformat:"0" }} € 35 | {% endif %} 36 |

37 |
38 |
39 | {% if request.user.subscription.cancel_at_period_end %} 40 |
42 | {% csrf_token %} 43 | 46 |
47 | {% else %} 48 |
50 | {% csrf_token %} 51 | 54 |
55 | {% endif %} 56 |
57 |
58 |
59 |
60 |

61 | {% if request.user.subscription.status == "trialing" %} 62 | Free trial ends 63 | {% elif request.user.subscription.cancel_at_period_end %} 64 | Current subscription ends 65 | {% else %} 66 | Next payment due 67 | {% endif %} 68 |

69 |
70 |
71 |

72 | {% if request.user.subscription.current_period_end is not None %} 73 | {{ request.user.subscription.current_period_end|timestamp_to_datetime|date }} 74 | {% else %} 75 | - 76 | {% endif %} 77 |

78 | {% if request.user.subscription.cancel_at_period_end %} 79 |

* It won't renew

80 | {% endif %} 81 |
82 |
83 |
84 |
85 |

Payment method

86 |
87 |
88 |

89 | {% if request.user.default_payment_method is not None %} 90 | {{ request.user.default_payment_method.card.brand|title }} ****{{ request.user.default_payment_method.card.last4 }} 91 | {% else %} 92 | - 93 | {% endif %} 94 |

95 |
96 |
97 | 105 |
106 |
107 |
108 |
109 |
110 |
111 |

Billing information

112 |
113 |
116 | {% csrf_token %} 117 |
118 |
{{ form }}
119 |
120 | 121 |
122 |
123 |
124 |
125 |
126 | 140 | {% endblock %} 141 | {% endraw %} 142 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/billing/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from {{cookiecutter.project_slug}}.billing import views 3 | 4 | app_name = "{{ cookiecutter.project_slug }}-billing" 5 | 6 | urlpatterns = [ 7 | path("settings/billing/", views.billing_settings_view, name="billing-settings"), 8 | path( 9 | "billing/subscribe-checkout/create/", 10 | views.create_subscribe_checkout_view, 11 | name="create-subscribe-checkout", 12 | ), 13 | path( 14 | "billing/cancel-subscription/", 15 | views.cancel_subscription_view, 16 | name="cancel-subscription", 17 | ), 18 | path( 19 | "billing/reactivate-subscription/", 20 | views.reactivate_subscription_view, 21 | name="reactivate-subscription", 22 | ), 23 | path("billing/stripe/webhook/", views.stripe_webhook_view, name="stripe-webhook"), 24 | ] 25 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/billing/utils.py: -------------------------------------------------------------------------------- 1 | import stripe 2 | from django.conf import settings 3 | from {{cookiecutter.project_slug}}.model_loaders import get_stripe_customer_model 4 | 5 | 6 | def create_subscription_for_user(user, trial_period_days): 7 | stripe.api_key = settings.STRIPE_SECRET_KEY 8 | 9 | customer = stripe.Customer.create( 10 | email=user.email, 11 | name=user.name, 12 | ) 13 | susbcription = stripe.Subscription.create( 14 | customer=customer.id, 15 | items=[{"price": settings.STRIPE_PRICE_ID}], 16 | trial_period_days=trial_period_days, 17 | ) 18 | 19 | StripeCustomer = get_stripe_customer_model() 20 | StripeCustomer.objects.create( 21 | user=user, 22 | stripe_customer_id=customer.id, 23 | stripe_subscription_id=susbcription.id, 24 | ) -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/billing/views.py: -------------------------------------------------------------------------------- 1 | import stripe 2 | from django.conf import settings 3 | from django.contrib.auth.decorators import login_required 4 | from django.http.response import HttpResponse, JsonResponse 5 | from django.shortcuts import redirect, render 6 | from django.views.decorators.cache import never_cache 7 | from django.views.decorators.csrf import csrf_exempt 8 | from django.views.decorators.http import require_GET, require_http_methods, require_POST 9 | from {{cookiecutter.project_slug}}.billing.forms import UpdateBillingInformationForm 10 | from {{cookiecutter.project_slug}}.model_loaders import get_stripe_customer_model 11 | 12 | 13 | @never_cache 14 | @require_http_methods(["GET", "POST"]) 15 | @login_required 16 | def billing_settings_view(request): 17 | context = { 18 | "current_tab": "billing", 19 | } 20 | 21 | stripe.api_key = settings.STRIPE_SECRET_KEY 22 | subscription = stripe.Subscription.retrieve( 23 | request.user.stripe_customer.stripe_subscription_id 24 | ) 25 | 26 | form = UpdateBillingInformationForm( 27 | request.POST or None, 28 | initial={ 29 | "name": None, 30 | "address_line_1": None, 31 | "address_line_2": None, 32 | "city": None, 33 | "postal_code": None, 34 | "country": None, 35 | }, 36 | ) 37 | context["form"] = form 38 | 39 | if request.method == "POST": 40 | if form.is_valid(): 41 | if form.has_changed(): 42 | pass 43 | 44 | return redirect("{{ cookiecutter.project_slug }}-billing:billing-settings") 45 | 46 | return render(request, "billing/pages/billing_settings.html", context) 47 | 48 | 49 | @csrf_exempt 50 | @require_GET 51 | @login_required 52 | def create_subscribe_checkout_view(request): 53 | domain_url = request.build_absolute_uri("/")[:-1] 54 | success = request.GET.get("success", None) 55 | cancel = request.GET.get("cancel", None) 56 | 57 | stripe.api_key = settings.STRIPE_SECRET_KEY 58 | 59 | try: 60 | checkout_session = stripe.checkout.Session.create( 61 | payment_method_types=["card"], 62 | success_url=domain_url + success + "?subcription_checkout_success=true", 63 | cancel_url=domain_url + cancel, 64 | client_reference_id=request.user.uuid, 65 | customer=request.user.stripe_customer.stripe_customer_id, 66 | customer_update={ 67 | "address": "auto", 68 | }, 69 | mode="setup", 70 | ) 71 | 72 | return JsonResponse({"checkout_session_id": checkout_session["id"]}) 73 | except Exception as e: 74 | return JsonResponse({"error": str(e)}) 75 | 76 | 77 | @require_POST 78 | @login_required 79 | def cancel_subscription_view(request): 80 | success = request.GET.get("success", None) 81 | 82 | stripe.api_key = settings.STRIPE_SECRET_KEY 83 | 84 | stripe.Subscription.modify( 85 | request.user.stripe_customer.stripe_subscription_id, 86 | cancel_at_period_end=True, 87 | ) 88 | 89 | return redirect(success + "?" + "subcription_cancel_success=true") 90 | 91 | 92 | @require_POST 93 | @login_required 94 | def reactivate_subscription_view(request): 95 | """ 96 | Reactivate canceled subscription which hasn't reached 97 | the end of the billing period. 98 | 99 | Note: If the cancellation has already been processed and the 100 | subscription is no longer active, a new subscription is 101 | needed for the customer. For this case, use the `create_subscribe_checkout_view` 102 | endpoint. 103 | """ 104 | success = request.GET.get("success", None) 105 | 106 | stripe.api_key = settings.STRIPE_SECRET_KEY 107 | 108 | stripe_customer = request.user.stripe_customer 109 | 110 | subscription = stripe.Subscription.retrieve(stripe_customer.stripe_subscription_id) 111 | 112 | if subscription.status.canceled: 113 | new_subscription = stripe.Subscription.create( 114 | customer=stripe_customer.stripe_customer_id, 115 | items=[{"price": settings.STRIPE_PRICE_ID}], 116 | automatic_tax={"enabled": True}, 117 | ) 118 | stripe_customer.stripe_subscription_id = new_subscription.id 119 | stripe_customer.save(update_fields=["stripe_subscription_id"]) 120 | else: 121 | stripe.Subscription.modify( 122 | subscription.id, 123 | cancel_at_period_end=False, 124 | proration_behavior="create_prorations", 125 | items=[ 126 | { 127 | "id": subscription["items"]["data"][0].id, 128 | "price": settings.STRIPE_PRICE_ID, 129 | } 130 | ], 131 | ) 132 | 133 | return redirect(success + "?" + "subcription_reactivated_success=true") 134 | 135 | 136 | @require_POST 137 | @login_required 138 | def remove_current_default_payment_method_view(request): 139 | pass 140 | 141 | 142 | @csrf_exempt 143 | def stripe_webhook_view(request): 144 | stripe.api_key = settings.STRIPE_SECRET_KEY 145 | webhook_secret = settings.STRIPE_WEBHOOK_SECRET 146 | payload = request.body 147 | sig_header = request.META["HTTP_STRIPE_SIGNATURE"] 148 | event = None 149 | 150 | try: 151 | event = stripe.Webhook.construct_event(payload, sig_header, webhook_secret) 152 | except ValueError as e: 153 | return HttpResponse(status=400) 154 | except stripe.error.SignatureVerificationError as e: 155 | return HttpResponse(status=400) 156 | 157 | if event["type"] == "checkout.session.completed": 158 | session = event["data"]["object"] 159 | 160 | # Retrieve setup intent 161 | setup_intent = stripe.SetupIntent.retrieve(session["setup_intent"]) 162 | 163 | # Set payment method as default 164 | stripe.Customer.modify( 165 | session["customer"], 166 | invoice_settings={"default_payment_method": setup_intent.payment_method}, 167 | ) 168 | 169 | StripeCustomer = get_stripe_customer_model() 170 | stripe_customer = StripeCustomer.objects.get( 171 | stripe_customer_id=session["customer"], 172 | ) 173 | 174 | subscription = stripe.Subscription.retrieve( 175 | stripe_customer.stripe_subscription_id 176 | ) 177 | 178 | # Check if subscription is still active 179 | # If subscription is active do nothing 180 | # If subscription is not active, create new subscription for same customer. 181 | 182 | return HttpResponse(status=200) 183 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/celery.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import os 4 | 5 | from celery import Celery 6 | from django.conf import settings 7 | 8 | os.environ.setdefault( 9 | "DJANGO_SETTINGS_MODULE", "{{ cookiecutter.project_slug }}.settings" 10 | ) 11 | 12 | app = Celery("{{ cookiecutter.project_slug }}") 13 | app.config_from_object("django.conf:settings", namespace="CELERY") 14 | app.autodiscover_tasks(settings.INSTALLED_APPS) 15 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/context_processors.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | 4 | def google_oauth_client_id(request): 5 | return {"google_oauth_client_id": settings.GOOGLE_OAUTH_CLIENT_ID} 6 | 7 | 8 | def stripe_publishable_key(request): 9 | return {"stripe_publishable_key": settings.STRIPE_PUBLISHABLE_KEY} 10 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/model_loaders.py: -------------------------------------------------------------------------------- 1 | from django.apps import apps 2 | 3 | 4 | def get_stripe_customer_model(): 5 | return apps.get_model("{{ cookiecutter.project_slug }}_billing.StripeCustomer") 6 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/permissions.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from rest_framework.permissions import SAFE_METHODS, BasePermission 3 | from rest_framework_api_key.models import APIKey 4 | from rest_framework_api_key.permissions import BaseHasAPIKey 5 | 6 | 7 | class AllowAnyInDebug(BasePermission): 8 | def has_permission(self, request, view): 9 | if settings.DEBUG: 10 | return True 11 | return False 12 | 13 | 14 | class IsAdminUserAndReadOnly(BasePermission): 15 | def has_permission(self, request, view): 16 | if request.method not in SAFE_METHODS: 17 | return False 18 | 19 | return request.user and request.user.is_staff 20 | 21 | 22 | class IsConsumerAuthenticated(BaseHasAPIKey): 23 | model = APIKey 24 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/search/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ernestofgonzalez/djangorocket/5946100cdf20e588dff73665b122aed009fb1808/templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/search/__init__.py -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/search/api_urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from {{cookiecutter.project_slug}}.search import api_views 4 | 5 | app_name = "{{ cookiecutter.project_slug }}-search" 6 | 7 | urlpatterns = [ 8 | path( 9 | "search/", 10 | api_views.search_view, 11 | name="search", 12 | ), 13 | ] 14 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/search/api_views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.decorators import api_view, permission_classes 2 | from rest_framework.permissions import IsAuthenticated 3 | from rest_framework.response import Response 4 | from rest_framework.status import HTTP_200_OK 5 | 6 | from {{cookiecutter.project_slug}}.permissions import IsConsumerAuthenticated 7 | from {{cookiecutter.project_slug}}.search.serializers import SearchHitSerializer 8 | from {{cookiecutter.project_slug}}.search.utils import search 9 | 10 | 11 | @api_view(["GET"]) 12 | @permission_classes([IsConsumerAuthenticated, IsAuthenticated]) 13 | def search_view(request): 14 | q = request.GET.get("q", None) 15 | 16 | course = request.GET.get("course", None) 17 | 18 | s = search(q, course_uuid=course) 19 | hits_serializer = SearchHitSerializer(s.hits, many=True) 20 | 21 | return Response( 22 | { 23 | "total": len(hits_serializer.data), 24 | "results": hits_serializer.data, 25 | }, 26 | status=HTTP_200_OK, 27 | ) 28 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/search/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class SearchConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "{{ cookiecutter.project_slug }}.search" 7 | label = "{{ cookiecutter.project_slug }}_search" 8 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/search/serializers.py: -------------------------------------------------------------------------------- 1 | import serpy 2 | 3 | from {{cookiecutter.project_slug}}.serializers import Serializer 4 | 5 | 6 | class SearchHitSerializer(Serializer): 7 | id = serpy.Field() 8 | type = serpy.Field() 9 | 10 | def _serialize(self, instance, fields=None): 11 | # Dynamically select the appropriate serializer based on the 'type' field 12 | type_to_serializer = { 13 | # NOTE: include a map from instance.type to respective serializer class 14 | } 15 | 16 | serializer_class = type_to_serializer.get(instance["type"]) 17 | if not serializer_class: 18 | raise ValueError(f"Unknown type: {instance['type']}") 19 | 20 | serialized_data = { 21 | "id": instance["uuid"], 22 | "type": instance["type"], 23 | **serializer_class(instance).data 24 | } 25 | 26 | return serialized_data 27 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/search/utils.py: -------------------------------------------------------------------------------- 1 | from django_opensearch_dsl.search import Search 2 | 3 | 4 | def search(query, course_uuid=None): 5 | search = Search(index="exercises") 6 | search = search.query( 7 | "multi_match", 8 | query=query, 9 | fields=[ 10 | "content_name^3", 11 | "content_messages_text^2", 12 | "content_messages_text_translation", 13 | ], 14 | fuzziness="AUTO", 15 | ) 16 | 17 | if course_uuid: 18 | search = search.filter("term", course_uuid=course_uuid) 19 | 20 | return search.execute() 21 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/serializers.py: -------------------------------------------------------------------------------- 1 | import serpy 2 | 3 | 4 | class Serializer(serpy.Serializer): 5 | def __init__(self, *args, **kwargs): 6 | fields = kwargs.pop("fields", None) 7 | super(Serializer, self).__init__(*args, **kwargs) 8 | 9 | # If fields is passed, includes only passed fields 10 | # in the .to_value() step, allowing to skip query/serialize 11 | # only the necessary fields. 12 | if fields is not None: 13 | allowed = set(fields) 14 | existing = set(self._field_map) 15 | for field_name in existing - allowed: 16 | del self._field_map[field_name] 17 | self._compiled_fields = list( 18 | filter(lambda x: x[0] in allowed, self._compiled_fields) 19 | ) 20 | 21 | context = kwargs.pop("context", None) 22 | if context is not None: 23 | self.context = context 24 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/settings.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from pathlib import Path 4 | 5 | import boto3 6 | import dj_database_url 7 | import sentry_sdk 8 | from django.utils.translation import gettext_lazy as _ 9 | from dotenv import load_dotenv 10 | from opensearchpy import AWSV4SignerAuth, RequestsHttpConnection 11 | 12 | # Load environment variables from .env file 13 | load_dotenv(verbose=True) 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = os.environ.get("SECRET_KEY") 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = False 27 | if os.environ.get("DEBUG", "False") == "True": 28 | DEBUG = True 29 | SECURE_CONTENT_TYPE_NOSNIFF = True 30 | SECURE_BROWSER_XSS_FILTER = True 31 | SECURE_SSL_REDIRECT = True 32 | if os.environ.get("SECURE_SSL_REDIRECT", "True") == "False": 33 | SECURE_SSL_REDIRECT = False 34 | SESSION_COOKIE_SECURE = True 35 | CSRF_COOKIE_SECURE = True 36 | X_FRAME_OPTIONS = "DENY" 37 | 38 | ALLOWED_HOSTS = json.loads(os.environ.get("ALLOWED_HOSTS")) 39 | INTERNAL_IPS = json.loads(os.environ.get("INTERNAL_IPS", "[]")) 40 | 41 | 42 | # Application definition 43 | 44 | INSTALLED_APPS = [ 45 | "django.contrib.admin", 46 | "django.contrib.auth", 47 | "django.contrib.contenttypes", 48 | "django.contrib.humanize", 49 | "django.contrib.sessions", 50 | "django.contrib.messages", 51 | "whitenoise.runserver_nostatic", 52 | "django.contrib.staticfiles", 53 | "compressor", 54 | "corsheaders", 55 | "django_celery_beat", 56 | "django_countries", 57 | "django_opensearch_dsl", 58 | "hijack", 59 | "hijack.contrib.admin", 60 | "phonenumber_field", 61 | "rest_framework", 62 | "rest_framework.authtoken", 63 | "rest_framework_api_key", 64 | "storages", 65 | "tailwind", 66 | "tailwind_theme", 67 | "{{ cookiecutter.project_slug }}.auth", 68 | "{{ cookiecutter.project_slug }}.billing", 69 | "{{ cookiecutter.project_slug }}.utils", 70 | ] 71 | 72 | if DEBUG is True: 73 | INSTALLED_APPS.append("django_browser_reload") 74 | INSTALLED_APPS.append("debug_toolbar") 75 | 76 | MIDDLEWARE = [ 77 | "django.middleware.security.SecurityMiddleware", 78 | "whitenoise.middleware.WhiteNoiseMiddleware", 79 | "django.contrib.sessions.middleware.SessionMiddleware", 80 | "corsheaders.middleware.CorsMiddleware", 81 | "django.middleware.common.CommonMiddleware", 82 | "django.middleware.csrf.CsrfViewMiddleware", 83 | "django.contrib.auth.middleware.AuthenticationMiddleware", 84 | "django.contrib.messages.middleware.MessageMiddleware", 85 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 86 | "hijack.middleware.HijackUserMiddleware", 87 | ] 88 | 89 | if DEBUG is True: 90 | MIDDLEWARE.insert(0, "django_browser_reload.middleware.BrowserReloadMiddleware") 91 | MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware") 92 | MIDDLEWARE.insert(0, "pyinstrument.middleware.ProfilerMiddleware") 93 | 94 | ROOT_URLCONF = "{{ cookiecutter.project_slug }}.urls" 95 | 96 | TEMPLATES = [ 97 | { 98 | "BACKEND": "django.template.backends.django.DjangoTemplates", 99 | "DIRS": [os.path.join(BASE_DIR, "templates")], 100 | "APP_DIRS": True, 101 | "OPTIONS": { 102 | "context_processors": [ 103 | "django.template.context_processors.debug", 104 | "django.template.context_processors.request", 105 | "django.contrib.auth.context_processors.auth", 106 | "django.contrib.messages.context_processors.messages", 107 | "{{ cookiecutter.project_slug }}.context_processors.google_oauth_client_id", 108 | "{{ cookiecutter.project_slug }}.context_processors.stripe_publishable_key", 109 | ], 110 | }, 111 | }, 112 | ] 113 | 114 | WSGI_APPLICATION = "{{ cookiecutter.project_slug }}.wsgi.application" 115 | 116 | CORS_ORIGIN_ALLOW_ALL = False 117 | if os.environ.get("CORS_ORIGIN_ALLOW_ALL", "False") == "True": 118 | CORS_ORIGIN_ALLOW_ALL = True 119 | CORS_ORIGIN_WHITELIST = json.loads(os.environ.get("CORS_ORIGIN_WHITELIST")) 120 | 121 | 122 | # Django Rest Framework 123 | # https://www.django-rest-framework.org/ 124 | 125 | REST_FRAMEWORK = { 126 | "DEFAULT_AUTHENTICATION_CLASSES": ( 127 | "rest_framework.authentication.TokenAuthentication", 128 | "rest_framework.authentication.SessionAuthentication", 129 | ), 130 | "DEFAULT_PERMISSION_CLASSES": ( 131 | "{{cookiecutter.project_slug}}.permissions.IsConsumerAuthenticated", 132 | "rest_framework.permissions.IsAuthenticated", 133 | ), 134 | "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", 135 | "PAGE_SIZE": 100, 136 | "DEFAULT_RENDERER_CLASSES": ( 137 | "rest_framework.renderers.JSONRenderer", 138 | ), 139 | } 140 | 141 | API_KEY_CUSTOM_HEADER = "HTTP_X_API_KEY" 142 | 143 | 144 | # Database 145 | # https://docs.djangoproject.com/en/4.1/ref/settings/#databases 146 | 147 | DATABASES = {"default": dj_database_url.config(default=os.environ.get("DATABASE_URL"))} 148 | 149 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 150 | 151 | AUTH_USER_MODEL = "{{ cookiecutter.project_slug }}_auth.User" 152 | 153 | AUTHENTICATION_BACKENDS = [ 154 | "django.contrib.auth.backends.ModelBackend", 155 | ] 156 | 157 | LOGIN_REDIRECT_URL = "" 158 | LOGIN_URL = "/login/" 159 | LOGOUT = "" 160 | 161 | # Caches 162 | # https://docs.djangoproject.com/en/4.1/ref/settings/#caches 163 | 164 | CACHES = { 165 | "default": { 166 | "BACKEND": "django.core.cache.backends.locmem.LocMemCache", 167 | } 168 | } 169 | 170 | 171 | # Password validation 172 | # https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators 173 | 174 | AUTH_PASSWORD_VALIDATORS = [ 175 | { 176 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 177 | }, 178 | { 179 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 180 | "OPTIONS": { 181 | "min_length": 8, 182 | }, 183 | }, 184 | { 185 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 186 | }, 187 | { 188 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 189 | }, 190 | ] 191 | 192 | 193 | # Internationalization 194 | # https://docs.djangoproject.com/en/4.1/topics/i18n/ 195 | 196 | LANGUAGE_CODE = "en-US" 197 | 198 | TIME_ZONE = "UTC" 199 | 200 | USE_I18N = True 201 | 202 | USE_TZ = True 203 | 204 | LANGUAGES = [ 205 | ("en", _("English")), 206 | ("es", _("Spanish")), 207 | ] 208 | 209 | 210 | # Celery Configuration Options 211 | # https://docs.celeryproject.org 212 | 213 | CELERY_BROKER_URL = os.environ.get("REDIS_URL") 214 | CELERY_REDIS_BACKEND_HEALTH_CHECK_INTERVAL = int( 215 | os.environ.get("CELERY_REDIS_BACKEND_HEALTH_CHECK_INTERVAL", "1000") 216 | ) 217 | CELERY_ACCEPT_CONTENT = ["json"] 218 | CELERY_TASK_SERIALIZER = "json" 219 | CELERY_TASK_TRACK_STARTED = True 220 | CELERY_TASK_TIME_LIMIT = int(os.environ.get("CELERY_TASK_TIME_LIMIT", "2000")) 221 | CELERY_WORKER_CONCURRENCY = 4 222 | CELERY_WORKER_MAX_MEMORY_PER_CHILD = 50000 223 | CELERY_WORKER_MAX_TASKS_PER_CHILD = 100 224 | CELERY_TIMEZONE = os.environ.get("CELERY_TIMEZONE", "UTC") 225 | CELERY_ENABLE_UTC = True 226 | CELERY_BEAT_SCHEDULER = "django_celery_beta.schedulers:DatabaseScheduler" 227 | CELERY_BEAT_SCHEDULE = {} 228 | 229 | 230 | # Static files (CSS, JavaScript, Images) 231 | # https://docs.djangoproject.com/en/4.1/howto/static-files/ 232 | 233 | STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" 234 | if DEBUG is True: 235 | STATICFILES_STORAGE = "django.contrib.staticfiles.storage.StaticFilesStorage" 236 | STATIC_DIR = os.path.join(BASE_DIR, "static") 237 | STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles") 238 | STATIC_HOST = os.environ.get("STATIC_HOST") 239 | STATICFILES_DIRS = [ 240 | STATIC_DIR, 241 | ] 242 | STATIC_URL = "/static/" 243 | 244 | 245 | COMPRESS_STORAGE = "compressor.storage.GzipCompressorFileStorage" 246 | COMPRESS_ROOT = os.path.join(BASE_DIR, "staticfiles") 247 | if DEBUG is True: 248 | COMPRESS_ROOT = os.path.join(BASE_DIR, "static") 249 | COMPRESS_ENABLED = not DEBUG 250 | COMPRESS_OFFLINE = True 251 | 252 | STATICFILES_FINDERS = [ 253 | "django.contrib.staticfiles.finders.FileSystemFinder", 254 | "django.contrib.staticfiles.finders.AppDirectoriesFinder", 255 | "compressor.finders.CompressorFinder", 256 | ] 257 | 258 | 259 | # Django Tailwind 260 | 261 | TAILWIND_APP_NAME = "tailwind_theme" 262 | 263 | 264 | # Email configurations 265 | # https://docs.djangoproject.com/en/4.1/topics/email/ 266 | 267 | EMAIL_BACKEND = os.environ.get( 268 | "EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend" 269 | ) 270 | EMAIL_HOST = os.environ.get("EMAIL_HOST") 271 | EMAIL_PORT = int(os.environ.get("EMAIL_PORT", "587")) 272 | EMAIL_USE_TLS = True 273 | if os.environ.get("EMAIL_USE_TLS", "True") == "False": 274 | EMAIL_USE_TLS = False 275 | EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER") 276 | EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD") 277 | DEFAULT_FROM_EMAIL = os.environ.get("DEFAULT_FROM_EMAIL") 278 | 279 | 280 | # Amazon Web Services configurations 281 | # https://aws.amazon.com 282 | 283 | AWS_ACCOUNT_ID = os.environ.get("AWS_ACCOUNT_ID") 284 | AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID") 285 | AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY") 286 | AWS_STORAGE_STATIC_BUCKET_NAME = os.environ.get("AWS_STORAGE_STATIC_BUCKET_NAME") 287 | AWS_STORAGE_MEDIA_INPUT_BUCKET_NAME = os.environ.get( 288 | "AWS_STORAGE_MEDIA_INPUT_BUCKET_NAME" 289 | ) 290 | AWS_STORAGE_MEDIA_INPUT_BUCKET_REGION_NAME = os.environ.get( 291 | "AWS_STORAGE_MEDIA_INPUT_BUCKET_REGION_NAME" 292 | ) 293 | AWS_STORAGE_MEDIA_OUTPUT_BUCKET_NAME = os.environ.get( 294 | "AWS_STORAGE_MEDIA_OUTPUT_BUCKET_NAME" 295 | ) 296 | AWS_STORAGE_STATIC_HOST = os.environ.get("AWS_STORAGE_STATIC_HOST", "s3.amazonaws.com") 297 | AWS_STORAGE_MEDIA_INPUT_HOST = os.environ.get( 298 | "AWS_STORAGE_MEDIA_INPUT_HOST", "s3.amazonaws.com" 299 | ) 300 | AWS_STORAGE_MEDIA_OUTPUT_HOST = os.environ.get( 301 | "AWS_STORAGE_MEDIA_OUTPUT_HOST", "s3.amazonaws.com" 302 | ) 303 | AWS_STORAGE_STATIC_DOMAIN = "%s.%s" % ( 304 | AWS_STORAGE_STATIC_BUCKET_NAME, 305 | AWS_STORAGE_STATIC_HOST, 306 | ) 307 | AWS_STORAGE_MEDIA_INPUT_DOMAIN = "%s.%s" % ( 308 | AWS_STORAGE_MEDIA_INPUT_BUCKET_NAME, 309 | AWS_STORAGE_MEDIA_INPUT_HOST, 310 | ) 311 | AWS_STORAGE_MEDIA_OUTPUT_DOMAIN = "%s.%s" % ( 312 | AWS_STORAGE_MEDIA_OUTPUT_BUCKET_NAME, 313 | AWS_STORAGE_MEDIA_OUTPUT_HOST, 314 | ) 315 | AWS_CLOUD_FRONT_DOMAIN_NAME = os.environ.get("AWS_CLOUD_FRONT_DOMAIN_NAME") 316 | AWS_CLOUD_FRONT_PRIVATE_KEY = os.environ.get("AWS_CLOUD_FRONT_PRIVATE_KEY") 317 | AWS_CLOUD_FRONT_KEY_PAIR_ID = os.environ.get("AWS_CLOUD_FRONT_KEY_PAIR_ID") 318 | AWS_OPEN_SEARCH_HOST = os.environ.get("AWS_OPEN_SEARCH_HOST") 319 | AWS_OPEN_SEARCH_REGION_NAME = os.environ.get("AWS_OPEN_SEARCH_REGION_NAME") 320 | AWS_SES_SMTP_USER = os.environ.get("AWS_SES_SMTP_USER") 321 | AWS_SES_SMTP_PASSWORD = os.environ.get("AWS_SES_SMTP_PASSWORD") 322 | AWS_SES_REGION_NAME = os.environ.get("AWS_SES_REGION_NAME") 323 | AWS_SES_REGION_ENDPOINT = os.environ.get("AWS_SES_REGION_ENDPOINT") 324 | 325 | 326 | # Google 327 | 328 | GOOGLE_OAUTH_CLIENT_ID = os.environ.get("GOOGLE_OAUTH_CLIENT_ID", None) 329 | GOOGLE_OAUTH_CLIENT_SECRET = os.environ.get("GOOGLE_OAUTH_CLIENT_SECRET", None) 330 | 331 | 332 | # Stripe 333 | 334 | STRIPE_PUBLISHABLE_KEY = os.environ.get("STRIPE_PUBLISHABLE_KEY", None) 335 | STRIPE_SECRET_KEY = os.environ.get("STRIPE_SECRET_KEY", None) 336 | STRIPE_WEBHOOK_SECRET = os.environ.get("STRIPE_WEBHOOK_SECRET", None) 337 | STRIPE_PRICE_ID = os.environ.get("STRIPE_PRICE_ID", None) 338 | 339 | 340 | # Mixpanel 341 | 342 | MIXPANEL_API_TOKEN = os.environ.get("MIXPANEL_API_TOKEN", None) 343 | 344 | 345 | # Django Opensearch DSL 346 | # https://django-opensearch-dsl.readthedocs.io/en/latest/ 347 | 348 | OPENSEARCH_DSL = { 349 | "default": { 350 | "hosts": AWS_OPEN_SEARCH_HOST, 351 | "http_auth": AWSV4SignerAuth( 352 | boto3.Session( 353 | aws_access_key_id=AWS_ACCESS_KEY_ID, 354 | aws_secret_access_key=AWS_SECRET_ACCESS_KEY, 355 | ).get_credentials(), 356 | AWS_OPEN_SEARCH_REGION_NAME, 357 | "es", 358 | ), 359 | "use_ssl": True, 360 | "verify_certs": True, 361 | "connection_class": RequestsHttpConnection, 362 | "pool_maxsize": 20, 363 | }, 364 | } 365 | 366 | 367 | # Sentry 368 | 369 | SENTRY_DSN = os.environ.get("SENTRY_DSN", None) 370 | 371 | sentry_sdk.init( 372 | dsn=SENTRY_DSN, 373 | traces_sample_rate=1.0, 374 | _experiments={ 375 | "continuous_profiling_auto_start": False, 376 | }, 377 | ) 378 | 379 | 380 | # {{ cookiecutter.project_name }} config 381 | 382 | SUBSCRIPTION_TRIAL_PERIOD_DAYS = 14 383 | 384 | AUTH_USER_NAME_MAX_LENGTH = int(os.environ.get("AUTH_USER_NAME_MAX_LENGTH", "150")) 385 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib import admin 3 | from django.contrib.staticfiles.urls import staticfiles_urlpatterns 4 | from django.urls import include, path 5 | from {{cookiecutter.project_slug}} import views 6 | 7 | api_urlpatterns = ( 8 | [ 9 | path("", include("{{ cookiecutter.project_slug }}.search.api_urls")), 10 | ], 11 | "api", 12 | ) 13 | 14 | urlpatterns = [ 15 | path("admin/", admin.site.urls), 16 | path("api/", include(api_urlpatterns)), 17 | path("", views.index_view, name="index"), 18 | path("", include("{{ cookiecutter.project_slug }}.auth.urls")), 19 | path("", include("{{ cookiecutter.project_slug }}.billing.urls")), 20 | ] 21 | 22 | if settings.DEBUG is True: 23 | urlpatterns += staticfiles_urlpatterns() 24 | urlpatterns += [ 25 | path("__debug__/", include("debug_toolbar.urls")), 26 | path("__reload__/", include("django_browser_reload.urls")), 27 | ] 28 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/utils/__init__.py: -------------------------------------------------------------------------------- 1 | import shortuuid 2 | 3 | 4 | def default_uuid(): 5 | return shortuuid.uuid()[:7] 6 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/utils/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UtilsConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "{{ cookiecutter.project_slug }}.utils" 7 | label = "{{ cookiecutter.project_slug }}_utils" 8 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/utils/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ernestofgonzalez/djangorocket/5946100cdf20e588dff73665b122aed009fb1808/templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/utils/templatetags/__init__.py -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/utils/templatetags/{{ cookiecutter.project_slug }}_utils_math.py: -------------------------------------------------------------------------------- 1 | import builtins 2 | import logging 3 | from decimal import Decimal 4 | 5 | from django import template 6 | 7 | register = template.Library() 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | def valid_numeric(arg): 13 | if isinstance(arg, (int, float, Decimal)): 14 | return arg 15 | try: 16 | return int(arg) 17 | except ValueError: 18 | return float(arg) 19 | 20 | 21 | def handle_float_decimal_combinations(value, arg, operation): 22 | if isinstance(value, float) and isinstance(arg, Decimal): 23 | logger.warning( 24 | "Unsafe operation: {0!r} {1} {2!r}.".format(value, operation, arg) 25 | ) 26 | value = Decimal(str(value)) 27 | if isinstance(value, Decimal) and isinstance(arg, float): 28 | logger.warning( 29 | "Unsafe operation: {0!r} {1} {2!r}.".format(value, operation, arg) 30 | ) 31 | arg = Decimal(str(arg)) 32 | return value, arg 33 | 34 | 35 | @register.filter 36 | def abs(value): 37 | return builtins.abs(value) 38 | 39 | 40 | @register.filter 41 | def div(value, arg): 42 | try: 43 | nvalue, narg = handle_float_decimal_combinations( 44 | valid_numeric(value), valid_numeric(arg), "/" 45 | ) 46 | return nvalue / narg 47 | except (ValueError, TypeError): 48 | try: 49 | return value / arg 50 | except Exception: 51 | return "" 52 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/utils/templatetags/{{ cookiecutter.project_slug }}_utils_timestamp.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from django import template 4 | 5 | register = template.Library() 6 | 7 | 8 | @register.filter 9 | def timestamp_to_datetime(value): 10 | return datetime.fromtimestamp(value) 11 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/utils/timezone/__init__.py: -------------------------------------------------------------------------------- 1 | from django.utils.timezone import now as django_utils_timezone_now 2 | 3 | 4 | def now(): 5 | return django_utils_timezone_now() 6 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from django.views.decorators.http import require_GET 3 | 4 | 5 | @require_GET 6 | def index_view(request): 7 | return render(request, "pages/index.html") 8 | -------------------------------------------------------------------------------- /templates/projects/base/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/wsgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.core.wsgi import get_wsgi_application 4 | 5 | os.environ.setdefault( 6 | "DJANGO_SETTINGS_MODULE", "{{ cookiecutter.project_slug }}.settings" 7 | ) 8 | 9 | application = get_wsgi_application() 10 | --------------------------------------------------------------------------------