Add job reports for node categories and execution times

This commit is contained in:
greg
2022-01-05 14:03:44 -05:00
parent 4a15ddd9e0
commit 4c1ce27e0a
9 changed files with 492 additions and 48 deletions
+170 -43
View File
@@ -104,6 +104,17 @@ def get_spark_time(time_period):
return time_period, _time_period
def get_curr_item(check_item, unique_items):
"""
When rendering by item and destination_id,
render the item uniquely.
"""
if check_item in unique_items:
return ('', unique_items)
unique_items.add(check_item)
return (check_item, unique_items)
class SpecifiedDateListGrid(grids.Grid):
class JobIdColumn(grids.IntegerColumn):
@@ -154,6 +165,13 @@ class SpecifiedDateListGrid(grids.Grid):
return query.filter(and_(model.Job.table.c.user_id == model.User.table.c.id,
model.User.table.c.email == column_filter))
class DestinationIdColumn(grids.GridColumn):
def filter(self, trans, user, query, column_filter):
if column_filter == 'All':
return query
return query.filter(model.Job.table.c.destination_id == column_filter)
class SpecifiedDateColumn(grids.GridColumn):
def filter(self, trans, user, query, column_filter):
@@ -187,6 +205,9 @@ class SpecifiedDateListGrid(grids.Grid):
StateColumn("State",
key="state",
attach_popup=False),
DestinationIdColumn("Destination Id",
key="destination_id",
attach_popup=False),
ToolColumn("Tool Id",
key="tool_id",
link=(lambda item: dict(operation="tool_per_month", id=item.id, webapp="reports")),
@@ -274,6 +295,9 @@ class Jobs(BaseUIController, ReportQueryBuilder):
**kwd))
elif operation == "user_for_month":
kwd['f-email'] = util.restore_text(kwd['email'])
elif operation == "user_for_month_by_destination":
kwd['f-email'] = util.restore_text(kwd['email'])
kwd['f-destination_id'] = kwd['destination_id']
elif operation == "user_per_month":
# The received id is the job id, so we need to get the id of the user
# that submitted the job.
@@ -313,18 +337,38 @@ class Jobs(BaseUIController, ReportQueryBuilder):
trends[key][job_day] += 1
return trends
def _calculate_job_table(self, sa_session, jobs_query):
def _calculate_job_table(self, sa_session, jobs_query, by_destination=False):
jobs = []
unique_month_year_strs = set()
for row in sa_session.execute(jobs_query):
month_name = row.date.strftime("%B")
year = int(row.date.strftime("%Y"))
jobs.append((
row.date.strftime("%Y-%m"),
row.total_jobs,
month_name,
year
))
if str(by_destination).lower() == 'true':
month_year_str = "%s %s" % (month_name, year)
curr_month_year_str, unique_month_year_strs = get_curr_item(month_year_str, unique_month_year_strs)
if curr_month_year_str == '':
curr_month = ''
curr_year = ''
else:
curr_month =row.date.strftime("%B")
curr_year = row.date.strftime("%Y")
jobs.append((
row.date.strftime("%Y-%m"),
row.total_jobs,
curr_month,
curr_year,
row.user_email,
row.destination_id,
row.execute_time
))
else:
jobs.append((
row.date.strftime("%Y-%m"),
row.total_jobs,
month_name,
year
))
return jobs
@web.expose
@@ -534,9 +578,11 @@ class Jobs(BaseUIController, ReportQueryBuilder):
@web.expose
def per_month_all(self, trans, **kwd):
"""
Queries the DB for all jobs. Avoids monitor jobs.
Queries the DB for all jobs. Avoids monitor jobs. The
by_destination param will group by User.email and
Job.destination_id.
"""
by_destination = str(kwd.get('by_destination', False)).lower()
message = ''
PageSpec = namedtuple('PageSpec', ['entries', 'offset', 'page', 'pages_found'])
@@ -570,26 +616,43 @@ class Jobs(BaseUIController, ReportQueryBuilder):
monitor_user_id = get_monitor_id(trans, monitor_email)
# Use to make the page table
jobs_by_month = sa.select((self.select_month(model.Job.table.c.create_time).label('date'),
sa.func.count(model.Job.table.c.id).label('total_jobs')),
whereclause=model.Job.table.c.user_id != monitor_user_id,
from_obj=[model.Job.table],
group_by=self.group_by_month(model.Job.table.c.create_time),
order_by=[_order],
offset=offset,
limit=limit)
if by_destination == 'true':
jobs_by_month = sa.select((self.select_month(model.Job.table.c.create_time).label('date'),
model.Job.table.c.destination_id.label('destination_id'),
sa.func.sum(model.Job.table.c.update_time - model.Job.table.c.create_time).label('execute_time'),
sa.func.count(model.Job.table.c.id).label('total_jobs'),
model.User.table.c.email.label('user_email')),
whereclause=model.Job.table.c.user_id != monitor_user_id,
from_obj=[sa.join(model.Job.table, model.User.table)],
group_by=['user_email', 'date', 'destination_id'],
order_by=[_order],
offset=offset,
limit=limit)
else:
jobs_by_month = sa.select((self.select_month(model.Job.table.c.create_time).label('date'),
sa.func.count(model.Job.table.c.id).label('total_jobs')),
whereclause=model.Job.table.c.user_id != monitor_user_id,
from_obj=[model.Job.table],
group_by=self.group_by_month(model.Job.table.c.create_time),
order_by=[_order],
offset=offset,
limit=limit)
# Use to make sparkline
all_jobs = sa.select((self.select_day(model.Job.table.c.create_time).label('date'),
model.Job.table.c.id.label('id')))
trends = self._calculate_trends_for_jobs(trans.sa_session, all_jobs)
jobs = self._calculate_job_table(trans.sa_session, jobs_by_month)
jobs = self._calculate_job_table(trans.sa_session, jobs_by_month, by_destination=by_destination)
pages_found = ceil(len(jobs) / float(entries))
page_specs = PageSpec(entries, offset, page, pages_found)
return trans.fill_template('/webapps/reports/jobs_per_month_all.mako',
if by_destination == 'true':
page = '/webapps/reports/jobs_per_month_by_user_and_destination.mako'
else:
page = '/webapps/reports/jobs_per_month_all.mako'
return trans.fill_template(page,
order=order,
arrow=arrow,
sort_id=sort_id,
@@ -674,6 +737,11 @@ class Jobs(BaseUIController, ReportQueryBuilder):
@web.expose
def per_user(self, trans, **kwd):
"""
Queries the DB for jobs per user. The by_destination
param will group by Job.destination_id.
"""
by_destination = str(kwd.get('by_destination', False)).lower()
total_time = Timer()
q_time = Timer()
@@ -711,24 +779,42 @@ class Jobs(BaseUIController, ReportQueryBuilder):
page = 1
jobs = []
jobs_per_user = sa.select((model.User.table.c.email.label('user_email'),
sa.func.count(model.Job.table.c.id).label('total_jobs')),
from_obj=[sa.outerjoin(model.Job.table, model.User.table)],
group_by=['user_email'],
order_by=[_order],
offset=offset,
limit=limit)
if by_destination == 'true':
jobs_per_user = sa.select((model.User.table.c.email.label('user_email'),
sa.func.count(model.Job.table.c.id).label('total_jobs'),
model.Job.table.c.destination_id.label('destination_id')),
from_obj=[sa.outerjoin(model.Job.table, model.User.table)],
group_by=['user_email', 'destination_id'],
order_by=[_order],
offset=offset,
limit=limit)
else:
jobs_per_user = sa.select((model.User.table.c.email.label('user_email'),
sa.func.count(model.Job.table.c.id).label('total_jobs')),
from_obj=[sa.outerjoin(model.Job.table, model.User.table)],
group_by=['user_email'],
order_by=[_order],
offset=offset,
limit=limit)
q_time.start()
unique_users = set()
for row in trans.sa_session.execute(jobs_per_user):
if (row.user_email is None):
jobs.append(('Anonymous',
row.total_jobs))
curr_user, unique_users = get_curr_item('Anonymous', unique_users)
if by_destination == 'true':
jobs.append((curr_user, row.destination_id, row.total_jobs))
else:
jobs.append((curr_user, row.total_jobs))
elif (row.user_email == monitor_email):
continue
else:
jobs.append((row.user_email,
row.total_jobs))
curr_user, unique_users = get_curr_item(row.user_email, unique_users)
if by_destination == 'true':
jobs.append((curr_user, row.destination_id, row.total_jobs))
else:
jobs.append((curr_user, row.total_jobs))
q_time.stop()
query1time = q_time.time_elapsed()
@@ -773,7 +859,11 @@ class Jobs(BaseUIController, ReportQueryBuilder):
total_time.stop()
ttime = total_time.time_elapsed()
return trans.fill_template('/webapps/reports/jobs_per_user.mako',
if by_destination == 'true':
page = '/webapps/reports/jobs_per_user_by_destination.mako'
else:
page = '/webapps/reports/jobs_per_user.mako'
return trans.fill_template(page,
order=order,
arrow=arrow,
sort_id=sort_id,
@@ -789,6 +879,11 @@ class Jobs(BaseUIController, ReportQueryBuilder):
@web.expose
def user_per_month(self, trans, **kwd):
"""
Queries the DB for jobs per user per month. The
by_destination param will group by Job.destination_id.
"""
by_destination = str(kwd.get('by_destination', False)).lower()
params = util.Params(kwd)
message = ''
@@ -799,12 +894,22 @@ class Jobs(BaseUIController, ReportQueryBuilder):
arrow = specs.arrow
_order = specs.exc_order
q = sa.select((self.select_month(model.Job.table.c.create_time).label('date'),
sa.func.count(model.Job.table.c.id).label('total_jobs')),
whereclause=model.User.table.c.email == email,
from_obj=[sa.join(model.Job.table, model.User.table)],
group_by=self.group_by_month(model.Job.table.c.create_time),
order_by=[_order])
if by_destination == 'true':
q = sa.select((self.select_month(model.Job.table.c.create_time).label('date'),
model.Job.table.c.destination_id.label('destination_id'),
sa.func.sum(model.Job.table.c.update_time - model.Job.table.c.create_time).label('execute_time'),
sa.func.count(model.Job.table.c.id).label('total_jobs')),
whereclause=model.User.table.c.email == email,
from_obj=[sa.join(model.Job.table, model.User.table)],
group_by=['date', 'destination_id'],
order_by=[_order])
else:
q = sa.select((self.select_month(model.Job.table.c.create_time).label('date'),
sa.func.count(model.Job.table.c.id).label('total_jobs')),
whereclause=model.User.table.c.email == email,
from_obj=[sa.join(model.Job.table, model.User.table)],
group_by=self.group_by_month(model.Job.table.c.create_time),
order_by=[_order])
all_jobs_per_user = sa.select((model.Job.table.c.create_time.label('date'),
model.Job.table.c.id.label('job_id')),
@@ -828,19 +933,41 @@ class Jobs(BaseUIController, ReportQueryBuilder):
trends[key][job_day] += 1
jobs = []
unique_month_year_strs = set()
for row in trans.sa_session.execute(q):
jobs.append((row.date.strftime("%Y-%m"),
row.total_jobs,
row.date.strftime("%B"),
row.date.strftime("%Y")))
return trans.fill_template('/webapps/reports/jobs_user_per_month.mako',
if by_destination == 'true':
month_year_str = "%s %s" % (row.date.strftime("%B"), row.date.strftime("%Y"))
curr_month_year_str, unique_month_year_strs = get_curr_item(month_year_str, unique_month_year_strs)
if curr_month_year_str == '':
curr_month = ''
curr_year = ''
else:
curr_month =row.date.strftime("%B")
curr_year = row.date.strftime("%Y")
jobs.append((row.date.strftime("%Y-%m"),
row.execute_time,
row.total_jobs,
curr_month,
curr_year,
row.destination_id))
else:
jobs.append((row.date.strftime("%Y-%m"),
row.total_jobs,
row.date.strftime("%B"),
row.date.strftime("%Y")))
if by_destination == 'true':
page = '/webapps/reports/jobs_user_per_month_by_destination.mako'
else:
page = '/webapps/reports/jobs_user_per_month.mako'
return trans.fill_template(page,
order=order,
arrow=arrow,
sort_id=sort_id,
id=kwd.get('id'),
trends=trends,
email=util.sanitize_text(email),
jobs=jobs, message=message)
jobs=jobs,
message=message)
@web.expose
def per_tool(self, trans, **kwd):
+8 -1
View File
@@ -3,7 +3,13 @@
</%doc>
<%def name="get_page_url( sort_id, order, *args, **kwargs )">
%try:
%if str(by_destination).lower() == "true":
<a href="${h.url_for( controller=args[0], action=args[1], by_destination=True, sort_id=sort_id, order=order, **kwargs )}">${kwargs.get("page")}</a>
%endif
%except NameError:
<a href="${h.url_for( controller=args[0], action=args[1], sort_id=sort_id, order=order, **kwargs )}">${kwargs.get("page")}</a>
%endtry
</%def>
<%!
@@ -44,11 +50,12 @@
</div>
</%def>
<%def name="get_entry_selector(controller, action, entries, sort_id, order)">
<%def name="get_entry_selector(controller, action, entries, sort_id, order, by_destination=False)">
<div id="entry_form" >
<form method="post" controller=${controller} action=${action}>
<input type="hidden" value=${sort_id} name="sort_id">
<input type="hidden" value=${order} name="order">
<input type="hidden" value=${by_destination} name="by_destination">
%try:
%if spark_limit:
<input type="hidden" value=${spark_limit} name="spark_limit">
+2
View File
@@ -49,8 +49,10 @@
<div class="toolTitle"><a target="galaxy_main" href="${h.url_for( controller='jobs', action='specified_month_in_error', sort_id='default', order='default' )}">Jobs in error per day this month</a></div>
<div class="toolTitle"><a target="galaxy_main" href="${h.url_for( controller='jobs', action='specified_date_handler', operation='unfinished', sort_id='default', order='default' )}">All unfinished jobs</a></div>
<div class="toolTitle"><a target="galaxy_main" href="${h.url_for( controller='jobs', action='per_month_all', sort_id='default', order='default' )}">Jobs per month</a></div>
<div class="toolTitle"><a target="galaxy_main" href="${h.url_for( controller='jobs', action='per_month_all', by_destination=True, sort_id='default', order='default' )}">Jobs per month by user / node type</a></div>
<div class="toolTitle"><a target="galaxy_main" href="${h.url_for( controller='jobs', action='per_month_in_error', sort_id='default', order='default' )}">Jobs in error per month</a></div>
<div class="toolTitle"><a target="galaxy_main" href="${h.url_for( controller='jobs', action='per_user', sort_id='default', order='default' )}">Jobs per user</a></div>
<div class="toolTitle"><a target="galaxy_main" href="${h.url_for( controller='jobs', action='per_user', by_destination=True, sort_id='default', order='default' )}">Jobs per user / node type</a></div>
<div class="toolTitle"><a target="galaxy_main" href="${h.url_for( controller='jobs', action='per_tool', sort_id='default', order='default' )}">Jobs per tool</a></div>
<div class="toolTitle"><a target="galaxy_main" href="${h.url_for( controller='jobs', action='errors_per_tool', sort_id='default', order='default', spark_time='')}">Errors per tool</a></div>
</div>
+7 -2
View File
@@ -47,16 +47,20 @@
<td>${job.job_runner_external_id}</td>
</tr>
<tr class="header">
<td colspan="5">Remote Host</td>
<td colspan="3">Remote Host</td>
<td>Destination Id</td>
<td>Destination params</td>
</tr>
<tr>
<td colspan="5">
<td colspan="3">
%if job.galaxy_session and job.galaxy_session.remote_host:
${job.galaxy_session.remote_host}
%else:
no remote host
%endif
</td>
<td>${job.destination_id}</td>
<td>${job.destination_params}</td>
</tr>
<tr class="header">
<td colspan="5">Command Line</td>
@@ -93,3 +97,4 @@
</table>
</div>
</div>
@@ -82,7 +82,10 @@ ${get_css()}
</a>
</td>
<td>${job[1]}</td>
${make_sparkline(key, trends[key], "bar", "/ day")}
%try:
${make_sparkline(key, trends[key], "bar", "/ day")}
%except KeyError:
%endtry
<td id="${key}"></td>
</tr>
<%
@@ -0,0 +1,115 @@
<%inherit file="/base.mako"/>
<%namespace file="/message.mako" import="render_msg" />
<%namespace file="/spark_base.mako" import="make_sparkline" />
<%namespace file="/sorting_base.mako" import="get_sort_url, get_css" />
<%namespace file="/page_base.mako" import="get_pages, get_entry_selector" />
<%
import datetime
%>
%if message:
${render_msg( message, 'done' )}
%endif
<%
page = page_specs.page
offset = page_specs.offset
entries = page_specs.entries
%>
${get_css()}
<!--jobs_per_month_by_user_and_destination.mako-->
<div class="report">
<div class="reportBody">
<table id="formHeader">
<tr>
<td>
${get_pages(sort_id, order, page_specs, 'jobs', 'per_month_all')}
</td>
<td>
<h4 align="center">Jobs Per Month by User/Node type</h4>
<h5 align="center">
Click Month to view details.
Graph goes from the 1st to the last of the month.
</h5>
</td>
<td align="right">
${get_entry_selector("jobs", "per_month_all", page_specs.entries, sort_id, order, by_destination=True)}
</td>
</tr>
</table>
<table align="center" width="80%" class="colored">
%if len( jobs ) == 0:
<tr><td colspan="7">There are no jobs.</td></tr>
%else:
<tr class="header">
<td class="seventh_width">
${get_sort_url(sort_id, order, 'date', 'jobs', 'per_month_all', 'Month', page=page, offset=offset, entries=entries, by_destination=True)}
<span class='dir_arrow date'>${arrow}</span>
</td>
<td class="seventh_width">
${get_sort_url(sort_id, order, 'date', 'jobs', 'per_month_all', 'User', page=page, offset=offset, entries=entries, by_destination=True)}
<span class='dir_arrow date'>${arrow}</span>
</td>
<td class="seventh_width">
${get_sort_url(sort_id, order, 'date', 'jobs', 'per_month_all', 'Node Type', page=page, offset=offset, entries=entries, by_destination=True)}
<span class='dir_arrow date'>${arrow}</span>
</td>
<td class="seventh_width">
${get_sort_url(sort_id, order, 'total_jobs', 'jobs', 'per_month_all', 'Jobs', page=page, offset=offset, entries=entries, by_destination=True)}
<span class='dir_arrow total_jobs'>${arrow}</span>
</td>
<td class="seventh_width">
Total Execution Time: seconds
</td>
<td class="seventh_width">
Total Execution Time: hh:mm:ss
</td>
<td></td>
</tr>
<%
ctr = 0
entries = 1
%>
%for job in jobs:
<% key = str(job[2]) + str(job[3]) %>
%if entries > page_specs.entries:
<%break%>
%endif
%if ctr % 2 == 1:
<tr class="odd_row">
%else:
<tr class="tr">
%endif
<td>
<a href="${h.url_for( controller='jobs', action='specified_month_all', specified_date=job[0]+'-01', sort_id='default', order='default', by_destination=True )}">
${job[2]} ${job[3]}
</a>
</td>
<td>${job[4]}</td>
<td>${job[5]}</td>
<td>${job[1]}</td>
<td id="${key}">${job[6].seconds}</td>
<td id="${key}">${datetime.timedelta(seconds=job[6].seconds)}</td>
%try:
${make_sparkline(key, trends[key], "bar", "/ day")}
%except KeyError:
%endtry
<td id="${key}"></td>
</tr>
<%
ctr += 1
entries += 1
%>
%endfor
%endif
</table>
</div>
</div>
<!--jobs_per_month_by_user_and_destination.mako-->
+1 -1
View File
@@ -20,7 +20,6 @@ ${get_css()}
%>
<!--jobs_per_user.mako-->
${q1time}, ${q2time}, ${ttime}
<div class="report">
<div class="reportBody">
<table id="formHeader">
@@ -95,3 +94,4 @@ ${q1time}, ${q2time}, ${ttime}
</div>
</div>
<!--End jobs_per_user.mako-->
@@ -0,0 +1,101 @@
<%inherit file="/base.mako"/>
<%namespace file="/message.mako" import="render_msg" />
<%namespace file="/spark_base.mako" import="make_sparkline, make_spark_settings" />
<%namespace file="/sorting_base.mako" import="get_sort_url, get_css" />
<%namespace file="/page_base.mako" import="get_pages, get_entry_selector" />
<%!
import re
%>
%if message:
${render_msg( message, 'done' )}
%endif
${get_css()}
<%
page = page_specs.page
offset = page_specs.offset
entries = page_specs.entries
%>
<!--jobs_per_user_by_destination.mako-->
<div class="report">
<div class="reportBody">
<table id="formHeader">
<tr>
<td>
${get_pages(sort_id, order, page_specs, 'jobs', 'per_user', by_destination=True, spark_time=time_period)}
</td>
<td>
<h4 align="center">Jobs Per User / Node Type</h4>
<h5 align="center">
Click User to view details.
Graph goes from present to past
${make_spark_settings("jobs", "per_user", spark_limit, sort_id, order, time_period, page=page, offset=offset, entries=entries, by_destination=True)}
</h5>
</td>
<td align="right">
${get_entry_selector("jobs", "per_user", page_specs.entries, sort_id, order, by_destination=True)}
</td>
</tr>
</table>
<table align="center" width="60%" class="colored">
%if len( jobs ) == 0:
<tr><td colspan="2">There are no jobs.</td></tr>
%else:
<tr class="header">
<td class="third_width">
${get_sort_url(sort_id, order, 'user_email', 'jobs', 'per_user', 'User', spark_time=time_period, page=page, offset=offset, entries=entries, by_destination=True)}
<span class='dir_arrow user_email'>${arrow}</span>
</td>
<td class="third_width">
${get_sort_url(sort_id, order, 'destination_id', 'jobs', 'per_user', 'Node Type', spark_time=time_period, page=page, offset=offset, entries=entries, by_destination=True)}
<span class='dir_arrow user_email'>${arrow}</span>
</td>
<td class="third_width">
${get_sort_url(sort_id, order, 'total_jobs', 'jobs', 'per_user', 'Total Jobs', spark_time=time_period, page=page, offset=offset, entries=entries, by_destination=True)}
<span class='dir_arrow total_jobs'>${arrow}</span>
</td>
<td></td>
</tr>
<%
ctr = 0
entries = 1
%>
%for job in jobs:
<% key = re.sub(r'\W+', '', job[0]) %>
%if entries > page_specs.entries:
<%break%>
%endif
%if ctr % 2 == 1:
<tr class="odd_row">
%else:
<tr class="tr">
%endif
<td>
<a href="${h.url_for( controller='jobs', action='user_per_month', email=job[0], by_destination=True, sort_id='default', order='default' )}">
${job[0]}
</a>
</td>
<td>${job[1]}</td>
<td>${job[2]}</td>
%try:
${make_sparkline(key, trends[key], "bar", "/ " + time_period[:-1])}
%except KeyError:
%endtry
<td id="${key}"></td>
</tr>
<%
ctr += 1
entries += 1
%>
%endfor
%endif
</table>
</div>
</div>
<!--End jobs_per_user_by_destination.mako-->
@@ -0,0 +1,84 @@
<%inherit file="/base.mako"/>
<%namespace file="/message.mako" import="render_msg" />
<%namespace file="/spark_base.mako" import="make_sparkline" />
<%namespace file="/sorting_base.mako" import="get_sort_url, get_css" />
<%
import datetime
from galaxy import util
%>
%if message:
${render_msg( message, 'done' )}
%endif
${get_css()}
<%
_email = util.restore_text( email )
%>
<!--jobs_user_per_month_by_destination.mako-->
<div class="report">
<div class="reportBody">
<h3 align="center">Jobs per month for user "${_email}" / node type</h3>
<h4 align="center">
<p>Click Total Jobs to see the user's jobs for that month</p>
<p>Graph goes from first of the month to the last</p>
</h4>
<table align="center" width="80%" class="colored">
%if len( jobs ) == 0:
<tr>
<td colspan="2">
There are no jobs for user "${ _email }"
</td>
</tr>
%else:
<tr class="header">
<td class="fifth_width">
${get_sort_url(sort_id, order, 'date', 'jobs', 'user_per_month', 'Month', email=email, by_destination=True)}
<span class='dir_arrow date'>${arrow}</span>
</td>
<td class="fifth_width">
Node Type
</td>
<td class="fifth_width">
${get_sort_url( sort_id, order, 'total_jobs', 'jobs', 'user_per_month', 'Total Jobs', email=email, by_destination=True)}
<span class='dir_arrow total_jobs'>${arrow}</span>
</td>
<td class="fifth_width">
Total Execution Time: seconds
</td>
<td class="fifth_width">
Total Execution Time: hh:mm:ss
</td>
</tr>
<% ctr = 0 %>
%for job in jobs:
<% key = job[3] + job[4] %>
%if ctr % 2 == 1:
<tr class="odd_row">
%else:
<tr class="tr">
%endif
<td>${job[3]}&nbsp;${job[4]}</td>
<td>${job[5]}</td>
<td>
<a href="${h.url_for( controller='jobs', action='specified_date_handler', operation='user_for_month_by_destination', email=email, specified_date=job[0], destination_id=job[5], sort_id='default', order='default')}">
${job[2]}
</a>
</td>
<td id="${key}">${job[1].seconds}</td>
<td id="${key}">${datetime.timedelta(seconds=job[1].seconds)}</td>
%try:
${make_sparkline(key, trends[key], "bar", "/ day")}
%except KeyError:
%endtry
</tr>
<% ctr += 1 %>
%endfor
%endif
</table>
</div>
</div>
<!--End jobs_user_per_month_by_destination.mako-->