Use of Prefetch with an inner select_related in Django - django

I basically have to display a list of service providers and in each, I need to display the categories of service they offer.
So as an example:
Possible Service Type Categories:
[id: 1, name:'Programming']
[id: 2, name:'Design']
Possible Service Types:
[id: 1, name: 'PHP Service', service_type_category_id: 1]
[id: 2, name: 'JAVA Service', service_type_category_id: 1]
[id: 3, name: 'Web Design Service', service_type_category_id: 2]
Example of Display Results:
Company Blue offers 'Programming'
Company Test offers 'Programming' and 'Design'
Company Orange offers 'Design' ....
I'm trying to write the least number of queries:
I have these models:
class ServiceTypeCategory( BaseModel ):
# Model Attributes
name = models.CharField( _( "name" ), max_length = 40 )
class ServiceType( BaseModel ):
# Model Attributes
service_type_category = models.ForeignKey( 'ServiceTypeCategory', verbose_name = _( 'category' ) )
name = models.CharField( _( "name" ), max_length = 60 )
description = models.TextField( _( "description" ) )
class Provider( BaseModel ):
# Model Attributes
display_name = models.CharField( _( "name" ), max_length = 80 )
# Many to many relations
countries = models.ManyToManyField( 'core.Country' ) # countries this provider support
service_types = models.ManyToManyField( 'ServiceType', through = 'Provider_ServiceTypes', related_name = 'service_types' )
class Provider_ServiceTypes( BaseModel ):
# Model Attributes
service_type = models.ForeignKey( 'ServiceType', verbose_name = _( 'service type' ) )
provider = models.ForeignKey( 'Provider', verbose_name = _( 'provider' ) )
is_top = models.BooleanField( _( "is top service" ), default = False )
Then, to run the query, I have the following:
providers = Provider.objects.select_related(
'user',
).prefetch_related(
Prefetch(
'service_types__service_type_category',
queryset = ServiceTypeCategory.objects
.only( 'name' )
)
).filter(
countries = country_id,
).only(
'id', 'display_name', 'user'
).order_by(
'-user__last_login'
)
This works out well, but it runs the 3 following queries:
SELECT app_provider.id, app_provider.user_id, app_provider.display_name, core_user.id, core_user.password, core_user.last_login, core_user.is_superuser, core_user.created_date, core_user.modified_date, core_user.email, core_user.name, core_user.is_active, core_user.is_admin
FROM app_provider
INNER JOIN app_provider_countries ON ( app_provider.id = app_provider_countries.provider_id )
INNER JOIN core_user ON ( app_provider.user_id = core_user.id )
LEFT OUTER JOIN core_userpersonal ON ( core_user.id = core_userpersonal.user_id )
LEFT OUTER JOIN core_userstats ON ( core_user.id = core_userstats.user_id )
WHERE app_provider_countries.country_id = 204
ORDER BY core_userstats.total_reviews DESC, core_userstats.total_contracts DESC, core_userstats.total_answers DESC, core_user.last_login DESC LIMIT 5
SELECT (app_provider_servicetypes.provider_id) AS _prefetch_related_val_provider_id, app_servicetype.id, app_servicetype.created_date, app_servicetype.modified_date, app_servicetype.service_type_category_id, app_servicetype.name, app_servicetype.description
FROM app_servicetype
INNER JOIN app_provider_servicetypes ON ( app_servicetype.id = app_provider_servicetypes.service_type_id )
WHERE app_provider_servicetypes.provider_id IN (2)
SELECT app_servicetypecategory.id, app_servicetypecategory.name
FROM app_servicetypecategory
WHERE app_servicetypecategory.id IN (1, 2)
Question is: How can I make to run just 2 queries in total? (The last 2 queries should be joined with INNER JOIN and a group by per service_type_category_name)
Thanks in advance!

Try this:
providers = Provider.objects.select_related(
'user',
).prefetch_related(
Prefetch(
'service_types',
queryset = ServiceType.objects\
.select_related('service_type_category')\
.only( 'service_type_category', 'name' )
)
).filter(
countries = country_id,
).only(
'id', 'display_name', 'user'
).order_by(
'-user__last_login'
)

Related

Tagged union type including ManyToManyField and model constraints

