3

How do I create a dynamic field on a model?

Let's say I'm writing an application related to the stock market. I make a purchase on one day and sometime later I want to check the gain (or loss) based on today's price. I'd have a model like this:

class Purchase(models.Model):
  ticker = models.CharField(max_length=5)
  date = models.DateField()
  price = models.DecimalField(max_digits=20, decimal_places=3)
  quantity = models.IntegerField()

What I'd like to do is define a model something like this:

class PurchaseGain(Purchase):
  gain = models.DecimalField(max_digits=20, decimal_places=3)
  class Meta:
    proxy = True

So that I could do this:

todays_price = get_price_from_webservice(ticker)
for p in PurchaseGain.objects.get_purchase_gain(todays_price):
  print '%s bought on %s for a gain of %s' % (p.ticker, p.date, p.gain)

Where p.gain is dynamically computed based on the input to get_purchase_gain. Rather than just constructing dictionaries on the fly I want to use a model, because I'd like to pass this around and generate forms, save changes, etc from the instance.

I tried creating a derived QuerySet, but that led to a circular dependency, because Purchase needed to know about the QuerySet (through a custom manager) and the QuerySet returned an iterator that needed to instantiate a PurchaseGain, which was derived from Purchase.

What options do I have?

Thanks, Craig

2 Answers 2

2

Why not add a gain() method to your model?

class Purchase(models.Model):
    ticker = models.CharField(max_length=5)
    date = models.DateField()
    price = models.DecimalField(max_digits=20, decimal_places=3)
    quantity = models.IntegerField()

    def gain(self, todays_price=None):
        if not todays_price:
            todays_price = get_price_from_webservice(self.ticker)
        result_gain = todays_price - self.price
        return result_gain

Then you can pretty much do what you want:

for p in Purchase.objects.all():
    print '%s bought on %s for a gain of %s' % (p.ticker, p.date, p.gain())
Sign up to request clarification or add additional context in comments.

4 Comments

You could even use the @property decorator so you can just refer to it as p.gain.
Thanks. That would work, but wouldn't scale. I could have hundreds or thousands of purchases for a single ticker. I'm looking for a way where I can precompute and pass it in to the query.Although perhaps I could just look at caching the remote call. I'll have to think about that.
I shouldn't have responded as soon as I woke up. The reason I didn't do it this way is that now the HTTP call is hardcoded into my model, which makes unit testing virtually impossible. I want something where I can dependency inject either a real HTTP client or a mock one for testing. That's what I like about the model I proposed above.
ok, I've added an optional "new_price" agurment to the gain method, this would allow testing. I agree with your comment above, if you're concerned with performance you should look into caching in some way. You could still put the cache retrieval in the gain() method.
2

Creating a proxy class is what confused me. By just adding attributes to a Purchase, I was able to accomplish what I wanted.

class PurchaseQuerySet(QuerySet):
  def __init__(self, *args, **kwargs):
    super(PurchaseQuerySet, self).__init__(*args, **kwargs)
    self.todays_price = None

  def get_with_todays_price(self, todays_price):
    self.todays_price = todays_price
    cloned = self.all()
    cloned.todays_price = todays_price
    return cloned

  def iterator(self):
    for p in super(PurchaseQuerySet, self).iterator():
      p.todays_price = self.todays_price
      yield p

class PurchaseManager(models.Manager):
  def get_query_set(self):
    return PurchaseQuerySet(self.model)

  def __getattr__(self, name)
    return getattr(self.get_query_set(), name)

class Purchase(models.Model):
  ticker = models.CharField(max_length=5)
  date = models.DateField()
  price = models.DecimalField(max_digits=20, decimal_places=3)
  quantity = models.IntegerField()

  objects = PurchaseManager()

  @property
  def gain(self):
    return self.todays_price - self.price

Now I can do:

for p in Purchase.objects.filter(ticker=ticker).get_with_todays_price(100):
  print p
  print p.gain

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.