├── .dockerignore ├── .editorconfig ├── .envs └── .local │ ├── .django │ └── .postgres ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .idea ├── djsniper.iml ├── misc.xml ├── modules.xml ├── runConfigurations │ ├── merge_production_dotenvs_in_dotenv.xml │ ├── migrate.xml │ ├── pytest___.xml │ ├── pytest__users.xml │ ├── runserver.xml │ └── runserver_plus.xml ├── vcs.xml └── webResources.xml ├── .pre-commit-config.yaml ├── .pylintrc ├── .readthedocs.yml ├── .vscode └── settings.json ├── CONTRIBUTORS.txt ├── LICENSE ├── README.md ├── compose ├── local │ ├── django │ │ ├── Dockerfile │ │ ├── celery │ │ │ ├── beat │ │ │ │ └── start │ │ │ ├── flower │ │ │ │ └── start │ │ │ └── worker │ │ │ │ └── start │ │ └── start │ └── docs │ │ ├── Dockerfile │ │ └── start └── production │ ├── aws │ ├── Dockerfile │ └── maintenance │ │ ├── download │ │ └── upload │ ├── django │ ├── Dockerfile │ ├── celery │ │ ├── beat │ │ │ └── start │ │ ├── flower │ │ │ └── start │ │ └── worker │ │ │ └── start │ ├── entrypoint │ └── start │ ├── postgres │ ├── Dockerfile │ └── maintenance │ │ ├── _sourced │ │ ├── constants.sh │ │ ├── countdown.sh │ │ ├── messages.sh │ │ └── yes_no.sh │ │ ├── backup │ │ ├── backups │ │ └── restore │ └── traefik │ ├── Dockerfile │ └── traefik.yml ├── config ├── __init__.py ├── api_router.py ├── celery_app.py ├── settings │ ├── __init__.py │ ├── base.py │ ├── local.py │ ├── production.py │ └── test.py ├── urls.py └── wsgi.py ├── djsniper ├── __init__.py ├── conftest.py ├── contrib │ ├── __init__.py │ └── sites │ │ ├── __init__.py │ │ └── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_alter_domain_unique.py │ │ ├── 0003_set_site_domain_and_name.py │ │ ├── 0004_alter_options_ordering_domain.py │ │ └── __init__.py ├── sniper │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── forms.py │ ├── management │ │ └── commands │ │ │ ├── fetch_nfts.py │ │ │ └── rank_nfts.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── tasks.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── static │ ├── css │ │ └── project.css │ ├── fonts │ │ └── .gitkeep │ ├── images │ │ └── favicons │ │ │ └── favicon.ico │ ├── js │ │ ├── celery_progress.js │ │ └── project.js │ └── sass │ │ ├── custom_bootstrap_vars.scss │ │ └── project.scss ├── templates │ ├── 403.html │ ├── 404.html │ ├── 500.html │ ├── account │ │ ├── account_inactive.html │ │ ├── base.html │ │ ├── email.html │ │ ├── email_confirm.html │ │ ├── login.html │ │ ├── logout.html │ │ ├── password_change.html │ │ ├── password_reset.html │ │ ├── password_reset_done.html │ │ ├── password_reset_from_key.html │ │ ├── password_reset_from_key_done.html │ │ ├── password_set.html │ │ ├── signup.html │ │ ├── signup_closed.html │ │ ├── verification_sent.html │ │ └── verified_email_required.html │ ├── base.html │ ├── nfts.html │ ├── pages │ │ └── about.html │ ├── sniper │ │ ├── fetch_nfts.html │ │ ├── project_clear.html │ │ ├── project_create.html │ │ ├── project_delete.html │ │ ├── project_detail.html │ │ ├── project_list.html │ │ └── project_update.html │ └── users │ │ ├── user_detail.html │ │ └── user_form.html ├── users │ ├── __init__.py │ ├── adapters.py │ ├── admin.py │ ├── api │ │ ├── serializers.py │ │ └── views.py │ ├── apps.py │ ├── forms.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── tasks.py │ ├── tests │ │ ├── __init__.py │ │ ├── factories.py │ │ ├── test_admin.py │ │ ├── test_drf_urls.py │ │ ├── test_drf_views.py │ │ ├── test_forms.py │ │ ├── test_models.py │ │ ├── test_tasks.py │ │ ├── test_urls.py │ │ └── test_views.py │ ├── urls.py │ └── views.py └── utils │ ├── __init__.py │ └── storages.py ├── docs ├── Makefile ├── __init__.py ├── conf.py ├── howto.rst ├── index.rst ├── make.bat ├── pycharm │ ├── configuration.rst │ └── images │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 7.png │ │ ├── 8.png │ │ ├── f1.png │ │ ├── f2.png │ │ ├── f3.png │ │ ├── f4.png │ │ ├── issue1.png │ │ └── issue2.png └── users.rst ├── local.yml ├── locale └── README.rst ├── manage.py ├── merge_production_dotenvs_in_dotenv.py ├── production.yml ├── pytest.ini ├── requirements ├── base.txt ├── local.txt └── production.txt └── setup.cfg /.dockerignore: -------------------------------------------------------------------------------- 1 | .editorconfig 2 | .gitattributes 3 | .github 4 | .gitignore 5 | .gitlab-ci.yml 6 | .idea 7 | .pre-commit-config.yaml 8 | .readthedocs.yml 9 | .travis.yml 10 | venv 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{py,rst,ini}] 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [*.{html,css,scss,json,yml}] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | 22 | [Makefile] 23 | indent_style = tab 24 | 25 | [nginx.conf] 26 | indent_style = space 27 | indent_size = 2 28 | -------------------------------------------------------------------------------- /.envs/.local/.django: -------------------------------------------------------------------------------- 1 | # General 2 | # ------------------------------------------------------------------------------ 3 | USE_DOCKER=yes 4 | IPYTHONDIR=/app/.ipython 5 | # Redis 6 | # ------------------------------------------------------------------------------ 7 | REDIS_URL=redis://redis:6379/0 8 | 9 | # Celery 10 | # ------------------------------------------------------------------------------ 11 | 12 | # Flower 13 | CELERY_FLOWER_USER=lRaelInFbhgNtvTDfIBOMzPRPYzUbHKg 14 | CELERY_FLOWER_PASSWORD=xEcgusNOu1dQ8Z0dClINJcAxV03gEnjpy3ujiVuVn2Hh10danRGj7wdypIHYsDjt 15 | -------------------------------------------------------------------------------- /.envs/.local/.postgres: -------------------------------------------------------------------------------- 1 | # PostgreSQL 2 | # ------------------------------------------------------------------------------ 3 | POSTGRES_HOST=postgres 4 | POSTGRES_PORT=5432 5 | POSTGRES_DB=djsniper 6 | POSTGRES_USER=uwQRXKxdEXATlomoPpEZuhvGZfAMNJmY 7 | POSTGRES_PASSWORD=wac84lgvKcFF0g6DIB5pOeVpXS0mfqo4dtdl4TCE1lynfvUL4SW07qM8sfSeByWZ 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Update Github actions in workflows 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | # Enable Buildkit and let compose use it to speed up image building 4 | env: 5 | DOCKER_BUILDKIT: 1 6 | COMPOSE_DOCKER_CLI_BUILD: 1 7 | 8 | on: 9 | pull_request: 10 | branches: [ "master", "main" ] 11 | paths-ignore: [ "docs/**" ] 12 | 13 | push: 14 | branches: [ "master", "main" ] 15 | paths-ignore: [ "docs/**" ] 16 | 17 | 18 | jobs: 19 | linter: 20 | runs-on: ubuntu-latest 21 | steps: 22 | 23 | - name: Checkout Code Repository 24 | uses: actions/checkout@v2 25 | 26 | - name: Set up Python 3.9 27 | uses: actions/setup-python@v2 28 | with: 29 | python-version: 3.9 30 | 31 | # Run all pre-commit hooks on all the files. 32 | # Getting only staged files can be tricky in case a new PR is opened 33 | # since the action is run on a branch in detached head state 34 | - name: Install and Run Pre-commit 35 | uses: pre-commit/action@v2.0.3 36 | 37 | # With no caching at all the entire ci process takes 4m 30s to complete! 38 | pytest: 39 | runs-on: ubuntu-latest 40 | 41 | steps: 42 | 43 | - name: Checkout Code Repository 44 | uses: actions/checkout@v2 45 | 46 | - name: Build the Stack 47 | run: docker-compose -f local.yml build 48 | 49 | - name: Run DB Migrations 50 | run: docker-compose -f local.yml run --rm django python manage.py migrate 51 | 52 | - name: Run Django Tests 53 | run: docker-compose -f local.yml run django pytest 54 | 55 | - name: Tear down the Stack 56 | run: docker-compose -f local.yml down 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Python template 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | staticfiles/ 55 | 56 | # Sphinx documentation 57 | docs/_build/ 58 | 59 | # PyBuilder 60 | target/ 61 | 62 | # pyenv 63 | .python-version 64 | 65 | # celery beat schedule file 66 | celerybeat-schedule 67 | 68 | # Environments 69 | .venv 70 | venv/ 71 | ENV/ 72 | 73 | # Rope project settings 74 | .ropeproject 75 | 76 | # mkdocs documentation 77 | /site 78 | 79 | # mypy 80 | .mypy_cache/ 81 | 82 | 83 | ### Node template 84 | # Logs 85 | logs 86 | *.log 87 | npm-debug.log* 88 | yarn-debug.log* 89 | yarn-error.log* 90 | 91 | # Runtime data 92 | pids 93 | *.pid 94 | *.seed 95 | *.pid.lock 96 | 97 | # Directory for instrumented libs generated by jscoverage/JSCover 98 | lib-cov 99 | 100 | # Coverage directory used by tools like istanbul 101 | coverage 102 | 103 | # nyc test coverage 104 | .nyc_output 105 | 106 | # Bower dependency directory (https://bower.io/) 107 | bower_components 108 | 109 | # node-waf configuration 110 | .lock-wscript 111 | 112 | # Compiled binary addons (http://nodejs.org/api/addons.html) 113 | build/Release 114 | 115 | # Dependency directories 116 | node_modules/ 117 | jspm_packages/ 118 | 119 | # Typescript v1 declaration files 120 | typings/ 121 | 122 | # Optional npm cache directory 123 | .npm 124 | 125 | # Optional eslint cache 126 | .eslintcache 127 | 128 | # Optional REPL history 129 | .node_repl_history 130 | 131 | # Output of 'npm pack' 132 | *.tgz 133 | 134 | # Yarn Integrity file 135 | .yarn-integrity 136 | 137 | 138 | ### Linux template 139 | *~ 140 | 141 | # temporary files which can be created if a process still has a handle open of a deleted file 142 | .fuse_hidden* 143 | 144 | # KDE directory preferences 145 | .directory 146 | 147 | # Linux trash folder which might appear on any partition or disk 148 | .Trash-* 149 | 150 | # .nfs files are created when an open file is removed but is still being accessed 151 | .nfs* 152 | 153 | 154 | ### VisualStudioCode template 155 | .vscode/* 156 | !.vscode/settings.json 157 | !.vscode/tasks.json 158 | !.vscode/launch.json 159 | !.vscode/extensions.json 160 | *.code-workspace 161 | 162 | # Local History for Visual Studio Code 163 | .history/ 164 | 165 | 166 | # Provided default Pycharm Run/Debug Configurations should be tracked by git 167 | # In case of local modifications made by Pycharm, use update-index command 168 | # for each changed file, like this: 169 | # git update-index --assume-unchanged .idea/djsniper.iml 170 | ### JetBrains template 171 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 172 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 173 | 174 | # User-specific stuff: 175 | .idea/**/workspace.xml 176 | .idea/**/tasks.xml 177 | .idea/dictionaries 178 | 179 | # Sensitive or high-churn files: 180 | .idea/**/dataSources/ 181 | .idea/**/dataSources.ids 182 | .idea/**/dataSources.xml 183 | .idea/**/dataSources.local.xml 184 | .idea/**/sqlDataSources.xml 185 | .idea/**/dynamic.xml 186 | .idea/**/uiDesigner.xml 187 | 188 | # Gradle: 189 | .idea/**/gradle.xml 190 | .idea/**/libraries 191 | 192 | # CMake 193 | cmake-build-debug/ 194 | 195 | # Mongo Explorer plugin: 196 | .idea/**/mongoSettings.xml 197 | 198 | ## File-based project format: 199 | *.iws 200 | 201 | ## Plugin-specific files: 202 | 203 | # IntelliJ 204 | out/ 205 | 206 | # mpeltonen/sbt-idea plugin 207 | .idea_modules/ 208 | 209 | # JIRA plugin 210 | atlassian-ide-plugin.xml 211 | 212 | # Cursive Clojure plugin 213 | .idea/replstate.xml 214 | 215 | # Crashlytics plugin (for Android Studio and IntelliJ) 216 | com_crashlytics_export_strings.xml 217 | crashlytics.properties 218 | crashlytics-build.properties 219 | fabric.properties 220 | 221 | 222 | 223 | ### Windows template 224 | # Windows thumbnail cache files 225 | Thumbs.db 226 | ehthumbs.db 227 | ehthumbs_vista.db 228 | 229 | # Dump file 230 | *.stackdump 231 | 232 | # Folder config file 233 | Desktop.ini 234 | 235 | # Recycle Bin used on file shares 236 | $RECYCLE.BIN/ 237 | 238 | # Windows Installer files 239 | *.cab 240 | *.msi 241 | *.msm 242 | *.msp 243 | 244 | # Windows shortcuts 245 | *.lnk 246 | 247 | 248 | ### macOS template 249 | # General 250 | *.DS_Store 251 | .AppleDouble 252 | .LSOverride 253 | 254 | # Icon must end with two \r 255 | Icon 256 | 257 | # Thumbnails 258 | ._* 259 | 260 | # Files that might appear in the root of a volume 261 | .DocumentRevisions-V100 262 | .fseventsd 263 | .Spotlight-V100 264 | .TemporaryItems 265 | .Trashes 266 | .VolumeIcon.icns 267 | .com.apple.timemachine.donotpresent 268 | 269 | # Directories potentially created on remote AFP share 270 | .AppleDB 271 | .AppleDesktop 272 | Network Trash Folder 273 | Temporary Items 274 | .apdisk 275 | 276 | 277 | ### SublimeText template 278 | # Cache files for Sublime Text 279 | *.tmlanguage.cache 280 | *.tmPreferences.cache 281 | *.stTheme.cache 282 | 283 | # Workspace files are user-specific 284 | *.sublime-workspace 285 | 286 | # Project files should be checked into the repository, unless a significant 287 | # proportion of contributors will probably not be using Sublime Text 288 | # *.sublime-project 289 | 290 | # SFTP configuration file 291 | sftp-config.json 292 | 293 | # Package control specific files 294 | Package Control.last-run 295 | Package Control.ca-list 296 | Package Control.ca-bundle 297 | Package Control.system-ca-bundle 298 | Package Control.cache/ 299 | Package Control.ca-certs/ 300 | Package Control.merged-ca-bundle 301 | Package Control.user-ca-bundle 302 | oscrypto-ca-bundle.crt 303 | bh_unicode_properties.cache 304 | 305 | # Sublime-github package stores a github token in this file 306 | # https://packagecontrol.io/packages/sublime-github 307 | GitHub.sublime-settings 308 | 309 | 310 | ### Vim template 311 | # Swap 312 | [._]*.s[a-v][a-z] 313 | [._]*.sw[a-p] 314 | [._]s[a-v][a-z] 315 | [._]sw[a-p] 316 | 317 | # Session 318 | Session.vim 319 | 320 | # Temporary 321 | .netrwhist 322 | 323 | # Auto-generated tag files 324 | tags 325 | 326 | ### Project template 327 | djsniper/media/ 328 | 329 | .pytest_cache/ 330 | .ipython/ 331 | .env 332 | .envs/* 333 | !.envs/.local/ 334 | -------------------------------------------------------------------------------- /.idea/djsniper.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 24 | 25 | 27 | 28 | 31 | 32 | 38 | 39 | 40 | 43 | 44 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations/merge_production_dotenvs_in_dotenv.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/runConfigurations/migrate.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 32 | 33 | -------------------------------------------------------------------------------- /.idea/runConfigurations/pytest___.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 25 | -------------------------------------------------------------------------------- /.idea/runConfigurations/pytest__users.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 25 | -------------------------------------------------------------------------------- /.idea/runConfigurations/runserver.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 33 | 34 | -------------------------------------------------------------------------------- /.idea/runConfigurations/runserver_plus.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 33 | 34 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/webResources.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: "^docs/|/migrations/" 2 | default_stages: [commit] 3 | 4 | repos: 5 | - repo: https://github.com/pre-commit/pre-commit-hooks 6 | rev: v4.0.1 7 | hooks: 8 | - id: trailing-whitespace 9 | - id: end-of-file-fixer 10 | - id: check-yaml 11 | 12 | - repo: https://github.com/psf/black 13 | rev: 21.12b0 14 | hooks: 15 | - id: black 16 | 17 | - repo: https://github.com/PyCQA/isort 18 | rev: 5.10.1 19 | hooks: 20 | - id: isort 21 | 22 | - repo: https://github.com/PyCQA/flake8 23 | rev: 4.0.1 24 | hooks: 25 | - id: flake8 26 | args: ["--config=setup.cfg"] 27 | additional_dependencies: [flake8-isort] 28 | 29 | # sets up .pre-commit-ci.yaml to ensure pre-commit dependencies stay up to date 30 | ci: 31 | autoupdate_schedule: weekly 32 | skip: [] 33 | submodules: false 34 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | load-plugins=pylint_django, pylint_celery 3 | django-settings-module=config.settings.base 4 | [FORMAT] 5 | max-line-length=120 6 | 7 | [MESSAGES CONTROL] 8 | disable=missing-docstring,invalid-name 9 | 10 | [DESIGN] 11 | max-parents=13 12 | 13 | [TYPECHECK] 14 | generated-members=REQUEST,acl_users,aq_parent,"[a-zA-Z]+_set{1,2}",save,delete 15 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | sphinx: 4 | configuration: docs/conf.py 5 | 6 | build: 7 | image: testing 8 | 9 | python: 10 | version: 3.9 11 | install: 12 | - requirements: requirements/local.txt 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.formatting.provider": "black" 3 | } -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | Matthew Freire 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | Copyright (c) 2021, Matthew Freire 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

3 | 4 | JustDjango 5 | 6 |

7 |

8 | The Definitive Django Learning Platform. 9 |

10 |

