Giter Club home page Giter Club logo

django-pint's Introduction

Build Status codecov PyPI Downloads Python Versions PyPI Version Project Status Wheel Build Code Style Black pre-commit pre-commit.ci status License: MIT Documentation Status

Django Quantity Field

A Small django field extension allowing you to store quantities in certain units and perform conversions easily. Uses pint behind the scenes. Also contains a form field class and form widget that allows a user to choose alternative units to input data. The cleaned_data will output the value in the base_units defined for the field, eg: you specify you want to store a value in grams but will allow users to input either grams or ounces.

Help wanted

I am currently not working with Django anymore. Therefore the Maintenance of this project is not a priority for me anymore. If there is anybody that could imagine helping out maintaining the project, send me a mail.

Compatibility

Requires django >= 3.2, and python 3.8/3.9/3.10/3.11

Tested with the following combinations:

  • Django 3.2 (Python 3.8, 3.9, 3.10, 3.11)
  • Django 4.2 (Python 3.8, 3.9, 3.10, 3.11)

Installation

pip install django-pint

Simple Example

Best way to illustrate is with an example

# app/models.py

from django.db import models
from quantityfield.fields import QuantityField

class HayBale(models.Model):
    weight = QuantityField('tonne')

Quantities are stored as float (Django FloatField) and retrieved like any other field

>> bale = HayBale.objects.create(weight=1.2)
>> bale = HayBale.objects.first()
>> bale.weight
<Quantity(1.2, 'tonne')>
>> bale.weight.magnitude
1.2
>> bale.weight.units
'tonne'
>> bale.weight.to('kilogram')
<Quantity(1200, 'kilogram')>
>> bale.weight.to('pound')
<Quantity(2645.55, 'pound')>

If your base unit is atomic (i.e. can be represented by an integer), you may also use IntegerQuantityField and BigIntegerQuantityField.

If you prefer exact units you can use the DecimalQuantityField

You can also pass Quantity objects to be stored in models. These are automatically converted to the units defined for the field ( but can be converted to something else when retrieved of course ).

>> from quantityfield.units import ureg
>> Quantity = ureg.Quantity
>> pounds = Quantity(500 * ureg.pound)
>> bale = HayBale.objects.create(weight=pounds)
>> bale.weight
<Quantity(0.226796, 'tonne')>

Use the inbuilt form field and widget to allow input of quantity values in different units

from quantityfield.fields import QuantityFormField

class HayBaleForm(forms.Form):
    weight = QuantityFormField(base_units='gram', unit_choices=['gram', 'ounce', 'milligram'])

The form will render a float input and a select widget to choose the units. Whenever cleaned_data is presented from the above form the weight field value will be a Quantity with the units set to grams (values are converted from the units input by the user). You also can add the unit_choices directly to the ModelField. It will be propagated correctly.

For comparative lookups, query values will be coerced into the correct units when comparing values, this means that comparing 1 ounce to 1 tonne should yield the correct results.

less_than_a_tonne = HayBale.objects.filter(weight__lt=Quantity(2000 * ureg.pound))

You can also use a custom Pint unit registry in your project settings.py

# project/settings.py

from pint import UnitRegistry

# django-pint will set the DJANGO_PINT_UNIT_REGISTER automatically
# as application_registry
DJANGO_PINT_UNIT_REGISTER = UnitRegistry('your_units.txt')
DJANGO_PINT_UNIT_REGISTER.define('beer_bootle_weight = 0.8 * kg = beer')

# app/models.py

class HayBale(models.Model):
    # now you can use your custom units in your models
    custom_unit = QuantityField('beer')

Note: As the documentation from pint states quite clearly: For each project there should be only one unit registry. Please note that if you change the unit registry for an already created project with data in a database, you could invalidate your data! So be sure you know what you are doing! Still only adding units should be okay.

Development

Preparation

You need to install all Python Version that django-pint is compatible with. In a *nix environment you best could use pyenv to do so.

Furthermore, you need to install tox and pre-commit to lint and test.

You also need docker as our tests require a postgres database to run. We don't use SQL lite as some bugs only occurred using a proper database.

I recommend using pipx to install them.

  1. Install pipx (see pipx documentation), i.e. with python3 -m pip install --user pipx && python3 -m pipx ensurepath
  2. Install pre-commit running pipx install pre-commit
  3. Install tox running pipx install tox
  4. Install the tox-docker plugin pipx inject tox tox-docker
  5. Fork django-pint and clone your fork (see Tutorial)
  6. Change into the repo cd django-pint
  7. Activate pre-commit for the repo running pre-commit install
  8. Check that all linter run fine with the cloned version by running pre-commit run --all-files
  9. Check that all tests succeed by running tox

Congratulation you successfully cloned and tested the upstream version of django-pint.

