5

What I need to do is run a function and append a prefix to the beginning of the results returned from that function. This needs to be done each time a new instance is created of my model.

What I have tried....

The following won't work because you cannot add a string to a function and would set the ID as s_<function name etc> and not the results of the function.

APP_PREFIX = "_s"
id = models.CharField(primary_key=True, max_length=50, unique=True,
                      default="{}{}".format(APP_PREFIX, make_id))

Nor will passing the prefix to the function because Django will generate the same key each time calling the function this way, no idea why tho:

id = models.CharField(primary_key=True, max_length=50, unique=True,
                      default=make_id(APP_PREFIX))

This won't work either:

id = models.CharField(primary_key=True, max_length=50, unique=True,
                      default=make_id + APP_PREFIX)

Or this:

id = models.CharField(primary_key=True, max_length=50, unique=True,
                      default=make_id() + APP_PREFIX)

How can this be achieved?

I could overwrite the save() method and achieve this but there must be a way to do this with the default parameter on the field!

Prometheus
  • 32,405
  • 54
  • 166
  • 302
  • You're not actually calling the function in the first example, have you tried calling it? – kylieCatt May 28 '15 at 14:45
  • @IanAuld yep and you get violates unique constraint on any object you create after the first., if I call it i.e. ``default=make_id()`` Django seem to call it just the once and not each time on creating a new instance. – Prometheus May 28 '15 at 14:47
  • http://stackoverflow.com/questions/12649659/how-to-set-a-django-model-fields-default-value-to-a-function-call-callable-e – kylieCatt May 28 '15 at 14:48
  • @IanAuld read that, but does not apply as he is adding 2 dates together. What I need to do is run a function and append a prefix to the beginning of the results returned. – Prometheus May 28 '15 at 14:49
  • If the result of your function is a string, which it is, format should work fine. Alternatively if concatenating them inside the field definition won't work just change your function to `return 's_' + base64.b64encode(uuid.uuid4().bytes).decode("utf-8")` – kylieCatt May 28 '15 at 14:53
  • @IanAuld ``s_`` is actually dynamic so thats not applicable. – Prometheus May 28 '15 at 14:55
  • How about using the `pre_save` signal https://docs.djangoproject.com/en/dev/ref/signals/#pre-save and setting the value there? – Adrian Ghiuta May 28 '15 at 14:57
  • @AdrianGhiuta I could also do it in the ``save()`` method but I would really like to find away to do this with the default parameter on the field if possible :) – Prometheus May 28 '15 at 14:59

4 Answers4

3

I found a way to achieve this on my model:

PREFIX = settings.PREFIXES['THIS']
def get_id():
  return make_id(PREFIX)

    class MyModel(models.Model):
        id = models.CharField(primary_key=True,unique=True,
                                  default=get_id,)
Prometheus
  • 32,405
  • 54
  • 166
  • 302
2

You can create a custom manager for your model and override the create() method. Then, in the create() method you can generate the ID field by explicitly calling the make_id function. The only thing is that you should not set the default property for the id field.

So the model would look something like this:

class MyModel(models.Model):
  id = models.CharField(primary_key=True, max_length=50, unique=True)
  objects = MyModelManager()

Then, the implementation of MyModelManager should look something like this:

class MyModelManager(models.Manager):
  def create(self, **kwargs):
    kwargs['id'] = make_id()
    return super(MyModelManager, self).create(**kwargs)

Of course, you should add to the create method any other parameters you need for the creation of instances of your model.

Creating an object instance of MyModel is then done by:

my_model = MyModel.objects.create()

Alternatively, if you'd like to use the default property, the following should work for you:

class MyModel(models.Model):
  APP_PREFIX = "_s"
  id = models.CharField(primary_key=True, max_length=50, unique=True,
                        default=lambda: generate_id(MyModel.APP_PREFIX))

def generate_id(prefix):
  return make_id() + prefix
OrenD
  • 1,751
  • 13
  • 12
  • That solution would work, but I don't believe this cannot be done using the default parameter. I would like to hear if this is possible. – Prometheus May 28 '15 at 14:58
  • I understand. Just verified it and the solution in [this](http://stackoverflow.com/questions/12649659/how-to-set-a-django-model-fields-default-value-to-a-function-call-callable-e) post should work for you. If you'd like I can edit the answer with a suggested solution based on it. – OrenD May 28 '15 at 15:24
  • Edited the answer with the suggested default based solution. – OrenD May 28 '15 at 15:34
  • I like the lambda soultion however when I try to use it I get ``/migrations/writer.py", line 388, in serialize raise ValueError("Cannot serialize function: lambda")`` – Prometheus May 28 '15 at 15:46
  • It seems from read more lambda cannot be used because of migrations http://stackoverflow.com/questions/27072222/django-1-7-1-makemigrations-fails-when-using-lambda-as-default-for-attribute – Prometheus May 28 '15 at 15:51
2

Rewriting the save method should be simple to implement:

from django.db import models

class YourModel(models.Model):
    id = models.CharField(primary_key=True, max_length=50, unique=True)
    # other fields for YourModel

    def make_id():
        return base64.b64encode(uuid.uuid4().bytes).decode("utf-8")

    def save(self, *args, ***kwargs):
        if not self.id:
            self.id = self.make_id()
        super(YourModel, self).save(*args, **kwargs)

Eventually make_id is not a method of YourModel, but that is just a slight change in the code. In the method save you can concatenate "s_" or whatever you need. The id is generated only if the entry hasn't got any id (meaning it doesn't yet exist).

Using self.pk might be an alternative (and maybe) better approach. You could omit the explicit creation of the field id.

cezar
  • 11,616
  • 6
  • 48
  • 84
  • Thanks for the reply, I already know I could do this but as I said in the OP there must be a way to do this with the default parameter on the field :) – Prometheus May 28 '15 at 15:16
  • I read your comment regarding ``save()`` after I wrote my answer. I'm sorry, but I also have no idea how to do it with the default parameter. – cezar May 28 '15 at 15:19
  • No problem, what you have would work, but I would like to first explore how this can be achieved on the field :) – Prometheus May 28 '15 at 15:43
1

Tip: Use functools to create callable.
The example below will generate prefixed hash like tag_HKh7788GjhbJu8K

import random
import string
from functools import partial

class PrefixedHashField(models.CharField):

    @staticmethod
    def gen_value(prefix, hash_length):
        hashtext = ''.join(random.choices(string.ascii_letters + string.digits, k=hash_length))
        return f"{prefix}_{hashtext}"

    def __init__(self, *args, **kwargs):
        prefix = kwargs.pop('prefix')
        hash_length = kwargs.pop("hash_length")
        kwargs['default'] = partial(self.gen_value, prefix, hash_length)
        super(PrefixedHashField, self).__init__(*args, **kwargs)


class Tag(models.Model):
    id = PrefixedHashField(primary_key=True, prefix="tag", hash_length=15, max_length=30)
stupidbodo
  • 466
  • 1
  • 5
  • 14