Django ORM, how to "rebase" a QuerySet to a related model - python

With a database model described in the simplified toy example below, we are trying to get a list of EAV attributes used for a specific Product.
The sample source code in the Actions section below serves the purpose; however, we feel the statement is overly verbose: We only need columns of the template_attribute table, but the values arguments need to maintain a fully qualified path starting from the original Product model. See the code below:
# Getting the `id` columns from multiple-orders of related models:
attribute_set = template. values(
"id", # the base model, Product
"template__id", # the related model, Template
"template__templateattribute__id" # the second related model, TemplateAttribute
)
So, we wonder if there is a way to refer to the columns directly from the containing model, e.g. templateattribute__id, or even better id, instead of template__templateattribute__id.
We are new to the Django ORM and appreciate any hints or suggestions.
Actions:
template = Product.active_objects.filter(id='xxx').select_related('template')
attribute_set = template. values("id", "template__id", "template__templateattribute__id")
for i, attr in enumerate(attribute_set):
print("{:03}: {}".format(i, attr))
# Output:
# 000: {'id': xxx, 'template__id': xxxx, 'template__templateattribute__id': xxxxx}
# 001: {'id': xxx, 'template__id': xxxx, 'template__templateattribute__id': xxxxx}
# 002: {'id': xxx, 'template__id': xxxx, 'template__templateattribute__id': xxxxx}
# ...
The models:
# Simplified toy example
class Product(models.Model):
product_template = models.ForeignKey(Template)
sku = models.CharField(max_length=100)
...
class Template(models.Model):
base_sku = models.CharField(max_length=100)
...
class TemplateAttribute(models.Model):
product_template = models.ForeignKey(Template)
attribute = models.ForeignKey(eav_models.Attribute)
...
# From the open-source Django EAV library
# imported as `eav_models`
#
class Attribute(models.Model):
name = models.CharField(_(u"name"), max_length=100,
help_text=_(u"User-friendly attribute name"))
...
slug = EavSlugField(_(u"slug"), max_length=50, db_index=True,
help_text=_(u"Short unique attribute label"))
...

Perhaps using a Manager with annotations or aliases could help?
You could try to add more magic by trying to dynamically add an annotation for each key during the manager's construction, but really at that point you are writing code that should have been in the EAV itself.
I would warn you, having attribute names and their values in a table instead of just model fields (and columns in the DB) will be an uphill battle, and already you are finding areas where your library isn't handling things for you.
from django.db import models
from django.db.models import F
class ProductManager(models.Manager):
def get_queryset(self):
qs = super().get_queryset() \
.annotate(
my_attribute_key=F('template__templateattribute__id')
)
return qs

In addition, we also figured out a workaround by defining a base path:
base = "template__templateattribute"
So, instead of
attribute_set = template. Values("template__templateattribute__id")
, we can do the following:
attribute_set = template. Values(base+"__id")
This is just an informal workaround, though. The better way is still to use a Manager class.

Related

'Category' object is not subscriptable Django