11 | 12 | # DjSniper 13 | 14 | Self-Hostable NFT rarity tool built with Django 15 | 16 | [![Built with Cookiecutter Django](https://img.shields.io/badge/built%20with-Cookiecutter%20Django-ff69b4.svg?logo=cookiecutter)](https://github.com/cookiecutter/cookiecutter-django/) 17 | 18 | [![Black code style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) 19 | 20 | Settings 21 | -------- 22 | 23 | Moved to 24 | [settings](http://cookiecutter-django.readthedocs.io/en/latest/settings.html). 25 | 26 | Basic Commands 27 | -------------- 28 | 29 | ### Setting Up Your Users 30 | 31 | - To create a **normal user account**, just go to Sign Up and fill out 32 | the form. Once you submit it, you\'ll see a \"Verify Your E-mail 33 | Address\" page. Go to your console to see a simulated email 34 | verification message. Copy the link into your browser. Now the 35 | user\'s email should be verified and ready to go. 36 | 37 | - To create an **superuser account**, use this command: 38 | 39 | $ python manage.py createsuperuser 40 | 41 | For convenience, you can keep your normal user logged in on Chrome and 42 | your superuser logged in on Firefox (or similar), so that you can see 43 | how the site behaves for both kinds of users. 44 | 45 | ### Type checks 46 | 47 | Running type checks with mypy: 48 | 49 | $ mypy djsniper 50 | 51 | ### Test coverage 52 | 53 | To run the tests, check your test coverage, and generate an HTML 54 | coverage report: 55 | 56 | $ coverage run -m pytest 57 | $ coverage html 58 | $ open htmlcov/index.html 59 | 60 | #### Running tests with py.test 61 | 62 | $ pytest 63 | 64 | ### Live reloading and Sass CSS compilation 65 | 66 | Moved to [Live reloading and SASS 67 | compilation](http://cookiecutter-django.readthedocs.io/en/latest/live-reloading-and-sass-compilation.html). 68 | 69 | ### Celery 70 | 71 | This app comes with Celery. 72 | 73 | To run a celery worker: 74 | 75 | ``` {.bash} 76 | cd djsniper 77 | celery -A config.celery_app worker -l info 78 | ``` 79 | 80 | Please note: For Celery\'s import magic to work, it is important *where* 81 | the celery commands are run. If you are in the same folder with 82 | *manage.py*, you should be right. 83 | 84 | ### Sentry 85 | 86 | Sentry is an error logging aggregator service. You can sign up for a 87 | free account at or 88 | download and host it yourself. The system is setup with reasonable 89 | defaults, including 404 logging and integration with the WSGI 90 | application. 91 | 92 | You must set the DSN url in production. 93 | 94 | Deployment 95 | ---------- 96 | 97 | The following details how to deploy this application. 98 | 99 | ### Docker 100 | 101 | See detailed [cookiecutter-django Docker 102 | documentation](http://cookiecutter-django.readthedocs.io/en/latest/deployment-with-docker.html). 103 | 104 | --- 105 | 106 | Other places you can find us:
107 | 108 | YouTube 109 | Twitter 110 | 111 | 112 | -------------------------------------------------------------------------------- /compose/local/django/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PYTHON_VERSION=3.9-slim-bullseye 2 | 3 | # define an alias for the specfic python version used in this file. 4 | FROM python:${PYTHON_VERSION} as python 5 | 6 | # Python build stage 7 | FROM python as python-build-stage 8 | 9 | ARG BUILD_ENVIRONMENT=local 10 | 11 | # Install apt packages 12 | RUN apt-get update && apt-get install --no-install-recommends -y \ 13 | # dependencies for building Python packages 14 | build-essential \ 15 | # psycopg2 dependencies 16 | libpq-dev 17 | 18 | # Requirements are installed here to ensure they will be cached. 19 | COPY ./requirements . 20 | 21 | # Create Python Dependency and Sub-Dependency Wheels. 22 | RUN pip wheel --wheel-dir /usr/src/app/wheels \ 23 | -r ${BUILD_ENVIRONMENT}.txt 24 | 25 | 26 | # Python 'run' stage 27 | FROM python as python-run-stage 28 | 29 | ARG BUILD_ENVIRONMENT=local 30 | ARG APP_HOME=/app 31 | 32 | ENV PYTHONUNBUFFERED 1 33 | ENV PYTHONDONTWRITEBYTECODE 1 34 | ENV BUILD_ENV ${BUILD_ENVIRONMENT} 35 | 36 | WORKDIR ${APP_HOME} 37 | 38 | # Install required system dependencies 39 | RUN apt-get update && apt-get install --no-install-recommends -y \ 40 | # psycopg2 dependencies 41 | libpq-dev \ 42 | # Translations dependencies 43 | gettext \ 44 | # cleaning up unused files 45 | && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ 46 | && rm -rf /var/lib/apt/lists/* 47 | 48 | # All absolute dir copies ignore workdir instruction. All relative dir copies are wrt to the workdir instruction 49 | # copy python dependency wheels from python-build-stage 50 | COPY --from=python-build-stage /usr/src/app/wheels /wheels/ 51 | 52 | # use wheels to install python dependencies 53 | RUN pip install --no-cache-dir --no-index --find-links=/wheels/ /wheels/* \ 54 | && rm -rf /wheels/ 55 | 56 | COPY ./compose/production/django/entrypoint /entrypoint 57 | RUN sed -i 's/\r$//g' /entrypoint 58 | RUN chmod +x /entrypoint 59 | 60 | COPY ./compose/local/django/start /start 61 | RUN sed -i 's/\r$//g' /start 62 | RUN chmod +x /start 63 | 64 | 65 | COPY ./compose/local/django/celery/worker/start /start-celeryworker 66 | RUN sed -i 's/\r$//g' /start-celeryworker 67 | RUN chmod +x /start-celeryworker 68 | 69 | COPY ./compose/local/django/celery/beat/start /start-celerybeat 70 | RUN sed -i 's/\r$//g' /start-celerybeat 71 | RUN chmod +x /start-celerybeat 72 | 73 | COPY ./compose/local/django/celery/flower/start /start-flower 74 | RUN sed -i 's/\r$//g' /start-flower 75 | RUN chmod +x /start-flower 76 | 77 | 78 | # copy application code to WORKDIR 79 | COPY . ${APP_HOME} 80 | 81 | ENTRYPOINT ["/entrypoint"] 82 | -------------------------------------------------------------------------------- /compose/local/django/celery/beat/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | 7 | rm -f './celerybeat.pid' 8 | celery -A config.celery_app beat -l INFO 9 | -------------------------------------------------------------------------------- /compose/local/django/celery/flower/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | 7 | celery \ 8 | -A config.celery_app \ 9 | -b "${CELERY_BROKER_URL}" \ 10 | flower \ 11 | --basic_auth="${CELERY_FLOWER_USER}:${CELERY_FLOWER_PASSWORD}" 12 | -------------------------------------------------------------------------------- /compose/local/django/celery/worker/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | 7 | watchgod celery.__main__.main --args -A config.celery_app worker -l INFO 8 | -------------------------------------------------------------------------------- /compose/local/django/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | 7 | 8 | python manage.py migrate 9 | python manage.py runserver_plus 0.0.0.0:8000 10 | -------------------------------------------------------------------------------- /compose/local/docs/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9-slim-bullseye 2 | 3 | ENV PYTHONUNBUFFERED 1 4 | ENV PYTHONDONTWRITEBYTECODE 1 5 | 6 | RUN apt-get update \ 7 | # dependencies for building Python packages 8 | && apt-get install -y build-essential \ 9 | # psycopg2 dependencies 10 | && apt-get install -y libpq-dev \ 11 | # Translations dependencies 12 | && apt-get install -y gettext \ 13 | # Uncomment below lines to enable Sphinx output to latex and pdf 14 | # && apt-get install -y texlive-latex-recommended \ 15 | # && apt-get install -y texlive-fonts-recommended \ 16 | # && apt-get install -y texlive-latex-extra \ 17 | # && apt-get install -y latexmk \ 18 | # cleaning up unused files 19 | && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ 20 | && rm -rf /var/lib/apt/lists/* 21 | 22 | # Requirements are installed here to ensure they will be cached. 23 | COPY ./requirements /requirements 24 | # All imports needed for autodoc. 25 | RUN pip install -r /requirements/local.txt -r /requirements/production.txt 26 | 27 | COPY ./compose/local/docs/start /start-docs 28 | RUN sed -i 's/\r$//g' /start-docs 29 | RUN chmod +x /start-docs 30 | 31 | WORKDIR /docs 32 | -------------------------------------------------------------------------------- /compose/local/docs/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | 7 | make livehtml 8 | -------------------------------------------------------------------------------- /compose/production/aws/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM garland/aws-cli-docker:1.15.47 2 | 3 | COPY ./compose/production/aws/maintenance /usr/local/bin/maintenance 4 | COPY ./compose/production/postgres/maintenance/_sourced /usr/local/bin/maintenance/_sourced 5 | 6 | RUN chmod +x /usr/local/bin/maintenance/* 7 | 8 | RUN mv /usr/local/bin/maintenance/* /usr/local/bin \ 9 | && rmdir /usr/local/bin/maintenance 10 | -------------------------------------------------------------------------------- /compose/production/aws/maintenance/download: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ### Download a file from your Amazon S3 bucket to the postgres /backups folder 4 | ### 5 | ### Usage: 6 | ### $ docker-compose -f production.yml run --rm awscli <1> 7 | 8 | set -o errexit 9 | set -o pipefail 10 | set -o nounset 11 | 12 | working_dir="$(dirname ${0})" 13 | source "${working_dir}/_sourced/constants.sh" 14 | source "${working_dir}/_sourced/messages.sh" 15 | 16 | export AWS_ACCESS_KEY_ID="${DJANGO_AWS_ACCESS_KEY_ID}" 17 | export AWS_SECRET_ACCESS_KEY="${DJANGO_AWS_SECRET_ACCESS_KEY}" 18 | export AWS_STORAGE_BUCKET_NAME="${DJANGO_AWS_STORAGE_BUCKET_NAME}" 19 | 20 | 21 | aws s3 cp s3://${AWS_STORAGE_BUCKET_NAME}${BACKUP_DIR_PATH}/${1} ${BACKUP_DIR_PATH}/${1} 22 | 23 | message_success "Finished downloading ${1}." 24 | -------------------------------------------------------------------------------- /compose/production/aws/maintenance/upload: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ### Upload the /backups folder to Amazon S3 4 | ### 5 | ### Usage: 6 | ### $ docker-compose -f production.yml run --rm awscli upload 7 | 8 | set -o errexit 9 | set -o pipefail 10 | set -o nounset 11 | 12 | working_dir="$(dirname ${0})" 13 | source "${working_dir}/_sourced/constants.sh" 14 | source "${working_dir}/_sourced/messages.sh" 15 | 16 | export AWS_ACCESS_KEY_ID="${DJANGO_AWS_ACCESS_KEY_ID}" 17 | export AWS_SECRET_ACCESS_KEY="${DJANGO_AWS_SECRET_ACCESS_KEY}" 18 | export AWS_STORAGE_BUCKET_NAME="${DJANGO_AWS_STORAGE_BUCKET_NAME}" 19 | 20 | 21 | message_info "Upload the backups directory to S3 bucket {$AWS_STORAGE_BUCKET_NAME}" 22 | 23 | aws s3 cp ${BACKUP_DIR_PATH} s3://${AWS_STORAGE_BUCKET_NAME}${BACKUP_DIR_PATH} --recursive 24 | 25 | message_info "Cleaning the directory ${BACKUP_DIR_PATH}" 26 | 27 | rm -rf ${BACKUP_DIR_PATH}/* 28 | 29 | message_success "Finished uploading and cleaning." 30 | -------------------------------------------------------------------------------- /compose/production/django/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PYTHON_VERSION=3.9-slim-bullseye 2 | 3 | 4 | 5 | # define an alias for the specfic python version used in this file. 6 | FROM python:${PYTHON_VERSION} as python 7 | 8 | # Python build stage 9 | FROM python as python-build-stage 10 | 11 | ARG BUILD_ENVIRONMENT=production 12 | 13 | # Install apt packages 14 | RUN apt-get update && apt-get install --no-install-recommends -y \ 15 | # dependencies for building Python packages 16 | build-essential \ 17 | # psycopg2 dependencies 18 | libpq-dev 19 | 20 | # Requirements are installed here to ensure they will be cached. 21 | COPY ./requirements . 22 | 23 | # Create Python Dependency and Sub-Dependency Wheels. 24 | RUN pip wheel --wheel-dir /usr/src/app/wheels \ 25 | -r ${BUILD_ENVIRONMENT}.txt 26 | 27 | 28 | # Python 'run' stage 29 | FROM python as python-run-stage 30 | 31 | ARG BUILD_ENVIRONMENT=production 32 | ARG APP_HOME=/app 33 | 34 | ENV PYTHONUNBUFFERED 1 35 | ENV PYTHONDONTWRITEBYTECODE 1 36 | ENV BUILD_ENV ${BUILD_ENVIRONMENT} 37 | 38 | WORKDIR ${APP_HOME} 39 | 40 | RUN addgroup --system django \ 41 | && adduser --system --ingroup django django 42 | 43 | 44 | # Install required system dependencies 45 | RUN apt-get update && apt-get install --no-install-recommends -y \ 46 | # psycopg2 dependencies 47 | libpq-dev \ 48 | # Translations dependencies 49 | gettext \ 50 | # cleaning up unused files 51 | && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ 52 | && rm -rf /var/lib/apt/lists/* 53 | 54 | # All absolute dir copies ignore workdir instruction. All relative dir copies are wrt to the workdir instruction 55 | # copy python dependency wheels from python-build-stage 56 | COPY --from=python-build-stage /usr/src/app/wheels /wheels/ 57 | 58 | # use wheels to install python dependencies 59 | RUN pip install --no-cache-dir --no-index --find-links=/wheels/ /wheels/* \ 60 | && rm -rf /wheels/ 61 | 62 | 63 | COPY --chown=django:django ./compose/production/django/entrypoint /entrypoint 64 | RUN sed -i 's/\r$//g' /entrypoint 65 | RUN chmod +x /entrypoint 66 | 67 | 68 | COPY --chown=django:django ./compose/production/django/start /start 69 | RUN sed -i 's/\r$//g' /start 70 | RUN chmod +x /start 71 | COPY --chown=django:django ./compose/production/django/celery/worker/start /start-celeryworker 72 | RUN sed -i 's/\r$//g' /start-celeryworker 73 | RUN chmod +x /start-celeryworker 74 | 75 | 76 | COPY --chown=django:django ./compose/production/django/celery/beat/start /start-celerybeat 77 | RUN sed -i 's/\r$//g' /start-celerybeat 78 | RUN chmod +x /start-celerybeat 79 | 80 | 81 | COPY ./compose/production/django/celery/flower/start /start-flower 82 | RUN sed -i 's/\r$//g' /start-flower 83 | RUN chmod +x /start-flower 84 | 85 | 86 | # copy application code to WORKDIR 87 | COPY --chown=django:django . ${APP_HOME} 88 | 89 | # make django owner of the WORKDIR directory as well. 90 | RUN chown django:django ${APP_HOME} 91 | 92 | USER django 93 | 94 | ENTRYPOINT ["/entrypoint"] 95 | -------------------------------------------------------------------------------- /compose/production/django/celery/beat/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | 7 | 8 | exec celery -A config.celery_app beat -l INFO 9 | -------------------------------------------------------------------------------- /compose/production/django/celery/flower/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | 7 | exec celery \ 8 | -A config.celery_app \ 9 | -b "${CELERY_BROKER_URL}" \ 10 | flower \ 11 | --basic_auth="${CELERY_FLOWER_USER}:${CELERY_FLOWER_PASSWORD}" 12 | -------------------------------------------------------------------------------- /compose/production/django/celery/worker/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | 7 | 8 | exec celery -A config.celery_app worker -l INFO 9 | -------------------------------------------------------------------------------- /compose/production/django/entrypoint: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | 7 | 8 | 9 | # N.B. If only .env files supported variable expansion... 10 | export CELERY_BROKER_URL="${REDIS_URL}" 11 | 12 | 13 | if [ -z "${POSTGRES_USER}" ]; then 14 | base_postgres_image_default_user='postgres' 15 | export POSTGRES_USER="${base_postgres_image_default_user}" 16 | fi 17 | export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" 18 | 19 | postgres_ready() { 20 | python << END 21 | import sys 22 | 23 | import psycopg2 24 | 25 | try: 26 | psycopg2.connect( 27 | dbname="${POSTGRES_DB}", 28 | user="${POSTGRES_USER}", 29 | password="${POSTGRES_PASSWORD}", 30 | host="${POSTGRES_HOST}", 31 | port="${POSTGRES_PORT}", 32 | ) 33 | except psycopg2.OperationalError: 34 | sys.exit(-1) 35 | sys.exit(0) 36 | 37 | END 38 | } 39 | until postgres_ready; do 40 | >&2 echo 'Waiting for PostgreSQL to become available...' 41 | sleep 1 42 | done 43 | >&2 echo 'PostgreSQL is available' 44 | 45 | exec "$@" 46 | -------------------------------------------------------------------------------- /compose/production/django/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | 7 | 8 | python /app/manage.py collectstatic --noinput 9 | 10 | 11 | /usr/local/bin/gunicorn config.wsgi --bind 0.0.0.0:5000 --chdir=/app 12 | -------------------------------------------------------------------------------- /compose/production/postgres/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres:14.1 2 | 3 | COPY ./compose/production/postgres/maintenance /usr/local/bin/maintenance 4 | RUN chmod +x /usr/local/bin/maintenance/* 5 | RUN mv /usr/local/bin/maintenance/* /usr/local/bin \ 6 | && rmdir /usr/local/bin/maintenance 7 | -------------------------------------------------------------------------------- /compose/production/postgres/maintenance/_sourced/constants.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | BACKUP_DIR_PATH='/backups' 5 | BACKUP_FILE_PREFIX='backup' 6 | -------------------------------------------------------------------------------- /compose/production/postgres/maintenance/_sourced/countdown.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | countdown() { 5 | declare desc="A simple countdown. Source: https://superuser.com/a/611582" 6 | local seconds="${1}" 7 | local d=$(($(date +%s) + "${seconds}")) 8 | while [ "$d" -ge `date +%s` ]; do 9 | echo -ne "$(date -u --date @$(($d - `date +%s`)) +%H:%M:%S)\r"; 10 | sleep 0.1 11 | done 12 | } 13 | -------------------------------------------------------------------------------- /compose/production/postgres/maintenance/_sourced/messages.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | message_newline() { 5 | echo 6 | } 7 | 8 | message_debug() 9 | { 10 | echo -e "DEBUG: ${@}" 11 | } 12 | 13 | message_welcome() 14 | { 15 | echo -e "\e[1m${@}\e[0m" 16 | } 17 | 18 | message_warning() 19 | { 20 | echo -e "\e[33mWARNING\e[0m: ${@}" 21 | } 22 | 23 | message_error() 24 | { 25 | echo -e "\e[31mERROR\e[0m: ${@}" 26 | } 27 | 28 | message_info() 29 | { 30 | echo -e "\e[37mINFO\e[0m: ${@}" 31 | } 32 | 33 | message_suggestion() 34 | { 35 | echo -e "\e[33mSUGGESTION\e[0m: ${@}" 36 | } 37 | 38 | message_success() 39 | { 40 | echo -e "\e[32mSUCCESS\e[0m: ${@}" 41 | } 42 | -------------------------------------------------------------------------------- /compose/production/postgres/maintenance/_sourced/yes_no.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | yes_no() { 5 | declare desc="Prompt for confirmation. \$\"\{1\}\": confirmation message." 6 | local arg1="${1}" 7 | 8 | local response= 9 | read -r -p "${arg1} (y/[n])? " response 10 | if [[ "${response}" =~ ^[Yy]$ ]] 11 | then 12 | exit 0 13 | else 14 | exit 1 15 | fi 16 | } 17 | -------------------------------------------------------------------------------- /compose/production/postgres/maintenance/backup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | ### Create a database backup. 5 | ### 6 | ### Usage: 7 | ### $ docker-compose -f .yml (exec |run --rm) postgres backup 8 | 9 | 10 | set -o errexit 11 | set -o pipefail 12 | set -o nounset 13 | 14 | 15 | working_dir="$(dirname ${0})" 16 | source "${working_dir}/_sourced/constants.sh" 17 | source "${working_dir}/_sourced/messages.sh" 18 | 19 | 20 | message_welcome "Backing up the '${POSTGRES_DB}' database..." 21 | 22 | 23 | if [[ "${POSTGRES_USER}" == "postgres" ]]; then 24 | message_error "Backing up as 'postgres' user is not supported. Assign 'POSTGRES_USER' env with another one and try again." 25 | exit 1 26 | fi 27 | 28 | export PGHOST="${POSTGRES_HOST}" 29 | export PGPORT="${POSTGRES_PORT}" 30 | export PGUSER="${POSTGRES_USER}" 31 | export PGPASSWORD="${POSTGRES_PASSWORD}" 32 | export PGDATABASE="${POSTGRES_DB}" 33 | 34 | backup_filename="${BACKUP_FILE_PREFIX}_$(date +'%Y_%m_%dT%H_%M_%S').sql.gz" 35 | pg_dump | gzip > "${BACKUP_DIR_PATH}/${backup_filename}" 36 | 37 | 38 | message_success "'${POSTGRES_DB}' database backup '${backup_filename}' has been created and placed in '${BACKUP_DIR_PATH}'." 39 | -------------------------------------------------------------------------------- /compose/production/postgres/maintenance/backups: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | ### View backups. 5 | ### 6 | ### Usage: 7 | ### $ docker-compose -f .yml (exec |run --rm) postgres backups 8 | 9 | 10 | set -o errexit 11 | set -o pipefail 12 | set -o nounset 13 | 14 | 15 | working_dir="$(dirname ${0})" 16 | source "${working_dir}/_sourced/constants.sh" 17 | source "${working_dir}/_sourced/messages.sh" 18 | 19 | 20 | message_welcome "These are the backups you have got:" 21 | 22 | ls -lht "${BACKUP_DIR_PATH}" 23 | -------------------------------------------------------------------------------- /compose/production/postgres/maintenance/restore: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | ### Restore database from a backup. 5 | ### 6 | ### Parameters: 7 | ### <1> filename of an existing backup. 8 | ### 9 | ### Usage: 10 | ### $ docker-compose -f .yml (exec |run --rm) postgres restore <1> 11 | 12 | 13 | set -o errexit 14 | set -o pipefail 15 | set -o nounset 16 | 17 | 18 | working_dir="$(dirname ${0})" 19 | source "${working_dir}/_sourced/constants.sh" 20 | source "${working_dir}/_sourced/messages.sh" 21 | 22 | 23 | if [[ -z ${1+x} ]]; then 24 | message_error "Backup filename is not specified yet it is a required parameter. Make sure you provide one and try again." 25 | exit 1 26 | fi 27 | backup_filename="${BACKUP_DIR_PATH}/${1}" 28 | if [[ ! -f "${backup_filename}" ]]; then 29 | message_error "No backup with the specified filename found. Check out the 'backups' maintenance script output to see if there is one and try again." 30 | exit 1 31 | fi 32 | 33 | message_welcome "Restoring the '${POSTGRES_DB}' database from the '${backup_filename}' backup..." 34 | 35 | if [[ "${POSTGRES_USER}" == "postgres" ]]; then 36 | message_error "Restoring as 'postgres' user is not supported. Assign 'POSTGRES_USER' env with another one and try again." 37 | exit 1 38 | fi 39 | 40 | export PGHOST="${POSTGRES_HOST}" 41 | export PGPORT="${POSTGRES_PORT}" 42 | export PGUSER="${POSTGRES_USER}" 43 | export PGPASSWORD="${POSTGRES_PASSWORD}" 44 | export PGDATABASE="${POSTGRES_DB}" 45 | 46 | message_info "Dropping the database..." 47 | dropdb "${PGDATABASE}" 48 | 49 | message_info "Creating a new database..." 50 | createdb --owner="${POSTGRES_USER}" 51 | 52 | message_info "Applying the backup to the new database..." 53 | gunzip -c "${backup_filename}" | psql "${POSTGRES_DB}" 54 | 55 | message_success "The '${POSTGRES_DB}' database has been restored from the '${backup_filename}' backup." 56 | -------------------------------------------------------------------------------- /compose/production/traefik/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM traefik:v2.2.11 2 | RUN mkdir -p /etc/traefik/acme \ 3 | && touch /etc/traefik/acme/acme.json \ 4 | && chmod 600 /etc/traefik/acme/acme.json 5 | COPY ./compose/production/traefik/traefik.yml /etc/traefik 6 | -------------------------------------------------------------------------------- /compose/production/traefik/traefik.yml: -------------------------------------------------------------------------------- 1 | log: 2 | level: INFO 3 | 4 | entryPoints: 5 | web: 6 | # http 7 | address: ":80" 8 | http: 9 | # https://docs.traefik.io/routing/entrypoints/#entrypoint 10 | redirections: 11 | entryPoint: 12 | to: web-secure 13 | 14 | web-secure: 15 | # https 16 | address: ":443" 17 | 18 | flower: 19 | address: ":5555" 20 | 21 | certificatesResolvers: 22 | letsencrypt: 23 | # https://docs.traefik.io/master/https/acme/#lets-encrypt 24 | acme: 25 | email: "matt@justdjango.com" 26 | storage: /etc/traefik/acme/acme.json 27 | # https://docs.traefik.io/master/https/acme/#httpchallenge 28 | httpChallenge: 29 | entryPoint: web 30 | 31 | http: 32 | routers: 33 | web-secure-router: 34 | rule: "Host(`djsniper.com`) || Host(`www.djsniper.com`)" 35 | entryPoints: 36 | - web-secure 37 | middlewares: 38 | - csrf 39 | service: django 40 | tls: 41 | # https://docs.traefik.io/master/routing/routers/#certresolver 42 | certResolver: letsencrypt 43 | 44 | flower-secure-router: 45 | rule: "Host(`djsniper.com`)" 46 | entryPoints: 47 | - flower 48 | service: flower 49 | tls: 50 | # https://docs.traefik.io/master/routing/routers/#certresolver 51 | certResolver: letsencrypt 52 | 53 | middlewares: 54 | csrf: 55 | # https://docs.traefik.io/master/middlewares/headers/#hostsproxyheaders 56 | # https://docs.djangoproject.com/en/dev/ref/csrf/#ajax 57 | headers: 58 | hostsProxyHeaders: ["X-CSRFToken"] 59 | 60 | services: 61 | django: 62 | loadBalancer: 63 | servers: 64 | - url: http://django:5000 65 | 66 | flower: 67 | loadBalancer: 68 | servers: 69 | - url: http://flower:5555 70 | 71 | providers: 72 | # https://docs.traefik.io/master/providers/file/ 73 | file: 74 | filename: /etc/traefik/traefik.yml 75 | watch: true 76 | -------------------------------------------------------------------------------- /config/__init__.py: -------------------------------------------------------------------------------- 1 | # This will make sure the app is always imported when 2 | # Django starts so that shared_task will use this app. 3 | from .celery_app import app as celery_app 4 | 5 | __all__ = ("celery_app",) 6 | -------------------------------------------------------------------------------- /config/api_router.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from rest_framework.routers import DefaultRouter, SimpleRouter 3 | 4 | from djsniper.users.api.views import UserViewSet 5 | 6 | if settings.DEBUG: 7 | router = DefaultRouter() 8 | else: 9 | router = SimpleRouter() 10 | 11 | router.register("users", UserViewSet) 12 | 13 | 14 | app_name = "api" 15 | urlpatterns = router.urls 16 | -------------------------------------------------------------------------------- /config/celery_app.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from celery import Celery 4 | 5 | # set the default Django settings module for the 'celery' program. 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") 7 | 8 | app = Celery("djsniper") 9 | 10 | # Using a string here means the worker doesn't have to serialize 11 | # the configuration object to child processes. 12 | # - namespace='CELERY' means all celery-related configuration keys 13 | # should have a `CELERY_` prefix. 14 | app.config_from_object("django.conf:settings", namespace="CELERY") 15 | 16 | # Load task modules from all registered Django app configs. 17 | app.autodiscover_tasks() 18 | -------------------------------------------------------------------------------- /config/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/config/settings/__init__.py -------------------------------------------------------------------------------- /config/settings/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Base settings to build other settings files upon. 3 | """ 4 | from pathlib import Path 5 | 6 | import environ 7 | 8 | ROOT_DIR = Path(__file__).resolve(strict=True).parent.parent.parent 9 | # djsniper/ 10 | APPS_DIR = ROOT_DIR / "djsniper" 11 | env = environ.Env() 12 | 13 | READ_DOT_ENV_FILE = env.bool("DJANGO_READ_DOT_ENV_FILE", default=False) 14 | if READ_DOT_ENV_FILE: 15 | # OS environment variables take precedence over variables from .env 16 | env.read_env(str(ROOT_DIR / ".env")) 17 | 18 | # GENERAL 19 | # ------------------------------------------------------------------------------ 20 | # https://docs.djangoproject.com/en/dev/ref/settings/#debug 21 | DEBUG = env.bool("DJANGO_DEBUG", False) 22 | # Local time zone. Choices are 23 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 24 | # though not all of them may be available with every OS. 25 | # In Windows, this must be set to your system time zone. 26 | TIME_ZONE = "UTC" 27 | # https://docs.djangoproject.com/en/dev/ref/settings/#language-code 28 | LANGUAGE_CODE = "en-us" 29 | # https://docs.djangoproject.com/en/dev/ref/settings/#site-id 30 | SITE_ID = 1 31 | # https://docs.djangoproject.com/en/dev/ref/settings/#use-i18n 32 | USE_I18N = True 33 | # https://docs.djangoproject.com/en/dev/ref/settings/#use-l10n 34 | USE_L10N = True 35 | # https://docs.djangoproject.com/en/dev/ref/settings/#use-tz 36 | USE_TZ = True 37 | # https://docs.djangoproject.com/en/dev/ref/settings/#locale-paths 38 | LOCALE_PATHS = [str(ROOT_DIR / "locale")] 39 | 40 | # DATABASES 41 | # ------------------------------------------------------------------------------ 42 | # https://docs.djangoproject.com/en/dev/ref/settings/#databases 43 | DATABASES = {"default": env.db("DATABASE_URL")} 44 | DATABASES["default"]["ATOMIC_REQUESTS"] = True 45 | # https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-DEFAULT_AUTO_FIELD 46 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 47 | 48 | # URLS 49 | # ------------------------------------------------------------------------------ 50 | # https://docs.djangoproject.com/en/dev/ref/settings/#root-urlconf 51 | ROOT_URLCONF = "config.urls" 52 | # https://docs.djangoproject.com/en/dev/ref/settings/#wsgi-application 53 | WSGI_APPLICATION = "config.wsgi.application" 54 | 55 | # APPS 56 | # ------------------------------------------------------------------------------ 57 | DJANGO_APPS = [ 58 | "django.contrib.auth", 59 | "django.contrib.contenttypes", 60 | "django.contrib.sessions", 61 | "django.contrib.sites", 62 | "django.contrib.messages", 63 | "django.contrib.staticfiles", 64 | # "django.contrib.humanize", # Handy template tags 65 | "django.contrib.admin", 66 | "django.forms", 67 | ] 68 | THIRD_PARTY_APPS = [ 69 | "crispy_forms", 70 | "crispy_tailwind", 71 | "allauth", 72 | "allauth.account", 73 | "allauth.socialaccount", 74 | "django_celery_beat", 75 | "rest_framework", 76 | "rest_framework.authtoken", 77 | "corsheaders", 78 | ] 79 | 80 | LOCAL_APPS = [ 81 | "djsniper.users", 82 | "djsniper.sniper", 83 | # Your stuff: custom apps go here 84 | ] 85 | # https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps 86 | INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS 87 | 88 | # MIGRATIONS 89 | # ------------------------------------------------------------------------------ 90 | # https://docs.djangoproject.com/en/dev/ref/settings/#migration-modules 91 | MIGRATION_MODULES = {"sites": "djsniper.contrib.sites.migrations"} 92 | 93 | # AUTHENTICATION 94 | # ------------------------------------------------------------------------------ 95 | # https://docs.djangoproject.com/en/dev/ref/settings/#authentication-backends 96 | AUTHENTICATION_BACKENDS = [ 97 | "django.contrib.auth.backends.ModelBackend", 98 | "allauth.account.auth_backends.AuthenticationBackend", 99 | ] 100 | # https://docs.djangoproject.com/en/dev/ref/settings/#auth-user-model 101 | AUTH_USER_MODEL = "users.User" 102 | # https://docs.djangoproject.com/en/dev/ref/settings/#login-redirect-url 103 | LOGIN_REDIRECT_URL = "users:redirect" 104 | # https://docs.djangoproject.com/en/dev/ref/settings/#login-url 105 | LOGIN_URL = "account_login" 106 | 107 | # PASSWORDS 108 | # ------------------------------------------------------------------------------ 109 | # https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers 110 | PASSWORD_HASHERS = [ 111 | # https://docs.djangoproject.com/en/dev/topics/auth/passwords/#using-argon2-with-django 112 | "django.contrib.auth.hashers.Argon2PasswordHasher", 113 | "django.contrib.auth.hashers.PBKDF2PasswordHasher", 114 | "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher", 115 | "django.contrib.auth.hashers.BCryptSHA256PasswordHasher", 116 | ] 117 | # https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators 118 | AUTH_PASSWORD_VALIDATORS = [ 119 | { 120 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" 121 | }, 122 | {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, 123 | {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, 124 | {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, 125 | ] 126 | 127 | # MIDDLEWARE 128 | # ------------------------------------------------------------------------------ 129 | # https://docs.djangoproject.com/en/dev/ref/settings/#middleware 130 | MIDDLEWARE = [ 131 | "django.middleware.security.SecurityMiddleware", 132 | "corsheaders.middleware.CorsMiddleware", 133 | "django.contrib.sessions.middleware.SessionMiddleware", 134 | "django.middleware.locale.LocaleMiddleware", 135 | "django.middleware.common.CommonMiddleware", 136 | "django.middleware.csrf.CsrfViewMiddleware", 137 | "django.contrib.auth.middleware.AuthenticationMiddleware", 138 | "django.contrib.messages.middleware.MessageMiddleware", 139 | "django.middleware.common.BrokenLinkEmailsMiddleware", 140 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 141 | ] 142 | 143 | # STATIC 144 | # ------------------------------------------------------------------------------ 145 | # https://docs.djangoproject.com/en/dev/ref/settings/#static-root 146 | STATIC_ROOT = str(ROOT_DIR / "staticfiles") 147 | # https://docs.djangoproject.com/en/dev/ref/settings/#static-url 148 | STATIC_URL = "/static/" 149 | # https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS 150 | STATICFILES_DIRS = [str(APPS_DIR / "static")] 151 | # https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders 152 | STATICFILES_FINDERS = [ 153 | "django.contrib.staticfiles.finders.FileSystemFinder", 154 | "django.contrib.staticfiles.finders.AppDirectoriesFinder", 155 | ] 156 | 157 | # MEDIA 158 | # ------------------------------------------------------------------------------ 159 | # https://docs.djangoproject.com/en/dev/ref/settings/#media-root 160 | MEDIA_ROOT = str(APPS_DIR / "media") 161 | # https://docs.djangoproject.com/en/dev/ref/settings/#media-url 162 | MEDIA_URL = "/media/" 163 | 164 | # TEMPLATES 165 | # ------------------------------------------------------------------------------ 166 | # https://docs.djangoproject.com/en/dev/ref/settings/#templates 167 | TEMPLATES = [ 168 | { 169 | # https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-TEMPLATES-BACKEND 170 | "BACKEND": "django.template.backends.django.DjangoTemplates", 171 | # https://docs.djangoproject.com/en/dev/ref/settings/#template-dirs 172 | "DIRS": [str(APPS_DIR / "templates")], 173 | "OPTIONS": { 174 | # https://docs.djangoproject.com/en/dev/ref/settings/#template-loaders 175 | # https://docs.djangoproject.com/en/dev/ref/templates/api/#loader-types 176 | "loaders": [ 177 | "django.template.loaders.filesystem.Loader", 178 | "django.template.loaders.app_directories.Loader", 179 | ], 180 | # https://docs.djangoproject.com/en/dev/ref/settings/#template-context-processors 181 | "context_processors": [ 182 | "django.template.context_processors.debug", 183 | "django.template.context_processors.request", 184 | "django.contrib.auth.context_processors.auth", 185 | "django.template.context_processors.i18n", 186 | "django.template.context_processors.media", 187 | "django.template.context_processors.static", 188 | "django.template.context_processors.tz", 189 | "django.contrib.messages.context_processors.messages", 190 | ], 191 | }, 192 | } 193 | ] 194 | 195 | # https://docs.djangoproject.com/en/dev/ref/settings/#form-renderer 196 | FORM_RENDERER = "django.forms.renderers.TemplatesSetting" 197 | 198 | # http://django-crispy-forms.readthedocs.io/en/latest/install.html#template-packs 199 | CRISPY_ALLOWED_TEMPLATE_PACKS = "tailwind" 200 | CRISPY_TEMPLATE_PACK = "tailwind" 201 | 202 | # FIXTURES 203 | # ------------------------------------------------------------------------------ 204 | # https://docs.djangoproject.com/en/dev/ref/settings/#fixture-dirs 205 | FIXTURE_DIRS = (str(APPS_DIR / "fixtures"),) 206 | 207 | # SECURITY 208 | # ------------------------------------------------------------------------------ 209 | # https://docs.djangoproject.com/en/dev/ref/settings/#session-cookie-httponly 210 | SESSION_COOKIE_HTTPONLY = True 211 | # https://docs.djangoproject.com/en/dev/ref/settings/#csrf-cookie-httponly 212 | CSRF_COOKIE_HTTPONLY = True 213 | # https://docs.djangoproject.com/en/dev/ref/settings/#secure-browser-xss-filter 214 | SECURE_BROWSER_XSS_FILTER = True 215 | # https://docs.djangoproject.com/en/dev/ref/settings/#x-frame-options 216 | X_FRAME_OPTIONS = "DENY" 217 | 218 | # EMAIL 219 | # ------------------------------------------------------------------------------ 220 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend 221 | EMAIL_BACKEND = env( 222 | "DJANGO_EMAIL_BACKEND", 223 | default="django.core.mail.backends.smtp.EmailBackend", 224 | ) 225 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-timeout 226 | EMAIL_TIMEOUT = 5 227 | 228 | # ADMIN 229 | # ------------------------------------------------------------------------------ 230 | # Django Admin URL. 231 | ADMIN_URL = "admin/" 232 | # https://docs.djangoproject.com/en/dev/ref/settings/#admins 233 | ADMINS = [("""Matthew Freire""", "matt@justdjango.com")] 234 | # https://docs.djangoproject.com/en/dev/ref/settings/#managers 235 | MANAGERS = ADMINS 236 | 237 | # LOGGING 238 | # ------------------------------------------------------------------------------ 239 | # https://docs.djangoproject.com/en/dev/ref/settings/#logging 240 | # See https://docs.djangoproject.com/en/dev/topics/logging for 241 | # more details on how to customize your logging configuration. 242 | LOGGING = { 243 | "version": 1, 244 | "disable_existing_loggers": False, 245 | "formatters": { 246 | "verbose": { 247 | "format": "%(levelname)s %(asctime)s %(module)s " 248 | "%(process)d %(thread)d %(message)s" 249 | } 250 | }, 251 | "handlers": { 252 | "console": { 253 | "level": "DEBUG", 254 | "class": "logging.StreamHandler", 255 | "formatter": "verbose", 256 | } 257 | }, 258 | "root": {"level": "INFO", "handlers": ["console"]}, 259 | } 260 | 261 | # Celery 262 | # ------------------------------------------------------------------------------ 263 | if USE_TZ: 264 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-timezone 265 | CELERY_TIMEZONE = TIME_ZONE 266 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-broker_url 267 | CELERY_BROKER_URL = env("CELERY_BROKER_URL") 268 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-result_backend 269 | CELERY_RESULT_BACKEND = CELERY_BROKER_URL 270 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-accept_content 271 | CELERY_ACCEPT_CONTENT = ["json"] 272 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-task_serializer 273 | CELERY_TASK_SERIALIZER = "json" 274 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-result_serializer 275 | CELERY_RESULT_SERIALIZER = "json" 276 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#task-time-limit 277 | # TODO: set to whatever value is adequate in your circumstances 278 | CELERY_TASK_TIME_LIMIT = 60 * 600 279 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#task-soft-time-limit 280 | # TODO: set to whatever value is adequate in your circumstances 281 | CELERY_TASK_SOFT_TIME_LIMIT = 60 * 600 282 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#beat-scheduler 283 | CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler" 284 | # django-allauth 285 | # ------------------------------------------------------------------------------ 286 | ACCOUNT_ALLOW_REGISTRATION = env.bool("DJANGO_ACCOUNT_ALLOW_REGISTRATION", True) 287 | # https://django-allauth.readthedocs.io/en/latest/configuration.html 288 | ACCOUNT_AUTHENTICATION_METHOD = "username" 289 | # https://django-allauth.readthedocs.io/en/latest/configuration.html 290 | ACCOUNT_EMAIL_REQUIRED = True 291 | # https://django-allauth.readthedocs.io/en/latest/configuration.html 292 | ACCOUNT_EMAIL_VERIFICATION = "mandatory" 293 | # https://django-allauth.readthedocs.io/en/latest/configuration.html 294 | ACCOUNT_ADAPTER = "djsniper.users.adapters.AccountAdapter" 295 | # https://django-allauth.readthedocs.io/en/latest/configuration.html 296 | SOCIALACCOUNT_ADAPTER = "djsniper.users.adapters.SocialAccountAdapter" 297 | 298 | # django-rest-framework 299 | # ------------------------------------------------------------------------------- 300 | # django-rest-framework - https://www.django-rest-framework.org/api-guide/settings/ 301 | REST_FRAMEWORK = { 302 | "DEFAULT_AUTHENTICATION_CLASSES": ( 303 | "rest_framework.authentication.SessionAuthentication", 304 | "rest_framework.authentication.TokenAuthentication", 305 | ), 306 | "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",), 307 | } 308 | 309 | # django-cors-headers - https://github.com/adamchainz/django-cors-headers#setup 310 | CORS_URLS_REGEX = r"^/api/.*$" 311 | # Your stuff... 312 | # ------------------------------------------------------------------------------ 313 | -------------------------------------------------------------------------------- /config/settings/local.py: -------------------------------------------------------------------------------- 1 | from .base import * # noqa 2 | from .base import env 3 | 4 | # GENERAL 5 | # ------------------------------------------------------------------------------ 6 | # https://docs.djangoproject.com/en/dev/ref/settings/#debug 7 | DEBUG = True 8 | # https://docs.djangoproject.com/en/dev/ref/settings/#secret-key 9 | SECRET_KEY = env( 10 | "DJANGO_SECRET_KEY", 11 | default="IxQhgTQIalHwa03m58IdEEC3QqK7WwlgVhUxfJptNQAQUVvnwdCjlwXLaOoTcY31", 12 | ) 13 | # https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts 14 | ALLOWED_HOSTS = ["localhost", "0.0.0.0", "127.0.0.1"] 15 | 16 | # CACHES 17 | # ------------------------------------------------------------------------------ 18 | # https://docs.djangoproject.com/en/dev/ref/settings/#caches 19 | CACHES = { 20 | "default": { 21 | "BACKEND": "django.core.cache.backends.locmem.LocMemCache", 22 | "LOCATION": "", 23 | } 24 | } 25 | 26 | # EMAIL 27 | # ------------------------------------------------------------------------------ 28 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend 29 | EMAIL_BACKEND = env( 30 | "DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.console.EmailBackend" 31 | ) 32 | 33 | # django-debug-toolbar 34 | # ------------------------------------------------------------------------------ 35 | # https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#prerequisites 36 | INSTALLED_APPS += ["debug_toolbar"] # noqa F405 37 | # https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#middleware 38 | MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"] # noqa F405 39 | # https://django-debug-toolbar.readthedocs.io/en/latest/configuration.html#debug-toolbar-config 40 | DEBUG_TOOLBAR_CONFIG = { 41 | "DISABLE_PANELS": ["debug_toolbar.panels.redirects.RedirectsPanel"], 42 | "SHOW_TEMPLATE_CONTEXT": True, 43 | } 44 | # https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#internal-ips 45 | INTERNAL_IPS = ["127.0.0.1", "10.0.2.2"] 46 | if env("USE_DOCKER") == "yes": 47 | import socket 48 | 49 | hostname, _, ips = socket.gethostbyname_ex(socket.gethostname()) 50 | INTERNAL_IPS += [".".join(ip.split(".")[:-1] + ["1"]) for ip in ips] 51 | 52 | # django-extensions 53 | # ------------------------------------------------------------------------------ 54 | # https://django-extensions.readthedocs.io/en/latest/installation_instructions.html#configuration 55 | INSTALLED_APPS += ["django_extensions"] # noqa F405 56 | # Celery 57 | # ------------------------------------------------------------------------------ 58 | 59 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#task-eager-propagates 60 | CELERY_TASK_EAGER_PROPAGATES = True 61 | # Your stuff... 62 | # ------------------------------------------------------------------------------ 63 | -------------------------------------------------------------------------------- /config/settings/production.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import sentry_sdk 4 | from sentry_sdk.integrations.django import DjangoIntegration 5 | from sentry_sdk.integrations.logging import LoggingIntegration 6 | from sentry_sdk.integrations.celery import CeleryIntegration 7 | 8 | from sentry_sdk.integrations.redis import RedisIntegration 9 | 10 | from .base import * # noqa 11 | from .base import env 12 | 13 | # GENERAL 14 | # ------------------------------------------------------------------------------ 15 | # https://docs.djangoproject.com/en/dev/ref/settings/#secret-key 16 | SECRET_KEY = env("DJANGO_SECRET_KEY") 17 | # https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts 18 | ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["djsniper.com"]) 19 | 20 | # DATABASES 21 | # ------------------------------------------------------------------------------ 22 | DATABASES["default"] = env.db("DATABASE_URL") # noqa F405 23 | DATABASES["default"]["ATOMIC_REQUESTS"] = True # noqa F405 24 | DATABASES["default"]["CONN_MAX_AGE"] = env.int("CONN_MAX_AGE", default=60) # noqa F405 25 | 26 | # CACHES 27 | # ------------------------------------------------------------------------------ 28 | CACHES = { 29 | "default": { 30 | "BACKEND": "django_redis.cache.RedisCache", 31 | "LOCATION": env("REDIS_URL"), 32 | "OPTIONS": { 33 | "CLIENT_CLASS": "django_redis.client.DefaultClient", 34 | # Mimicing memcache behavior. 35 | # https://github.com/jazzband/django-redis#memcached-exceptions-behavior 36 | "IGNORE_EXCEPTIONS": True, 37 | }, 38 | } 39 | } 40 | 41 | # SECURITY 42 | # ------------------------------------------------------------------------------ 43 | # https://docs.djangoproject.com/en/dev/ref/settings/#secure-proxy-ssl-header 44 | SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") 45 | # https://docs.djangoproject.com/en/dev/ref/settings/#secure-ssl-redirect 46 | SECURE_SSL_REDIRECT = env.bool("DJANGO_SECURE_SSL_REDIRECT", default=True) 47 | # https://docs.djangoproject.com/en/dev/ref/settings/#session-cookie-secure 48 | SESSION_COOKIE_SECURE = True 49 | # https://docs.djangoproject.com/en/dev/ref/settings/#csrf-cookie-secure 50 | CSRF_COOKIE_SECURE = True 51 | # https://docs.djangoproject.com/en/dev/topics/security/#ssl-https 52 | # https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-seconds 53 | # TODO: set this to 60 seconds first and then to 518400 once you prove the former works 54 | SECURE_HSTS_SECONDS = 60 55 | # https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-include-subdomains 56 | SECURE_HSTS_INCLUDE_SUBDOMAINS = env.bool( 57 | "DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS", default=True 58 | ) 59 | # https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-preload 60 | SECURE_HSTS_PRELOAD = env.bool("DJANGO_SECURE_HSTS_PRELOAD", default=True) 61 | # https://docs.djangoproject.com/en/dev/ref/middleware/#x-content-type-options-nosniff 62 | SECURE_CONTENT_TYPE_NOSNIFF = env.bool( 63 | "DJANGO_SECURE_CONTENT_TYPE_NOSNIFF", default=True 64 | ) 65 | 66 | # STORAGES 67 | # ------------------------------------------------------------------------------ 68 | # https://django-storages.readthedocs.io/en/latest/#installation 69 | INSTALLED_APPS += ["storages"] # noqa F405 70 | # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings 71 | AWS_ACCESS_KEY_ID = env("DJANGO_AWS_ACCESS_KEY_ID") 72 | # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings 73 | AWS_SECRET_ACCESS_KEY = env("DJANGO_AWS_SECRET_ACCESS_KEY") 74 | # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings 75 | AWS_STORAGE_BUCKET_NAME = env("DJANGO_AWS_STORAGE_BUCKET_NAME") 76 | # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings 77 | AWS_QUERYSTRING_AUTH = False 78 | # DO NOT change these unless you know what you're doing. 79 | _AWS_EXPIRY = 60 * 60 * 24 * 7 80 | # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings 81 | AWS_S3_OBJECT_PARAMETERS = { 82 | "CacheControl": f"max-age={_AWS_EXPIRY}, s-maxage={_AWS_EXPIRY}, must-revalidate" 83 | } 84 | # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings 85 | AWS_S3_REGION_NAME = env("DJANGO_AWS_S3_REGION_NAME", default=None) 86 | # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#cloudfront 87 | AWS_S3_CUSTOM_DOMAIN = env("DJANGO_AWS_S3_CUSTOM_DOMAIN", default=None) 88 | aws_s3_domain = AWS_S3_CUSTOM_DOMAIN or f"{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com" 89 | # STATIC 90 | # ------------------------ 91 | STATICFILES_STORAGE = "djsniper.utils.storages.StaticRootS3Boto3Storage" 92 | COLLECTFAST_STRATEGY = "collectfast.strategies.boto3.Boto3Strategy" 93 | STATIC_URL = f"https://{aws_s3_domain}/static/" 94 | # MEDIA 95 | # ------------------------------------------------------------------------------ 96 | DEFAULT_FILE_STORAGE = "djsniper.utils.storages.MediaRootS3Boto3Storage" 97 | MEDIA_URL = f"https://{aws_s3_domain}/media/" 98 | 99 | # TEMPLATES 100 | # ------------------------------------------------------------------------------ 101 | # https://docs.djangoproject.com/en/dev/ref/settings/#templates 102 | TEMPLATES[-1]["OPTIONS"]["loaders"] = [ # type: ignore[index] # noqa F405 103 | ( 104 | "django.template.loaders.cached.Loader", 105 | [ 106 | "django.template.loaders.filesystem.Loader", 107 | "django.template.loaders.app_directories.Loader", 108 | ], 109 | ) 110 | ] 111 | 112 | # EMAIL 113 | # ------------------------------------------------------------------------------ 114 | # https://docs.djangoproject.com/en/dev/ref/settings/#default-from-email 115 | DEFAULT_FROM_EMAIL = env( 116 | "DJANGO_DEFAULT_FROM_EMAIL", default="djsniper " 117 | ) 118 | # https://docs.djangoproject.com/en/dev/ref/settings/#server-email 119 | SERVER_EMAIL = env("DJANGO_SERVER_EMAIL", default=DEFAULT_FROM_EMAIL) 120 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-subject-prefix 121 | EMAIL_SUBJECT_PREFIX = env( 122 | "DJANGO_EMAIL_SUBJECT_PREFIX", 123 | default="[djsniper]", 124 | ) 125 | 126 | # ADMIN 127 | # ------------------------------------------------------------------------------ 128 | # Django Admin URL regex. 129 | ADMIN_URL = env("DJANGO_ADMIN_URL") 130 | 131 | # Anymail 132 | # ------------------------------------------------------------------------------ 133 | # https://anymail.readthedocs.io/en/stable/installation/#installing-anymail 134 | INSTALLED_APPS += ["anymail"] # noqa F405 135 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend 136 | # https://anymail.readthedocs.io/en/stable/installation/#anymail-settings-reference 137 | # https://anymail.readthedocs.io/en/stable/esps/mailgun/ 138 | EMAIL_BACKEND = "anymail.backends.mailgun.EmailBackend" 139 | ANYMAIL = { 140 | "MAILGUN_API_KEY": env("MAILGUN_API_KEY"), 141 | "MAILGUN_SENDER_DOMAIN": env("MAILGUN_DOMAIN"), 142 | "MAILGUN_API_URL": env("MAILGUN_API_URL", default="https://api.mailgun.net/v3"), 143 | } 144 | 145 | # Collectfast 146 | # ------------------------------------------------------------------------------ 147 | # https://github.com/antonagestam/collectfast#installation 148 | INSTALLED_APPS = ["collectfast"] + INSTALLED_APPS # noqa F405 149 | 150 | # LOGGING 151 | # ------------------------------------------------------------------------------ 152 | # https://docs.djangoproject.com/en/dev/ref/settings/#logging 153 | # See https://docs.djangoproject.com/en/dev/topics/logging for 154 | # more details on how to customize your logging configuration. 155 | 156 | LOGGING = { 157 | "version": 1, 158 | "disable_existing_loggers": True, 159 | "formatters": { 160 | "verbose": { 161 | "format": "%(levelname)s %(asctime)s %(module)s " 162 | "%(process)d %(thread)d %(message)s" 163 | } 164 | }, 165 | "handlers": { 166 | "console": { 167 | "level": "DEBUG", 168 | "class": "logging.StreamHandler", 169 | "formatter": "verbose", 170 | } 171 | }, 172 | "root": {"level": "INFO", "handlers": ["console"]}, 173 | "loggers": { 174 | "django.db.backends": { 175 | "level": "ERROR", 176 | "handlers": ["console"], 177 | "propagate": False, 178 | }, 179 | # Errors logged by the SDK itself 180 | "sentry_sdk": {"level": "ERROR", "handlers": ["console"], "propagate": False}, 181 | "django.security.DisallowedHost": { 182 | "level": "ERROR", 183 | "handlers": ["console"], 184 | "propagate": False, 185 | }, 186 | }, 187 | } 188 | 189 | # Sentry 190 | # ------------------------------------------------------------------------------ 191 | SENTRY_DSN = env("SENTRY_DSN") 192 | SENTRY_LOG_LEVEL = env.int("DJANGO_SENTRY_LOG_LEVEL", logging.INFO) 193 | 194 | sentry_logging = LoggingIntegration( 195 | level=SENTRY_LOG_LEVEL, # Capture info and above as breadcrumbs 196 | event_level=logging.ERROR, # Send errors as events 197 | ) 198 | integrations = [ 199 | sentry_logging, 200 | DjangoIntegration(), 201 | CeleryIntegration(), 202 | RedisIntegration(), 203 | ] 204 | sentry_sdk.init( 205 | dsn=SENTRY_DSN, 206 | integrations=integrations, 207 | environment=env("SENTRY_ENVIRONMENT", default="production"), 208 | traces_sample_rate=env.float("SENTRY_TRACES_SAMPLE_RATE", default=0.0), 209 | ) 210 | 211 | # Your stuff... 212 | # ------------------------------------------------------------------------------ 213 | -------------------------------------------------------------------------------- /config/settings/test.py: -------------------------------------------------------------------------------- 1 | """ 2 | With these settings, tests run faster. 3 | """ 4 | 5 | from .base import * # noqa 6 | from .base import env 7 | 8 | # GENERAL 9 | # ------------------------------------------------------------------------------ 10 | # https://docs.djangoproject.com/en/dev/ref/settings/#secret-key 11 | SECRET_KEY = env( 12 | "DJANGO_SECRET_KEY", 13 | default="uVzhhe9QPvSFos0NZuiTyTnn3S6HCWypbNeaHy0NHAHUMaGmCdnt4Jjvw3DzeWwK", 14 | ) 15 | # https://docs.djangoproject.com/en/dev/ref/settings/#test-runner 16 | TEST_RUNNER = "django.test.runner.DiscoverRunner" 17 | 18 | # PASSWORDS 19 | # ------------------------------------------------------------------------------ 20 | # https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers 21 | PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"] 22 | 23 | # TEMPLATES 24 | # ------------------------------------------------------------------------------ 25 | TEMPLATES[-1]["OPTIONS"]["loaders"] = [ # type: ignore[index] # noqa F405 26 | ( 27 | "django.template.loaders.cached.Loader", 28 | [ 29 | "django.template.loaders.filesystem.Loader", 30 | "django.template.loaders.app_directories.Loader", 31 | ], 32 | ) 33 | ] 34 | 35 | # EMAIL 36 | # ------------------------------------------------------------------------------ 37 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend 38 | EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" 39 | 40 | # Your stuff... 41 | # ------------------------------------------------------------------------------ 42 | -------------------------------------------------------------------------------- /config/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.conf.urls.static import static 3 | from django.contrib import admin 4 | from django.urls import include, path 5 | from django.views import defaults as default_views 6 | from django.views.generic import TemplateView 7 | from rest_framework.authtoken.views import obtain_auth_token 8 | 9 | 10 | urlpatterns = [ 11 | path("", include("djsniper.sniper.urls", namespace="sniper")), 12 | path( 13 | "about/", TemplateView.as_view(template_name="pages/about.html"), name="about" 14 | ), 15 | # Django Admin, use {% url 'admin:index' %} 16 | path(settings.ADMIN_URL, admin.site.urls), 17 | # User management 18 | path("users/", include("djsniper.users.urls", namespace="users")), 19 | path("accounts/", include("allauth.urls")), 20 | path("celery-progress/", include("celery_progress.urls")), 21 | # Your stuff: custom urls includes go here 22 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 23 | 24 | # API URLS 25 | urlpatterns += [ 26 | # API base url 27 | path("api/", include("config.api_router")), 28 | # DRF auth token 29 | path("auth-token/", obtain_auth_token), 30 | ] 31 | 32 | if settings.DEBUG: 33 | # This allows the error pages to be debugged during development, just visit 34 | # these url in browser to see how these error pages look like. 35 | urlpatterns += [ 36 | path( 37 | "400/", 38 | default_views.bad_request, 39 | kwargs={"exception": Exception("Bad Request!")}, 40 | ), 41 | path( 42 | "403/", 43 | default_views.permission_denied, 44 | kwargs={"exception": Exception("Permission Denied")}, 45 | ), 46 | path( 47 | "404/", 48 | default_views.page_not_found, 49 | kwargs={"exception": Exception("Page not Found")}, 50 | ), 51 | path("500/", default_views.server_error), 52 | ] 53 | if "debug_toolbar" in settings.INSTALLED_APPS: 54 | import debug_toolbar 55 | 56 | urlpatterns = [path("__debug__/", include(debug_toolbar.urls))] + urlpatterns 57 | -------------------------------------------------------------------------------- /config/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for djsniper project. 3 | 4 | This module contains the WSGI application used by Django's development server 5 | and any production WSGI deployments. It should expose a module-level variable 6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 7 | this application via the ``WSGI_APPLICATION`` setting. 8 | 9 | Usually you will have the standard Django WSGI application here, but it also 10 | might make sense to replace the whole Django WSGI application with a custom one 11 | that later delegates to the Django one. For example, you could introduce WSGI 12 | middleware here, or combine a Django application with an application of another 13 | framework. 14 | 15 | """ 16 | import os 17 | import sys 18 | from pathlib import Path 19 | 20 | from django.core.wsgi import get_wsgi_application 21 | 22 | # This allows easy placement of apps within the interior 23 | # djsniper directory. 24 | ROOT_DIR = Path(__file__).resolve(strict=True).parent.parent 25 | sys.path.append(str(ROOT_DIR / "djsniper")) 26 | # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks 27 | # if running multiple sites in the same mod_wsgi process. To fix this, use 28 | # mod_wsgi daemon mode with each site in its own daemon process, or use 29 | # os.environ["DJANGO_SETTINGS_MODULE"] = "config.settings.production" 30 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production") 31 | 32 | # This application object is used by any WSGI server configured to use this 33 | # file. This includes Django's development server, if the WSGI_APPLICATION 34 | # setting points here. 35 | application = get_wsgi_application() 36 | # Apply WSGI middleware here. 37 | # from helloworld.wsgi import HelloWorldApplication 38 | # application = HelloWorldApplication(application) 39 | -------------------------------------------------------------------------------- /djsniper/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.0" 2 | __version_info__ = tuple( 3 | [ 4 | int(num) if num.isdigit() else num 5 | for num in __version__.replace("-", ".", 1).split(".") 6 | ] 7 | ) 8 | -------------------------------------------------------------------------------- /djsniper/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from djsniper.users.models import User 4 | from djsniper.users.tests.factories import UserFactory 5 | 6 | 7 | @pytest.fixture(autouse=True) 8 | def media_storage(settings, tmpdir): 9 | settings.MEDIA_ROOT = tmpdir.strpath 10 | 11 | 12 | @pytest.fixture 13 | def user() -> User: 14 | return UserFactory() 15 | -------------------------------------------------------------------------------- /djsniper/contrib/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | To understand why this file is here, please read: 3 | 4 | http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django 5 | """ 6 | -------------------------------------------------------------------------------- /djsniper/contrib/sites/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | To understand why this file is here, please read: 3 | 4 | http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django 5 | """ 6 | -------------------------------------------------------------------------------- /djsniper/contrib/sites/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | import django.contrib.sites.models 2 | from django.contrib.sites.models import _simple_domain_name_validator 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [] 9 | 10 | operations = [ 11 | migrations.CreateModel( 12 | name="Site", 13 | fields=[ 14 | ( 15 | "id", 16 | models.AutoField( 17 | verbose_name="ID", 18 | serialize=False, 19 | auto_created=True, 20 | primary_key=True, 21 | ), 22 | ), 23 | ( 24 | "domain", 25 | models.CharField( 26 | max_length=100, 27 | verbose_name="domain name", 28 | validators=[_simple_domain_name_validator], 29 | ), 30 | ), 31 | ("name", models.CharField(max_length=50, verbose_name="display name")), 32 | ], 33 | options={ 34 | "ordering": ("domain",), 35 | "db_table": "django_site", 36 | "verbose_name": "site", 37 | "verbose_name_plural": "sites", 38 | }, 39 | bases=(models.Model,), 40 | managers=[("objects", django.contrib.sites.models.SiteManager())], 41 | ) 42 | ] 43 | -------------------------------------------------------------------------------- /djsniper/contrib/sites/migrations/0002_alter_domain_unique.py: -------------------------------------------------------------------------------- 1 | import django.contrib.sites.models 2 | from django.db import migrations, models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [("sites", "0001_initial")] 8 | 9 | operations = [ 10 | migrations.AlterField( 11 | model_name="site", 12 | name="domain", 13 | field=models.CharField( 14 | max_length=100, 15 | unique=True, 16 | validators=[django.contrib.sites.models._simple_domain_name_validator], 17 | verbose_name="domain name", 18 | ), 19 | ) 20 | ] 21 | -------------------------------------------------------------------------------- /djsniper/contrib/sites/migrations/0003_set_site_domain_and_name.py: -------------------------------------------------------------------------------- 1 | """ 2 | To understand why this file is here, please read: 3 | 4 | http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django 5 | """ 6 | from django.conf import settings 7 | from django.db import migrations 8 | 9 | 10 | def update_site_forward(apps, schema_editor): 11 | """Set site domain and name.""" 12 | Site = apps.get_model("sites", "Site") 13 | Site.objects.update_or_create( 14 | id=settings.SITE_ID, 15 | defaults={ 16 | "domain": "djsniper.com", 17 | "name": "djsniper", 18 | }, 19 | ) 20 | 21 | 22 | def update_site_backward(apps, schema_editor): 23 | """Revert site domain and name to default.""" 24 | Site = apps.get_model("sites", "Site") 25 | Site.objects.update_or_create( 26 | id=settings.SITE_ID, defaults={"domain": "example.com", "name": "example.com"} 27 | ) 28 | 29 | 30 | class Migration(migrations.Migration): 31 | 32 | dependencies = [("sites", "0002_alter_domain_unique")] 33 | 34 | operations = [migrations.RunPython(update_site_forward, update_site_backward)] 35 | -------------------------------------------------------------------------------- /djsniper/contrib/sites/migrations/0004_alter_options_ordering_domain.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.7 on 2021-02-04 14:49 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("sites", "0003_set_site_domain_and_name"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name="site", 15 | options={ 16 | "ordering": ["domain"], 17 | "verbose_name": "site", 18 | "verbose_name_plural": "sites", 19 | }, 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /djsniper/contrib/sites/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | To understand why this file is here, please read: 3 | 4 | http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django 5 | """ 6 | -------------------------------------------------------------------------------- /djsniper/sniper/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/djsniper/sniper/__init__.py -------------------------------------------------------------------------------- /djsniper/sniper/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import NFTProject, NFT, NFTTrait, NFTAttribute 3 | 4 | 5 | class NFTAdmin(admin.ModelAdmin): 6 | list_display = ["nft_id", "rank", "rarity_score"] 7 | search_fields = ["nft_id__exact"] 8 | 9 | 10 | class NFTAttributeAdmin(admin.ModelAdmin): 11 | list_display = ["name", "value"] 12 | list_filter = ["name"] 13 | 14 | 15 | admin.site.register(NFTProject) 16 | admin.site.register(NFTTrait) 17 | admin.site.register(NFT, NFTAdmin) 18 | admin.site.register(NFTAttribute, NFTAttributeAdmin) 19 | -------------------------------------------------------------------------------- /djsniper/sniper/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class SniperConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "djsniper.sniper" 7 | -------------------------------------------------------------------------------- /djsniper/sniper/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from .models import NFTProject 3 | 4 | 5 | class ProjectForm(forms.ModelForm): 6 | class Meta: 7 | model = NFTProject 8 | fields = ("name", "contract_address", "contract_abi", "number_of_nfts") 9 | 10 | 11 | class ConfirmForm(forms.Form): 12 | hidden = forms.HiddenInput() 13 | -------------------------------------------------------------------------------- /djsniper/sniper/management/commands/fetch_nfts.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | import requests 3 | from web3.main import Web3 4 | from djsniper.sniper.models import NFTProject, NFT, NFTAttribute, NFTTrait 5 | 6 | INFURA_PROJECT_ID = "" 7 | INFURA_ENDPOINT = f"https://mainnet.infura.io/v3/{INFURA_PROJECT_ID}" 8 | 9 | 10 | class Command(BaseCommand): 11 | def handle(self, *args, **options): 12 | self.fetch_nfts(1) 13 | 14 | def fetch_nfts(self, project_id): 15 | project = NFTProject.objects.get(id=project_id) 16 | 17 | w3 = Web3(Web3.HTTPProvider(INFURA_ENDPOINT)) 18 | contract_instance = w3.eth.contract( 19 | address=project.contract_address, abi=project.contract_abi 20 | ) 21 | 22 | # Hardcoding only 10 NFTs otherwise it takes long 23 | for i in range(0, 10): 24 | ipfs_uri = contract_instance.functions.tokenURI(i).call() 25 | data = requests.get( 26 | f"https://ipfs.io/ipfs/{ipfs_uri.split('ipfs://')[1]}" 27 | ).json() 28 | nft = NFT.objects.create( 29 | nft_id=i, project=project, image_url=data["image"].split("ipfs://")[1] 30 | ) 31 | attributes = data["attributes"] 32 | for attribute in attributes: 33 | nft_attribute, created = NFTAttribute.objects.get_or_create( 34 | project=project, 35 | name=attribute["trait_type"], 36 | value=attribute["value"], 37 | ) 38 | NFTTrait.objects.create(nft=nft, attribute=nft_attribute) 39 | -------------------------------------------------------------------------------- /djsniper/sniper/management/commands/rank_nfts.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from django.db.models import OuterRef, Func, Subquery 3 | from djsniper.sniper.models import NFTProject, NFTAttribute, NFTTrait 4 | 5 | 6 | class Command(BaseCommand): 7 | def handle(self, *args, **options): 8 | self.rank_nfts(1) 9 | 10 | def rank_nfts(self, project_id): 11 | project = NFTProject.objects.get(id=project_id) 12 | 13 | # calculate sum of NFT trait types 14 | trait_count_subquery = ( 15 | NFTTrait.objects.filter(attribute=OuterRef("id")) 16 | .order_by() 17 | .annotate(count=Func("id", function="Count")) 18 | .values("count") 19 | ) 20 | 21 | attributes = NFTAttribute.objects.all().annotate( 22 | trait_count=Subquery(trait_count_subquery) 23 | ) 24 | 25 | # Group traits under each type 26 | trait_type_map = {} 27 | for i in attributes: 28 | if i.name in trait_type_map.keys(): 29 | trait_type_map[i.name][i.value] = i.trait_count 30 | else: 31 | trait_type_map[i.name] = {i.value: i.trait_count} 32 | 33 | # Calculate rarity 34 | """ 35 | [Rarity Score for a Trait Value] = 1 / ([Number of Items with that Trait Value] / [Total Number of Items in Collection]) 36 | """ 37 | 38 | for nft in project.nfts.all(): 39 | # fetch all traits for NFT 40 | total_score = 0 41 | 42 | for nft_attribute in nft.nft_attributes.all(): 43 | trait_name = nft_attribute.attribute.name 44 | trait_value = nft_attribute.attribute.value 45 | 46 | # Number of Items with that Trait Value 47 | trait_sum = trait_type_map[trait_name][trait_value] 48 | 49 | rarity_score = 1 / (trait_sum / project.number_of_nfts) 50 | 51 | nft_attribute.rarity_score = rarity_score 52 | nft_attribute.save() 53 | 54 | total_score += rarity_score 55 | 56 | nft.rarity_score = total_score 57 | nft.save() 58 | 59 | # Rank NFTs 60 | for index, nft in enumerate(project.nfts.all().order_by("-rarity_score")): 61 | nft.rank = index + 1 62 | nft.save() 63 | -------------------------------------------------------------------------------- /djsniper/sniper/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.9 on 2021-12-09 15:54 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='NFT', 17 | fields=[ 18 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('rarity_score', models.FloatField(null=True)), 20 | ('nft_id', models.PositiveIntegerField()), 21 | ('image_url', models.CharField(max_length=200)), 22 | ('rank', models.PositiveIntegerField(null=True)), 23 | ], 24 | ), 25 | migrations.CreateModel( 26 | name='NFTAttribute', 27 | fields=[ 28 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 29 | ('name', models.CharField(max_length=50)), 30 | ('value', models.CharField(max_length=100)), 31 | ], 32 | ), 33 | migrations.CreateModel( 34 | name='NFTProject', 35 | fields=[ 36 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 37 | ('contract_address', models.CharField(max_length=100)), 38 | ('contract_abi', models.TextField()), 39 | ('name', models.CharField(max_length=50)), 40 | ('number_of_nfts', models.PositiveIntegerField()), 41 | ], 42 | ), 43 | migrations.CreateModel( 44 | name='NFTTrait', 45 | fields=[ 46 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 47 | ('rarity_score', models.FloatField(null=True)), 48 | ('attribute', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='traits', to='sniper.nftattribute')), 49 | ('nft', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='nft_attributes', to='sniper.nft')), 50 | ], 51 | ), 52 | migrations.AddField( 53 | model_name='nftattribute', 54 | name='project', 55 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attributes', to='sniper.nftproject'), 56 | ), 57 | migrations.AddField( 58 | model_name='nft', 59 | name='project', 60 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='nfts', to='sniper.nftproject'), 61 | ), 62 | ] 63 | -------------------------------------------------------------------------------- /djsniper/sniper/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/djsniper/sniper/migrations/__init__.py -------------------------------------------------------------------------------- /djsniper/sniper/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class NFTProject(models.Model): 5 | contract_address = models.CharField(max_length=100) 6 | contract_abi = models.TextField() 7 | name = models.CharField(max_length=50) # e.g BAYC 8 | number_of_nfts = models.PositiveIntegerField() 9 | 10 | def __str__(self) -> str: 11 | return self.name 12 | 13 | 14 | class NFT(models.Model): 15 | project = models.ForeignKey( 16 | NFTProject, on_delete=models.CASCADE, related_name="nfts" 17 | ) 18 | rarity_score = models.FloatField(null=True) 19 | nft_id = models.PositiveIntegerField() 20 | image_url = models.CharField(max_length=200) 21 | rank = models.PositiveIntegerField(null=True) 22 | 23 | def __str__(self): 24 | return f"{self.project.name}: {self.nft_id}" 25 | 26 | 27 | class NFTAttribute(models.Model): 28 | project = models.ForeignKey( 29 | NFTProject, on_delete=models.CASCADE, related_name="attributes" 30 | ) 31 | name = models.CharField(max_length=50) 32 | value = models.CharField(max_length=100) 33 | 34 | def __str__(self) -> str: 35 | return f"{self.name}: {self.value}" 36 | 37 | 38 | class NFTTrait(models.Model): 39 | nft = models.ForeignKey( 40 | NFT, on_delete=models.CASCADE, related_name="nft_attributes" 41 | ) 42 | attribute = models.ForeignKey( 43 | NFTAttribute, on_delete=models.CASCADE, related_name="traits" 44 | ) 45 | rarity_score = models.FloatField(null=True) 46 | 47 | def __str__(self) -> str: 48 | return f"{self.attribute.name}: {self.attribute.value}" 49 | -------------------------------------------------------------------------------- /djsniper/sniper/tasks.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from time import sleep 3 | from web3.main import Web3 4 | 5 | from celery import shared_task 6 | from celery_progress.backend import ProgressRecorder 7 | 8 | from django.db.models import OuterRef, Func, Subquery 9 | 10 | from djsniper.sniper.models import NFTProject, NFT, NFTAttribute, NFTTrait 11 | 12 | INFURA_PROJECT_ID = "" 13 | INFURA_ENDPOINT = f"https://mainnet.infura.io/v3/{INFURA_PROJECT_ID}" 14 | 15 | 16 | @shared_task 17 | def rank_nfts_task(project_id): 18 | project = NFTProject.objects.get(id=project_id) 19 | 20 | # calculate sum of NFT trait types 21 | trait_count_subquery = ( 22 | NFTTrait.objects.filter(attribute=OuterRef("id")) 23 | .order_by() 24 | .annotate(count=Func("id", function="Count")) 25 | .values("count") 26 | ) 27 | 28 | attributes = NFTAttribute.objects.all().annotate( 29 | trait_count=Subquery(trait_count_subquery) 30 | ) 31 | 32 | # Group traits under each type 33 | trait_type_map = {} 34 | for i in attributes: 35 | if i.name in trait_type_map.keys(): 36 | trait_type_map[i.name][i.value] = i.trait_count 37 | else: 38 | trait_type_map[i.name] = {i.value: i.trait_count} 39 | 40 | # Calculate rarity 41 | """ 42 | [Rarity Score for a Trait Value] = 1 / ([Number of Items with that Trait Value] / [Total Number of Items in Collection]) 43 | """ 44 | 45 | for nft in project.nfts.all(): 46 | # fetch all traits for NFT 47 | total_score = 0 48 | 49 | for nft_attribute in nft.nft_attributes.all(): 50 | trait_name = nft_attribute.attribute.name 51 | trait_value = nft_attribute.attribute.value 52 | 53 | # Number of Items with that Trait Value 54 | trait_sum = trait_type_map[trait_name][trait_value] 55 | 56 | rarity_score = 1 / (trait_sum / project.number_of_nfts) 57 | 58 | nft_attribute.rarity_score = rarity_score 59 | nft_attribute.save() 60 | 61 | total_score += rarity_score 62 | 63 | nft.rarity_score = total_score 64 | nft.save() 65 | 66 | # Rank NFTs 67 | for index, nft in enumerate(project.nfts.all().order_by("-rarity_score")): 68 | nft.rank = index + 1 69 | nft.save() 70 | 71 | 72 | @shared_task(bind=True) 73 | def fetch_nfts_task(self, project_id): 74 | progress_recorder = ProgressRecorder(self) 75 | project = NFTProject.objects.get(id=project_id) 76 | 77 | w3 = Web3(Web3.HTTPProvider(INFURA_ENDPOINT)) 78 | contract_instance = w3.eth.contract( 79 | address=project.contract_address, abi=project.contract_abi 80 | ) 81 | 82 | for i in range(0, project.number_of_nfts): 83 | print("Fetching NFT ...", i) 84 | ipfs_uri = contract_instance.functions.tokenURI(i).call() 85 | data = requests.get( 86 | f"https://ipfs.io/ipfs/{ipfs_uri.split('ipfs://')[1]}" 87 | ).json() 88 | nft = NFT.objects.create( 89 | nft_id=i, project=project, image_url=data["image"].split("ipfs://")[1] 90 | ) 91 | attributes = data["attributes"] 92 | for attribute in attributes: 93 | nft_attribute, created = NFTAttribute.objects.get_or_create( 94 | project=project, name=attribute["trait_type"], value=attribute["value"] 95 | ) 96 | NFTTrait.objects.create(nft=nft, attribute=nft_attribute) 97 | progress_recorder.set_progress(i + 1, project.number_of_nfts) 98 | sleep(1) 99 | 100 | # Call rank function 101 | rank_nfts_task(project_id) 102 | -------------------------------------------------------------------------------- /djsniper/sniper/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /djsniper/sniper/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | 4 | app_name = "sniper" 5 | 6 | urlpatterns = [ 7 | path("", views.ProjectListView.as_view(), name="project-list"), 8 | path("create/", views.ProjectCreateView.as_view(), name="project-create"), 9 | path("project//", views.ProjectDetailView.as_view(), name="project-detail"), 10 | path( 11 | "project//update/", views.ProjectUpdateView.as_view(), name="project-update" 12 | ), 13 | path( 14 | "project//delete/", views.ProjectDeleteView.as_view(), name="project-delete" 15 | ), 16 | path("project//clear/", views.ProjectClearView.as_view(), name="project-clear"), 17 | path("project//fetch-nfts/", views.FetchNFTsView.as_view(), name="fetch-nfts"), 18 | ] 19 | -------------------------------------------------------------------------------- /djsniper/sniper/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from celery.result import AsyncResult 4 | 5 | from django.http import HttpResponse 6 | from django.shortcuts import render, redirect 7 | from django.urls import reverse 8 | from django.views import generic 9 | from django.views.generic.detail import SingleObjectMixin 10 | from djsniper.sniper.forms import ConfirmForm, ProjectForm 11 | from djsniper.sniper.models import NFTAttribute, NFTProject 12 | from djsniper.sniper.tasks import fetch_nfts_task 13 | 14 | 15 | class ProjectListView(generic.ListView): 16 | template_name = "sniper/project_list.html" 17 | 18 | def get_queryset(self): 19 | return NFTProject.objects.all() 20 | 21 | 22 | class ProjectDetailView(generic.DetailView): 23 | template_name = "sniper/project_detail.html" 24 | 25 | def get_queryset(self): 26 | return NFTProject.objects.all() 27 | 28 | def get_context_data(self, **kwargs): 29 | context = super().get_context_data(**kwargs) 30 | nft_project = self.get_object() 31 | order = self.request.GET.get("order", None) 32 | nfts = nft_project.nfts.all() 33 | if order == "rank": 34 | nfts = nfts.order_by("rank") 35 | context.update({"nfts": nfts[0:12]}) 36 | return context 37 | 38 | 39 | class ProjectCreateView(generic.CreateView): 40 | template_name = "sniper/project_create.html" 41 | form_class = ProjectForm 42 | 43 | def form_valid(self, form): 44 | instance = form.save() 45 | return redirect("sniper:project-detail", pk=instance.id) 46 | 47 | def get_queryset(self): 48 | return NFTProject.objects.all() 49 | 50 | 51 | class ProjectUpdateView(generic.UpdateView): 52 | template_name = "sniper/project_update.html" 53 | form_class = ProjectForm 54 | 55 | def get_queryset(self): 56 | return NFTProject.objects.all() 57 | 58 | def get_success_url(self): 59 | return reverse("sniper:project-detail", kwargs={"pk": self.get_object().id}) 60 | 61 | 62 | class ProjectDeleteView(generic.DeleteView): 63 | template_name = "sniper/project_delete.html" 64 | 65 | def get_queryset(self): 66 | return NFTProject.objects.all() 67 | 68 | def get_success_url(self): 69 | return reverse("sniper:project-list") 70 | 71 | 72 | class ProjectClearView(SingleObjectMixin, generic.FormView): 73 | template_name = "sniper/project_clear.html" 74 | form_class = ConfirmForm 75 | 76 | def get(self, request, *args, **kwargs): 77 | self.object = self.get_object() 78 | context = self.get_context_data(object=self.object) 79 | return self.render_to_response(context) 80 | 81 | def get_queryset(self): 82 | return NFTProject.objects.all() 83 | 84 | def form_valid(self, form): 85 | nft_project = self.get_object() 86 | nft_project.nfts.all().delete() 87 | NFTAttribute.objects.filter(project=nft_project).delete() 88 | return super().form_valid(form) 89 | 90 | def get_success_url(self): 91 | return reverse("sniper:project-detail", kwargs={"pk": self.kwargs["pk"]}) 92 | 93 | 94 | def nft_list(request): 95 | project = NFTProject.objects.get(name="BAYC") 96 | nfts = project.nfts.all().order_by("-rarity_score")[0:12] 97 | return render(request, "nfts.html", {"nfts": nfts}) 98 | 99 | 100 | class FetchNFTsView(generic.FormView): 101 | template_name = "sniper/fetch_nfts.html" 102 | form_class = ConfirmForm 103 | 104 | def form_valid(self, form): 105 | result = fetch_nfts_task.apply_async((self.kwargs["pk"],), countdown=1) 106 | return render(self.request, self.template_name, {"task_id": result.task_id}) 107 | 108 | 109 | def get_progress(request, task_id): 110 | result = AsyncResult(task_id) 111 | response_data = { 112 | "state": result.state, 113 | "details": result.info, 114 | } 115 | return HttpResponse(json.dumps(response_data), content_type="application/json") 116 | -------------------------------------------------------------------------------- /djsniper/static/css/project.css: -------------------------------------------------------------------------------- 1 | /* These styles are generated from project.scss. */ 2 | 3 | .alert-debug { 4 | color: black; 5 | background-color: white; 6 | border-color: #d6e9c6; 7 | } 8 | 9 | .alert-error { 10 | color: #b94a48; 11 | background-color: #f2dede; 12 | border-color: #eed3d7; 13 | } 14 | -------------------------------------------------------------------------------- /djsniper/static/fonts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/djsniper/static/fonts/.gitkeep -------------------------------------------------------------------------------- /djsniper/static/images/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/djsniper/static/images/favicons/favicon.ico -------------------------------------------------------------------------------- /djsniper/static/js/celery_progress.js: -------------------------------------------------------------------------------- 1 | class CeleryProgressBar { 2 | 3 | constructor(progressUrl, options) { 4 | this.progressUrl = progressUrl; 5 | options = options || {}; 6 | let progressBarId = options.progressBarId || 'progress-bar'; 7 | let progressBarMessage = options.progressBarMessageId || 'progress-bar-message'; 8 | this.progressBarElement = options.progressBarElement || document.getElementById(progressBarId); 9 | this.progressBarMessageElement = options.progressBarMessageElement || document.getElementById(progressBarMessage); 10 | this.onProgress = options.onProgress || this.onProgressDefault; 11 | this.onSuccess = options.onSuccess || this.onSuccessDefault; 12 | this.onError = options.onError || this.onErrorDefault; 13 | this.onTaskError = options.onTaskError || this.onTaskErrorDefault; 14 | this.onDataError = options.onDataError || this.onError; 15 | this.onRetry = options.onRetry || this.onRetryDefault; 16 | this.onIgnored = options.onIgnored || this.onIgnoredDefault; 17 | let resultElementId = options.resultElementId || 'celery-result'; 18 | this.resultElement = options.resultElement || document.getElementById(resultElementId); 19 | this.onResult = options.onResult || this.onResultDefault; 20 | // HTTP options 21 | this.onNetworkError = options.onNetworkError || this.onError; 22 | this.onHttpError = options.onHttpError || this.onError; 23 | this.pollInterval = options.pollInterval || 500; 24 | // Other options 25 | let barColorsDefault = { 26 | success: '#76ce60', 27 | error: '#dc4f63', 28 | progress: '#68a9ef', 29 | ignored: '#7a7a7a' 30 | } 31 | this.barColors = Object.assign({}, barColorsDefault, options.barColors); 32 | 33 | let defaultMessages = { 34 | waiting: 'Waiting for task to start...', 35 | started: 'Task started...', 36 | } 37 | this.messages = Object.assign({}, defaultMessages, options.defaultMessages); 38 | } 39 | 40 | onSuccessDefault(progressBarElement, progressBarMessageElement, result) { 41 | result = this.getMessageDetails(result); 42 | if (progressBarElement) { 43 | progressBarElement.style.backgroundColor = this.barColors.success; 44 | } 45 | if (progressBarMessageElement) { 46 | progressBarMessageElement.textContent = "Success! " + result; 47 | } 48 | } 49 | 50 | onResultDefault(resultElement, result) { 51 | if (resultElement) { 52 | resultElement.textContent = result; 53 | } 54 | } 55 | 56 | /** 57 | * Default handler for all errors. 58 | * @param data - A Response object for HTTP errors, undefined for other errors 59 | */ 60 | onErrorDefault(progressBarElement, progressBarMessageElement, excMessage, data) { 61 | progressBarElement.style.backgroundColor = this.barColors.error; 62 | excMessage = excMessage || ''; 63 | progressBarMessageElement.textContent = "Uh-Oh, something went wrong! " + excMessage; 64 | } 65 | 66 | onTaskErrorDefault(progressBarElement, progressBarMessageElement, excMessage) { 67 | let message = this.getMessageDetails(excMessage); 68 | this.onError(progressBarElement, progressBarMessageElement, message); 69 | } 70 | 71 | onRetryDefault(progressBarElement, progressBarMessageElement, excMessage, retryWhen) { 72 | retryWhen = new Date(retryWhen); 73 | let message = 'Retrying in ' + Math.round((retryWhen.getTime() - Date.now())/1000) + 's: ' + excMessage; 74 | this.onError(progressBarElement, progressBarMessageElement, message); 75 | } 76 | 77 | onIgnoredDefault(progressBarElement, progressBarMessageElement, result) { 78 | progressBarElement.style.backgroundColor = this.barColors.ignored; 79 | progressBarMessageElement.textContent = result || 'Task result ignored!' 80 | } 81 | 82 | onProgressDefault(progressBarElement, progressBarMessageElement, progress) { 83 | progressBarElement.style.backgroundColor = this.barColors.progress; 84 | progressBarElement.style.width = progress.percent + "%"; 85 | var description = progress.description || ""; 86 | if (progress.current == 0) { 87 | if (progress.pending === true) { 88 | progressBarMessageElement.textContent = this.messages.waiting; 89 | } else { 90 | progressBarMessageElement.textContent = this.messages.started; 91 | } 92 | } else { 93 | progressBarMessageElement.textContent = progress.current + ' of ' + progress.total + ' processed. ' + description; 94 | } 95 | } 96 | 97 | getMessageDetails(result) { 98 | if (this.resultElement) { 99 | return '' 100 | } else { 101 | return result || ''; 102 | } 103 | } 104 | 105 | /** 106 | * Process update message data. 107 | * @return true if the task is complete, false if it's not, undefined if `data` is invalid 108 | */ 109 | onData(data) { 110 | let done = false; 111 | if (data.progress) { 112 | this.onProgress(this.progressBarElement, this.progressBarMessageElement, data.progress); 113 | } 114 | if (data.complete === true) { 115 | done = true; 116 | if (data.success === true) { 117 | this.onSuccess(this.progressBarElement, this.progressBarMessageElement, data.result); 118 | } else if (data.success === false) { 119 | if (data.state === 'RETRY') { 120 | this.onRetry(this.progressBarElement, this.progressBarMessageElement, data.result.message, data.result.when); 121 | done = false; 122 | delete data.result; 123 | } else { 124 | this.onTaskError(this.progressBarElement, this.progressBarMessageElement, data.result); 125 | } 126 | } else { 127 | if (data.state === 'IGNORED') { 128 | this.onIgnored(this.progressBarElement, this.progressBarMessageElement, data.result); 129 | delete data.result; 130 | } else { 131 | done = undefined; 132 | this.onDataError(this.progressBarElement, this.progressBarMessageElement, "Data Error"); 133 | } 134 | } 135 | if (data.hasOwnProperty('result')) { 136 | this.onResult(this.resultElement, data.result); 137 | } 138 | } else if (data.complete === undefined) { 139 | done = undefined; 140 | this.onDataError(this.progressBarElement, this.progressBarMessageElement, "Data Error"); 141 | } 142 | return done; 143 | } 144 | 145 | async connect() { 146 | let response; 147 | try { 148 | response = await fetch(this.progressUrl); 149 | } catch (networkError) { 150 | this.onNetworkError(this.progressBarElement, this.progressBarMessageElement, "Network Error"); 151 | throw networkError; 152 | } 153 | 154 | if (response.status === 200) { 155 | let data; 156 | try { 157 | data = await response.json(); 158 | } catch (parsingError) { 159 | this.onDataError(this.progressBarElement, this.progressBarMessageElement, "Parsing Error") 160 | throw parsingError; 161 | } 162 | 163 | const complete = this.onData(data); 164 | 165 | if (complete === false) { 166 | setTimeout(this.connect.bind(this), this.pollInterval); 167 | } 168 | } else { 169 | this.onHttpError(this.progressBarElement, this.progressBarMessageElement, "HTTP Code " + response.status, response); 170 | } 171 | } 172 | 173 | static initProgressBar(progressUrl, options) { 174 | const bar = new this(progressUrl, options); 175 | bar.connect(); 176 | } 177 | } -------------------------------------------------------------------------------- /djsniper/static/js/project.js: -------------------------------------------------------------------------------- 1 | /* Project specific Javascript goes here. */ 2 | -------------------------------------------------------------------------------- /djsniper/static/sass/custom_bootstrap_vars.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/djsniper/static/sass/custom_bootstrap_vars.scss -------------------------------------------------------------------------------- /djsniper/static/sass/project.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | // project specific CSS goes here 6 | 7 | //////////////////////////////// 8 | //Variables// 9 | //////////////////////////////// 10 | 11 | // Alert colors 12 | 13 | $white: #fff; 14 | $mint-green: #d6e9c6; 15 | $black: #000; 16 | $pink: #f2dede; 17 | $dark-pink: #eed3d7; 18 | $red: #b94a48; 19 | 20 | //////////////////////////////// 21 | //Alerts// 22 | //////////////////////////////// 23 | 24 | // bootstrap alert CSS, translated to the django-standard levels of 25 | // debug, info, success, warning, error 26 | 27 | .alert-debug { 28 | background-color: $white; 29 | border-color: $mint-green; 30 | color: $black; 31 | } 32 | 33 | .alert-error { 34 | background-color: $pink; 35 | border-color: $dark-pink; 36 | color: $red; 37 | } 38 | -------------------------------------------------------------------------------- /djsniper/templates/403.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Forbidden (403){% endblock %} 4 | 5 | {% block content %} 6 |

