├── .gitignore ├── LICENSE ├── README.md ├── inclusive_django_range_fields ├── __init__.py ├── drf │ ├── __init__.py │ └── fields.py ├── fields.py ├── forms.py ├── ranges.py └── settings.py ├── pyproject.toml └── tests ├── __init__.py └── test_inclusive_django_range_fields.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | .idea/ 132 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Hipo 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 | # Inclusive Django Range Fields 2 | 3 | ![Inclusive](https://media.giphy.com/media/xUOwGdD7RGT4CTnUaY/giphy.gif "Inclusive") 4 | 5 | The default bound of Django range fields is `[)`. This package follows default bounds as `[]`. 6 | 7 | ## How to use? 8 | 9 | ```sh 10 | pip install inclusive-django-range-fields 11 | ``` 12 | 13 | ### Django 14 | 15 | ```python 16 | # models.py 17 | 18 | from django.db import models 19 | from inclusive_django_range_fields import InclusiveIntegerRangeField 20 | 21 | class AdCampaign(models.Model): 22 | age_target = InclusiveIntegerRangeField() 23 | ``` 24 | 25 | ``` 26 | >> AdCampaign.objects.first().age_target 27 | NumericRange(18, 30, '[]') 28 | ``` 29 | ### Django Rest Framework 30 | 31 | ```python 32 | # serializers.py 33 | 34 | from rest_framework import serializers 35 | from inclusive_django_range_fields.drf import InclusiveIntegerRangeField 36 | 37 | class AdCampaignSerializer(serializers.ModelSerializer): 38 | age_target = InclusiveIntegerRangeField() 39 | 40 | class Meta: 41 | model = AdCampaign 42 | fields = ( 43 | "id", 44 | "age_target", 45 | ) 46 | ``` 47 | 48 | ```json 49 | { 50 | "id": 1993, 51 | "age_target": { 52 | "lower": 18, 53 | "upper": 30 54 | } 55 | } 56 | ``` 57 | 58 | ## Reference 59 | 60 | ### Model Fields 61 | 62 | - `inclusive_django_range_fields.InclusiveIntegerRangeField` 63 | - `inclusive_django_range_fields.InclusiveBigIntegerRangeField` 64 | - `inclusive_django_range_fields.InclusiveDateRangeField` 65 | 66 | ### Ranges 67 | 68 | - `inclusive_django_range_fields.InclusiveNumericRange` 69 | - `inclusive_django_range_fields.InclusiveDateRange` 70 | - `inclusive_django_range_fields.InclusiveDateTimeTZRange` 71 | 72 | 73 | ### Django Rest Framework Serializers 74 | 75 | - `inclusive_django_range_fields.drf.InclusiveIntegerRangeField` 76 | - `inclusive_django_range_fields.drf.InclusiveDateRangeField` 77 | 78 | 79 | ### Form Fields 80 | 81 | - `inclusive_django_range_fields.InclusiveIntegerRangeFormField` 82 | - `inclusive_django_range_fields.InclusiveDateRangeFormField` 83 | 84 | 85 | ## PyPI 86 | https://pypi.org/project/inclusive-django-range-fields/ 87 | -------------------------------------------------------------------------------- /inclusive_django_range_fields/__init__.py: -------------------------------------------------------------------------------- 1 | from inclusive_django_range_fields.fields import InclusiveIntegerRangeField, InclusiveBigIntegerRangeField, \ 2 | InclusiveDateRangeField 3 | from inclusive_django_range_fields.forms import InclusiveIntegerRangeFormField, InclusiveDateRangeFormField 4 | from inclusive_django_range_fields.ranges import InclusiveNumericRange, InclusiveDateRange, InclusiveDateTimeTZRange 5 | -------------------------------------------------------------------------------- /inclusive_django_range_fields/drf/__init__.py: -------------------------------------------------------------------------------- 1 | from inclusive_django_range_fields.drf.fields import InclusiveIntegerRangeField, InclusiveDateRangeField 2 | -------------------------------------------------------------------------------- /inclusive_django_range_fields/drf/fields.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from rest_framework.exceptions import ValidationError 3 | 4 | from inclusive_django_range_fields import InclusiveNumericRange, InclusiveDateRange 5 | 6 | 7 | def _validate_data_shape(data): 8 | if not data.get("lower"): 9 | raise ValidationError("lower is required") 10 | elif not data.get("upper"): 11 | raise ValidationError("upper is required") 12 | 13 | 14 | class InclusiveIntegerRangeField(serializers.Field): 15 | 16 | def to_representation(self, value): 17 | return { 18 | "lower": value.lower, 19 | "upper": value.upper, 20 | } 21 | 22 | def to_internal_value(self, data): 23 | _validate_data_shape(data=data) 24 | 25 | lower = serializers.IntegerField().to_internal_value(data["lower"]) 26 | upper = serializers.IntegerField().to_internal_value(data["upper"]) 27 | 28 | return InclusiveNumericRange(lower=lower, upper=upper) 29 | 30 | 31 | class InclusiveDateRangeField(serializers.Field): 32 | 33 | def to_representation(self, value): 34 | return { 35 | "lower": value.lower, 36 | "upper": value.upper, 37 | } 38 | 39 | def to_internal_value(self, data): 40 | _validate_data_shape(data=data) 41 | 42 | lower = serializers.DateField().to_internal_value(data["lower"]) 43 | upper = serializers.DateField().to_internal_value(data["upper"]) 44 | 45 | return InclusiveDateRange(lower=lower, upper=upper) 46 | -------------------------------------------------------------------------------- /inclusive_django_range_fields/fields.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from django.contrib.postgres.fields import RangeField, IntegerRangeField, BigIntegerRangeField, DateRangeField 4 | 5 | from inclusive_django_range_fields.forms import InclusiveIntegerRangeFormField, InclusiveDateRangeFormField 6 | 7 | 8 | class BaseInclusiveRangeField(RangeField): 9 | form_field = None 10 | precision = None 11 | 12 | def from_db_value(self, value, expression, connection): 13 | if value is not None: 14 | if not value.upper_inf and not value.upper_inc and value._bounds != "[]": 15 | value._upper -= self.precision 16 | value._bounds = "[]" 17 | 18 | return value 19 | 20 | 21 | class InclusiveIntegerRangeField(BaseInclusiveRangeField, IntegerRangeField): 22 | form_field = InclusiveIntegerRangeFormField 23 | precision = 1 24 | 25 | 26 | class InclusiveBigIntegerRangeField(BaseInclusiveRangeField, BigIntegerRangeField): 27 | form_field = InclusiveIntegerRangeFormField 28 | precision = 1 29 | 30 | 31 | class InclusiveDateRangeField(BaseInclusiveRangeField, DateRangeField): 32 | form_field = InclusiveDateRangeFormField 33 | precision = timedelta(days=1) 34 | -------------------------------------------------------------------------------- /inclusive_django_range_fields/forms.py: -------------------------------------------------------------------------------- 1 | from django.contrib.postgres.forms import BaseRangeField, IntegerRangeField, DateRangeField 2 | 3 | 4 | class BaseInclusiveRangeFormField(BaseRangeField): 5 | 6 | def compress(self, values): 7 | values = super().compress(values) 8 | values._bounds = "[]" 9 | return values 10 | 11 | 12 | class InclusiveIntegerRangeFormField(BaseInclusiveRangeFormField, IntegerRangeField): 13 | pass 14 | 15 | 16 | class InclusiveDateRangeFormField(BaseInclusiveRangeFormField, DateRangeField): 17 | pass 18 | -------------------------------------------------------------------------------- /inclusive_django_range_fields/ranges.py: -------------------------------------------------------------------------------- 1 | from psycopg2.extras import NumericRange, DateRange, DateTimeTZRange 2 | 3 | 4 | class InclusiveNumericRange(NumericRange): 5 | 6 | def __init__(self, lower=None, upper=None, bounds='[]', empty=False): 7 | super().__init__(lower, upper, bounds, empty) 8 | 9 | 10 | class InclusiveDateRange(DateRange): 11 | 12 | def __init__(self, lower=None, upper=None, bounds='[]', empty=False): 13 | super().__init__(lower, upper, bounds, empty) 14 | 15 | 16 | class InclusiveDateTimeTZRange(DateTimeTZRange): 17 | 18 | def __init__(self, lower=None, upper=None, bounds='[]', empty=False): 19 | super().__init__(lower, upper, bounds, empty) -------------------------------------------------------------------------------- /inclusive_django_range_fields/settings.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hipo/inclusive-django-range-fields/f40d915fc8bfbfda5cba59fcabb1831fae486fd4/inclusive_django_range_fields/settings.py -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "inclusive_django_range_fields" 3 | version = "0.2.3" 4 | description = "Inclusive Django Range Fields which uses default bounds as '[]'" 5 | authors = ["Hipo "] 6 | license = "Apache-2.0" 7 | readme = "README.md" 8 | homepage = "https://github.com/hipo/inclusive-django-range-fields" 9 | repository = "https://github.com/hipo/inclusive-django-range-fields" 10 | keywords = ["python", "django", "django rest framework", "postgres", "range field"] 11 | classifiers = [ 12 | "Framework :: Django", 13 | "Operating System :: OS Independent", 14 | "License :: OSI Approved :: Apache Software License", 15 | 'Topic :: Internet :: WWW/HTTP', 16 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 17 | "Topic :: Software Development :: Libraries :: Python Modules", 18 | 'Programming Language :: Python', 19 | 'Programming Language :: Python :: 2', 20 | 'Programming Language :: Python :: 2.7', 21 | 'Programming Language :: Python :: 3', 22 | 'Programming Language :: Python :: 3.4', 23 | 'Programming Language :: Python :: 3.5', 24 | 'Programming Language :: Python :: 3.6', 25 | 'Programming Language :: Python :: 3.7', 26 | ] 27 | include = [ 28 | "LICENSE", 29 | ] 30 | 31 | [tool.poetry.dependencies] 32 | python = ">=2.7, !=3.0, !=3.1, !=3.2, !=3.3, !=3.4, <4" 33 | Django = { version = ">=1.8"} 34 | djangorestframework = { version = ">=3.0"} 35 | 36 | [tool.poetry.dev-dependencies] 37 | pytest = "^3.0" 38 | 39 | [build-system] 40 | requires = ["poetry>=0.12"] 41 | build-backend = "poetry.masonry.api" 42 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hipo/inclusive-django-range-fields/f40d915fc8bfbfda5cba59fcabb1831fae486fd4/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_inclusive_django_range_fields.py: -------------------------------------------------------------------------------- 1 | def test_version(): 2 | assert True 3 | --------------------------------------------------------------------------------