I have a model on which the unique_together parameter is not working. The reason is that most of the times the "client_repr" variable is set on the save() method.
If someone creates a task with the same ('client_repr', 'task_type'... ) combination the model won't detect it because the "client_repr" value is null until the end of the save() method.
How can i call for a unique constraint verification inside the save() method?
class Task(models.Model):
client = models.ForeignKey(Client, related_name = 'tasks', on_delete = models.CASCADE)
b_client = models.ForeignKey(BClient, related_name = 'tasks', on_delete = models.CASCADE, null = True, blank = True)
client_repr = models.CharField(max_length = 100, null = True, blank = True)
task_type = models.CharField(max_length = 100)
task_description = models.CharField(max_length = 100)
department = models.ForeignKey(Department, related_name = 'tasks', on_delete = models.CASCADE)
extra_fields = models.ManyToManyField(ExtraField, blank = True)
spot = models.BooleanField(default = False)
class Meta:
unique_together = (('client_repr', 'task_type', 'task_description', 'department'), )
def __str__(self):
return ' | '.join([f'{self.client_repr}', f'{self.task_description}', f'{self.task_type}'])
def save(self, *args, **kwargs):
if not self.b_client:
self.client_repr = str(self.client)
else:
self.client_repr = str(self.b_client)
super().save(*args, **kwargs)
I know i could just make a search (ex: if Task.objects.get(...): ) but is it the most django-pythonic way?
Option 1: Using clean()
It's not directly what you asked, but it's generally more django-friendly and does not require weird things like overriding save()
class Task(Model):
def clean():
# your code starts here
if not self.b_client:
self.client_repr = str(self.client)
else:
self.client_repr = str(self.b_client)
# your code ends here
Since this custom clean() is called before django calls validate_unique(), it should fulfill your requirements.
See the details in the official documentation.
Option 2: Continue doing everything in save()
To check unique constraints, you can do the following:
from django.core.exceptions import ValidationError
def save(self, *args, **kwargs):
... # your code that automatically sets some fields
try:
self.validate_unique()
# self.full_clean() # <- alternatively, can use this to validate **everything**, see my comments below
except ValidationError:
# failed
# that's up to you what to do in this case
# you cannot just re-raise the ValidationError because Django doesn't expect ValidationError happening inside of save()
super().save(*args, **kwargs)
Note:
doing only self.validate_unique() does not guarantee, that the updated earlier in save() values are good and don't violate something else
self.full_clean() is safer, but will be slightly slower (how slow - depends on the validators you have)
Documentation
Django documentation says:
There are three steps involved in validating a model:
Validate the model fields - Model.clean_fields()
Validate the model as a whole - Model.clean()
Validate the field uniqueness - Model.validate_unique()
All three steps are performed when you call a model’s full_clean() method.
Related
I want automatically update field status in my model after 14 days.
Here is my model. I want to change status_of_renting to 0 after 15 days from date_of_rental (if date.today() if greater than date_of_return)
class RentCar(models.Model):
NORMAL = 1
PENALTY = 0
STATUS = (
(NORMAL, 'All is fine'),
(PENALTY, 'Penalty for using car')
)
car = models.ForeignKey('Car', on_delete = models.CASCADE, related_name = 'car')
person = models.ForeignKey('Person', on_delete = models.CASCADE, related_name = 'client')
date_of_rental = models.DateField(blank= True, default=timezone.now)
date_of_return = models.DateField(blank= True, default=date.today() + timedelta(days=14))
status_of_renting = models.IntegerField(choices = STATUS, default = 1)
def __str__(self):
return f'{self.rental} - {self.car} / {self.client.fullname}'
I can override def get_queryset() or dispatch in my generics.ListView, but I*m sure it's terrible decision. Is there some better solution to change the status in field status_of_renting.
views.py
class RentCatView(ListView):
model = RentCar
def get_queryset(self):
self.model.objects.filter(Q(status_of_renting = 1)&Q(date_of_return__lte = date.today())).update(status_of_renting=0)
return self.model.objects.all()
Trying to update database in queryset is indeed bad idea - it will execute every time someone try to fetch list of RentCar which will pollute your database with requests.
You need to setup cron job to run your query to update RentCar. Good thing is that you can run it only once a day, since it is only time that status_of_renting should be changed.
You can use one of django app, such as django-cron or django-background-tasks.
For example, this is what you need to do with django-background-tasks:
from background_task import background
from .models import RentCar
#background(schedule=24*60*60)
def update_status_of_renting(user_id):
RentCar.objects.filter(Q(status_of_renting = 1)&Q(date_of_return__lte = date.today())).update(status_of_renting=0)
I have a 3-level Test model I want to present as nested formsets. Each Test has multiple Results, and each Result can have multiple Lines. I am following Yergler's method for creating nested formsets, along with this SO question that updates Yergler's code for more recent Django version (I'm on 1.4)
I am running into trouble because I want to use FormSet's "extra" parameter to include an extra Line in the formset. The ForeignKey for each Line must point to the Result that the Line belongs to, but cannot be changed by the user, so I use a HiddenInput field to contain the Result in each of the FormSet's Lines.
This leads to "missing required field" validation errors because the result field is always filled out (in add_fields), but the text and severity may not (if the user chose not to enter another line). I do not know the correct way to handle this situation. I think that I don't need to include the initial result value in add_fields, and that there must be a better way that actually works.
Update below towards bottom of this question
I will gladly add more detail if necessary.
The code of my custom formset:
LineFormSet = modelformset_factory(
Line,
form=LineForm,
formset=BaseLineFormSet,
extra=1)
class BaseResultFormSet(BaseInlineFormSet):
def __init__(self, *args, **kwargs):
super(BaseResultFormSet, self).__init__(*args, **kwargs)
def is_valid(self):
result = super(BaseResultFormSet, self).is_valid()
for form in self.forms:
if hasattr(form, 'nested'):
for n in form.nested:
n.data = form.data
if form.is_bound:
n.is_bound = True
for nform in n:
nform.data = form.data
if form.is_bound:
nform.is_bound = True
# make sure each nested formset is valid as well
result = result and n.is_valid()
return result
def save_all(self, commit=True):
objects = self.save(commit=False)
if commit:
for o in objects:
o.save()
if not commit:
self.save_m2m()
for form in set(self.initial_forms + self.saved_forms):
for nested in form.nested:
nested.save(commit=commit)
def add_fields(self, form, index):
# Call super's first
super(BaseResultFormSet, self).add_fields(form, index)
try:
instance = self.get_queryset()[index]
pk_value = instance.pk
except IndexError:
instance=None
pk_value = hash(form.prefix)
q = Line.objects.filter(result=pk_value)
form.nested = [
LineFormSet(
queryset = q, #data=self.data, instance = instance, prefix = 'LINES_%s' % pk_value)]
prefix = 'lines-%s' % pk_value,
initial = [
{'result': instance,}
]
)]
Test Model
class Test(models.Model):
id = models.AutoField(primary_key=True, blank=False, null=False)
attempt = models.ForeignKey(Attempt, blank=False, null=False)
alarm = models.ForeignKey(Alarm, blank=False, null=False)
trigger = models.CharField(max_length=64)
tested = models.BooleanField(blank=False, default=True)
Result Model
class Result(models.Model):
id = models.AutoField(primary_key=True)
test = models.ForeignKey(Test)
location = models.CharField(max_length=16, choices=locations)
was_audible = models.CharField('Audible?', max_length=8, choices=audible, default=None, blank=True)
Line Model
class Line(models.Model):
id = models.AutoField(primary_key=True)
result = models.ForeignKey(Result, blank=False, null=False)
text = models.CharField(max_length=64)
severity = models.CharField(max_length=4, choices=severities, default=None)
Update
Last night I added this to my LineForm(ModelForm) class:
def save(self, commit=True):
saved_instance = None
if not(len(self.changed_data) == 1 and 'result' in self.changed_data):
saved_instance = super(LineForm, self).save(commit=commit)
return saved_instance
It ignores the requests to save if only the result (a HiddenInput) is filled out. I haven't run into any problems with this approach yet, but I haven't tried adding new forms.
When I used extra on formsets in similar situation I ended up having to include all the required fields from the model in the form, as HiddenInputs. A bit ugly but it worked, curious if anyone has a hack-around.
edit
I was confused when I wrote above, I'd just been working on formsets using extra with initial to pre-fill the extra forms and also I hadn't fully got all the details of your questions.
If I understand correctly, where you instantiate the LineFormSets in add_fields each of those will point to the same Result instance?
In this case you don't really want to supply result in initial due to the problems you're having. Instead you could remove that field from the LineForm model-form altogether and customise the LineFormSet class something like:
class LineFormSet(forms.BaseModelFormSet):
# whatever other code you have in it already
# ...
# ...
def __init__(self, result, *args, **kwargs):
super(LineFormSet, self).__init__(*args, **kwargs)
self.result = result
def save_new(self, form, commit=True):
instance = form.save(commit=False)
instance.result = self.result
if commit:
instance.save()
return instance
def save_existing(self, form, instance, commit=True):
return self.save_new(form, commit)
(this should be ok in Django 1.3 and 1.4, not sure other versions)
so the relevant part of your add_fields method would look like:
form.nested = [
LineFormSet(
result = instance,
queryset = q, #data=self.data, instance = instance, prefix = 'LINES_%s' % pk_value)]
prefix = 'lines-%s' % pk_value,
)]
I am working on a library system to manage certain items in our office, I don't need a full-blown integrated library system so I decided to hand roll one with Django.
Below is a simplified version of my model:
class ItemObjects(models.Model):
# Static Variables
IN_STATUS = 'Available'
OUT_STATUS = 'Checked out'
MISSING = 'Missing'
STATUS_CHOICES = (
(IN_STATUS, 'Available'),
(OUT_STATUS, 'Checked out'),
(MISSING, 'Missing'),
)
# Fields
slug = models.SlugField(unique=True)
date_added = models.DateField(auto_now_add=True)
last_checkin = models.DateTimeField(editable=False, null=True)
last_checkout = models.DateTimeField(editable=False, null=True)
last_activity = models.DateTimeField(editable=False, null=True)
status = models.CharField(choices=STATUS_CHOICES, default=IN_STATUS, max_length=25)
who_has = models.OneToOneField(User, blank=True, null=True)
times_out = models.PositiveIntegerField(default=0, editable=False)
notes = models.CharField(blank=True, max_length=500)
history = models.TextField(blank=True, editable=False)
pending_checkin = models.BooleanField(default=False)
pending_transfer = models.BooleanField(default=False)
At first I was using a method on ItemObject to process checking out an item to a user and who_has was an EmailField because I couldn't get a CharfField to populate with the logged in user's name, but I figured using a OneToOneField is probably closer to the "right" way to do this.. While who_has was an EmailField, the following method worked:
def check_out_itemobject(self, user):
user_profile = user.get_profile()
if self.status == 'Available' and self.who_has == '':
self.status = 'Checked out'
self.who_has = user.email
self.last_checkout = datetime.datetime.now()
self.last_activity = datetime.datetime.now()
self.times_out += 1
if self.history == '':
self.history += "%s" % user_profile.full_name
else:
self.history += ", %s" % user_profile.full_name
if user_profile.history == '':
user_profile.history += self.title
else:
user_profile.history += ", %s" % self.title
else:
return False # Not sure is this is "right"
user_profile.save()
super(ItemObjects, self).save()
Now that I am using a OneToOneField this doesn't work, so I started looking at using a subclass of ModelForm but none of the cases I saw here on SO seemed to apply for what I am trying to do; my form would be a button, and that's it. Here are some of the questions I looked at:
Django: saving multiple modelforms simultaneously (complex case)
(Django) (Foreign Key Issues) model.person_id May not be NULL
django update modelform
So was I on the right track with a sort of altered save() method, or would a ModelForm subclass be the way to go?
EDIT/UPDATE: Many thanks to #ChrisPratt!
So I am trying to get Chris Pratt's suggestion for showing ItemHistory to work, but when I try to render it on a page I get an AttributeError that states "'User' object has no attribute 'timestamp'". So my question is, why is it complaining about a User object when last_activity is an attribute on the ItemObject object ?
My view:
#login_required
def item_detail(request, slug):
item = get_object_or_404(Item, slug=slug)
i_history = item.last_activity
user = request.user
return render_to_response('items/item_detail.html',
{ 'item' : item,
'i_history': i_history,
'user' : user })
I do not see why a User object is coming up at this point.
EDIT2: Nevermind, history is clearly a M2M field whose target is User. That's why!
Assuming users will log in and check out books to themselves, then what you most likely want is a ForeignKey to User. A book will only have one User at any given time, but presumably Users could check out other items as well. If there is some limit, even if the limit is actually one per user, it would be better to validate this in the model's clean method. Something like:
def clean(self):
if self.who_has and self.who_has.itemobject_set.count() >= LIMIT:
raise ValidationError('You have already checked out your maximum amount of items.')
Now, you checkout method has a number of issues. First, status should be a defined set of choices, not just random strings.
class ItemObject(models.Model):
AVAILABLE = 1
CHECKED_OUT = 2
STATUS_CHOICES = (
(AVAILABLE, 'Available'),
(CHECKED_OUT, 'Checked Out'),
)
...
status = models.PositiveIntegerField(choices=STATUS_CHOICES, default=AVAILABLE)
Then, you can run your checks like:
if self.status == self.STATUS_AVAILABLE:
self.status = self.STATUS_CHECKED_OUT
You could use strings and a CharField instead if you like, as well. The key is to decouple the static text from your code, which allows much greater flexibility in your app going forward.
Next, history needs to be a ManyToManyField. Right now, your "history" is only who last checked the item out or what the last item the user checked out was, and as a result is pretty useless.
class ItemObject(models.Model):
...
history = models.ManyToManyField(User, through='ItemHistory', related_name='item_history', blank=True)
class ItemHistory(models.Model):
CHECKED_OUT = 1
RETURNED = 2
ACTIVITY_CHOICES = (
(CHECKED_OUT, 'Checked Out'),
(RETURNED, 'Returned'),
)
item = models.ForeignKey(ItemObject)
user = models.ForeignKey(User)
activity = models.PostiveIntegerField(choices=ACTIVITY_CHOICES)
timestamp = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-timestamp'] # latest first
Which then allows you to get full histories:
some_item.history.all()
some_user.item_history.all()
To add a new history, you would do:
ItemHistory.objects.create(item=some_item, user=some_user, activity=ItemHistory.CHECKED_OUT)
The auto_now_add attribute ensures that the timestamp is automatically set when the relationship is created.
You could then actually get rid of the last_checkout and last_activity fields entirely and use something like the following:
class ItemObject(models.Model):
...
def _last_checkout(self):
try:
return self.history.filter(activity=ItemHistory.CHECKED_OUT)[0].timestamp
except IndexError:
return None
last_checkout = property(_last_checkout)
def _last_activity(self):
try:
return self.history.all()[0].timestamp
except IndexError:
return None
last_activity = property(_last_activity)
And, you can then use them as normal:
some_item.last_checkout
Finally, your checkout method is not an override of save so it's not appropriate to call super(ItemObject, self).save(). Just use self.save() instead.
I have a situation where I want to use the Meta options of unique_together to enforce a certain rule, here's the intermediary model:
class UserProfileExtension(models.Model):
extension = models.ForeignKey(Extension, unique=False)
userprofile = models.ForeignKey(UserProfile, unique=False)
user = models.ForeignKey(User, unique=False)
class Meta:
unique_together = (("userprofile", "extension"),
("user", "extension"),
# How can I enforce UserProfile's Client
# and Extension to be unique? This obviously
# doesn't work, but is this idea possible without
# creating another FK in my intermediary model
("userprofile__client", "extension"))
and here's UserProfile:
class UserProfile(models.Model):
user = models.ForeignKey(User, unique=True)
client = models.ForeignKey(Client)
Thanks.
You can't.
The unique_together clause is directly translated to the SQL unique index. And you can only set those on columns of a single table, not a combination of several tables.
You can add validation for it yourself though, simply overwrite the validate_unique method and add this validation to it.
Docs: http://docs.djangoproject.com/en/dev/ref/models/instances/#django.db.models.Model.validate_unique
My 2 cents, complementing the accepted response from #Wolph
You can add validation for it yourself though, simply overwrite the validate_unique method and add this validation to it.
This is a working example code someone could find usefull.
from django.core.exceptions import ValidationError
class MyModel(models.Model):
fk = models.ForeignKey(AnotherModel, on_delete=models.CASCADE)
my_field = models.CharField(...) # whatever
def validate_unique(self, *args, **kwargs):
super().validate_unique(*args, **kwargs)
if self.__class__.objects.\
filter(fk=self.fk, my_field=self.my_field).\
exists():
raise ValidationError(
message='MyModel with this (fk, my_field) already exists.',
code='unique_together',
)
My solution was to use Django's get_or_create. By using get_or_create, a useless get will occur if the row already exists in the database, and the row will be created if it does not exist.
Example:
extension = Extension.objects.get(pk=someExtensionPK)
userProfile = UserProfile.objects.get(pk=someUserProfilePK)
UserProfileExtension.objects.get_or_create(extension=extension, userprofile=userProfile)
From django 2.2+ versions, it is suggested to use constraint & Index as model class meta option:
https://docs.djangoproject.com/en/3.2/ref/models/options/#django.db.models.Options.unique_together
https://docs.djangoproject.com/en/3.2/ref/models/options/#django.db.models.Options.constraints
class UniqueConstraintModel(models.Model):
race_name = models.CharField(max_length=100)
position = models.IntegerField()
global_id = models.IntegerField()
fancy_conditions = models.IntegerField(null=True)
class Meta:
constraints = [
models.UniqueConstraint(
name="unique_constraint_model_global_id_uniq",
fields=('global_id',),
),
models.UniqueConstraint(
name="unique_constraint_model_fancy_1_uniq",
fields=('fancy_conditions',),
condition=models.Q(global_id__lte=1)
),
models.UniqueConstraint(
name="unique_constraint_model_fancy_3_uniq",
fields=('fancy_conditions',),
condition=models.Q(global_id__gte=3)
),
models.UniqueConstraint(
name="unique_constraint_model_together_uniq",
fields=('race_name', 'position'),
condition=models.Q(race_name='example'),
)
]
You need to call Models.full_clean() method to call validate_unique for foreignKey. You can override save() to call this
class UserProfileExtension(models.Model):
extension = models.ForeignKey(Extension, unique=False)
userprofile = models.ForeignKey(UserProfile, unique=False)
user = models.ForeignKey(User, unique=False)
def save(self, *args, **kwargs):
self.full_clean()
super().save(*args, **kwargs)
class Meta:
unique_together = (("userprofile", "extension"),
("user", "extension"),
# How can I enforce UserProfile's Client
# and Extension to be unique? This obviously
# doesn't work, but is this idea possible without
# creating another FK in my intermediary model
("userprofile__client", "extension"))
from django.core.exceptions import ValidationError
.....
class UserProfileExtension(models.Model):
extension = models.ForeignKey(Extension, unique=False)
userprofile = models.ForeignKey(UserProfile, unique=False)
user = models.ForeignKey(User, unique=False)
def validate_unique(self, *args, **kwargs):
super(UserProfileExtension, self).validate_unique(*args, **kwargs)
query = UserProfileExtension.objects.filter(extension=self.extension)
if query.filter(userprofile__client=self.userprofile.client).exists():
raise ValidationError({'extension':['Extension already exits for userprofile__client',]})
The first query is to filter all records in UserProfileExtension model which has the same extension we are putting in the current record.
Then we filter the query returned to find if it already contains userprofile__client which we are passing in the current record.
Another possible solution is to add this on your save method from your Model:
def save(self, *args, **kwargs):
unique = self.__class__.objects.filter( extension =self.extension, userprofile=self.userprofile )
if unique.exists():
self.id = unique[0].id
super(self.__class__, self).save(*args, **kwargs)
I just started to learn Django and I had a question.
I'm trying to automatically add the missing information, when saving form data. I get to change/add the desired "cleaned_data" information by overriding save() method of modelform class, but changes are not recorded in the database. Actually, how to write the modified information? This is code:
def save(self, commit = True, *args, **kwargs):
temp = ServiceMethods(url = self.cleaned_data.get('url'), wsdl_url = self.cleaned_data.get('wsdl_url'))
if not temp.get_wsdl_url():
temp.make_wsdl_url()
if temp.get_wsdl_url():
temp.make_wsdl()
self.cleaned_data['wsdl_url'] = temp.get_wsdl_url()
self.cleaned_data['wsdl_description'] = temp.get_wsdl_description()
super(ServiceForm, self).save(commit = commit, *args, **kwargs)
And model:
class Services(models.Model):
name = models.CharField('Имя', max_length=256)
url = models.URLField('Ссылка', unique = True)
wsdl_url = models.URLField('Ссылка на WSDL-документ', blank=True)
description = models.TextField('Описание сервиса',blank=True)
wsdl_description = models.TextField('WSDL описание', blank=True, editable=False)
added = models.DateTimeField('Добавлено', auto_now_add=True)
TIA
Try setting the data on self.instance instead of in self.cleaned_data, and let me know if that works.