Now you can work on your feature branch and test your changes using tox. Your code will be automatically linted and formatted by pre-commit if you commit your changes. If it fails, simply add all changes and try again. If this doesn't help look at the output of your git commit command.

Once you are done, create a pull request.

Local development environment with Docker

To run a local development environment with Docker you need to run the following steps: This is helpful if you have troubles installing postgresql or psycopg2-binary.

  1. git clone your fork
  2. run cp .env.example .env
  3. edit .env file and change it with your credentials ( the postgres host should match the service name in docker-file so you can use "postgres" )
  4. run cp tests/local.py.docker-example tests/local.py
  5. run docker-compose up in the root folder, this should build and start 2 containers, one for postgres and the other one python dependencies. Note you have to be in the docker group for this to work.
  6. open a new terminal and run docker-compose exec app bash, this should open a ssh console in the docker container
  7. you can run pytest inside the container to see the result of the tests.

Updating the package

Python and Django major versions have defined EOL. To reduce the maintenance burden and encourage users to use version still receiving security updates any django-pint update should match all and only these version of Python and Django that are supported. Updating these dependencies have to be done in multiple places:

  • README.md: Describing it to end users
  • tox.ini: For local testing
  • setup.cfg: For usage with pip and displaying it in PyPa
  • .github/workflows/test.yaml: For the CI/CD Definition

django-pint's People

Contributors

alexbhandari avatar bharling avatar carlijoy avatar cornelv avatar ikseek avatar jacklinke avatar jonashaag avatar jwygoda avatar mikeford1 avatar mikeford3 avatar pre-commit-ci[bot] avatar raiderrobert avatar samueljennings avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

django-pint's Issues

Pint formatting template tag

Pint has special string formatting support. For example, from the docs:

>>> accel = 1.3 * ureg['meter/second**2']
>>> 'The HTML representation is {:H}'.format(accel)
'The HTML representation is 1.3 meter/second<sup>2</sup>'

I added this file as myapp/templatetags/pint_fmt.py:

from django import template
from django.utils.safestring import mark_safe

register = template.Library()
    
@register.filter
def pint(value, fmt): 
    full_fmt = '{:' + fmt + '}'
    return mark_safe(full_fmt.format(value))

Now I can do this in my templates:

{% load pint_fmt %}

{{ my_model.distance_field|pint:".2H" }}

And it gives me my field with two decimal places, and renders the units in HTML.

I haven't thought through the security implications of the mark_safe (my web site will be used by only me), and I don't know if you like the details of this way of getting at the formatting code, but I thought you might want to think about it.

Thanks for django-pint, it's great - exactly what I needed!

conversion to base

Hi @CarliJoy

Is the conversion to base units in the database necessary for Pint etc, or could it be made optional?

In some cases I'd like for users to be able to enter data in SI or US units and then have them displayed in update or detail views etc, but using the same units rather than automatic conversion to base.

Thanks

Use UnitRegistry in settings instead of setting it per Field

The documentation of pint suggest that the UnitRegistry is set on a central place in project.

As it would be wise to use the same registry also in one django project it might be the best the add a setting for it that initialized once.

This could reduce also the possibilities of bugs during migrations and make the used app easier to maintain.

Introduce Sphinx Doctest

We have some code examples in the README.
It would be great if this examples would be checked automatically with doctest

But for this we would need to convert the README probably to rst file, which I personally don't like that much.
On the other hand the README.md is already converted to an rst file during the build of the code.

Therefore it would be quite easy.

Upload the new version to pypi

Still waiting for Ben to give me permission for the project on PyPi.
Once that's done i will upload a new version to pypi.

Make Widget Works with proper database

After using postgres some test started to fail.
They are all related to comparing pint values and int in widgets.

The branch is called "improve_testing"
@cornelv If you have time that would be definitely an issue worth looking into!

Feature Request: Store selected_unit along with base_units to determine appropriate value for display

On first edit of a form page, you can select the unit to enter. Django-Pint will then convert it to the base_units to be stored.
The challenge with that is, the next time you edit the value, it defaults back to the base unit.

For example:

Edit1 for field with "minutes" as base_unit, entered:

8 days

After Save 1, the model stores 11520 (in minutes)

Edit2 for field:

value shows 11520 in minutes. The user says "no, that should be 'days'" and selects that. Doesn't realize the data doesn't auto update, and now you store 11520 days into the DB.

Or, the value shows 11520 minutes, but the end-user doesn't know that's 8 days.

Can't adapt type Quantity when using DecimalQuantityField

Hi, I've been trying to implement this into a new Django app but I was having a lot of trouble saving new models using the DecimalQuantityField. QuantityField by itself works fine.

