Random/non-constant default value for model field? - django

I've got a model that looks something like this
class SecretKey(Model):
user = ForeignKey('User', related_name='secret_keys')
created = DateTimeField(auto_now_add=True)
updated = DateTimeField(auto_now=True)
key = CharField(max_length=16, default=randstr(length=16))
purpose = PositiveIntegerField(choices=SecretKeyPurposes)
expiry_date = DateTimeField(default=datetime.datetime.now()+datetime.timedelta(days=7), null=True, blank=True)
You'll notice that the default value for key is a random 16-character string. Problem is, I think this value is getting cached and being used several times in a row. Is there any way I can get a different string every time? (I don't care about uniqueness/collisions)

Yes, the default will only be set when the Model metaclass is initialized, not when you create a new instance of SecretKey.
A solution is to make the default value a callable, in which case the function will be called each time a new instance is created.
def my_random_key():
return randstr(16)
class SecretKey(Model):
key = CharField(max_length=16, default=my_random_key)
You could, of course, also set the value in the model's __init__ function, but callables are cleaner and will still work with standard syntax like model = SecretKey(key='blah').

Related

Create django model with as many fields as an integer in other field

I have the following model in django
class params(models.Model):
name = models.CharField(max_length=30, default = 'no_name')
cs_n = models.IntegerField(default=16)
alt_n = models.IntegerField(default=2)
opt_out = models.BooleanField(default=1)
at_n = models.IntegerField(default=4)
I want to create a new model with as many fields as at_n. For example, if the user enter "4" in at_n, I want this to create automatically:
class params(models.Model):
at_1 = models.IntegerField(default=2)
at_2 = models.IntegerField(default=2)
at_3 = models.IntegerField(default=2)
at_4 = models.IntegerField(default=2)
Thanks
This probably isn't a good data model to follow as Django models are intended to closely mirror database tables. For example, you wouldn't want to dynamically update the DDL of a table in a database because doing so places you at risk of messing up data that already exists in said table.
Instead, I think it would be a better approach for you to re-evaluate your data model.
For example, if there was a main object you were trying to tie these attributes to, then make a model for that main object and then make a separate model for main object attributes/values.
From there, you could use view logic to actually validate that the appropriate number of attributes assigned to a particular main object.
I'm thinking something kind of like this:
class MainModel(models.Model):
....
{Your main model attributes}
at_n = models.IntegerField(default=4)
....
class MainModelAttributes(model.Model):
main_model = models.ForeignKey(MainModel)
attr_value = models.IntegerField()
Then in your views.py file, you could use logic to make sure that the number of attributes on the MainModelAttributes model match the number stored in MainModel.at_n.

Django factory boy post_generation in field with default value don't work