Forbidden (403)

7 | 8 |

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

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

Page not found

7 | 8 |

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

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

Ooops!!! 500

7 | 8 |

Looks like something went wrong!

9 | 10 |

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

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

{% translate "Account Inactive" %}

9 | 10 |

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

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

{% translate "E-mail Addresses" %}

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

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

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

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

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

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

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

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

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

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

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

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

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

{% translate "Sign In" %}

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

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

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

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

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

{% translate "Sign Out" %}

9 | 10 |

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

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

{% translate "Change Password" %}

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

{% translate "Password Reset" %}

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

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

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

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

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

{% translate "Password Reset" %}

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

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

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

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

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

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

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

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

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

{% translate "Change Password" %}

8 |

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

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

{% translate "Set Password" %}

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

{% translate "Sign Up" %}

10 | 11 |

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

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

{% translate "Sign Up Closed" %}

9 | 10 |

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

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

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

9 | 10 |

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

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

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

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

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

15 | 16 |

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

19 | 20 |

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

21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /djsniper/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static i18n %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% block title %}Dj NFT Sniper{% endblock title %} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {% block css %} 17 | 18 | 19 | 20 | {% endblock %} 21 | 23 | {# Placed at the top of the document so pages load faster with defer #} 24 | {% block javascript %} 25 | 26 | 27 | {% endblock javascript %} 28 | 29 | 30 | 31 | 32 | 33 |
34 | 63 | 64 |
65 | {% if messages %} 66 | {% for message in messages %} 67 |
68 | {{ message }} 69 | 70 |
71 | {% endfor %} 72 | {% endif %} 73 |
74 |
75 | {% block content %} 76 |

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

77 | {% endblock content %} 78 |
79 |
80 |
81 |
82 | 83 | {% block modal %}{% endblock modal %} 84 | 85 | {% block inline_javascript %} 86 | {% comment %} 87 | Script tags with only code, no src (defer by default). To run 88 | with a "defer" so that you run run inline code: 89 | 92 | {% endcomment %} 93 | {% endblock inline_javascript %} 94 | 95 | 96 | -------------------------------------------------------------------------------- /djsniper/templates/nfts.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 |

6 | Bored Ape Yacht Club 7 |

8 |
9 | {% for nft in nfts %} 10 |
11 | 12 |
13 |

#{{ nft.nft_id }}

14 |

Score: {{ nft.rarity_score|floatformat:2 }}

15 |
16 |
17 | {% endfor %} 18 |
19 | 20 | {% endblock content %} -------------------------------------------------------------------------------- /djsniper/templates/pages/about.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | -------------------------------------------------------------------------------- /djsniper/templates/sniper/fetch_nfts.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 | {% load static %} 4 | 5 |
6 |

Fetch NFTs

7 |

Confirming this form will start the celery tasks

8 |
9 | {% if task_id %} 10 | 11 |
12 |
13 |   14 |
15 |
16 |
Waiting for progress to start...
17 | 18 | {% else %} 19 | 20 |
21 | {% csrf_token %} 22 | {{ form }} 23 | 27 |
28 | 29 | {% endif %} 30 |
31 |
32 | 33 | {% endblock content %} 34 | 35 | {% block inline_javascript %} 36 | 37 | 44 | 45 | {% endblock inline_javascript %} -------------------------------------------------------------------------------- /djsniper/templates/sniper/project_clear.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load tailwind_filters %} 3 | 4 | {% block content %} 5 | 6 |
7 |
8 |

