Am I using the best SQLAlchemy query? - python

I've got an Opportunity table and an Owner table. The Opportunity table has a many to one relationship with Owner. I'm using Python, SQLAlchemy, FLask-Alchemy and SQLite on this project. I need to do an inner join on the Opportunity table against the Owner table and return a single result set (with fields from both tables).
I need to build a dictionary out of the result set so that I can easily generate Json. The following code all works... but... It took a ton of debugging to figure out how to transform the result set returned (which, in this case is a generator object) into a dictionary.
Did I make this harder than I needed to? Is there a better way to do this using SQLAlchemy? (such as using the expression language instead of the ORM)
owner = db.session.query(Owner).filter(Owner.id == owner_id).first()
if owner is None:
return None
result = {'data': []}
report_date = datetime.datetime.strptime('2016-05-01', '%Y-%m-%d')
rows = db.session.query(Opportunity)\
.filter(Opportunity.status == 'Won',
Opportunity.owner == owner,
or_(Opportunity.actual_close_date_iso_year_week == '2016/16',\
Opportunity.actual_close_date_iso_year_week == '2016/17'),
Opportunity.report_date == report_date)\
.order_by(Opportunity.account)\
.join(Owner, Owner.id == Opportunity.owner_id)\
.values(Opportunity.account,
Opportunity.report_date_iso_year_week,
Opportunity.report_date,
Owner.first_name)
for row in rows:
result_row = {}
fields = getattr(row, '_fields')
for field in fields:
result_row[field] = getattr(row, field)
result['data'].append(result_row)

I think that basically this is the way to get what you need, however I would suggest few minor changes:
You don't really need to join, as after .filter(Opportunity.owner==owner) all the rows that you get from the Opportunity table have the same owner_id.
I think it's better to define the list of needed fields once, instead of trying to get it from each row tuple.
So, the code may be like this:
required_fields = ('account','report_date_iso_year_week','report_date')
owner = db.session.query(Owner).filter(Owner.id == owner_id).first()
if owner is None:
return None
result = {'data': []}
report_date = datetime.datetime.strptime('2016-05-01', '%Y-%m-%d')
rows = db.session.query(Opportunity)\
.filter(Opportunity.status == 'Won',
Opportunity.owner == owner,
or_(Opportunity.actual_close_date_iso_year_week == '2016/16',\
Opportunity.actual_close_date_iso_year_week == '2016/17'),
Opportunity.report_date == report_date)\
.order_by(Opportunity.account)\
.values(*[getattr(Opportunity,f) for f in required_fields])
for row in rows:
result_row = {'first_name':owner.first_name}
for field in required_fields:
result_row[field] = getattr(row, field)
result['data'].append(result_row)

Related

Is there a built-in method for Many-to-many relationships with Flask, SQLAlchemy, & WTForms

I'm using flask, WTForms, & SQLAlchemy to edit the database via a webpage. I have a table for a many-to-many relationship between elements and tasks. The model for this table is this:
class tasks_elements(db.Model):
id = db.Column(db.Integer, primary_key=True)
tasks_id = db.Column(db.Integer(), db.ForeignKey('tasks.id'))
elements_id = db.Column(db.Integer(), db.ForeignKey('elements.id'))
On the Task page I use this to pull the relationships for a particular task:
taskellist = tasks_elements.query.filter_by(tasks_id=Id)
There is a multi-checkbox on the form where users put a checkbox beside elements they want associated with a task. I need this list to reflect the associations and for items to be selectable and removable. I populate the currently selected elements before adding them to the form like so:
thisItem = tasks.query.filter_by(id=Id).first()
ellist = elements.query.with_entities(elements.id, elements.description, elements.host).order_by(elements.description)
thisItem.elCheck = []
for check in taskellist:
for el in ellist:
if el.id == check.elements_id:
thisItem.elCheck.append(el.id)
I then check the form once submitted and add or delete as required:
for i in form.elCheck.data:
p = 0
for q in taskellist:
if i == q.elements_id:
p += 1
if p == 0:
newTaskEl = tasks_elements(tasks_id=Id, elements_id=i)
db.session.add(newTaskEl)
for o in taskellist:
a = 0
for w in form.elCheck.data:
if o.elements_id == w:
a += 1
if a == 0:
db.session.delete(o)
This works as intended, but it seems terribly inefficient and I feel like I'm missing something that should make this more concise, like I'm opening the can from the wrong end.
Question: is this the correct way of doing this for many to many relationships, or is there a better way?

Orator ORM model Create method invalid SQL

