I have a Django model with a GenericForeignKey, and several other models pointing to it through GenericRelation:
class InventoryAction(CustomModel):
action_content_type = models.ForeignKey(ContentType, on_delete=models.PROTECT,limit_choices_to={'model__in': ('inventoryinput', 'inventorytransfer', 'inventoryadjustment', 'physicalinventory', 'requisition', 'sale', 'inventorysalecancellation', 'inventorystockinit')}, related_name='inventory_actions', verbose_name=_("Tipo de Acción"))
action_object_id = models.PositiveIntegerField(verbose_name=_("ID de la acción"))
action_object = GenericForeignKey('action_content_type', 'action_object_id')
timestamp = models.DateTimeField(auto_now=True, verbose_name=_("Fecha y hora"))
class InventoryStockInit(CustomModel):
repository = models.ForeignKey(Repository, on_delete=models.PROTECT, related_name='stock_init', verbose_name=_("Almacén"))
timestamp = models.DateTimeField(auto_now=True, verbose_name=_("Fecha y Hora"))
comments = models.TextField(null=True, blank=True, verbose_name=_("Comentarios"))
inventory_action = GenericRelation(InventoryAction, content_type_field='action_content_type', object_id_field='action_object_id')
class InventoryInput(CustomModel):
repository = models.ForeignKey(Repository, on_delete=models.PROTECT, related_name='inputs', verbose_name=_("Almacén"))
reference = models.ForeignKey(InventoryAction, null=True, blank=True, on_delete=models.PROTECT, limit_choices_to=Q(action_content_type__model__in=['inventorytransfer', ]), related_name='referenced_by', verbose_name=_("Referencia"))
inventory_action = GenericRelation(InventoryAction, content_type_field='action_content_type', object_id_field='action_object_id')
And I have a Django Rest Framework viewset that attempts to get all related records from the GenericForeignKey:
class InventoryActionForListViewSet(viewsets.ViewSet):
permission_classes = (permissions.IsAuthenticated,)
def list(self, request):
self.repository = request.query_params['repository']
inventory_actions = models.InventoryAction.objects.filter(inventory_action__repository_id=self.repository).order_by('-timestamp')
inventory_actions_to_return = serializers.InventoryActionForListSerializer(inventory_actions, many=True)
return Response(inventory_actions_to_return)
The problem is that the view raises the following exception:
django.core.exceptions.FieldError: Cannot resolve keyword 'inventory_action' into field. Choices are: action_content_type, action_content_type_id, action_object, action_object_id, batch, id, products, referenced_by, referenced_by_input_or_output, referenced_by_output, timestamp
I can see that the GenericRelation is not being recognized. how can I execute the query I want, using generic relationships?
inventory_action is a field on your InventoryStockInit and InventoryInput models - you can't expect it to be a field in your InventoryAction model.
With the relation you defined, each of your InventoryAction objects can be related to a single object in one of he models stated in limit_choices_to. There is only one object related to a single InventoryAction. You can access it by accessing action object, for example:
inventory_action = InventoryAction.objects.first()
inventory_action.action_object # this will be a single object of one of the models in limit_choices_to.
Your InventoryInput model on the other hand can be pointed by multiple inventory actions. To see which inventory actions are pointing to a particular InventoryInput object, you can do:
inventory_input = InventoryInput.objects.first()
inventory_input.inventory_action.all()
As you can see, inventory_action is a manager of all related objects, it would be better to call it inventory_actions (plural). I think what you might be trying to achieve is a reverse relation (single InventoryAction object referenced by multiple objects of other models).
Related
please I need your help how to reduce the database call when using ModelChoiceField as it requires a queryset and I have to use it three times separately with a model that is recursively foreign key on itself, the code is below:
ModelForm code in the init function
self.fields['category'] = forms.ModelChoiceField(queryset=queryset)
self.fields['super_category'] = forms.ModelChoiceField(queryset=)
self.fields['product_type'] = forms.ModelChoiceField(queryset=)
the model class:
class Category(ProjectBaseModel, AuditLogMixin):
parent_id = models.ForeignKey('self', related_name='children', blank=True, null=True, on_delete=models.CASCADE,verbose_name=_('Parent'))
what i tried to do is collect all ids of the desired categories in array and make only one filter queryset with them like the following:
category = auction.category
super_category = category.parent_id
product_type = super_category.parent_id
ids= [category.id,super_category.id,product_type.id]
queryset = Category.objects.filter(id__in=ids)
How to proceed with that solution
I have a MySQL database with four related tables: project, unit, unit_equipment, and equipment. A project can have many units; a unit can have many related equipment entries. A single unit can only belong to one project, but there is a many-to-many between equipment and unit (hence the unit_equipment bridge table in the DB). I'm using Django and trying to create a view (or a list?) that shows all 3 models on the same page, together. So it would list all projects, all units, and all equipment. Ideally, the display would be like this:
Project --------- Unit ------------- Equipment
Project 1 first_unit some_equipment1, some_equipment2
Project 1 second_unit more_equipment1, more_equipment2
Project 2 another_unit some_equipment1, more_equipment1
Project 2 and_another_unit some_equipment2, more_equipment2
but at this point I'd also be happy with just having a separate line for each piece of equipment, if comma-separating them is a pain.
Although it seems straightforward to create a form where I can add a new project and add related unit and equipment data (using the TabularInline class), I cannot for the life of me figure out how to bring this data together and just display it. I just want a "master list" of everything in the database, basically.
Here's the code I have so far:
models.py
class Project(models.Model):
name = models.CharField(max_length=255, blank=True, null=True)
class Meta:
managed = False
db_table = 'project'
def __str__(self):
return self.name
class Unit(models.Model):
project = models.ForeignKey(Project, models.DO_NOTHING, blank=True, null=True)
name = models.CharField(max_length=255, blank=True, null=True)
class Meta:
managed = False
db_table = 'unit'
def __str__(self):
return self.name
class UnitEquipment(models.Model):
unit = models.ForeignKey(Unit, models.DO_NOTHING, blank=True, null=True)
equipment = models.ForeignKey(Equipment, models.DO_NOTHING, blank=True, null=True)
class Meta:
managed = False
db_table = 'unit_equipment'
class Equipment(models.Model):
name = models.CharField(max_length=100, blank=True, null=True)
description = models.CharField(max_length=255, blank=True, null=True)
class Meta:
managed = False
db_table = 'equipment'
def __str__(self):
return self.name
views.py
def project_detail_view(request):
obj = Project.objects.all()
context = {'object': obj}
return render(request, "project/project_detail.html", context)
urls.py
urlpatterns = [
path('project/', project_detail_view),
path('', admin.site.urls),
]
admin.py
class UnitTabularInLine(admin.TabularInline):
model = Unit
extra = 0
class ProjectAdmin(admin.ModelAdmin):
inlines = [UnitTabularInLine]
class Meta:
model = Project
# a list of displayed columns name.
list_display = ['name']
# define search columns list, then a search box will be added at the top of list page.
search_fields = ['name']
# define filter columns list, then a filter widget will be shown at right side of list page.
list_filter = ['name']
# define model data list ordering.
ordering = ('name')
I think I need to somehow add more entries to the list_display in the admin file, but every time I try to add unit or equipment it throws an error. I've also tried adding more attributes to Project, but I can't seem to get the syntax right, and I'm never sure which model class I'm supposed to make it.
I've also looked at FormSets, but I cannot get my head around how to alter my current code to get it to work.
How do I get these models together into a unified view?
You don't need to edit the admin view to add your own view: which you may find you are able to do in this case to get your data displayed exactly as you want.
If you do want to show the related object values in the admin list, then you can use lookups and custom columns: however in this case your list would be based upon the Unit.
# You don't need an explicit UnitEquipment model here: you can
# use a simple ManyToManyField
class Unit(models.Model):
project = ...
name = ...
equipment = models.ManyToManyField(Equipment, related_name='units')
def equipment_list(admin, instance):
return ', '.join([x.name for x in instance.equimpent.all()])
class UnitAdmin(admin.ModelAdmin):
class Meta:
model = Unit
list_display = ['project__name', 'name', equipment_list]
def get_queryset(self, request):
return super().get_queryset(request)\
.select_related('project')\
.prefetch_related('equipment')
Note that you need to have the queryset override, otherwise there will be a bunch of extra queries as each unit also requires fetching the project and list of equipment for that unit.
There's also a further improvement you can make to your queries: you could aggregate the related equipment names using a Subquery annotation, and prevent the second query (that fetches all related equipment items for the units in the queryset). This would replace the prefetch_related()
Thanks to #Matthew Schinckel, I was able to find my way to the answer. Here's what my files look like now (only edited the Unit class in models.py):
models.py
class Unit(models.Model):
project = models.ForeignKey(Project, models.DO_NOTHING, blank=True, null=True)
name = models.CharField(max_length=255, blank=True, null=True)
equipment = models.ManyToManyField(Equipment, related_name='units')
class Meta:
managed = False
db_table = 'unit'
def __str__(self):
return self.name
def equipment_list(self):
return ', '.join([x.name for x in self.equipment.all()])
admin.py
class UnitAdmin(admin.ModelAdmin):
class Meta:
model = Unit
# a list of displayed columns name.
list_display = ('project', 'name', 'equipment_list')
# define search columns list, then a search box will be added at the top of list page.
search_fields = ['project']
# define filter columns list, then a filter widget will be shown at right side of list page.
list_filter = ['project', 'name']
# define model data list ordering.
ordering = ('project', 'name')
def get_queryset(self, request):
return super().get_queryset(request)\
.select_related('project')\
.prefetch_related('equipment')
So the changes I made were:
1. Make list_display a tuple instead of a list.
2. Throw def equipment_list(self) into the Unit class (so it's callable as an attribute of Unit) and pass (self) instead of (admin, instance) (I kept getting an error that was looking for the instance argument).
How to follow an intermediate 'through' model, in a Q object?
I'm trying to get a list of the ids of all Property objects that a User is a member of, in one query.
A User is a member of a BusinessUnit through BusinessUnitMember.
How do I make a filter with a Q object that finds all (multiple) BusinessUnitMembers for a particular User, and then finds all (multiple) BusinessUnits for those BusinessUnitMembers?
class Property(BaseModel):
"""Physical Things belonging to various Business Units"""
category = models.CharField(max_length=255, choices=CATEGORY_CHOICES)
objects = PropertyQuerySet.as_manager()
#property
def asset(self):
asset_maps = {
'typea': 'type_a_asset',
'typeb': 'type_b_asset',
}
if self.category not in asset_maps:
return None
return getattr(self, asset_maps.get(self.category), None)
class AssetBase(models.Model):
"""Abstract Base Asset model."""
business_unit = models.ForeignKey('BusinessUnit', null=True, on_delete=models.SET_NULL)
class TypeAAsset(AssetBase):
"""Type A Assets"""
property = models.OneToOneField('Property', null=True, on_delete=models.CASCADE,
related_name='type_a_asset')
class TypeBAsset(AssetBase):
"""Type B Assets"""
property = models.OneToOneField('Property', null=True, on_delete=models.CASCADE,
related_name='type_b_asset')
class BusinessUnit(models.Model):
name = models.CharField(max_length=255)
owner = models.ForeignKey('core.User', null=True,
on_delete=models.SET_NULL,
related_name='owned_business_units')
class BusinessUnitMember(models.Model):
""" ``through`` model for BU and members. """
business_unit = models.ForeignKey('BusinessUnit', on_delete=models.CASCADE)
member = models.ForeignKey('core.User', on_delete=models.CASCADE)
I need to find the ids of all Property objects that the current User is a member of.
I'm thinking of a filter like:
available_ids = Property.objects.filter(Q(type_a_asset__business_unit__in=user.businessunitmember.all().businessunit_set.all())).values_list('id', flat=True)
but the
businessunitmember_set.all().businessunit_set.all()
part doesn't work because businessunitmember_set.all() returns a queryset object which then won't have the attribute 'businessunit_set'. Using businessunitmember_set.get() doesn't work either, as it would return 3 results instead of 1.
I already got it working through iterating over the individual models, but of course those create a ton of queries.
My app has a model "OptimizationResult", where I store results from mathmatical optimization. The optimization distributes timeslots over projects. I need to indicate whether the current results is different from a recent result, based on a set of attributes (in particularly not the primary key)
The attribute optimization_run is a coutner for different runs
Project is a ForeignKey to the project.
By overwriting the __hash__ and __eq__ functions on the model I can compare the different instances by
OptimizationResults.objects.filter(proj = 1).filter(optimization_run =1).first() == OptimizationResults.objects.filter(proj = 1).filter(optimization_run = 2).first()
. But as I understand __eq__ and __hash__ are not available on the database.
How would I annotate the results accordingly? Something like
OptimizationResults.objects.filter(optimization_run = 2).annotate(same_as_before = Case(When(),default=False))
Edit
Added .first() to the code, to ensure that there is only one element.
class OptimizationResult(models.Model):
project = models.ForeignKey(project, on_delete=models.CASCADE)
request_weight = models.IntegerField()
periods_to_plan = models.IntegerField()
unscheduled_periods = models.IntegerField()
scheduled_periods = models.IntegerField()
start = models.DateField(null=True, blank=True, default=None)
end = models.DateField(null=True, blank=True, default=None)
pub_date = models.DateTimeField('Erstellungsdatum', auto_now_add=True, editable=False)
optimization_run= models.ForeignKey(OptimizationRun, on_delete=models.CASCADE)
I'd like to compore different entries on the basis of start and end.
Edit 2
My fruitless attempt with Subquery:
old = OptimizationResult.objects.filter(project=OuterRef('pk')).filter(optimization_run=19)
newest = OptimizationResult.objects.filter(project=OuterRef('pk')).filter(optimization_run=21)
Project.objects.annotate(changed = Subquery(newest.values('start')[:1])== Subquery(old.values('start')[:1]))
results in TypeError: QuerySet.annotate() received non-expression(s): False
We can use a subquery here, to make an annotation:
from django.db.models import Exists, OuterRef, Subquery, Q
to_exclude = {'pk', 'id', 'project', 'project_id', 'optimization_run', 'optimization_run_id'}
subquery = OptimizationResult.objects.filter(
project_id=OuterRef('project_id')
optimization_run=1,
**{f.name: OuterRef(f.name)
for f in OptimizationResult._meta.get_fields()
if f.name not in to_exclude
}
)
OptimizationResult.objects.filter(
optimization_run=2
).annotate(
are_same=Exist(subquery)
)
Here we will thus annotate all the OptimizationResults with an optimization_run=2, with an extra attribute .are_same that checks if there exists an OptimizationResult object for optimization_run=1 and for the same project_id, where all fields are the same, except the ones in the to_exclude set.
I've been looking at the docs for search_fields in django admin in the attempt to allow searching of related fields.
So, here are some of my models.
# models.py
class Team(models.Model):
name = models.CharField(max_length=255)
class AgeGroup(models.Model):
group = models.CharField(max_length=255)
class Runner(models.Model):
"""
Model for the runner holding a course record.
"""
name = models.CharField(max_length=100)
agegroup = models.ForeignKey(AgeGroup)
team = models.ForeignKey(Team, blank=True, null=True)
class Result(models.Model):
"""
Model for the results of records.
"""
runner = models.ForeignKey(Runner)
year = models.IntegerField(_("Year"))
time = models.CharField(_("Time"), max_length=8)
class YearRecord(models.Model):
"""
Model for storing the course records of a year.
"""
result = models.ForeignKey(Result)
year = models.IntegerField()
What I'd like is for the YearRecord admin to be able to search for the team which a runner belongs to. However as soon as I attempt to add the Runner FK relationship to the search fields I get an error on searches; TypeError: Related Field got invalid lookup: icontains
So, here is the admin setup where I'd like to be able to search through the relationships. I'm sure this matches the docs, but am I misunderstanding something here? Can this be resolved & the result__runner be extended to the team field of the Runner model?
# admin.py
class YearRecordAdmin(admin.ModelAdmin):
model = YearRecord
list_display = ('result', 'get_agegroup', 'get_team', 'year')
search_fields = ['result__runner', 'year']
def get_team(self, obj):
return obj.result.runner.team
get_team.short_description = _("Team")
def get_agegroup(self, obj):
return obj.result.runner.agegroup
get_agegroup.short_description = _("Age group")
The documentation reads:
These fields should be some kind of text field, such as CharField or TextField.
so you should use 'result__runner__team__name'.