Django ORM seems not to permit constraints on ManyToManyField.
class Component(ComponentMetaData):
class Type(models.IntegerChoices):
Part = 0
Components = 1
type = models.IntegerField(
choices=Type.choices,
)
components = models.ManyToManyField(
"self",
blank=True,
null=True,
)
def clean(self) -> None:
if self.type == 0 and self.components:
raise ValidationError("Parts could not have components.")
return super().clean()
class Meta:
constraints = [
models.CheckConstraint(
check=(
Q(type = 0) &
Q(components__isnull = True)
) | (
Q(type = 1)
# componenets only fields
),
name = "components_enum"
)
]
Migration attempt with the above model results in the error below;
ERRORS:
myapp.Component: (models.E013) 'constraints' refers to a ManyToManyField 'components', but ManyToManyFields are not permitted in 'constraints'.
Does anyone know why this is not permitted and what should one do if one would like to keep a ManyToManyField empty on some condition based on the values of other fields?
UPDATE 2023-01-24
Following the advise of #Selcuk, I tried below.
class Component(ComponentMetaData):
class TypeTag(models.IntegerChoices):
Part = 0
Components = 1
type_tag = models.IntegerField(
choices=TypeTag.choices,
)
child_components = models.ManyToManyField(
"self",
through="ComponentRelation",
blank=True,
null=True,
related_name="parent_component",
symmetrical=False,
)
def clean(self) -> None:
if self.type_tag == 0 and self.components:
raise ValidationError("Parts could not have components.")
return super().clean()
class Meta:
constraints = [
models.CheckConstraint(
check=(
Q(type_tag=0)
# part only fields
) | (
Q(type_tag=1)
# componenets only fields
),
name="component_enum"
)
]
class ComponentRelation(models.Model):
from_component = models.ForeignKey(
"Component",
on_delete=models.CASCADE,
related_name="from_component",
)
to_component = models.ForeignKey(
"component",
on_delete=models.CASCADE,
related_name="to_component",
)
class Meta:
constraints = [
models.CheckConstraint(
check=(
Q(from_component__type_tag = 1)
),
name="component_relation"
)
]
It results following error on migration.
myapp.ComponentRelation: (models.E041) 'constraints' refers to the joined field 'from_component__type_tag'.
It seems model constraint is a mere interface of DB functionality and could only include constraints for a single row, but no inter-table, complex constraints, generally speaking.
Is there any other way to do this??

django exedittabledataview search columns

I am trying to make a search option for my data table in my site,
in models.py I have:
class Mymodel(models.Model):
id = models.AutoField(primary_key=True)
title = models.TextField()
body = models.TextField()
creator = models.CharField(max_length=255, blank=True, db_index = True)
creation_time = models.DateTimeField( db_index = True )
event_id = models.CharField(unique=True, max_length=255)
capture_rule = models.ForeignKey(CaptureRule, db_index = True)
status = models.ForeignKey(EventStatus, null = True, blank=True, db_index = True)
comment = models.TextField( null = True, blank = True )
source_url = models.CharField(max_length=4096, null = True, blank = True )
links = models.ManyToManyField( Link, related_name = 'events' )
and in my views.py:
class edittest(XEditableDatatableView):
model = Event
template_name = 'webapp/index.html'
datatable_options = {
'columns':[( 'Time', 'creation_time', 'get_event_age' ),
( 'Status', 'status', make_xeditable( type='select' ), 'status__name' ),
( 'Creator', 'creator' ),
( 'Source', 'source' ),
( 'Title', 'title' ),
( 'Body', 'body', 'get_body_with_markup' ),
'comment',
'id',
'source_url',
( 'Tag', 'capture_rule__tag__name' ),
( 'Matcher', 'capture_rule__matcher'),
'linked_urls',
'capture_rule'
],
'search_fields': [ 'status__name' ],
'hidden_columns' : ['body', 'comment', 'id', 'source_url', 'linked_urls', 'capture_rule' ],
'ordering':['-creation_time'],
}
and it works ok but when I change the search_fields parameter to
'search_fields': [ 'title' ] or 'search_fields': [ 'comment' ]
it search in the whole columns like if I wrote : 'search_fields': []
That's probably because the search field should not already appear in the column list.
A list of filter-like ORM fields that are always appended to the list
of search fields when a search is performed on the table.
search_fields should only contain ORM paths to fields that aren't
already in the column definitions, since those are already searched by
default.
https://pypi.python.org/pypi/django-datatable-view/0.5.5

