├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ └── main.yml ├── .gitignore ├── .markdownlint.json ├── .pre-commit-config.yaml ├── AUTHORS ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── django_fast_utils ├── __init__.py ├── apps.py ├── auth │ ├── __init__.py │ ├── backends.py │ ├── generics.py │ └── middleware.py ├── backends.py ├── db │ ├── __init__.py │ └── fields.py ├── exceptions.py ├── fields.py ├── helpers.py ├── paginator.py ├── permissions.py ├── settings.py ├── utils │ ├── __init__.py │ └── audit.py └── views │ ├── __init__.py │ ├── auth │ ├── __init__.py │ ├── serializers.py │ └── views.py │ └── generics.py ├── docs ├── auth │ └── generics.md ├── backends.md ├── db │ └── fields.md ├── exceptions.md ├── fields.md ├── helpers.md ├── index.md ├── paginator.md ├── permissions.md ├── releases.md ├── utils │ └── audit.md └── views │ ├── auth.md │ └── generics.md ├── mkdocs.yml ├── requirements.txt └── setup.py /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at http://editorconfig.org 3 | 4 | ; Top-level editorconfig file for this project. 5 | root = true 6 | 7 | [*] 8 | indent_style = space 9 | indent_size = 2 10 | end_of_line = lf 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = false 14 | 15 | # See Google Shell Style Guide 16 | # https://google.github.io/styleguide/shell.xml 17 | [*.sh] 18 | indent_size = 2 # shfmt: like -i 2 19 | insert_final_newline = true 20 | switch_case_indent = true # shfmt: like -ci 21 | 22 | [*.py] 23 | insert_final_newline = true 24 | indent_size = 4 25 | max_line_length = 120 26 | 27 | [*.md] 28 | trim_trailing_whitespace = false 29 | 30 | [Makefile] 31 | indent_style = tab 32 | 33 | [*.js] 34 | indent_size = 4 35 | insert_final_newline = true 36 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [tarsil] 2 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build and Publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - "**" 7 | schedule: 8 | - cron: "0 0 * * *" 9 | 10 | jobs: 11 | tests: 12 | name: Python ${{ matrix.python-version }} 13 | runs-on: ubuntu-20.04 14 | 15 | strategy: 16 | matrix: 17 | python-version: 18 | - "3.9" 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v3 23 | 24 | - name: Set Python version 25 | uses: actions/setup-python@v3 26 | with: 27 | python-version: ${{ matrix.python-version }} 28 | cache: "pip" 29 | cache-dependency-path: "requirements.txt" 30 | 31 | - name: Install Python dependencies 32 | run: pip install wheel twine mkdocs-material 33 | 34 | - name: Build Python package 35 | run: python setup.py sdist bdist_wheel 36 | 37 | - name: Upload to test pypi. 38 | run: twine upload --repository-url https://test.pypi.org/legacy/ -u ${{ secrets.TEST_PYPI_USER }} -p ${{ secrets.TEST_PYPI_PASSWORD }} --skip-existing dist/*.whl 39 | 40 | - name: Upload to pypi. 41 | run: twine upload -u ${{ secrets.PYPI_USER }} -p ${{ secrets.PYPI_PASSWORD }} --skip-existing dist/*.whl 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | MANIFEST 2 | .DS_Store 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | docs/_build/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | *.eggs 25 | .python-version 26 | 27 | # Pipfile 28 | Pipfile 29 | Pipfile.lock 30 | site/ 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coveragerc 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | 45 | # IDEs 46 | .idea/ 47 | .vscode/ 48 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "MD003": { 4 | "style": "atx" 5 | }, 6 | "MD007": { 7 | "indent": 2 8 | }, 9 | "MD009": { 10 | "br_spaces": 2, 11 | "list_item_empty_lines": true 12 | }, 13 | "MD010": { 14 | "code_blocks": false 15 | }, 16 | "MD012": true, 17 | "MD013": { 18 | "code_blocks": false, 19 | "line_length": 100, 20 | "tables": false 21 | }, 22 | "MD024": { 23 | "allow_different_nesting": true 24 | }, 25 | "MD026": { 26 | "punctuation": ".,;:!。,;:!" 27 | }, 28 | "MD027": true, 29 | "MD028": true, 30 | "MD030": true, 31 | "MD036": true, 32 | "MD037": true, 33 | "MD038": true, 34 | "MD039": true, 35 | "MD046": { 36 | "style": "fenced" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_stages: [commit, push] 2 | 3 | default_language_version: 4 | python: python3.8 5 | 6 | repos: 7 | - repo: git://github.com/pre-commit/pre-commit-hooks 8 | rev: v2.3.0 9 | hooks: 10 | - id: trailing-whitespace 11 | args: 12 | - --markdown-linebreak-ext=md 13 | - id: check-ast 14 | - id: check-case-conflict 15 | - id: check-docstring-first 16 | - id: check-json 17 | - id: check-merge-conflict 18 | - id: check-xml 19 | - id: check-yaml 20 | - id: detect-private-key 21 | - id: end-of-file-fixer 22 | - id: check-symlinks 23 | - id: no-commit-to-branch 24 | - id: debug-statements 25 | - id: pretty-format-json 26 | args: 27 | - --autofix 28 | - --no-sort-keys 29 | - id: requirements-txt-fixer 30 | - id: check-added-large-files 31 | args: 32 | - --maxkb=500 33 | - id: flake8 34 | args: 35 | - --max-line-length=120 36 | - --ignore=E731,W503,W504 37 | - repo: git://github.com/Lucas-C/pre-commit-hooks.git 38 | sha: v1.1.9 39 | hooks: 40 | - id: remove-crlf 41 | - id: remove-tabs 42 | args: ["--whitespaces-count", "2"] # defaults to: 4 43 | - repo: git://github.com/trbs/pre-commit-hooks-trbs.git 44 | sha: e233916fb2b4b9019b4a3cc0497994c7926fe36b 45 | hooks: 46 | - id: forbid-executables 47 | exclude: manage.py|setup.py 48 | #- repo: git://github.com/pre-commit/mirrors-csslint 49 | # sha: v1.0.5 50 | # hooks: 51 | # - id: csslint 52 | - repo: https://github.com/Lucas-C/pre-commit-hooks-safety 53 | sha: v1.1.0 54 | hooks: 55 | - id: python-safety-dependencies-check 56 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Tiago Silva 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020-present Tiago Silva and contributors. 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include LICENSE 3 | include README.md 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := help 2 | 3 | .PHONY: help 4 | help: 5 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 6 | 7 | .PHONY: all 8 | all: init test ## Initiate all tests 9 | 10 | .PHONY: init 11 | init: ## Installs the develop tools and runs the coverage 12 | python setup.py develop 13 | pip install tox "coverage<5" 14 | 15 | .PHONY: test 16 | test: ## Runs the tests 17 | coverage erase 18 | tox --parallel--safe-build 19 | coverage html 20 | 21 | .PHONY: serve-docs 22 | serve-docs: ## Runs the local docs 23 | mkdocs serve 24 | 25 | .PHONY: build-docs 26 | build-docs: ## Runs the local docs 27 | mkdocs build 28 | 29 | ifndef VERBOSE 30 | .SILENT: 31 | endif 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django Fast Utils 2 | 3 | ![Build and Publish](https://github.com/tarsil/django-fast-utils/actions/workflows/main.yml/badge.svg) 4 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=tarsil_django-fast-utils&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=tarsil_django-fast-utils) 5 | [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=tarsil_django-fast-utils&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=tarsil_django-fast-utils) 6 | [![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=tarsil_django-fast-utils&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=tarsil_django-fast-utils) 7 | 8 | **Official Documentation** - 9 | 10 | --- 11 | 12 | ## Table of Contents 13 | 14 | - [Django Fast Utils](#django-fast-utils) 15 | - [Table of Contents](#table-of-contents) 16 | - [About Django Fast Utils](#about-django-fast-utils) 17 | - [Overview](#overview) 18 | - [Supported Django and Python Versions](#supported-django-and-python-versions) 19 | - [Documentation](#documentation) 20 | - [Installation](#installation) 21 | - [Documentation and Support](#documentation-and-support) 22 | - [License](#license) 23 | 24 | --- 25 | 26 | ## About Django Fast Utils 27 | 28 | Django Fast Utils is a miscellaneous of common utilities for every new or existing 29 | django project. From auditing models to database fields and REST framework mixins, 30 | you name it. 31 | 32 | ### Overview 33 | 34 | #### Supported Django and Python Versions 35 | 36 | | Django / Python | 3.7 | 3.8 | 3.9 | 3.10 | 37 | | --------------- | --- | --- | --- | ---- | 38 | | 2.2 | Yes | Yes | Yes | Yes | 39 | | 3.0 | Yes | Yes | Yes | Yes | 40 | | 3.1 | Yes | Yes | Yes | Yes | 41 | | 3.2 | Yes | Yes | Yes | Yes | 42 | | 4.0 | Yes | Yes | Yes | Yes | 43 | 44 | ## Documentation 45 | 46 | ### Installation 47 | 48 | To install django-fast-utils: 49 | 50 | ```shell 51 | $ pip install django-fast-utils 52 | ``` 53 | 54 | ## Documentation and Support 55 | 56 | Full documentation for the project is available at https://django-fast-utils.tarsild.io 57 | 58 | ## License 59 | 60 | Copyright (c) 2022-present Tiago Silva and contributors under the [MIT license](https://opensource.org/licenses/MIT). 61 | -------------------------------------------------------------------------------- /django_fast_utils/__init__.py: -------------------------------------------------------------------------------- 1 | __title__ = "Django Fast Utils" 2 | __version__ = "2.0.4" 3 | __author__ = "Tiago Silva" 4 | __license__ = "MIT" 5 | 6 | # Version synonym 7 | VERSION = __version__ 8 | -------------------------------------------------------------------------------- /django_fast_utils/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class FastUtilsConfig(AppConfig): 5 | name = 'django_fast_utils' 6 | verbose_name = "Django Fast Utils" 7 | -------------------------------------------------------------------------------- /django_fast_utils/auth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/django-fast-utils/6c1e8d4b7285eed9034b056a2a3d16942813a528/django_fast_utils/auth/__init__.py -------------------------------------------------------------------------------- /django_fast_utils/auth/backends.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from rest_framework import exceptions 3 | from rest_framework.authentication import CSRFCheck 4 | from rest_framework_simplejwt.authentication import JWTAuthentication 5 | 6 | 7 | def enforce_csrf(request): 8 | check = CSRFCheck() 9 | check.process_request(request) 10 | reason = check.process_view(request, None, (), {}) 11 | if reason: 12 | raise exceptions.PermissionDenied('CSRF Failed: %s' % reason) 13 | 14 | 15 | class JWTCustomAuthentication(JWTAuthentication): 16 | def authenticate(self, request): 17 | header = self.get_header(request) 18 | 19 | if header is None: 20 | raw_token = request.COOKIES.get(settings.SIMPLE_JWT['AUTH_COOKIE']) or None 21 | elif header.decode('utf-8') == 'null': 22 | raw_token = request.COOKIES.get(settings.SIMPLE_JWT['AUTH_COOKIE']) or None 23 | else: 24 | raw_token = self.get_raw_token(header) 25 | if raw_token is None: 26 | return 27 | 28 | validated_token = self.get_validated_token(raw_token) 29 | enforce_csrf(request) 30 | return self.get_user(validated_token), validated_token 31 | -------------------------------------------------------------------------------- /django_fast_utils/auth/generics.py: -------------------------------------------------------------------------------- 1 | """ 2 | All the mixins that can be used in the system and for Django REST Framework Views 3 | """ 4 | from rest_framework import authentication 5 | from rest_framework.generics import GenericAPIView 6 | from rest_framework.permissions import IsAuthenticated 7 | from rest_framework.views import APIView 8 | 9 | 10 | class AnonymousAuthentication(authentication.BaseAuthentication): 11 | def authenticate(self, request): 12 | return (request._request.user, None) 13 | 14 | 15 | class AuthMeta(type): 16 | """ 17 | Metaclass to create/read from permissions. 18 | """ 19 | def __new__(cls, name, bases, attrs): 20 | permissions = [] 21 | for base in bases: 22 | if hasattr(base, 'permissions'): 23 | permissions.extend(base.permissions) 24 | attrs['permissions'] = permissions + attrs.get('permissions', []) 25 | return type.__new__(cls, name, bases, attrs) 26 | 27 | 28 | class AccessMixin(metaclass=AuthMeta): 29 | """Django rest framework doesn't append permission_classes on inherited models which can bring issues when 30 | it comes to call an API programmatically, this way we create a metaclass that will read from a property custom 31 | from our subclasses and will append to the default `permission_classes` on the subclasses of AccessMixin 32 | """ 33 | pass 34 | 35 | 36 | class AuthMixin(AccessMixin, APIView): 37 | """ 38 | Base APIView requiring login credentials to access it from the inside of the platform 39 | Or via request (if known) 40 | 41 | Example: 42 | ```class MyView(AuthMixin): 43 | permissions = [MyNewPermission] 44 | ``` 45 | """ 46 | permissions = [IsAuthenticated] 47 | 48 | def __init__(self, *args, **kwargs) -> None: 49 | super().__init__(*args, **kwargs) 50 | self.permission_classes = self.permissions 51 | 52 | 53 | class NoPermissionsMixin(APIView): 54 | """ 55 | Remove all the permissions from a view 56 | """ 57 | permission_classes = [] 58 | 59 | 60 | class RequiredUserContextView(GenericAPIView): 61 | """Handles with Generics for user specific views""" 62 | 63 | def get_serializer(self, *args, **kwargs): 64 | serializer_class = self.get_serializer_class() 65 | kwargs['context'] = self.get_serializer_context() 66 | return serializer_class(*args, **kwargs) 67 | 68 | def get_serializer_context(self): 69 | """ """ 70 | context = super().get_serializer_context() 71 | context.update({ 72 | 'request': self.request, 73 | 'user': self.request.user, 74 | }) 75 | return context -------------------------------------------------------------------------------- /django_fast_utils/auth/middleware.py: -------------------------------------------------------------------------------- 1 | """ 2 | All things middleware 3 | """ 4 | from datetime import datetime 5 | 6 | from django.conf import settings 7 | from django.utils.deprecation import MiddlewareMixin 8 | from rest_framework import HTTP_HEADER_ENCODING 9 | from rest_framework.exceptions import ValidationError 10 | from rest_framework_simplejwt.exceptions import InvalidToken, TokenError 11 | from rest_framework_simplejwt.tokens import RefreshToken, UntypedToken 12 | 13 | from .. import settings as api_settings 14 | 15 | AUTH_HEADER_TYPES = settings.SIMPLE_JWT['AUTH_HEADER_TYPES'] 16 | 17 | AUTH_HEADER_TYPE_BYTES = set( 18 | h.encode(HTTP_HEADER_ENCODING) 19 | for h in AUTH_HEADER_TYPES 20 | ) 21 | 22 | if settings.SIMPLE_JWT['BLACKLIST_AFTER_ROTATION']: 23 | from rest_framework_simplejwt.token_blacklist.models import BlacklistedToken 24 | 25 | 26 | def validate_and_refresh(refresh_token): 27 | """ 28 | Validates the refresh token and sets the new token. 29 | """ 30 | refresh = RefreshToken(refresh_token) 31 | data = {"access": str(refresh.access_token)} 32 | 33 | if settings.SIMPLE_JWT['ROTATE_REFRESH_TOKENS']: 34 | if settings.SIMPLE_JWT['BLACKLIST_AFTER_ROTATION']: 35 | try: 36 | refresh.blacklist() 37 | except AttributeError: 38 | pass 39 | 40 | refresh.set_jti() 41 | refresh.set_exp() 42 | refresh.set_iat() 43 | 44 | data['refresh'] = str(refresh) 45 | 46 | return data 47 | 48 | 49 | def validate_token(token): 50 | """ 51 | Verifies if a token sent is valid. 52 | """ 53 | try: 54 | token = UntypedToken(token) 55 | except TokenError as e: 56 | return InvalidToken(e.args[0]) 57 | 58 | if ( 59 | settings.SIMPLE_JWT['BLACKLIST_AFTER_ROTATION'] 60 | and "rest_framework_simplejwt.token_blacklist" in settings.INSTALLED_APPS 61 | ): 62 | jti = token.get(settings.SIMPLE_JWT['JTI_CLAIM']) 63 | if BlacklistedToken.objects.filter(token__jti=jti).exists(): 64 | raise ValidationError("Token is blacklisted") 65 | 66 | return {} 67 | 68 | 69 | class JWTRefreshRequestCookies(MiddlewareMixin): 70 | """ 71 | Refreshes the cookie if exists in the request and 72 | """ 73 | def __init__(self, *args, **kwargs): 74 | super().__init__(*args, **kwargs) 75 | self.app_settings = settings if hasattr(settings, 'DJANGO_FAST_UTILS') else api_settings 76 | 77 | def get_timestamp(self, date_to_parse): 78 | """ 79 | Converts a date into timestamp. 80 | """ 81 | return datetime.timestamp(date_to_parse) 82 | 83 | def get_header(self, request): 84 | """ 85 | Extracts the header containing the JSON web token from the given 86 | request. 87 | """ 88 | header = request.META.get(settings.SIMPLE_JWT['AUTH_HEADER_NAME']) 89 | 90 | if isinstance(header, str): 91 | # Work around django test client oddness 92 | header = header.encode(HTTP_HEADER_ENCODING) 93 | 94 | return header 95 | 96 | def set_cookie(self, response, key, value, max_age, expires, secure, httponly, samesite): 97 | """ 98 | Adds the cookie to the client 99 | """ 100 | now = datetime.utcnow() 101 | max_age = self.get_timestamp(now + max_age) 102 | expires = now + expires 103 | 104 | response.set_cookie( 105 | key=key, value=value, max_age=max_age, expires=expires, secure=secure, httponly=httponly, 106 | samesite=samesite 107 | ) 108 | return response 109 | 110 | def refresh(self, refresh_token): 111 | """ 112 | Validates and refreshes the access token 113 | """ 114 | try: 115 | data = validate_and_refresh(refresh_token) 116 | except TokenError as e: 117 | raise InvalidToken(e.args[0]) 118 | return data 119 | 120 | def exclude_from_request(self, request): 121 | """ 122 | Extracts the full path information. 123 | """ 124 | return request.get_full_path_info() 125 | 126 | def handle_request(self, request): 127 | """ 128 | Handles the request for the acess an and refresh token. 129 | """ 130 | access_token = request.COOKIES.get(settings.SIMPLE_JWT['AUTH_COOKIE']) or None 131 | refresh_token = request.COOKIES.get(settings.SIMPLE_JWT['REFRESH_COOKIE']) or None 132 | data = {} 133 | 134 | if access_token: 135 | is_valid = validate_token(access_token) 136 | 137 | if hasattr(is_valid, 'default_code'): 138 | if is_valid.default_code == 'token_not_valid': 139 | if refresh_token: 140 | data = self.refresh(refresh_token) 141 | request.COOKIES[settings.SIMPLE_JWT['AUTH_COOKIE']] = data[settings.SIMPLE_JWT['AUTH_COOKIE']] 142 | else: 143 | if refresh_token: 144 | data = self.refresh(refresh_token) 145 | request.COOKIES[settings.SIMPLE_JWT['AUTH_COOKIE']] = data[settings.SIMPLE_JWT['AUTH_COOKIE']] 146 | 147 | def handle_response(self, request, response): 148 | """ 149 | Handles the response for the cookie. 150 | """ 151 | access_token = request.COOKIES.get(settings.SIMPLE_JWT['AUTH_COOKIE']) or None 152 | 153 | if not access_token: 154 | return response 155 | 156 | self.set_cookie( 157 | response, 158 | key=settings.SIMPLE_JWT['AUTH_COOKIE'], 159 | value=access_token, 160 | max_age=settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME'], 161 | expires=settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME'], 162 | secure=settings.SIMPLE_JWT['AUTH_COOKIE_SECURE'], 163 | httponly=settings.SIMPLE_JWT['AUTH_COOKIE_HTTP_ONLY'], 164 | samesite=settings.SIMPLE_JWT['AUTH_COOKIE_SAMESITE'] 165 | ) 166 | return response 167 | 168 | def process_request(self, request): 169 | """ 170 | Processes the request by checking the headers of the request and if the tokens 171 | are present in the cookies. 172 | 173 | 1. Checks for the access token 174 | 2. Validates the token 175 | 3. Refreshes the token 176 | 4. Adds it back to the headers 177 | """ 178 | assert 'LOGOUT_URL' in self.app_settings.DJANGO_FAST_UTILS, ( 179 | "'LOGOUT_URL' key is missing from the settings." 180 | ) 181 | assert isinstance(self.app_settings.DJANGO_FAST_UTILS['LOGOUT_URL'], list), ( 182 | f"'LOGOUT_URL' should be a list and not {type(self.app_settings.DJANGO_FAST_UTILS['LOGOUT_URL'])}." 183 | ) 184 | 185 | url = self.exclude_from_request(request) 186 | if url not in self.app_settings.DJANGO_FAST_UTILS['LOGOUT_URL']: 187 | return self.handle_request(request) 188 | 189 | def process_response(self, request, response): 190 | """ 191 | Sends the new access token to the client. 192 | """ 193 | url = self.exclude_from_request(request) 194 | if url not in self.app_settings.DJANGO_FAST_UTILS['LOGOUT_URL']: 195 | return self.handle_response(request, response) 196 | return self.get_response(request) 197 | -------------------------------------------------------------------------------- /django_fast_utils/backends.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provides a set of backends to be used for the login. 3 | """ 4 | from django.contrib.auth import get_user_model 5 | from django.contrib.auth.backends import ModelBackend 6 | 7 | UserModel = get_user_model() 8 | 9 | 10 | class EmailBackend(ModelBackend): 11 | """ 12 | Enables the login backend through email. 13 | By Default django only login with username. This overrides that default and adds a new backend possibility, 14 | allowing also to login using emails. 15 | 16 | Usage: 17 | AUTHENTICATION_BACKENDS = ( 18 | ... 19 | "fast_utils.backends.EmailBackend", 20 | ) 21 | """ 22 | 23 | def authenticate(self, request, email=None, password=None, **kwargs): 24 | if email is None: 25 | email = kwargs.get(UserModel.EMAIL_FIELD) 26 | if email is None or password is None: 27 | return 28 | try: 29 | user = UserModel.objects.get(email__iexact=email) 30 | except UserModel.DoesNotExist: 31 | # Run the default password hasher once to reduce the timing 32 | # difference between an existing and a nonexistent user (#20760). 33 | UserModel().set_password(password) 34 | else: 35 | if user.check_password(password) and self.user_can_authenticate(user): 36 | return user 37 | -------------------------------------------------------------------------------- /django_fast_utils/db/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/django-fast-utils/6c1e8d4b7285eed9034b056a2a3d16942813a528/django_fast_utils/db/__init__.py -------------------------------------------------------------------------------- /django_fast_utils/db/fields.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.db import models 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | 7 | class SubList(list): 8 | def __init__(self, delimiter, *args): 9 | self.delimiter = delimiter 10 | super().__init__(*args) 11 | 12 | def __str__(self): 13 | return self.delimiter.join(self) 14 | 15 | 16 | class ListField(models.TextField): 17 | """ 18 | Special Field type specially design to work with Lists for special databases 19 | """ 20 | 21 | description = _("Group Concat List field") 22 | 23 | def __init__(self, *args, **kwargs): 24 | self.delimiter = kwargs.pop("delimiter", ",") 25 | super().__init__(*args, **kwargs) 26 | 27 | def parse(self, value_string): 28 | json_value = json.loads(value_string) 29 | return list(json_value) 30 | 31 | def get_internal_type(self): 32 | return "ListField" 33 | 34 | def to_python(self, value): 35 | if isinstance(value, list): 36 | return value 37 | if value is None: 38 | return SubList(self.delimiter) 39 | return SubList(self.delimiter, value.split(self.delimiter)) 40 | 41 | def from_db_value(self, value, expression, connection): 42 | return self.to_python(value) 43 | 44 | def get_prep_value(self, value): 45 | if value is None: 46 | return 47 | return value 48 | 49 | def value_to_string(self, obj): 50 | value = self._get_val_from_obj(obj) 51 | return self.get_db_prep_value(value) 52 | -------------------------------------------------------------------------------- /django_fast_utils/exceptions.py: -------------------------------------------------------------------------------- 1 | from django.utils.translation import gettext_lazy as _ 2 | from rest_framework import status 3 | from rest_framework.exceptions import APIException 4 | from rest_framework.exceptions import _get_error_details 5 | 6 | 7 | class ValidationException(APIException): 8 | """ 9 | Wrapper over APIException being more flexible with the core 10 | """ 11 | status_code = status.HTTP_403_FORBIDDEN 12 | default_detail = _('Invalid input.') 13 | default_code = 'invalid' 14 | 15 | def __init__(self, detail=None, code=None, status_code=None): 16 | if detail is None: 17 | detail = self.default_detail 18 | if code is None: 19 | code = self.default_code 20 | self.status_code = status_code or self.status_code 21 | 22 | # For validation failures, we may collect many errors together, 23 | # so the details should always be coerced to a list if not already. 24 | if not isinstance(detail, dict) and not isinstance(detail, list): 25 | detail = {"detail": detail} 26 | 27 | self.detail = _get_error_details(detail, code) 28 | 29 | 30 | class ValidationError(ValidationException): 31 | status_code = status.HTTP_400_BAD_REQUEST 32 | default_detail = _('Invalid input.') 33 | default_code = 'invalid' 34 | 35 | 36 | class NotAuthorized(ValidationException): 37 | status_code = status.HTTP_401_UNAUTHORIZED 38 | default_detail = _('You do not have permission to perform this action.') 39 | default_code = 'not_authorized' 40 | 41 | 42 | class PermissionDenied(ValidationException): 43 | status_code = status.HTTP_403_FORBIDDEN 44 | default_detail = _('You do not have permission to perform this action.') 45 | default_code = 'not_allowed' 46 | -------------------------------------------------------------------------------- /django_fast_utils/fields.py: -------------------------------------------------------------------------------- 1 | """ 2 | Custom fields added to Retools 3 | """ 4 | from rest_framework import serializers 5 | 6 | 7 | class ChoicesField(serializers.Field): 8 | """Choices Field to handle properly with it""" 9 | def __init__(self, choices, **kwargs): 10 | self._choices = choices 11 | super().__init__(**kwargs) 12 | 13 | def to_representation(self, obj): 14 | return self._choices[obj] 15 | 16 | def to_internal_value(self, data): 17 | return getattr(self._choices, data) 18 | 19 | 20 | class WritableSerializerMethodField(serializers.SerializerMethodField): 21 | """ 22 | Serializer method field to be writable. 23 | """ 24 | 25 | def __init__(self, method_name=None, **kwargs): 26 | super().__init__(**kwargs) 27 | self.read_only = False 28 | 29 | def get_default(self): 30 | default = super().get_default() 31 | 32 | return {self.field_name: default} 33 | 34 | def to_internal_value(self, data): 35 | return {self.field_name: data} 36 | 37 | 38 | class AbsoluteImageField(serializers.ImageField): 39 | """ 40 | Returns the absolute path of a image object. 41 | """ 42 | 43 | def to_representation(self, value): 44 | if not value: 45 | return None 46 | 47 | request = self.context.get('request', None) 48 | if request is not None: 49 | return request.build_absolute_uri(value.url) 50 | return value.url -------------------------------------------------------------------------------- /django_fast_utils/helpers.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import pytz 3 | import json 4 | from collections import namedtuple 5 | import functools 6 | 7 | 8 | class BaseClass: 9 | """ 10 | This class serves with the purpose of dynamically create a constructor and override the default 11 | __eq__ operator from python allowing the direct comparison between python objects 12 | """ 13 | 14 | def __init__(self, **kwargs): 15 | for i in kwargs: 16 | setattr(self, i, kwargs[i]) 17 | 18 | def __eq__(self, instance): 19 | if not isinstance(instance, self.__class__): 20 | return False 21 | if hash(frozenset(self.__dict__.items())) == hash(frozenset(instance.__dict__.items())): 22 | return True 23 | return False 24 | 25 | def __ne__(self, instance): 26 | if not isinstance(instance, self.__class__): 27 | return True 28 | if hash(frozenset(self.__dict__.items())) != hash(frozenset(instance.__dict__.items())): 29 | return True 30 | return False 31 | 32 | @property 33 | def utc(self): 34 | utc_now = pytz.utc.localize(datetime.datetime.utcnow()) 35 | return utc_now 36 | 37 | def __repr__(self): 38 | return '<%s - %s />' % (self.__class__.__name__, self.utc) 39 | 40 | 41 | class Slot2Object: 42 | """ 43 | Class that converts a dict into a object using slots. 44 | Performance wise, is faster and gives a better memory usage. 45 | 46 | Usage: 47 | _dict = {'a': 1, 'b': {'c': 1}} 48 | s = Slot2Object(_dict) 49 | 50 | Result: 51 | s.a 52 | 1 53 | 54 | s.b 55 | {'c': 1} 56 | """ 57 | __slots__ = ['__dict__'] 58 | 59 | def __init__(self, __dict__): 60 | self.__dict__ = __dict__ 61 | 62 | def __getitem__(self, item): 63 | return getattr(self, item) 64 | 65 | 66 | def json_2object(json_payload): 67 | """ 68 | Returns a Python type object from a json type 69 | :param json_payload: Json to be converted 70 | :return: Python object 71 | """ 72 | return json.loads(json_payload, object_hook=lambda d: namedtuple('X', list(d.keys()))(*list(d.values()))) 73 | 74 | 75 | def rgetattr(obj, attr, *args): # pragma: no cover 76 | """ 77 | A replacement of the default getattr() with a nuance of getting the nested values from objects 78 | :param obj: Object to lookup 79 | :param attr: Attribute to search 80 | :param args: Defaut value 81 | :return: Value 82 | """ 83 | 84 | def _getattr(obj, attr): 85 | return getattr(obj, attr, *args) 86 | 87 | return functools.reduce(_getattr, [obj] + attr.split('.')) 88 | 89 | 90 | class SlotObject: 91 | """ 92 | Class that converts a dict into an object using slots. 93 | Performance wise, is faster and gives a better memory usage. 94 | This class provides a nested setattr. 95 | 96 | Usage: 97 | _dict = {'a': 1, 'b': {'c': 1}} 98 | s = SlotObject(_dict) 99 | 100 | Result: 101 | s.a 102 | 1 103 | 104 | s.b.c 105 | 1 106 | """ 107 | __slots__ = ['__dict__'] 108 | 109 | def __init__(self, __dict__): 110 | for k, v in __dict__.items(): 111 | setattr( 112 | self, k, self.__class__(v)) if isinstance(v, dict) and v else setattr(self, k, v) 113 | 114 | def __getitem__(self, item): 115 | return getattr(self, item) 116 | 117 | 118 | def remove_prefix(text, prefix): 119 | """ 120 | In python 3.9 there is already a native function to remove the prefixes. 121 | See https://www.python.org/dev/peps/pep-0616/ 122 | If the version is inferior to 3.9 then runs the below solution. 123 | """ 124 | if text.startswith(prefix): 125 | return text[len(prefix):] 126 | return text 127 | 128 | 129 | class Singleton: 130 | """ 131 | Simple python object representation of a Singleton. 132 | """ 133 | instance = None 134 | 135 | def __new__(cls, *args, **kwargs): 136 | if cls.instance is not None: 137 | return cls.instance 138 | 139 | _instance = cls.instance = super().__new__(cls, *args, **kwargs) 140 | return _instance -------------------------------------------------------------------------------- /django_fast_utils/paginator.py: -------------------------------------------------------------------------------- 1 | from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger 2 | from rest_framework import pagination 3 | from rest_framework.response import Response 4 | 5 | 6 | class NumberDetailPagination(pagination.PageNumberPagination): 7 | """ 8 | Custom paginator for REST API responses 9 | 'links': { 10 | 'next': next page url, 11 | 'previous': previous page url 12 | }, 13 | 'count': number of records fetched, 14 | 'total_pages': total number of pages, 15 | 'next': bool has next page, 16 | 'previous': bool has previous page, 17 | 'results': result set 18 | }) 19 | 20 | Example: 21 | ``` 22 | class MyView(ListAPIView): 23 | pagination_class = [NumberDetailPagination] 24 | ``` 25 | """ 26 | 27 | def get_paginated_response(self, data): 28 | """ 29 | Args: 30 | data: 31 | Returns: 32 | """ 33 | return Response({ 34 | 'links': { 35 | 'next': self.get_next_link(), 36 | 'previous': self.get_previous_link() 37 | }, 38 | 'pagination': { 39 | 'previous_page': self.page.number - 1 if self.page.number != 1 else None, 40 | 'current_page': self.page.number, 41 | 'next_page': self.page.number + 1 if self.page.has_next() else None, 42 | 'page_size': self.page_size 43 | }, 44 | 'count': self.page.paginator.count, 45 | 'total_pages': self.page.paginator.num_pages, 46 | 'next': self.page.has_next(), 47 | 'previous': self.page.has_previous(), 48 | 'results': data 49 | }) 50 | 51 | 52 | def paginator(request, queryset=None, number_per_page=NumberDetailPagination.page_size): 53 | """Returns the custom paginator for the django custom pages 54 | 55 | Args: 56 | request: The request of a page 57 | queryset: The queryset to calculate the number of pages (Default value = None) 58 | number_per_page: Number of results per page (Default value = NumberDetailPagination.page_size) 59 | 60 | Returns: 61 | paginator 62 | """ 63 | paginator = Paginator(queryset, number_per_page) 64 | page = request.GET.get('page') 65 | try: 66 | pages = paginator.page(page) 67 | except PageNotAnInteger: 68 | pages = paginator.page(1) 69 | except EmptyPage: 70 | pages = paginator.page(paginator.num_pages) 71 | return pages -------------------------------------------------------------------------------- /django_fast_utils/permissions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provides a set pluggable permission tools. 3 | """ 4 | 5 | from guardian.shortcuts import assign_perm, remove_perm 6 | 7 | 8 | def assign_user_perm(perm, user_or_group, obj, revoke=False): 9 | """Assigns permissions for a given object to a given django user or group 10 | 11 | Args: 12 | perm: permission to be assigned 13 | user_or_group: django user or group 14 | obj: object to assign to 15 | revoke: True if permissions of a given object is to be removed. 16 | 17 | Example: 18 | `assign_user_perm('is_super_user', user, cars)` (Default value = False) 19 | """ 20 | if not revoke: 21 | assign_perm(perm, user_or_group, obj) 22 | else: 23 | remove_perm(perm, user_or_group, obj) 24 | -------------------------------------------------------------------------------- /django_fast_utils/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Settings used for Django Fast Utils 3 | """ 4 | 5 | DJANGO_FAST_UTILS = { 6 | 'LOGOUT_URL': ['/logout'] 7 | } -------------------------------------------------------------------------------- /django_fast_utils/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/django-fast-utils/6c1e8d4b7285eed9034b056a2a3d16942813a528/django_fast_utils/utils/__init__.py -------------------------------------------------------------------------------- /django_fast_utils/utils/audit.py: -------------------------------------------------------------------------------- 1 | """ 2 | For auditing models. 3 | """ 4 | 5 | from django.conf import settings 6 | from django.db import models 7 | 8 | exclude = ["created_at", "created_by", "updated_at", "updated_by"] 9 | 10 | CREATED_AT = "created_%(class)s_set" 11 | MODIFIED_AT = "modified_%(class)s_set" 12 | 13 | 14 | class GeneralDateTimeModel(models.Model): 15 | created_at = models.DateTimeField(null=False, blank=False, auto_now_add=True) 16 | updated_at = models.DateTimeField(null=False, blank=False, auto_now=True) 17 | 18 | class Meta: 19 | abstract = True 20 | 21 | 22 | class IndexGeneralDateTimeModel(models.Model): 23 | created_at = models.DateTimeField(null=False, blank=False, auto_now_add=True, db_index=True) 24 | updated_at = models.DateTimeField(null=False, blank=False, auto_now=True, db_index=True) 25 | 26 | class Meta: 27 | abstract = True 28 | 29 | 30 | class TimeStampedModel(models.Model): 31 | created_at = models.DateTimeField(null=False, blank=False, auto_now_add=True) 32 | created_by = models.ForeignKey( 33 | settings.AUTH_USER_MODEL, 34 | related_name=CREATED_AT, 35 | null=False, 36 | blank=True, 37 | on_delete=models.DO_NOTHING, 38 | ) 39 | updated_at = models.DateTimeField(null=False, blank=False, auto_now=True) 40 | updated_by = models.ForeignKey( 41 | settings.AUTH_USER_MODEL, 42 | related_name=MODIFIED_AT, 43 | null=False, 44 | blank=True, 45 | on_delete=models.SET_NULL, 46 | ) 47 | 48 | class Meta: 49 | abstract = True 50 | 51 | 52 | class GeneralTimeStampedModel(models.Model): 53 | created_at = models.DateTimeField(null=False, blank=False, auto_now_add=True) 54 | created_by = models.ForeignKey( 55 | settings.AUTH_USER_MODEL, 56 | related_name=CREATED_AT, 57 | null=True, 58 | blank=True, 59 | on_delete=models.DO_NOTHING, 60 | ) 61 | updated_at = models.DateTimeField(null=False, blank=False, auto_now=True) 62 | updated_by = models.ForeignKey( 63 | settings.AUTH_USER_MODEL, 64 | related_name=MODIFIED_AT, 65 | null=True, 66 | blank=True, 67 | on_delete=models.SET_NULL, 68 | ) 69 | 70 | class Meta: 71 | abstract = True 72 | 73 | 74 | class IndexedGeneralTimeStampedModel(models.Model): 75 | created_at = models.DateTimeField(null=False, blank=False, auto_now_add=True, db_index=True) 76 | created_by = models.ForeignKey( 77 | settings.AUTH_USER_MODEL, 78 | related_name=CREATED_AT, 79 | null=True, 80 | blank=True, 81 | on_delete=models.DO_NOTHING, 82 | ) 83 | updated_at = models.DateTimeField(null=False, blank=False, auto_now=True, db_index=True) 84 | updated_by = models.ForeignKey( 85 | settings.AUTH_USER_MODEL, 86 | related_name=MODIFIED_AT, 87 | null=True, 88 | blank=True, 89 | on_delete=models.SET_NULL, 90 | ) 91 | 92 | class Meta: 93 | abstract = True -------------------------------------------------------------------------------- /django_fast_utils/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/django-fast-utils/6c1e8d4b7285eed9034b056a2a3d16942813a528/django_fast_utils/views/__init__.py -------------------------------------------------------------------------------- /django_fast_utils/views/auth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/django-fast-utils/6c1e8d4b7285eed9034b056a2a3d16942813a528/django_fast_utils/views/auth/__init__.py -------------------------------------------------------------------------------- /django_fast_utils/views/auth/serializers.py: -------------------------------------------------------------------------------- 1 | import bleach 2 | from django.contrib.auth import authenticate, get_user_model 3 | from rest_framework import serializers 4 | 5 | User = get_user_model() 6 | 7 | 8 | class LoginSerializer(serializers.Serializer): 9 | """Validates the login of a User""" 10 | email = serializers.EmailField(allow_blank=False, required=True) 11 | password = serializers.CharField(allow_blank=False, required=True) 12 | 13 | def __init__(self, *args, **kwargs): 14 | super().__init__(*args, **kwargs) 15 | self.authed_user = None 16 | 17 | def get_user(self): 18 | return self.authed_user 19 | 20 | def validate(self, attrs): 21 | try: 22 | email = attrs['email'].strip() 23 | password = attrs['password'] 24 | try: 25 | self.authed_user = authenticate(email=bleach.clean(email), password=password) 26 | except ValueError: 27 | self.authed_user = None 28 | 29 | if self.authed_user: 30 | return attrs 31 | 32 | except (User.DoesNotExist, KeyError): 33 | pass 34 | raise serializers.ValidationError("Your login details were incorrect. Please try again.") 35 | -------------------------------------------------------------------------------- /django_fast_utils/views/auth/views.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from django.conf import settings 4 | from django.http import HttpResponse 5 | from django.middleware import csrf 6 | from django.utils.translation import ugettext_lazy as _ 7 | from rest_framework import status 8 | from rest_framework.response import Response 9 | from rest_framework.views import APIView 10 | from rest_framework_simplejwt.tokens import RefreshToken 11 | 12 | from .serializers import LoginSerializer 13 | 14 | 15 | def get_tokens_for_user(user): 16 | """ 17 | Gets the token for a given user using JWT 18 | """ 19 | refresh = RefreshToken.for_user(user) 20 | 21 | return { 22 | 'access': str(refresh.access_token), 23 | 'refresh': str(refresh) 24 | } 25 | 26 | 27 | class LoginJWTApiView(APIView): 28 | """ 29 | User login for JWT 30 | """ 31 | serializer_class = LoginSerializer 32 | 33 | def get_timestamp(self, last_login): 34 | return datetime.timestamp(last_login) 35 | 36 | def set_cookie(self, response, key, value, max_age, expires, secure, httponly, samesite): 37 | now = datetime.utcnow() 38 | max_age = self.get_timestamp(now + max_age) 39 | expires = now + expires 40 | 41 | response.set_cookie( 42 | key=key, value=value, max_age=max_age, expires=expires, secure=secure, httponly=httponly, 43 | samesite=samesite 44 | ) 45 | return response 46 | 47 | def process_access_token(self, response, data): 48 | """ 49 | Processes a JWT access token 50 | """ 51 | response = self.set_cookie( 52 | response, 53 | key=settings.SIMPLE_JWT['AUTH_COOKIE'], 54 | value=data["access"], 55 | max_age=settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME'], 56 | expires=settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME'], 57 | secure=settings.SIMPLE_JWT['AUTH_COOKIE_SECURE'], 58 | httponly=settings.SIMPLE_JWT['AUTH_COOKIE_HTTP_ONLY'], 59 | samesite=settings.SIMPLE_JWT['AUTH_COOKIE_SAMESITE'] 60 | ) 61 | return response 62 | 63 | def process_refresh_token(self, response, data): 64 | """ 65 | Processes a JWT refresh token 66 | """ 67 | response = self.set_cookie( 68 | response, 69 | key=settings.SIMPLE_JWT['REFRESH_COOKIE'], 70 | value=data["refresh"], 71 | max_age=settings.SIMPLE_JWT['REFRESH_TOKEN_LIFETIME'], 72 | expires=settings.SIMPLE_JWT['REFRESH_TOKEN_LIFETIME'], 73 | secure=settings.SIMPLE_JWT['REFRESH_COOKIE_SECURE'], 74 | httponly=settings.SIMPLE_JWT['REFRESH_COOKIE_HTTP_ONLY'], 75 | samesite=settings.SIMPLE_JWT['REFRESH_COOKIE_SAMESITE'] 76 | ) 77 | return response 78 | 79 | def assert_values(self): 80 | """ 81 | Checks if the values needed for the login are in the settings. 82 | """ 83 | assert hasattr(settings, 'SIMPLE_JWT'), ( 84 | 'SIMPLE_JWT is not in the settings.' 85 | ) 86 | 87 | values = [ 88 | 'AUTH_COOKIE', 89 | 'ACCESS_TOKEN_LIFETIME', 90 | 'AUTH_COOKIE_SECURE', 91 | 'AUTH_COOKIE_HTTP_ONLY', 92 | 'AUTH_COOKIE_SAMESITE', 93 | 'REFRESH_COOKIE', 94 | 'REFRESH_TOKEN_LIFETIME', 95 | 'REFRESH_COOKIE_SECURE', 96 | 'REFRESH_COOKIE_HTTP_ONLY', 97 | 'REFRESH_COOKIE_SAMESITE' 98 | ] 99 | 100 | for value in values: 101 | assert value in settings.SIMPLE_JWT, ( 102 | "The %s is not in SIMPLE_JWT settings." % value 103 | ) 104 | 105 | 106 | def post(self, request, format=None): 107 | """ 108 | Gets an email and a password, sanitizes and log a user. 109 | """ 110 | self.assert_values() 111 | 112 | serializer = self.serializer_class(data=request.data) 113 | serializer.is_valid(raise_exception=True) 114 | response = Response() 115 | 116 | # GETS THE USER 117 | user = serializer.get_user() 118 | 119 | if user is None or not user: 120 | return Response({ 121 | 'detail': 'Invalid email or password.' 122 | }, status=status.HTTP_404_NOT_FOUND) 123 | 124 | # CHECK ACTIVE USER 125 | if not user.is_active: 126 | return Response({ 127 | 'detail': 'This account is not active.' 128 | }, status=status.HTTP_404_NOT_FOUND) 129 | 130 | data = get_tokens_for_user(user) 131 | response = self.process_access_token(response, data) 132 | response = self.process_refresh_token(response, data) 133 | 134 | # UPDATES CSRF 135 | csrf.get_token(request) 136 | response.data = data 137 | return response 138 | 139 | 140 | class LogoutJWTApiView(APIView): 141 | 142 | def post(self, request): 143 | """ 144 | Logs out of the system and removes the cookie. 145 | """ 146 | response = Response() 147 | response.delete_cookie(key="csrftoken") 148 | response.delete_cookie( 149 | key=settings.SIMPLE_JWT['AUTH_COOKIE'] 150 | ) 151 | response.delete_cookie( 152 | key=settings.SIMPLE_JWT['REFRESH_COOKIE'] 153 | ) 154 | return response 155 | -------------------------------------------------------------------------------- /django_fast_utils/views/generics.py: -------------------------------------------------------------------------------- 1 | """ 2 | Useful addons to be used in the 3 | """ 4 | from django.db.models.query import QuerySet 5 | from rest_framework.generics import GenericAPIView, ListAPIView 6 | 7 | 8 | class SelectRelatedMixin(GenericAPIView): 9 | select_related = None 10 | 11 | def get_queryset(self): 12 | assert self.model is not None, ( 13 | "'%s' should include a `model` attribute, " 14 | "or override the `get_model()` method." 15 | % self.__class__.__name__ 16 | ) 17 | 18 | assert self.select_related is None or ( 19 | isinstance(self.select_related, list) or isinstance(self.select_related, tuple) 20 | ), ( 21 | "'select_related' must be a list and not %s." % type(self.select_related) 22 | ) 23 | 24 | queryset = self.queryset 25 | if isinstance(queryset, QuerySet): 26 | queryset = queryset.select_related(*self.select_related).all() 27 | return queryset 28 | 29 | 30 | class PrefetchRelatedMixin(GenericAPIView): 31 | prefetch_related = None 32 | 33 | def get_queryset(self): 34 | assert self.model is not None, ( 35 | "'%s' should include a `model` attribute, " 36 | "or override the `get_model()` method." 37 | % self.__class__.__name__ 38 | ) 39 | 40 | assert self.prefetch_related is None or ( 41 | isinstance(self.prefetch_related, list) or isinstance(self.prefetch_related, tuple) 42 | ), ( 43 | "'prefetch_related' must be a list and not %s." % type(self.prefetch_related) 44 | ) 45 | 46 | queryset = self.queryset 47 | if isinstance(queryset, QuerySet): 48 | queryset = queryset.all() 49 | queryset = queryset.prefetch_related(*self.prefetch_related) 50 | return queryset 51 | -------------------------------------------------------------------------------- /docs/auth/generics.md: -------------------------------------------------------------------------------- 1 | # Auth 2 | 3 | [Django Rest framework](https://www.django-rest-framework.org/) provides a set of abstractions 4 | on the top of django. 5 | 6 | This package provides an abstraction on the top of Django Rest framework. 7 | 8 | ## AnonymousAuthentication 9 | 10 | Mixin for anonymous users. 11 | 12 | ## How to use 13 | 14 | ```python 15 | from django_fast_utils.auth.generics import AnonymousAuthentication 16 | from rest_framework.views import APIView 17 | 18 | 19 | class MyView(AnonymousAuthentication, APIView): 20 | pass 21 | 22 | ``` 23 | 24 | ## AuthMixin 25 | 26 | Django rest framework doesn't append permission_classes on inherited models which can bring issues when 27 | it comes to call an API programmatically, this way we create a metaclass that will read from a property custom 28 | from our subclasses and will append to the default `permission_classes`. 29 | 30 | ## How to use 31 | 32 | ```python 33 | from django_fast_utils.auth.generics import AuthMixin 34 | from rest_framework.views import APIView 35 | from rest_framework.permissions import IsAdminUser 36 | 37 | 38 | class MyView(AuthMixin, APIView): 39 | permissions = [IsAdminUser] 40 | 41 | 42 | MyView.permissions # Returns IsAuthenticated (default from AuthMixin) and IsAdminUser 43 | 44 | ``` 45 | 46 | Django Fast Utils allows to extend the `permissions` on every inherited view without overriding anything. 47 | 48 | ## NoPermissionsMixin 49 | 50 | No permissions applied to the views. Only used when no permissions (auth) is not needed for 51 | specific cases. 52 | 53 | ## RequiredUserContextView 54 | 55 | Used to inject the `request.user` into serializers when needed. 56 | 57 | ### How to use 58 | 59 | ```python 60 | from django_fast_utils.auth.generics import RequiredUserContextView 61 | 62 | 63 | class MyView(RequiredUserContextView, ListAPIView): 64 | ... 65 | 66 | 67 | class MySecondView(RequiredUserContextView, APIView): 68 | 69 | def post(self, request, *args, **kwargs): 70 | serializer = MySerializer(data=request.data, context=self.get_serializer_context()) 71 | 72 | ``` 73 | -------------------------------------------------------------------------------- /docs/backends.md: -------------------------------------------------------------------------------- 1 | # Backends 2 | 3 | ## Email Backend 4 | 5 | Authentication backend that uses the `email` instead of the `username` for the logins. 6 | 7 | ### How to use 8 | 9 | In your `settings.py` file, add the following. 10 | 11 | ```python 12 | 13 | AUTHENTICATION_BACKENDS = ( 14 | ... 15 | "django_fast_utils.backends.EmailBackend", 16 | ) 17 | ``` 18 | 19 | You can now use the email to login into your application using the `authenticate` django 20 | standard. 21 | 22 | ## Example 23 | 24 | Using [Django Rest framework](https://www.django-rest-framework.org/). 25 | 26 | - `views.py` 27 | 28 | ```python 29 | from rest_framework.views import APIView 30 | from rest_framework.response import Response 31 | from rest_framework import status 32 | from .serializers import LoginSerializer 33 | 34 | 35 | class LoginApiView(APIView): 36 | serializer_class = LoginSerializer 37 | 38 | def post(self, request, *args, **kwargs): 39 | serializer = self.serializer_class(data=request.data) 40 | serializer.is_valid(raise_exception=True) 41 | user = serializer.get_user() 42 | login(request, user) 43 | 44 | return Response(status=status.HTTP_200_OK) 45 | 46 | ``` 47 | 48 | - `serializers.py` 49 | 50 | ```python 51 | from rest_framework import serializers 52 | 53 | 54 | class LoginSerializer(serializers.Serializer): 55 | email = serializers.EmailField(allow_blank=False, required=True) 56 | password = serializers.CharField(allow_blank=False, required=True) 57 | 58 | def __init__(self, *args, **kwargs): 59 | super().__init__(*args, **kwargs) 60 | self.auth_user = None 61 | 62 | def get_user(self): 63 | return self.auth_user 64 | 65 | def validate(self, attrs): 66 | try: 67 | email = attrs["email"].strip() 68 | password = attrs["password"] 69 | try: 70 | self.auth_user = authenticate(email=email, password=password) 71 | except ValueError: 72 | self.auth_user = None 73 | 74 | if self.auth_user: 75 | return attrs 76 | 77 | except (accounts.models.HubUser.DoesNotExist, KeyError): 78 | raise serializers.ValidationError(_("Your login details were incorrect. Please try again.")) 79 | ``` 80 | -------------------------------------------------------------------------------- /docs/db/fields.md: -------------------------------------------------------------------------------- 1 | # Database 2 | 3 | Custom fields to be used within the models. 4 | 5 | ## ListField 6 | 7 | Save lists in python format directly. 8 | 9 | ### How to use 10 | 11 | ```python 12 | from django.db import models 13 | from django_fast_utils.db.fields import ListField 14 | 15 | 16 | class MyModel(models.Model): 17 | my_list = ListField() 18 | ... 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/exceptions.md: -------------------------------------------------------------------------------- 1 | # Exceptions 2 | 3 | [Django Rest framework](https://www.django-rest-framework.org/) brings all the package 4 | when it comes to validations but sometimes there is a need to have something more unique 5 | and granular. 6 | 7 | ## ValidationException 8 | 9 | The application on any level (view, model, serializer) can raise an exception similar to APIException 10 | but with the possibility of providing different `status_code` and message details. 11 | 12 | ### How to use 13 | 14 | ```python 15 | ... 16 | from django_fast_utils.exceptions import ValidationException 17 | 18 | class MyView(APIView): 19 | 20 | def get(self, request, *args, **kwargs): 21 | try: 22 | user = User.objects.get(pk=1) 23 | except User.DoesNotExist: 24 | raise ValidationException( 25 | "User does not exist.", 26 | status_code=200 27 | ) 28 | ``` 29 | 30 | ## ValidationError 31 | 32 | Same as as ValidationException with the default to `HTTP_400_BAD_REQUEST`. 33 | 34 | ## NotAuthorized 35 | 36 | Same as as ValidationException with the default to `HTTP_401_UNAUTHORIZED`. 37 | 38 | ## PermissionDenied 39 | 40 | Same as as ValidationException with the default to `HTTP_403_FORBIDDEN`. 41 | -------------------------------------------------------------------------------- /docs/fields.md: -------------------------------------------------------------------------------- 1 | # Fields 2 | 3 | Custom fields to be used within `models` or `serializers` of any django application using 4 | [Django Rest framework](https://www.django-rest-framework.org/). 5 | 6 | ## ChoicesField 7 | 8 | Django provides a choices inside the CharField with the attribute `choices`. This field 9 | is a wrapper that allows direct declaration in the models. 10 | 11 | ### How to use 12 | 13 | ```python 14 | from django_fast_utils.fields import ChoicesField 15 | from django.db import models 16 | 17 | 18 | class MyModel(models.Model): 19 | custom_choices = ChoicesField(choices=MY_CHOICES) 20 | ... 21 | ``` 22 | 23 | ## WritableSerializerMethodField 24 | 25 | Custom version of `SerializerMethodField` from [Django Rest framework](https://www.django-rest-framework.org/) 26 | that allows read/write. 27 | 28 | ### How to use 29 | 30 | ```python 31 | from django_fast_utils.fields import WritableSerializerMethodField 32 | from rest_framwork import serializers 33 | 34 | 35 | class MySerializer(serializers.Serializer): 36 | name = WritableSerializerMethodField() 37 | 38 | def get_name(self, instance): 39 | ... 40 | ... 41 | ``` 42 | 43 | ## AbsoluteImageField 44 | 45 | When serializing an ImageField url usually implies some extra code to show the full path url 46 | and expose the full url into APIs. 47 | 48 | ### How to use 49 | 50 | ```python 51 | from django_fast_utils.fields import AbsoluteImageField 52 | from rest_framwork import serializers 53 | 54 | 55 | class MySerializer(serializers.Serializer): 56 | photo = AbsoluteImageField() 57 | 58 | ``` 59 | -------------------------------------------------------------------------------- /docs/helpers.md: -------------------------------------------------------------------------------- 1 | # Helpers 2 | 3 | The package provides some helpers to make the development easier. 4 | 5 | ## BaseClass 6 | 7 | This class is used to simulate a direct comparison between python objects excluding the `id` allowing 8 | an immediate object to object valuation. 9 | 10 | ### How to use 11 | 12 | ```python 13 | from django_fast_utils.helpers import BaseClass 14 | 15 | 16 | class MyObject(BaseClass): 17 | def __init__(self, name): 18 | self.name = name 19 | 20 | obj1 = MyObject(name='test') 21 | obj2 = MyObject(name='test') 22 | 23 | obj1 == obj2 # returns True 24 | 25 | ``` 26 | 27 | ## Slot2Object 28 | 29 | Converts a dictionary to a python like direct object with one level. 30 | 31 | ### How to use 32 | 33 | ```python 34 | from django_fast_utils.helpers import Slot2Object 35 | 36 | _dict = {'a': 1, 'b': {'c': 1}, {'d': {'d1': 3}}} 37 | s = Slot2Object(_dict) 38 | 39 | Result: 40 | s.a 41 | 1 42 | 43 | s.b 44 | {'c': 1} 45 | 46 | ``` 47 | 48 | ## json_2object 49 | 50 | Returns a Python type object from a json type 51 | 52 | ## SlotObject 53 | 54 | Similar to [Slot2Object](#slot2object) but with access to all object levels. 55 | 56 | Class that converts a dict into an object using slots. 57 | Performance wise, is faster and gives a better memory usage. 58 | This class provides a nested setattr. 59 | 60 | ### How to use 61 | 62 | ```python 63 | from django_fast_utils.helpers import SlotObject 64 | 65 | _dict = {'a': 1, 'b': {'c': 1}} 66 | s = SlotObject(_dict) 67 | 68 | Result: 69 | s.a 70 | 1 71 | 72 | s.b.c 73 | 1 74 | ``` 75 | 76 | ## remove_prefix 77 | 78 | For versions prior to python 3.9, removes the prefixes from a given string. 79 | 80 | ### How to use 81 | 82 | ```python 83 | from django_fast_utils.helpers import remove_prefix 84 | 85 | remove_prefix('mytest', 'my') # Returns 'test' 86 | 87 | ``` 88 | 89 | ## Singleton 90 | 91 | Python object implementation of a Singleton 92 | 93 | ### How to use 94 | 95 | ```python 96 | from django_fast_utils.helpers import Singleton 97 | 98 | class A: 99 | pass 100 | 101 | class B(Singleton): 102 | pass 103 | 104 | a1 = A() 105 | a2 = A() 106 | b1 = B() 107 | b2 = B() 108 | 109 | assert a1 != a2 # True 110 | assert b1 == b2 # True 111 | 112 | ``` 113 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Django Fast Utils 2 | 3 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=tarsil_django-fast-utils&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=tarsil_django-fast-utils) 4 | [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=tarsil_django-fast-utils&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=tarsil_django-fast-utils) 5 | [![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=tarsil_django-fast-utils&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=tarsil_django-fast-utils) 6 | 7 | **Official Documentation** - https://tarsil.github.io/django-fast-utils/ 8 | 9 | --- 10 | 11 | ## Table of Contents 12 | 13 | - [Django Fast Utils](#django-fast-utils) 14 | - [Table of Contents](#table-of-contents) 15 | - [About Django Fast Utils](#about-django-fast-utils) 16 | - [Overview](#overview) 17 | - [Supported Django and Python Versions](#supported-django-and-python-versions) 18 | - [Documentation](#documentation) 19 | - [Installation](#installation) 20 | - [License](#license) 21 | 22 | --- 23 | 24 | ## About Django Fast Utils 25 | 26 | Django Fast Utils is a miscellaneous of common utilities for every new or existing 27 | django projects. From auditing models to database fields and REST framework mixins. 28 | 29 | ### Overview 30 | 31 | #### Supported Django and Python Versions 32 | 33 | | Django / Python | 3.7 | 3.8 | 3.9 | 3.10 | 34 | | --------------- | --- | --- | --- | ---- | 35 | | 2.2 | Yes | Yes | Yes | Yes | 36 | | 3.0 | Yes | Yes | Yes | Yes | 37 | | 3.1 | Yes | Yes | Yes | Yes | 38 | | 3.2 | Yes | Yes | Yes | Yes | 39 | | 4.0 | Yes | Yes | Yes | Yes | 40 | 41 | ## Documentation 42 | 43 | ### Installation 44 | 45 | To install django-fast-utils: 46 | 47 | ```shell 48 | pip install django-fast-utils 49 | ``` 50 | 51 | ## License 52 | 53 | Copyright (c) 2022-present Tiago Silva and contributors under the [MIT license](https://opensource.org/licenses/MIT). 54 | -------------------------------------------------------------------------------- /docs/paginator.md: -------------------------------------------------------------------------------- 1 | # Paginator 2 | 3 | ## NumberDetailPagination 4 | 5 | [Django Rest framework](https://www.django-rest-framework.org/) provides standard paginator 6 | classes but sometimes more details are needed to sent in the payload. 7 | 8 | ### Payload definition 9 | 10 | ```python 11 | links': { 12 | 'next': next page url, 13 | 'previous': previous page url 14 | }, 15 | 'count': number of records fetched, 16 | 'total_pages': total number of pages, 17 | 'next': bool has next page, 18 | 'previous': bool has previous page, 19 | 'results': result set 20 | }) 21 | ``` 22 | 23 | ### How to use 24 | 25 | - View level: 26 | 27 | ```python 28 | from django_fast_utils.paginator import NumberDetailPagination 29 | ... 30 | 31 | class MyView(ListAPIView): 32 | pagination_class = NumberDetailPagination 33 | ... 34 | ``` 35 | 36 | - Making it global and default for all views with pagination: 37 | 38 | ```python 39 | REST_FRAMEWORK = { 40 | ... 41 | "DEFAULT_PAGINATION_CLASS": "django_fast_utils.paginator.NumberDetailPagination" 42 | ... 43 | } 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/permissions.md: -------------------------------------------------------------------------------- 1 | # Permissions 2 | 3 | Django Fast Utils uses [Django Guardian](https://django-guardian.readthedocs.io/en/stable/) 4 | to manipulate the permissions of objects. 5 | 6 | ## How to use 7 | 8 | - Add `django-guardian` to your project `settings.py`. 9 | 10 | ```python 11 | 12 | INSTALLED_APPS = ( 13 | # ... 14 | 'guardian' 15 | ) 16 | 17 | AUTHENTICATION_BACKENDS = ( 18 | ... 19 | 'guardian.backends.ObjectPermissionBackend', 20 | ... 21 | ) 22 | 23 | ``` 24 | 25 | - Use in your code. 26 | 27 | ```python 28 | from django_fast_utils.permissions import assign_user_perm 29 | 30 | 31 | assign_user_perm('is_super_user', user, cars) # (Default value = False) 32 | user.has_perm('is_super_user', user) # True 33 | 34 | ``` 35 | 36 | Django guardian by default creates an anonymous user and that can be disabled in the 37 | `settings.py`. 38 | 39 | ```python 40 | 41 | ANONYMOUS_USER_NAME = None 42 | 43 | ``` 44 | -------------------------------------------------------------------------------- /docs/releases.md: -------------------------------------------------------------------------------- 1 | # Releases 2 | 3 | ## 2.0.4 4 | 5 | - Fixed code smell in `get_prep_value` from `ListField`. 6 | 7 | ## 2.0.3 8 | 9 | - Fixed middleware validation and verification. 10 | - Added `DJANGO_FAST_UTILS` settings. 11 | - Added `LogoutJWTApiView`. 12 | 13 | ## 2.0.2 14 | 15 | - Changed imports. 16 | 17 | ## 2.0.1 18 | 19 | - Added missing requirement. 20 | 21 | ## 2.0.0 22 | 23 | - Added `LoginJWTApiView` allowing the JWT Token being protected via `httpOnly=true` cookie 24 | and refreshing the token via `middleware`. [Docs here](./views/auth.md). 25 | 26 | ## 1.0.3 27 | 28 | - Fixed typo in `PrefetchRelatedMixin`. 29 | 30 | ## 1.0.2 31 | 32 | - Add `SelectRelatedMixin` and `PrefetchRelatedMixin` for generic views. 33 | 34 | ## 1.0.1 35 | 36 | - Fix `django-guardian` dependency. 37 | 38 | ## 1.0.0 39 | 40 | - Initial release of `django-fast-utils`. 41 | -------------------------------------------------------------------------------- /docs/utils/audit.md: -------------------------------------------------------------------------------- 1 | # Audit 2 | 3 | Mixins that injects fields into django models allowing the audit trailing. 4 | 5 | ## GeneralDateTimeModel 6 | 7 | Adds only `created_at` and `updated_at` 8 | 9 | ### How to use 10 | 11 | ```python 12 | from django.db import models 13 | from django_fast_utils.utils.audit import GeneralDateTimeModel 14 | 15 | 16 | class MyModel(GeneralDateTimeModel): 17 | ... 18 | 19 | ``` 20 | 21 | ## IndexGeneralDateTimeModel 22 | 23 | Adds only `created_at` as index and `updated_at` as index. 24 | 25 | ### How to use 26 | 27 | ```python 28 | from django.db import models 29 | from django_fast_utils.utils.audit import IndexGeneralDateTimeModel 30 | 31 | 32 | class MyModel(IndexGeneralDateTimeModel): 33 | ... 34 | 35 | ``` 36 | 37 | ## TimeStampedModel 38 | 39 | Adds `created_at`, `updated_at`, `created_by` and `updated_by` to the model and `created_by` and `updated_by` 40 | **are mandatory**. 41 | 42 | `created_by` and `updated_by` are FK to the `settings.AUTH_USER_MODEL`. 43 | 44 | ### How to use 45 | 46 | ```python 47 | from django.db import models 48 | from django_fast_utils.utils.audit import TimeStampedModel 49 | 50 | 51 | class MyModel(TimeStampedModel): 52 | ... 53 | 54 | ``` 55 | 56 | ## GeneralTimeStampedModel 57 | 58 | Adds `created_at`, `updated_at`, `created_by` and `updated_by` to the model and `created_by` and `updated_by` 59 | are **not mandatory**. 60 | 61 | `created_by` and `updated_by` are FK to the `settings.AUTH_USER_MODEL`. 62 | 63 | ### How to use 64 | 65 | ```python 66 | from django.db import models 67 | from django_fast_utils.utils.audit import GeneralTimeStampedModel 68 | 69 | 70 | class MyModel(GeneralTimeStampedModel): 71 | ... 72 | 73 | ``` 74 | 75 | ## IndexedGeneralTimeStampedModel 76 | 77 | Adds `created_at`, `updated_at`, `created_by` and `updated_by` to the model and `created_by` and `updated_by` 78 | are **not mandatory**. 79 | 80 | `created_by` and `updated_by` are FK to the `settings.AUTH_USER_MODEL`. 81 | 82 | ### How to use 83 | 84 | ```python 85 | from django.db import models 86 | from django_fast_utils.utils.audit import IndexedGeneralTimeStampedModel 87 | 88 | 89 | class MyModel(IndexedGeneralTimeStampedModel): 90 | ... 91 | 92 | ``` 93 | -------------------------------------------------------------------------------- /docs/views/auth.md: -------------------------------------------------------------------------------- 1 | # Authentication with JWT 2 | 3 | JWT (JSON Web Token) is widely used for authentication but with that also brings certain 4 | level of concerns such as where to store the access and refresh tokens. 5 | 6 | There are multiple ways: 7 | 8 | 1. Local Storage - Prompt for XSS attacks. 9 | 2. Session Storage - Deplects the UX of a user as it needs to login on each tab session. 10 | 3. Cookie - Limited but with specific options and safer using `httpOnly = True`. 11 | 12 | Django Fast Utils covers the Cookies. 13 | 14 | ## Cookie httpOnly 15 | 16 | When a cookie is set to httpOnly true, JavaScript cannot read and/or access those values 17 | making the refresh and access tokens safer in the browser and reducing the attacks. 18 | 19 | ## Installation 20 | 21 | Django Fast Utils uses [Django Rest Framework SimpleJWT](https://django-rest-framework-simplejwt.readthedocs.io/en/latest/getting_started.html) 22 | 23 | ### Settings 24 | 25 | 1. Add `rest_framework_simplejwt` and `rest_framework_simple_jwt.token_blacklist` to your `settings.py` 26 | 27 | ```python 28 | 29 | INSTALLED_APPS = [ 30 | ... 31 | 'rest_framework_simplejwt', 32 | 'rest_framework_simplejwt.token_blacklist', 33 | ... 34 | ] 35 | 36 | ``` 37 | 38 | 2. Add the `django_fast_utils.auth.middleware.JWTRefreshRequestCookies' to `MIDDLEWARE`. 39 | 40 | ```python 41 | 42 | MIDDLEWARE = [ 43 | ... 44 | 'django_fast_utils.auth.middleware.JWTRefreshRequestCookies', 45 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 46 | ... 47 | ] 48 | 49 | ``` 50 | 51 | 3. Update your `REST_FRAMEWORK` settings. 52 | 53 | ```python 54 | REST_FRAMEWORK = { 55 | # Use Django's standard `django.contrib.auth` permissions, 56 | # or allow read-only access for unauthenticated users. 57 | "DEFAULT_AUTHENTICATION_CLASSES": ( 58 | ... 59 | ... 60 | 'django_fast_utils.auth.backends.JWTCustomAuthentication', 61 | ), 62 | } 63 | ``` 64 | 65 | 4. Update the `SIMPLE_JWT` to have `django_fast_utils` settings for the authentication. 66 | 67 | ```python 68 | 69 | SIMPLE_JWT = { 70 | ... 71 | # JWT FOR HTTP ONLY COOKIE SETTINGS 72 | 'AUTH_COOKIE': 'access', # Cookie name. Enables cookies if value is set. 73 | 'AUTH_COOKIE_DOMAIN': None, # A string like "example.com", or None for standard domain cookie. 74 | 'AUTH_COOKIE_SECURE': True, # Whether the auth cookies should be secure (https:// only). 75 | 'AUTH_COOKIE_HTTP_ONLY' : True, # Http only cookie flag. It's not fetch by JS. 76 | 'AUTH_COOKIE_PATH': '/', # The path of the auth cookie. 77 | 'AUTH_COOKIE_SAMESITE': 'Lax', # Whether to set the flag restricting cookie leaks on cross-site requests. This can be 'Lax', 'Strict', or None to disable the flag. 78 | 79 | 'REFRESH_COOKIE': 'refresh', # Cookie name. Enables cookies if value is set. 80 | 'REFRESH_COOKIE_DOMAIN': None, # A string like "example.com", or None for standard domain cookie. 81 | 'REFRESH_COOKIE_SECURE': True, # Whether the auth cookies should be secure (https:// only). 82 | 'REFRESH_COOKIE_HTTP_ONLY' : True, # Http only cookie flag.It's not fetch by JS. 83 | 'REFRESH_COOKIE_PATH': '/', # The path of the auth cookie. 84 | 'REFRESH_COOKIE_SAMESITE': 'Lax', # Flag restricting cookie leaks on cross-site requests. This can be 'Lax', 'Strict', or None to disable the flag. 85 | } 86 | 87 | ``` 88 | 89 | 5. Add the `LoginJWTApiView` and `LogoutJWTApiView` to your urls. 90 | 91 | In your settings.py add `DJANGO_FAST_UTILS` setting: 92 | 93 | ```python 94 | DJANGO_FAST_UTILS = { 95 | 'LOGOUT_URL': ['/logout'] 96 | } 97 | ``` 98 | 99 | The `LOGOUT_URL` is expecting as list of possible `logout` urls used by the application. 100 | This will ensure the middleware doesn't execute logic for specific views such as `refresh` of 101 | tokens. 102 | 103 | **Default**: `['/logout']` 104 | 105 | ```python 106 | from django.urls import path 107 | from django_fast_utils.views.auth.views import LoginJWTApiView, LogoutJWTApiView 108 | 109 | urlpatterns = [ 110 | ... 111 | path('login', LoginJWTApiView.as_view(), name='login'), 112 | path('logout', LoginJWTApiView.as_view(), name='logout') 113 | ... 114 | ] 115 | ``` 116 | 117 | **That's it!**. You can now login into your application using `email` and `password` and you 118 | can see in your browser that the cookies are now set as httpOnly true. 119 | 120 | ## Refresh 121 | 122 | Djago Fast Utils middleware handles with the automatic `refresh` of the token based on the 123 | settings you added on the default `SIMPLE_JWT` settings. 124 | 125 | ```python 126 | 127 | SIMPLE_JWT = { 128 | 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5), 129 | 'REFRESH_TOKEN_LIFETIME': timedelta(days=200), 130 | ... 131 | } 132 | 133 | ``` 134 | -------------------------------------------------------------------------------- /docs/views/generics.md: -------------------------------------------------------------------------------- 1 | # Generics 2 | 3 | Mixins that can be used to optimise some of the querysets. 4 | 5 | ## SelectRelatedMixin 6 | 7 | Optimises the queryset on a SQL level. [More information](https://docs.djangoproject.com/en/4.0/ref/models/querysets/#select-related). 8 | 9 | ### How to use 10 | 11 | ```python 12 | from django.db import models 13 | from django_fast_utils.views.generics import SelectRelatedMixin 14 | from rest_framework.generics import ListAPIView 15 | 16 | 17 | class MyView(SelectRelatedMixin, ListAPIView): 18 | select_related = ['company', 'company__user'] 19 | 20 | ``` 21 | 22 | ## PrefetchRelatedMixin 23 | 24 | Optimises the queryset on a Pythonic level. [More information](https://docs.djangoproject.com/en/4.0/ref/models/querysets/#prefetch-related). 25 | 26 | ### How to use 27 | 28 | ```python 29 | from django.db import models 30 | from django_fast_utils.views.generics import PrefetchRelatedMixin 31 | from rest_framework.generics import ListAPIView 32 | 33 | 34 | class MyView(PrefetchRelatedMixin, ListAPIView): 35 | prefetch_related = ['company', 'company__user'] 36 | 37 | ``` 38 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Django Fast Utils 2 | site_description: The little library for your django projects with little effort. 3 | 4 | repo_name: tarsil/django-fast-utils 5 | repo_url: https://github.com/tarsil/django-fast-utils 6 | edit_uri: "" 7 | 8 | theme: 9 | name: material 10 | 11 | palette: 12 | # Toggle light mode 13 | - scheme: default 14 | primary: light green 15 | accent: teal 16 | toggle: 17 | icon: material/toggle-switch 18 | name: Switch to light mode 19 | 20 | # Toggle dark mode 21 | - scheme: slate 22 | primary: pink 23 | accent: blue 24 | toggle: 25 | icon: material/toggle-switch-off-outline 26 | name: Switch to dark mode 27 | 28 | markdown_extensions: 29 | - toc: 30 | permalink: true 31 | - pymdownx.highlight 32 | - pymdownx.superfences 33 | - pymdownx.emoji: 34 | emoji_index: !!python/name:materialx.emoji.twemoji 35 | emoji_generator: !!python/name:materialx.emoji.to_svg 36 | 37 | nav: 38 | - Introduction: "index.md" 39 | - "backends.md" 40 | - "exceptions.md" 41 | - "fields.md" 42 | - "helpers.md" 43 | - "paginator.md" 44 | - "permissions.md" 45 | - Auth: 46 | - Generics: "auth/generics.md" 47 | - Views: 48 | - Generics: "views/generics.md" 49 | - JWT Auth: "views/auth.md" 50 | - Database: 51 | - Fields: "db/fields.md" 52 | - Utils: 53 | - Audit: "utils/audit.md" 54 | - Releases: "releases.md" 55 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django>=3.2.13 2 | django-guardian==2.4.0 3 | djangorestframework==3.13.1 4 | djangorestframework_simplejwt==5.2.0 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import re 4 | import shutil 5 | import sys 6 | from io import open 7 | 8 | from setuptools import find_packages, setup 9 | 10 | CURRENT_PYTHON = sys.version_info[:2] 11 | REQUIRED_PYTHON = (3, 7) 12 | 13 | # This check and everything above must remain compatible with Python 2.7. 14 | if CURRENT_PYTHON < REQUIRED_PYTHON: 15 | sys.stderr.write(""" 16 | ========================== 17 | Unsupported Python version 18 | ========================== 19 | This version of Django Fast Utils requires Python {}.{}, but you're trying 20 | to install it on Python {}.{}. 21 | This may be because you are using a version of pip that doesn't 22 | understand the python_requires classifier. Make sure you 23 | have pip >= 9.0 and setuptools >= 24.2, then try again: 24 | $ python -m pip install --upgrade pip setuptools 25 | $ python -m pip install django_fast_utils 26 | This will install the latest version of Django Fast Utils which works on 27 | your version of Python. If you can't upgrade your pip (or Python), request 28 | an older version of Django Fast Utils. 29 | """.format(*(REQUIRED_PYTHON + CURRENT_PYTHON))) 30 | sys.exit(1) 31 | 32 | 33 | def read(f): 34 | return open(f, 'r', encoding='utf-8').read() 35 | 36 | 37 | def get_version(package): 38 | """ 39 | Return package version as listed in `__version__` in `init.py`. 40 | """ 41 | init_py = open(os.path.join(package, '__init__.py')).read() 42 | return re.search("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1) 43 | 44 | 45 | version = get_version('django_fast_utils') 46 | 47 | setup( 48 | name='django_fast_utils', 49 | version=version, 50 | url=' https://tarsil.github.io/django-fast-utils/', 51 | license='MIT', 52 | description='Utils for Django with little effort.', 53 | long_description=read('README.md'), 54 | long_description_content_type='text/markdown', 55 | author='Tiago Silva', 56 | author_email='tiago.arasilva@gmail.com', 57 | packages=find_packages(exclude=['tests*']), 58 | include_package_data=True, 59 | install_requires=["django>=2.2", "pytz", "django-guardian>=2.4.0", "djangorestframework_simplejwt>=5.2.0"], 60 | python_requires=">=3.7", 61 | zip_safe=False, 62 | classifiers=[ 63 | 'Development Status :: 5 - Production/Stable', 64 | 'Environment :: Web Environment', 65 | 'Framework :: Django', 66 | 'Framework :: Django :: 2.2', 67 | 'Framework :: Django :: 3.0', 68 | 'Framework :: Django :: 3.1', 69 | 'Framework :: Django :: 3.2', 70 | 'Framework :: Django :: 4.0', 71 | 'Intended Audience :: Developers', 72 | 'License :: OSI Approved :: BSD License', 73 | 'Operating System :: OS Independent', 74 | 'Programming Language :: Python', 75 | 'Programming Language :: Python :: 3', 76 | 'Programming Language :: Python :: 3.6', 77 | 'Programming Language :: Python :: 3.7', 78 | 'Programming Language :: Python :: 3.8', 79 | 'Programming Language :: Python :: 3.9', 80 | 'Programming Language :: Python :: 3.10', 81 | 'Programming Language :: Python :: 3 :: Only', 82 | 'Topic :: Internet :: WWW/HTTP', 83 | ], 84 | ) 85 | --------------------------------------------------------------------------------