I have a model with a field that is a int type, that field have a default value
I'm trying to set a value in that field with post_generation but nothing is happening, the field stay with the default value, when I try to use .set I get the following error:
AttributeError: 'int' object has no attribute 'set'
this is the field that I'm trying to populate
#factory.post_generation
def priority(obj, create, extracted, **kwargs):
for series in range(obj.patrimony.count()): # this is a sequence of numbers
series += 1
obj.priority.set(series)
and this is the model, is just a simple model
class Series(models.Model):
priority = models.IntegerField(_("Priority"), default=0, null=True)
Can someone open my eyes please?
You are meeting two issues:
Setting the field value
Series.priority is always an int, and integers have no .set() method (they are immutable objects).
You should set it by using obj.priority = series
Setting the value at the right time
factory_boy creates objects in 3 steps:
1. Evaluate all pre-declarations (LazyAttribute, Sequence, etc.);
2. Create the object in the database (calling Series.objects.create(...))
3. Evaluate post-generation declarations
If obj.patrimony is known before creating the series, you could simply have:
class SeriesFactory(factory.django.DjangoModelFactory):
class Meta:
model = Series
priority = factory.LazyAttribute(lambda o: o.patrimony.count())
(I've also adjusted the declaration, since your for series in ... loop is strictly equivalent to obj.priority = obj.patrimony.count())

Set ``upload_to`` function of a field in a foreign key

I have the following models structure:
class BaseMovie(models.Model):
movie = models.FileField('Movie')
width = models.IntegerField('Width')
height = models.IntegerField('Height')
duration = models.FloatField('Duration')
class SpecialMovie(models.model):
base_movie = models.ForeignKey(BaseMovie, 'Base movie')
# some other stuff
The idea behind is that I intend to use BaseMovie in several models.
My question is:
How can I set, in the SpecialMovie model, the location to which the special movies will be uploaded?
I think that storing only the metadata in the BaseMovie class and having the movie field with appropriate upload_to function in the SpecialMovie is not satisfying, because I would have to copy the code that fills the metadata in all the models with a foreign key to BaseMovie.
upload_to can be a callable; you could set it to a function that checks if the instance has a related SpecialMovie, and return the correct value accordingly.

Customizing the entry uniqueness in Django

I have a database containing a list of ingredients. I'd like to avoid duplicate entries in this table. I don't want to use the unique keyword for 2 reasons :
My uniqueness constraints are a bit more sophisticated than a mere =
I don't want to raise an exception when a pre-existing ingredient model is created, instead I just want to return that model, so that I can write Ingredient(ingredient_name='tomato') and just go on with my day rather than encapsulating all of that in a try clause. This will allow me to easily add ingredients to my recipe table on the fly.
One solution is simply to have a wrapper function like create_ingredient, but I don't find that to be particularly elegant and more specifically it's not robust to some other developer down the line simply forgetting to use the wrapper. So instead, I'm playing around with the pre_init and post_init signals.
Here's what I have so far :
class Ingredient(models.Model):
ingredient_name = models.CharField(max_length=200)
recipes = models.ManyToManyField(Recipe,related_name='ingredients')
def __str__(self):
return self.ingredient_name
class Name(models.Model):
main_name = models.CharField(max_length=200, default=None)
equivalent_name = models.CharField(max_length=200, primary_key=True, default=None)
def _add_ingredient(sender, args, **kwargs):
if 'ingredient_name' not in kwargs['kwargs'] :
return
kwargs['kwargs']['ingredient_name'] = kwargs['kwargs']['ingredient_name'].lower()
# check if equivalent name exists, make this one the main one otherwise
try:
kwargs['kwargs']['ingredient_name'] = Name.objects.filter(
equivalent_name=kwargs['kwargs']['ingredient_name']
)[0].main_name
except IndexError:
name = Name(main_name=kwargs['kwargs']['ingredient_name'],
equivalent_name=kwargs['kwargs']['ingredient_name'])
name.save()
pre_init.connect(_add_ingredient, Ingredient)
So far so good. This actually works and will replace ingredient_name when needed before the model is initialized. Now what I'd like is to check if the ingredient in question already exists and have the initializer return it if it does. I think I need to play around with post_init to do this but I don't know how to modify the particular instance that's being created. Here's what I mean by that :
def _finalize_ingredient(sender, instance, **kwargs):
try:
# doesn't work because of python's "pass arguments in python's super unique way of doing things" thing
instance = Ingredient.objects.filter(ingredient_name=instance.ingredient_name)[0]
except IndexError:
pass
post_init.connect(_finalize_ingredient, Ingredient)
As I've commented, I don't expect this to work because instance = ... doesn't actually modify instance, it just reassigns the variable name (incidentally if you try to run this all sorts of terrible things happen which I don't care to understand because I know this is flat out wrong). So how do I actually do this ? I really hope wrapper functions aren't the cleanest option here. I'm a big fan of OOP and gosh darn it I want an OOP solution to this (which, as I've said, I think in the long run would be much more robust and safer than wrappers).
I realize of course that I can add an add_ingredient method to Recipe which will do all of this for me, but I really like the idea of containing all of this in my Ingredient class as it will guarantee the proper database behavior under any circumstance. I'm also curious as to know if/how the post_init method can be used to completely override the created object for a given circumstance.
By the way, some of you may be wondering why I don't have a ForeignKey entry in my Name class that would connect the Name table to the Ingredient table. After all, isn't this what my check is essentially accomplishing in my _add_ingredient method ? One of the reasons is that if I do this then I end up with the same problem I'm trying to solve here : If I want to create an ingredient on the fly to add it to my recipe, I could simply create a Name object when creating an Ingredient object, but that would raise an exception if it corresponds to a main_name that is already in use (rather than simply returning the object I need).
I believe you are looking for get_or_create(), which is already a built-in in Django.
You mention:
One solution is simply to have a wrapper function like create_ingredient, but I don't find that to be particularly elegant and more specifically it's not robust to some other developer down the line simply forgetting to use the wrapper.
Well, look at it the other way around. What if you actually need to create a "duplicate" ingredient? Then it is nice to have the possibility.
I've come up with something that is as elegant and robust as I think it's possible to be given what I'm after. I've still had to define an add_ingredient method, but I still have the robustness that I need. I've made it so that it can be generalized to any class with a primary key, and the Name table will contain the info that will define the name uniqueness of any table :
class Name(models.Model):
main_name = models.CharField(max_length=200, default=None)
equivalent_name = models.CharField(max_length=200, primary_key=True, default=None)
def _pre_init_unique_fetcher(sender, args, **kwargs):
pk_name = sender._meta.pk.name
if pk_name not in kwargs['kwargs'] :
return
kwargs['kwargs'][pk_name] = kwargs['kwargs'][pk_name].lower()
# check if equivalent name exists, make this one the main one otherwise
try:
kwargs['kwargs'][pk_name] = Name.objects.filter(
equivalent_name=kwargs['kwargs'][pk_name]
)[0].main_name
except IndexError:
name = Name(main_name=kwargs['kwargs'][pk_name],
equivalent_name=kwargs['kwargs'][pk_name])
name.save()
sender._input_dict = kwargs['kwargs']
def _post_init_unique_fetcher(sender, instance, **kwargs):
pk_name = sender._meta.pk.name
pk_instance = instance.__dict__[pk_name]
filter_dict = {}
filter_dict[pk_name] = pk_instance
try:
post_init.disconnect(_post_init_unique_fetcher,sender)
instance.__dict__ = sender.objects.filter(**filter_dict)[0].__dict__
post_init.connect(_post_init_unique_fetcher, sender)
for key in sender._input_dict:
instance.__dict__[key] = sender._input_dict[key]
del sender._input_dict
except IndexError:
post_init.connect(_post_init_unique_fetcher, sender)
except:
post_init.connect(_post_init_unique_fetcher, sender)
raise
unique_fetch_models = [Ingredient, Recipe, WeekPlan]
for unique_fetch_model in unique_fetch_models :
pre_init.connect(_pre_init_unique_fetcher, unique_fetch_model)
post_init.connect(_post_init_unique_fetcher, unique_fetch_model)
Now what this will do is load up any new model with the pre-existing data of the previous model (rather than the default values) if one with the same name exists. The reason I still need an add_ingredient method in my Recipe class is because I can't call Ingredient.objects.create() for a pre-existing ingredient without raising an exception despite the fact that I can create the model and immediately save it. This has to do with how Django handles the primary_key designation : if you create the model then save it, it assumes you're just updating the entry if it already exists with that key, and yet if you create it, it tries to add another entry and that conflicts with the primary_key designation. So now I can do things like recipe.add_ingredient(Ingredient(ingredient_name='tomato', vegetarian=True)).

Django - sorting object based on user defined order in template

I want the user to be able to order a list of objects in a table using javascript. Then, in a django function I would like to sort those object based on the same ordering, not on an attribute.
Is it possible? I was thinking about passing a list of pk from the template to the view and then ordering the objects according to this list, but I have not found a way to do it yet.
I don't think this is possible with queryset. Try following:
pk_list = [2, 1, 3, 4]
pk2obj = {obj.pk: obj for obj in Model.objects.all()}
objects_ordered = [pk2obj[pk] for pk in pk_list]
pkg2obj is mapping between pk and model instance object. To make a dictionary I used dictionary comprehension.
If you want to omit deleted objects:
objects_ordered = [pk2obj[pk] for pk in pk_list if pk in pk2obj]
Else if you want to replace deleted objects with default value (None in following code):
objects_ordered = [pk2obj.get(pk, None) for pk in pk_list]
I've had to solve this exact problem before.
If you want the user to be able to reorder them into a user-defined order, you can easily define a field to store this order.
As you say, initially, you could serve them in order according to id or an upload_date DateTimeField. But you could also have an PositiveIntegerField in the model, named position or order, to represent the user-defined order.
class MediaItem(models.Model):
user = models.ForeignKey(User)
upload_date = models.DateTimeField(auto_now_add = True)
position = models.PositiveIntegerField()
Whenever a user changes the order on the frontend, the JS can send the new order as an array of objects (ie. new_order = [{"pk":3, "position":1}, {"pk":1, "position":2}, {"pk":2, "position":3}]). The view can look up each instance by pk, and change the position:
for obj in new_order:
media_item = MediaItem.objects.get(pk=obj['pk'])
media_item.position = obj['position']
media_item.save()
Then always query using
objects_ordered.objects.order_by('position')
That's how we managed to do it. If you have more specific questions regarding this approach, feel free to ask in the comments.
Edit:
If the same object can be a member of many different groups or lists, and you want to store the position of the membership within that list, you can achieve this using a through model. A through model is useful when you need to store data that relates to the relationship between two objects that are related. In addition to the MediaItem class shown above, this is what your other models would look like:
class Album(models.Model):
media_items = models.ManyToManyField(MediaItem,
related_name = 'album_media_items',
through = 'Membership')
class Membership(models.Model):
album = models.ForeignKey(Album,
related_name = 'album')
media_item = models.ForeignKey(MediaItem,
related_name = 'media_item')
date = models.DateTimeField(auto_now_add = True)
position = models.PositiveIntegerField()
Then, you could query the Membership instances, instead of the MediaItem instances.
# get id of list, or album...
alb = Album.objects.get(pk=id_of_album)
media_items = Membership.objects.filter(album=alb).order_by('position')
for item in media_items:
# return the items, or do whatever...
# keep in mind they are now in the user-defined order
You can do this:
pk_list = [1,5,3,9]
foo = Foo.objects.filter(id__in=pk_list)
#Order your QuerySet in base of your pk_list using Lambda
order_foo = sorted(foo, key = lambda:x , pk_list.index(x.pk))