Create a path to directly download the spreadsheet file (and avoid the weird dance on the front end of making an API call to get file data.)

Fixing pagination.  Seems the front end uses a page_index that is 0 based, and sqlAlchemy prefers to start at 1.
This commit is contained in:
Dan 2022-03-12 16:19:07 -05:00
parent a536a79e87
commit 2fc4b44ef3
6 changed files with 60 additions and 15 deletions

View File

@ -293,14 +293,21 @@ paths:
schema: schema:
type: integer type: integer
format: int32 format: int32
- name : auth_token
in : query
required : true
description : User Auth Toeken
schema:
type: string
get: get:
operationId: crc.api.study.download_logs_for_study operationId: crc.api.study.download_logs_for_study
summary: Returns a csv file of logged events that occured within a study summary: Returns a csv file of logged events that occured within a study
security: [] # Will verify manually with provided Auth Token.
tags: tags:
- Studies - Studies
responses: responses:
'200': '200':
description: Returns the csv file of logged events description: Returns the spreadsheet file of logged events
content: content:
application/octet-stream: application/octet-stream:
schema: schema:

View File

@ -2,7 +2,6 @@ from datetime import datetime
from flask import g, send_file from flask import g, send_file
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from crc import session from crc import session
from crc.api.common import ApiError, ApiErrorSchema from crc.api.common import ApiError, ApiErrorSchema
from crc.models.study import Study, StudyEventType, StudyModel, StudySchema, StudyForUpdateSchema, \ from crc.models.study import Study, StudyEventType, StudyModel, StudySchema, StudyForUpdateSchema, \
@ -15,6 +14,7 @@ from crc.services.user_service import UserService
from crc.services.workflow_processor import WorkflowProcessor from crc.services.workflow_processor import WorkflowProcessor
from crc.services.workflow_service import WorkflowService from crc.services.workflow_service import WorkflowService
from crc.services.workflow_spec_service import WorkflowSpecService from crc.services.workflow_spec_service import WorkflowSpecService
from crc.api.user import verify_token
import io import io
@ -117,11 +117,16 @@ def get_study_associates(study_id):
def get_logs_for_study(study_id, body): def get_logs_for_study(study_id, body):
task_log_query = TaskLogQuery(**body) task_log_query = TaskLogQuery(**body)
task_log_query.study_id = study_id # Force the study id
return TaskLogQuerySchema().dump( return TaskLogQuerySchema().dump(
TaskLoggingService.get_logs_for_study_paginated(study_id, task_log_query)) TaskLoggingService.get_logs_for_study_paginated(study_id, task_log_query))
def download_logs_for_study(study_id): def download_logs_for_study(study_id, auth_token):
# Download links incorporate an auth token in the request for direct download
if not verify_token(auth_token):
raise ApiError('not_authenticated', 'You need to include an authorization token in the URL with this')
title = f'Study {study_id}' title = f'Study {study_id}'
logs, headers = TaskLoggingService.get_log_data_for_download(study_id) logs, headers = TaskLoggingService.get_log_data_for_download(study_id)
spreadsheet = SpreadsheetService.create_spreadsheet(logs, headers, title) spreadsheet = SpreadsheetService.create_spreadsheet(logs, headers, title)

View File

@ -1,6 +1,10 @@
import enum import enum
import urllib
import flask
import marshmallow import marshmallow
from flask import url_for
from marshmallow.fields import Method
from crc import db, ma from crc import db, ma
from crc.models.study import StudyModel from crc.models.study import StudyModel
@ -62,9 +66,10 @@ class TaskLogModelSchema(ma.Schema):
class TaskLogQuery: class TaskLogQuery:
"""Encapsulates the paginated queries and results when retrieving and filtering task logs over the """Encapsulates the paginated queries and results when retrieving and filtering task logs over the
API""" API"""
def __init__(self, code="", level="", user="", page=1, per_page=10, def __init__(self, study_id=None, code="", level="", user="", page=0, per_page=10,
sort_column=None, sort_reverse=False, items=None, sort_column=None, sort_reverse=False, items=None,
pages=0, total=0, has_next=False, has_prev=False): pages=0, total=0, has_next=False, has_prev=False, download_url=None):
self.study_id = study_id # Filter on Study.
self.code = code # Filter on code. self.code = code # Filter on code.
self.level = level # Filter on level. self.level = level # Filter on level.
self.user = user # Filter on user. self.user = user # Filter on user.
@ -77,11 +82,12 @@ class TaskLogQuery:
self.pages = pages self.pages = pages
self.has_next = False self.has_next = False
self.has_prev = False self.has_prev = False
self.download_url = None
def update_from_sqlalchemy_paginator(self, paginator): def update_from_sqlalchemy_paginator(self, paginator):
"""Updates this with results that are returned from the paginator""" """Updates this with results that are returned from the paginator"""
self.items = paginator.items self.items = paginator.items
self.page = paginator.page self.page = paginator.page - 1
self.per_page = paginator.per_page self.per_page = paginator.per_page
self.pages = paginator.pages self.pages = paginator.pages
self.has_next = paginator.has_next self.has_next = paginator.has_next
@ -94,5 +100,18 @@ class TaskLogQuerySchema(ma.Schema):
model = TaskLogModel model = TaskLogModel
fields = ["code", "level", "user", fields = ["code", "level", "user",
"page", "per_page", "sort_column", "sort_reverse", "items", "pages", "total", "page", "per_page", "sort_column", "sort_reverse", "items", "pages", "total",
"has_next", "has_prev"] "has_next", "has_prev", "download_url"]
items = marshmallow.fields.List(marshmallow.fields.Nested(TaskLogModelSchema)) items = marshmallow.fields.List(marshmallow.fields.Nested(TaskLogModelSchema))
download_url = Method("get_url")
def get_url(self, obj):
token = 'not_available'
if hasattr(obj, 'study_id') and obj.study_id is not None:
file_url = url_for("/v1_0.crc_api_study_download_logs_for_study", study_id=obj.study_id, _external=True)
if hasattr(flask.g, 'user'):
token = flask.g.user.encode_auth_token()
url = file_url + '?auth_token=' + urllib.parse.quote_plus(token)
return url
else:
return ""