9 | Clear Project Data 10 |

11 |
12 | 13 | 18 | 19 |
20 | {% csrf_token %} 21 | 23 |
24 | 25 |
26 | 27 | {% endblock content %} -------------------------------------------------------------------------------- /djsniper/templates/sniper/project_create.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load tailwind_filters %} 3 | 4 | {% block content %} 5 | 6 |
7 |

8 | Create Project 9 |

10 | 11 |
12 | {% csrf_token %} 13 | {{ form|crispy }} 14 | 16 |
17 |
18 | 19 | {% endblock content %} -------------------------------------------------------------------------------- /djsniper/templates/sniper/project_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load tailwind_filters %} 3 | 4 | {% block content %} 5 | 6 |
7 |
8 |

9 | Delete Project 10 |

11 |
12 | 13 | 18 | 19 |
20 | {% csrf_token %} 21 | 23 |
24 | 25 |
26 | 27 | {% endblock content %} -------------------------------------------------------------------------------- /djsniper/templates/sniper/project_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 |
6 |
7 |
8 |

9 | {{ object.name }} 10 |

11 |
12 | 18 |
19 | 20 |
21 |

{{ object.number_of_nfts }} NFTs

22 |
23 | {% for nft in nfts %} 24 |
25 | 26 |
27 |

