Django admin: programatically choose visible fieldsets in inlines - sql

What I want is to be able to choose which formsets to display according to the selection of a ForeignKey selector in the django Admin.
I have four models:
Product
Rating
Criterion (question)
CriterionAnswer
My database is organised as follows:
Product <- ManyToOne -- Rating <- ManyToMany -> Criterion -
| |
-- OneToMany -> CriterionAnswer <- OneToMany --
When I select a Rating in the Product model form of the admin and save the Product, I create all the missing CriterionAnswers that correspond to the Criterions in the chosen Rating. The next time I open the Product, I see the CriterionAnswers as inlines. If I then select another Rating, other CriterionAnswers will be saved and displayed along the old ones. How do I choose to display only the CriterionAnswers which correspond to the chosen Rating?
Models:
################
# Product
################
class Product (models.Model):
rating = models.ForeignKey(
'Rating',
on_delete=models.SET_NULL,
null=True,
)
def save(self, *args, **kwargs):
super(Product, self).save(*args, **kwargs)
# get all criteria that need an answer in order to create the rating
criteriaToAnswer = Criterion.objects.filter(rating=self.rating)
# iterate over all criterias and check if the answer exists in database
for crit in criteriaToAnswer:
# if it doesn't exist, create an answer
if not CriterionAnswer.objects.filter(criterion=crit).filter(product=self):
print "Criteria answer to: <%s> does not exist... creating new one" % crit.__str__()
new_criterionAnswer = CriterionAnswer()
new_criterionAnswer.criterion = crit
new_criterionAnswer.product = self
new_criterionAnswer.save()
else:
print "Criteria answer to: <%s> exists." % crit.__str__()
################
# CriterionAnswer
################
class CriterionAnswer(models.Model):
criterion = models.ForeignKey('Criterion', on_delete=models.CASCADE)
product = models.ForeignKey('Product', on_delete=models.CASCADE)
################
# Rating
################
class Rating(models.Model):
name = models.CharField(max_length=30)
################
# Criterion
################
class Criterion(models.Model):
category = models.ForeignKey(
'Category',
on_delete=models.PROTECT,
)
rating = models.ManyToManyField(
'Rating',
through='CriterionInRating',
through_fields=('criterion', 'rating'),
)
Admin:
class ProductAdmin(admin.ModelAdmin):
inlines = [
# Something here...
]
# Or something here that could solve my problem.

Related

SQLAlchemy ORM: Filter by multiple include/exclude matches on many-to-many relationship

