tags:

views:

332

answers:

4

Hello, I need some help with sending email when an order is placed. To illustrate the problem, following is the abstract code:

class Order(models.Model):
    user = models.ForeignKey(User)

class OrderItem(modes.Model):
    order = models.ForeignKey(Order, related_name='items')
    item = models.CharField(max_length=255)
    unit_price = models.DecimalField()
    qty = models.IntegerField()
    item_amount = models.DecimalField()

def email_order_on_save(sender, instance, **kwargs):
    # Need order.items.all() here
    pass

post_save.connect(email_order_on_save, sender=Order)

Most of the problems on SO and google seem to deal with one child object at a time; such as this.

Listening to OrderItem would release 5 signals if 5 orders items saved from admin inlines. I can't seem to get my head around this problem. One way, I think (not sure if possible), could be listening to last of all(5) OrderItem's post_save signals.

Any help appreciated.

+2  A: 

I'm guessing you're trying to solve this in the wrong place. Sending an email when the order is completed and saving the Order model are at different levels of abstraction.

I think sending the email should be triggered by some condition in the view that has more information about whether the order is completely saved or not. Think for example of what will happen if an order needs updating (say it's status changes)? Should the email be sent then too?

Tom
I'm curious. Could you elaborate on why they are "different levels of abstraction"? If sending the e-mail is part of the business logic, shouldn't it be triggered in some way by the models? Also the logic about sending the e-mail doesn't need to just blindly send out the e-mail at every save... `email_order_on_save` can receive as parameters informations on if this is an insert, an update (and what kind of update) and send the appropriate message...
celopes
Ok, the model may be the right place, but the save method/signal might not be. Django models serve two purposes, one is as the "model" layer (i.e. the business model), the other is persisting the data to the database, they overlap but aren't identical. What may be needed is a "submit order" method that calls save() to persist the model, then sends the email if the appropriate conditions are met (maybe using a custom signal as suggested by @celopes.)
Tom
Thanks Tom and celopes for your input. I agree with celopes. Email "is part of the business logic" and it is to be sent on each Order (along with OrderItems) creation which can be captured by `if kwargs['created']:` inside the `email_order_on_save` function.
Ah, I like the `submit_order` idea. It would live in the model and have all the business logic related to saving the models that compose a logical order and submitting it, including the e-mail. +1 for that. Combine this with a custom signal and you should be golden.
celopes
+1  A: 

Create your own custom signal and send it at the point when you have the data you need saved. Pass in as parameters whatever data structures you need.

Listen for your custom signal in your callback function email_order_on_save and make appropriate decisions based on the parameters about sending or not the e-mail.

celopes
Being a beginner Django user, I am not sure where that "point" would be where I have the Order with OrderItems saved. Other way I could think of is using `django-cron` by asynchronously checking for new orders and sending email. But that doesn't seem to be an optimal solution.
I think Tom has given you a great suggestion. Instead of, in the view, calling the individual model.save methods, create a `submit_order` method on Order and call that from the view. See my comment on Tom's answer.
celopes
Duh... Forgot to say: and send your signal from the new `submit_order` method.
celopes
A: 

I think you can have a problem with signals, OrderItem with inlines will not send save signal, read this

diegueus9
+1  A: 

You could create your model as follows

ORDER_STATE = (
    (1, 'Completed'),
    (2, 'Processing'),
)

class Order(models.Model):
    user = models.ForeignKey(User)
    state = models.IntegerField(choices = ORDER_STATE)

You could have many states for the order. The state "Completed" could represent that the order processing is complete. You could change the state of your order in your views.

In the signal handler, you could check for the state of the order and then send mail, if the order is in completed state.

Mayuresh
Thanks for your input Mayuresh but your method would require manual intervention which I'd rather avoid.