list_filter triggering a FieldError

One of my queries suddenly started to fail after I made some changes to the AdminModel.
After searching a bit, I found that adding a list_filter to my admin_model is creating the FieldError, which seems really strange to me...
My models:
class PiafInfo( models.Model ):
no = models.IntegerField( u'No du Piaf', unique=True )
origin_city = models.ForeignKey( City )
...
class PiafInfoAdmin( admin.ModelAdmin ):
list_display = ('no', 'origin_city', 'group', 'fleet')
list_filter = ['origin_city', ]
ordering = ('no',)
search_fields = ('no', 'group', 'fleet')
admin.site.register( PiafInfo, PiafInfoAdmin )
class PiafTrans( models.Model ):
ttype = models.IntegerField( choices=TTYPE_CHOICES, default=TTYPE_RELOAD)
date = models.DateTimeField()
piafSerial = models.ForeignKey( PiafInfo )
...
class PiafTransAdmin( admin.ModelAdmin):
list_display = ('date', 'piafSerial', 'city', 'ttype', 'amount', 'subscrName' )
date_hierarchy = 'date'
list_filter = ('city', 'ttype')
search_fields = ('piafSerial', 'group', 'fleet', 'subscrName' )
admin.site.register( PiafTrans, PiafTransAdmin )
The query posing problem (second line) :
piafInfos = models.PiafInfo.objects.all().distinct()
piafInfos = piafInfos.filter( piaftrans__date__range=(startDate,endDate) ).distinct()
Like that, I get the following error:
FieldError: Cannot resolve keyword 'piaftrans' into field. Choices are: fleet, group, id, no, origin_city
It tries to interpret piaftrans as a field name instead of a model.
If I comment a single line in PiafInfoAdmin:
class PiafInfoAdmin( admin.ModelAdmin ):
list_display = ('no', 'origin_city', 'group', 'fleet')
# list_filter = ['origin_city', ]
ordering = ('no',)
search_fields = ('no', 'group', 'fleet')
admin.site.register( PiafInfo, PiafInfoAdmin )
The error disappears. I don't see any correlation between the error and the change I made!
This is with django 1.3 and Python 2.7 on Windows.
Can't reproduce here. Using
models.py:
class City( models.Model ):
name = models.CharField(max_length=110)
class PiafInfo( models.Model ):
no = models.IntegerField( u'No du Piaf', unique=True )
origin_city = models.ForeignKey( City )
class PiafTrans( models.Model ):
ttype = models.IntegerField()
date = models.DateTimeField()
piafSerial = models.ForeignKey( PiafInfo )
admin.py:
class PiafInfoAdmin( admin.ModelAdmin ):
list_display = ('no', 'origin_city',)
list_filter = ['origin_city', ]
ordering = ('no',)
search_fields = ('no',)
admin.site.register( PiafInfo, PiafInfoAdmin )
class PiafTransAdmin( admin.ModelAdmin):
list_display = ('date', 'piafSerial', 'ttype', )
date_hierarchy = 'date'
list_filter = ('ttype')
search_fields = ('piafSerial', )
admin.site.register( PiafTrans, PiafTransAdmin )
django 1.3.8 pre-alfa (latest from 1.3 branch)
and the following code:
startDate = datetime.today()
endDate = datetime.today()
piafInfos = PiafInfo.objects.all().distinct()
piafInfos = piafInfos.filter( piaftrans__date__range=(startDate,endDate) ).distinct()

(Django) How do I set limit_choices_to another class from where the ManyToManyField points to?

I currently have three classes User, UserProfile, and Vendor. User is the built-in Django one from from django.contrib.auth.models import User. The other two are as follow
Here is UserProfile
class UserProfile( models.Model ) :
TYPE_CHOICES = (
( 't', 'tenant' ),
( 'm', 'property manager' ),
( 'o', 'property owner' ),
( 'v', 'vendor' ),
( 'w', 'web user' ),
( 'x', 'other' ),
)
user = models.ForeignKey( User, unique = True )
user_type = models.CharField( max_length = 1, choices = TYPE_CHOICES, default = 't' )
User.profile = property( lambda u : UserProfile.objects.get_or_create( user = u )[ 0 ] )
And here is Vendor
class Vendor( models.Model ) :
def __unicode__( self ) :
return self.name
name = models.CharField( max_length = 135 )
users = models.ManyToManyField( User, null = True, blank = True )
I want to limit the Vendor.users to only User whose UserProfile.user_type is vendor.
How do I use limit_choices_to. Can I do something like...
users = models.ManyToManyField( User, null = True, blank = True, limit_choices_to = { UserProfile.objects.filter( user = self.users ).user_type : 'm' } )
I know the above code will throw an error, but hopefully you can see what I'm trying to do.
Thanks in advance!
I was able to do it with
manager = models.ForeignKey( User, related_name = 'manager', limit_choices_to = { 'userprofile__user_type' : 2 } )
where 2 is the primary key (user_type_id) of property manager in the UserType class.