I have a database I created with a migration. One of my tables looks like this
def create_customer_table(self):
with self.schema.create("customer") as table:
table.char("name",120).unique()
table.integer("transmitting_hours").default(24) #how many hours after transmission vehicle is considered transmitting
table.boolean("is_tpms").default(False)
table.boolean("is_dor").default(False)
table.boolean("is_otr").default(False)
table.boolean("is_track_and_trace").default(False)
table.char("contact_person",25)
table.char("created_by",25)
table.enum("temperature_unit",TEMP_UNITS)
table.enum("pressure_unit",PRESSURE_UNITS)
table.enum("distance_unit",DISTANCE_UNITS)
table.char("time_zone",25)
table.char("language",2)
table.timestamps()
I have a very simplistic ORM model on top
class Customer(Model):
__table__ = "customer"
__timestamps__ = False
__primary_key__ = "name"
__fillable__ = ['*']
I then try to do a basic insert with the following code
def add_sample_customer():
sample_customer = {}
sample_customer["name"] = "customer_2"
sample_customer["contact_person"] = "Abradolf"
sample_customer["created_by"] = "Frodo"
sample_customer["time_zone"] = "GMT-5"
sample_customer["language"] = "EN"
sample_customer["temperature_unit"] = "FAHRENHEIT"
sample_customer["pressure_unit"] = "PSI"
sample_customer["distance_unit"] = "MI"
customer_model = Customer.create(_attributes = sample_customer)
The exception I get from this code looks like
orator.exceptions.query.QueryException: syntax error at or near ")"
LINE 1: INSERT INTO "customer" () VALUES () RETURNING "name"
(SQL: INSERT INTO "customer" () VALUES () RETURNING "name" ([]))
it looks like orator just isn't filling in the cols and vals here. I have also tried it with a few different syntactic ways of dropping the dict in there, using **sample_customer and also just putting the dict in directly and none of them work, all with the same exception. I started debugging by printing stuff out of the orator libraries but haven't gotten anywhere yet.
my inserts work if I do the model attribute assignment individually and use the model.save() method like this
def add_sample_customer():
sample_customer = {}
sample_customer["name"] = "customer_2"
sample_customer["contact_person"] = "Abradolf"
sample_customer["created_by"] = "Frodo"
sample_customer["time_zone"] = "GMT-5"
sample_customer["language"] = "EN"
sample_customer["temperature_unit"] = "FAHRENHEIT"
sample_customer["pressure_unit"] = "PSI"
sample_customer["distance_unit"] = "MI"
customer_model = Customer()
for k,v in sample_customer.items():
setattr(customer_model,k,v)
customer_model.save()
Does anyone understand why the model.create() syntax fails?
I would think the answer would be:
Simply passing the dictionary instead of using keyword notation with attributes:
Customer.create(sample_customer)
or
Customer.create(attribute=value,attribute2=value2,..etc)
Which are the valid notations

Django-tables2 - can't I use [A('argument')] inside the "text" parameter?

I'm trying to make this table with a clickable field which changes the boolean for the entry to its opposite value. It works, but I want an alternative text as "False" or "True" does not look nice, and the users are mainly Norwegian.
def bool_to_norwegian(boolean):
if boolean:
return "Ja"
else:
return "Nei"
class OrderTable(tables.Table):
id = tables.LinkColumn('admin_detail', args=[A('id')])
name = tables.Column()
address = tables.Column()
order = tables.Column()
order_placed_at = tables.DateTimeColumn()
order_delivery_at = tables.DateColumn()
price = tables.Column()
comment = tables.Column()
sent = tables.LinkColumn('status_sent', args=[A('id')])
paid = tables.LinkColumn('status_paid', args=[A('id')], text=[A('paid')])
class Meta:
attrs = {'class': 'order-table'}
If you look under the "paid" entry I am testing this right now, why can't I access the data with the same accessor as I do in the args? If I change the args to args=[A('paid')] and look at the link, it does indeed have the correct data on it. The model names are the same as the ones in this table, and "paid" and "sent" are BooleanFields.
This is kind of what I ultimately want:
text=bool_to_norwegian([A('paid')])
Here is what I send to the table:
orders = Order.objects.order_by("-order_delivery_at")
orders = orders.values()
table = OrderTable(orders)
RequestConfig(request).configure(table)
The text argument expects a callable that accepts a record, and returns a text value. You are passing it a list (which it will just ignore), and your function is expecting a boolean instead of a record. There is also no need for using accessors here.
Something like this should work:
def bool_to_norwegian(record):
if record.paid:
return "Ja"
else:
return "Nei"
Then in your column:
paid = tables.LinkColumn('status_paid', text=bool_to_norwegian)
(Note, it is not clear from your question where the data is coming from - is paid a boolean? You may need to adjust this to fit).
As an aside, the way you are passing args to your columns is weird (it seems the documentation also recommends this, but I don't understand why - it's very confusing). A more standard approach would be:
id = tables.LinkColumn('admin_detail', A('id'))
or using named arguments:
id = tables.LinkColumn('admin_detail', accessor=A('id'))

