views:

317

answers:

2

basically i'm creating a tariff calculator for a utility company in my country, based on a progressive rates table, an example given below

2.(a) In any one month for less than 1,001 gallons a fee of ………………………..… $9.23

(b) Proportionate fee for every gallon thereafter up to 2,000 gallons inclusive at the rate per 1,000 gallons of ………………..$15.89

(c) Proportionate fee for every gallon above 2,000 gallons and up to 3,000 gallons inclusive at the rate per 1,000 gallons of .………..$17.43

(d) Proportionate fee for every gallon above 3,000 gallons and up to 4,000 gallons inclusive at the rate per 1,000 gallons of .………..$18.45

(e) Proportionate fee for every gallon above 4,000 gallons and up to 5,000 gallons inclusive at the rate per 1,000 gallons of .………..$19.48

(f) Proportionate fee for every gallon above 5,000 gallons and up to 6,000 gallons inclusive at the rate per 1,000 gallons of .………..$20.50

(g) Proportionate fee for every gallon above 6,000 gallons and up to 7,000 gallons inclusive at the rate per 1,000 gallons of .………..$21.01

(h) Proportionate fee for every gallon above 7,000 gallons and up to 8,000 gallons inclusive at the rate per 1,000 gallons of .………..$21.53

(i) Proportionate fee for every gallon above 8,000 at the rate per 1,000 gallons of ...$22.04

I'm thinking of doing a Rate model with the following fields:

  1. area of supply
  2. description
  3. from_gallons (starting range of gallons)
  4. to_gallons (end range of gallons)
  5. rate_per_1000g

my view would accept post from a form with the amount of gallons, area of supply and should post back the usage calculated from the rate table.

i would do something like this in the view

from models import Rate    
def tarrif_calc(request,gallons,area):
    rate = Rate.objects.get(area_of_supply=area, from_gallons >= gallons, to_gallons <= gallons)
    rate_per_gal = rate.rate_per_1000g/1000
    usage = gallons * rate_per_gal
    return usage

this is the most simple e.g i can think of, however i'm not sure how i would handle the progressive rates... any ideas? am i on the right track?

+1  A: 

Business rules should be expressed in code. More info

After reading your question a little closer, It looks possible that you might have many such tables you've got to query. Putting many similar such structures legitimately goes into a database. For now I'll assume you have that. If you really only have one table, a case analysis is probably most concise.

In my own django apps I've found that a very natural place to put business logic is in forms. Models seem obvious, but seldom have access to the context necessary to make informed decisions about what can go where.

Usually I'll divide this into several, dependent apps. One app provides very high level, abstract models and forms. Another app extends the forms from the first with business rules.

The first problem is querying the Rate model. The bit of code for that doesn't actually work. You can't pass lazy comparisons in python. Django has a mechanism for that, by mangling the argument names.

rate = Rate.objects.get(area_of_supply=area, from_gallons__gte=gallons, to_gallons__lte=gallons)

I'm not sure what your db looks like, but make sure there's an index on from_gallons and to_gallons, or this query will be very slow.

Your example doesn't sound like it actually alters state. If this is true, then you should probably be using a GET view rather than POST.

In either case the first part of the logic is the same. Recieve a request and validate the form input.

# in "myproject/businessrules/forms.py"
from django import forms
from myproject.otherapp.models import Area

class RateForm(forms.Form):
    # declare as instance variables the data actually posted
    quantity = forms.DecimalField(label="amount of gallons")
    area = forms.ModelChoiceField(label="area of supply", queryset=Area.objects.all() )
    # now add the business rule!
    def clean(self):
        cleaned_data = self.cleaned_data # quantity and area area already validated 
        rate = Rate.objects.get(
                            area_of_supply=cleaned_data["area"],
                            from_gallons__gte=cleaned_data["gallons"],
                            to_gallons__lte=cleaned_data["gallons"])
        usage = cleaned_data["gallons"] * rate.rate_per_1000g/1000
        cleaned_data["usage"] = usage
        return cleaned_data

Now all thats needed is to actually use it.

# in "myproject/myapp/views.py"
from django.shortcuts import render_to_response
from businessrules import RateForm

def tarrif_view(request):
    form = RateForm(request.GET)
    if form.is_valid():
        context = { "form": form, "valid": True, "usage":form.cleaned_data["usage"] }
    else:
        context = { "form": form, "valid": False }
    return render_to_response("tarrif.html", context)

Very little business knowledge is embedded in the view. Only the fact that the form stores a cleaned calculation result in its "usage" key. But since the view is precisely for obtaining that value, this is doing the right thing.

A template to use the form is also required. (My django template skill is only so-so. As i said, I prefer jinja2 and so should you)

Personally, I don't much like to muck about with moving individual form results into a template context, I just use the whole, processed form. I write my own templates in jinja2, though, so if your template designers are not trustworthy, you might want to craft your contexts a bit more carefully than this.

