From fc8d2bb57b703e0b4a7e50658fdb5ed53bbfbf61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?I=C3=B1igo=20R=2E?= Date: Mon, 20 May 2024 19:10:57 +0200 Subject: [PATCH] Initial Commit --- .gitignore | 228 +++++++++++++++++++++++++++++ .gitlab-ci.yml | 14 ++ README.md | 26 ++++ enhanced_drf_jsonapi/__init__.py | 0 enhanced_drf_jsonapi/api.py | 88 +++++++++++ enhanced_drf_jsonapi/pagination.py | 37 +++++ setup.py | 32 ++++ 7 files changed, 425 insertions(+) create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 README.md create mode 100644 enhanced_drf_jsonapi/__init__.py create mode 100644 enhanced_drf_jsonapi/api.py create mode 100644 enhanced_drf_jsonapi/pagination.py create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..efaaba7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,228 @@ +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Linux template +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### Python template +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..a5a1114 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,14 @@ +image: python:3.8 + +stages: + - publish + +pack: + stage: publish + script: + - pip install twine + - python setup.py sdist bdist_wheel + - python -m twine upload -u gitlab-ci-token -p $CI_JOB_TOKEN --repository-url $CI_API_V4_URL/projects/$CI_PROJECT_ID/packages/pypi dist/* + + only: + - tags diff --git a/README.md b/README.md new file mode 100644 index 0000000..8281a07 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# Enhanced Django Rest Framework JSON Api + +This is a library aimed to fix some classes in Django Rest Framework and Django Rest Framework JSON Api libraries. + +## Contents + +### enhanced_drf_jsonapi module + +#### API +- ##### class PreloadIncludesMixin + Overwrites the method get_queryset(self, *args, **kwarg) +- ##### class ReasonableModelViewSet + Overwrites the attribute http_method_names + +- ##### class ReasonableModelSerializer + Overwrites the method get_field_names(self, declared_fields, info) + +#### PAGINATION +- ##### class NgxJsonApiPageNumberPagination + Overwrites the method get_paginated_response(self, data) + +## Build the library +In root directory, run `python setup.py bdist_wheel`. This will create a wheel file in `dist` folder. + +## Install +Run this command in the desired python environment `pip install path/to/wheelfile.whl`. diff --git a/enhanced_drf_jsonapi/__init__.py b/enhanced_drf_jsonapi/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/enhanced_drf_jsonapi/api.py b/enhanced_drf_jsonapi/api.py new file mode 100644 index 0000000..cbf965e --- /dev/null +++ b/enhanced_drf_jsonapi/api.py @@ -0,0 +1,88 @@ +from rest_framework import viewsets +from rest_framework_json_api import serializers +from rest_framework_json_api.utils import get_included_resources +from rest_framework_json_api.views import AutoPrefetchMixin, RelatedMixin + +basic_filter = ('exact', 'isnull') +text_filter = ('exact', 'contains', 'iexact', 'icontains', 'startswith', 'istartswith', 'endswith', 'iendswith') +date_filter = ('exact', 'gte', 'lte') +int_filter = ('exact', 'gte', 'lte') + + +class PreloadIncludesMixin(object): + """ + This mixin provides a helper attributes to select or prefetch related models + based on the include specified in the URL. + + __all__ can be used to specify a prefetch which should be done regardless of the include + + + .. code:: python + + # When MyViewSet is called with ?include=author it will prefetch author and authorbio + class MyViewSet(viewsets.ModelViewSet): + queryset = Book.objects.all() + prefetch_for_includes = { + '__all__': [], + 'category.section': ['category'] + } + select_for_includes = { + '__all__': [], + 'author': ['author', 'author__authorbio'], + } + """ + + def get_select_related(self, include): + return getattr(self, 'select_for_includes', {}).get(include, None) + + def get_prefetch_related(self, include): + return getattr(self, 'prefetch_for_includes', {}).get(include, None) + + def get_queryset(self, *args, **kwargs): + qs = super(PreloadIncludesMixin, self).get_queryset(*args, **kwargs) + + included_resources = get_included_resources(self.request) + for included in included_resources + ['__all__']: + + select_related = self.get_select_related(included) + if select_related is not None: + qs = qs.select_related(*select_related) + + prefetch_related = self.get_prefetch_related(included) + if prefetch_related is not None: + if not isinstance(prefetch_related, list): + qs = qs.prefetch_related(*prefetch_related(self)) + else: + qs = qs.prefetch_related(*prefetch_related) + + return qs + + +class ReasonableModelViewSet(AutoPrefetchMixin, + PreloadIncludesMixin, + RelatedMixin, + viewsets.ModelViewSet): + http_method_names = ['get', 'post', 'patch', 'delete', 'head', 'options'] + + +class ReasonableModelSerializer(serializers.ModelSerializer): + """ + Reusable class to extend DJA ModelSerializer and make it more JSONAPI compatible + """ + + def get_field_names(self, declared_fields, info): + method = self.context['request'].method + + included_rel_fields = [] + if method == 'GET': + included_rel_fields = [qp for qp in ( + self.context['request'].query_params['include'].split(',') + if 'include' in self.context['request'].query_params else [] + ) if qp in (self.included_serializers if hasattr(self, 'included_serializers') else [])] + elif method == 'POST' or method == 'PATCH': + included_rel_fields = [irel for irel in ( + self.context['request'].data.keys() + ) if irel in (self.included_serializers if hasattr(self, 'included_serializers') else [])] + + fields = super().get_field_names(declared_fields, info) + return fields + included_rel_fields \ No newline at end of file diff --git a/enhanced_drf_jsonapi/pagination.py b/enhanced_drf_jsonapi/pagination.py new file mode 100644 index 0000000..2e87bee --- /dev/null +++ b/enhanced_drf_jsonapi/pagination.py @@ -0,0 +1,37 @@ +from collections import OrderedDict + +from rest_framework.response import Response +from rest_framework_json_api.pagination import JsonApiPageNumberPagination + + +class NgxJsonApiPageNumberPagination(JsonApiPageNumberPagination): + def get_paginated_response(self, data): + next = None + previous = None + + if self.page.has_next(): + next = self.page.next_page_number() + if self.page.has_previous(): + previous = self.page.previous_page_number() + + return Response( + { + "results": data, + "meta": OrderedDict( + [ + ("page", self.page.number), + ("pages", self.page.paginator.num_pages), + ("total_resources", self.page.paginator.count), + ("resources_per_page", self.page.paginator.per_page), + ] + ), + "links": OrderedDict( + [ + ("first", self.build_link(1)), + ("last", self.build_link(self.page.paginator.num_pages)), + ("next", self.build_link(next)), + ("prev", self.build_link(previous)), + ] + ), + } + ) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..df19dd6 --- /dev/null +++ b/setup.py @@ -0,0 +1,32 @@ +from setuptools import setup, find_packages +import pathlib + +HERE = pathlib.Path(__file__).parent + +VERSION = '1.0.4' +PACKAGE_NAME = 'enhanced_drf_jsonapi' + +LICENSE = 'MIT' +DESCRIPTION = 'Patch library for django rest framework json api' +LONG_DESCRIPTION = (HERE / "README.md").read_text(encoding='utf-8') +LONG_DESC_TYPE = "text/markdown" + + +INSTALL_REQUIRES = [ + 'djangorestframework-jsonapi~=6.0.0' + ] + +setup( + name=PACKAGE_NAME, + version=VERSION, + description=DESCRIPTION, + long_description=LONG_DESCRIPTION, + long_description_content_type=LONG_DESC_TYPE, + author=AUTHOR, + author_email=AUTHOR_EMAIL, + url=URL, + install_requires=INSTALL_REQUIRES, + license=LICENSE, + packages=find_packages(), + include_package_data=True +)