Each "time range" entry of the TimeClass is dependent on each other.
They cannot overlap and start_time < end_time.
models.py
class Xyz(models.Model):
...
class TimeRangeClass(models.Model)
start_time = models.TimeField()
end_time = models.TimeField()
xyz = models.ForeignKey(Xyz)
# other fields here
def clean(self):
# Here I loop through TimeRangeClass.objects.all() and
# check for conflicts through my custom "my_validator_method".
# If there is a conflict I throw an error
#(I've since modified it to just be one single query as per Titusz advice)
for each in TimeRangeClass.objects.filter(xyz=self.xyz).exclude(id=self.id):
my_validator_method(start_time1=self.start_time,
end_time1=self.end_time,
start_time2=each.start_time,
end_time2=each.end_time)
admin.py
from .models import TimeRangeClass, Xyz
class TimeRangeClassInLine(admin.TabularInline):
model = TimeRangeClass
extra = 3
#admin.register(Xyz)
class Xyz(admin.ModelAdmin):
exclude = []
inlines = [TimeRangeClassInLine]
Problem: I can edit/add multiple TimeRangeClass's at once through the admin. But given that the models.Model clean method only evaluates 1 change at a time I can't validate multiple edits against each other.
Example:
Save an Entry1 & Entry2 without conflict
Change Entry2 to produce a validation error
Adjust Entry1 (instead of #2) so they do not overlap
This doesn't register because neither changes are written to the db.
I'm looking for a workaround.
Some hints on the problem:
You should not iterate over the full table when checking for overlapping rows. Just filter for the problematic rows... something like:
overlaps = TimeRangeClass.objects.filter(
Q(start_time__gte=self.start_time, start_time__lt=self.end_time) |
Q(end_time__gt=self.start_time, end_time__lte=self.end_time)
)
overlaps is now a queryset that evaluates when you iterate over it and only returns the conflicting objects.
If you are using Django with postgres you should check out https://docs.djangoproject.com/es/1.9/ref/contrib/postgres/fields/#datetimerangefield.
Once you have the conflicting objects you should be able to change their start and end times within the function and save the changes. Model.save() will not automatically call the model.clean() method. But be aware, if you save an object from the Django admin it will call the model.clean() method before saving.
So something like that:
def clean():
overlaps = TimeRangeClass.overlaps.for_trc(self)
for trc_object in overlaps:
fixed_object = fix_start_end(trc_object, self)
fixed_object.save()
If you feel brave you should also read up on transactions to make the mutation of multiple objects in the database all succeed or all fail and nothing in between.
def clean():
with transaction.atomic():
# do your multi object magic here ...
Update on clarified question:
If you want to validate or pre/process data that comes from admin inlines you have to hook into the corresponding ModelAdmin method(s). There are multiple ways to approach this. I guess the easiest would be to override ModelAdmin.save_fromset. Here you have access to all the inlineforms before they have been saved.
Related
I want to change the default value of an attribute from a model in Django. So I want to update the existing values in the database. Strange enough, a bulk update doesn't change those values.
My model:
class UserSettings(models.Model):
offline_notification_filter = models.BooleanField(default=False)
My test
class TestSetOfflineNotificationMigration(APITestCase):
def test_set_offline_notification_filter_to_false(self):
user_settings_1 = UserSettingsFactory(offline_notification_filter=True)
user_settings_2 = UserSettingsFactory(offline_notification_filter=False)
user_settings_3 = UserSettingsFactory(offline_notification_filter=True)
user_settings_4 = UserSettingsFactory()
all_user_settings = UserSettings.objects.all()
for user_setting in all_user_settings:
user_setting.offline_notification_filter = False
UserSettings.objects.bulk_update(
all_user_settings, ["offline_notification_filter"]
)
self.assertEqual(user_settings_1.offline_notification_filter, False)
This test is failing because the the offlince_notification_filter is not updating. Anyone knows why not?
I think you are working with outdated instance, so you might need to user_settings_1.refresh_from_db().
bulk_update() is not necessarily needed in this case. Since this update operation is being performed directly on all objects of queryset, so just using update() will automatically make changes in a single operation.
Bulk update is more suited to operations where instances of different models are being updated.
However if you still prefer to use bulk_update, then here's the answer:
user_settings_1 is an in-memory instance here, whereas bulk_update operation has made the change in database. You need to refresh it from database.
As stated in the docs: https://docs.djangoproject.com/en/dev/ref/models/querysets/#django.db.models.query.QuerySet.bulk_update
You need to provide an array of the models to update, and specify the fields you want to update.
I would suggest the following code:
class TestSetOfflineNotificationMigration(APITestCase):
def test_set_offline_notification_filter_to_false(self):
user_settings_1 = UserSettingsFactory(offline_notification_filter=True)
user_settings_2 = UserSettingsFactory(offline_notification_filter=False)
user_settings_3 = UserSettingsFactory(offline_notification_filter=True)
user_settings_4 = UserSettingsFactory()
all_user_settings = UserSettings.objects.all()
# Create list to append new models
new_user_settings = []
for user_setting in all_user_settings:
# Create new model with the updated fields
new_user_setting = UserSettings(offline_notification_filter=False)
# Append
new_user_settings.append(new_user_setting)
# Response is amount of models updated
q = UserSettings.objects.bulk_update(
new_user_settings, ["offline_notification_filter"]
)
self.assertEqual(q, 4)
If you have a long set of fields to update, you can update the fields with the _meta method and create the list of fields to update like:
# Get list of fields
fields = [f.name for f in UserSettings._meta.fields]
# delete the ones you don't want to update with Sets
list_of_fields = fields - {"id"}
q = UserSettings.objects.bulk_update(
new_user_settings, list_of_fields
)
I have a foreign key relationship in my Django (v3) models:
class Example(models.Model):
title = models.CharField(max_length=200) # this is irrelevant for the question here
not_before = models.DateTimeField(auto_now_add=True)
...
class ExampleItem(models.Model):
myParent = models.ForeignKey(Example, on_delete=models.CASCADE)
execution_date = models.DateTimeField(auto_now_add=True)
....
Can I have code running/triggered whenever an ExampleItem is "added to the list of items in an Example instance"? What I would like to do is run some checks and, depending on the concrete Example instance possibly alter the ExampleItem before saving it.
To illustrate
Let's say the Example's class not_before date dictates that the ExampleItem's execution_date must not be before not_before I would like to check if the "to be saved" ExampleItem's execution_date violates this condition. If so, I would want to either change the execution_date to make it "valid" or throw an exception (whichever is easier). The same is true for a duplicate execution_date (i.e. if the respective Example already has an ExampleItem with the same execution_date).
So, in a view, I have code like the following:
def doit(request, example_id):
# get the relevant `Example` object
example = get_object_or_404(Example, pk=example_id)
# create a new `ExampleItem`
itm = ExampleItem()
# set the item's parent
itm.myParent = example # <- this should trigger my validation code!
itm.save() # <- (or this???)
The thing is, this view is not the only way to create new ExampleItems; I also have an API for example that can do the same (let alone that a user could potentially "add ExampleItems manually via REPL). Preferably the validation code must not be duplicated in all the places where new ExampleItems can be created.
I was looking into Signals (Django docu), specifically pre_save and post_save (of ExampleItem) but I think pre_save is too early while post_save is too late... Also m2m_changed looks interesting, but I do not have a many-to-many relationship.
What would be the best/correct way to handle these requirements? They seem to be rather common, I imagine. Do I have to restructure my model?
The obvious solution here is to put this code in the ExampleItem.save() method - just beware that Model.save() is not invoked by some queryset bulk operations.
Using signals handlers on your own app's models is actually an antipattern - the goal of signal is to allow for your app to hook into other app's lifecycle without having to change those other apps code.
Also (unrelated but), you can populate your newly created models instances directly via their initializers ie:
itm = ExampleItem(myParent=example)
itm.save()
and you can even save them directly:
# creates a new instance, populate it AND save it
itm = ExampleItem.objects.create(myParent=example)
This will still invoke your model's save method so it's safe for your use case.
Here is an abstract base class for many of my "Treatment" models (TreatmentA, TreatmentB, etc):
class TreatmentBase(models.Model):
URL_PREFIX = '' # child classes define this string
code = shared.models.common.RandomCharField(length=6)
class Meta:
abstract = True
Each Treatment instance has a URL, that when visited by a user, takes them to a page specific to that treatment. I want to be able to create a Treatment in Django Admin, and immediately get this URL so I can send it to users. This URL can be created with the following method on TreatmentBase:
def get_url(self):
return '{}/{}/'.format(self.URL_PREFIX, self.code)
However, I am stuck with how to get this URL to display in Django Admin. I can think of the following solutions:
(1) Customize the display of the code field so that it becomes a clickable URL. Problem: I don't know how to do this.
(2) Add the get_url method to ModelAdmin.list_display. Problem: This means I would have to define a separate list_display for each of the child models of BaseTreatment, and I would have to explicitly list all the fields of the model, meaning I have to update it every time I modify a model, violating DRY.
(3) Add an extra field like this:
url = models.URLField(default = get_url)
Problem: get_url is an instance method (since it needs to refer to the self.code field), and from my reading of the docs about the default argument, it just has to be a simple callable without arguments.
Any way to do this seemingly simple task?
You could go with option 2 (adding to the admin display) but add it to the
readonly_fields which may alleviate your DRY concerns when models changes.
Option 3 (the extra field) could also work if you override the save method setting the URL property. You'd either want to set the field as readonly in the admin or only set the value in the save method if it's currently None.
View:
transfer_details = TransferDetail.objects.filter(user=request.user).select_related('transfermethod_set')
print transfer_details.filter(method__name='PayPal')
Models:
class TransferMethod(models.Model):
name = models.CharField(max_length=30)
...
class TransferDetail(models.Model):
data = models.TextField()
...
method = models.ForeignKey(TransferMethod)
user = models.ForeignKey(User)
I expect transfer_details QuerySet from line one to be used without further database calls.
What I am missing?
UPDATE 1
So I discovered when I have these two lines there are no additional queries:
x = transfer_details.filter(method__name='PayPal')
x2 = transfer_details.filter(method__name='Something')
But when I add the following two lines, it's making 2 DB queries:
list(x[:1])
list(x2[:1])
What's happening under the hood and how I can avoid the extra calls?
UPDATE 2
I tried:
transfer_details.get(method__name='PayPal').data
...
It's also making two queries.
Correctly it should be (assuming you also want to get the user data in one query):
transfer_details = TransferDetail.objects.filter(
user=request.user).select_related('method', 'user')
You wouldn't need to select method because when you filter for it in print transfer_details.filter(method__name='PayPal') it should get selected automatically. When you call print TansferDetail's __unicode__ will get invoked, so a reason for additional could be that you're outputting some other related data there (eg. from the Usermodel, which should be solved with the code above...).
To answer your edited question: If you call list on a queryset the queryset gets evaluated, which means the actual query is made.
Don't know if you are accessing request.user at some point before in your code, but if that is not the case it's possible that the second query is the result of getting the user for the current request.
I've been working through an ordered ManyToManyField widget, and have the front-end aspect of it working nicely:
Unfortunately, I'm having a great deal of trouble getting the backend working. The obvious way to hook up the backend is to use a through table keyed off a model with ForeignKeys to both sides of the relationship and overwrite the save method. This would work great, except that due to idiosyncrasies of the content, it is an absolute requirement that this widget be placed in a fieldset (using the ModelAdmin fieldsets property), which is apparently not possible.
I'm out of ideas. Any suggestions?
Thanks!
In regard to how to set up the models, you're right in that a through table with an "order" column is the ideal way to represent it. You're also right in that Django will not let you refer to that relationship in a fieldset. The trick to cracking this problem is to remember that the field names you specify in the "fieldsets" or "fields" of a ModelAdmin do not actually refer to the fields of the Model, but to the fields of the ModelForm, which we are free to override to our heart's delight. With many2many fields, this gets tricky, but bear with me:
Let's say you're trying to represent contests and competitors that compete in them, with an ordered many2many between contests and competitors where the order represents the competitors' ranking in that contest. Your models.py would then look like this:
from django.db import models
class Contest(models.Model):
name = models.CharField(max_length=50)
# More fields here, if you like.
contestants = models.ManyToManyField('Contestant', through='ContestResults')
class Contestant(models.Model):
name = models.CharField(max_length=50)
class ContestResults(models.Model):
contest = models.ForeignKey(Contest)
contestant = models.ForeignKey(Contestant)
rank = models.IntegerField()
Hopefully, this is similar to what you're dealing with. Now, for the admin. I've written an example admin.py with plenty of comments to explain what's happening, but here's a summary to help you along:
Since I don't have the code to the ordered m2m widget you've written, I've used a placeholder dummy widget that simply inherits from TextInput. The input holds a comma-separated list (without spaces) of contestant IDs, and the order of their appearance in the string determines the value of their "rank" column in the ContestResults model.
What happens is that we override the default ModelForm for Contest with our own, and then define a "results" field inside it (we can't call the field "contestants", since there would be a name conflict with the m2m field in the model). We then override __init__(), which is called when the form is displayed in the admin, so we can fetch any ContestResults that may have already been defined for the Contest, and use them to populate the widget. We also override save(), so that we can in turn get the data from the widget and create the needed ContestResults.
Note that for the sake of simplicity this example omits things like validation of the data from the widget, so things will break if you try to type in anything unexpected in the text input. Also, the code for creating the ContestResults is quite simplistic, and could be greatly improved upon.
I should also add that I've actually ran this code and verified that it works.
from django import forms
from django.contrib import admin
from models import Contest, Contestant, ContestResults
# Generates a function that sequentially calls the two functions that were
# passed to it
def func_concat(old_func, new_func):
def function():
old_func()
new_func()
return function
# A dummy widget to be replaced with your own.
class OrderedManyToManyWidget(forms.widgets.TextInput):
pass
# A simple CharField that shows a comma-separated list of contestant IDs.
class ResultsField(forms.CharField):
widget = OrderedManyToManyWidget()
class ContestAdminForm(forms.models.ModelForm):
# Any fields declared here can be referred to in the "fieldsets" or
# "fields" of the ModelAdmin. It is crucial that our custom field does not
# use the same name as the m2m field field in the model ("contestants" in
# our example).
results = ResultsField()
# Be sure to specify your model here.
class Meta:
model = Contest
# Override init so we can populate the form field with the existing data.
def __init__(self, *args, **kwargs):
instance = kwargs.get('instance', None)
# See if we are editing an existing Contest. If not, there is nothing
# to be done.
if instance and instance.pk:
# Get a list of all the IDs of the contestants already specified
# for this contest.
contestants = ContestResults.objects.filter(contest=instance).order_by('rank').values_list('contestant_id', flat=True)
# Make them into a comma-separated string, and put them in our
# custom field.
self.base_fields['results'].initial = ','.join(map(str, contestants))
# Depending on how you've written your widget, you can pass things
# like a list of available contestants to it here, if necessary.
super(ContestAdminForm, self).__init__(*args, **kwargs)
def save(self, *args, **kwargs):
# This "commit" business complicates things somewhat. When true, it
# means that the model instance will actually be saved and all is
# good. When false, save() returns an unsaved instance of the model.
# When save() calls are made by the Django admin, commit is pretty
# much invariably false, though I'm not sure why. This is a problem
# because when creating a new Contest instance, it needs to have been
# saved in the DB and have a PK, before we can create ContestResults.
# Fortunately, all models have a built-in method called save_m2m()
# which will always be executed after save(), and we can append our
# ContestResults-creating code to the existing same_m2m() method.
commit = kwargs.get('commit', True)
# Save the Contest and get an instance of the saved model
instance = super(ContestAdminForm, self).save(*args, **kwargs)
# This is known as a lexical closure, which means that if we store
# this function and execute it later on, it will execute in the same
# context (i.e. it will have access to the current instance and self).
def save_m2m():
# This is really naive code and should be improved upon,
# especially in terms of validation, but the basic gist is to make
# the needed ContestResults. For now, we'll just delete any
# existing ContestResults for this Contest and create them anew.
ContestResults.objects.filter(contest=instance).delete()
# Make a list of (rank, contestant ID) tuples from the comma-
# -separated list of contestant IDs we get from the results field.
formdata = enumerate(map(int, self.cleaned_data['results'].split(',')), 1)
for rank, contestant in formdata:
ContestResults.objects.create(contest=instance, contestant_id=contestant, rank=rank)
if commit:
# If we're committing (fat chance), simply run the closure.
save_m2m()
else:
# Using a function concatenator, ensure our save_m2m closure is
# called after the existing save_m2m function (which will be
# called later on if commit is False).
self.save_m2m = func_concat(self.save_m2m, save_m2m)
# Return the instance like a good save() method.
return instance
class ContestAdmin(admin.ModelAdmin):
# The precious fieldsets.
fieldsets = (
('Basic Info', {
'fields': ('name', 'results',)
}),)
# Here's where we override our form
form = ContestAdminForm
admin.site.register(Contest, ContestAdmin)
In case you're wondering, I had ran into this problem myself on a project I've been working on, so most of this code comes from that project. I hope you find it useful.