After a bit of debugging, I noticed a Quantity object was being passed to the django SQLInsertCompiler. I'm new to Django so not sure if this is the right place to be looking. Anyways, this is because in fields.py DecimalQuantityField, this method returns a Quantity() object:

    def get_db_prep_save(self, value, connection):
        """
        Get Value that shall be saved to database, make sure it is transformed
        """
        value = self.get_prep_value(value)
        return super().get_db_prep_save(value, connection)

I changed it to this:

    def get_db_prep_save(self, value, connection):
        """
        Get Value that shall be saved to database, make sure it is transformed
        """
        value = self.get_prep_value(value)
        return value

And it fixed my issue! The super().get_db_prep_save() was going all the way up to django.db.models.fields.DecimalField. Surely this isn't correct?

I would fork and submit a pull request, but I wanted to make sure I am understanding / fixing the issue correctly. If my change is correct, let me know and I can do a pull request.

Thank you so much!

Dependencies:
asgiref==3.4.1
Django==3.2.8
django-environ==0.8.1
gunicorn==20.1.0
psycopg2-binary==2.9.1
pytz==2021.3
sqlparse==0.4.2
typing-extensions==3.10.0.2
django-bootstrap-v5==1.0.6
fontawesomefree==5.15.4
django-pint==0.6.2

Using postgres!

error converting small numbers-QuantityWidget decompress scientific notation

When converting quantities with small magnitudes defined in the model as a QuantityField the use of scientific notation causes an error in the decompress function in QuantityWidget.

I have a speed quantity with base units of meters per second and a unit choice of inches per second, defined below:

alpha = QuantityField( verbose_name="Burn Rate Coefficient", base_units="m/s", unit_choices=["in/s", "m/s"] )

When saved to the database with 1 in/s this is correctly displayed in a view as 0.0254 m/s, but when 0.0005079 in/s is used then 1.29006605 m/s is displayed. I think that this is caused in the decompress function, pint converts 0.0005079 in/s to 1.290066e-05 but then the non_decimal.sub("", str(value)) removes the 'e-', leaving 1.29006605.

I think that the simplest fix might be to change str(value) to "{:f}".format(value) or fstring equivalent, or just removing the non_decimal call and erroring if value cannot be converted to a number.

Form field base_units is not sent to widget

I am working on a project where each user can have a default unit set (metric/imperial). This app is perfect for this situation but I found 2 problems:

  1. the form field base unit is not passed over to Widget so the select input doesn't have the correct value when the form has initial data
  2. when the form has initial data the field value is not converted to the base_units ( it shows the value that is saved in the DB )

Just installed 0.7.1 - django.core.exceptions.ImproperlyConfigured

Just installed 0.7.1 using PIP with Django 4.2.2 and Python 3.9.6

I added a single field in my models using QuantityField to test. Upon testing using Django Shell, i get the following:

