Django项目中不同计量系统的概念

9 投票
2 回答
2661 浏览
提问于 2025-04-18 04:51

我想在我的Django项目中实现不同的计量系统,让用户可以选择使用公制单位还是英制单位。不过,我不知道该怎么做才好。

目前我的模型没有考虑计量单位的字段(它们都是整数或小数类型),我希望不需要直接修改这些字段。

因为我现在数据库中的值已经是公制的,所以我打算保持这种方式,这意味着我需要处理用户输入和输出值的转换。Pint这个库似乎非常适合这个目的。

有没有可能在不修改应用程序字段的情况下,使用注册模式或者其他方法来实现呢?我想要实现的目标大致是这样的:

  1. 我定义一个数据结构,包含不同的计量类型和可能的值,比如长度:米、英尺;重量:千克、磅等等。
  2. 我在每个应用目录中添加一个新的文件“measurements.py”或类似的文件,里面有存储计量值的字段。在这个字段中,我可以定义哪些是具体的计量字段,以及它们的类型,比如字段 = {mymodelfield: 长度, myothermodelfield: 重量}等等。
  3. 可以在设置文件中设置一些默认选项,并在应用文件中覆盖这些选项,比如每种计量的默认单位(存储在数据库中的单位)。
  4. 用户可以为每种计量类型设置自己的偏好。每种计量类型都有一个默认单位,如前面提到的。需要有逻辑来处理用户输入和默认单位不匹配的情况。绑定的表单也应该能够将字段值从默认单位转换回用户偏好的单位。

这种解决方案可以更方便地与现有应用连接,因为不需要直接在应用中更改字段和表单。使用注册模式的基本示例会很有帮助。

任何相关的信息都欢迎提供,包括一般的想法和在非Django项目中如何实现的模式。

2 个回答

0

Django的地理信息系统(GIS)包有一些基本的测量功能

>>> from django.contrib.gis.measure import D, Distance
>>> d1 = Distance(km=5)
>>> print(d1)
5.0 km
>>> d2 = D(mi=5)  # `D` is an alias for `Distance`
>>> print(d2)
5.0 mi

如果想要增加每个用户、会话等的个性化设置,并在模板、API等地方显示出来,其实是比较简单的。

5

既然你想继续使用公制单位存储数据,那么你可能需要转换的地方只有两个:

  1. 用户输入
  2. 向用户展示数据

对于用户输入,你可以使用一个自定义的 MultiValueField 和一个自定义的 MultiWidget,这样就能输出公制值。

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])

TestForm 中,num 的默认设置是公制的,但你可以在创建表单时用 initial={'num':[None,'imp']} 来覆盖这个设置,这样就可以把用户在个人资料中选择的偏好插入进来。

使用上面的表单,当你看到表单的 is_valid() 返回为真时,表单返回给你的数据将是公制单位。

当用户看到这个表单时,它会显示为一个 Django 的 NumberInput,后面跟着一个下拉选择框,默认选项已经被选中。

在展示数据时,你可以使用一个 模板过滤器。这里有一个简单的例子供你参考:

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"

然后在模板中,它看起来会像这样:

{{a_value|convert_units:user_pref}}

当然,这些只是把英里和公里进行转换。你可能还需要复制并修改或者子类化这个小部件/字段,可能还需要一些额外的模板过滤器来处理其他单位,比如磅和千克。

撰写回答