is there something like getters and setters for django model's fields?
For example, I have a text field in which i need to make a string replace before it get saved (in the admin panel, for both insert and update operations) and make another, different replace each time it is read. Those string replace are dynamic and need to be done at the moment of saving and reading.
As I'm using python 2.5, I cannot use python 2.6 getters / setters.
Any help?
You can also override setattr and getattr. For example, say you wanted to mark a field dirty, you might have something like this:
class MyModel:
_name_dirty = False
name = models.TextField()
def __setattr__(self, attrname, val):
super(MyModel, self).__setattr__(attrname, val)
self._name_dirty = (attrname == 'name')
def __getattr__(self, attrname):
if attrname == 'name' and self._name_dirty:
raise('You should get a clean copy or save this object.')
return super(MyModel, self).__getattr__(attrname)
You can add a pre_save signal handler to the Model you want to save which updates the values before they get saved to the database.
It's not quite the same as a setter function since the values will remain in their incorrect format until the value is saved. If that's an acceptable compromise for your situation then signals are the easiest way to achieve this without working around Django's ORM.
Edit:
In your situation standard Python properties are probably the way to go with this. There's a long standing ticket to add proper getter/setter support to Django but it's not a simple issue to resolve.
You can add the property fields to the admin using the techniques in this blog post
Overriding setattr is a good solution except that this can cause problems initializing the ORM object from the DB. However, there is a trick to get around this, and it's universal.
class MyModel(models.Model):
foo = models.CharField(max_length = 20)
bar = models.CharField(max_length = 20)
def __setattr__(self, attrname, val):
setter_func = 'setter_' + attrname
if attrname in self.__dict__ and callable(getattr(self, setter_func, None)):
super(MyModel, self).__setattr__(attrname, getattr(self, setter_func)(val))
else:
super(MyModel, self).__setattr__(attrname, val)
def setter_foo(self, val):
return val.upper()
The secret is 'attrname in self.__dict__'. When the model initializes either from new or hydrated from the __dict__!
While I was researching the problem, I came across the solution with property decorator.
For example, if you have
class MyClass(models.Model):
my_date = models.DateField()
you can turn it into
class MyClass(models.Model):
_my_date = models.DateField(
db_column="my_date", # allows to avoid migrating to a different column
)
#property
def my_date(self):
return self._my_date
#my_date.setter
def my_date(self, value):
if value > datetime.date.today():
logger.warning("The date chosen was in the future.")
self._my_date = value
and avoid any migrations.
Source: https://www.stavros.io/posts/how-replace-django-model-field-property/
Related
After searching for a while, I can't seem to find any answer for this so I'm asking this question. What I want to is display some values based on a person's salary and a bracket. My models looks like this.
class Employee
salary = models.DecimalField
class Benefit
some_fields
class EmployeeBenefit
employee = models.ForeignKey
benefit = models.ForeignKey
class BenefitVersion
benefit = models.ForeignKey
class BenefitBracket
benefit_version = models.ForeignKey
from_salary = models.DecimalField
to_salary = models.DecimalField
the_fields_I_want_to_display
As you can see it's quite deep, there would be a bunch of querying to do to get to the fields I want.
I basically need to get the employee's salary(which is easy because this is gonna be inside EmployeeAdmin) then get the current benefit of the EmployeeBenefit list item, then based on the benefit and the employee's salary, get the bracket and then display some of it's fields on the inline.
I want to display the the_fields_I_want_to_display on the admin.TabularInline for EmployeeBenefit inside my EmployeeAdmin. I was testing using a forms.ModelForm in the inline and modifying it's contents using get_form based on this answer but django is not calling get_form. I also previously tried using calculated_fields but it's not being rendered as well.
I'm using django 3.1.7 btw
Edit:
I found this and try it out right now. I think it has potential but unfortunately the obj that get_formset gives is Employee so I still need to find a way to get the right EmployeeBenefit for the list item
After some more searching, I already figured out a way to do this. Based on this SO answer, I got an idea on how I should receive the data I passed to the formset using get_formset.
So, in my get_formset, I did something like this.
def get_formset(self, request, obj=None, **kwargs):
formset = super(Inline, self).get_formset(request, obj, **kwargs)
formset.request = request
benefit_details_dict_arr = []
emp_contribs = models.EmployeeBenefit.objects.filter(employee=obj)
#because what I'm getting here is Employee and not a EmployeeBenefit, I have to query it first
for contrib in emp_contribs:
bracket = contrib.get_benefit_bracket(datetime.now())
benefit_details_dict_arr.append({
"key": bracket.val,
"key2": bracket.val2,
})
formset.benefit_details = benefit_details_dict_arr
return formset
Then on the formset, I receive the data this way.
class EmpBenefitFormSet(forms.models.BaseInlineFormSet):
model = models.EmployeeBenefit
def __init__(self, *args, **kwargs):
super(EmpBenefitFormSet, self).__init__(*args, **kwargs)
cntr = 0
for form in self.forms:
init_data = self.benefit_details[cntr]
for field in form.fields:
if field in init_data:
form.initial[field] = init_data[field]
form.fields[field].required = False
form.fields[field].disabled = True
cntr += 1
Also, if you're gonna use this way of receiving the data, each field you want to inflate should be declared on the fields of the inline. This should work if the fields you want to set an initial value are actual fields of the model. You might need to tweak this a bit to work for custom fields
quick question. Does anyone have any idea how to write conditionals in django models?
For example I have this code here:
class Trip(models.Model):
tripName = models.CharField(max_length=64)
tripLogo = models.ImageField(default='default_trip.jpg', upload_to='trip_pics')
So here default value is 'default_trip.jpg', but I'd like to write a conditional that if tripName == "russian" than default=russia.jpg. Maybe not change default, but another image will be initiated.
This is not something that can be done on the model level, it must be done in the controller (otherwise, this would break the MVC pattern).
Keep in mind that Django's ORM wrapper must turn your model class into a usable table in whatever the underlaying database engine is. This type of "conditional default" is not part of any database engine that I know of.
default arg can be a calable.
def contact_default():
return {"email": "to1#example.com"}
contact_info = JSONField("ContactInfo", default=contact_default)
read this
So this part of code helped me to solve my problem.
def save(self, *args, **kwargs):
tripName = getattr(self, 'tripName')
if tripName in tripImages:
self.tripLogo = "{}.png".format(tripName.lower())
else:
self.tripLogo = "default_trip.png"
I have a model, News, that has a ForeignKey to another model, Category.
The categories are defined with id, name, slug and a few more parameters.
My categories will never change in the future, so I am wondering why have I to store them in the database.
Is there any way to hardcode them inside the code, like using the choices or any other technique?
yes, choices: store pairs of ID+name as a tuple of tuples and in another structure any other parameters.
CATEGORIES = (
(1, "Some category"),
(2, "Another category"),
)
CATEGORIES_PARAMS = {
1: {'slug': 'param_value'},
2: {'slug': 'another_value'},
}
class News(models.Model):
category = models.IntegerField("category", choices=CATEGORIES)
...
then you can use other params following way:
CATEGORIES_PARAMS[news_instance.category]['slug']
or create simple wrapper class:
class Category(object):
def __init__(self, id):
self.id = id
def __getattr__(self, attr):
return CATEGORIES_PARAMS[self.id][attr]
cat = Category(news_instance.category)
print(cat.slug)
If you intend to create a relationship between your News and Category model using a ForeignKey, then there has to be a model Category. Now you're saying your Category will never change but in-case if they need to be changed in future, you need to redesign your code. To use django's full set of ORM features, it's always better to store it in the database and stick with the design.
I am trying to create a custom cleaning method which look in the db if the value of one specific data exists already and if yes raises an error.
I'm using a model form of a class (subsystem) who is inheriting from an other class (project).
I want to check if the sybsystem already exists or not when i try to add a new one in a form.
I get project name in my view function.
class SubsytemForm(forms.ModelForm):
class Meta:
model = Subsystem
exclude = ('project_name')
def clean(self,project_name):
cleaned_data = super(SubsytemForm, self).clean(self,project_name)
form_subsystem_name = cleaned_data.get("subsystem_name")
Subsystem.objects.filter(project__project_name=project_name)
subsystem_objects=Subsystem.objects.filter(project__project_name=project_name)
nb_subsystem = subsystem_objects.count()
for i in range (nb_subsystem):
if (subsystem_objects[i].subsystem_name==form_subsystem_name):
msg = u"Subsystem already existing"
self._errors["subsystem_name"] = self.error_class([msg])
# These fields are no longer valid. Remove them from the
# cleaned data.
del cleaned_data["subsystem_name"]
return cleaned_data
My view function :
def addform(request,project_name):
if form.is_valid():
form=form.save(commit=False)
form.project_id=Project.objects.get(project_name=project_name).id
form.clean(form,project_name)
form.save()
This is not working and i don't know how to do.
I have the error : clean() takes exactly 2 arguments (1 given)
My model :
class Project(models.Model):
project_name = models.CharField("Project name", max_length=20)
Class Subsystem(models.Model):
subsystem_name = models.Charfield("Subsystem name", max_length=20)
projects = models.ForeignKey(Project)
There are quite a few things wrong with this code.
Firstly, you're not supposed to call clean explicitly. Django does it for you automatically when you call form.is_valid(). And because it's done automatically, you can't pass extra arguments. You need to pass the argument in when you instantiate the form, and keep it as an instance variable which your clean code can reference.
Secondly, the code is actually only validating a single field. So it should be done in a specific clean_fieldname method - ie clean_subsystem_name. That avoids the need for mucking about with _errors and deleting the unwanted data at the end.
Thirdly, if you ever find yourself getting a count of something, iterating through a range, then using that index to point back into the original list, you're doing it wrong. In Python, you should always iterate through the actual thing - in this case, the queryset - that you're interested in. However, in this case that is irrelevant anyway as you should query for the actual name directly in the database and check if it exists, rather than iterating through checking for matches.
So, putting it all together:
class SubsytemForm(forms.ModelForm):
class Meta:
model = Subsystem
exclude = ('project_name')
def __init__(self, *args, **kwargs):
self.project_name = kwargs.pop('project_name', None)
super(SubsystemForm, self).__init__(*args, **kwargs)
def clean_subsystem_name(self):
form_subsystem_name = self.cleaned_data.get("subsystem_name")
existing = Subsystem.objects.filter(
project__project_name=self.project_name,
subsytem_name=form_subsystem_name
).exists()
if existing:
raise forms.ValidationError(u"Subsystem already existing")
return form_subsystem_name
When you do form=form.save(commit=False) you store a Subsystem instance in the variable form but the clean method is defined in SubsystemForm. Isn't it?
I have a private boolean flag on my model, and a custom manager that overwrites the get_query_set method, with a filter, removing private=True:
class myManager(models.Manager):
def get_query_set(self):
qs = super(myManager, self).get_query_set()
qs = qs.filter(private=False)
return qs
class myModel(models.Model):
private = models.BooleanField(default=False)
owner = models.ForeignKey('Profile', related_name="owned")
#...etc...
objects = myManager()
I want the default queryset to exclude the private models be default as a security measure, preventing accidental usage of the model showing private models.
Sometimes, however, I will want to show the private models, so I have the following on the manager:
def for_user(self, user):
if user and not user.is_authenticated():
return self.get_query_set()
qs = super(myManager, self).get_query_set()
qs = qs.filter(Q(owner=user, private=True) | Q(private=False))
return qs
This works excellently, with the limitation that I can't chain the filter. This becomes a problem when I have a fk pointing the myModel and use otherModel.mymodel_set. otherModel.mymodel_set.for_user(user) wont work because mymodel_set returns a QuerySet object, rather than the manager.
Now the real problem starts, as I can't see a way to make the for_user() method work on a QuerySet subclass, because I can't access the full, unfiltered queryset (basically overwriting the get_query_set) form the QuerySet subclass, like I can in the manager (using super() to get the base queryset.)
What is the best way to work around this?
I'm not tied to any particular interface, but I would like it to be as djangoy/DRY as it can be. Obviously I could drop the security and just call a method to filter out private tasks on each call, but I really don't want to have to do that.
Update
manji's answer below is very close, however it fails when the queryset I want isn't a subset of the default queryset. I guess the real question here is how can I remove a particular filter from a chained query?
Define a custom QuerySet (containing your custom filter methods):
class MyQuerySet(models.query.QuerySet):
def public(self):
return self.filter(private=False)
def for_user(self, user):
if user and not user.is_authenticated():
return self.public()
return self.filter(Q(owner=user, private=True) | Q(private=False))
Define a custom manager that will use MyQuerySet (MyQuerySet custom filters will be accessible as if they were defined in the manager[by overriding __getattr__]):
# A Custom Manager accepting custom QuerySet
class MyManager(models.Manager):
use_for_related_fields = True
def __init__(self, qs_class=models.query.QuerySet):
self.queryset_class = qs_class
super(QuerySetManager, self).__init__()
def get_query_set(self):
return self.queryset_class(self.model).public()
def __getattr__(self, attr, *args):
try:
return getattr(self.__class__, attr, *args)
except AttributeError:
return getattr(self.get_query_set(), attr, *args)
Then in the model:
class MyModel(models.Model):
private = models.BooleanField(default=False)
owner = models.ForeignKey('Profile', related_name="owned")
#...etc...
objects = myManager(MyQuerySet)
Now you can:
¤ access by default only public models:
MyModel.objects.filter(..
¤ access for_user models:
MyModel.objects.for_user(user1).filter(..
Because of (use_for_related_fields = True), this same manager wil be used for related managers. So you can also:
¤ access by default only public models from related managers:
otherModel.mymodel_set.filter(..
¤ access for_user from related managers:
otherModel.mymodel_set.for_user(user).filter(..
More informations: Subclassing Django QuerySets & Custom managers with chainable filters (django snippet)
To use the chain you should override the get_query_set in your manager and place the for_user in your custom QuerySet.
I don't like this solution, but it works.
class CustomQuerySet(models.query.QuerySet):
def for_user(self):
return super(CustomQuerySet, self).filter(*args, **kwargs).filter(private=False)
class CustomManager(models.Manager):
def get_query_set(self):
return CustomQuerySet(self.model, using=self._db)
If you need to "reset" the QuerySet you can access the model of the queryset and call the original manager again (to fully reset). However that's probably not very useful for you, unless you were keeping track of the previous filter/exclude etc statements and can replay them again on the reset queryset. With a bit of planning that actually wouldn't be too hard to do, but may be a bit brute force.
Overall manji's answer is definitely the right way to go.
So amending manji's answer you need to replace the existing "model"."private" = False with ("model"."owner_id" = 2 AND "model"."private" = True ) OR "model"."private" = False ). To do that you will need to walk through the where object on the query object of the queryset to find the relevant bit to remove. The query object has a WhereNode object that represents the tree of the where clause, with each node having multiple children. You'd have to call the as_sql on the node to figure out if it's the one you are after:
from django.db import connection
qn = connection.ops.quote_name
q = myModel.objects.all()
print q.query.where.children[0].as_sql(qn, connection)
Which should give you something like:
('"model"."private" = ?', [False])
However trying to do that is probably way more effort than it's worth and it's delving into bits of Django that are probably not API-stable.
My recommendation would be to use two managers. One that can access everything (an escape hatch of sort), the other with the default filtering applied. The default manager is the first one, so you need to play around with the ordering depending on what you need to do. Then restructure your code to know which one to use - so you don't have the problem of having the extra private=False clause in there already.