>>> from quantityfield.fields import QuantityField


Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/webroot/venv/lib/python3.9/site-packages/quantityfield/fields.py", line 17, in <module>
    from .units import ureg
  File "/webroot/venv/lib/python3.9/site-packages/quantityfield/units.py", line 1, in <module>
    from .settings import DJANGO_PINT_UNIT_REGISTER
  File "webroot/venv/lib/python3.9/site-packages/quantityfield/settings.py", line 7, in <module>
    DJANGO_PINT_UNIT_REGISTER = getattr(
  File "/webroot/venv/lib/python3.9/site-packages/django/conf/__init__.py", line 102, in __getattr__
    self._setup(name)
  File "/webroot/venv/lib/python3.9/site-packages/django/conf/__init__.py", line 82, in _setup
    raise ImproperlyConfigured(
django.core.exceptions.ImproperlyConfigured: Requested setting DJANGO_PINT_UNIT_REGISTER, but settings are not configured. You must either define the environment variable DJANGO_SETTINGS_MODULE or call settings.configure() before accessing settings.
>>>

While I'm told the settings.py file gets auto-added with the default registry, I still get this message. I tried manually adding this following to settings.py but it doesn't resolve the error:

from pint import UnitRegistry
DJANGO_PINT_UNIT_REGISTER = UnitRegistry()

Any thoughts? I understand you're not actively developing, and I'm happy to help identify the solution for others. Just need a little direction.

Feature Request: Table of `unit_choices` conversions within Form Widget

Because the value of an item using QuantityFormField defaults to the base unit when updating, even if the user originally entered a value using some other unit from unit_choices, they may later have some confusion about what value they originally entered.

To alleviate this, providing an optional table with converted values from the base unit to each of the unit choices within the form widget would be helpful. For configuration, it should only require something like setting show_table=True in the form field definition of a Django Form, defaulting to False if the attribute is not provided.

In cases where end users may set a value in a form and then need to edit that value in the future, this would help them to identify the current value since they can see both the base unit (in the input field itself) and their preferred unit (as well as any other unit choices) in the table below the input.

Additionally, users of django-pint should be able to override the template used for this table within the widget to meet their needs.


If all of this sounds like something that would be beneficial, please let me know and I'll submit a PR to provide the functionality and update the readme.

Quantity fields don't work using temperature units ("°C", "K", etc.)

Issue

Temperature units are special in pint as they "are non-multiplicative units" (see Pint docs here). The current implementation in django-pint does not handle these properly and declaring a field with temperature units and attempting to save will raise:

pint.errors.OffsetUnitCalculusError: Ambiguous operation with offset unit (degree_Celsius). See https://pint.readthedocs.io/en/stable/user/nonmult.html for guidance.

Solution

Thankfully, I think the solution is pretty simple. In quantityfields.fields, first:

class QuantityFieldMixin(object):
    ...
    def from_db_value(self, value: Any, *args, **kwargs) -> Optional[Quantity]:
        if value is None:
            return None
    #     return self.ureg.Quantity(value * getattr(self.ureg, self.base_units))
      return self.ureg.Quantity(value, getattr(self.ureg, self.base_units))

and then also,

class QuantityFormFieldMixin(object):
    ...
    def clean(self, value):
        ...
        # val = self.ureg.Quantity(val * getattr(self.ureg, units)).to(self.base_units)
        val = self.ureg.Quantity(val, getattr(self.ureg, units)).to(self.base_units)
        self.validate(val.magnitude)
        self.run_validators(val.magnitude)
        return val

As far as I'm aware, this change shouldn't affect any other fields.

decompress question

Thanks for implementing the fix to decompress. #40

Should the value.units in the case of a pint.Quantity be self.base_units as for the other types? When playing with this my form converts the quantity I defined to the base units and displays them (in a detail view with the widget.attrs readonly set to True and the field disabled). But, when using an update view the units selected in the dropdown aren't the base units, but the first units in the unit_choices list, with the converted magnitude. e.g

  1. defined model with a field with a base length of metres, with inches as the (first) choice.
  2. created instance using inches, 1 inch (ok)
  3. viewed in detail view, 0.0254 metre (ok)
  4. viewed in update view, 0.0254 inch (?)

Replace Six

Hi there,

Please can you replace:
from django.utils.six import python_2_unicode_compatible

With:
from six import python_2_unicode_compatible

So that this repo will be Py3 compatiable? Seems to work fine but might be worth you testing it properly as Im only using for testing a new project and only need very specific parts of it

Is this repro still maintained?

Dear @bharling,

is this repro still maintained?
It wasn't updated in four years.

Would you be okay with a change of a maintainer?

Here is one documentation of how to do it:
https://medium.com/@shazow/how-to-hand-over-an-open-source-project-to-a-new-maintainer-db433aaf57e8

I am interested to continue working on this project, merge the open pull request, make it work with Python 3 and django>2.2.
But I would drop support for Python 2 and django below 2.2. as it deprecated by now.

What do you think?

Current quantity value not rendered in admin view

Nice work on this, excited to help it develop.

I noticed that the value of the QuantityField is not displayed in the input box in the admin interface. I'm a django beginner, but if you point me in the right direction, happy to submit a pr.

Specify unit choices in model field declaration

I see that I can add a QuantityField to my model, for instance, quantity = QuantityField('mL'), but then in the admin form I only get the one 'mL' option in the popup.

It would be nice to be able to set choices=['floz', 'mL', 'quart'] on the model declaration and have that propagated to the form field.

As it is, I'm not sure what I have to do—write a custom form that specifies the a QuantityFormField with the appropriate unit_choices?

Check Comparison in filter with proper tests

Currently the orignal comparing code is commented out:
# This code is untested and not documented. It also does not call the super method
But still the test do not fail.

We should check if filter like this

less_than_a_tonne = HayBale.objects.filter(weight__lt=Quantity(2000 * ureg.pound))

really work properly by adding some more test.
I.e. use HayBale.weight (base unit is gram) and add an elements "100g". Compare it mit _eq to 0.1kg and 100000mg.
After that do Test with weigh_lt 0.99kg and 99 000mg as well as smaller 101g with units again.

If the function needs to be used, don't forget to call the super method.

@cornelv This would be an issue worth spending time on.

Why the requirement of django < 1.10 ?

First off, thanks for starting this. I was just about to dive in and attempt to create something very similar to this either based on Pint or Unum.

I'm wondering what the reasoning is behind requiring Django version < 1.10 in your package setup. Were you just being cautious? I'm trying to see if I can use this with the latest version of Django (1.10.1) and based on the 1.10 release notes I'm not sure if the restriction is necessary.

Thanks!

Use with ArrayField

The widgets provided for ArrayField in the admin do not handle the units in an elegant way. I would have expected a comma delimited list of values and a drop down for units afterward. Instead I get a comma delimited list of: value unit, value unit....

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.