CarliJoy / django-pint

Simple django app for storing Quantities with units, leveraging the Pint library
https://django-pint.readthedocs.io
MIT License
38 stars 16 forks source link

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

Open jacklinke opened 2 years ago

jacklinke commented 2 years ago

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.

CarliJoy commented 2 years ago

Dear jacklinke,

I agree with your analysis and the idea sounds awesome but also a bit tricky because you would need to link the chosen units with new database fields. Unfortunaetly it isn't possible to generate more than one database field from one django field. So the settings would to have be done only in the django form.

I also don't yet fully understand what your definition of "table" is? Do you want to use a database table and link each values to it? In this case we again would need to track which value of the field used belongs to which which value in my target table. Instead I would prefer handling it with a field naming scheme i.e:

class Beer:
   volume = QuantityField(...)
   volume_unit = models.CharField()+
   voulme_original = models.Float()

Or do you mean to use an Relation like suggested here: https://stackoverflow.com/questions/57590136/multiple-django-fields-reflects-to-one-db-field

jacklinke commented 2 years ago

I was thinking something even simpler than that - an actual html table of the conversions to each unit in unit_choices, displayed in the widget (optionally). The conversions would be done on-the-fly when displaying a form field using pint, of course. Something like this:

Screenshot from 2021-11-30 07-02-40

Users of django-pint could then style the table with css, or override the widget template to do things like move the table to be above the input box, for instance.

(I'm also intrigued by the concept you mention above and will think through how that might also be approached for as a future initiative)

CarliJoy commented 2 years ago

Ah okay. I understand now. Looks good and simple.

But then the naming is a bit confusing. Maybe call the argument show_conversion? And the make it an enum which currently is only has "Do not show" and "Table". So in the future we could add other tempalte if required.

I happy to look into your merge request.

jacklinke commented 2 years ago

Great suggestions! I'll finish this up and get a PR in this week :)

CarliJoy commented 2 years ago

If you need any help, let me know.

mikeford3 commented 2 years ago

@jacklinke, are you still expecting to submit a PR for this? It would be a very nice addition to the library :)

jacklinke commented 2 years ago

@mikeford3 It had slipped off my radar a bit, but yes will work o finishing it up this week.

CarliJoy commented 2 years ago

Dear jacklinke,

I agree with your analysis and the idea sounds awesome but also a bit tricky because you would need to link the chosen units with new database fields. Unfortunaetly it isn't possible to generate more than one database field from one django field. So the settings would to have be done only in the django form.

I also don't yet fully understand what your definition of "table" is? Do you want to use a database table and link each values to it? In this case we again would need to track which value of the field used belongs to which which value in my target table. Instead I would prefer handling it with a field naming scheme i.e:

class Beer:
   volume = QuantityField(...)
   volume_unit = models.CharField()+
   voulme_original = models.Float()

Or do you mean to use an Relation like suggested here: https://stackoverflow.com/questions/57590136/multiple-django-fields-reflects-to-one-db-field

Some more links to this approach: Hack: https://blog.elsdoerfer.name/2008/01/08/fuzzydates-or-one-django-model-field-multiple-database-columns/ Django Issue: https://code.djangoproject.com/ticket/5929 (Currently down, so you can use https://web.archive.org/web/20200809115733/https://code.djangoproject.com/ticket/5929)

mikeford3 commented 2 years ago

Thanks @jacklinke!

I interpreted table as just an HTML table, rather than a table in a database.

I was hoping to use this display a small table, like the one Jack showed above, when the user hovers over a field. I thought I'd try to use django-pint in the view to handle the conversions from base units (or the units the field currently uses) to the other unit_choices, and how to represent the units. The view would then be able to give the template a set of magnitudes and units for each field in the context variable.

jacklinke commented 2 years ago

@mikeford3 & @CarliJoy I think this issue should focus on the displayed table, with the changes to the db as a separate issue.

With that in mind, I'm nearly done with the work to display a table. Code still needs some tweaks and cleanup, but here's a screenshot I took just now using the dummyapp, using:

weight = QuantityFormField(
    base_units="gram",
    unit_choices=["ounce", "gram", "pound", "ton", "kilogram", "milligram"],
    show_conversion=DisplayOption.TABLE,
)

Screenshot from 2022-05-26 07-21-41


The html template for the widget looks like this

{% spaceless %}{% for widget in widget.subwidgets %}{% include widget.template_name %}{% endfor %}{% endspaceless %}

{% if values_list %}
    <table style="margin:20px;">
        <tr>
            <th>Unit</th>
            <th>Value</th>
        </tr>
        {% for value_item in values_list %}
            <tr>
                <td>{{ value_item.units }}</td>
                <td style="text-align:right">
                    {{ value_item.magnitude|floatformat:6 }}
                </td>
            </tr>
        {% endfor %}
    </table>
{% else %}
    <br>
{% endif %}
CarliJoy commented 2 years ago

Looks nice. I agree that the DB issue is just something very different.

mikeford3 commented 2 years ago

Hi @jacklinke, is still this something that you're working on?

apaxson commented 1 year ago

I, too, am looking at enhancing the QuantityField, where the selected magnitude is actually stored with the data. Except, the ideas I've had would render the DB value near useless for any external access/reporting tools. The easiest is to pickle the object. Or, more flexible, converting the Long field to a CharField and appending the magnitude to the value. You would obviously lose the precision characteristics of a Long, but gain descriptive value.

i.e. base_unit = minutes, selected = 8 days = "11520:days"

CarliJoy commented 1 year ago

"Pickling" numbers as strings in a database is not a good idea. You will increase the needed storage by multitude and loose all sql math capabilities like sum, average etc...

As long https://code.djangoproject.com/ticket/5929 is not resolved I don't see a proper way to solve this issue properly.

apaxson commented 1 year ago

I agree about Pickling. It's an idea, but a bad one. Until we can do this "proper", I've written a custom field that just uses markup in the DB. Horrible for external tools to the DB, but it works for me. Looking forward to solving this without markup.

i.e. with base_units = 'kilograms', saving an object using 'pounds'

# ureg= UnitRegistry()
# weight = 1000 * ureg.lb
# weight
<Quantity(1000, 'pound')>

# car = Car()
# car.name = "testcar"
# car.weight = weight
# car.save()
get_prep_value(): return value=453.5923700000001:pound

# new_car = Car.objects.get(pk=1)
from_db_value(): return value=1000.0000000000001 pound

# new_car.weight
<Quantity(1000.0, 'pound')>