I'm trying to use an SQLite database via SQLAlchemy 1.4 ORM to filter data based on relationships.
Data
My example data consists of groups and members in a simple many-to-many schema:
[...]
member_f = Member(name="F")
group_1 = Group(name="1", members=[member_a, member_b, member_c]) <-- find this group via member names
group_2 = Group(name="2", members=[member_a, member_b, member_f])
group_3 = Group(name="3", members=[member_a, member_c, member_d])
group_4 = Group(name="4", members=[member_d, member_e, member_f])
[...]
Full running example code (schema, objects, queries):
from sqlalchemy import Table, Column, Integer, String, ForeignKey
from sqlalchemy import create_engine, select, func, text, and_
from sqlalchemy.orm import relationship, declarative_base, sessionmaker
engine = create_engine("sqlite+pysqlite:///:memory:", future=True, echo=False)
Session = sessionmaker(bind=engine, future=True)
# Schema
Base = declarative_base()
groups_members = Table("groups_members", Base.metadata,
Column("group_id", ForeignKey("groups.id")),
Column("member_name", ForeignKey("members.name")),
)
class Group(Base):
__tablename__ = "groups"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String, nullable=False)
members = relationship("Member", secondary=groups_members, backref="groups", lazy="subquery")
def __repr__(self):
return f"<Group: {self.name}>"
class Member(Base):
__tablename__ = "members"
name = Column(String, primary_key=True)
def __repr__(self):
return f"<Member: {self.name}>"
Base.metadata.create_all(engine)
# Objects
member_a = Member(name="A")
member_b = Member(name="B")
member_c = Member(name="C")
member_d = Member(name="D")
member_e = Member(name="E")
member_f = Member(name="F")
group_1 = Group(name="1", members=[member_a, member_b, member_c])
group_2 = Group(name="2", members=[member_a, member_b, member_f])
group_3 = Group(name="3", members=[member_a, member_c, member_d])
group_4 = Group(name="4", members=[member_d, member_e, member_f])
print(f"{member_a}: {member_a.groups}") # OK
with Session() as session:
session.add(group_1)
session.add(group_2)
session.add(group_3)
session.add(group_4)
session.commit()
print(session.query(Group).all()) # OK
# Query users example
def get_members_in_more_than_2_groups():
with Session() as session:
return session.execute(
select(Member, func.count(groups_members.columns.group_id).label('group_members_count'))
.join(groups_members)
.group_by(Member.name)
.having(text('group_members_count > 2'))
).all()
for m in get_members_in_more_than_2_groups():
print(m) # OK
# Query groups problem: associated with A and B but not with E or F
def get_groups_by_member_names(member_names_included, member_names_excluded):
with Session() as session:
included = session.execute(select(Member).where(Member.name.in_(member_names_included))).all()
excluded = session.execute(select(Member).where(Member.name.in_(member_names_excluded))).all()
return session.execute(
select(Group)
.join(Group.members)
.where(
and_(
Group.members.contains(included),
~Group.members.contains(excluded),
)
)
.group_by(Group.id)
).scalars().all()
for g in get_groups_by_member_names(member_names_included=["A", "B"], member_names_excluded=["E", "F"]):
print(g) # Expected output: <Group: 1>
Goal
Now I'm trying to find all groups that
have both members with the names A and B (that's groups 1 and 2)
and don't have any member named E or F (removing group 2)
resulting in just group 1.
Problem
The relevant (and failing) function in the example code is get_groups_by_member_names and with my lack of database knowledge, I'm quite stuck.
Most existing questions that I could find on SO only need to filter by one relationship value. But I need them to consider the lists of included and excluded member names.
I have tried to get the members as SQLAlchemy objects first and inserting those into the query but without any luck. I may have done that completely wrong, though.
I also tried joining the tables, filtering with the names list and counting the grouped results... It's hard for me to tell whether I'm on the right track or not at all.
Running over all groups in Python and applying the filtering there would be my fallback workaround. But with many items, the database can probably handle it more efficiently.
Any help greatly appreciated, I am happy with anything that works. I could probably also work my way up from a functioning SQL statement.
Thanks for your time!
Edit 1:
I found this answer https://stackoverflow.com/a/21104689/5123171 and while it works on small data sets, it's terribly slow on larger ones (about 60 seconds for 500 members and 10k groups):
def get_group_by_members(member_names_included, member_names_excluded):
with Session() as session:
return session.query(Group).join(groups_members).filter(
groups_members.columns.member_name.in_(member_names_included)).group_by(Group.id).having(func.count(groups_members.columns.member_name) == len(member_names_included),
).filter(
~Group.members.any(Member.name.in_(member_names_excluded)),
).all()
Ok, here's what I ended up with via trial & error in an SQL editor. This is faster than the previous attempt (50 milliseconds on the same data).
The comments in the code correspond to the following steps:
Find groups containing excluded members
Filter those out of the assignment table
Filter remaining assignment table by included members
Group by group IDs
Return all remaining groups matching the number of included members
SQL
SELECT * FROM groups
WHERE id IN (
SELECT group_id FROM groups_members
WHERE member_name IN ("A", "B") // 3.
AND group_id NOT IN ( // 2
SELECT group_id FROM groups_members
WHERE member_name IN ("E", "F") // 1.
)
GROUP BY group_id // 4.
HAVING count(member_name) == 2 // 5.
)
SQLAlchemy
session.query(Group)
.where(Group.id.in_(
session.query(groups_members.c.group_id)
.where(
groups_members.c.member_name.in_(member_names_included), # 3.
groups_members.c.group_id.not_in( # 2.
session.query(groups_members.c.group_id).where(
groups_members.c.member_name.in_(member_names_excluded) # 1.
)
),
)
.group_by(groups_members.c.group_id) # 4.
.having(
func.count(groups_members.c.member_name)
== len(member_names_included) # 5.
))
)
.all()
And the full running example in one piece:
from sqlalchemy import Table, Column, Integer, String, ForeignKey
from sqlalchemy import create_engine, select, func, text, and_, not_
from sqlalchemy.orm import relationship, declarative_base, sessionmaker
engine = create_engine("sqlite+pysqlite:///:memory:", future=True, echo=False)
Session = sessionmaker(bind=engine, future=True)
# Schema
Base = declarative_base()
groups_members = Table(
"groups_members",
Base.metadata,
Column("group_id", ForeignKey("groups.id")),
Column("member_name", ForeignKey("members.name")),
)
class Group(Base):
__tablename__ = "groups"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String, nullable=False)
members = relationship(
"Member", secondary=groups_members, backref="groups", lazy="subquery"
)
def __repr__(self):
return f"<Group {self.id}: {self.name}>"
class Member(Base):
__tablename__ = "members"
name = Column(String, primary_key=True)
def __repr__(self):
return f"<Member: {self.name}>"
Base.metadata.create_all(engine)
# Objects
member_a = Member(name="A")
member_b = Member(name="B")
member_c = Member(name="C")
member_d = Member(name="D")
member_e = Member(name="E")
member_f = Member(name="F")
group_1 = Group(name="1", members=[member_a, member_b, member_c])
group_2 = Group(name="2", members=[member_a, member_b, member_f])
group_3 = Group(name="3", members=[member_a, member_c, member_d])
group_4 = Group(name="4", members=[member_d, member_e, member_f])
with Session() as session:
session.add(group_1)
session.add(group_2)
session.add(group_3)
session.add(group_4)
session.commit()
# Query
member_names_included = ["A", "B"]
member_names_excluded = ["E", "F"]
# Raw SQL variant
with Session() as session:
result = session.execute(
f"""
SELECT * FROM groups
WHERE id IN (
SELECT group_id FROM groups_members
WHERE member_name IN ("A", "B")
AND group_id NOT IN (
SELECT group_id FROM groups_members
WHERE member_name IN ("E", "F")
)
GROUP BY group_id
HAVING count(member_name) == 2
)
"""
).all()
groups = [Group(**r) for r in result]
for r in groups:
print(f'SQL {r}')
# ORM Variant
with Session() as session:
result = (
session.query(Group)
.where(
Group.id.in_(
session.query(groups_members.c.group_id)
.where(
# Matching any included members
groups_members.c.member_name.in_(member_names_included),
# Removing any groups containing excluded members
groups_members.c.group_id.not_in(
session.query(groups_members.c.group_id).where(
groups_members.c.member_name.in_(member_names_excluded)
)
),
)
# This is to make sure that all included members exist in a group, not just a few
.group_by(groups_members.c.group_id)
.having(
func.count(groups_members.c.member_name)
== len(member_names_included)
)
)
)
.all()
)
for r in result:
print(f'ORM {r}')
I hope this is helpful to anyone and if you have suggestions for improvement, please let me know.