{# in "myproject/templates/tarrif.html" #}
{% if valid %}
Usage: {{ usage }} dollars.
{% else %}
<form action="" method="GET">
    {{ form }}
    <input type="submit" \>
</form>
{% endif %}

Again, no more business logic appears in the template than does in the view.

TokenMacGuy
i guess my question's summary should have been, is there an accepted way to handle such situations in django? from the link you provided, i gather that the database isn't the right place to store these rules?
Rasiel
i like your answer, but i still don't see how i would embed.. for e.g the above rates in a form without using the DB
Rasiel
an example with code maybe?
Rasiel
accepted, but see my solution below
Rasiel
A: 

A very complete example now from @TokenMacGuy above. and i see what you mean about the business logic in the form. Though i think you didn't account for the progressive rates which would make it quite longer.

I'm answering my own question after having completed the problem and in the hopes that someone else can input and enlighten django users about business logic cases.

so heres what i did:

first created a model for the rates, for it to be stored in the database. I marked each rate with a rate level to be able to handle the progressive rating.

class Rate(models.Model):
    area_of_supply = models.ForeignKey(SupplyArea)
    description = models.TextField()
    rate_level = models.CharField(max_length=2,choices=RATE_LEVEL_CHOICES,)
    rate_per_1k_gallons = models.DecimalField(max_digits=10,decimal_places=2)

I then created in my views.py the view that will handle the request from the form, (in my case both in ajax and a regular form). As well as the "business logic", in my case the "traffic_calc()"

def tariff(request):
    if request.method == 'POST':
        form = TariffForm(request.POST)
        if form.is_valid():
            gallons = form.cleaned_data['gallons']
            gallons = int(gallons)
            area_id = form.cleaned_data['tariff']
            area = area_id.name
            usage = tarrif_calc(request, area_id, gallons)    
            response_dict = {'gallons': gallons,'area': area,'usage': usage}
            if request.is_ajax():
                return HttpResponse(simplejson.dumps(response_dict), mimetype='application/json')
            return render_to_response('rates/tariff_form.html', response_dict)
    else:
        form = TariffForm()
    return render_to_response('rates/tariff_form.html', {'form': form})

def tarrif_calc(request, area, gallons):
    r_levels = gallons//1000
    remainder = gallons%1000
    usage = 0
    i = 1

    if r_levels < 1:
        rate = Rate.objects.get(area_of_supply=area,rate_level=1)
        usage = rate.rate_per_1k_gallons    
    else:
        if r_levels >= 8:
            while i <= 9:
                rate = Rate.objects.get(area_of_supply=area,rate_level=i)
                usage += rate.rate_per_1k_gallons
                i += 1

            if gallons > 9000:
                remainder = gallons - 9000
            remainder_rate = Rate.objects.get(area_of_supply=area,rate_level=9)
            usage += remainder * remainder_rate.rate_per_1k_gallons/1000
            usage = round(usage,2)
        else:    
            while i <= r_levels:
                rate = Rate.objects.get(area_of_supply=area,rate_level=i)
                usage += rate.rate_per_1k_gallons
                i += 1
            remainder_level = r_levels+1
            if remainder_level >= 9:
                remainder_rate = Rate.objects.get(area_of_supply=area,rate_level=9)
            else:
                remainder_rate = Rate.objects.get(area_of_supply=area, rate_level=remainder_level)    
                usage += remainder * remainder_rate.rate_per_1k_gallons/1000
                usage = round(usage,2)
    return usage

I agree with @TokenMacGuy.. i should have probably used GET, but used POST i guess because in the event that js isn't available the regular form works just as well. I also agree that at this level @TokenMacGuy could probably just from views import tariff_calc and use it in his forms.py.

I then created the forms and templates necessary and the javascript that handles the ajax posting (though not necessary)

// JavaScript Document
$(document).ready(function()    {

$('#tariff_form').submit(function(event){
    event.preventDefault();  // stop the form from submitting and refreshing the page
    var form = this;               // in jQuery, this refers to the current element, which is #article_form

    // grab each form value
    var data = {};
    gallons = $('#id_gallons').val();
    tariff = $('#id_tariff').val();

    // now send the data to the server via AJAX
    if(gallons && tariff)
    {
        $.ajax({
            type: "POST",
            url: "/tariff-calculator/",
            data: "gallons="+gallons+"&tariff="+tariff,
            dataType: "json",
            success: function(json){
                $('#gallons_cont').html(json['gallons']);
                $('#area_cont').html(json['area']);
                $('#usage_cont').html(json['usage'])
                $('#results_json').show('slow');            
            },
            });
    }
    else
    {
        alert('Please Fill All Fields')
    }
}); 

});

I'm guessing both methods are similar and both work (though i havn't tried @TokenMacGuy's), except for the tariff_calc() function, it seems MackGuy solved the problem so i will award him the correct answer, though i would love more feedback about business logic in forms.py as i'm not fully convinced, or think there is a huge difference.

Rasiel