I have set up two models where one has a foreign key relationship to the other. Since I have multiple databases I also set up a routers.py file as both databases live in the same application. I have been reading and following the documenation found here and here and as far as I can tell I am following the documentation to the letter.
models.py
class Customers(models.Model):
customer_id = models.AutoField(primary_key=True)
name = models.CharField(max_length=100)
active = models.IntegerField()
class Meta:
db_table = 'customers'
app_label = 'db1'
class Users(models.Model):
user_id = models.AutoField(primary_key=True)
customer = models.ForeignKey(Customers, models.CASCADE)
username = models.CharField(max_length=35)
role = models.ForeignKey(Roles, models.CASCADE)
active = models.IntegerField()
class Meta:
db_table = 'users'
app_label = 'db1'
routers.py
class BackendRouter(object):
def db_for_read(self, model, **hints):
if model._meta.app_label == 'db1':
return 'default'
elif model._meta.app_label == 'db2':
return 'db2'
return None
def db_for_write(self, model, **hints):
if model._meta.app_label == 'db1':
return 'default'
elif model._meta.app_label == 'db2':
return 'db2'
return None
def allow_relation(self, obj1, obj2, **hints):
if obj1._meta.app_label == 'db1' or \
obj2._meta.app_label == 'db1':
return True
elif obj1._meta.app_label == 'db2' or \
obj2._meta.app_label == 'db2':
return True
return None
def allow_migrate(self, db, app_label, model_name=None, **hints):
if app_label == 'db1':
return db == 'default'
elif app_label == 'db2':
return db == 'db2'
return None
Ignore for a second that I have a users table without deriving from AbstractBaseUser, when I run the following query:
Customers.objects.filter(users__username__contains='sl')
I get the following error:
django.core.exceptions.FieldError: Cannot resolve keyword 'users' into
field. Choices are: active, customer_id, name
Per the example in the documentation:
Blog.objects.filter(entry__headline__contains='Lennon')
This should work. What am I missing here?
As a point of reference I have already tried the following as well:
Customers.objects.filter(users_set__username__contains=‘sl’)
As well as setting related_name='users' on the customers ForeignKey.
Based on the docs, it seems I should be able to filter the way I have in my question and not run into an issue. However I am not sure if I can actually filter on more than a single object when doing "reverse" relations. I did achieve what I wanted with the following solution:
user = Users.objects.filter(username='someuser').only('customer_id')
customer = Customers.objects.filter(customer_id__in=user)
I am sure there is a more efficient way though. Like such:
Users.objects.filter(username='someuser').values_list('customer__name', flat=True)[0]
I just figured it would be better if I could get what I need via something like:
Customers.objects.filter(users__username='someuser').values_list('name', flat=True)[0]
It actually appears that this was caused by both of my models living in the same application. Once I split them out into there own seperate apps, reverse relationships began to work.
Related
dbrouter.py
class DbRouter(object):
mhris = [HrUserMstr]
profocus_db = [VendorEntry, ApVendorMt, ApVendorDt, ApVendorCertificateDt]
apps_ppl = []
def db_for_read(self, model, **hints):
if model in self.profocus_db:
return 'PROFOCUS_DB'
elif model in self.apps_ppl:
return 'APPS_PPPL'
elif model in self.mhris:
return 'MHRIS'
else:
return 'default'
def db_for_write(self, model, **hints):
if model in self.profocus_db:
return 'PROFOCUS_DB'
elif model in self.apps_ppl:
return 'APPS_PPPL'
elif model in self.mhris:
return 'MHRIS'
else:
return 'default'
def allow_migrate(self, db, app_label, model_name=None, **hints):
if model_name in self.profocus_db:
return 'PROFOCUS_DB'
elif model_name in self.apps_ppl:
return 'APPS_PPPL'
elif model_name in self.mhris:
return 'MHRIS'
else:
return 'default'
models.py
class ApVendorDt(models.Model):
certificate = models.ForeignKey(ApVendorCertificateDt, on_delete=models.CASCADE)
vendor_code = models.CharField(max_length=30, default='')
vendor_doc = models.FileField(upload_to='vendor_doc/')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.vendor_id
class Meta:
db_table = 'APVENDOR_DT'
db_for_write and db_for_read functions are working fine but allow_migrate is not working. I'm trying to create a new table using django model it is creating table in default database But I want to create table in only profocus_db which is my secondary database.
django 4.0.1
I'm saving a list of values for each user on my Django app, taken from a daily poll. I can't get the 'User' field responder to save properly as it always opts for the default value 'Null'. Instead of a list of values for each user, I am getting a single list in the database for all the users combined.
Here is my model:
class Repondez(Model):
score_list = JSONField()
responder = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True, blank=True)
def __str__(self):
return self.score_list
Here is views.py:
r = Repondez()
r.score_list = []
global list_length
def home(request):
context = {}
return render(request, 'rating/home.html', context)
#login_required
def add(request):
if request.method == 'POST':
selected_option = (request.POST['rating'])
list_length = len(r.score_list)
if selected_option == 'option1':
r.score_list.append(1)
elif selected_option == 'option2':
r.score_list.append(2)
elif selected_option == 'option3':
r.score_list.append(3)
elif selected_option == 'option4':
r.score_list.append(4)
elif selected_option == 'option5':
r.score_list.append(5)
else:
return HttpResponse(400, 'Invalid form')
r.save()
return redirect(add)
context = {
'r' : r,
}
return render(request, 'rating/add.html', context)
I have tried to avoid having a default value for responder but am always asked for one during migrate. I tried a ForeignKey and ManytoManyField for the 'User' relationship as well but in each case I am getting a single, combined list for all users. How can I save the responder field properly so each list is saved for their respective user?
In your add view you need to set the responder field for the instance:
r.responder = request.user
r.save()
Ok to solve the list issue you are having I would change the model to use a foreign key to the user instead of onetoone field
class Respondez(models.Model):
class Meta:
ordering = (‘day’,)
responder = models.ForeignKey(
settings.AUTH_USER_MODEL
related_name=‘scores’)
score = models.IntegerField()
day = models.DateField(auto_add_now=True)
Then access the scores from the user instance like so:
request.user.scores.all()
You can also access the score list using values_list:
request.user.scores.all().values_list(‘score’, flat=True)
DjangoVersion:2.1.7
PythonVersion:3.8.2
I have a StoreLocation model which has ForeignKeys to Store, City and Site (this is the default DjangoSitesFramework model). I have created TabularInline for StoreLocation and added it to the Store admin. everything works fine and there isn't any problem with these basic parts.
now I'm trying to optimize my admin database queries, therefore I realized that StoreLocationInline will hit database 3 times for each StoreLocation inline data.
I'm using raw_id_fields, managed to override get_queryset and select_related or prefetch_related on StoreLocationInline, StoreAdmin and even in StoreLocationManager, i have tried BaseInlineFormsSet to create custom formset for StoreLocationInline. None of them worked.
Store Model:
class Store(models.AbstractBaseModel):
name = models.CharField(max_length=50)
description = models.TextField(blank=True)
def __str__(self):
return '[{}] {}'.format(self.id, self.name)
City Model:
class City(models.AbstractBaseModel, models.AbstractLatitudeLongitudeModel):
province = models.ForeignKey('locations.Province', on_delete=models.CASCADE, related_name='cities')
name = models.CharField(max_length=200)
has_shipping = models.BooleanField(default=False)
class Meta:
unique_together = ('province', 'name')
def __str__(self):
return '[{}] {}'.format(self.id, self.name)
StoreLocation model with manager:
class StoreLocationManager(models.Manager):
def get_queryset(self):
return super().get_queryset().select_related('store', 'city', 'site')
class StoreLocation(models.AbstractBaseModel):
store = models.ForeignKey('stores.Store', on_delete=models.CASCADE, related_name='locations')
city = models.ForeignKey('locations.City', on_delete=models.CASCADE, related_name='store_locations')
site = models.ForeignKey(models.Site, on_delete=models.CASCADE, related_name='store_locations')
has_shipping = models.BooleanField(default=False)
# i have tried without manager too
objects = StoreLocationManager()
class Meta:
unique_together = ('store', 'city', 'site')
def __str__(self):
return '[{}] {} / {} / {}'.format(self.id, self.store.name, self.city.name, self.site.name)
# removing shipping_status property does not affect anything for this problem.
#property
def shipping_status(self):
return self.has_shipping and self.city.has_shipping
StoreLocationInline with custom FormSet:
class StoreLocationFormSet(BaseInlineFormSet):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.queryset = self.queryset.select_related('site', 'city', 'store')
class StoreLocationInline(admin.TabularInline):
model = StoreLocation
# i have tried without formset and multiple combinations too
formset = StoreLocationFormset
fk_name = 'store'
extra = 1
raw_id_fields = ('city', 'site')
fields = ('is_active', 'has_shipping', 'site', 'city')
# i have tried without get_queryset and multiple combinations too
def get_queryset(self, request):
return super().get_queryset(request).select_related('site', 'city', 'store')
StoreAdmin:
class StoreAdmin(AbstractBaseAdmin):
list_display = ('id', 'name') + AbstractBaseAdmin.default_list_display
search_fields = ('id', 'name')
inlines = (StoreLocationInline,)
# i have tried without get_queryset and multiple combinations too
def get_queryset(self, request):
return super().get_queryset(request).prefetch_related('locations', 'locations__city', 'locations__site')
so when i check Store admin change form, it will do this for each StoreLocationInline item:
DEBUG 2020-04-13 09:32:23,201 [utils:109] (0.000) SELECT `locations_city`.`id`, `locations_city`.`is_active`, `locations_city`.`priority`, `locations_city`.`created_at`, `locations_city`.`updated_at`, `locations_city`.`latitude`, `locations_city`.`longitude`, `locations_city`.`province_id`, `locations_city`.`name`, `locations_city`.`has_shipping`, `locations_city`.`is_default` FROM `locations_city` WHERE `locations_city`.`id` = 110; args=(110,)
DEBUG 2020-04-13 09:32:23,210 [utils:109] (0.000) SELECT `django_site`.`id`, `django_site`.`domain`, `django_site`.`name` FROM `django_site` WHERE `django_site`.`id` = 4; args=(4,)
DEBUG 2020-04-13 09:32:23,240 [utils:109] (0.000) SELECT `stores_store`.`id`, `stores_store`.`is_active`, `stores_store`.`priority`, `stores_store`.`created_at`, `stores_store`.`updated_at`, `stores_store`.`name`, `stores_store`.`description` FROM `stores_store` WHERE `stores_store`.`id` = 22; args=(22,)
after i have added store to the select_related, the last query for store disappeared. so now i have 2 queries for each StoreLocation.
I have checked the following questions too:
Django Inline for ManyToMany generate duplicate queries
Django admin inline: select_related
Problem is caused by ForeignKeyRawIdWidget.label_and_url_for_value()
as #AndreyKhoronko mentioned in comments, this problem is caused by ForeignKeyRawIdWidget.label_and_url_for_value() located in django.contrib.admin.widgets which will hit database with that key to generate a link to admin change form.
class ForeignKeyRawIdWidget(forms.TextInput):
# ...
def label_and_url_for_value(self, value):
key = self.rel.get_related_field().name
try:
obj = self.rel.model._default_manager.using(self.db).get(**{key: value})
except (ValueError, self.rel.model.DoesNotExist, ValidationError):
return '', ''
try:
url = reverse(
'%s:%s_%s_change' % (
self.admin_site.name,
obj._meta.app_label,
obj._meta.object_name.lower(),
),
args=(obj.pk,)
)
except NoReverseMatch:
url = '' # Admin not registered for target model.
return Truncator(obj).words(14, truncate='...'), url
So I have a modelformset for a House model, which has an owners manytomany field, I'm excluding the owners field due to the fact that I want it to just automatically save the currently logged in users id as the owner - I could hack this with hidden fields, but would rather know how it's properly done.
For clarity, the Integrity Error is that owner_id can not be null, my attempt at fixing it by hardcoding just to see failed in the beginning of manage_houses
views.py
def manage_houses(request):
HousesFormSet = modelformset_factory(House, form=ManageHousesForm)
if request.method == 'POST':
formset = HousesFormSet(request.POST)
# failed attempt at fixing integrity error
for form in formset:
form.owner_id = 1
if formset.is_valid():
if formset.save():
notice = "Success! Your houses were updated in the system."
notice_type = "success"
elif not formset.has_changed():
pass
else:
notice = "Something went wrong! Your houses may not have been updated."
notice_type = "error"
else:
formset = SpecialsFormSet()
response_details = { 'formset': formset,
'fields': ManageHousesForm.base_fields }
try:
response_details['notice'] = notice
response_details['notice_type'] = notice_type
except NameError:
pass
return render_to_response('houses/manage.djhtml', response_details)
models.py
class House(models.Model):
class Meta:
app_label = 'houses'
# Fields
owners = models.ManyToManyField(User)
name = models.CharField(max_length=50)
slug = models.SlugField(max_length=100, blank=True, editable=False, unique=True)
address = models.CharField(max_length=75)
def __unicode__(self):
return self.name
According to the Django Documentation, this is what inline formsets are for:
Inline formsets is a small abstraction layer on top of model formsets.
These simplify the case of working with related objects via a foreign
key.
I have a custom django admin page, and I would like to make the two ForeignKey fields optional in the admin interface. I do not want to change the underlying model.
This is the model:
class IncorporationTicket(models.Model, AdminURL):
ordered_by = models.ForeignKey('Organisation', # organisation which ordered this
null = True,
blank = False, # i.e. can only be null as a result of delete
on_delete = models.SET_NULL)
ordered_by_individual = models.ForeignKey('Individual', # individual at organisation which ordered this
null = True,
blank = False, # i.e. can only be null as a result of delete
on_delete = models.SET_NULL)
(AdminURL is a mixin which provides get_absolute_url)
This is the ModelAdmin:
class TicketAdmin(admin.ModelAdmin):
readonly_fields = ('ordered', 'charge', 'amount_paid', 'submitted_on')
formfield_overrides = {
models.ForeignKey: {'required': False},
}
def formfield_for_foreignkey(self, db_field, request, **kwargs):
pk = resolve(request.path).args[0] # the request url should only have one arg, the pk
instance = self.get_object(request, pk)
user = request.user
kwargs['required'] = False # will be passed to every field
if db_field.name == "ordered_by_individual":
# queryset should be a union of (a) individual already set on object (b) individual for current user
## None option is provided by admin interface - just need to let field be optional.
if instance.ordered_by_individual:
kwargs["queryset"] = (
Individual.objects.filter(pk = instance.ordered_by_individual.pk) |
user.individual_set.all())
else: kwargs["queryset"] = user.individual_set.all()
elif db_field.name == "ordered_by":
# queryset should be a union of (a) organisation already set (b) any organisations for which user is authorised
try:
individual = user.individual_set.all()[0]
all_orgs = Organisation.all_organisations_for_which_individual_authorised_to_incorporate(individual)
except:
all_orgs = Organisation.objects.none()
if instance.ordered_by:
kwargs["queryset"] = (
Organisation.objects.filter(pk = instance.ordered_by.pk) |
all_orgs)
else: kwargs["queryset"] = all_orgs
return super(type(self), self).formfield_for_foreignkey(db_field, request, **kwargs)
As you can see, I have tried to use both formfield_overrides, and formfield_for_foreignkey to set required = False on the FormField, but it is not having the required effect: when attempting to save through the admin interface without setting (that is, leaving the field in its original blank state), the admin interface shows the error 'This field is required.'
So, my question is: how does one prevent the underlying form from requiring certain fields, while still also setting the choices in formfield_for_foreignkey?
While I'm not sure why kwargs['required'] wouldn't work, you could always override the admin form with your own form. It hasn't failed me with magical django admin behavior so it's a pretty good bet.
class MyForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
self.fields['my_fk_field'].required = False
class Meta:
model = MyModel
class MyAdmin(admin.ModelAdmin):
form = MyForm
This would still allow you to modify the QuerySet via the formfield_for_foo methods.
... almost 9 years later, in Django v3.1.2 ...
blank=True works fine for me:
from django.contrib.auth.models import User
owner = models.ForeignKey(User,
related_name="notes",
on_delete=models.CASCADE,
null=True,
blank=True)
(the solution has been taken from here)