#constants.py #set up conversions dictionary INCH_DECIMALS = [str(float(i) / float(16)) for i in range(0, 16)] INCH_FRACTIONS = ['---', '1/16', '1/8', '3/16', '1/4', '5/16', '3/8', '7/16', '1/2', '9/16', '5/8', '11/16', '3/4', '13/16', '7/8', '15/16'] conversions = zip(INCH_DECIMALS, INCH_FRACTIONS) INCH_CONVERSIONS = tuple(conversions) INCH_CONVERSIONS_DICT = dict(conversions) #utils.py from decimal import Decimal from math import floor from app.constants import INCH_CONVERSIONS_DICT def inches_to_feet_and_inches(value_in_inches): if value_in_inches: value_in_inches = float(value_in_inches) feet = int(round(value_in_inches, 0) / 12) inches = int(floor(value_in_inches) % 12) fractional_inches = value_in_inches % floor(value_in_inches) return feet, inches, fractional_inches return 0, 0, 0 def sum_feet_inches_fractional_inches(feet, inches, fractional_inches): return Decimal(str(((float(feet) * 12) + float(inches)) + fractional_inches)) def format_inches_to_feet_and_inches(value_in_inches): feet, inches, fractional_inches = inches_to_feet_and_inches(value_in_inches) if value_in_inches > 11.9375: value = "%s' %s" % (feet, inches) else: value = "%s" % inches if fractional_inches > 0: value = '%s %s' % (value, INCH_CONVERSIONS_DICT[str(fractional_inches)]) return value + '"' #custom_fields.py from django.core.exceptions import ValidationError from django.forms.fields import MultiValueField, IntegerField, FloatField from app.custom_widgets import FeetAndInchesWidget from app.utils import sum_feet_inches_fractional_inches class FeetAndInchesField(MultiValueField): widget = FeetAndInchesWidget def __init__(self, *args, **kwargs): errors = self.default_error_messages.copy() if 'error_messages' in kwargs: errors.update(kwargs['error_messages']) localize = kwargs.get('localize', False) fields = ( IntegerField(min_value=0, required=False, localize=localize), IntegerField(min_value=0, localize=localize), FloatField() ) super(FeetAndInchesField, self).__init__(fields, *args, **kwargs) def compress(self, data_list): if data_list: feet = data_list[0] inches = data_list[1] fractional_inches = data_list[2] if feet == inches == fractional_inches == 0: raise ValidationError(u'Please specify a value for feet or inches') return sum_feet_inches_fractional_inches(feet, inches, fractional_inches) return None #custom_widgets.py from django import forms from django.template.defaultfilters import mark_safe from app.constants import INCH_FRACTIONS, INCH_CONVERSIONS, INCH_CONVERSIONS_DICT from app.utils import inches_to_feet_and_inches class FeetAndInchesWidget(forms.MultiWidget): """ A widget that splits foot / inch and fraction text input into a decimal value """ def __init__(self, attrs=None): self.attrs = attrs or {} self.inch_conversions = INCH_CONVERSIONS widgets = ( forms.TextInput(attrs={'size' : '3'}), forms.TextInput(attrs={'size' : '3'}), forms.Select(attrs=attrs, choices=self.inch_conversions) ) super(FeetAndInchesWidget, self).__init__(widgets, attrs) def decompress(self, value): if value: feet, inches, fractional_inches = inches_to_feet_and_inches(value) return [feet, inches, fractional_inches] return [0, 0, 0] def format_output(self, rendered_widgets): return u'%s Feet  %s %s Inches' % \ (rendered_widgets[0], rendered_widgets[1], rendered_widgets[2]) """ Sample Usage and accompanying form/admin classes """ #models.py from django.db import models from app.constants import INCH_CONVERSIONS_DICT from app.custom_fields import FeetAndInchesField from app.utils import inches_to_feet_and_inches, \ format_inches_to_feet_and_inches class Cut(models.Model): roll = models.ForeignKey(Roll) date_cut = models.DateField(auto_now_add=True) job_number = models.CharField(max_length=50) qty = models.DecimalField(max_digits=9, decimal_places=4, default=0) def __unicode__(self): return '%s - Job #: %s - Qty: %s' % (self.date_cut, self.job_number, self.length_in_feet_and_inches) @property def length_in_feet_and_inches(self): return format_inches_to_feet_and_inches(self.qty) #forms.py from django import forms from app.custom_fields import FeetAndInchesField from app.custom_widgets import FeetAndInchesWidget from warehouse.models import Cut, Roll class CutForm(forms.ModelForm): class Meta: model = Cut qty = FeetAndInchesField() class CutInlineFormset(forms.models.BaseInlineFormSet): def clean(self): for form in self.forms: try: if form.cleaned_data: delete = form.cleaned_data.get('DELETE') if not delete: roll = form.cleaned_data.get('roll', None) qty = form.cleaned_data.get('qty', 0) check_remaining_roll_length = False cut = form.save(commit=False) try: original_cut = self.model.objects.get(pk=cut.id) if original_cut.qty != cut.qty: check_remaining_roll_length = True except self.model.DoesNotExist: #A new Cut object is being created so check_remaining_roll_length = True if check_remaining_roll_length and roll and qty > 0: if qty > roll.remaining_qty: raise forms.ValidationError(u'Cut length exceeds remaining length of roll') except AttributeError: pass #admin.py from django.contrib import admin from warehouse.forms import CutForm, CutInlineFormset from warehouse.models import Roll, Cut class CutInline(admin.TabularInline): model = Cut form = CutForm formset = CutInlineFormset extra = 0 class RollAdmin(admin.ModelAdmin): form = RollForm inlines = [CutInline] admin.site.register(Roll, RollAdmin)