Convert SQL query in Django model format

I'm trying to convert an SQL query into django format but as I'm quite new to django I'm having some trouble.
My query:
select make_name, count(*) as count from main_app_make as make
join main_app_model as model on make.id = model.make_id
join main_app_vehicle as vehicle on model.id = vehicle.model_id
group by make.make_name
The result:
Audi 1
Mercedes-Benz 2
My models:
class Make(models.Model):
make_name = models.CharField(max_length=50)
make_logo = models.CharField(max_length=400)
def __str__(self):
return self.make_name
class Model(models.Model):
model_name = models.CharField(max_length=50)
make = models.ForeignKey(Make, on_delete=models.CASCADE)
def __str__(self):
return self.model_name
class Vehicle(models.Model):
type = models.ForeignKey(VehicleType, on_delete=models.CASCADE)
model = models.ForeignKey(Model, on_delete=models.CASCADE)
body_type = models.ForeignKey(Body, on_delete=models.CASCADE)
...
This is what I tried:
options = Make.objects.all().values('make_name').annotate(total=Count('make_name'))
I think you need to include the children models in the Count :
options = Make.objects.values('make_name').annotate(total=Count('model_set__vehicle_set'))
Reference : https://docs.djangoproject.com/en/stable/topics/db/aggregation/#following-relationships-backwards