I am learning Django. I wrote a simple model and some views method in Django rest framework so that I can modify some particular attributes when needed to all the records that need that. Here is the model:
from django.db import models
class Category(models.Model):
name = models.CharField(max_length=255)
isActive = models.BooleanField(default=True)
def __str__(self):
return self.name
Then, I created this view to modify the isActive session when I call it:
class CategoriesChangeActiveView(views.APIView):
def post(self, request, format=None):
try:
categories = request.data.get('categories')
for category in categories:
category = Category.objects.get(id=category['id'])
category.isActive = category['isActive']
category.save()
except Exception as e:
return Response({'error': 'Bad request'}, status=status.HTTP_400_BAD_REQUEST)
return Response({'success': 'Active changed'}, status=status.HTTP_200_OK)
Even when the format of my request is correct ( I debugged each line ) when it comes to the line category.isActive = category['isActive']it throws the error that'Category' object is not subscriptable`. I don't know why or how to fix it.
I saw in the official documentation, on older StackOverflow questions that this is doable, but I don't understand why I can't.
Can someone please suggest what I am doing wrong? Thank you
it's a simple mistake.
Simply change it as follows and it should be fixed:
categories = request.data.get('categories')
for category in categories:
category_obj = Category.objects.get(id=category['id'])
category_obj.isActive = category['isActive']
category_obj.save()
What you're doing is changing what the variable category is. You for loop and the unpacked variable is category, but then you get the model object and set the variable as category
So initially, the category variable is in fact a dictionary object, but you change it to be a django model object instance.
Specifically, the issue is here:
category = Category.objects.get(id=category['id'])
category.isActive = category['isActive']
You set category to be an instance of the Category model (which in this case corresponds to a db record, but that bit is a little irrelevant).
Accessing attributes on a class instance is not done by the square bracket notation, but rather dot notation.
So instead of category['isActive'] use category.isActive
If category was a dictionary, eg.
category = {
"name": "cat",
"isActive": True,
}
Then you would use the square bracket notation as category["isActive"] to get that value.
As it is, it's not a dict, so python thinks you are trying to subscript the instance somehow, which will not work.

Extend django-import-export's import form to specify fixed value for each imported row

I am using django-import-export 1.0.1 with admin integration in Django 2.1.1. I have two models
from django.db import models
class Sector(models.Model):
code = models.CharField(max_length=30, primary_key=True)
class Location(models.Model):
code = models.CharField(max_length=30, primary_key=True)
sector = ForeignKey(Sector, on_delete=models.CASCADE, related_name='locations')
and they can be imported/exported just fine using model resources
from import_export import resources
from import_export.fields import Field
from import_export.widgets import ForeignKeyWidget
class SectorResource(resources.ModelResource):
code = Field(attribute='code', column_name='Sector')
class Meta:
model = Sector
import_id_fields = ('code',)
class LocationResource(resources.ModelResource):
code = Field(attribute='code', column_name='Location')
sector = Field(attribute='sector', column_name='Sector',
widget=ForeignKeyWidget(Sector, 'code'))
class Meta:
model = Location
import_id_fields = ('code',)
and import/export actions can be integrated into the admin by
from django.contrib import admin
from import_export.admin import ImportExportModelAdmin
class SectorAdmin(ImportExportModelAdmin):
resource_class = SectorResource
class LocationAdmin(ImportExportModelAdmin):
resource_class = LocationResource
admin.site.register(Sector, SectorAdmin)
admin.site.register(Location, LocationAdmin)
For Reasons™, I would like to change this set-up so that a spreadsheet of Locations which does not contain a Sector column can be imported; the value of sector (for each imported row) should be taken from an extra field on the ImportForm in the admin.
Such a field can indeed be added by overriding import_action on the ModelAdmin as described in Extending the admin import form for django import_export. The next step, to use this value for all imported rows, is missing there, and I have not been able to figure out how to do it.
EDIT(2): Solved through the use of sessions. Having a get_confirm_import_form hook would still really help here, but even better would be having the existing ConfirmImportForm carry across all the submitted fields & values from the initial import form.
EDIT: I'm sorry, I thought I had this nailed, but my own code wasn't working as well as I thought it was. This doesn't solve the problem of passing along the sector form field in the ConfirmImportForm, which is necessary for the import to complete. Currently looking for a solution which doesn't involve pasting the whole of import_action() into an ImportMixin subclass. Having a get_confirm_import_form() hook would help a lot here.
Still working on a solution for myself, and when I have one I'll update this too.
Don't override import_action. It's a big complicated method that you don't want to replicate. More importantly, as I discovered today: there are easier ways of doing this.
First (as you mentioned), make a custom import form for Location that allows the user to choose a Sector:
class LocationImportForm(ImportForm):
sector = forms.ModelChoiceField(required=True, queryset=Sector.objects.all())
In the Resource API, there's a before_import_row() hook that is called once per row. So, implement that in your LocationResource class, and use it to add the Sector column:
def before_import_row(self, row, **kwargs):
sector = self.request.POST.get('sector', None)
if contract:
self.request.session['import_context_sector'] = sector
else:
# if this raises a KeyError, we want to know about it.
# It means that we got to a point of importing data without
# contract context, and we don't want to continue.
try:
sector = self.request.session['import_context_sector']
except KeyError as e:
raise Exception("Sector context failure on row import, " +
f"check resources.py for more info: {e}")
row['sector'] = sector
(Note: This code uses Django sessions to carry the sector value from the import form to the import confirmation screen. If you're not using sessions, you'll need to find another way to do it.)
This is all you need to get the extra data in, and it works for both the dry-run preview and the actual import.
Note that self.request doesn't exist in the default ModelResource - we have to install it by giving LocationResource a custom constructor:
def __init__(self, request=None):
super()
self.request = request
(Don't worry about self.request sticking around. Each LocationResource instance doesn't persist beyond a single request.)
The request isn't usually passed to the ModelResource constructor, so we need to add it to the kwargs dict for that call. Fortunately, Django Import/Export has a dedicated hook for that. Override ImportExportModelAdmin's get_resource_kwargs method in LocationAdmin:
def get_resource_kwargs(self, request, *args, **kwargs):
rk = super().get_resource_kwargs(request, *args, **kwargs)
rk['request'] = request
return rk
And that's all you need.

Django model one foreign key to many tables

So I have a question I was thinking of creating a single table that has a foreign key to several other tables, and using another field "type" to say what table the key should belong to.
class Status(Models.model):
request = models.ForeignKey("Request1", "Request2", "Request3")
request_type = models.IntegerField()
...Some status related data
class Request1(Models.model):
...Some data
class Request2(Models.model):
...Some other data
Class Request3(Models.model):
...different data
My question is, is it possible to define a foreign key like this?
another solution I thought of was to define my model like this
class Status(Models.model):
request1 = models.ForeignKey("Request1")
request2 = models.ForeignKey("Request2")
request3 = models.ForeignKey("Request3")
...Some status related data
class Request1(Models.model):
...Some data
class Request2(Models.model):
...Some other data
Class Request3(Models.model):
...different data
But if I do it this way is it possible to define a constraint via django that says only 1 foreign key is allowed to have data and the other two must be null? or will I have to strictly set this constraint up on the db side.(I'm using postgres) I would like to be able to tell django to do it when it creates the db so I don't have to remember every time someone recreates the db.
Any input or advice would be greatly appreciated. I am not married to either of these ideas, so if there is another clever way to achieve the same effect i am up to hear it. Thank you for your time.
Edit: I am using django 1.7.10
You should use the contentypes framework in Django.
There's an example for a generic relation here :https://docs.djangoproject.com/en/1.8/ref/contrib/contenttypes/#generic-relations
For your requirement it could look something like this:
from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
class Status(models.Model):
request_type = models.ForeignKey(ContentType)
request_id = models.PositiveIntegerField()
request = GenericForeignKey('request_type', 'request_id')
You can then do something like following:
status1 = Status(request=Request1("foo"))
status1.save()
status2 = Status(request=Request2("bar"))
status2.save()
status1.request // <Request1 "foo">
status2.request // <Request2 "bar">

Plone/Sqlalchemy (saconfig) - Using .filter of a session object, how can I filter on specific fields that were chosen by a user?

I have mapped fields defined in an Interface to a mysql backend database through the use of saconfig and a declarative base.* On my Plone Site instance, I have a form (z3c.form) that will use these fields and will be used to search for "Assets" from a mysql database based on criteria entered in several fields. In my case, I would like to only query on fields that have data entered in them.
This is what my interface and ORMBase class look like:
class IAsset(Interface):
"""Interface class of an asset
"""
Asset_ID = schema.Int(title=u"Asset ID",
required=False
)
GPCL_Asset_ID = schema.TextLine(title=u"GPCL Asset Tracker",
required=False
)
Asset_Type = schema.Int(title=u"Asset Type",
required=False
)
Manufacturer = schema.Int(title=u"Manufacturer",
required=True
)
Model = schema.TextLine(title=u"Serial Number",
required=False
)
Serial_Number = schema.TextLine(title=u"Serial Number",
required=False
)
class Asset(ORMBase):
"""Class for asset
"""
__tablename__ = 'Assets'
Asset_ID = sqlalchemy.Column(sqlalchemy.Integer(),
primary_key=True,
autoincrement=True
)
GPCL_Asset_ID = sqlalchemy.Column(sqlalchemy.String(255),
nullable=True
)
Asset_Type = sqlalchemy.Column(sqlalchemy.Integer(),
nullable=False
)
Manufacturer = sqlalchemy.Column(sqlalchemy.Integer(),
nullable=False
)
Model = sqlalchemy.Column(sqlalchemy.String(255),
nullable=True
)
Serial_Number = sqlalchemy.Column(sqlalchemy.String(255),
nullable=True
)
For testing purposes, I am using the view class of an Asset, and I have a function (current called through a template for testing purposes) that builds a dictionary based on fields that are filled out. I also have a utility class called AssetUtils and there is a function in the class called queryAssets that accepts a dictionary containing the criteria, and returns the search results found.
class View(grok.View):
"""View class
"""
...grok definitions and fields being set for form
def searchAssets(self):
#get data
....
criteria = {}
#build dictionary
#if field (i.e. GPCL_Asset_ID) is not empty, add to the dictionary GPCL_Asset_ID:valueEnteredByUser
assets = queryAssets(criteria)
class AssetUtils(grok.GlobalUtility):
def queryAssets(searchCriteria):
I have a session object defined at the root of the class. In queryAssets I would like to use session.query(Asset).filter() to query the Assets table. The problem I am having though is figuring out what to pass into filter(). I know how to use the query function, like session.query(Asset).filter(GPCL_Asset_ID.like('D%'),Manufacturer==15).
However, say Serial Number was present, but GPCL_Asset_ID was not, so I would like to use .filter(Serial_Number.like("EX12%")), and then another case where only Model and Manufacturer were present. .filter(Model.like("A3%"),Manufacturer==32)
So my question is, how can I make it so that .filter filters on fields that were defined/chosen in the dictionary passed into the queryAssets function I have, as opposed to fields being pre-defined in the .filter function?
*I am following along with the book "Professional Plone 4 Development" by Martin Aspeli, chapter 12 and a slideshow presentation (http://www.slideshare.net/jbellis/pycon-2010-sqlalchemy-tutorial) - Slide 27, 29. Compared to the slideshow, I am actually using saconfig as found in Martin Aspeli's book.
Looking at your class, from the searchAssets method you simply need to access the self.request.form dict-like object and read all the data submitted (by HTTP GET or POST) to that view.
You can then build your list of filters parameters like:
params = []
if 'param1' in self.request.form:
params.append(Asset_ID.like(...))
if ...
session.query(*params)

How to get a collection_name without having and instance of the referencing object?

I'm doing a simple program about customers, products and drafts.
Since they are referenced to each other in some way, when I delete one entity of a kind, another entity of another kind might give an error.
Here's what I have:
-customer.py
class Customer(db.Model):
"""Defines the Customer entity or model."""
c_name = db.StringProperty(required=True)
c_address = db.StringProperty()
c_email = db.StringProperty() ...
-draft.py
class Draft(db.Model):
"""Defines the draft entity or model."""
d_customer = db.ReferenceProperty( customer.Customer,
collection_name='draft_set')
d_address = db.StringProperty()
d_country = db.StringProperty() ...
Ok, now what I want to do is check if a customer has any Draft referencing to him, before deleting him.
This is the code I'm using:
def deleteCustomer(self, customer_key):
'''Deletes an existing Customer'''
# Get the customer by its key
customer = Customer.get(customer_key)
if customer.draft_set: # (or customer.draft_set.count > 0...)
customer.delete()
else:
do_something_else()
And now, it comes the problem.
If I have a draft previously created with the selected customer on it, there's no problem at all, and it does what has to do. But if I haven't created any draft that references to that customer, when trying to delete him, it will show this error:
AttributeError: 'Customer' object has no attribute 'draft_set'
What am I doing wrong? Is it needed to always create a Draft including a Customer for him to have the collection_name property "available"?
EDIT: I found out what the error was.
Since I have both classes in different .py files, it seems that GAE loads the entities into the datastore at the same moment as it "goes through" the file that contains that model.
Therefore, if I'm executing the program, and never use or import that file, the datastore is not updated until then.
Now what I'm doing is:
from draft.py import Draft
inside de "deleteCustomer()" function and it's finally working fine, but I get a horrible "warning not used" because of so.
Is there any other way I can fix this?
The collection_name property a query, so it should always be available.
What you may be missing is the reference_class parameter (check the ReferenceProperty docs)
class Draft(db.Model):
"""Defines the draft entity or model."""
d_customer = db.ReferenceProperty(reference_class=customer.Customer, collection_name='draft_set')
The following should work:
if customer.draft_set.count():
customer.delete()
note that customer.draft_set will always return true, as it is the generated Query object, so you MUST use the count()
There were two possible solutions:
Ugly, bad one: as described in my edited question.
Best practice: put all the models together inside one file (e.g. models.py) that looks like this:
class Customer(db.Model):
"""Defines the Customer entity or model."""
c_name = db.StringProperty(required=True)
c_address = db.StringProperty()
c_email = db.StringProperty() ...
class Draft(db.Model):
"""Defines the draft entity or model."""
d_customer = db.ReferenceProperty( customer.Customer,
collection_name='draft_set')
d_address = db.StringProperty()
d_country = db.StringProperty() ...
Easy!

Categories