Question

I would like to implement different systems of measurement in my Django project, so that users can choose wether they want to use metric or imperial units. However, I don't know what is the correct approach to do this.

Currently my models don't have measurement unit aware fields (they're integer/decimal fields) and I would prefer that I don't have to change my fields directly.

Since my current database values already represent metric values, I am planning to keep it that way, which means that I will have to handle user input / value output conversion. Pint seems to be a great library for this purpose.

Is this possible without editing fields in app, but using registration patterns or something else instead? What I would like to achieve is something like this:

  1. I define a data structure holding different measurement types and possible values, such as length: meters, feet; weight: kilograms, pounds etc.
  2. I add a new file "measurements.py" or something similar in each app directory, which has fields that hold measurement values. In this field I could then define which are the exact measurement fields and what are their types, such as fields = {mymodelfield: length, myothermodelfield: weight} etc.
  3. Some default settings could be set in settings file and overwritten in app file, such as default unit for each measurement (unit that is stored in database).
  4. User sets his preference for each measurement type. There is default unit for each measurement type as mentioned in previous point. Logic to convert user input in case of preference / default (stored) unit mismatch is required. Bound forms should also be able to convert field value back from default to user preferred unit.

This kind of solution would make it easier to plug it to existing apps since no fields and forms would be changed directly in the app. Basic examples using registration patterns would be helpful.

Any relevant information is welcome, including general ideas and patterns how this is done in non-Django projects.

Was it helpful?

Solution

Since you want to continue to store in metric then the only time you are going to possibly convert is:

  1. User input
  2. Displaying data to the user

For user input you can use a custom MultiValueField and a custom MultiWidget which will output a metric value.

from django import forms 
from django.forms import widgets,Form, CharField, MultiValueField, ChoiceField, DecimalField

imperial = "imp"
imperial_display = "Miles"
metric = "met"
metric_display  = "Kilometers"
unit_types = ((imperial, imperial_display), (metric, metric_display))

#You would use that library instead of this, and maybe not floats
def to_imperial(val):
    return float(val) * 1.60934

def to_metric(val):
    return float(val) / 0.62137

class NumberUnitWidget(widgets.MultiWidget):
    def __init__(self, default_unit=None, attrs=None):
        #I feel like this default_unit thing I'm doing is ugly
        #but I couldn't think of another way to get the decompress
        #to know which unit to convert to
        self.default_unit = default_unit

        _widgets = [
            widgets.NumberInput(attrs=attrs),
            widgets.Select(attrs=attrs, choices=unit_types),
        ]
        super(NumberUnitWidget, self).__init__(_widgets, attrs)

    #convert a single value to list for the form
    def decompress(self, value):
        if value:
            if self.default_unit == imperial:
                return [to_imperial(value), self.default_unit]

            #value is the correct format
            return [value, self.default_unit]
        return [0, self.default_unit]

class NumberUnitField(MultiValueField):

    def __init__(self, *args, **kwargs):
        _widget = NumberUnitWidget(default_unit=kwargs['initial'][0])
        fields = [
            DecimalField(label="Number!"),
            ChoiceField(choices=unit_types)
        ]
        super(NumberUnitField, self).__init__(fields=fields, widget = _widget, *args, **kwargs)


    def compress(self, values):
        if values[1] == imperial:
            #They inputed using imperial, convert to kilo
            return  to_metric(values[0])
        return values[0] #You dont need to convert because its in the format you want

class TestForm(Form):
    name = CharField()
    num = NumberUnitField(initial=[None, metric])

The default setting of metric for num in TestForm can be overwritten when you instantiate the form using initial={'num':[None,'imp']} so that is where you could insert the users preference from their profile.

Using the above form when you see that the form is_valid() the data the form will return to you will be in metric.

When the user sees the form it will appear as a Django NumberInput followed by a select with the default setting already selected.

For displaying data you could use a template filter. Here is a simple one to give you an idea

from django import template
register = template.Library()
@register.filter
def convert_units(value, user_pref):
    #You probably want to use that library you found here to do conversions
    #instead of what i've used.
    if user_pref == "met":
        return str(value) + " Kilometers"
    else:
        return str(float(value)/ 1.60934) + " Miles"

Then in the template it would look something like:

{{a_value|convert_units:user_pref}}

Of course all of this is just converting Miles/Kilometers. You'd have to duplicate and change or subclass the widget/field and probably have some more template filters to cover other things like pounds/kilograms.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top