how override creat method in sale order odoo 10 to add po RFQ

I am try to override create method in sale order to make purchase order with current data in sale order and my code did not work
Iam try this:
#api.multi
def creat(self):
# for order in self:
# order.state = 'sale'
# order.confirmation_date = fields.Datetime.now()
# if self.env.context.get('send_email'):
# self.force_quotation_send()
# order.order_line._action_procurement_create()
# if self.env['ir.values'].get_default('sale.config.settings', 'auto_done_setting'):
# self.action_done()
self.env['purchase.order'].create({'partner_id': partner_id.id,
'location_id':location_id.id,
'pricelist_id': pricelist_id.id,
'order_line': [(0, 0, {'product_id': product_id.id,
'name': name.id,
'date_planned': date_planned.id,
'price_unit': price_unit.id})]})
return True
You have to use api.model for create method as there is no id for the record when creating a new record.
here is a sample code
#api.model
def create(self, vals):
res = super(ClassName, self).create(vals)
# Your code here
# vals will contain dictionary of all variables.
return res
Also You don't have to change code for creating purchase order from sales.
You just have to set routes(Buy in this case) for the product and product's vendor.

what should I add to display the value of fields correctly?

what should I add to display user_id and cat correctly
#api.model
def create(self, vals):
record=super(test, self).create(vals)
if vals['total'] > 0:
vals['date'] = fields.Datetime.now()
self.env['journal'].create({
'user_id': record.patient_id,
'cat': record.cat,})
....
.....
on the tree view (journal):
user_id is displayed as test.user(6,)
cat is displayed as cat1
EDITS:
class test(models.Model):
_name = 'test'
cat = fields.Selection(
required=True,
related='test_type_cat.name',
store=True,
)
user_id = fields.Many2one('res.users', string='user', readonly=True,)
.....
#api.model
def create(self, vals):
record=super(test, self).create(vals)
if vals['total'] > 0:
vals['date'] = fields.Datetime.now()
self.env['journal'].create({
'patient_id': record.patient_id.name,
'cat': record.cat,
'user_id': record.user_id.name,
})
record.total = 0
return record
why does it work with .name and not .id ?
for m2o field should I pass the integer value ? if it is the case why does it work here with .name ? and what about m2m and o2m?
this worked for you because you are creating a record in model: journal not in test model.
and if you go to journal model you will find that patient_id is Char field not a many2one field.
so if you pass: record.patient_id you are passing an object and it's converted to char this is why you get test(1,). because pateint_id is a many2one field in test model witch mean is an object.
Hope this clear thing little bit for you.

Is there anything like unique_together(max_occurences=3)?

I have a model:
class MyModel(models.Model):
a = models.IntegerField()
b = models.IntegerField()
c = models.IntegerField()
Now, I need something like unique_together(a,b, max_occurences=3) constraint to be added to the above model (so that there can be up to 3 values of c for each pair of (a,b), and ideally those 3 values of c should be also unique for given (a,b)), but I don't know what to look for (and if something like this even exists in MySQL). Is there anything like that, or I have to do something like this:
class MyModel(models.Model):
a = models.IntegerField()
b = models.IntegerField()
c1 = models.IntegerField()
c2 = models.IntegerField()
c3 = models.IntegerField()
class Meta:
unique_together = ('a', 'b')
-- and handle c1..c3 myself?
You should override the save() method for the model and check your constraint before each save and raise a ValueError if the constraint is violated.
class MyModel(models.Model):
a = models.IntegerField()
b = models.IntegerField()
c = models.IntegerField()
def save(self):
try:
# Check values in model here
except:
raise ValueError("Cannot save more than 3 Cs with an A")
super(MyModel, self).save(*args, **kwargs)