The goal is removing of duplicates from list field while saving model. For example creation in migration:
def migrate_model(apps, *args):
MyModel = apps.get_model('my_app.MyModel')
m = MyModel.objects.create(
array_field=['123','123'],
)
m.array_field # ['123']
I tried to overwrite save but it doesn't work
class MyModel(models.Model):
array_field = ArrayField(models.CharField(max_length=5))
def save(self, *args, **kwargs):
if self.array_field:
self.array_field = list(set(self.array_field))
super(MyModel, self).save(*args, **kwargs)
How can I do this?
Careful, the save() method is NOT called when using create() according to django docs.
Maybe that is causing you the problems, because your overriden save method doesn't actually gets called.
Related
thanks for tanking the time to look at this query.
I'm setting an ID field within one of my Django models. This is a CharField and looks like the following:
my_id = models.CharField(primary_key=True, max_length=5,
validators=[RegexValidator(
regex=ID_REGEX,
message=ID_ERR_MSG,
code=ID_ERR_CODE
)])
I would like to add a default/blank or null option that calls a global or class function that will cycle through the existing IDs, find the first one that doesn't exist and assign it as the next user ID. However, when I add the call blank=foo() I get an error code that the function doesn't exist.
Best,
pb
Edit1: I also tried using a separate utils file and importing the function, but (unsurprisingly) I get a circular import error as I need the call the class to get the objects.
Edit2 (Reply to Eugene): Tried that, solved the circular import but I'm getting the following error:
TypeError: super(type, obj): obj must be an instance or subtype of type
Previously my override of the save function worked perfectly:
def save(self, *args, **kwargs):
self.full_clean()
super(Staff, self).save(*args, **kwargs)
The custom id function:
def get_id_default():
from .models import MyObj
for temp_id in range(10_000, 100_000):
try:
MyObj.objects.get(my_id=str(temp_id))
except ObjectDoesNotExist:
break # Id doesn't exist
return str(hive_id)
Edit 3 (Reply to PersonPr7): Unfortunately, the kwargs doesn't seem to have my id in it. Actually, after having a print the kwargs dictionary comes back empty.
Save function:
def save(self, *args, **kwargs):
print(kwargs) # --> Returns {}
if kwargs["my_id"] is None:
kwargs["my_id"] = self.get_id_default()
self.full_clean()
super(Staff, self).save(*args, **kwargs)
Where the get_id_default is a class function:
def get_id_default(self):
for temp_id in range(10_000, 100000):
try:
self.objects.get(my_id=str(temp_id))
except ObjectDoesNotExist:
break # Id doesn't exist
return str(temp_id)
Solution1:
For those who are may be struggling with this in the future:
Create a utils/script .py file (or whatever you wanna call it) and create your custom script inside.
from .models import MyModel
def my_custom_default:
# your custom code
return your_value
Inside the main.models.py file.
from django.db import models
from .my_utils import my_custom_default
class MyModel(model.Model):
my_field = models.SomeField(..., default=my_custom_default)
Solution2: Create a static function within your Model class that will create your default value.
#staticmethod
def get_my_default():
# your logic
return your_value
# NOTE: Initially I had the function use self
# to retrieve the objects (self.objects.get(...))
# However, this raised an exception: AttributeError:
# Manager isn't accessible via Sites instances
When setting up your model give your field some kind of default i.e. default=None
Additionally, you need to override the models save function like so:
def save(self, *args, **kwargs):
if self.your_field is None:
self.my_field = self.get_my_default()
self.full_clean()
super().save(*args, **kwargs)
Try overriding the Model's save method and performing the logic there:
def save(self, *args, **kwargs):
#Custom logic
super().save(*args, **kwargs)
Edit:
You don't need to use **kwargs.
You can access your whole model from the save method and loop over objects / ids.
I am trying to add dynamically new form fields (I used this blog post), for a form used in admin interface :
class ServiceRoleAssignmentForm(forms.ModelForm):
class Meta:
model = ServiceRoleAssignment
def __init__(self, *args, **kwargs):
super(ServiceRoleAssignmentForm, self).__init__(*args, **kwargs)
self.fields['test'] = forms.CharField(label='test')
class ServiceRoleAssignmentAdmin(admin.ModelAdmin):
form = ServiceRoleAssignmentForm
admin.site.register(ServiceRoleAssignment, ServiceRoleAssignmentAdmin)
However, no matter what I try, the field doesn't appear on my admin form ! Could it be a problem related to the way admin works ? Or to ModelForm ?
Thank for any help !
Sébastien
PS : I am using django 1.3
When rendering your form in template, fields enumerating from fieldsets variable, not from fields. Sure you can redefine fieldsets in your AdminForm, but then validations will fail as original form class doesn't have such field. One workaround I can propose is to define this field in form definition statically and then redefine that field in form's init method dynamically. Here is an example:
class ServiceRoleAssignmentForm(forms.ModelForm):
test = forms.Field()
class Meta:
model = ServiceRoleAssignment
def __init__(self, *args, **kwargs):
super(ServiceRoleAssignmentForm, self).__init__(*args, **kwargs)
# Here we will redefine our test field.
self.fields['test'] = forms.CharField(label='test2')
I actually have a the same issue which I'm working through at the moment.
While not ideal, I have found a temporary workaround that works for my use case. It might be of use to you?
In my case I have a static name for the field, so I just declared it in my ModelForm. as normal, I then override the init() as normal to override some options.
ie:
def statemachine_form(for_model=None):
"""
Factory function to create a special case form
"""
class _StateMachineBaseModelForm(forms.ModelForm):
_sm_action = forms.ChoiceField(choices=[], label="Take Action")
class Meta:
model = for_model
def __init__(self, *args, **kwargs):
super(_StateMachineBaseModelForm, self).__init__(*args, **kwargs)
actions = (('', '-----------'),)
for action in self.instance.sm_state_actions():
actions += ((action, action),)
self.fields['_sm_action'] = forms.ChoiceField(choices=actions,
label="Take Action")
if for_model: return _StateMachineBaseModelForm
class ContentItemAdmin(admin.ModelAdmin):
form = statemachine_form(for_model=ContentItem)
Now as I mentioned before, this is not entirely 'dynamic', but this will do for me for the time being.
I have the exact same problem that, if I add the field dynamically, without declaring it first, then it doesn't actually exist. I think this does in fact have something to do with the way that ModelForm creates the fields.
I'm hoping someone else can give us some more info.
Django - Overriding get_form to customize admin forms based on request
Try to add the field before calling the super.init:
def __init__(self, *args, **kwargs):
self.fields['test'] = forms.CharField(label='test')
super(ServiceRoleAssignmentForm, self).__init__(*args, **kwargs)
I've created a model, and I'm rendering the default/unmodified model form for it. This alone generates 64 SQL queries because it has quite a few foreign keys, and those in turn have more foreign keys.
Is it possible to force it to always (by default) perform a select_related every time one of these models are returned?
You can create a custom manager, and simply override get_queryset for it to apply everywhere. For example:
class MyManager(models.Manager):
def get_queryset(self):
return super(MyManager, self).get_queryset().select_related('foo', 'bar')
(Prior to Django 1.6, it was get_query_set).
Here's also a fun trick:
class DefaultSelectOrPrefetchManager(models.Manager):
def __init__(self, *args, **kwargs):
self._select_related = kwargs.pop('select_related', None)
self._prefetch_related = kwargs.pop('prefetch_related', None)
super(DefaultSelectOrPrefetchManager, self).__init__(*args, **kwargs)
def get_queryset(self, *args, **kwargs):
qs = super(DefaultSelectOrPrefetchManager, self).get_queryset(*args, **kwargs)
if self._select_related:
qs = qs.select_related(*self._select_related)
if self._prefetch_related:
qs = qs.prefetch_related(*self._prefetch_related)
return qs
class Sandwich(models.Model):
bread = models.ForeignKey(Bread)
extras = models.ManyToManyField(Extra)
# ...
objects = DefaultSelectOrPrefetchManager(select_related=('bread',), prefetch_related=('extras',))
Then you can re-use the manager easily between model classes. As an example use case, this would be appropriate if you had a __unicode__ method on the model which rendered a string that included some information from a related model (or anything else that meant a related model was almost always required).
...and if you really want to get wacky, here's a more generalized version. It allows you to call any sequence of methods on the default queryset with any combination of args or kwargs. There might be some errors in the code, but you get the idea.
from django.db import models
class MethodCalls(object):
"""
A mock object which logs chained method calls.
"""
def __init__(self):
self._calls = []
def __getattr__(self, name):
c = Call(self, name)
self._calls.append(c)
return c
def __iter__(self):
for c in self._calls:
yield tuple(c)
class Call(object):
"""
Used by `MethodCalls` objects internally to represent chained method calls.
"""
def __init__(self, calls_obj, method_name):
self._calls = calls_obj
self.method_name = method_name
def __call__(self, *method_args, **method_kwargs):
self.method_args = method_args
self.method_kwargs = method_kwargs
return self._calls
def __iter__(self):
yield self.method_name
yield self.method_args
yield self.method_kwargs
class DefaultQuerysetMethodCallsManager(models.Manager):
"""
A model manager class which allows specification of a sequence of
method calls to be applied by default to base querysets.
`DefaultQuerysetMethodCallsManager` instances expose a property
`default_queryset_method_calls` to which chained method calls can be
applied to indicate which methods should be called on base querysets.
"""
def __init__(self, *args, **kwargs):
self.default_queryset_method_calls = MethodCalls()
super(DefaultQuerysetMethodCallsManager, self).__init__(*args, **kwargs)
def get_queryset(self, *args, **kwargs):
qs = super(DefaultQuerysetMethodCallsManager, self).get_queryset(*args, **kwargs)
for method_name, method_args, method_kwargs in self.default_queryset_method_calls:
qs = getattr(qs, method_name)(*method_args, **method_kwargs)
return qs
class Sandwich(models.Model):
bread = models.ForeignKey(Bread)
extras = models.ManyToManyField(Extra)
# Other field definitions...
objects = DefaultQuerysetMethodCallsManager()
objects.default_queryset_method_calls.filter(
bread__type='wheat',
).select_related(
'bread',
).prefetch_related(
'extras',
)
The python-mock-inspired MethodCalls object is an attempt at making the API more natural. Some might find that a bit confusing. If so, you could sub out that code for an __init__ arg or kwarg that just accepts a tuple of method call information.
Create a custom models.Manager and override all the methods (filter, get etc.) and append select_related onto every query. Then set this manager as the objects attribute on the model.
I would recommend just going through your code and adding the select_related where needed, because doing select_related on everything is going to cause some serious performance issues down the line (and it wouldn't be entirely clear where it's coming from).
say I've got:
class LogModel(models.Model):
message = models.CharField(max_length=512)
class Assignment(models.Model):
someperson = models.ForeignKey(SomeOtherModel)
def save(self, *args, **kwargs):
super(Assignment, self).save()
old_person = #?????
LogModel(message="%s is no longer assigned to %s"%(old_person, self).save()
LogModel(message="%s is now assigned to %s"%(self.someperson, self).save()
My goal is to save to LogModel some messages about who Assignment was assigned to. Notice that I need to know the old, presave value of this field.
I have seen code that suggests, before super().save(), retrieve the instance from the database via primary key and grab the old value from there. This could work, but is a bit messy.
In addition, I plan to eventually split this code out of the .save() method via signals - namely pre_save() and post_save(). Trying to use the above logic (Retrieve from the db in pre_save, make the log entry in post_save) seemingly fails here, as pre_save and post_save are two seperate methods. Perhaps in pre_save I can retrieve the old value and stick it on the model as an attribute?
I was wondering if there was a common idiom for this. Thanks.
A couple of months ago I found somewhere online a good way to do this...
class YourModel(models.Model):
def __init__(self, *args, **kwargs):
super(YourModel, self).__init__(*args, **kwargs)
self.original = {}
id = getattr(self, 'id', None)
for field in self._meta.fields:
if id:
self.original[field.name] = getattr(self, field.name, None)
else:
self.original[field.name] = None
Basically a copy of the model fields will get saved to self.original. You can then access it elsewhere in the model...
def save(self, *args, **kwargs):
if self.original['my_property'] != self.my_property:
# ...
It can be easily done with signals. There are, respectively a pre-save and post-save signal for every Django Model.
So I came up with this:
class LogModel(models.Model):
message = models.CharField(max_length=512)
class Assignment(models.Model):
someperson = models.ForeignKey(SomeOtherModel)
import weakref
_save_magic = weakref.WeakKeyDictionary()
#connect(pre_save, Assignment)
def Assignment_presave(sender, instance, **kwargs):
if instance.pk:
_save_magic[instance] = Assignment.objects.get(pk=instance.pk).someperson
#connect(post_save, Assignment)
def Assignment_postsave(sender, instance, **kwargs):
old = None
if instance in _save_magic:
old = _save_magic[instance]
del _save_magic[instance]
LogModel(message="%s is no longer assigned to %s"%(old, self).save()
LogModel(message="%s is now assigned to %s"%(instance.someperson, self).save()
What does StackOverflow think? Anything better? Any tips?
I need to detect when some of the fields of certain model have changed in the admin, to later send notifications depending on which fields changed and previous/current values of those fields.
I tried using a ModelForm and overriding the save() method, but the form's self.cleaned_data and seld.instance already have the new values of the fields.
Modifying the answer above... taking the brilliant function from Dominik Szopa and changing it will solve your relationship change detection: Use this:
def get_changes_between_models(model1, model2, excludes = []):
changes = {}
for field in model1._meta.fields:
if not (field.name in excludes):
if field.value_from_object(model1) != field.value_from_object(model2):
changes[field.verbose_name] = (field.value_from_object(model1),
field.value_from_object(model2))
return changes
Then in your code you can say (avoid try/except for performance reasons):
if (self.id):
old = MyModel.Objects.get(pk=self.id)
changes = get_changes_between_models(self, old)
if (changes):
# Process based on what is changed.
If you are doing this at the "model" level, there is no way to save the extra query. The data has already been changed by the time you reach the "Save" point. My first post, so forgive me if I sound like an idiot.
To avoid extra DB lookup, I modified constructor to remember initial value and use this in save method later:
class Package(models.Model):
feedback = models.IntegerField(default = 0, choices = FEEDBACK_CHOICES)
feedback_time = models.DateTimeField(null = True)
def __init__(self, *args, **kw):
super(Package, self).__init__(*args, **kw)
self._old_feedback = self.feedback
def save(self, force_insert=False, force_update=False, *args, **kwargs):
if not force_insert and self.feedback != self._old_feedback:
self.feedback_time = datetime.utcnow()
return super(Package, self).save(force_insert, force_update, *args, **kwargs)
In order to get differences of two model instances, you can also use this function. It compare to model instances and returns dictionary of changes.
What you'll need to do is get an extra copy of the object you're working on from the database inside the save method before fully saving it. Example:
class MyModel(models.Model):
field1 = models.CharField(max_length=50)
def save(self):
if self.id:
try:
old = MyModel.objects.get(pk=self.id)
if old.field1 != self.field1:
# Process somehow
except MyModel.DoesNotExist:
pass
super(MyModel, self).save()