#{{ nft.nft_id }}

28 |

Score: {{ nft.rarity_score|floatformat:2 }}

29 |
30 |
31 | {% endfor %} 32 |
33 | 39 |
40 | 41 | 55 |
56 | 57 | {% endblock content %} -------------------------------------------------------------------------------- /djsniper/templates/sniper/project_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 |

6 | Projects 7 |

8 | 9 | {% for project in object_list %} 10 | 11 | 18 | 19 | {% endfor %} 20 | 21 | {% endblock content %} -------------------------------------------------------------------------------- /djsniper/templates/sniper/project_update.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load tailwind_filters %} 3 | 4 | {% block content %} 5 | 6 |
7 |

8 | Update {{ object.name }} 9 |

10 | 11 | 16 | 17 |
18 | {% csrf_token %} 19 | {{ form|crispy }} 20 | 22 |
23 |
24 | 25 | {% endblock content %} -------------------------------------------------------------------------------- /djsniper/templates/users/user_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | 4 | {% block title %}User: {{ object.username }}{% endblock %} 5 | 6 | {% block content %} 7 |
8 | 9 |
10 |
11 | 12 |

{{ object.username }}

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

{{ object.name }}

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

{{ user.username }}

8 |
9 | {% csrf_token %} 10 | {{ form|crispy }} 11 |
12 |
13 | 14 |
15 |
16 |
17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /djsniper/users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/djsniper/users/__init__.py -------------------------------------------------------------------------------- /djsniper/users/adapters.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from allauth.account.adapter import DefaultAccountAdapter 4 | from allauth.socialaccount.adapter import DefaultSocialAccountAdapter 5 | from django.conf import settings 6 | from django.http import HttpRequest 7 | 8 | 9 | class AccountAdapter(DefaultAccountAdapter): 10 | def is_open_for_signup(self, request: HttpRequest): 11 | return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True) 12 | 13 | 14 | class SocialAccountAdapter(DefaultSocialAccountAdapter): 15 | def is_open_for_signup(self, request: HttpRequest, sociallogin: Any): 16 | return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True) 17 | -------------------------------------------------------------------------------- /djsniper/users/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth import admin as auth_admin 3 | from django.contrib.auth import get_user_model 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | from djsniper.users.forms import UserChangeForm, UserCreationForm 7 | 8 | User = get_user_model() 9 | 10 | 11 | @admin.register(User) 12 | class UserAdmin(auth_admin.UserAdmin): 13 | 14 | form = UserChangeForm 15 | add_form = UserCreationForm 16 | fieldsets = ( 17 | (None, {"fields": ("username", "password")}), 18 | (_("Personal info"), {"fields": ("name", "email")}), 19 | ( 20 | _("Permissions"), 21 | { 22 | "fields": ( 23 | "is_active", 24 | "is_staff", 25 | "is_superuser", 26 | "groups", 27 | "user_permissions", 28 | ), 29 | }, 30 | ), 31 | (_("Important dates"), {"fields": ("last_login", "date_joined")}), 32 | ) 33 | list_display = ["username", "name", "is_superuser"] 34 | search_fields = ["name"] 35 | -------------------------------------------------------------------------------- /djsniper/users/api/serializers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from rest_framework import serializers 3 | 4 | User = get_user_model() 5 | 6 | 7 | class UserSerializer(serializers.ModelSerializer): 8 | class Meta: 9 | model = User 10 | fields = ["username", "name", "url"] 11 | 12 | extra_kwargs = { 13 | "url": {"view_name": "api:user-detail", "lookup_field": "username"} 14 | } 15 | -------------------------------------------------------------------------------- /djsniper/users/api/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from rest_framework import status 3 | from rest_framework.decorators import action 4 | from rest_framework.mixins import ListModelMixin, RetrieveModelMixin, UpdateModelMixin 5 | from rest_framework.response import Response 6 | from rest_framework.viewsets import GenericViewSet 7 | 8 | from .serializers import UserSerializer 9 | 10 | User = get_user_model() 11 | 12 | 13 | class UserViewSet(RetrieveModelMixin, ListModelMixin, UpdateModelMixin, GenericViewSet): 14 | serializer_class = UserSerializer 15 | queryset = User.objects.all() 16 | lookup_field = "username" 17 | 18 | def get_queryset(self, *args, **kwargs): 19 | assert isinstance(self.request.user.id, int) 20 | return self.queryset.filter(id=self.request.user.id) 21 | 22 | @action(detail=False) 23 | def me(self, request): 24 | serializer = UserSerializer(request.user, context={"request": request}) 25 | return Response(status=status.HTTP_200_OK, data=serializer.data) 26 | -------------------------------------------------------------------------------- /djsniper/users/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class UsersConfig(AppConfig): 6 | name = "djsniper.users" 7 | verbose_name = _("Users") 8 | 9 | def ready(self): 10 | try: 11 | import djsniper.users.signals # noqa F401 12 | except ImportError: 13 | pass 14 | -------------------------------------------------------------------------------- /djsniper/users/forms.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import forms as admin_forms 2 | from django.contrib.auth import get_user_model 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | User = get_user_model() 6 | 7 | 8 | class UserChangeForm(admin_forms.UserChangeForm): 9 | class Meta(admin_forms.UserChangeForm.Meta): 10 | model = User 11 | 12 | 13 | class UserCreationForm(admin_forms.UserCreationForm): 14 | class Meta(admin_forms.UserCreationForm.Meta): 15 | model = User 16 | 17 | error_messages = { 18 | "username": {"unique": _("This username has already been taken.")} 19 | } 20 | -------------------------------------------------------------------------------- /djsniper/users/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.9 on 2021-11-20 11:23 2 | import django.contrib.auth.models 3 | import django.contrib.auth.validators 4 | from django.db import migrations, models 5 | import django.utils.timezone 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ("auth", "0012_alter_user_first_name_max_length"), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name="User", 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 | ("password", models.CharField(max_length=128, verbose_name="password")), 30 | ( 31 | "last_login", 32 | models.DateTimeField( 33 | blank=True, null=True, verbose_name="last login" 34 | ), 35 | ), 36 | ( 37 | "is_superuser", 38 | models.BooleanField( 39 | default=False, 40 | help_text="Designates that this user has all permissions without explicitly assigning them.", 41 | verbose_name="superuser status", 42 | ), 43 | ), 44 | ( 45 | "username", 46 | models.CharField( 47 | error_messages={ 48 | "unique": "A user with that username already exists." 49 | }, 50 | help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", 51 | max_length=150, 52 | unique=True, 53 | validators=[ 54 | django.contrib.auth.validators.UnicodeUsernameValidator() 55 | ], 56 | verbose_name="username", 57 | ), 58 | ), 59 | ( 60 | "email", 61 | models.EmailField( 62 | blank=True, max_length=254, verbose_name="email address" 63 | ), 64 | ), 65 | ( 66 | "is_staff", 67 | models.BooleanField( 68 | default=False, 69 | help_text="Designates whether the user can log into this admin site.", 70 | verbose_name="staff status", 71 | ), 72 | ), 73 | ( 74 | "is_active", 75 | models.BooleanField( 76 | default=True, 77 | help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", 78 | verbose_name="active", 79 | ), 80 | ), 81 | ( 82 | "date_joined", 83 | models.DateTimeField( 84 | default=django.utils.timezone.now, verbose_name="date joined" 85 | ), 86 | ), 87 | ( 88 | "name", 89 | models.CharField( 90 | blank=True, max_length=255, verbose_name="Name of User" 91 | ), 92 | ), 93 | ( 94 | "groups", 95 | models.ManyToManyField( 96 | blank=True, 97 | help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", 98 | related_name="user_set", 99 | related_query_name="user", 100 | to="auth.Group", 101 | verbose_name="groups", 102 | ), 103 | ), 104 | ( 105 | "user_permissions", 106 | models.ManyToManyField( 107 | blank=True, 108 | help_text="Specific permissions for this user.", 109 | related_name="user_set", 110 | related_query_name="user", 111 | to="auth.Permission", 112 | verbose_name="user permissions", 113 | ), 114 | ), 115 | ], 116 | options={ 117 | "verbose_name": "user", 118 | "verbose_name_plural": "users", 119 | "abstract": False, 120 | }, 121 | managers=[ 122 | ("objects", django.contrib.auth.models.UserManager()), 123 | ], 124 | ), 125 | ] 126 | -------------------------------------------------------------------------------- /djsniper/users/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/djsniper/users/migrations/__init__.py -------------------------------------------------------------------------------- /djsniper/users/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import AbstractUser 2 | from django.db.models import CharField 3 | from django.urls import reverse 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | 7 | class User(AbstractUser): 8 | """Default user for djsniper.""" 9 | 10 | #: First and last name do not cover name patterns around the globe 11 | name = CharField(_("Name of User"), blank=True, max_length=255) 12 | first_name = None # type: ignore 13 | last_name = None # type: ignore 14 | 15 | def get_absolute_url(self): 16 | """Get url for user's detail view. 17 | 18 | Returns: 19 | str: URL for user detail. 20 | 21 | """ 22 | return reverse("users:detail", kwargs={"username": self.username}) 23 | -------------------------------------------------------------------------------- /djsniper/users/tasks.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | 3 | from config import celery_app 4 | 5 | User = get_user_model() 6 | 7 | 8 | @celery_app.task() 9 | def get_users_count(): 10 | """A pointless Celery task to demonstrate usage.""" 11 | return User.objects.count() 12 | -------------------------------------------------------------------------------- /djsniper/users/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/djsniper/users/tests/__init__.py -------------------------------------------------------------------------------- /djsniper/users/tests/factories.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Sequence 2 | 3 | from django.contrib.auth import get_user_model 4 | from factory import Faker, post_generation 5 | from factory.django import DjangoModelFactory 6 | 7 | 8 | class UserFactory(DjangoModelFactory): 9 | 10 | username = Faker("user_name") 11 | email = Faker("email") 12 | name = Faker("name") 13 | 14 | @post_generation 15 | def password(self, create: bool, extracted: Sequence[Any], **kwargs): 16 | password = ( 17 | extracted 18 | if extracted 19 | else Faker( 20 | "password", 21 | length=42, 22 | special_chars=True, 23 | digits=True, 24 | upper_case=True, 25 | lower_case=True, 26 | ).evaluate(None, None, extra={"locale": None}) 27 | ) 28 | self.set_password(password) 29 | 30 | class Meta: 31 | model = get_user_model() 32 | django_get_or_create = ["username"] 33 | -------------------------------------------------------------------------------- /djsniper/users/tests/test_admin.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.urls import reverse 3 | 4 | from djsniper.users.models import User 5 | 6 | pytestmark = pytest.mark.django_db 7 | 8 | 9 | class TestUserAdmin: 10 | def test_changelist(self, admin_client): 11 | url = reverse("admin:users_user_changelist") 12 | response = admin_client.get(url) 13 | assert response.status_code == 200 14 | 15 | def test_search(self, admin_client): 16 | url = reverse("admin:users_user_changelist") 17 | response = admin_client.get(url, data={"q": "test"}) 18 | assert response.status_code == 200 19 | 20 | def test_add(self, admin_client): 21 | url = reverse("admin:users_user_add") 22 | response = admin_client.get(url) 23 | assert response.status_code == 200 24 | 25 | response = admin_client.post( 26 | url, 27 | data={ 28 | "username": "test", 29 | "password1": "My_R@ndom-P@ssw0rd", 30 | "password2": "My_R@ndom-P@ssw0rd", 31 | }, 32 | ) 33 | assert response.status_code == 302 34 | assert User.objects.filter(username="test").exists() 35 | 36 | def test_view_user(self, admin_client): 37 | user = User.objects.get(username="admin") 38 | url = reverse("admin:users_user_change", kwargs={"object_id": user.pk}) 39 | response = admin_client.get(url) 40 | assert response.status_code == 200 41 | -------------------------------------------------------------------------------- /djsniper/users/tests/test_drf_urls.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.urls import resolve, reverse 3 | 4 | from djsniper.users.models import User 5 | 6 | pytestmark = pytest.mark.django_db 7 | 8 | 9 | def test_user_detail(user: User): 10 | assert ( 11 | reverse("api:user-detail", kwargs={"username": user.username}) 12 | == f"/api/users/{user.username}/" 13 | ) 14 | assert resolve(f"/api/users/{user.username}/").view_name == "api:user-detail" 15 | 16 | 17 | def test_user_list(): 18 | assert reverse("api:user-list") == "/api/users/" 19 | assert resolve("/api/users/").view_name == "api:user-list" 20 | 21 | 22 | def test_user_me(): 23 | assert reverse("api:user-me") == "/api/users/me/" 24 | assert resolve("/api/users/me/").view_name == "api:user-me" 25 | -------------------------------------------------------------------------------- /djsniper/users/tests/test_drf_views.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.test import RequestFactory 3 | 4 | from djsniper.users.api.views import UserViewSet 5 | from djsniper.users.models import User 6 | 7 | pytestmark = pytest.mark.django_db 8 | 9 | 10 | class TestUserViewSet: 11 | def test_get_queryset(self, user: User, rf: RequestFactory): 12 | view = UserViewSet() 13 | request = rf.get("/fake-url/") 14 | request.user = user 15 | 16 | view.request = request 17 | 18 | assert user in view.get_queryset() 19 | 20 | def test_me(self, user: User, rf: RequestFactory): 21 | view = UserViewSet() 22 | request = rf.get("/fake-url/") 23 | request.user = user 24 | 25 | view.request = request 26 | 27 | response = view.me(request) 28 | 29 | assert response.data == { 30 | "username": user.username, 31 | "name": user.name, 32 | "url": f"http://testserver/api/users/{user.username}/", 33 | } 34 | -------------------------------------------------------------------------------- /djsniper/users/tests/test_forms.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for all Form Tests. 3 | """ 4 | import pytest 5 | from django.utils.translation import gettext_lazy as _ 6 | 7 | from djsniper.users.forms import UserCreationForm 8 | from djsniper.users.models import User 9 | 10 | pytestmark = pytest.mark.django_db 11 | 12 | 13 | class TestUserCreationForm: 14 | """ 15 | Test class for all tests related to the UserCreationForm 16 | """ 17 | 18 | def test_username_validation_error_msg(self, user: User): 19 | """ 20 | Tests UserCreation Form's unique validator functions correctly by testing: 21 | 1) A new user with an existing username cannot be added. 22 | 2) Only 1 error is raised by the UserCreation Form 23 | 3) The desired error message is raised 24 | """ 25 | 26 | # The user already exists, 27 | # hence cannot be created. 28 | form = UserCreationForm( 29 | { 30 | "username": user.username, 31 | "password1": user.password, 32 | "password2": user.password, 33 | } 34 | ) 35 | 36 | assert not form.is_valid() 37 | assert len(form.errors) == 1 38 | assert "username" in form.errors 39 | assert form.errors["username"][0] == _("This username has already been taken.") 40 | -------------------------------------------------------------------------------- /djsniper/users/tests/test_models.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from djsniper.users.models import User 4 | 5 | pytestmark = pytest.mark.django_db 6 | 7 | 8 | def test_user_get_absolute_url(user: User): 9 | assert user.get_absolute_url() == f"/users/{user.username}/" 10 | -------------------------------------------------------------------------------- /djsniper/users/tests/test_tasks.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from celery.result import EagerResult 3 | 4 | from djsniper.users.tasks import get_users_count 5 | from djsniper.users.tests.factories import UserFactory 6 | 7 | pytestmark = pytest.mark.django_db 8 | 9 | 10 | def test_user_count(settings): 11 | """A basic test to execute the get_users_count Celery task.""" 12 | UserFactory.create_batch(3) 13 | settings.CELERY_TASK_ALWAYS_EAGER = True 14 | task_result = get_users_count.delay() 15 | assert isinstance(task_result, EagerResult) 16 | assert task_result.result == 3 17 | -------------------------------------------------------------------------------- /djsniper/users/tests/test_urls.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.urls import resolve, reverse 3 | 4 | from djsniper.users.models import User 5 | 6 | pytestmark = pytest.mark.django_db 7 | 8 | 9 | def test_detail(user: User): 10 | assert ( 11 | reverse("users:detail", kwargs={"username": user.username}) 12 | == f"/users/{user.username}/" 13 | ) 14 | assert resolve(f"/users/{user.username}/").view_name == "users:detail" 15 | 16 | 17 | def test_update(): 18 | assert reverse("users:update") == "/users/~update/" 19 | assert resolve("/users/~update/").view_name == "users:update" 20 | 21 | 22 | def test_redirect(): 23 | assert reverse("users:redirect") == "/users/~redirect/" 24 | assert resolve("/users/~redirect/").view_name == "users:redirect" 25 | -------------------------------------------------------------------------------- /djsniper/users/tests/test_views.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.conf import settings 3 | from django.contrib import messages 4 | from django.contrib.auth.models import AnonymousUser 5 | from django.contrib.messages.middleware import MessageMiddleware 6 | from django.contrib.sessions.middleware import SessionMiddleware 7 | from django.http import HttpRequest, HttpResponseRedirect 8 | from django.test import RequestFactory 9 | from django.urls import reverse 10 | 11 | from djsniper.users.forms import UserChangeForm 12 | from djsniper.users.models import User 13 | from djsniper.users.tests.factories import UserFactory 14 | from djsniper.users.views import ( 15 | UserRedirectView, 16 | UserUpdateView, 17 | user_detail_view, 18 | ) 19 | 20 | pytestmark = pytest.mark.django_db 21 | 22 | 23 | class TestUserUpdateView: 24 | """ 25 | TODO: 26 | extracting view initialization code as class-scoped fixture 27 | would be great if only pytest-django supported non-function-scoped 28 | fixture db access -- this is a work-in-progress for now: 29 | https://github.com/pytest-dev/pytest-django/pull/258 30 | """ 31 | 32 | def dummy_get_response(self, request: HttpRequest): 33 | return None 34 | 35 | def test_get_success_url(self, user: User, rf: RequestFactory): 36 | view = UserUpdateView() 37 | request = rf.get("/fake-url/") 38 | request.user = user 39 | 40 | view.request = request 41 | 42 | assert view.get_success_url() == f"/users/{user.username}/" 43 | 44 | def test_get_object(self, user: User, rf: RequestFactory): 45 | view = UserUpdateView() 46 | request = rf.get("/fake-url/") 47 | request.user = user 48 | 49 | view.request = request 50 | 51 | assert view.get_object() == user 52 | 53 | def test_form_valid(self, user: User, rf: RequestFactory): 54 | view = UserUpdateView() 55 | request = rf.get("/fake-url/") 56 | 57 | # Add the session/message middleware to the request 58 | SessionMiddleware(self.dummy_get_response).process_request(request) 59 | MessageMiddleware(self.dummy_get_response).process_request(request) 60 | request.user = user 61 | 62 | view.request = request 63 | 64 | # Initialize the form 65 | form = UserChangeForm() 66 | form.cleaned_data = [] 67 | view.form_valid(form) 68 | 69 | messages_sent = [m.message for m in messages.get_messages(request)] 70 | assert messages_sent == ["Information successfully updated"] 71 | 72 | 73 | class TestUserRedirectView: 74 | def test_get_redirect_url(self, user: User, rf: RequestFactory): 75 | view = UserRedirectView() 76 | request = rf.get("/fake-url") 77 | request.user = user 78 | 79 | view.request = request 80 | 81 | assert view.get_redirect_url() == f"/users/{user.username}/" 82 | 83 | 84 | class TestUserDetailView: 85 | def test_authenticated(self, user: User, rf: RequestFactory): 86 | request = rf.get("/fake-url/") 87 | request.user = UserFactory() 88 | 89 | response = user_detail_view(request, username=user.username) 90 | 91 | assert response.status_code == 200 92 | 93 | def test_not_authenticated(self, user: User, rf: RequestFactory): 94 | request = rf.get("/fake-url/") 95 | request.user = AnonymousUser() 96 | 97 | response = user_detail_view(request, username=user.username) 98 | login_url = reverse(settings.LOGIN_URL) 99 | 100 | assert isinstance(response, HttpResponseRedirect) 101 | assert response.status_code == 302 102 | assert response.url == f"{login_url}?next=/fake-url/" 103 | -------------------------------------------------------------------------------- /djsniper/users/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from djsniper.users.views import ( 4 | user_detail_view, 5 | user_redirect_view, 6 | user_update_view, 7 | ) 8 | 9 | app_name = "users" 10 | urlpatterns = [ 11 | path("~redirect/", view=user_redirect_view, name="redirect"), 12 | path("~update/", view=user_update_view, name="update"), 13 | path("/", view=user_detail_view, name="detail"), 14 | ] 15 | -------------------------------------------------------------------------------- /djsniper/users/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.contrib.auth.mixins import LoginRequiredMixin 3 | from django.contrib.messages.views import SuccessMessageMixin 4 | from django.urls import reverse 5 | from django.utils.translation import gettext_lazy as _ 6 | from django.views.generic import DetailView, RedirectView, UpdateView 7 | 8 | User = get_user_model() 9 | 10 | 11 | class UserDetailView(LoginRequiredMixin, DetailView): 12 | 13 | model = User 14 | slug_field = "username" 15 | slug_url_kwarg = "username" 16 | 17 | 18 | user_detail_view = UserDetailView.as_view() 19 | 20 | 21 | class UserUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): 22 | 23 | model = User 24 | fields = ["name"] 25 | success_message = _("Information successfully updated") 26 | 27 | def get_success_url(self): 28 | assert ( 29 | self.request.user.is_authenticated 30 | ) # for mypy to know that the user is authenticated 31 | return self.request.user.get_absolute_url() 32 | 33 | def get_object(self): 34 | return self.request.user 35 | 36 | 37 | user_update_view = UserUpdateView.as_view() 38 | 39 | 40 | class UserRedirectView(LoginRequiredMixin, RedirectView): 41 | 42 | permanent = False 43 | 44 | def get_redirect_url(self): 45 | return reverse("users:detail", kwargs={"username": self.request.user.username}) 46 | 47 | 48 | user_redirect_view = UserRedirectView.as_view() 49 | -------------------------------------------------------------------------------- /djsniper/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/djsniper/utils/__init__.py -------------------------------------------------------------------------------- /djsniper/utils/storages.py: -------------------------------------------------------------------------------- 1 | from storages.backends.s3boto3 import S3Boto3Storage 2 | 3 | 4 | class StaticRootS3Boto3Storage(S3Boto3Storage): 5 | location = "static" 6 | default_acl = "public-read" 7 | 8 | 9 | class MediaRootS3Boto3Storage(S3Boto3Storage): 10 | location = "media" 11 | file_overwrite = False 12 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = ./_build 10 | APP = /app 11 | 12 | .PHONY: help livehtml apidocs Makefile 13 | 14 | # Put it first so that "make" without argument is like "make help". 15 | help: 16 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -c . 17 | 18 | # Build, watch and serve docs with live reload 19 | livehtml: 20 | sphinx-autobuild -b html --host 0.0.0.0 --port 7000 --watch $(APP) -c . $(SOURCEDIR) $(BUILDDIR)/html 21 | 22 | # Outputs rst files from django application code 23 | apidocs: 24 | sphinx-apidoc -o $(SOURCEDIR)/api $(APP) 25 | 26 | # Catch-all target: route all unknown targets to Sphinx using the new 27 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 28 | %: Makefile 29 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -c . 30 | -------------------------------------------------------------------------------- /docs/__init__.py: -------------------------------------------------------------------------------- 1 | # Included so that Django's startproject comment runs against the docs directory 2 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | 13 | import os 14 | import sys 15 | import django 16 | 17 | if os.getenv("READTHEDOCS", default=False) == "True": 18 | sys.path.insert(0, os.path.abspath("..")) 19 | os.environ["DJANGO_READ_DOT_ENV_FILE"] = "True" 20 | os.environ["USE_DOCKER"] = "no" 21 | else: 22 | sys.path.insert(0, os.path.abspath("/app")) 23 | os.environ["DATABASE_URL"] = "sqlite:///readthedocs.db" 24 | os.environ["CELERY_BROKER_URL"] = os.getenv("REDIS_URL", "redis://redis:6379") 25 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") 26 | django.setup() 27 | 28 | # -- Project information ----------------------------------------------------- 29 | 30 | project = "djsniper" 31 | copyright = """2021, Matthew Freire""" 32 | author = "Matthew Freire" 33 | 34 | 35 | # -- General configuration --------------------------------------------------- 36 | 37 | # Add any Sphinx extension module names here, as strings. They can be 38 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 39 | # ones. 40 | extensions = [ 41 | "sphinx.ext.autodoc", 42 | "sphinx.ext.napoleon", 43 | ] 44 | 45 | # Add any paths that contain templates here, relative to this directory. 46 | # templates_path = ["_templates"] 47 | 48 | # List of patterns, relative to source directory, that match files and 49 | # directories to ignore when looking for source files. 50 | # This pattern also affects html_static_path and html_extra_path. 51 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 52 | 53 | # -- Options for HTML output ------------------------------------------------- 54 | 55 | # The theme to use for HTML and HTML Help pages. See the documentation for 56 | # a list of builtin themes. 57 | # 58 | html_theme = "alabaster" 59 | 60 | # Add any paths that contain custom static files (such as style sheets) here, 61 | # relative to this directory. They are copied after the builtin static files, 62 | # so a file named "default.css" will overwrite the builtin "default.css". 63 | # html_static_path = ["_static"] 64 | -------------------------------------------------------------------------------- /docs/howto.rst: -------------------------------------------------------------------------------- 1 | How To - Project Documentation 2 | ====================================================================== 3 | 4 | Get Started 5 | ---------------------------------------------------------------------- 6 | 7 | Documentation can be written as rst files in `djsniper/docs`. 8 | 9 | 10 | To build and serve docs, use the commands:: 11 | 12 | docker-compose -f local.yml up docs 13 | 14 | 15 | 16 | Changes to files in `docs/_source` will be picked up and reloaded automatically. 17 | 18 | `Sphinx `_ is the tool used to build documentation. 19 | 20 | Docstrings to Documentation 21 | ---------------------------------------------------------------------- 22 | 23 | The sphinx extension `apidoc `_ is used to automatically document code using signatures and docstrings. 24 | 25 | Numpy or Google style docstrings will be picked up from project files and availble for documentation. See the `Napoleon `_ extension for details. 26 | 27 | For an in-use example, see the `page source <_sources/users.rst.txt>`_ for :ref:`users`. 28 | 29 | To compile all docstrings automatically into documentation source files, use the command: 30 | :: 31 | 32 | make apidocs 33 | 34 | 35 | This can be done in the docker container: 36 | :: 37 | 38 | docker run --rm docs make apidocs 39 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. djsniper documentation master file, created by 2 | sphinx-quickstart. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to djsniper's documentation! 7 | ====================================================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | howto 14 | pycharm/configuration 15 | users 16 | 17 | 18 | 19 | Indices and tables 20 | ================== 21 | 22 | * :ref:`genindex` 23 | * :ref:`modindex` 24 | * :ref:`search` 25 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | 8 | if "%SPHINXBUILD%" == "" ( 9 | set SPHINXBUILD=sphinx-build -c . 10 | ) 11 | set SOURCEDIR=_source 12 | set BUILDDIR=_build 13 | set APP=..\djsniper 14 | 15 | if "%1" == "" goto help 16 | 17 | %SPHINXBUILD% >NUL 2>NUL 18 | if errorlevel 9009 ( 19 | echo. 20 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 21 | echo.installed, then set the SPHINXBUILD environment variable to point 22 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 23 | echo.may add the Sphinx directory to PATH. 24 | echo. 25 | echo.Install sphinx-autobuild for live serving. 26 | echo.If you don't have Sphinx installed, grab it from 27 | echo.http://sphinx-doc.org/ 28 | exit /b 1 29 | ) 30 | 31 | %SPHINXBUILD% -b %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 32 | goto end 33 | 34 | :livehtml 35 | sphinx-autobuild -b html --open-browser -p 7000 --watch %APP% -c . %SOURCEDIR% %BUILDDIR%/html 36 | GOTO :EOF 37 | 38 | :apidocs 39 | sphinx-apidoc -o %SOURCEDIR%/api %APP% 40 | GOTO :EOF 41 | 42 | :help 43 | %SPHINXBUILD% -b help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 44 | 45 | :end 46 | popd 47 | -------------------------------------------------------------------------------- /docs/pycharm/configuration.rst: -------------------------------------------------------------------------------- 1 | Docker Remote Debugging 2 | ======================= 3 | 4 | To connect to python remote interpreter inside docker, you have to make sure first, that Pycharm is aware of your docker. 5 | 6 | Go to *Settings > Build, Execution, Deployment > Docker*. If you are on linux, you can use docker directly using its socket `unix:///var/run/docker.sock`, if you are on Windows or Mac, make sure that you have docker-machine installed, then you can simply *Import credentials from Docker Machine*. 7 | 8 | .. image:: images/1.png 9 | 10 | Configure Remote Python Interpreter 11 | ----------------------------------- 12 | 13 | This repository comes with already prepared "Run/Debug Configurations" for docker. 14 | 15 | .. image:: images/2.png 16 | 17 | But as you can see, at the beginning there is something wrong with them. They have red X on django icon, and they cannot be used, without configuring remote python interpreter. To do that, you have to go to *Settings > Build, Execution, Deployment* first. 18 | 19 | 20 | Next, you have to add new remote python interpreter, based on already tested deployment settings. Go to *Settings > Project > Project Interpreter*. Click on the cog icon, and click *Add Remote*. 21 | 22 | .. image:: images/3.png 23 | 24 | Switch to *Docker Compose* and select `local.yml` file from directory of your project, next set *Service name* to `django` 25 | 26 | .. image:: images/4.png 27 | 28 | Having that, click *OK*. Close *Settings* panel, and wait few seconds... 29 | 30 | .. image:: images/7.png 31 | 32 | After few seconds, all *Run/Debug Configurations* should be ready to use. 33 | 34 | .. image:: images/8.png 35 | 36 | **Things you can do with provided configuration**: 37 | 38 | * run and debug python code 39 | 40 | .. image:: images/f1.png 41 | 42 | * run and debug tests 43 | 44 | .. image:: images/f2.png 45 | .. image:: images/f3.png 46 | 47 | * run and debug migrations or different django management commands 48 | 49 | .. image:: images/f4.png 50 | 51 | * and many others.. 52 | 53 | Known issues 54 | ------------ 55 | 56 | * Pycharm hangs on "Connecting to Debugger" 57 | 58 | .. image:: images/issue1.png 59 | 60 | This might be fault of your firewall. Take a look on this ticket - https://youtrack.jetbrains.com/issue/PY-18913 61 | 62 | * Modified files in `.idea` directory 63 | 64 | Most of the files from `.idea/` were added to `.gitignore` with a few exceptions, which were made, to provide "ready to go" configuration. After adding remote interpreter some of these files are altered by PyCharm: 65 | 66 | .. image:: images/issue2.png 67 | 68 | In theory you can remove them from repository, but then, other people will lose a ability to initialize a project from provided configurations as you did. To get rid of this annoying state, you can run command:: 69 | 70 | $ git update-index --assume-unchanged djsniper.iml 71 | -------------------------------------------------------------------------------- /docs/pycharm/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/docs/pycharm/images/1.png -------------------------------------------------------------------------------- /docs/pycharm/images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/docs/pycharm/images/2.png -------------------------------------------------------------------------------- /docs/pycharm/images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/docs/pycharm/images/3.png -------------------------------------------------------------------------------- /docs/pycharm/images/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/docs/pycharm/images/4.png -------------------------------------------------------------------------------- /docs/pycharm/images/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/docs/pycharm/images/7.png -------------------------------------------------------------------------------- /docs/pycharm/images/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/docs/pycharm/images/8.png -------------------------------------------------------------------------------- /docs/pycharm/images/f1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/docs/pycharm/images/f1.png -------------------------------------------------------------------------------- /docs/pycharm/images/f2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/docs/pycharm/images/f2.png -------------------------------------------------------------------------------- /docs/pycharm/images/f3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/docs/pycharm/images/f3.png -------------------------------------------------------------------------------- /docs/pycharm/images/f4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/docs/pycharm/images/f4.png -------------------------------------------------------------------------------- /docs/pycharm/images/issue1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/docs/pycharm/images/issue1.png -------------------------------------------------------------------------------- /docs/pycharm/images/issue2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django-nft-sniper/75306fa223493ec60cf621489c264b43110970c8/docs/pycharm/images/issue2.png -------------------------------------------------------------------------------- /docs/users.rst: -------------------------------------------------------------------------------- 1 | .. _users: 2 | 3 | Users 4 | ====================================================================== 5 | 6 | Starting a new project, it’s highly recommended to set up a custom user model, 7 | even if the default User model is sufficient for you. 8 | 9 | This model behaves identically to the default user model, 10 | but you’ll be able to customize it in the future if the need arises. 11 | 12 | .. automodule:: djsniper.users.models 13 | :members: 14 | :noindex: 15 | 16 | -------------------------------------------------------------------------------- /local.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | volumes: 4 | local_postgres_data: {} 5 | local_postgres_data_backups: {} 6 | 7 | services: 8 | django: &django 9 | build: 10 | context: . 11 | dockerfile: ./compose/local/django/Dockerfile 12 | image: djsniper_local_django 13 | container_name: sniper_django 14 | depends_on: 15 | - postgres 16 | - redis 17 | volumes: 18 | - .:/app:z 19 | env_file: 20 | - ./.envs/.local/.django 21 | - ./.envs/.local/.postgres 22 | ports: 23 | - "8000:8000" 24 | command: /start 25 | 26 | postgres: 27 | build: 28 | context: . 29 | dockerfile: ./compose/production/postgres/Dockerfile 30 | image: djsniper_production_postgres 31 | container_name: sniper_postgres 32 | volumes: 33 | - local_postgres_data:/var/lib/postgresql/data:Z 34 | - local_postgres_data_backups:/backups:z 35 | env_file: 36 | - ./.envs/.local/.postgres 37 | 38 | # docs: 39 | # image: djsniper_local_docs 40 | # container_name: sniper_docs 41 | # build: 42 | # context: . 43 | # dockerfile: ./compose/local/docs/Dockerfile 44 | # env_file: 45 | # - ./.envs/.local/.django 46 | # volumes: 47 | # - ./docs:/docs:z 48 | # - ./config:/app/config:z 49 | # - ./djsniper:/app/djsniper:z 50 | # ports: 51 | # - "7000:7000" 52 | # command: /start-docs 53 | 54 | redis: 55 | image: redis:6 56 | container_name: sniper_redis 57 | 58 | celeryworker: 59 | <<: *django 60 | image: djsniper_local_celeryworker 61 | container_name: sniper_celeryworker 62 | depends_on: 63 | - redis 64 | - postgres 65 | ports: [] 66 | command: /start-celeryworker 67 | 68 | celerybeat: 69 | <<: *django 70 | image: djsniper_local_celerybeat 71 | container_name: sniper_celerybeat 72 | depends_on: 73 | - redis 74 | - postgres 75 | ports: [] 76 | command: /start-celerybeat 77 | 78 | flower: 79 | <<: *django 80 | image: djsniper_local_flower 81 | container_name: sniper_flower 82 | ports: 83 | - "5555:5555" 84 | command: /start-flower 85 | -------------------------------------------------------------------------------- /locale/README.rst: -------------------------------------------------------------------------------- 1 | Translations 2 | ============ 3 | 4 | Translations will be placed in this folder when running:: 5 | 6 | python manage.py makemessages 7 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | from pathlib import Path 5 | 6 | if __name__ == "__main__": 7 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") 8 | 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError: 12 | # The above import may fail for some other reason. Ensure that the 13 | # issue is really that Django is missing to avoid masking other 14 | # exceptions on Python 2. 15 | try: 16 | import django # noqa 17 | except ImportError: 18 | raise ImportError( 19 | "Couldn't import Django. Are you sure it's installed and " 20 | "available on your PYTHONPATH environment variable? Did you " 21 | "forget to activate a virtual environment?" 22 | ) 23 | 24 | raise 25 | 26 | # This allows easy placement of apps within the interior 27 | # djsniper directory. 28 | current_path = Path(__file__).parent.resolve() 29 | sys.path.append(str(current_path / "djsniper")) 30 | 31 | execute_from_command_line(sys.argv) 32 | -------------------------------------------------------------------------------- /merge_production_dotenvs_in_dotenv.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | from typing import Sequence 4 | 5 | import pytest 6 | 7 | ROOT_DIR_PATH = Path(__file__).parent.resolve() 8 | PRODUCTION_DOTENVS_DIR_PATH = ROOT_DIR_PATH / ".envs" / ".production" 9 | PRODUCTION_DOTENV_FILE_PATHS = [ 10 | PRODUCTION_DOTENVS_DIR_PATH / ".django", 11 | PRODUCTION_DOTENVS_DIR_PATH / ".postgres", 12 | ] 13 | DOTENV_FILE_PATH = ROOT_DIR_PATH / ".env" 14 | 15 | 16 | def merge( 17 | output_file_path: str, merged_file_paths: Sequence[str], append_linesep: bool = True 18 | ) -> None: 19 | with open(output_file_path, "w") as output_file: 20 | for merged_file_path in merged_file_paths: 21 | with open(merged_file_path, "r") as merged_file: 22 | merged_file_content = merged_file.read() 23 | output_file.write(merged_file_content) 24 | if append_linesep: 25 | output_file.write(os.linesep) 26 | 27 | 28 | def main(): 29 | merge(DOTENV_FILE_PATH, PRODUCTION_DOTENV_FILE_PATHS) 30 | 31 | 32 | @pytest.mark.parametrize("merged_file_count", range(3)) 33 | @pytest.mark.parametrize("append_linesep", [True, False]) 34 | def test_merge(tmpdir_factory, merged_file_count: int, append_linesep: bool): 35 | tmp_dir_path = Path(str(tmpdir_factory.getbasetemp())) 36 | 37 | output_file_path = tmp_dir_path / ".env" 38 | 39 | expected_output_file_content = "" 40 | merged_file_paths = [] 41 | for i in range(merged_file_count): 42 | merged_file_ord = i + 1 43 | 44 | merged_filename = ".service{}".format(merged_file_ord) 45 | merged_file_path = tmp_dir_path / merged_filename 46 | 47 | merged_file_content = merged_filename * merged_file_ord 48 | 49 | with open(merged_file_path, "w+") as file: 50 | file.write(merged_file_content) 51 | 52 | expected_output_file_content += merged_file_content 53 | if append_linesep: 54 | expected_output_file_content += os.linesep 55 | 56 | merged_file_paths.append(merged_file_path) 57 | 58 | merge(output_file_path, merged_file_paths, append_linesep) 59 | 60 | with open(output_file_path, "r") as output_file: 61 | actual_output_file_content = output_file.read() 62 | 63 | assert actual_output_file_content == expected_output_file_content 64 | 65 | 66 | if __name__ == "__main__": 67 | main() 68 | -------------------------------------------------------------------------------- /production.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | volumes: 4 | production_postgres_data: {} 5 | production_postgres_data_backups: {} 6 | production_traefik: {} 7 | 8 | services: 9 | django: &django 10 | build: 11 | context: . 12 | dockerfile: ./compose/production/django/Dockerfile 13 | image: djsniper_production_django 14 | depends_on: 15 | - postgres 16 | - redis 17 | env_file: 18 | - ./.envs/.production/.django 19 | - ./.envs/.production/.postgres 20 | command: /start 21 | 22 | postgres: 23 | build: 24 | context: . 25 | dockerfile: ./compose/production/postgres/Dockerfile 26 | image: djsniper_production_postgres 27 | volumes: 28 | - production_postgres_data:/var/lib/postgresql/data:Z 29 | - production_postgres_data_backups:/backups:z 30 | env_file: 31 | - ./.envs/.production/.postgres 32 | 33 | traefik: 34 | build: 35 | context: . 36 | dockerfile: ./compose/production/traefik/Dockerfile 37 | image: djsniper_production_traefik 38 | depends_on: 39 | - django 40 | volumes: 41 | - production_traefik:/etc/traefik/acme:z 42 | ports: 43 | - "0.0.0.0:80:80" 44 | - "0.0.0.0:443:443" 45 | - "0.0.0.0:5555:5555" 46 | 47 | redis: 48 | image: redis:6 49 | 50 | celeryworker: 51 | <<: *django 52 | image: djsniper_production_celeryworker 53 | command: /start-celeryworker 54 | 55 | celerybeat: 56 | <<: *django 57 | image: djsniper_production_celerybeat 58 | command: /start-celerybeat 59 | 60 | flower: 61 | <<: *django 62 | image: djsniper_production_flower 63 | command: /start-flower 64 | 65 | awscli: 66 | build: 67 | context: . 68 | dockerfile: ./compose/production/aws/Dockerfile 69 | env_file: 70 | - ./.envs/.production/.django 71 | volumes: 72 | - production_postgres_data_backups:/backups:z 73 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --ds=config.settings.test --reuse-db 3 | python_files = tests.py test_*.py 4 | -------------------------------------------------------------------------------- /requirements/base.txt: -------------------------------------------------------------------------------- 1 | pytz==2021.3 # https://github.com/stub42/pytz 2 | python-slugify==5.0.2 # https://github.com/un33k/python-slugify 3 | Pillow==8.4.0 # https://github.com/python-pillow/Pillow 4 | argon2-cffi==21.1.0 # https://github.com/hynek/argon2_cffi 5 | redis==3.5.3 # https://github.com/redis/redis-py 6 | hiredis==2.0.0 # https://github.com/redis/hiredis-py 7 | celery==5.2.1 # pyup: < 6.0 # https://github.com/celery/celery 8 | django-celery-beat==2.2.1 # https://github.com/celery/django-celery-beat 9 | flower==1.0.0 # https://github.com/mher/flower 10 | web3==5.25.0 # https://github.com/ethereum/web3.py 11 | 12 | # Django 13 | # ------------------------------------------------------------------------------ 14 | django==3.2.9 # pyup: < 4.0 # https://www.djangoproject.com/ 15 | django-environ==0.8.1 # https://github.com/joke2k/django-environ 16 | django-model-utils==4.2.0 # https://github.com/jazzband/django-model-utils 17 | django-allauth==0.46.0 # https://github.com/pennersr/django-allauth 18 | django-crispy-forms==1.13.0 # https://github.com/django-crispy-forms/django-crispy-forms 19 | crispy-bootstrap5==0.6 # https://github.com/django-crispy-forms/crispy-bootstrap5 20 | django-redis==5.1.0 # https://github.com/jazzband/django-redis 21 | # Django REST Framework 22 | djangorestframework==3.12.4 # https://github.com/encode/django-rest-framework 23 | django-cors-headers==3.10.1 # https://github.com/adamchainz/django-cors-headers 24 | crispy-tailwind==0.5.0 # https://github.com/django-crispy-forms/crispy-tailwind 25 | celery-progress==0.1.1 -------------------------------------------------------------------------------- /requirements/local.txt: -------------------------------------------------------------------------------- 1 | -r base.txt 2 | 3 | Werkzeug==2.0.2 # https://github.com/pallets/werkzeug 4 | ipdb==0.13.9 # https://github.com/gotcha/ipdb 5 | psycopg2==2.9.2 # https://github.com/psycopg/psycopg2 6 | watchgod==0.7 # https://github.com/samuelcolvin/watchgod 7 | 8 | # Testing 9 | # ------------------------------------------------------------------------------ 10 | mypy==0.910 # https://github.com/python/mypy 11 | django-stubs==1.9.0 # https://github.com/typeddjango/django-stubs 12 | pytest==6.2.5 # https://github.com/pytest-dev/pytest 13 | pytest-sugar==0.9.4 # https://github.com/Frozenball/pytest-sugar 14 | djangorestframework-stubs==1.4.0 # https://github.com/typeddjango/djangorestframework-stubs 15 | 16 | # Documentation 17 | # ------------------------------------------------------------------------------ 18 | sphinx==4.3.1 # https://github.com/sphinx-doc/sphinx 19 | sphinx-autobuild==2021.3.14 # https://github.com/GaretJax/sphinx-autobuild 20 | 21 | # Code quality 22 | # ------------------------------------------------------------------------------ 23 | flake8==4.0.1 # https://github.com/PyCQA/flake8 24 | flake8-isort==4.1.1 # https://github.com/gforcada/flake8-isort 25 | coverage==6.2 # https://github.com/nedbat/coveragepy 26 | black==21.12b0 # https://github.com/psf/black 27 | pylint-django==2.4.4 # https://github.com/PyCQA/pylint-django 28 | pylint-celery==0.3 # https://github.com/PyCQA/pylint-celery 29 | pre-commit==2.16.0 # https://github.com/pre-commit/pre-commit 30 | 31 | # Django 32 | # ------------------------------------------------------------------------------ 33 | factory-boy==3.2.1 # https://github.com/FactoryBoy/factory_boy 34 | 35 | django-debug-toolbar==3.2.2 # https://github.com/jazzband/django-debug-toolbar 36 | django-extensions==3.1.5 # https://github.com/django-extensions/django-extensions 37 | django-coverage-plugin==2.0.2 # https://github.com/nedbat/django_coverage_plugin 38 | pytest-django==4.5.1 # https://github.com/pytest-dev/pytest-django 39 | -------------------------------------------------------------------------------- /requirements/production.txt: -------------------------------------------------------------------------------- 1 | # PRECAUTION: avoid production dependencies that aren't in development 2 | 3 | -r base.txt 4 | 5 | gunicorn==20.1.0 # https://github.com/benoitc/gunicorn 6 | psycopg2==2.9.2 # https://github.com/psycopg/psycopg2 7 | Collectfast==2.2.0 # https://github.com/antonagestam/collectfast 8 | sentry-sdk==1.5.0 # https://github.com/getsentry/sentry-python 9 | 10 | # Django 11 | # ------------------------------------------------------------------------------ 12 | django-storages[boto3]==1.12.3 # https://github.com/jschneier/django-storages 13 | django-anymail[mailgun]==8.4 # https://github.com/anymail/django-anymail 14 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv 4 | 5 | [pycodestyle] 6 | max-line-length = 120 7 | exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv 8 | 9 | [isort] 10 | line_length = 88 11 | known_first_party = djsniper,config 12 | multi_line_output = 3 13 | default_section = THIRDPARTY 14 | skip = venv/ 15 | skip_glob = **/migrations/*.py 16 | include_trailing_comma = true 17 | force_grid_wrap = 0 18 | use_parentheses = true 19 | 20 | [mypy] 21 | python_version = 3.9 22 | check_untyped_defs = True 23 | ignore_missing_imports = True 24 | warn_unused_ignores = True 25 | warn_redundant_casts = True 26 | warn_unused_configs = True 27 | plugins = mypy_django_plugin.main, mypy_drf_plugin.main 28 | 29 | [mypy.plugins.django-stubs] 30 | django_settings_module = config.settings.test 31 | 32 | [mypy-*.migrations.*] 33 | # Django migrations should not produce any errors: 34 | ignore_errors = True 35 | 36 | [coverage:run] 37 | include = djsniper/* 38 | omit = *migrations*, *tests* 39 | plugins = 40 | django_coverage_plugin 41 | --------------------------------------------------------------------------------