├── .DS_Store ├── .dockerignore ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTORS.txt ├── LICENSE ├── README.md ├── SECURITY.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 ├── 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 ├── frontend ├── .eslintrc ├── .gitignore ├── next.config.js ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ └── vercel.svg └── src │ ├── actions │ ├── auth.js │ └── types.js │ ├── components │ └── Navbar.js │ ├── config │ └── index.js │ ├── hocs │ └── Layout.js │ ├── pages │ ├── _app.js │ ├── api │ │ └── account │ │ │ ├── login.js │ │ │ ├── logout.js │ │ │ ├── refresh.js │ │ │ ├── register.js │ │ │ ├── user.js │ │ │ └── verify.js │ ├── dashboard.js │ ├── index.js │ ├── login.js │ └── register.js │ ├── reducers │ ├── auth.js │ └── index.js │ └── store.js ├── local.yml ├── locale └── README.rst ├── manage.py ├── mentors ├── __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 ├── static │ ├── css │ │ └── project.css │ ├── fonts │ │ └── .gitkeep │ ├── images │ │ └── favicons │ │ │ └── favicon.ico │ ├── js │ │ └── project.js │ └── sass │ │ ├── custom_bootstrap_vars.scss │ │ └── project.scss ├── templates │ ├── 403.html │ ├── 404.html │ ├── 500.html │ ├── account │ │ ├── account_inactive.html │ │ ├── base.html │ │ ├── email.html │ │ ├── email_confirm.html │ │ ├── login.html │ │ ├── logout.html │ │ ├── password_change.html │ │ ├── password_reset.html │ │ ├── password_reset_done.html │ │ ├── password_reset_from_key.html │ │ ├── password_reset_from_key_done.html │ │ ├── password_set.html │ │ ├── signup.html │ │ ├── signup_closed.html │ │ ├── verification_sent.html │ │ └── verified_email_required.html │ ├── base.html │ ├── pages │ │ ├── about.html │ │ └── home.html │ └── users │ │ ├── user_detail.html │ │ └── user_form.html ├── users │ ├── __init__.py │ ├── adapters.py │ ├── admin.py │ ├── api │ │ ├── serializers.py │ │ └── views.py │ ├── apps.py │ ├── context_processors.py │ ├── forms.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── serializers.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_swagger_ui.py │ │ ├── test_tasks.py │ │ ├── test_urls.py │ │ └── test_views.py │ ├── urls.py │ └── views.py └── utils │ ├── __init__.py │ └── storages.py ├── merge_production_dotenvs_in_dotenv.py ├── production.yml ├── pytest.ini ├── requirements ├── base.txt ├── local.txt └── production.txt └── setup.cfg /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python019/marketplace-django/3c742aee405f24463e42775c08a75941e2daeddf/.DS_Store -------------------------------------------------------------------------------- /.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,xml}] 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 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Celery stuff 108 | celerybeat-schedule 109 | celerybeat.pid 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | ENV/ 120 | env.bak/ 121 | venv.bak/ 122 | 123 | # Spyder project settings 124 | .spyderproject 125 | .spyproject 126 | 127 | # Rope project settings 128 | .ropeproject 129 | 130 | # mkdocs documentation 131 | /site 132 | 133 | # mypy 134 | .mypy_cache/ 135 | .dmypy.json 136 | dmypy.json 137 | 138 | # Pyre type checker 139 | .pyre/ 140 | 141 | # pytype static type analyzer 142 | .pytype/ 143 | 144 | # Cython debug symbols 145 | cython_debug/ 146 | 147 | # PyCharm 148 | # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can 149 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 150 | # and can be added to the global gitignore or merged into this file. For a more nuclear 151 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 152 | #.idea/ 153 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Docker Node.js Launch", 5 | "type": "docker", 6 | "request": "launch", 7 | "preLaunchTask": "docker-run: debug", 8 | "platform": "node", 9 | "node": { 10 | "package": "${workspaceFolder}/frontend/package.json", 11 | "localRoot": "${workspaceFolder}/frontend" 12 | } 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "docker-build", 6 | "label": "docker-build", 7 | "platform": "node", 8 | "dockerBuild": { 9 | "dockerfile": "${workspaceFolder}/frontend/Dockerfile", 10 | "context": "${workspaceFolder}/frontend", 11 | "pull": true 12 | }, 13 | "node": { 14 | "package": "${workspaceFolder}/frontend/package.json" 15 | } 16 | }, 17 | { 18 | "type": "docker-run", 19 | "label": "docker-run: release", 20 | "dependsOn": [ 21 | "docker-build" 22 | ], 23 | "platform": "node", 24 | "node": { 25 | "package": "${workspaceFolder}/frontend/package.json" 26 | } 27 | }, 28 | { 29 | "type": "docker-run", 30 | "label": "docker-run: debug", 31 | "dependsOn": [ 32 | "docker-build" 33 | ], 34 | "dockerRun": { 35 | "env": { 36 | "DEBUG": "*", 37 | "NODE_ENV": "development" 38 | } 39 | }, 40 | "node": { 41 | "package": "${workspaceFolder}/frontend/package.json", 42 | "enableDebugging": true 43 | } 44 | } 45 | ] 46 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at susyswdg@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | Jumayev Ubaydullo 2 | Crimson Development Group 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jumayev Ubaydullo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Marketplace -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | Security Policy 2 | 3 | The SUSYS team and community take security issues seriously. To report a security issue, write to susyswdg@gmail.com 4 | 5 | You can help us make SUSYS and all it's users more secure by following the Reporting guidelines. 6 | 7 | We appreciate your efforts to responsibly disclose your findings. We'll endeavor to respond quickly, and will keep you updated throughout the process. -------------------------------------------------------------------------------- /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 | 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 | 7 | # Python build stage 8 | FROM python as python-build-stage 9 | 10 | ENV PYTHONDONTWRITEBYTECODE 1 11 | 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 | # cleaning up unused files 18 | && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ 19 | && rm -rf /var/lib/apt/lists/* 20 | 21 | # Requirements are installed here to ensure they will be cached. 22 | COPY ./requirements /requirements 23 | 24 | # create python dependency wheels 25 | RUN pip wheel --no-cache-dir --no-deps --wheel-dir /usr/src/app/wheels \ 26 | -r /requirements/local.txt -r /requirements/production.txt \ 27 | && rm -rf /requirements 28 | 29 | 30 | # Python 'run' stage 31 | FROM python as python-run-stage 32 | 33 | ARG BUILD_ENVIRONMENT 34 | ENV PYTHONUNBUFFERED 1 35 | ENV PYTHONDONTWRITEBYTECODE 1 36 | 37 | RUN apt-get update && apt-get install --no-install-recommends -y \ 38 | # To run the Makefile 39 | make \ 40 | # psycopg2 dependencies 41 | libpq-dev \ 42 | # Translations dependencies 43 | gettext \ 44 | # Uncomment below lines to enable Sphinx output to latex and pdf 45 | # texlive-latex-recommended \ 46 | # texlive-fonts-recommended \ 47 | # texlive-latex-extra \ 48 | # latexmk \ 49 | # cleaning up unused files 50 | && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ 51 | && rm -rf /var/lib/apt/lists/* 52 | 53 | # copy python dependency wheels from python-build-stage 54 | COPY --from=python-build-stage /usr/src/app/wheels /wheels 55 | 56 | # use wheels to install python dependencies 57 | RUN pip install --no-cache /wheels/* \ 58 | && rm -rf /wheels 59 | 60 | COPY ./compose/local/docs/start /start-docs 61 | RUN sed -i 's/\r$//g' /start-docs 62 | RUN chmod +x /start-docs 63 | 64 | WORKDIR /docs 65 | -------------------------------------------------------------------------------- /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 | /usr/local/bin/gunicorn config.wsgi --bind 0.0.0.0:5000 --chdir=/app 11 | -------------------------------------------------------------------------------- /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(`example.com`) || Host(`www.example.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(`example.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 django.urls import path 3 | from rest_framework.routers import DefaultRouter, SimpleRouter 4 | 5 | from mentors.users.api.views import UserViewSet, LoadUserView 6 | 7 | if settings.DEBUG: 8 | router = DefaultRouter() 9 | else: 10 | router = SimpleRouter() 11 | 12 | router.register("users", UserViewSet) 13 | 14 | 15 | app_name = "api" 16 | urlpatterns = router.urls 17 | urlpatterns += [ 18 | path("users/me/", LoadUserView.as_view()), 19 | ] 20 | -------------------------------------------------------------------------------- /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("mentors") 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/python019/marketplace-django/3c742aee405f24463e42775c08a75941e2daeddf/config/settings/__init__.py -------------------------------------------------------------------------------- /config/settings/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Base settings to build other settings files upon. 3 | """ 4 | from datetime import timedelta 5 | from pathlib import Path 6 | 7 | import environ 8 | 9 | ROOT_DIR = Path(__file__).resolve(strict=True).parent.parent.parent 10 | # mentors/ 11 | APPS_DIR = ROOT_DIR / "mentors" 12 | env = environ.Env() 13 | 14 | READ_DOT_ENV_FILE = env.bool("DJANGO_READ_DOT_ENV_FILE", default=False) 15 | if READ_DOT_ENV_FILE: 16 | # OS environment variables take precedence over variables from .env 17 | env.read_env(str(ROOT_DIR / ".env")) 18 | 19 | # GENERAL 20 | # ------------------------------------------------------------------------------ 21 | # https://docs.djangoproject.com/en/dev/ref/settings/#debug 22 | DEBUG = env.bool("DJANGO_DEBUG", False) 23 | # Local time zone. Choices are 24 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 25 | # though not all of them may be available with every OS. 26 | # In Windows, this must be set to your system time zone. 27 | TIME_ZONE = "UTC" 28 | # https://docs.djangoproject.com/en/dev/ref/settings/#language-code 29 | LANGUAGE_CODE = "en-us" 30 | # https://docs.djangoproject.com/en/dev/ref/settings/#site-id 31 | SITE_ID = 1 32 | # https://docs.djangoproject.com/en/dev/ref/settings/#use-i18n 33 | USE_I18N = True 34 | # https://docs.djangoproject.com/en/dev/ref/settings/#use-l10n 35 | USE_L10N = True 36 | # https://docs.djangoproject.com/en/dev/ref/settings/#use-tz 37 | USE_TZ = True 38 | # https://docs.djangoproject.com/en/dev/ref/settings/#locale-paths 39 | LOCALE_PATHS = [str(ROOT_DIR / "locale")] 40 | 41 | # DATABASES 42 | # ------------------------------------------------------------------------------ 43 | # https://docs.djangoproject.com/en/dev/ref/settings/#databases 44 | DATABASES = {"default": env.db("DATABASE_URL")} 45 | DATABASES["default"]["ATOMIC_REQUESTS"] = True 46 | # https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-DEFAULT_AUTO_FIELD 47 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 48 | 49 | # URLS 50 | # ------------------------------------------------------------------------------ 51 | # https://docs.djangoproject.com/en/dev/ref/settings/#root-urlconf 52 | ROOT_URLCONF = "config.urls" 53 | # https://docs.djangoproject.com/en/dev/ref/settings/#wsgi-application 54 | WSGI_APPLICATION = "config.wsgi.application" 55 | 56 | # APPS 57 | # ------------------------------------------------------------------------------ 58 | DJANGO_APPS = [ 59 | "django.contrib.auth", 60 | "django.contrib.contenttypes", 61 | "django.contrib.sessions", 62 | "django.contrib.sites", 63 | "django.contrib.messages", 64 | "django.contrib.staticfiles", 65 | # "django.contrib.humanize", # Handy template tags 66 | "django.contrib.admin", 67 | "django.forms", 68 | ] 69 | THIRD_PARTY_APPS = [ 70 | "crispy_forms", 71 | "crispy_bootstrap5", 72 | "allauth", 73 | "allauth.account", 74 | "allauth.socialaccount", 75 | "django_celery_beat", 76 | "rest_framework", 77 | "rest_framework.authtoken", 78 | "corsheaders", 79 | "drf_spectacular", 80 | 'rest_framework_simplejwt.token_blacklist', 81 | ] 82 | 83 | LOCAL_APPS = [ 84 | "mentors.users", 85 | # Your stuff: custom apps go here 86 | ] 87 | # https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps 88 | INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS 89 | 90 | # MIGRATIONS 91 | # ------------------------------------------------------------------------------ 92 | # https://docs.djangoproject.com/en/dev/ref/settings/#migration-modules 93 | MIGRATION_MODULES = {"sites": "mentors.contrib.sites.migrations"} 94 | 95 | # AUTHENTICATION 96 | # ------------------------------------------------------------------------------ 97 | # https://docs.djangoproject.com/en/dev/ref/settings/#authentication-backends 98 | AUTHENTICATION_BACKENDS = [ 99 | "django.contrib.auth.backends.ModelBackend", 100 | "allauth.account.auth_backends.AuthenticationBackend", 101 | ] 102 | # https://docs.djangoproject.com/en/dev/ref/settings/#auth-user-model 103 | AUTH_USER_MODEL = "users.User" 104 | # https://docs.djangoproject.com/en/dev/ref/settings/#login-redirect-url 105 | LOGIN_REDIRECT_URL = "users:redirect" 106 | # https://docs.djangoproject.com/en/dev/ref/settings/#login-url 107 | LOGIN_URL = "account_login" 108 | 109 | # PASSWORDS 110 | # ------------------------------------------------------------------------------ 111 | # https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers 112 | PASSWORD_HASHERS = [ 113 | # https://docs.djangoproject.com/en/dev/topics/auth/passwords/#using-argon2-with-django 114 | "django.contrib.auth.hashers.Argon2PasswordHasher", 115 | "django.contrib.auth.hashers.PBKDF2PasswordHasher", 116 | "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher", 117 | "django.contrib.auth.hashers.BCryptSHA256PasswordHasher", 118 | ] 119 | # https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators 120 | AUTH_PASSWORD_VALIDATORS = [ 121 | { 122 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" 123 | }, 124 | {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, 125 | {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, 126 | {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, 127 | ] 128 | 129 | # MIDDLEWARE 130 | # ------------------------------------------------------------------------------ 131 | # https://docs.djangoproject.com/en/dev/ref/settings/#middleware 132 | MIDDLEWARE = [ 133 | "django.middleware.security.SecurityMiddleware", 134 | "corsheaders.middleware.CorsMiddleware", 135 | "django.contrib.sessions.middleware.SessionMiddleware", 136 | "django.middleware.locale.LocaleMiddleware", 137 | "django.middleware.common.CommonMiddleware", 138 | "django.middleware.csrf.CsrfViewMiddleware", 139 | "django.contrib.auth.middleware.AuthenticationMiddleware", 140 | "django.contrib.messages.middleware.MessageMiddleware", 141 | "django.middleware.common.BrokenLinkEmailsMiddleware", 142 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 143 | ] 144 | 145 | # STATIC 146 | # ------------------------------------------------------------------------------ 147 | # https://docs.djangoproject.com/en/dev/ref/settings/#static-root 148 | STATIC_ROOT = str(ROOT_DIR / "staticfiles") 149 | # https://docs.djangoproject.com/en/dev/ref/settings/#static-url 150 | STATIC_URL = "/static/" 151 | # https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS 152 | STATICFILES_DIRS = [str(APPS_DIR / "static")] 153 | # https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders 154 | STATICFILES_FINDERS = [ 155 | "django.contrib.staticfiles.finders.FileSystemFinder", 156 | "django.contrib.staticfiles.finders.AppDirectoriesFinder", 157 | ] 158 | 159 | # MEDIA 160 | # ------------------------------------------------------------------------------ 161 | # https://docs.djangoproject.com/en/dev/ref/settings/#media-root 162 | MEDIA_ROOT = str(APPS_DIR / "media") 163 | # https://docs.djangoproject.com/en/dev/ref/settings/#media-url 164 | MEDIA_URL = "/media/" 165 | 166 | # TEMPLATES 167 | # ------------------------------------------------------------------------------ 168 | # https://docs.djangoproject.com/en/dev/ref/settings/#templates 169 | TEMPLATES = [ 170 | { 171 | # https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-TEMPLATES-BACKEND 172 | "BACKEND": "django.template.backends.django.DjangoTemplates", 173 | # https://docs.djangoproject.com/en/dev/ref/settings/#dirs 174 | "DIRS": [str(APPS_DIR / "templates")], 175 | # https://docs.djangoproject.com/en/dev/ref/settings/#app-dirs 176 | "APP_DIRS": True, 177 | "OPTIONS": { 178 | # https://docs.djangoproject.com/en/dev/ref/settings/#template-context-processors 179 | "context_processors": [ 180 | "django.template.context_processors.debug", 181 | "django.template.context_processors.request", 182 | "django.contrib.auth.context_processors.auth", 183 | "django.template.context_processors.i18n", 184 | "django.template.context_processors.media", 185 | "django.template.context_processors.static", 186 | "django.template.context_processors.tz", 187 | "django.contrib.messages.context_processors.messages", 188 | "mentors.users.context_processors.allauth_settings", 189 | ], 190 | }, 191 | } 192 | ] 193 | 194 | # https://docs.djangoproject.com/en/dev/ref/settings/#form-renderer 195 | FORM_RENDERER = "django.forms.renderers.TemplatesSetting" 196 | 197 | # http://django-crispy-forms.readthedocs.io/en/latest/install.html#template-packs 198 | CRISPY_TEMPLATE_PACK = "bootstrap5" 199 | CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" 200 | 201 | # FIXTURES 202 | # ------------------------------------------------------------------------------ 203 | # https://docs.djangoproject.com/en/dev/ref/settings/#fixture-dirs 204 | FIXTURE_DIRS = (str(APPS_DIR / "fixtures"),) 205 | 206 | # SECURITY 207 | # ------------------------------------------------------------------------------ 208 | # https://docs.djangoproject.com/en/dev/ref/settings/#session-cookie-httponly 209 | SESSION_COOKIE_HTTPONLY = True 210 | # https://docs.djangoproject.com/en/dev/ref/settings/#csrf-cookie-httponly 211 | CSRF_COOKIE_HTTPONLY = True 212 | # https://docs.djangoproject.com/en/dev/ref/settings/#secure-browser-xss-filter 213 | SECURE_BROWSER_XSS_FILTER = True 214 | # https://docs.djangoproject.com/en/dev/ref/settings/#x-frame-options 215 | X_FRAME_OPTIONS = "DENY" 216 | 217 | # EMAIL 218 | # ------------------------------------------------------------------------------ 219 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend 220 | EMAIL_BACKEND = env( 221 | "DJANGO_EMAIL_BACKEND", 222 | default="django.core.mail.backends.smtp.EmailBackend", 223 | ) 224 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-timeout 225 | EMAIL_TIMEOUT = 5 226 | 227 | # ADMIN 228 | # ------------------------------------------------------------------------------ 229 | # Django Admin URL. 230 | ADMIN_URL = "admin/" 231 | # https://docs.djangoproject.com/en/dev/ref/settings/#admins 232 | ADMINS = [("""Matthew Freire""", "matt@justdjango.com")] 233 | # https://docs.djangoproject.com/en/dev/ref/settings/#managers 234 | MANAGERS = ADMINS 235 | 236 | # LOGGING 237 | # ------------------------------------------------------------------------------ 238 | # https://docs.djangoproject.com/en/dev/ref/settings/#logging 239 | # See https://docs.djangoproject.com/en/dev/topics/logging for 240 | # more details on how to customize your logging configuration. 241 | LOGGING = { 242 | "version": 1, 243 | "disable_existing_loggers": False, 244 | "formatters": { 245 | "verbose": { 246 | "format": "%(levelname)s %(asctime)s %(module)s " 247 | "%(process)d %(thread)d %(message)s" 248 | } 249 | }, 250 | "handlers": { 251 | "console": { 252 | "level": "DEBUG", 253 | "class": "logging.StreamHandler", 254 | "formatter": "verbose", 255 | } 256 | }, 257 | "root": {"level": "INFO", "handlers": ["console"]}, 258 | } 259 | 260 | # Celery 261 | # ------------------------------------------------------------------------------ 262 | if USE_TZ: 263 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-timezone 264 | CELERY_TIMEZONE = TIME_ZONE 265 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-broker_url 266 | CELERY_BROKER_URL = env("CELERY_BROKER_URL") 267 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-result_backend 268 | CELERY_RESULT_BACKEND = CELERY_BROKER_URL 269 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-accept_content 270 | CELERY_ACCEPT_CONTENT = ["json"] 271 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-task_serializer 272 | CELERY_TASK_SERIALIZER = "json" 273 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-result_serializer 274 | CELERY_RESULT_SERIALIZER = "json" 275 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#task-time-limit 276 | # TODO: set to whatever value is adequate in your circumstances 277 | CELERY_TASK_TIME_LIMIT = 5 * 60 278 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#task-soft-time-limit 279 | # TODO: set to whatever value is adequate in your circumstances 280 | CELERY_TASK_SOFT_TIME_LIMIT = 60 281 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#beat-scheduler 282 | CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler" 283 | # django-allauth 284 | # ------------------------------------------------------------------------------ 285 | ACCOUNT_ALLOW_REGISTRATION = env.bool("DJANGO_ACCOUNT_ALLOW_REGISTRATION", True) 286 | # https://django-allauth.readthedocs.io/en/latest/configuration.html 287 | ACCOUNT_AUTHENTICATION_METHOD = "username" 288 | # https://django-allauth.readthedocs.io/en/latest/configuration.html 289 | ACCOUNT_EMAIL_REQUIRED = True 290 | # https://django-allauth.readthedocs.io/en/latest/configuration.html 291 | ACCOUNT_EMAIL_VERIFICATION = "mandatory" 292 | # https://django-allauth.readthedocs.io/en/latest/configuration.html 293 | ACCOUNT_ADAPTER = "mentors.users.adapters.AccountAdapter" 294 | # https://django-allauth.readthedocs.io/en/latest/forms.html 295 | ACCOUNT_FORMS = {"signup": "mentors.users.forms.UserSignupForm"} 296 | # https://django-allauth.readthedocs.io/en/latest/configuration.html 297 | SOCIALACCOUNT_ADAPTER = "mentors.users.adapters.SocialAccountAdapter" 298 | # https://django-allauth.readthedocs.io/en/latest/forms.html 299 | SOCIALACCOUNT_FORMS = {"signup": "mentors.users.forms.UserSocialSignupForm"} 300 | 301 | # django-rest-framework 302 | # ------------------------------------------------------------------------------- 303 | # django-rest-framework - https://www.django-rest-framework.org/api-guide/settings/ 304 | REST_FRAMEWORK = { 305 | 'DEFAULT_PERMISSION_CLASSES': [ 306 | "rest_framework.permissions.IsAuthenticated", 307 | ], 308 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 309 | "rest_framework.authentication.SessionAuthentication", 310 | "rest_framework.authentication.TokenAuthentication", 311 | "rest_framework_simplejwt.authentication.JWTAuthentication", 312 | ) 313 | } 314 | 315 | SIMPLE_JWT = { 316 | 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30), 317 | 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), 318 | 'ROTATE_REFRESH_TOKENS': True, 319 | 'BLACKLIST_AFTER_ROTATION': True, 320 | 'AUTH_HEADER_TYPES': ('Bearer', ), 321 | 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken', ), 322 | } 323 | 324 | # django-cors-headers - https://github.com/adamchainz/django-cors-headers#setup 325 | CORS_URLS_REGEX = r"^/api/.*$" 326 | CORS_ALLOWED_ORIGINS = [ 327 | 'http://localhost:3000', 328 | ] 329 | 330 | # By Default swagger ui is available only to admin user. You can change permission classs to change that 331 | # See more configuration options at https://drf-spectacular.readthedocs.io/en/latest/settings.html#settings 332 | SPECTACULAR_SETTINGS = { 333 | "TITLE": "mentors API", 334 | "DESCRIPTION": "Documentation of API endpoiints of mentors", 335 | "VERSION": "1.0.0", 336 | "SERVE_PERMISSIONS": ["rest_framework.permissions.IsAdminUser"], 337 | "SERVERS": [ 338 | {"url": "https://127.0.0.1:8000", "description": "Local Development server"}, 339 | {"url": "https://example.com", "description": "Production server"}, 340 | ], 341 | } 342 | # Your stuff... 343 | # ------------------------------------------------------------------------------ 344 | -------------------------------------------------------------------------------- /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="HmlfdE0DYGVeokDl1OlXAjZ29I42Fnsx8w7Tr3qp1zfupwmCZHg1qLw1OHquFYGt", 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.celery import CeleryIntegration 5 | from sentry_sdk.integrations.django import DjangoIntegration 6 | from sentry_sdk.integrations.logging import LoggingIntegration 7 | from sentry_sdk.integrations.redis import RedisIntegration 8 | 9 | from .base import * # noqa 10 | from .base import env 11 | 12 | # GENERAL 13 | # ------------------------------------------------------------------------------ 14 | # https://docs.djangoproject.com/en/dev/ref/settings/#secret-key 15 | SECRET_KEY = env("DJANGO_SECRET_KEY") 16 | # https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts 17 | ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["example.com"]) 18 | 19 | # DATABASES 20 | # ------------------------------------------------------------------------------ 21 | DATABASES["default"] = env.db("DATABASE_URL") # noqa F405 22 | DATABASES["default"]["ATOMIC_REQUESTS"] = True # noqa F405 23 | DATABASES["default"]["CONN_MAX_AGE"] = env.int("CONN_MAX_AGE", default=60) # noqa F405 24 | 25 | # CACHES 26 | # ------------------------------------------------------------------------------ 27 | CACHES = { 28 | "default": { 29 | "BACKEND": "django_redis.cache.RedisCache", 30 | "LOCATION": env("REDIS_URL"), 31 | "OPTIONS": { 32 | "CLIENT_CLASS": "django_redis.client.DefaultClient", 33 | # Mimicing memcache behavior. 34 | # https://github.com/jazzband/django-redis#memcached-exceptions-behavior 35 | "IGNORE_EXCEPTIONS": True, 36 | }, 37 | } 38 | } 39 | 40 | # SECURITY 41 | # ------------------------------------------------------------------------------ 42 | # https://docs.djangoproject.com/en/dev/ref/settings/#secure-proxy-ssl-header 43 | SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") 44 | # https://docs.djangoproject.com/en/dev/ref/settings/#secure-ssl-redirect 45 | SECURE_SSL_REDIRECT = env.bool("DJANGO_SECURE_SSL_REDIRECT", default=True) 46 | # https://docs.djangoproject.com/en/dev/ref/settings/#session-cookie-secure 47 | SESSION_COOKIE_SECURE = True 48 | # https://docs.djangoproject.com/en/dev/ref/settings/#csrf-cookie-secure 49 | CSRF_COOKIE_SECURE = True 50 | # https://docs.djangoproject.com/en/dev/topics/security/#ssl-https 51 | # https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-seconds 52 | # TODO: set this to 60 seconds first and then to 518400 once you prove the former works 53 | SECURE_HSTS_SECONDS = 60 54 | # https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-include-subdomains 55 | SECURE_HSTS_INCLUDE_SUBDOMAINS = env.bool( 56 | "DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS", default=True 57 | ) 58 | # https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-preload 59 | SECURE_HSTS_PRELOAD = env.bool("DJANGO_SECURE_HSTS_PRELOAD", default=True) 60 | # https://docs.djangoproject.com/en/dev/ref/middleware/#x-content-type-options-nosniff 61 | SECURE_CONTENT_TYPE_NOSNIFF = env.bool( 62 | "DJANGO_SECURE_CONTENT_TYPE_NOSNIFF", default=True 63 | ) 64 | 65 | # STORAGES 66 | # ------------------------------------------------------------------------------ 67 | # https://django-storages.readthedocs.io/en/latest/#installation 68 | INSTALLED_APPS += ["storages"] # noqa F405 69 | # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings 70 | AWS_ACCESS_KEY_ID = env("DJANGO_AWS_ACCESS_KEY_ID") 71 | # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings 72 | AWS_SECRET_ACCESS_KEY = env("DJANGO_AWS_SECRET_ACCESS_KEY") 73 | # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings 74 | AWS_STORAGE_BUCKET_NAME = env("DJANGO_AWS_STORAGE_BUCKET_NAME") 75 | # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings 76 | AWS_QUERYSTRING_AUTH = False 77 | # DO NOT change these unless you know what you're doing. 78 | _AWS_EXPIRY = 60 * 60 * 24 * 7 79 | # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings 80 | AWS_S3_OBJECT_PARAMETERS = { 81 | "CacheControl": f"max-age={_AWS_EXPIRY}, s-maxage={_AWS_EXPIRY}, must-revalidate" 82 | } 83 | # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings 84 | AWS_S3_REGION_NAME = env("DJANGO_AWS_S3_REGION_NAME", default=None) 85 | # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#cloudfront 86 | AWS_S3_CUSTOM_DOMAIN = env("DJANGO_AWS_S3_CUSTOM_DOMAIN", default=None) 87 | aws_s3_domain = AWS_S3_CUSTOM_DOMAIN or f"{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com" 88 | # STATIC 89 | # ------------------------ 90 | STATICFILES_STORAGE = "mentors.utils.storages.StaticRootS3Boto3Storage" 91 | COLLECTFAST_STRATEGY = "collectfast.strategies.boto3.Boto3Strategy" 92 | STATIC_URL = f"https://{aws_s3_domain}/static/" 93 | # MEDIA 94 | # ------------------------------------------------------------------------------ 95 | DEFAULT_FILE_STORAGE = "mentors.utils.storages.MediaRootS3Boto3Storage" 96 | MEDIA_URL = f"https://{aws_s3_domain}/media/" 97 | 98 | # EMAIL 99 | # ------------------------------------------------------------------------------ 100 | # https://docs.djangoproject.com/en/dev/ref/settings/#default-from-email 101 | DEFAULT_FROM_EMAIL = env( 102 | "DJANGO_DEFAULT_FROM_EMAIL", 103 | default="mentors ", 104 | ) 105 | # https://docs.djangoproject.com/en/dev/ref/settings/#server-email 106 | SERVER_EMAIL = env("DJANGO_SERVER_EMAIL", default=DEFAULT_FROM_EMAIL) 107 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-subject-prefix 108 | EMAIL_SUBJECT_PREFIX = env( 109 | "DJANGO_EMAIL_SUBJECT_PREFIX", 110 | default="[mentors]", 111 | ) 112 | 113 | # ADMIN 114 | # ------------------------------------------------------------------------------ 115 | # Django Admin URL regex. 116 | ADMIN_URL = env("DJANGO_ADMIN_URL") 117 | 118 | # Anymail 119 | # ------------------------------------------------------------------------------ 120 | # https://anymail.readthedocs.io/en/stable/installation/#installing-anymail 121 | INSTALLED_APPS += ["anymail"] # noqa F405 122 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend 123 | # https://anymail.readthedocs.io/en/stable/installation/#anymail-settings-reference 124 | # https://anymail.readthedocs.io/en/stable/esps/mailgun/ 125 | EMAIL_BACKEND = "anymail.backends.mailgun.EmailBackend" 126 | ANYMAIL = { 127 | "MAILGUN_API_KEY": env("MAILGUN_API_KEY"), 128 | "MAILGUN_SENDER_DOMAIN": env("MAILGUN_DOMAIN"), 129 | "MAILGUN_API_URL": env("MAILGUN_API_URL", default="https://api.mailgun.net/v3"), 130 | } 131 | 132 | # Collectfast 133 | # ------------------------------------------------------------------------------ 134 | # https://github.com/antonagestam/collectfast#installation 135 | INSTALLED_APPS = ["collectfast"] + INSTALLED_APPS # noqa F405 136 | 137 | # LOGGING 138 | # ------------------------------------------------------------------------------ 139 | # https://docs.djangoproject.com/en/dev/ref/settings/#logging 140 | # See https://docs.djangoproject.com/en/dev/topics/logging for 141 | # more details on how to customize your logging configuration. 142 | 143 | LOGGING = { 144 | "version": 1, 145 | "disable_existing_loggers": True, 146 | "formatters": { 147 | "verbose": { 148 | "format": "%(levelname)s %(asctime)s %(module)s " 149 | "%(process)d %(thread)d %(message)s" 150 | } 151 | }, 152 | "handlers": { 153 | "console": { 154 | "level": "DEBUG", 155 | "class": "logging.StreamHandler", 156 | "formatter": "verbose", 157 | } 158 | }, 159 | "root": {"level": "INFO", "handlers": ["console"]}, 160 | "loggers": { 161 | "django.db.backends": { 162 | "level": "ERROR", 163 | "handlers": ["console"], 164 | "propagate": False, 165 | }, 166 | # Errors logged by the SDK itself 167 | "sentry_sdk": {"level": "ERROR", "handlers": ["console"], "propagate": False}, 168 | "django.security.DisallowedHost": { 169 | "level": "ERROR", 170 | "handlers": ["console"], 171 | "propagate": False, 172 | }, 173 | }, 174 | } 175 | 176 | # Sentry 177 | # ------------------------------------------------------------------------------ 178 | SENTRY_DSN = env("SENTRY_DSN") 179 | SENTRY_LOG_LEVEL = env.int("DJANGO_SENTRY_LOG_LEVEL", logging.INFO) 180 | 181 | sentry_logging = LoggingIntegration( 182 | level=SENTRY_LOG_LEVEL, # Capture info and above as breadcrumbs 183 | event_level=logging.ERROR, # Send errors as events 184 | ) 185 | integrations = [ 186 | sentry_logging, 187 | DjangoIntegration(), 188 | CeleryIntegration(), 189 | RedisIntegration(), 190 | ] 191 | sentry_sdk.init( 192 | dsn=SENTRY_DSN, 193 | integrations=integrations, 194 | environment=env("SENTRY_ENVIRONMENT", default="production"), 195 | traces_sample_rate=env.float("SENTRY_TRACES_SAMPLE_RATE", default=0.0), 196 | ) 197 | 198 | # Your stuff... 199 | # ------------------------------------------------------------------------------ 200 | -------------------------------------------------------------------------------- /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="P9BPFR5fCUPYJLHE2PkUglZPmU51AUjtz7sSfMuQJcvRYKUVA11YPJ6kC2Nsv5Wl", 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 | # EMAIL 24 | # ------------------------------------------------------------------------------ 25 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend 26 | EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" 27 | 28 | # Your stuff... 29 | # ------------------------------------------------------------------------------ 30 | -------------------------------------------------------------------------------- /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 drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView 8 | from rest_framework_simplejwt.views import ( 9 | TokenObtainPairView, 10 | TokenRefreshView, 11 | TokenVerifyView, 12 | ) 13 | 14 | from mentors.users.api.views import RegisterView 15 | 16 | urlpatterns = [ 17 | path("", TemplateView.as_view(template_name="pages/home.html"), name="home"), 18 | path( 19 | "about/", TemplateView.as_view(template_name="pages/about.html"), name="about" 20 | ), 21 | # Django Admin, use {% url 'admin:index' %} 22 | path(settings.ADMIN_URL, admin.site.urls), 23 | # User management 24 | path("users/", include("mentors.users.urls", namespace="users")), 25 | path("accounts/", include("allauth.urls")), 26 | # Your stuff: custom urls includes go here 27 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 28 | 29 | # API URLS 30 | urlpatterns += [ 31 | # API base url 32 | path("api/", include("config.api_router")), 33 | # DRF auth token 34 | path("api/auth/token/", TokenObtainPairView.as_view()), 35 | path("api/auth/token/refresh/", TokenRefreshView.as_view()), 36 | path("api/auth/token/verify/", TokenVerifyView.as_view()), 37 | path("api/auth/register/", RegisterView.as_view()), 38 | path("api/schema/", SpectacularAPIView.as_view(), name="api-schema"), 39 | path( 40 | "api/docs/", 41 | SpectacularSwaggerView.as_view(url_name="api-schema"), 42 | name="api-docs", 43 | ), 44 | ] 45 | 46 | if settings.DEBUG: 47 | # This allows the error pages to be debugged during development, just visit 48 | # these url in browser to see how these error pages look like. 49 | urlpatterns += [ 50 | path( 51 | "400/", 52 | default_views.bad_request, 53 | kwargs={"exception": Exception("Bad Request!")}, 54 | ), 55 | path( 56 | "403/", 57 | default_views.permission_denied, 58 | kwargs={"exception": Exception("Permission Denied")}, 59 | ), 60 | path( 61 | "404/", 62 | default_views.page_not_found, 63 | kwargs={"exception": Exception("Page not Found")}, 64 | ), 65 | path("500/", default_views.server_error), 66 | ] 67 | if "debug_toolbar" in settings.INSTALLED_APPS: 68 | import debug_toolbar 69 | 70 | urlpatterns = [path("__debug__/", include(debug_toolbar.urls))] + urlpatterns 71 | -------------------------------------------------------------------------------- /config/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for mentors 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 | # mentors directory. 24 | ROOT_DIR = Path(__file__).resolve(strict=True).parent.parent 25 | sys.path.append(str(ROOT_DIR / "mentors")) 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 | -------------------------------------------------------------------------------- /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 = "marketplace" 31 | copyright = """2022, Jumayev Ubaydullo""" 32 | author = "Jumayev Ubaydullo" 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 `mentors/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 | .. mentors 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 mentors'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=..\mentors 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 mentors.iml 71 | -------------------------------------------------------------------------------- /docs/pycharm/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python019/marketplace-django/3c742aee405f24463e42775c08a75941e2daeddf/docs/pycharm/images/1.png -------------------------------------------------------------------------------- /docs/pycharm/images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python019/marketplace-django/3c742aee405f24463e42775c08a75941e2daeddf/docs/pycharm/images/2.png -------------------------------------------------------------------------------- /docs/pycharm/images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python019/marketplace-django/3c742aee405f24463e42775c08a75941e2daeddf/docs/pycharm/images/3.png -------------------------------------------------------------------------------- /docs/pycharm/images/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python019/marketplace-django/3c742aee405f24463e42775c08a75941e2daeddf/docs/pycharm/images/4.png -------------------------------------------------------------------------------- /docs/pycharm/images/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python019/marketplace-django/3c742aee405f24463e42775c08a75941e2daeddf/docs/pycharm/images/7.png -------------------------------------------------------------------------------- /docs/pycharm/images/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python019/marketplace-django/3c742aee405f24463e42775c08a75941e2daeddf/docs/pycharm/images/8.png -------------------------------------------------------------------------------- /docs/pycharm/images/f1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python019/marketplace-django/3c742aee405f24463e42775c08a75941e2daeddf/docs/pycharm/images/f1.png -------------------------------------------------------------------------------- /docs/pycharm/images/f2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python019/marketplace-django/3c742aee405f24463e42775c08a75941e2daeddf/docs/pycharm/images/f2.png -------------------------------------------------------------------------------- /docs/pycharm/images/f3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python019/marketplace-django/3c742aee405f24463e42775c08a75941e2daeddf/docs/pycharm/images/f3.png -------------------------------------------------------------------------------- /docs/pycharm/images/f4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python019/marketplace-django/3c742aee405f24463e42775c08a75941e2daeddf/docs/pycharm/images/f4.png -------------------------------------------------------------------------------- /docs/pycharm/images/issue1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python019/marketplace-django/3c742aee405f24463e42775c08a75941e2daeddf/docs/pycharm/images/issue1.png -------------------------------------------------------------------------------- /docs/pycharm/images/issue2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python019/marketplace-django/3c742aee405f24463e42775c08a75941e2daeddf/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:: mentors.users.models 13 | :members: 14 | :noindex: 15 | 16 | -------------------------------------------------------------------------------- /frontend/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next", "next/core-web-vitals"] 3 | } 4 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /frontend/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reactStrictMode: true, 3 | } 4 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "cookie": "^0.4.1", 13 | "next": "^12.0.10", 14 | "react": "17.0.2", 15 | "react-dom": "17.0.2", 16 | "react-loader-spinner": "^4.0.0", 17 | "react-redux": "^7.2.4", 18 | "redux": "^4.1.0", 19 | "redux-devtools-extension": "^2.13.9", 20 | "redux-thunk": "^2.3.0" 21 | }, 22 | "devDependencies": { 23 | "eslint": "7.31.0", 24 | "eslint-config-next": "11.0.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python019/marketplace-django/3c742aee405f24463e42775c08a75941e2daeddf/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/src/actions/auth.js: -------------------------------------------------------------------------------- 1 | import { 2 | REGISTER_SUCCESS, 3 | REGISTER_FAIL, 4 | RESET_REGISTER_SUCCESS, 5 | LOGIN_SUCCESS, 6 | LOGIN_FAIL, 7 | LOGOUT_SUCCESS, 8 | LOGOUT_FAIL, 9 | LOAD_USER_SUCCESS, 10 | LOAD_USER_FAIL, 11 | AUTHENTICATED_SUCCESS, 12 | AUTHENTICATED_FAIL, 13 | REFRESH_SUCCESS, 14 | REFRESH_FAIL, 15 | SET_AUTH_LOADING, 16 | REMOVE_AUTH_LOADING, 17 | } from "./types"; 18 | 19 | export const load_user = () => async (dispatch) => { 20 | try { 21 | const res = await fetch("/api/account/user", { 22 | method: "GET", 23 | headers: { 24 | Accept: "application/json", 25 | }, 26 | }); 27 | 28 | const data = await res.json(); 29 | 30 | if (res.status === 200) { 31 | dispatch({ 32 | type: LOAD_USER_SUCCESS, 33 | payload: data, 34 | }); 35 | } else { 36 | dispatch({ 37 | type: LOAD_USER_FAIL, 38 | }); 39 | } 40 | } catch (err) { 41 | dispatch({ 42 | type: LOAD_USER_FAIL, 43 | }); 44 | } 45 | }; 46 | 47 | export const check_auth_status = () => async (dispatch) => { 48 | try { 49 | const res = await fetch("/api/account/verify", { 50 | method: "GET", 51 | headers: { 52 | Accept: "application/json", 53 | }, 54 | }); 55 | 56 | if (res.status === 200) { 57 | dispatch({ 58 | type: AUTHENTICATED_SUCCESS, 59 | }); 60 | dispatch(load_user()); 61 | } else { 62 | dispatch({ 63 | type: AUTHENTICATED_FAIL, 64 | }); 65 | } 66 | } catch (err) { 67 | dispatch({ 68 | type: AUTHENTICATED_FAIL, 69 | }); 70 | } 71 | }; 72 | 73 | export const request_refresh = () => async (dispatch) => { 74 | try { 75 | const res = await fetch("/api/account/refresh", { 76 | method: "GET", 77 | headers: { 78 | Accept: "application/json", 79 | }, 80 | }); 81 | 82 | if (res.status === 200) { 83 | dispatch({ 84 | type: REFRESH_SUCCESS, 85 | }); 86 | dispatch(check_auth_status()); 87 | } else { 88 | dispatch({ 89 | type: REFRESH_FAIL, 90 | }); 91 | } 92 | } catch (err) { 93 | dispatch({ 94 | type: REFRESH_FAIL, 95 | }); 96 | } 97 | }; 98 | 99 | export const register = 100 | (first_name, last_name, username, password, re_password) => 101 | async (dispatch) => { 102 | const body = JSON.stringify({ 103 | first_name, 104 | last_name, 105 | username, 106 | password, 107 | re_password, 108 | }); 109 | 110 | dispatch({ 111 | type: SET_AUTH_LOADING, 112 | }); 113 | 114 | try { 115 | const res = await fetch("/api/account/register", { 116 | method: "POST", 117 | headers: { 118 | Accept: "application/json", 119 | "Content-Type": "application/json", 120 | }, 121 | body: body, 122 | }); 123 | 124 | if (res.status === 201) { 125 | dispatch({ 126 | type: REGISTER_SUCCESS, 127 | }); 128 | } else { 129 | dispatch({ 130 | type: REGISTER_FAIL, 131 | }); 132 | } 133 | } catch (err) { 134 | dispatch({ 135 | type: REGISTER_FAIL, 136 | }); 137 | } 138 | 139 | dispatch({ 140 | type: REMOVE_AUTH_LOADING, 141 | }); 142 | }; 143 | 144 | export const reset_register_success = () => (dispatch) => { 145 | dispatch({ 146 | type: RESET_REGISTER_SUCCESS, 147 | }); 148 | }; 149 | 150 | export const login = (username, password) => async (dispatch) => { 151 | const body = JSON.stringify({ 152 | username, 153 | password, 154 | }); 155 | 156 | dispatch({ 157 | type: SET_AUTH_LOADING, 158 | }); 159 | 160 | try { 161 | const res = await fetch("/api/account/login", { 162 | method: "POST", 163 | headers: { 164 | Accept: "application/json", 165 | "Content-Type": "application/json", 166 | }, 167 | body: body, 168 | }); 169 | 170 | if (res.status === 200) { 171 | dispatch({ 172 | type: LOGIN_SUCCESS, 173 | }); 174 | dispatch(load_user()); 175 | } else { 176 | dispatch({ 177 | type: LOGIN_FAIL, 178 | }); 179 | } 180 | } catch (err) { 181 | dispatch({ 182 | type: LOGIN_FAIL, 183 | }); 184 | } 185 | 186 | dispatch({ 187 | type: REMOVE_AUTH_LOADING, 188 | }); 189 | }; 190 | 191 | export const logout = () => async (dispatch) => { 192 | try { 193 | const res = await fetch("/api/account/logout", { 194 | method: "POST", 195 | headers: { 196 | Accept: "application/json", 197 | }, 198 | }); 199 | 200 | if (res.status === 200) { 201 | dispatch({ 202 | type: LOGOUT_SUCCESS, 203 | }); 204 | } else { 205 | dispatch({ 206 | type: LOGOUT_FAIL, 207 | }); 208 | } 209 | } catch (err) { 210 | dispatch({ 211 | type: LOGOUT_FAIL, 212 | }); 213 | } 214 | }; 215 | -------------------------------------------------------------------------------- /frontend/src/actions/types.js: -------------------------------------------------------------------------------- 1 | export const REGISTER_SUCCESS = 'REGISTER_SUCCESS'; 2 | export const REGISTER_FAIL = 'REGISTER_FAIL'; 3 | export const RESET_REGISTER_SUCCESS = 'RESET_REGISTER_SUCCESS'; 4 | export const LOGIN_SUCCESS = 'LOGIN_SUCCESS'; 5 | export const LOGIN_FAIL = 'LOGIN_FAIL'; 6 | export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS'; 7 | export const LOGOUT_FAIL = 'LOGOUT_FAIL'; 8 | export const LOAD_USER_SUCCESS = 'LOAD_USER_SUCCESS'; 9 | export const LOAD_USER_FAIL = 'LOAD_USER_FAIL'; 10 | export const AUTHENTICATED_SUCCESS = 'AUTHENTICATED_SUCCESS'; 11 | export const AUTHENTICATED_FAIL = 'AUTHENTICATED_FAIL'; 12 | export const REFRESH_SUCCESS = 'REFRESH_SUCCESS'; 13 | export const REFRESH_FAIL = 'REFRESH_FAIL'; 14 | export const SET_AUTH_LOADING = 'SET_AUTH_LOADING'; 15 | export const REMOVE_AUTH_LOADING = 'REMOVE_AUTH_LOADING'; 16 | -------------------------------------------------------------------------------- /frontend/src/components/Navbar.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { useRouter } from 'next/router'; 3 | import { useSelector, useDispatch } from 'react-redux'; 4 | import { logout } from '../actions/auth'; 5 | 6 | const navbar = () => { 7 | const dispatch = useDispatch(); 8 | const router = useRouter(); 9 | 10 | const isAuthenticated = useSelector(state => state.auth.isAuthenticated); 11 | 12 | const logoutHandler = () => { 13 | if (dispatch && dispatch !== null && dispatch !== undefined) 14 | dispatch(logout()); 15 | }; 16 | 17 | const authLinks = ( 18 | <> 19 |
  • 20 | 21 | 25 | Dashboard 26 | 27 | 28 |
  • 29 |
  • 30 | 35 | Logout 36 | 37 |
  • 38 | 39 | ); 40 | 41 | const guestLinks = ( 42 | <> 43 |
  • 44 | 45 | 49 | Register 50 | 51 | 52 |
  • 53 |
  • 54 | 55 | 59 | Login 60 | 61 | 62 |
  • 63 | 64 | ); 65 | 66 | return ( 67 | 104 | ); 105 | }; 106 | 107 | export default navbar; 108 | -------------------------------------------------------------------------------- /frontend/src/config/index.js: -------------------------------------------------------------------------------- 1 | export const API_URL = process.env.NEXT_PUBLIC_API_URL; -------------------------------------------------------------------------------- /frontend/src/hocs/Layout.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | import { request_refresh } from '../actions/auth'; 4 | import Head from 'next/head'; 5 | import Navbar from '../components/Navbar'; 6 | 7 | const Layout = ({ title, content, children }) => { 8 | const dispatch = useDispatch(); 9 | 10 | useEffect(() => { 11 | if (dispatch && dispatch !== null && dispatch !== undefined) 12 | dispatch(request_refresh()); 13 | }, [dispatch]); 14 | 15 | return ( 16 | <> 17 | 18 | {title} 19 | 20 | 21 | 22 |
    23 | {children} 24 |
    25 | 26 | ); 27 | }; 28 | 29 | Layout.defaultProps = { 30 | title: 'httpOnly Auth', 31 | content: 'Tutorial for showing you how to use httpOnly cookies for storing json web tokens' 32 | } 33 | 34 | export default Layout; 35 | -------------------------------------------------------------------------------- /frontend/src/pages/_app.js: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import { Provider } from 'react-redux'; 3 | import { useStore } from '../store'; 4 | 5 | const App = ({ Component, pageProps }) => { 6 | const store = useStore(pageProps.initialReduxState); 7 | 8 | return ( 9 | 10 | 11 | HTTPOnly Auth 12 | 13 | 19 | 24 | 25 | 26 | 27 | ); 28 | }; 29 | 30 | export default App; 31 | -------------------------------------------------------------------------------- /frontend/src/pages/api/account/login.js: -------------------------------------------------------------------------------- 1 | import cookie from "cookie"; 2 | import { API_URL } from "../../../config/index"; 3 | 4 | export default async (req, res) => { 5 | if (req.method === "POST") { 6 | const { username, password } = req.body; 7 | 8 | const body = JSON.stringify({ 9 | username, 10 | password, 11 | }); 12 | 13 | try { 14 | const apiRes = await fetch(`${API_URL}/api/auth/token/`, { 15 | method: "POST", 16 | headers: { 17 | Accept: "application/json", 18 | "Content-Type": "application/json", 19 | }, 20 | body: body, 21 | }); 22 | 23 | const data = await apiRes.json(); 24 | 25 | if (apiRes.status === 200) { 26 | res.setHeader("Set-Cookie", [ 27 | cookie.serialize("access", data.access, { 28 | httpOnly: true, 29 | secure: process.env.NODE_ENV !== "development", 30 | maxAge: 60 * 30, 31 | sameSite: "strict", 32 | path: "/api/", 33 | }), 34 | cookie.serialize("refresh", data.refresh, { 35 | httpOnly: true, 36 | secure: process.env.NODE_ENV !== "development", 37 | maxAge: 60 * 60 * 24, 38 | sameSite: "strict", 39 | path: "/api/", 40 | }), 41 | ]); 42 | 43 | return res.status(200).json({ 44 | success: "Logged in successfully", 45 | }); 46 | } else { 47 | return res.status(apiRes.status).json({ 48 | error: "Authentication failed", 49 | }); 50 | } 51 | } catch (err) { 52 | return res.status(500).json({ 53 | error: "Something went wrong when authenticating", 54 | }); 55 | } 56 | } else { 57 | res.setHeader("Allow", ["POST"]); 58 | return res.status(405).json({ error: `Method ${req.method} now allowed` }); 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /frontend/src/pages/api/account/logout.js: -------------------------------------------------------------------------------- 1 | import cookie from 'cookie'; 2 | 3 | export default async (req, res) => { 4 | if (req.method === 'POST') { 5 | res.setHeader('Set-Cookie', [ 6 | cookie.serialize( 7 | 'access', '', { 8 | httpOnly: true, 9 | secure: process.env.NODE_ENV !== 'development', 10 | expires: new Date(0), 11 | sameSite: 'strict', 12 | path: '/api/' 13 | } 14 | ), 15 | cookie.serialize( 16 | 'refresh', '', { 17 | httpOnly: true, 18 | secure: process.env.NODE_ENV !== 'development', 19 | expires: new Date(0), 20 | sameSite: 'strict', 21 | path: '/api/' 22 | } 23 | ) 24 | ]); 25 | 26 | return res.status(200).json({ 27 | success: 'Successfully logged out' 28 | }); 29 | } else { 30 | res.setHeader('Allow', ['POST']); 31 | return res.status(405).json({ 32 | error: `Method ${req.method} now allowed` 33 | }); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /frontend/src/pages/api/account/refresh.js: -------------------------------------------------------------------------------- 1 | import cookie from "cookie"; 2 | import { API_URL } from "../../../config/index"; 3 | 4 | export default async (req, res) => { 5 | if (req.method === "GET") { 6 | const cookies = cookie.parse(req.headers.cookie ?? ""); 7 | const refresh = cookies.refresh ?? false; 8 | 9 | if (refresh === false) { 10 | return res.status(401).json({ 11 | error: "User unauthorized to make this request", 12 | }); 13 | } 14 | 15 | const body = JSON.stringify({ 16 | refresh, 17 | }); 18 | 19 | try { 20 | const apiRes = await fetch(`${API_URL}/api/auth/token/refresh/`, { 21 | method: "POST", 22 | headers: { 23 | Accept: "application/json", 24 | "Content-Type": "application/json", 25 | }, 26 | body: body, 27 | }); 28 | 29 | const data = await apiRes.json(); 30 | 31 | if (apiRes.status === 200) { 32 | res.setHeader("Set-Cookie", [ 33 | cookie.serialize("access", data.access, { 34 | httpOnly: true, 35 | secure: process.env.NODE_ENV !== "development", 36 | maxAge: 60 * 30, 37 | sameSite: "strict", 38 | path: "/api/", 39 | }), 40 | cookie.serialize("refresh", data.refresh, { 41 | httpOnly: true, 42 | secure: process.env.NODE_ENV !== "development", 43 | maxAge: 60 * 60 * 24, 44 | sameSite: "strict", 45 | path: "/api/", 46 | }), 47 | ]); 48 | 49 | return res.status(200).json({ 50 | success: "Refresh request successful", 51 | }); 52 | } else { 53 | return res.status(apiRes.status).json({ 54 | error: "Failed to fulfill refresh request", 55 | }); 56 | } 57 | } catch (err) { 58 | return res.status(500).json({ 59 | error: "Something went wrong when trying to fulfill refresh request", 60 | }); 61 | } 62 | } else { 63 | res.setHeader("Allow", ["GET"]); 64 | return res.status(405).json({ error: `Method ${req.method} not allowed` }); 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /frontend/src/pages/api/account/register.js: -------------------------------------------------------------------------------- 1 | import { API_URL } from "../../../config/index"; 2 | 3 | export default async (req, res) => { 4 | if (req.method === "POST") { 5 | const { first_name, last_name, username, password, re_password } = req.body; 6 | 7 | const body = JSON.stringify({ 8 | first_name, 9 | last_name, 10 | username, 11 | password, 12 | re_password, 13 | }); 14 | 15 | try { 16 | const apiRes = await fetch(`${API_URL}/api/auth/register/`, { 17 | method: "POST", 18 | headers: { 19 | Accept: "application/json", 20 | "Content-Type": "application/json", 21 | }, 22 | body: body, 23 | }); 24 | 25 | const data = await apiRes.json(); 26 | 27 | if (apiRes.status === 201) { 28 | return res.status(201).json({ success: data.success }); 29 | } else { 30 | return res.status(apiRes.status).json({ 31 | error: data.error, 32 | }); 33 | } 34 | } catch (err) { 35 | return res.status(500).json({ 36 | error: "Something went wrong when registering for an account", 37 | }); 38 | } 39 | } else { 40 | res.setHeader("Allow", ["POST"]); 41 | return res.status(405).json({ error: `Method ${req.method} not allowed` }); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /frontend/src/pages/api/account/user.js: -------------------------------------------------------------------------------- 1 | import cookie from "cookie"; 2 | import { API_URL } from "../../../config/index"; 3 | 4 | export default async (req, res) => { 5 | if (req.method === "GET") { 6 | const cookies = cookie.parse(req.headers.cookie ?? ""); 7 | const access = cookies.access ?? false; 8 | 9 | if (access === false) { 10 | return res.status(401).json({ 11 | error: "User unauthorized to make this request", 12 | }); 13 | } 14 | 15 | try { 16 | const apiRes = await fetch(`${API_URL}/api/users/me/`, { 17 | method: "GET", 18 | headers: { 19 | Accept: "application/json", 20 | Authorization: `Bearer ${access}`, 21 | }, 22 | }); 23 | const data = await apiRes.json(); 24 | 25 | if (apiRes.status === 200) { 26 | return res.status(200).json({ 27 | user: data, 28 | }); 29 | } else { 30 | return res.status(apiRes.status).json({ 31 | error: data.error, 32 | }); 33 | } 34 | } catch (err) { 35 | return res.status(500).json({ 36 | error: "Something went wrong when retrieving user", 37 | }); 38 | } 39 | } else { 40 | res.setHeader("Allow", ["GET"]); 41 | return res.status(405).json({ 42 | error: `Method ${req.method} not allowed`, 43 | }); 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /frontend/src/pages/api/account/verify.js: -------------------------------------------------------------------------------- 1 | import { API_URL } from "../../../config/index"; 2 | import cookie from "cookie"; 3 | 4 | export default async (req, res) => { 5 | if (req.method === "GET") { 6 | const cookies = cookie.parse(req.headers.cookie ?? ""); 7 | const access = cookies.access ?? false; 8 | 9 | if (access === false) { 10 | return res.status(403).json({ 11 | error: "User forbidden from making the request", 12 | }); 13 | } 14 | 15 | const body = JSON.stringify({ 16 | token: access, 17 | }); 18 | 19 | try { 20 | const apiRes = await fetch(`${API_URL}/api/auth/token/verify/`, { 21 | method: "POST", 22 | headers: { 23 | Accept: "application/json", 24 | "Content-Type": "application/json", 25 | }, 26 | body: body, 27 | }); 28 | 29 | if (apiRes.status === 200) { 30 | return res.status(200).json({ success: "Authenticated successfully" }); 31 | } else { 32 | return res.status(apiRes.status).json({ 33 | error: "Failed to authenticate", 34 | }); 35 | } 36 | } catch (err) { 37 | return res.status(500).json({ 38 | error: "Something went wrong when trying to authenticate", 39 | }); 40 | } 41 | } else { 42 | res.setHeader("Allow", ["GET"]); 43 | return res.status(405).json({ error: `Method ${req.method} not allowed` }); 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /frontend/src/pages/dashboard.js: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import { useSelector } from 'react-redux'; 3 | import Layout from '../hocs/Layout'; 4 | 5 | const Dashboard = () => { 6 | const router = useRouter(); 7 | 8 | const isAuthenticated = useSelector(state => state.auth.isAuthenticated); 9 | const user = useSelector(state => state.auth.user); 10 | const loading = useSelector(state => state.auth.loading); 11 | 12 | if (typeof window !== 'undefined' && !loading && !isAuthenticated) 13 | router.push('/login'); 14 | 15 | return ( 16 | 20 |
    21 |
    22 |

    23 | User Dashboard 24 |

    25 |

    26 | Welcome {user !== null && user.first_name} to the httpOnly Auth Tutorial Site! 27 |

    28 |
    29 |
    30 |
    31 | ); 32 | }; 33 | 34 | export default Dashboard; 35 | -------------------------------------------------------------------------------- /frontend/src/pages/index.js: -------------------------------------------------------------------------------- 1 | import Layout from '../hocs/Layout'; 2 | 3 | const homePage = () => ( 4 | 8 |
    9 |
    10 |

    Home Page

    11 |

    12 | Welcome to the httpOnly Auth Tutorial Site! 13 |

    14 |
    15 |
    16 |
    17 | ); 18 | 19 | export default homePage; 20 | -------------------------------------------------------------------------------- /frontend/src/pages/login.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { useRouter } from 'next/router'; 4 | import { login, reset_register_success } from '../actions/auth'; 5 | import Layout from '../hocs/Layout'; 6 | import Loader from 'react-loader-spinner'; 7 | 8 | const LoginPage = () => { 9 | const dispatch = useDispatch(); 10 | const router = useRouter(); 11 | const isAuthenticated = useSelector(state => state.auth.isAuthenticated); 12 | const loading = useSelector(state => state.auth.loading); 13 | 14 | const [formData, setFormData] = useState({ 15 | username: '', 16 | password: '', 17 | }); 18 | 19 | const { 20 | username, 21 | password, 22 | } = formData; 23 | 24 | useEffect(() => { 25 | if (dispatch && dispatch !== null && dispatch !== undefined) 26 | dispatch(reset_register_success()); 27 | }, [dispatch]); 28 | 29 | const onChange = e => setFormData({ ...formData, [e.target.name]: e.target.value }); 30 | 31 | const onSubmit = e => { 32 | e.preventDefault(); 33 | 34 | if (dispatch && dispatch !== null && dispatch !== undefined) 35 | dispatch(login(username, password)); 36 | }; 37 | 38 | if (typeof window !== 'undefined' && isAuthenticated) 39 | router.push('/dashboard'); 40 | 41 | return ( 42 | 46 |

    Login Page

    47 |
    48 |

    Log Into Your Account

    49 |
    50 | 53 | 62 |
    63 |
    64 | 67 | 77 |
    78 | { 79 | loading ? ( 80 |
    81 | 87 |
    88 | ) : ( 89 | 92 | ) 93 | } 94 |
    95 |
    96 | ); 97 | }; 98 | 99 | export default LoginPage; 100 | -------------------------------------------------------------------------------- /frontend/src/pages/register.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { useRouter } from 'next/router'; 4 | import { register } from '../actions/auth'; 5 | import Layout from '../hocs/Layout'; 6 | import Loader from 'react-loader-spinner'; 7 | import router from 'next/router'; 8 | 9 | const RegisterPage = () => { 10 | const dispatch = useDispatch(); 11 | const router = useRouter(); 12 | const register_success = useSelector(state => state.auth.register_success); 13 | const isAuthenticated = useSelector(state => state.auth.isAuthenticated); 14 | const loading = useSelector(state => state.auth.loading); 15 | 16 | const [formData, setFormData] = useState({ 17 | first_name: '', 18 | last_name: '', 19 | username: '', 20 | password: '', 21 | re_password: '', 22 | }); 23 | 24 | const { 25 | first_name, 26 | last_name, 27 | username, 28 | password, 29 | re_password 30 | } = formData; 31 | 32 | const onChange = e => setFormData({ ...formData, [e.target.name]: e.target.value }); 33 | 34 | const onSubmit = e => { 35 | e.preventDefault(); 36 | 37 | if (dispatch && dispatch !== null && dispatch !== undefined) 38 | dispatch(register(first_name, last_name, username, password, re_password)); 39 | }; 40 | 41 | if (typeof window !== 'undefined' && isAuthenticated) 42 | router.push('/dashboard'); 43 | if (register_success) 44 | router.push('/login'); 45 | 46 | return ( 47 | 51 |

    Register Page

    52 |
    53 |

    Create An Account

    54 |
    55 | 58 | 67 |
    68 |
    69 | 72 | 81 |
    82 |
    83 | 86 | 95 |
    96 |
    97 | 100 | 110 |
    111 |
    112 | 115 | 125 |
    126 | { 127 | loading ? ( 128 |
    129 | 135 |
    136 | ) : ( 137 | 140 | ) 141 | } 142 |
    143 |
    144 | ); 145 | }; 146 | 147 | export default RegisterPage; 148 | -------------------------------------------------------------------------------- /frontend/src/reducers/auth.js: -------------------------------------------------------------------------------- 1 | import { 2 | REGISTER_SUCCESS, 3 | REGISTER_FAIL, 4 | RESET_REGISTER_SUCCESS, 5 | LOGIN_SUCCESS, 6 | LOGIN_FAIL, 7 | LOGOUT_SUCCESS, 8 | LOGOUT_FAIL, 9 | LOAD_USER_SUCCESS, 10 | LOAD_USER_FAIL, 11 | AUTHENTICATED_SUCCESS, 12 | AUTHENTICATED_FAIL, 13 | REFRESH_SUCCESS, 14 | REFRESH_FAIL, 15 | SET_AUTH_LOADING, 16 | REMOVE_AUTH_LOADING, 17 | } from '../actions/types'; 18 | 19 | const initialState = { 20 | user: null, 21 | isAuthenticated: false, 22 | loading: false, 23 | register_success: false 24 | }; 25 | 26 | const authReducer = (state = initialState, action) => { 27 | const { type, payload } = action; 28 | 29 | switch(type) { 30 | case REGISTER_SUCCESS: 31 | return { 32 | ...state, 33 | register_success: true 34 | } 35 | case REGISTER_FAIL: 36 | return { 37 | ...state, 38 | } 39 | case RESET_REGISTER_SUCCESS: 40 | return { 41 | ...state, 42 | register_success: false 43 | } 44 | case LOGIN_SUCCESS: 45 | return { 46 | ...state, 47 | isAuthenticated: true 48 | } 49 | case LOGIN_FAIL: 50 | return { 51 | ...state, 52 | isAuthenticated: false 53 | } 54 | case LOGOUT_SUCCESS: 55 | return { 56 | ...state, 57 | isAuthenticated: false, 58 | user: null 59 | } 60 | case LOGOUT_FAIL: 61 | return { 62 | ...state 63 | } 64 | case LOAD_USER_SUCCESS: 65 | return { 66 | ...state, 67 | user: payload.user 68 | } 69 | case LOAD_USER_FAIL: 70 | return { 71 | ...state, 72 | user: null 73 | } 74 | case AUTHENTICATED_SUCCESS: 75 | return { 76 | ...state, 77 | isAuthenticated: true 78 | } 79 | case AUTHENTICATED_FAIL: 80 | return { 81 | ...state, 82 | isAuthenticated: false, 83 | user: null 84 | } 85 | case REFRESH_SUCCESS: 86 | return { 87 | ...state, 88 | } 89 | case REFRESH_FAIL: 90 | return { 91 | ...state, 92 | isAuthenticated: false, 93 | user: null 94 | } 95 | case SET_AUTH_LOADING: 96 | return { 97 | ...state, 98 | loading: true 99 | } 100 | case REMOVE_AUTH_LOADING: 101 | return { 102 | ...state, 103 | loading: false 104 | } 105 | default: 106 | return state; 107 | }; 108 | }; 109 | 110 | export default authReducer; 111 | -------------------------------------------------------------------------------- /frontend/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import authReducer from './auth'; 3 | 4 | export default combineReducers({ 5 | auth: authReducer 6 | }); 7 | -------------------------------------------------------------------------------- /frontend/src/store.js: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { createStore, applyMiddleware } from 'redux'; 3 | import { composeWithDevTools } from 'redux-devtools-extension'; 4 | import thunkMiddleware from 'redux-thunk'; 5 | import reducers from './reducers'; 6 | 7 | let store; 8 | 9 | function initStore(initialState) { 10 | return createStore( 11 | reducers, 12 | initialState, 13 | composeWithDevTools(applyMiddleware(thunkMiddleware)) 14 | ); 15 | }; 16 | 17 | export const initializeStore = (preloadedState) => { 18 | let _store = store ?? initStore(preloadedState); 19 | 20 | // After navigating to a page with an initial Redux state, merge that state 21 | // with the current state in the store, and create a new store 22 | if (preloadedState && store) { 23 | _store = initStore({ 24 | ...store.getState(), 25 | ...preloadedState, 26 | }) 27 | // Reset the current store 28 | store = undefined; 29 | } 30 | 31 | // For SSG and SSR always create a new store 32 | if (typeof window === 'undefined') return _store; 33 | // Create the store once in the client 34 | if (!store) store = _store; 35 | 36 | return _store; 37 | } 38 | 39 | export function useStore(initialState) { 40 | const store = useMemo(() => initializeStore(initialState), [initialState]); 41 | return store; 42 | }; 43 | -------------------------------------------------------------------------------- /local.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | volumes: 4 | mentors_local_postgres_data: {} 5 | mentors_local_postgres_data_backups: {} 6 | 7 | services: 8 | django: &django 9 | build: 10 | context: . 11 | dockerfile: ./compose/local/django/Dockerfile 12 | image: mentors_local_django 13 | container_name: mentors_local_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: mentors_production_postgres 31 | container_name: mentors_local_postgres 32 | volumes: 33 | - mentors_local_postgres_data:/var/lib/postgresql/data:Z 34 | - mentors_local_postgres_data_backups:/backups:z 35 | env_file: 36 | - ./.envs/.local/.postgres 37 | 38 | docs: 39 | image: mentors_local_docs 40 | container_name: mentors_local_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 | - ./mentors:/app/mentors:z 50 | ports: 51 | - "7000:7000" 52 | command: /start-docs 53 | 54 | redis: 55 | image: redis:6 56 | container_name: mentors_local_redis 57 | 58 | celeryworker: 59 | <<: *django 60 | image: mentors_local_celeryworker 61 | container_name: mentors_local_celeryworker 62 | depends_on: 63 | - redis 64 | - postgres 65 | ports: [] 66 | command: /start-celeryworker 67 | 68 | celerybeat: 69 | <<: *django 70 | image: mentors_local_celerybeat 71 | container_name: mentors_local_celerybeat 72 | depends_on: 73 | - redis 74 | - postgres 75 | ports: [] 76 | command: /start-celerybeat 77 | 78 | flower: 79 | <<: *django 80 | image: mentors_local_flower 81 | container_name: mentors_local_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 | # mentors directory. 28 | current_path = Path(__file__).parent.resolve() 29 | sys.path.append(str(current_path / "mentors")) 30 | 31 | execute_from_command_line(sys.argv) 32 | -------------------------------------------------------------------------------- /mentors/__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 | -------------------------------------------------------------------------------- /mentors/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from mentors.users.models import User 4 | from mentors.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 | -------------------------------------------------------------------------------- /mentors/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 | -------------------------------------------------------------------------------- /mentors/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 | -------------------------------------------------------------------------------- /mentors/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 | -------------------------------------------------------------------------------- /mentors/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 | -------------------------------------------------------------------------------- /mentors/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_or_create_site_with_sequence(site_model, connection, domain, name): 11 | """Update or create the site with default ID and keep the DB sequence in sync.""" 12 | site, created = site_model.objects.update_or_create( 13 | id=settings.SITE_ID, 14 | defaults={ 15 | "domain": domain, 16 | "name": name, 17 | }, 18 | ) 19 | if created: 20 | # We provided the ID explicitly when creating the Site entry, therefore the DB 21 | # sequence to auto-generate them wasn't used and is now out of sync. If we 22 | # don't do anything, we'll get a unique constraint violation the next time a 23 | # site is created. 24 | # To avoid this, we need to manually update DB sequence and make sure it's 25 | # greater than the maximum value. 26 | max_id = site_model.objects.order_by('-id').first().id 27 | with connection.cursor() as cursor: 28 | cursor.execute("SELECT last_value from django_site_id_seq") 29 | (current_id,) = cursor.fetchone() 30 | if current_id <= max_id: 31 | cursor.execute( 32 | "alter sequence django_site_id_seq restart with %s", 33 | [max_id + 1], 34 | ) 35 | 36 | 37 | def update_site_forward(apps, schema_editor): 38 | """Set site domain and name.""" 39 | Site = apps.get_model("sites", "Site") 40 | _update_or_create_site_with_sequence( 41 | Site, 42 | schema_editor.connection, 43 | "example.com", 44 | "mentors", 45 | ) 46 | 47 | 48 | def update_site_backward(apps, schema_editor): 49 | """Revert site domain and name to default.""" 50 | Site = apps.get_model("sites", "Site") 51 | _update_or_create_site_with_sequence( 52 | Site, 53 | schema_editor.connection, 54 | "example.com", 55 | "example.com", 56 | ) 57 | 58 | 59 | class Migration(migrations.Migration): 60 | 61 | dependencies = [("sites", "0002_alter_domain_unique")] 62 | 63 | operations = [migrations.RunPython(update_site_forward, update_site_backward)] 64 | -------------------------------------------------------------------------------- /mentors/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 | -------------------------------------------------------------------------------- /mentors/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 | -------------------------------------------------------------------------------- /mentors/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 | -------------------------------------------------------------------------------- /mentors/static/fonts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python019/marketplace-django/3c742aee405f24463e42775c08a75941e2daeddf/mentors/static/fonts/.gitkeep -------------------------------------------------------------------------------- /mentors/static/images/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python019/marketplace-django/3c742aee405f24463e42775c08a75941e2daeddf/mentors/static/images/favicons/favicon.ico -------------------------------------------------------------------------------- /mentors/static/js/project.js: -------------------------------------------------------------------------------- 1 | /* Project specific Javascript goes here. */ 2 | -------------------------------------------------------------------------------- /mentors/static/sass/custom_bootstrap_vars.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python019/marketplace-django/3c742aee405f24463e42775c08a75941e2daeddf/mentors/static/sass/custom_bootstrap_vars.scss -------------------------------------------------------------------------------- /mentors/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 | -------------------------------------------------------------------------------- /mentors/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 | -------------------------------------------------------------------------------- /mentors/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 | -------------------------------------------------------------------------------- /mentors/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 | -------------------------------------------------------------------------------- /mentors/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 | -------------------------------------------------------------------------------- /mentors/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 | -------------------------------------------------------------------------------- /mentors/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 | -------------------------------------------------------------------------------- /mentors/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 | -------------------------------------------------------------------------------- /mentors/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 |

    17 | {% translate "Please sign in with one of your existing third party accounts:" %} 18 | {% if ACCOUNT_ALLOW_REGISTRATION %} 19 | {% blocktranslate trimmed %} 20 | Or, sign up 21 | for a {{ site_name }} account and sign in below: 22 | {% endblocktranslate %} 23 | {% endif %} 24 |

    25 | 26 |
    27 | 28 |
      29 | {% include "socialaccount/snippets/provider_list.html" with process="login" %} 30 |
    31 | 32 | 33 | 34 |
    35 | 36 | {% include "socialaccount/snippets/login_extra.html" %} 37 | 38 | {% else %} 39 | {% if ACCOUNT_ALLOW_REGISTRATION %} 40 |

    41 | {% blocktranslate trimmed %} 42 | If you have not created an account yet, then please 43 | sign up first. 44 | {% endblocktranslate %} 45 |

    46 | {% endif %} 47 | {% endif %} 48 | 49 | 58 | 59 | {% endblock %} 60 | -------------------------------------------------------------------------------- /mentors/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 | -------------------------------------------------------------------------------- /mentors/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 | -------------------------------------------------------------------------------- /mentors/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 | -------------------------------------------------------------------------------- /mentors/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 | -------------------------------------------------------------------------------- /mentors/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 | -------------------------------------------------------------------------------- /mentors/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 | -------------------------------------------------------------------------------- /mentors/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 | -------------------------------------------------------------------------------- /mentors/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 | -------------------------------------------------------------------------------- /mentors/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 | -------------------------------------------------------------------------------- /mentors/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 | -------------------------------------------------------------------------------- /mentors/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 | -------------------------------------------------------------------------------- /mentors/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static i18n %} 2 | {% get_current_language as LANGUAGE_CODE %} 3 | 4 | 5 | 6 | 7 | {% block title %}mentors{% endblock title %} 8 | 9 | 10 | 11 | 12 | 13 | 14 | {% block css %} 15 | 16 | 17 | 18 | 19 | 20 | 21 | {% endblock %} 22 | 24 | {# Placed at the top of the document so pages load faster with defer #} 25 | {% block javascript %} 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | {% endblock javascript %} 34 | 35 | 36 | 37 | 38 | 39 |
    40 | 80 | 81 |
    82 | 83 |
    84 | 85 | {% if messages %} 86 | {% for message in messages %} 87 |
    88 | {{ message }} 89 | 90 |
    91 | {% endfor %} 92 | {% endif %} 93 | 94 | {% block content %} 95 |

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

    96 | {% endblock content %} 97 | 98 |
    99 | 100 | {% block modal %}{% endblock modal %} 101 | 102 | {% block inline_javascript %} 103 | {% comment %} 104 | Script tags with only code, no src (defer by default). To run 105 | with a "defer" so that you run inline code: 106 | 109 | {% endcomment %} 110 | {% endblock inline_javascript %} 111 | 112 | 113 | -------------------------------------------------------------------------------- /mentors/templates/pages/about.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | -------------------------------------------------------------------------------- /mentors/templates/pages/home.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | -------------------------------------------------------------------------------- /mentors/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 | -------------------------------------------------------------------------------- /mentors/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 | -------------------------------------------------------------------------------- /mentors/users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python019/marketplace-django/3c742aee405f24463e42775c08a75941e2daeddf/mentors/users/__init__.py -------------------------------------------------------------------------------- /mentors/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 | -------------------------------------------------------------------------------- /mentors/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 mentors.users.forms import UserAdminChangeForm, UserAdminCreationForm 7 | 8 | User = get_user_model() 9 | 10 | 11 | @admin.register(User) 12 | class UserAdmin(auth_admin.UserAdmin): 13 | 14 | form = UserAdminChangeForm 15 | add_form = UserAdminCreationForm 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 | -------------------------------------------------------------------------------- /mentors/users/api/serializers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from rest_framework import serializers 3 | from rest_framework.validators import UniqueValidator 4 | from django.contrib.auth.password_validation import validate_password 5 | 6 | 7 | User = get_user_model() 8 | 9 | 10 | class UserSerializer(serializers.ModelSerializer): 11 | class Meta: 12 | model = User 13 | fields = ["username", "name", "url"] 14 | 15 | extra_kwargs = { 16 | "url": {"view_name": "api:user-detail", "lookup_field": "username"} 17 | } 18 | 19 | 20 | class RegisterSerializer(serializers.ModelSerializer): 21 | # email = serializers.EmailField( 22 | # validators=[UniqueValidator(queryset=User.objects.all())] 23 | # ) 24 | 25 | password = serializers.CharField(write_only=True, validators=[validate_password]) 26 | re_password = serializers.CharField(write_only=True) 27 | 28 | class Meta: 29 | model = User 30 | fields = ('username', 'password', 're_password') 31 | 32 | def validate(self, attrs): 33 | if attrs['password'] != attrs['re_password']: 34 | raise serializers.ValidationError({"password": "Password fields didn't match."}) 35 | 36 | return attrs 37 | 38 | def create(self, validated_data): 39 | user = User.objects.create( 40 | username=validated_data['username'], 41 | # email=validated_data['email'], 42 | # first_name=validated_data['first_name'], 43 | # last_name=validated_data['last_name'] 44 | ) 45 | 46 | user.set_password(validated_data['password']) 47 | user.save() 48 | 49 | return user 50 | 51 | 52 | 53 | # class UserSerializer(serializers.ModelSerializer): 54 | # class Meta: 55 | # model = User 56 | # fields = ('first_name', 'last_name', 'username', ) 57 | -------------------------------------------------------------------------------- /mentors/users/api/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from rest_framework import permissions, status 3 | from rest_framework.decorators import action 4 | from rest_framework import generics 5 | from rest_framework.mixins import ListModelMixin, RetrieveModelMixin, UpdateModelMixin 6 | from rest_framework.response import Response 7 | from rest_framework.views import APIView 8 | from rest_framework.viewsets import GenericViewSet 9 | from .serializers import UserSerializer, RegisterSerializer 10 | 11 | 12 | User = get_user_model() 13 | 14 | 15 | class UserViewSet(RetrieveModelMixin, ListModelMixin, UpdateModelMixin, GenericViewSet): 16 | serializer_class = UserSerializer 17 | queryset = User.objects.all() 18 | lookup_field = "username" 19 | 20 | def get_queryset(self, *args, **kwargs): 21 | assert isinstance(self.request.user.id, int) 22 | return self.queryset.filter(id=self.request.user.id) 23 | 24 | @action(detail=False) 25 | def me(self, request): 26 | serializer = UserSerializer(request.user, context={"request": request}) 27 | return Response(status=status.HTTP_200_OK, data=serializer.data) 28 | 29 | 30 | class RegisterView(generics.CreateAPIView): 31 | queryset = User.objects.all() 32 | permission_classes = (permissions.AllowAny,) 33 | serializer_class = RegisterSerializer 34 | 35 | 36 | class LoadUserView(APIView): 37 | def get(self, request, format=None): 38 | try: 39 | user = request.user 40 | user = UserSerializer(user) 41 | 42 | return Response({"user": user.data}, status=status.HTTP_200_OK) 43 | except: 44 | return Response( 45 | {"error": "Something went wrong when trying to load user"}, 46 | status=status.HTTP_500_INTERNAL_SERVER_ERROR, 47 | ) 48 | -------------------------------------------------------------------------------- /mentors/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 = "mentors.users" 7 | verbose_name = _("Users") 8 | 9 | def ready(self): 10 | try: 11 | import mentors.users.signals # noqa F401 12 | except ImportError: 13 | pass 14 | -------------------------------------------------------------------------------- /mentors/users/context_processors.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | 4 | def allauth_settings(request): 5 | """Expose some settings from django-allauth in templates.""" 6 | return { 7 | "ACCOUNT_ALLOW_REGISTRATION": settings.ACCOUNT_ALLOW_REGISTRATION, 8 | } 9 | -------------------------------------------------------------------------------- /mentors/users/forms.py: -------------------------------------------------------------------------------- 1 | from allauth.account.forms import SignupForm 2 | from allauth.socialaccount.forms import SignupForm as SocialSignupForm 3 | from django.contrib.auth import forms as admin_forms 4 | from django.contrib.auth import get_user_model 5 | from django.utils.translation import gettext_lazy as _ 6 | 7 | User = get_user_model() 8 | 9 | 10 | class UserAdminChangeForm(admin_forms.UserChangeForm): 11 | class Meta(admin_forms.UserChangeForm.Meta): 12 | model = User 13 | 14 | 15 | class UserAdminCreationForm(admin_forms.UserCreationForm): 16 | """ 17 | Form for User Creation in the Admin Area. 18 | To change user signup, see UserSignupForm and UserSocialSignupForm. 19 | """ 20 | 21 | class Meta(admin_forms.UserCreationForm.Meta): 22 | model = User 23 | 24 | error_messages = { 25 | "username": {"unique": _("This username has already been taken.")} 26 | } 27 | 28 | 29 | class UserSignupForm(SignupForm): 30 | """ 31 | Form that will be rendered on a user sign up section/screen. 32 | Default fields will be added automatically. 33 | Check UserSocialSignupForm for accounts created from social. 34 | """ 35 | 36 | 37 | class UserSocialSignupForm(SocialSignupForm): 38 | """ 39 | Renders the form when user has signed up using social accounts. 40 | Default fields will be added automatically. 41 | See UserSignupForm otherwise. 42 | """ 43 | -------------------------------------------------------------------------------- /mentors/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 | -------------------------------------------------------------------------------- /mentors/users/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python019/marketplace-django/3c742aee405f24463e42775c08a75941e2daeddf/mentors/users/migrations/__init__.py -------------------------------------------------------------------------------- /mentors/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 | """ 9 | Default custom user model for mentors. 10 | If adding fields that need to be filled at user signup, 11 | check forms.SignupForm and forms.SocialSignupForms accordingly. 12 | """ 13 | 14 | #: First and last name do not cover name patterns around the globe 15 | name = CharField(_("Name of User"), blank=True, max_length=255) 16 | first_name = None # type: ignore 17 | last_name = None # type: ignore 18 | 19 | def get_absolute_url(self): 20 | """Get url for user's detail view. 21 | 22 | Returns: 23 | str: URL for user detail. 24 | 25 | """ 26 | return reverse("users:detail", kwargs={"username": self.username}) 27 | -------------------------------------------------------------------------------- /mentors/users/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from django.contrib.auth import get_user_model 3 | 4 | User = get_user_model() 5 | 6 | 7 | class UserSerializer(serializers.ModelSerializer): 8 | class Meta: 9 | model = User 10 | fields = ('first_name', 'last_name', 'username', ) 11 | -------------------------------------------------------------------------------- /mentors/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 | -------------------------------------------------------------------------------- /mentors/users/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python019/marketplace-django/3c742aee405f24463e42775c08a75941e2daeddf/mentors/users/tests/__init__.py -------------------------------------------------------------------------------- /mentors/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 | -------------------------------------------------------------------------------- /mentors/users/tests/test_admin.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.urls import reverse 3 | 4 | from mentors.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 | -------------------------------------------------------------------------------- /mentors/users/tests/test_drf_urls.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.urls import resolve, reverse 3 | 4 | from mentors.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 | -------------------------------------------------------------------------------- /mentors/users/tests/test_drf_views.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.test import RequestFactory 3 | 4 | from mentors.users.api.views import UserViewSet 5 | from mentors.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 | -------------------------------------------------------------------------------- /mentors/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 mentors.users.forms import UserAdminCreationForm 8 | from mentors.users.models import User 9 | 10 | pytestmark = pytest.mark.django_db 11 | 12 | 13 | class TestUserAdminCreationForm: 14 | """ 15 | Test class for all tests related to the UserAdminCreationForm 16 | """ 17 | 18 | def test_username_validation_error_msg(self, user: User): 19 | """ 20 | Tests UserAdminCreation 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 = UserAdminCreationForm( 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 | -------------------------------------------------------------------------------- /mentors/users/tests/test_models.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from mentors.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 | -------------------------------------------------------------------------------- /mentors/users/tests/test_swagger_ui.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.urls import reverse 3 | 4 | pytestmark = pytest.mark.django_db 5 | 6 | 7 | def test_swagger_accessible_by_admin(admin_client): 8 | url = reverse("api-docs") 9 | response = admin_client.get(url) 10 | assert response.status_code == 200 11 | 12 | 13 | def test_swagger_ui_not_accessible_by_normal_user(client): 14 | url = reverse("api-docs") 15 | response = client.get(url) 16 | assert response.status_code == 403 17 | -------------------------------------------------------------------------------- /mentors/users/tests/test_tasks.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from celery.result import EagerResult 3 | 4 | from mentors.users.tasks import get_users_count 5 | from mentors.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 | -------------------------------------------------------------------------------- /mentors/users/tests/test_urls.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.urls import resolve, reverse 3 | 4 | from mentors.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 | -------------------------------------------------------------------------------- /mentors/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 mentors.users.forms import UserAdminChangeForm 12 | from mentors.users.models import User 13 | from mentors.users.tests.factories import UserFactory 14 | from mentors.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 = UserAdminChangeForm() 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 | -------------------------------------------------------------------------------- /mentors/users/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from mentors.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 | -------------------------------------------------------------------------------- /mentors/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 | -------------------------------------------------------------------------------- /mentors/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python019/marketplace-django/3c742aee405f24463e42775c08a75941e2daeddf/mentors/utils/__init__.py -------------------------------------------------------------------------------- /mentors/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 | -------------------------------------------------------------------------------- /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: mentors_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: mentors_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: mentors_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: mentors_production_celeryworker 53 | command: /start-celeryworker 54 | 55 | celerybeat: 56 | <<: *django 57 | image: mentors_production_celerybeat 58 | command: /start-celerybeat 59 | 60 | flower: 61 | <<: *django 62 | image: mentors_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==9.0.0 # https://github.com/python-pillow/Pillow 4 | argon2-cffi==21.3.0 # https://github.com/hynek/argon2_cffi 5 | redis==4.1.2 # https://github.com/redis/redis-py 6 | hiredis==2.0.0 # https://github.com/redis/hiredis-py 7 | celery==5.2.3 # 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 | 11 | # Django 12 | # ------------------------------------------------------------------------------ 13 | django==3.2.12 # pyup: < 4.0 # https://www.djangoproject.com/ 14 | django-environ==0.8.1 # https://github.com/joke2k/django-environ 15 | django-model-utils==4.2.0 # https://github.com/jazzband/django-model-utils 16 | django-allauth==0.48.0 # https://github.com/pennersr/django-allauth 17 | django-crispy-forms==1.14.0 # https://github.com/django-crispy-forms/django-crispy-forms 18 | crispy-bootstrap5==0.6 # https://github.com/django-crispy-forms/crispy-bootstrap5 19 | django-redis==5.2.0 # https://github.com/jazzband/django-redis 20 | # Django REST Framework 21 | djangorestframework==3.13.1 # https://github.com/encode/django-rest-framework 22 | django-cors-headers==3.11.0 # https://github.com/adamchainz/django-cors-headers 23 | # DRF-spectacular for api documentation 24 | drf-spectacular==0.21.1 25 | djangorestframework-simplejwt==5.0.0 26 | -------------------------------------------------------------------------------- /requirements/local.txt: -------------------------------------------------------------------------------- 1 | -r base.txt 2 | 3 | Werkzeug[watchdog]==2.0.2 # https://github.com/pallets/werkzeug 4 | ipdb==0.13.9 # https://github.com/gotcha/ipdb 5 | psycopg2==2.9.3 # https://github.com/psycopg/psycopg2 6 | watchgod==0.7 # https://github.com/samuelcolvin/watchgod 7 | 8 | # Testing 9 | # ------------------------------------------------------------------------------ 10 | mypy==0.931 # https://github.com/python/mypy 11 | django-stubs==1.9.0 # https://github.com/typeddjango/django-stubs 12 | pytest==7.0.0 # 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.4.0 # 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.3 # https://github.com/nedbat/coveragepy 26 | black==22.1.0 # https://github.com/psf/black 27 | pylint-django==2.5.0 # https://github.com/PyCQA/pylint-django 28 | pylint-celery==0.3 # https://github.com/PyCQA/pylint-celery 29 | pre-commit==2.17.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.4 # 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.2 # 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.3 # https://github.com/psycopg/psycopg2 7 | Collectfast==2.2.0 # https://github.com/antonagestam/collectfast 8 | sentry-sdk==1.5.4 # 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.5 # 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 = mentors,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 = mentors/* 38 | omit = *migrations*, *tests* 39 | plugins = 40 | django_coverage_plugin 41 | --------------------------------------------------------------------------------