pax_global_header00006660000000000000000000000064136500171670014520gustar00rootroot0000000000000052 comment=b5c2a3fb99df622c5562372f0e64c64c0690badb django-measurement-3.2.3/000077500000000000000000000000001365001716700153125ustar00rootroot00000000000000django-measurement-3.2.3/.github/000077500000000000000000000000001365001716700166525ustar00rootroot00000000000000django-measurement-3.2.3/.github/workflows/000077500000000000000000000000001365001716700207075ustar00rootroot00000000000000django-measurement-3.2.3/.github/workflows/ci.yml000066400000000000000000000042051365001716700220260ustar00rootroot00000000000000name: CI on: pull_request jobs: isort: runs-on: ubuntu-latest steps: - uses: actions/setup-python@v1 - uses: actions/checkout@v2 - run: python -m pip install isort - run: isort --check-only --diff --recursive . flake8: runs-on: ubuntu-latest steps: - uses: actions/setup-python@v1 - uses: actions/checkout@v2 - run: python -m pip install flake8 - run: flake8 . pydocstyle: runs-on: ubuntu-latest steps: - uses: actions/setup-python@v1 - uses: actions/checkout@v2 - run: python -m pip install pydocstyle - run: pydocstyle . black: runs-on: ubuntu-latest steps: - uses: actions/setup-python@v1 - uses: actions/checkout@v2 - run: python -m pip install black - run: black --check --diff . docs: runs-on: ubuntu-latest steps: - uses: actions/setup-python@v1 - run: sudo apt-get install -y graphviz - uses: actions/checkout@v2 - run: git fetch --prune --unshallow - run: python setup.py develop - run: python setup.py build_sphinx -W dist: runs-on: ubuntu-latest steps: - uses: actions/setup-python@v1 - run: python -m pip install --upgrade pip setuptools wheel twine readme-renderer - uses: actions/checkout@v2 - run: python setup.py sdist bdist_wheel - run: python -m twine check dist/* pytest: needs: - isort - pydocstyle - flake8 - black strategy: matrix: os: - ubuntu-latest python-version: - 3.7 - 3.8 django-version: - "2.2" - "3.0" runs-on: ${{ matrix.os }} steps: - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 with: python-version: ${{ matrix.python-version }} - uses: actions/checkout@v2 - run: python -m pip install --upgrade setuptools wheel codecov - run: python -m pip install Django~=${{ matrix.django-version }} - run: python setup.py test - run: codecov env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} django-measurement-3.2.3/.github/workflows/release.yml000066400000000000000000000010351365001716700230510ustar00rootroot00000000000000name: Release on: [release] jobs: PyPi: runs-on: ubuntu-latest steps: - uses: actions/setup-python@v1 - uses: actions/checkout@v2 - name: Install dependencies run: python -m pip install --upgrade pip setuptools wheel twine - name: Build dist packages run: python setup.py sdist bdist_wheel - name: Upload packages run: python -m twine upload dist/* env: TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }} TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} django-measurement-3.2.3/.gitignore000066400000000000000000000022041365001716700173000ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg # 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/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # dotenv .env # virtualenv .venv venv/ ENV/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/django-measurement-3.2.3/LICENSE000066400000000000000000000020721365001716700163200ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2014 Adam Coddington Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. django-measurement-3.2.3/MANIFEST.in000066400000000000000000000000421365001716700170440ustar00rootroot00000000000000exclude .* prune tests prune docs django-measurement-3.2.3/README.rst000066400000000000000000000046471365001716700170140ustar00rootroot00000000000000|version| |ci| |coverage| |license| Django Measurement ================== Easily use, manipulate, and store unit-aware measurement objects using Python and Django. `django.contrib.gis.measure `_ has these wonderful 'Distance' objects that can be used not only for storing a unit-aware distance measurement, but also for converting between different units and adding/subtracting these objects from one another. This module provides for a django model field and admin interface for storing any measurements provided by `python-measurement`_. Example use with a model: .. code-block:: python from django_measurement.models import MeasurementField from measurement.measures import Volume from django.db import models class BeerConsumptionLogEntry(models.Model): name = models.CharField(max_length=255) volume = MeasurementField(measurement=Volume) def __str__(self): return f"{self.name} of {self.volume}" entry = BeerConsumptionLogEntry() entry.name = "Bear Republic Racer 5" entry.volume = Volume(us_pint=1) entry.save() These stored measurement objects can be used in all of the usual ways supported by `python-measurement`_ too: .. code-block:: python >>> from measurement.measures import Mass >>> weight_1 = Mass(lb=125) >>> weight_2 = Mass(kg=40) >>> added_together = weight_1 + weight_2 >>> added_together Mass(lb=213.18497680735112) >>> added_together.kg # Maybe I actually need this value in kg? 96.699 - Documentation for django-measurement is available via `Read the Docs`_. - Please post issues on GitHub_. .. _Read the Docs: https://django-measurement.readthedocs.io/ .. _GitHub: https://github.com/coddingtonbear/django-measurement/issues .. _python-measurement: https://github.com/coddingtonbear/python-measurement .. |version| image:: https://img.shields.io/pypi/v/django-measurement.svg :target: https://pypi.python.org/pypi/django-measurement .. |ci| image:: https://api.travis-ci.org/coddingtonbear/django-measurement.svg?branch=master :target: https://travis-ci.org/coddingtonbear/django-measurement .. |coverage| image:: https://codecov.io/gh/coddingtonbear/django-measurement/branch/master/graph/badge.svg :target: https://codecov.io/gh/coddingtonbear/django-measurement .. |license| image:: https://img.shields.io/badge/license-MIT-blue.svg :target: LICENSEdjango-measurement-3.2.3/django_measurement/000077500000000000000000000000001365001716700211615ustar00rootroot00000000000000django-measurement-3.2.3/django_measurement/__init__.py000066400000000000000000000000001365001716700232600ustar00rootroot00000000000000django-measurement-3.2.3/django_measurement/conf.py000066400000000000000000000005721365001716700224640ustar00rootroot00000000000000"""Settings for django-measurement.""" from appconf import AppConf from django.conf import settings __all__ = ("settings",) class DjangoMeasurementConf(AppConf): """Settings for django-measurement.""" BIDIMENSIONAL_SEPARATOR = "/" """ For measurement classes subclassing a BidimensionalMeasure, this . """ class Meta: prefix = "measurement" django-measurement-3.2.3/django_measurement/forms.py000077500000000000000000000111641365001716700226670ustar00rootroot00000000000000from itertools import product from django import forms from django.core.validators import MaxValueValidator, MinValueValidator from measurement.base import BidimensionalMeasure, MeasureBase from django_measurement import utils from django_measurement.conf import settings class MeasurementWidget(forms.MultiWidget): def __init__( self, attrs=None, float_widget=None, unit_choices_widget=None, unit_choices=None, *args, **kwargs ): self.unit_choices = unit_choices if not float_widget: float_widget = forms.TextInput(attrs=attrs) if not unit_choices_widget: unit_choices_widget = forms.Select(attrs=attrs, choices=unit_choices) widgets = (float_widget, unit_choices_widget) super(MeasurementWidget, self).__init__(widgets, attrs) def decompress(self, value): if value: choice_units = set([u for u, n in self.unit_choices]) unit = value.STANDARD_UNIT if unit not in choice_units: unit = choice_units.pop() magnitude = getattr(value, unit) return [magnitude, unit] return [None, None] class MeasurementField(forms.MultiValueField): def __init__( self, measurement, max_value=None, min_value=None, unit_choices=None, validators=None, bidimensional_separator=settings.MEASUREMENT_BIDIMENSIONAL_SEPARATOR, *args, **kwargs ): if not issubclass(measurement, (MeasureBase, BidimensionalMeasure)): raise ValueError("%s must be a subclass of MeasureBase" % measurement) self.measurement_class = measurement if not unit_choices: if issubclass(measurement, BidimensionalMeasure): assert isinstance(bidimensional_separator, str), ( "Supplied bidimensional_separator for %s must be of string/unicode type;" " Instead got type %s" % (measurement, str(type(bidimensional_separator)),) ) unit_choices = tuple( ( ( "{0}__{1}".format(primary, reference), "{0}{1}{2}".format( getattr( measurement.PRIMARY_DIMENSION, "LABELS", {} ).get(primary, primary), bidimensional_separator, getattr( measurement.REFERENCE_DIMENSION, "LABELS", {} ).get(reference, reference), ), ) for primary, reference in product( measurement.PRIMARY_DIMENSION.get_units(), measurement.REFERENCE_DIMENSION.get_units(), ) ) ) else: unit_choices = tuple( ( (u, getattr(measurement, "LABELS", {}).get(u, u)) for u in measurement.get_units() ) ) if validators is None: validators = [] if min_value is not None: if not isinstance(min_value, MeasureBase): msg = '"min_value" must be a measure, got %s' % type(min_value) raise ValueError(msg) validators += [MinValueValidator(min_value)] if max_value is not None: if not isinstance(max_value, MeasureBase): msg = '"max_value" must be a measure, got %s' % type(max_value) raise ValueError(msg) validators += [MaxValueValidator(max_value)] float_field = forms.FloatField(*args, **kwargs) choice_field = forms.ChoiceField(choices=unit_choices) defaults = { "widget": MeasurementWidget( float_widget=float_field.widget, unit_choices_widget=choice_field.widget, unit_choices=unit_choices, ), } defaults.update(kwargs) fields = (float_field, choice_field) super(MeasurementField, self).__init__( fields, validators=validators, *args, **defaults ) def compress(self, data_list): if not data_list: return None value, unit = data_list if value in self.empty_values: return None return utils.get_measurement(self.measurement_class, value, unit) django-measurement-3.2.3/django_measurement/models.py000066400000000000000000000107271365001716700230250ustar00rootroot00000000000000import logging import warnings from django.db.models import FloatField from django.utils.translation import ugettext_lazy as _ from measurement import measures from measurement.base import BidimensionalMeasure, MeasureBase from . import forms from .utils import get_measurement logger = logging.getLogger("django_measurement") class MeasurementField(FloatField): description = "Easily store, retrieve, and convert python measures." empty_strings_allowed = False MEASURE_BASES = ( BidimensionalMeasure, MeasureBase, ) default_error_messages = { "invalid_type": _( "'%(value)s' (%(type_given)s) value" " must be of type %(type_wanted)s." ), } def __init__( self, verbose_name=None, name=None, measurement=None, measurement_class=None, unit_choices=None, *args, **kwargs ): if not measurement and measurement_class is not None: warnings.warn( '"measurement_class" will be removed in version 4.0', DeprecationWarning ) measurement = getattr(measures, measurement_class) if not measurement: raise TypeError( "MeasurementField() takes a measurement" " keyword argument. None given." ) if not issubclass(measurement, self.MEASURE_BASES): raise TypeError( "MeasurementField() takes a measurement keyword argument." " It has to be a valid MeasureBase subclass." ) self.measurement = measurement self.widget_args = { "measurement": measurement, "unit_choices": unit_choices, } super(MeasurementField, self).__init__(verbose_name, name, *args, **kwargs) def deconstruct(self): name, path, args, kwargs = super(MeasurementField, self).deconstruct() kwargs["measurement"] = self.measurement return name, path, args, kwargs def get_prep_value(self, value): if value is None: return None elif isinstance(value, self.MEASURE_BASES): # sometimes we get sympy.core.numbers.Float, which the # database does not understand, so explicitely convert to # float return float(value.standard) else: return super(MeasurementField, self).get_prep_value(value) def get_default_unit(self): unit_choices = self.widget_args["unit_choices"] if unit_choices: return unit_choices[0][0] return self.measurement.STANDARD_UNIT def from_db_value(self, value, *args, **kwargs): if value is None: return None return get_measurement( measure=self.measurement, value=value, original_unit=self.get_default_unit(), ) def value_to_string(self, obj): value = self.value_from_object(obj) if not isinstance(value, self.MEASURE_BASES): return value return "%s:%s" % (value.value, value.unit) def deserialize_value_from_string(self, s: str): parts = s.split(":", 1) if len(parts) != 2: return None value, unit = float(parts[0]), parts[1] measure = get_measurement(self.measurement, value=value, unit=unit) return measure def to_python(self, value): if value is None: return value elif isinstance(value, self.MEASURE_BASES): return value elif isinstance(value, str): parsed = self.deserialize_value_from_string(value) if parsed is not None: return parsed value = super(MeasurementField, self).to_python(value) return_unit = self.get_default_unit() msg = ( 'You assigned a %s instead of %s to %s.%s.%s, unit was guessed to be "%s".' % ( type(value).__name__, str(self.measurement.__name__), self.model.__module__, self.model.__name__, self.name, return_unit, ) ) logger.warning(msg) return get_measurement(measure=self.measurement, value=value, unit=return_unit,) def formfield(self, **kwargs): defaults = {"form_class": forms.MeasurementField} defaults.update(kwargs) defaults.update(self.widget_args) return super(MeasurementField, self).formfield(**defaults) django-measurement-3.2.3/django_measurement/utils.py000066400000000000000000000005171365001716700226760ustar00rootroot00000000000000from measurement.base import BidimensionalMeasure def get_measurement(measure, value, unit=None, original_unit=None): unit = unit or measure.STANDARD_UNIT m = measure(**{unit: value}) if original_unit: m.unit = original_unit if isinstance(m, BidimensionalMeasure): m.reference.value = 1 return m django-measurement-3.2.3/docs/000077500000000000000000000000001365001716700162425ustar00rootroot00000000000000django-measurement-3.2.3/docs/conf.py000066400000000000000000000000251365001716700175360ustar00rootroot00000000000000master_doc = "index" django-measurement-3.2.3/docs/index.rst000066400000000000000000000051551365001716700201110ustar00rootroot00000000000000Django Measurement ================== |version| |ci| |coverage| |license| Easily use, manipulate, and store unit-aware measurement objects using Python and Django. Contents: --------- .. toctree:: :maxdepth: 2 :glob: topics/* Summary: -------- `django.contrib.gis.measure `_ has these wonderful 'Distance' objects that can be used not only for storing a unit-aware distance measurement, but also for converting between different units and adding/subtracting these objects from one another. This module provides for a django model field and admin interface for storing any measurements provided by `python-measurement`_. Example use with a model: .. code-block:: python from django_measurement.models import MeasurementField from measurement.measures import Volume from django.db import models class BeerConsumptionLogEntry(models.Model): name = models.CharField(max_length=255) volume = MeasurementField(measurement=Volume) def __unicode__(self): return u"%s of %s" % (self.name, self.volume) entry = BeerConsumptionLogEntry() entry.name = 'Bear Republic Racer 5' entry.volume = Volume(us_pint=1) entry.save() These stored measurement objects can be used in all of the usual ways supported by `python-measurement`_ too: .. code-block:: python >>> from measurement.measures import Weight >>> weight_1 = Weight(lb=125) >>> weight_2 = Weight(kg=40) >>> added_together = weight_1 + weight_2 >>> added_together Weight(lb=213.184976807) >>> added_together.kg # Maybe I actually need this value in kg? 96.699 - Documentation for django-measurement is available via `Read the Docs`_. - Please post issues on GitHub_. .. _Read the Docs: https://django-measurement.readthedocs.io/ .. _GitHub: https://github.com/coddingtonbear/django-measurement/issues .. _python-measurement: https://github.com/coddingtonbear/python-measurement .. |version| image:: https://img.shields.io/pypi/v/django-measurement.svg :target: https://pypi.python.org/pypi/django-measurement .. |ci| image:: https://api.travis-ci.org/coddingtonbear/django-measurement.svg?branch=master :target: https://travis-ci.org/coddingtonbear/django-measurement .. |coverage| image:: https://codecov.io/gh/coddingtonbear/django-measurement/branch/master/graph/badge.svg :target: https://codecov.io/gh/coddingtonbear/django-measurement .. |license| image:: https://img.shields.io/badge/license-MIT-blue.svg :target: LICENSE Indices and tables ------------------ * :ref:`genindex` * :ref:`modindex` * :ref:`search` django-measurement-3.2.3/docs/topics/000077500000000000000000000000001365001716700175435ustar00rootroot00000000000000django-measurement-3.2.3/docs/topics/forms.rst000077500000000000000000000033361365001716700214330ustar00rootroot00000000000000 Using Measurement Objects in Forms ================================== This is an example for a simple form field usage:: from django import forms from django_measurement.forms import MeasurementField class BeerForm(forms.Form): volume = MeasurementField(Volume) You can limit the units in the select field by using the 'unit_choices' keyword argument. To limit the value choices of the MeasurementField uses the regular 'choices' keyword argument:: class BeerForm(forms.Form): volume = MeasurementField( measurement=Volume, unit_choices=(("l","l"), ("oz","oz")), choices=((1.0, 'one'), (2.0, 'two')) ) If unicode symbols are needed in the labels for a MeasurementField, define a LABELS dictionary for your subclassed MeasureBase object:: # -*- coding: utf-8 -*- from sympy import S, Symbol class Temperature(MeasureBase): SU = Symbol('kelvin') STANDARD_UNIT = 'k' UNITS = { 'c': SU - S(273.15), 'f': (SU - S(273.15)) * S('9/5') + 32, 'k': 1.0 } LABELS = { 'c':u'°C', 'f':u'°F', 'k':u'°K', } For a `MeasurementField` that represents a `BidimensionalMeasure`, you can set the separator either in settings.py (`MEASUREMENT_BIDIMENSIONAL_SEPARATOR is '/'` by default, add setting to override for all BiDimensionalMeasure subclasses) or override for an individual field with the kwarg bidimensional_separator:: speed = MeasurementField( measurement=Speed, bidimensional_separator=' per ' ) # Rendered option labels will now be in the format "ft per s", "m per hr", etc django-measurement-3.2.3/docs/topics/installation.rst000066400000000000000000000005371365001716700230030ustar00rootroot00000000000000 Installation ============ You can either install from pip:: pip install django-measurement *or* checkout and install the source from the `github repository `_:: git clone https://github.com/coddingtonbear/django-measurement.git cd django-measurement python setup.py install django-measurement-3.2.3/docs/topics/measures.rst000066400000000000000000000002751365001716700221250ustar00rootroot00000000000000 Measures ======== See `python-measurement's documentation `_ for information about what measures are available. django-measurement-3.2.3/docs/topics/settings.rst000066400000000000000000000005511365001716700221360ustar00rootroot00000000000000 Settings ======== ``MEASUREMENT_BIDIMENSIONAL_SEPARATOR`` --------------------------------------- For any BidimensionalMeasure, what is placed between the primary and reference dimensions on rendered label MEASUREMENT_BIDIMENSIONAL_SEPARATOR = " per " Defaults to "/". Can be overriden as kwarg `bidimensional_separator` for a given MeasurementField. django-measurement-3.2.3/docs/topics/storing.rst000066400000000000000000000034321365001716700217640ustar00rootroot00000000000000 Storing Measurement Objects =========================== Suppose you were trying to cut back on drinking, and needed to store a log of how much beer you drink day-to-day; you might (naively) create a model like such:: from django_measurement.models import MeasurementField from measurement.measures import Volume from django.db import models class BeerConsumptionLogEntry(models.Model): name = models.CharField(max_length=255) volume = MeasurementField(measurement=Volume) def __str__(self): return '%s of %s' % (self.name, self.volume) and assume you had a pint of `Ninkasi's Total Domination `_; you'd add it to your log like so:: from measurement.measures import Volume beer = BeerConsumptionLogEntry() beer.name = 'Total Domination' beer.volume = Volume(us_pint=1) beer.save() print beer # '1 us_pint of Total Domination' Perhaps you next recklessly dove into your stash of terrible, but nostalgia-inducing Russian beer and had a half-liter of `Baltika's #9 `_; you'd add it to your log like so:: another_beer = BeerConsumptionLogEntry() another_beer.name = '#9' another_beer.volume = Volume(l=0.5) another_beer.save() print beer # '0.5 l of #9' Note that although the original unit specified is stored for display, that the unit is abstracted to the measure's standard unit for storage and comparison:: print beer.volume # '1 us_pint' print another_beer.volume # '0.5 l' print beer.volume > another_beer.volume # False How is this data stored? ------------------------ Since django-measurement v2.0 there value will be stored in a single float field. django-measurement-3.2.3/docs/topics/use.rst000066400000000000000000000010111365001716700210620ustar00rootroot00000000000000 Using Measurement Objects ========================= You can import any of the above measures from `measurement.measures` and use it for easily handling measurements like so:: from measurement.measures import Weight w = Weight(lb=135) # Represents 135lbs print w # '135.0 lb' print w.kg # '61.234919999999995' See `Python-measurement's documentation `_ for more information about interacting with measurements. django-measurement-3.2.3/setup.cfg000066400000000000000000000043361365001716700171410ustar00rootroot00000000000000[metadata] name = django-measurement author = Adam Coddington author_email = me@adamcoddington.net description = Convenient fields and classes for handling measurements long_description = file: README.rst long_description_content_type = text/x-rst url = https://github.com/coddingtonbear/django-measurement license = MIT license_file = LICENSE classifier = Development Status :: 5 - Production/Stable Intended Audience :: Developers License :: OSI Approved :: MIT License Operating System :: OS Independent Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Environment :: Web Environment Framework :: Django Framework :: Django :: 2.2 Framework :: Django :: 3.0 Topic :: Utilities Topic :: Scientific/Engineering Topic :: Scientific/Engineering :: Astronomy Topic :: Scientific/Engineering :: Atmospheric Science Topic :: Scientific/Engineering :: Chemistry Topic :: Scientific/Engineering :: GIS Topic :: Scientific/Engineering :: Mathematics Topic :: Scientific/Engineering :: Physics Topic :: Software Development :: Localization keywords = measurement django [options] packages = find: install_requires = django>=2.2 django-appconf>=1.0.2 measurement>=1.6,<4.0 setup_requires = setuptools_scm sphinx pytest-runner tests_require = pytest pytest-cov pytest-django [options.packages.find] exclude = tests [bdist_wheel] universal = 1 [build_sphinx] source-dir = docs build-dir = docs/_build [aliases] test = pytest [tool:pytest] norecursedirs=env docs .tox .eggs DJANGO_SETTINGS_MODULE=tests.settings addopts = --doctest-glob='*.rst' --doctest-modules --cov=django_measurement [coverage:report] show_missing = True [flake8] max-line-length=88 select = C,E,F,W,B,B950 ignore = E203, E501, W503 exclude = venv,.tox,.eggs [pydocstyle] add-ignore = D1 [isort] atomic = true line_length = 88 multi_line_output=3 include_trailing_comma=True force_grid_wrap=0 use_parentheses=True known_first_party = django_measurement, tests default_section=THIRDPARTY combine_as_imports = true django-measurement-3.2.3/setup.py000077500000000000000000000001531365001716700170260ustar00rootroot00000000000000#!/usr/bin/env python from setuptools import setup setup(name="django-measurement", use_scm_version=True) django-measurement-3.2.3/tests/000077500000000000000000000000001365001716700164545ustar00rootroot00000000000000django-measurement-3.2.3/tests/__init__.py000066400000000000000000000000001365001716700205530ustar00rootroot00000000000000django-measurement-3.2.3/tests/custom_measure_base.py000066400000000000000000000011641365001716700230550ustar00rootroot00000000000000from measurement.base import BidimensionalMeasure, MeasureBase from sympy import S, Symbol __all__ = ( "Temperature", "Time", "DegreePerTime", ) class Temperature(MeasureBase): SU = Symbol("kelvin") STANDARD_UNIT = "k" UNITS = {"c": SU - S(273.15), "f": (SU - S(273.15)) * S("9/5") + 32, "k": 1.0} LABELS = { "c": u"°C", "f": u"°F", "k": u"°K", } class Time(MeasureBase): UNITS = { "s": 3600.0, "h": 1.0, } SI_UNITS = ["s"] class DegreePerTime(BidimensionalMeasure): PRIMARY_DIMENSION = Temperature REFERENCE_DIMENSION = Time django-measurement-3.2.3/tests/forms.py000066400000000000000000000010541365001716700201540ustar00rootroot00000000000000from django import forms from django_measurement.forms import MeasurementField from tests.custom_measure_base import DegreePerTime, Temperature, Time from tests.models import MeasurementTestModel class MeasurementTestForm(forms.ModelForm): class Meta: model = MeasurementTestModel exclude = [] class LabelTestForm(forms.Form): simple = MeasurementField(Temperature) class SITestForm(forms.Form): simple = MeasurementField(Time) class BiDimensionalLabelTestForm(forms.Form): simple = MeasurementField(DegreePerTime) django-measurement-3.2.3/tests/models.py000066400000000000000000000052251365001716700203150ustar00rootroot00000000000000from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from measurement import measures from django_measurement.models import MeasurementField from tests.custom_measure_base import DegreePerTime, Temperature, Time class MeasurementTestModel(models.Model): measurement_distance = MeasurementField( measurement=measures.Distance, validators=[ MinValueValidator(measures.Distance(mi=1.0)), MaxValueValidator(measures.Distance(mi=3.0)), ], blank=True, null=True, ) measurement_distance_km = MeasurementField( measurement=measures.Distance, unit_choices=(("km", "km"),), validators=[ MinValueValidator(measures.Distance(km=1.0)), MaxValueValidator(measures.Distance(km=3.0)), ], blank=True, null=True, ) measurement_weight = MeasurementField( measurement=measures.Weight, validators=[ MinValueValidator(measures.Weight(kg=1.0)), MaxValueValidator(measures.Weight(kg=3.0)), ], blank=True, null=True, ) measurement_speed = MeasurementField( measurement=measures.Speed, validators=[ MinValueValidator(measures.Speed(mph=1.0)), MaxValueValidator(measures.Speed(mph=3.0)), ], blank=True, null=True, ) measurement_temperature = MeasurementField( measurement=measures.Temperature, validators=[ MinValueValidator(measures.Temperature(1.0)), MaxValueValidator(measures.Temperature(3.0)), ], blank=True, null=True, ) measurement_temperature2 = MeasurementField( measurement_class="Temperature", validators=[ MinValueValidator(measures.Temperature(1.0)), MaxValueValidator(measures.Temperature(3.0)), ], blank=True, null=True, ) measurement_speed_mph = MeasurementField( measurement=measures.Speed, unit_choices=(("mi__hr", "mph"),), validators=[ MinValueValidator(measures.Speed(mph=1.0)), MaxValueValidator(measures.Speed(mph=3.0)), ], blank=True, null=True, ) measurement_custom_degree_per_time = MeasurementField( measurement=DegreePerTime, blank=True, null=True, ) measurement_custom_temperature = MeasurementField( measurement=Temperature, blank=True, null=True, ) measurement_custom_time = MeasurementField(measurement=Time, blank=True, null=True,) def __str__(self): return self.measurement django-measurement-3.2.3/tests/settings.py000066400000000000000000000004411365001716700206650ustar00rootroot00000000000000DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}} INSTALLED_APPS = ( "django.contrib.auth", "django.contrib.contenttypes", "django_measurement", "tests", ) SITE_ID = 1 ROOT_URLCONF = "core.urls" SECRET_KEY = "foobar" USE_L10N = True django-measurement-3.2.3/tests/test_fields.py000066400000000000000000000231671365001716700213440ustar00rootroot00000000000000import pytest from django.core import serializers from django.core.exceptions import ValidationError from django.utils import module_loading from measurement import measures from measurement.measures import Distance from django_measurement.forms import MeasurementField from tests.custom_measure_base import DegreePerTime, Temperature, Time from tests.forms import ( BiDimensionalLabelTestForm, LabelTestForm, MeasurementTestForm, SITestForm, ) from tests.models import MeasurementTestModel pytestmark = [ pytest.mark.django_db, ] class TestMeasurementField: def test_storage_of_standard_measurement(self): measurement = measures.Weight(g=20) instance = MeasurementTestModel() instance.measurement_weight = measurement instance.save() retrieved = MeasurementTestModel.objects.get(pk=instance.pk) assert retrieved.measurement_weight == measurement def test_storage_of_temperature(self): measurement = measures.Temperature(c=20) instance = MeasurementTestModel() instance.measurement_temperature = measurement instance.save() retrieved = MeasurementTestModel.objects.get(pk=instance.pk) assert retrieved.measurement_temperature == measurement def test_storage_of_string_value(self): instance = MeasurementTestModel() instance.measurement_weight = "21.4" instance.save() retrieved = MeasurementTestModel.objects.get(pk=instance.pk) assert retrieved.measurement_weight == measures.Weight(g=21.4) def test_storage_of_float_value(self): instance = MeasurementTestModel() instance.measurement_weight = 21.4 instance.save() retrieved = MeasurementTestModel.objects.get(pk=instance.pk) assert retrieved.measurement_weight == measures.Weight(g=21.4) def test_storage_of_bidimensional_measurement(self): measurement = measures.Speed(mph=20) instance = MeasurementTestModel() instance.measurement_speed = measurement instance.save() retrieved = MeasurementTestModel.objects.get(pk=instance.pk) assert retrieved.measurement_speed == measurement def test_storage_and_retrieval_of_bidimensional_measurement(self): original_value = measures.Speed(mph=65) MeasurementTestModel.objects.create(measurement_speed=original_value,) retrieved = MeasurementTestModel.objects.get() new_value = retrieved.measurement_speed assert new_value == original_value assert type(new_value) == type(original_value) assert new_value.unit == original_value.STANDARD_UNIT def test_storage_and_retrieval_of_bidimensional_measurement_choice(self): original_value = measures.Speed(mph=65) MeasurementTestModel.objects.create(measurement_speed_mph=original_value,) retrieved = MeasurementTestModel.objects.get() new_value = retrieved.measurement_speed_mph assert new_value == original_value assert type(new_value) == type(original_value) assert new_value.unit == original_value.unit def test_storage_and_retrieval_of_measurement(self): original_value = measures.Weight(lb=124) MeasurementTestModel.objects.create(measurement_weight=original_value,) retrieved = MeasurementTestModel.objects.get() new_value = retrieved.measurement_weight assert new_value == original_value assert type(new_value) == type(original_value) assert new_value.unit == original_value.STANDARD_UNIT def test_storage_and_retrieval_of_measurement_choice(self): original_value = measures.Distance(km=100) MeasurementTestModel.objects.create(measurement_distance_km=original_value,) retrieved = MeasurementTestModel.objects.get() new_value = retrieved.measurement_distance_km assert new_value == original_value assert type(new_value) == type(original_value) assert new_value.unit == original_value.unit @pytest.mark.parametrize( "fieldname, measure_cls", [ ("measurement_weight", measures.Weight), ("measurement_distance", measures.Distance), ("measurement_distance_km", measures.Distance), ("measurement_speed", measures.Speed), ("measurement_temperature", measures.Temperature), ("measurement_speed_mph", measures.Speed), ("measurement_custom_degree_per_time", DegreePerTime), ("measurement_custom_temperature", Temperature), ("measurement_custom_time", Time), ], ) class TestDeconstruct: def test_deconstruct(self, fieldname, measure_cls): field = MeasurementTestModel._meta.get_field(fieldname) name, path, args, kwargs = field.deconstruct() assert name == fieldname assert path == "django_measurement.models.MeasurementField" assert args == [] assert kwargs["blank"] == field.blank assert kwargs["null"] == field.null assert kwargs["measurement"] == field.measurement new_cls = module_loading.import_string(path) new_field = new_cls(name=name, *args, **kwargs) assert type(field) == type(new_field) assert field.deconstruct() == (name, path, args, kwargs) @pytest.mark.parametrize( "fieldname, measure, expected_serialized_value", [ ("measurement_weight", measures.Weight(kg=4.0), "4.0:kg"), ("measurement_speed", measures.Speed(mi__hr=2.0), "2.0:mi__hr"), ], ) class TestSerialization: def test_deconstruct(self, fieldname, measure, expected_serialized_value): instance = MeasurementTestModel(pk=0) setattr(instance, fieldname, measure) serialized_object = serializers.serialize("python", [instance])[0] serialized_value = serialized_object["fields"][fieldname] assert isinstance(serialized_value, str) assert serialized_value == expected_serialized_value deserialized_obj = next(serializers.deserialize("python", [serialized_object])) deserialized_value = getattr(deserialized_obj.object, fieldname) assert deserialized_value == measure class TestMeasurementFormField: def test_max_value(self): valid_form = MeasurementTestForm( {"measurement_distance_0": 2.0, "measurement_distance_1": "mi"} ) invalid_form = MeasurementTestForm( {"measurement_distance_0": 4.0, "measurement_distance_1": "mi"} ) assert valid_form.is_valid() assert not invalid_form.is_valid() field = MeasurementField(measures.Distance, max_value=measures.Distance(mi=1)) field.clean([0.5, "mi"]) with pytest.raises(ValidationError) as e: field.clean([2.0, "mi"]) assert "Ensure this value is less than or equal to 1.0 mi." in str(e) with pytest.raises(ValueError) as e: MeasurementField(measures.Distance, max_value=1.0) assert bytes(e) == '"max_value" must be a measure, got float' def test_form_storage(self): form = MeasurementTestForm( {"measurement_distance_0": 2.0, "measurement_distance_1": "mi"} ) assert form.is_valid() obj = form.save() assert obj.measurement_distance == measures.Distance(mi=2) def test_form_bidir(self): form = MeasurementTestForm( {"measurement_speed_0": 2.0, "measurement_speed_1": "mi__hr"} ) assert form.is_valid() obj = form.save() assert obj.measurement_speed == measures.Speed(mi__hr=2) def test_min_value(self): field = MeasurementField(measures.Distance, min_value=measures.Distance(mi=1.0)) field.clean([2.0, "mi"]) with pytest.raises(ValidationError) as e: field.clean([0.5, "mi"]) assert "Ensure this value is greater than or equal to 1.0 mi." in str(e) with pytest.raises(ValueError) as e: MeasurementField(measures.Distance, min_value=1.0) assert str(e) == '"min_value" must be a measure, got float' def test_float_casting(self, caplog): m = MeasurementTestModel( measurement_distance=float(2000), measurement_distance_km=2, ) m.full_clean() assert m.measurement_distance.value == 2000 assert m.measurement_distance.unit == Distance.STANDARD_UNIT assert m.measurement_distance_km.value == 2 assert m.measurement_distance_km.unit == "km" assert m.measurement_distance_km == Distance(km=2) m.measurement_distance_km = Distance(km=1) m.full_clean() assert m.measurement_distance_km.value == 1 assert m.measurement_distance_km.unit == "km" assert m.measurement_distance_km == Distance(km=1) record = caplog.records[0] assert record.levelname == "WARNING" assert record.message == ( "You assigned a float instead of Distance to" " tests.models.MeasurementTestModel.measurement_distance," ' unit was guessed to be "m".' ) def test_unicode_labels(self): form = LabelTestForm() assert ("c", u"°C") in form.fields["simple"].fields[1].choices assert ("f", u"°F") in form.fields["simple"].fields[1].choices assert ("k", u"°K") in form.fields["simple"].fields[1].choices si_form = SITestForm() assert ("ms", "ms") in si_form.fields["simple"].fields[1].choices assert ("Ps", "Ps") in si_form.fields["simple"].fields[1].choices bi_dim_form = BiDimensionalLabelTestForm() assert ("c__ms", u"°C/ms") in bi_dim_form.fields["simple"].fields[1].choices assert ("c__Ps", u"°C/Ps") in bi_dim_form.fields["simple"].fields[1].choices