7

I actually have this method in my Model:

def speed_score_compute(self):
  # Speed score:                                                                                                     
  #                                                                                                                  
  #   - 8 point for every % of time spent in                                                                         
  #     high intensity running phase.                                                                                
  #   - Average Speed in High Intensity                                                                              
  #     running phase (30km/h = 50 points                                                                            
  #     0-15km/h = 15 points )

  try:
    high_intensity_running_ratio = ((
      (self.h_i_run_time * 100)/self.training_length) * 8)
  except ZeroDivisionError:
    return 0
  high_intensity_running_ratio = min(50, high_intensity_running_ratio)
  if self.h_i_average_speed < 15:
    average_speed_score = 10
  else:
    average_speed_score = self.cross_multiplication(
      30, self.h_i_average_speed, 50)
  final_speed_score = high_intensity_running_ratio + average_speed_score
  return final_speed_score

I want to use it as default for my Model like this:

speed_score = models.IntegerField(default=speed_score_compute)

But this don't work (see Error message below) . I've checked different topic like this one, but this work only for function (not using self attribute) but not for methods (I must use methods since I'm working with actual object attributes).

Django doc. seems to talk about this but I don't get it clearly maybe because I'm still a newbie to Django and Programmation in general.

Is there a way to achieve this ?

EDIT:

My function is defined above my models. But here is my error message:

ValueError: Could not find function speed_score_compute in tournament.models. Please note that due to Python 2 limitations, you cannot serialize unbound method functions (e.g. a method declared and used in the same class body). Please move the function into the main module body to use migrations. For more information, see https://docs.djangoproject.com/en/1.8/topics/migrations/#serializing-values

Error message is clear, it seems that I'm not able to do this. But is there another way to achieve this ?

Community
  • 1
  • 1
Sami Boudoukha
  • 539
  • 10
  • 25
  • My `speed_score_compute()` function is defined above `speed_score` model. – Sami Boudoukha Feb 17 '16 at 15:33
  • What does "But this don't work" entail? errors? invalid results? – Sayse Feb 17 '16 at 15:35
  • Question has been updated with my Error message. – Sami Boudoukha Feb 17 '16 at 15:35
  • I got many methods about score computing, I just putted one here to keep the question clear. If I remove `activity_score_compute()` method, there will be the same message with the `speed_score_compute()` one. I will edit to put the speed_score one to avoid confusions. – Sami Boudoukha Feb 17 '16 at 15:40
  • @Addict Can I modify your question to make it more generic and canonical, so that it can be easily referenced to anyone who asks the same question? – bakkal Feb 25 '16 at 11:01

1 Answers1

10

The problem

When we provide a default=callable and provide a method from a model, it doesn't get called with the self argument that is the model instance.

Overriding save()

I haven't found a better solution than to override MyModel.save()* like below:

class MyModel(models.Model):
    def save(self, *args, **kwargs):
        if self.speed_score is None:
            self.speed_score = ...
            # or
            self.calculate_speed_score()

        # Now we call the actual save method
        super(MyModel, self).save(*args, **kwargs)

This makes it so that if you try to save your model, without a set value for that field, it is populated before the save.

Personally I just prefer this approach of having everything that belongs to a model, defined in the model (data, methods, validation, default values etc). I think this approach is referred to as the fat Django model approach.

*If you find a better approach, I'd like to learn about it too!

Using a pre_save signal

Django provides a pre_save signal that runs before save() is ran on the model. Signals run synchronously, i.e. the pre_save code needs to finish running before save() is called on the model. You'll get the same results (and order of execution) as overriding the save().

from django.db.models.signals import pre_save
from django.dispatch import receiver
from myapp.models import MyModel


@receiver(pre_save, sender=MyModel)
def my_handler(sender, **kwargs):
    instance = kwargs['instance']
    instance.populate_default_values()

If you prefer to keep the default values behavior separated from the model, this approach is for you!

When is save() called? Can I work with the object before it gets saved?

Good questioin! Because we'd like the ability to work with our object before subjecting to saving of populating default values.

To get an object, without saving it, just to work with it we can do:

instance = MyModel()

If you create it using MyModel.objects.create(), then save() will be called. It is essentially (see source code) equivalent to:

instance = MyModel()
instance.save()

If it's interesting to you, you can also define a MyModel.populate_default_values(), that you can call at any stage of the object lifecycle (at creation, at save, or on-demande, it's up to you)

bakkal
  • 54,350
  • 12
  • 131
  • 107
  • I'd like to know when the save method is called in the Django process ? Everytime you create a new Object or ?... – Sami Boudoukha Feb 17 '16 at 15:44
  • I think `s = SpeedScore()` wouldn't call `save()`, but `SpeedScore.objects.create()` would, so you _can_ control and have an object to work with before saving it. – bakkal Feb 17 '16 at 15:47
  • I would argue that a little bit, I think `pre_save` signal is better. It will have less impact on the default behavior of `save()`, it has also better interface for checking object states by looking at the arguments like `update_fields`, `instance`, `sender`, etc. – Shang Wang Feb 17 '16 at 15:53
  • Since Django signals are synchronous/blocking, in the flow of things I don't see a big difference, because the all the signal code will run before the original `save()`, which I call in the custom save(). The small plus side in overriding `save()` I see is just that the code stays in the model, if you like the _fat models_ approach in Django. – bakkal Feb 17 '16 at 15:56
  • 1
    Here, I'll add a section using a `pre_save` too, why the hell not :P – bakkal Feb 17 '16 at 15:57
  • 1
    Thanks @Sayse, added it! – bakkal Feb 17 '16 at 16:02