How do you have a single select to select two different loosely related models in a django form

I have these models:
OrderLine
StockItem
WaybillItem
Waybill
The Waybill consists of a number of WaybillItem connected to one Order Number.
The WaybillItem consists of a OrderLine & a StockItem
The relation between StockItem & Orderline is a loose many to many dependent on a query from the data of the OrderLine:
StockItem.objects.filter( wh_code = OrderLine.origin_wh_code ).filter( si_code = OrderLine.si_code ).filter( commodity_code = OrderLine.commodity_code )
In the form I would like the user to have a single select to choose the possible options where for each matching StockItem/OrderLine there is one item.
Example:
In OrderLines there is the following OrderLines:
Line_ID, OrderNumber, commodity_code, si_code, origin_wh_code
1001,1, Wheat, 2222,Rome
1002,1, Oat, 2222, Rome
1003,2, Oat, 2222, Rome
In StockItem you have:
Stock_ID, commodity_code, si_code, wh_code, reference_code
10, Wheat, 2222, Rome, 222201
12, Oat, 2222, Rome, 222202
13, Wheat, 2222, Rome, 222203
14, Wheat, 2222, Paris, 222203
This should result in a dropdown for waybillline for waybill with number 1 with:
Line_ID, Stock_ID, (commodity_code, si_code, reference_code)
1001, 10, (Wheat, 2222, 222201
1001, 13, (Wheat, 2222, 222203)
1002, 12, (Oat,2222, 222202)
When selected the Waybill line should have both the Line_ID & Stock_ID saved, but the user is only using a single Dropdown.
I am using inlineformset_factory to create the list of wabill lines.
Can anyone help?
Edited Adding Models View and Template
Models:
The Waybill
class Waybill( models.Model ):
ltiNumber = models.CharField( max_length = 20 )
waybillNumber = models.CharField( max_length = 20 )
dateOfLoading = models.DateField( null = True, blank = True )
recipientLocation = models.CharField( max_length = 100, blank = True )
destinationWarehouse = models.ForeignKey( Places, blank = True )
The Order
class OrderLine( models.Model ):
lti_pk = models.CharField( max_length = 50, primary_key = True, db_column = 'LTI_PK' )
lti_id = models.CharField( max_length = 40, db_column = 'LTI_ID' )
code = models.CharField( max_length = 40, db_column = 'CODE' )
origin_wh_code = models.CharField( max_length = 13, blank = True, db_column = 'ORIGIN_WH_CODE' )
destination_location_code = models.CharField( max_length = 10, db_column = 'DESTINATION_LOCATION_CODE' )
si_code = models.CharField( max_length = 8, db_column = 'SI_CODE' )
commodity_code = models.CharField( max_length = 18, db_column = 'COMMODITY_CODE' )
number_of_units = models.DecimalField( max_digits = 7, decimal_places = 0, db_column = 'NUMBER_OF_UNITS' )
This is the stock model
class StockItem( models.Model ):
wh_pk = models.CharField( max_length = 90, blank = True, primary_key = True )
wh_code = models.CharField( max_length = 13 )
wh_name = models.CharField( max_length = 50, blank = True )
si_code = models.CharField( max_length = 8 )
origin_id = models.CharField( max_length = 23 )
commodity_code = models.CharField( max_length = 18 )
number_of_units = models.IntegerField()
This is the WaybillItem
class WaybillItem( models.Model ):
wbNumber = models.ForeignKey( Waybill )
siNo = models.ForeignKey( OrderLine )
coi_code = models.ForeignKey( StockItem )
numberUnitsLoaded = models.DecimalField( default = 0, blank = False, null = False, max_digits = 10, decimal_places = 3 )
View:
def waybillCreate( request, lti_code ):
#Retrive the LTIs for this Waybill
current_lti = OrderLine.objects.filter( code = lti_code )
for lti in current_lti:
c_sis.append( lti.si_code )
current_stock = StockItem.in_stock_objects.filter( si_code__in = c_sis ).filter( wh_code = current_lti[0].origin_wh_code )
class LoadingDetailDispatchForm( ModelForm ):
#Here are the order lines
siNo = ModelChoiceField( queryset = current_lti, label = 'Commodity' )
#This is from the Stock
coi_code = ModelChoiceField( queryset = current_stock )
overload = forms.BooleanField( required = False )
class Meta:
model = LoadingDetail
fields = ( 'siNo', 'numberUnitsLoaded', 'wbNumber', 'overloadedUnits', 'overOffloadUnits' , 'coi_code' )
LDFormSet = inlineformset_factory( Waybill, LoadingDetail, form = LoadingDetailDispatchForm, fk_name = "wbNumber", formset = BaseLoadingDetailFormFormSet, extra = 5, max_num = 5 )
current_wh = ''
if request.method == 'POST':
form = WaybillForm( request.POST )
form.fields["destinationWarehouse"].queryset = Places.objects.filter( geo_name = current_lti[0].destination_loc_name )
formset = LDFormSet( request.POST )
if form.is_valid() and formset.is_valid():
wb_new = form.save()
instances = formset.save( commit = False )
wb_new.waybillNumber = new_waybill_no( wb_new )
for subform in instances:
subform.wbNumber = wb_new
subform.save()
wb_new.save()
return HttpResponseRedirect( '../viewwb/' + str( wb_new.id ) )
else:
loggit( formset.errors )
loggit( form.errors )
loggit( formset.non_form_errors )
else:
qs = Places.objects.filter( geo_name = current_lti[0].destination_loc_name ).filter( organization_id = current_lti[0].consegnee_code )
if len( qs ) == 0:
qs = Places.objects.filter( geo_name = current_lti[0].destination_loc_name )
else:
current_wh = qs[0]
form = WaybillForm(
initial = {
'dispatcherName': request.user.profile.compasUser.person_pk,
'dispatcherTitle': request.user.profile.compasUser.title,
'ltiNumber': current_lti[0].code,
'dateOfLoading': datetime.date.today(),
'dateOfDispatch': datetime.date.today(),
'recipientLocation': current_lti[0].destination_loc_name,
'recipientConsingee':current_lti[0].consegnee_name,
'transportContractor': current_lti[0].transport_name,
'invalidated':'False',
'destinationWarehouse':current_wh,
'waybillNumber':'N/A'
}
)
form.fields["destinationWarehouse"].queryset = qs
formset = LDFormSet()
return render_to_response( 'waybill/createWaybill.html', {'form': form, 'lti_list':current_lti, 'formset':formset}, context_instance = RequestContext( request ) )
Template (part):
<table cellpadding="1px" cellspacing="1px" id="example" style="border:1px;">
<thead>
<tr><th></th><th>Commodity</th><th>{%if lti_list.0.is_bulk%}Metric Tons{%else%}Number of Units{%endif%}</th><th>Overload</th></tr>
</thead>
{% for sform in formset.forms %}
<tr>
<td>{{ sform.id }}{{forloop.counter}}</td>
<td>{{ sform.siNo }} {{sform.coi_code}}</td>
<td>{{ sform.numberUnitsLoaded }} {%for error in sform.numberUnitsLoaded.errors%}<span class='error'> {{error|escape}}</span>{%endfor%}</td>
<td>{{ sform.overloadedUnits}}</td>
</tr>
{% endfor %}
</table>
First of all, you should refactor your models.
Create model WareHouse:
class WareHouse(models.Model):
code = models.CharField...
Link OrderItem and StockItem to warehouse with foreign keys instead of 'origin_wh_code' and 'wh_code'. Only then you will be able to select data using joins.
Use choice field with custom choices or even better - create inherited from forms.TypedChoiceField field, which encapsulates logic of serializing (stock item, order item) pks.
choices = StockItem.objects.filter(commodity_code=F("warehouse__orders__commodity_code"))\
.values('pk', 'warehouse__orders__pk')# or , <other required fields>)
item = CustomChoiceField(choices= choices, required=True)
After post you could retrieve pair:
stock_item, order_item = form.cleaned_data['item']
#Save'em here