Is it possible to treat dictionary values as objects?

Here is a class that analyses data:
class TopFive:
def __init__(self, catalog_data, sales_data, query, **kwargs):
self.catalog_data = catalog_data
self.sales_data = sales_data
self.query = query
def analyse(self):
CATALOG_DATA = self.catalog_data
SALES_DATA = self.sales_data
query = self.query
products = {}
# Creating a dict with ID, city or hour ( depending on query ) as keys and their income as values.
for row in SALES_DATA:
QUERIES = {
'category': row[0],
'city': row[2],
'hour': row[3]
}
if QUERIES[query] in products:
products[QUERIES[query]] += float(row[4])
products[QUERIES[query]] = round(products[QUERIES[query]], 2)
else:
products[QUERIES[query]] = float(row[4])
if query == 'category':
top_five = {}
top_five_items = sorted(products, key=products.get, reverse=True)[:5] # Getting top 5 categories.
for key in top_five_items:
for row in CATALOG_DATA:
if key == row[0]:
key_string = row[5] + ', ' + row[4]
top_five[key_string] = products[key]
return top_five
else:
return products
It is being called like so:
holder = TopFive(catalog_data=catalog_data, sales_data=sales_data, query='hour')
top_hour = holder.analyse()
What I want to do now is work with the dates. They come in from an input csv file looking like this:
2015-12-11T17:14:05+01:00
Now I need to change to UTC time zone. I thought of using:
.astimezone(pytz.utc)
And now to my question: Can I somehow do so in my QUERIES dictionary, so that when the 'hour' argument is passed to the class I can then execute the program, without changing the following code's structure:
if QUERIES[query] in products:
products[QUERIES[query]] += float(row[4])
products[QUERIES[query]] = round(products[QUERIES[query]], 2)
else:
products[QUERIES[query]] = float(row[4])
and without adding more conditions.
I am thinking of something like:
'hour': row[3].astimezone(pytz.utc)
But this is not working. I can understand why, I am just wondering if there is a similar approach that works. Otherwise I would have to add yet another condition with separate return value and work there.
Got it! The answer to my question is yes: you can use methods in dictionary, just as I tried:
QUERIES = {
'category': row[0],
'city': row[2],
'hour': hour.astimezone(pytz.utc)
}
What I just realized was that I forgot to parse the csv input into datetime format. So obviously when I try to use .astimezone on string it raises error. Sorry for the long useless post, but I'm still very new to OOP and its quite difficult keeping track of all files, instances and so on ;D Thanks

How to compute a databasefield with the field-id

Model:
db.define_table('orders',
Field('customer_id', db.customer)
Field('order_id', 'string')
)
I want to get a special order_id like XY-150012 where XY is part of the customer name, 15 is the year and 12 the id the actual record-id of orders. I tried in the model:
db.orders.order_id.compute = lambda r: "%s-%s00%s" % (db.customer(r['customer_id']).short, str(request.now.year)[2:], r['id'])
The id is never recognized, the computation ends up as None. If I remove r['id'] from the compute-line it works.
EDIT:
After adding an extra field field('running_number', 'integer') to the model I can access this fields content.
Is there a easy way to set this fields default=db.orders.id?
SOLUTION:
With Anthony´s Input, and reading about recursive selects I came up with this solution:
db.define_table('orders',
Field('customer_id', db.customer),
Field('order_id', 'string', default = None))
def get_order_id(id, short):
y = str(request.now.year)[2:]
return '%s-%s00%s' % (short, y, id)
def set_id_after_insert(fields,id):
fields.update(id=id)
def set_order_id_after_update(s,f):
row = s.select().first()
if row['order_id'] == None:
s.update_naive(order_id=get_order_id(row['id'], row['customer_id'].short)
else:
return
db.orders._after_insert.append(lambda f,id: set_id_after_insert(f,id))
db.orders._after_update.append(lambda s,f: set_order_id_after_update(s,f))
The problem is that the record ID is not known until after the record has been inserted in the database, as the id field is an auto-incrementing integer field whose value is generated by the database, not by web2py.
One option would be to define an _after_insert callback that updates the order_id field after the insert:
def order_after_insert(fields, id):
fields.update(id=id)
db(db.order.id == id).update(order_id=db.order.order_id.compute(fields))
db.order._after_insert.append(order_after_insert)
You might also want to create an _after_update callback, but in that case, be sure to use the update_naive argument in both callbacks when defining the Set (see above link for details).
Depending on how the order_id is used, another option might be a virtual field.

Categories

Resources