View File

@ -89,7 +89,7 @@ class TaskLoggingService(object):
sort_column = desc(task_log_query.sort_column) sort_column = desc(task_log_query.sort_column)
else: else:
sort_column = task_log_query.sort_column sort_column = task_log_query.sort_column
paginator = sql_query.order_by(sort_column).paginate(task_log_query.page, task_log_query.per_page, paginator = sql_query.order_by(sort_column).paginate(task_log_query.page + 1, task_log_query.per_page,
error_out=False) error_out=False)
task_log_query.update_from_sqlalchemy_paginator(paginator) task_log_query.update_from_sqlalchemy_paginator(paginator)
return task_log_query return task_log_query

View File

@ -153,7 +153,7 @@ class TestTaskLogging(BaseTest):
logs = TaskLoggingService().get_logs_for_study_paginated(study.id, TaskLogQuery(per_page=5)) logs = TaskLoggingService().get_logs_for_study_paginated(study.id, TaskLogQuery(per_page=5))
self.assertEqual(40, logs.total) self.assertEqual(40, logs.total)
self.assertEqual(5, len(logs.items), "I can limit results to 5") self.assertEqual(5, len(logs.items), "I can limit results to 5")
self.assertEqual(1, logs.page) self.assertEqual(0, logs.page)
self.assertEqual(8, logs.pages) self.assertEqual(8, logs.pages)
self.assertEqual(5, logs.per_page) self.assertEqual(5, logs.per_page)
self.assertEqual(True, logs.has_next) self.assertEqual(True, logs.has_next)
@ -166,3 +166,5 @@ class TestTaskLogging(BaseTest):
logs = TaskLoggingService.get_logs_for_study_paginated(study.id, TaskLogQuery(per_page=5, sort_column="level", sort_reverse=True)) logs = TaskLoggingService.get_logs_for_study_paginated(study.id, TaskLogQuery(per_page=5, sort_column="level", sort_reverse=True))
for i in range(0, 5): for i in range(0, 5):
self.assertEqual('info', logs.items[i].level, "It is possible to sort on a column") self.assertEqual('info', logs.items[i].level, "It is possible to sort on a column")

View File

@ -1,7 +1,10 @@
import json
from tests.base_test import BaseTest from tests.base_test import BaseTest
from crc import session from crc import session
from crc.models.task_log import TaskLogModel from crc.models.task_log import TaskLogModel, TaskLogQuery, TaskLogQuerySchema
from crc.models.user import UserModel
from openpyxl import load_workbook from openpyxl import load_workbook
from io import BytesIO from io import BytesIO
@ -40,10 +43,19 @@ class TestDownloadLogsForStudy(BaseTest):
'message': 'This is a third message.'} 'message': 'This is a third message.'}
self.add_log(study_id, workflow.id, task.name, 'empty_workflow', log_data) self.add_log(study_id, workflow.id, task.name, 'empty_workflow', log_data)
rv = self.app.get(f'/v1.0/study/{study_id}/log/download', # Run the query, which should include a 'download_url' link that we can click on.
content_type="application/json", url = f'/v1.0/study/{workflow.study_id}/log'
headers=self.logged_in_headers()) task_log_query = TaskLogQuery()
user = session.query(UserModel).filter_by(uid=self.test_uid).first()
rv = self.app.put(url, headers=self.logged_in_headers(user), content_type="application/json",
data=TaskLogQuerySchema().dump(task_log_query))
self.assert_success(rv)
log_query = json.loads(rv.get_data(as_text=True))
self.assertIsNotNone(log_query['download_url'])
# Use the provided link to get the file.
rv = self.app.get(log_query['download_url'])
self.assert_success(rv)
wb = load_workbook(BytesIO(rv.data)) wb = load_workbook(BytesIO(rv.data))
ws = wb.active ws = wb.active