Skip to content

Commit

Permalink
Add download to query interface
Browse files Browse the repository at this point in the history
  • Loading branch information
jochenklar committed Sep 10, 2024
1 parent 00de29e commit 28caa59
Show file tree
Hide file tree
Showing 11 changed files with 186 additions and 9 deletions.
12 changes: 11 additions & 1 deletion daiquiri/core/assets/js/utils/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,14 @@ const encodeParams = params => {
}).join('&')
}

export { encodeParams }
const downloadFile = url => {
const iframe = document.createElement('iframe')
iframe.style.display = 'none'
iframe.src = url
iframe.onload = function() {
this.parentNode.removeChild(this)
}
document.body.appendChild(iframe)
}

export { downloadFile, encodeParams }
2 changes: 1 addition & 1 deletion daiquiri/core/settings/django.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@
EMAIL_PORT = env.get('EMAIL_PORT', '25')
EMAIL_USE_TLS = env.get_bool('EMAIL_USE_TLS')

SENDFILE_BACKEND = env.get('SENDFILE_BACKEND', 'sendfile.backends.simple')
SENDFILE_BACKEND = env.get('SENDFILE_BACKEND', 'django_sendfile.backends.simple')

MEMCACHE_KEY_PREFIX = env.get('MEMCACHE_KEY_PREFIX')
if MEMCACHE_KEY_PREFIX:
Expand Down
10 changes: 9 additions & 1 deletion daiquiri/core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,5 +393,13 @@ def handle_file_upload(directory, file):

return file_path


def sanitize_str(strval):
return re.sub(r'[^a-zA-Z0-9]', '_', strval.lower())
return re.sub(r'[^a-zA-Z0-9]', '_', strval.lower())


def get_file_size(file_path):
try:
return os.stat(file_path).st_size
except FileNotFoundError:
return 0
20 changes: 19 additions & 1 deletion daiquiri/query/assets/js/query/api/QueryApi.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import BaseApi from 'daiquiri/core/assets/js/api/BaseApi'
import { encodeParams } from 'daiquiri/core/assets/js/utils/api'
import { downloadFile, encodeParams } from 'daiquiri/core/assets/js/utils/api'

class QueryApi extends BaseApi {

Expand Down Expand Up @@ -27,6 +27,10 @@ class QueryApi extends BaseApi {
return this.get('/query/api/querylanguages/')
}

static fetchDownloadFormats() {
return this.get('/query/api/downloadformats/')
}

static fetchPhases() {
return this.get('/query/api/phases/')
}
Expand Down Expand Up @@ -92,6 +96,20 @@ class QueryApi extends BaseApi {
return this.delete(`/query/api/jobs/${id}/`)
}

static submitDownloadJob(id, downloadKey, downloadFormatKey) {
return this.post(`/query/api/jobs/${id}/download/${downloadKey}/`, { format_key: downloadFormatKey })
}

static fetchDownloadJob(id, downloadKey, downloadId) {
const url = `/query/api/jobs/${id}/download/${downloadKey}/${downloadId}/`
return this.get(`${url}?download=`).then((response) => {
if (response.phase === 'COMPLETED') {
downloadFile(url)
}
return response
})
}

}

export default QueryApi
90 changes: 89 additions & 1 deletion daiquiri/query/assets/js/query/components/job/JobDownload.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,96 @@
import React from 'react'
import PropTypes from 'prop-types'

import { bytes2human } from 'daiquiri/core/assets/js/utils/bytes'

import { useDownloadFormatsQuery, useDownloadJobQuery } from '../../hooks/queries'
import { useSubmitDownloadJobMutation } from '../../hooks/mutations'

const JobDownload = ({ job }) => {
return <pre>Download {job.id}</pre>
const mutation = useSubmitDownloadJobMutation()
const downloadJobId = mutation.data && mutation.data.id

const { data: downloadFormats } = useDownloadFormatsQuery()
const { data: downloadJob} = useDownloadJobQuery(job.id, 'table', downloadJobId)

const handleSubmit = (downloadFormat) => {
mutation.mutate({ job, downloadKey: 'table', downloadFormatKey: downloadFormat.key })
}

const getDownloadJobInfo = () => {
switch (downloadJob.phase) {
case 'QUEUED':
return (
<p className="text-info-emphasis">
{gettext('The download has been queued on the server.')}
</p>
)
case 'EXECUTING':
return (
<p className="text-info-emphasis">
{interpolate(gettext('The file is currently created on the server (current size: %s).'),
[bytes2human(downloadJob.size)])}
{' '}
{gettext('Once completed, the download will start automatically.')}
</p>
)
case 'COMPLETED':
return (
<p className="text-success">
{gettext('The file was successfully created on the server, the download should start now.')}
</p>
)
case 'ERROR':
return (
<p className="text-danger">
{gettext('An error occured while creating the file.')}
{' '}
{gettext('Please contact the maintainers of this site, if the problem persists.')}
</p>
)
default:
return null
}
}

return (
<div>
<p>
{gettext('For further processing of the data, you can download the results table' +
' to your local machine. For this file several formats are available.' +
' Please choose a format for the download from the list below.')}
</p>

<h4>{gettext('Download table')}</h4>

{
downloadFormats && downloadFormats.map((downloadFormat, index) => (
<div key={index} className="row">
<div className="col-md-3">
<p>
<button className="btn btn-link text-start" onClick={() => handleSubmit(downloadFormat)}>
{downloadFormat.label}
</button>
</p>
</div>
<div className="col-md-9">
<p>
{downloadFormat.help}
</p>
</div>
</div>
))
}

{
downloadJob && (
<div>
{getDownloadJobInfo()}
</div>
)
}
</div>
)
}

JobDownload.propTypes = {
Expand Down
8 changes: 8 additions & 0 deletions daiquiri/query/assets/js/query/hooks/mutations.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,11 @@ export const useAbortJobMutation = () => {
}
})
}

export const useSubmitDownloadJobMutation = () => {
return useMutation({
mutationFn: (variables) => {
return QueryApi.submitDownloadJob(variables.job.id, variables.downloadKey, variables.downloadFormatKey)
}
})
}
20 changes: 20 additions & 0 deletions daiquiri/query/assets/js/query/hooks/queries.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ export const useQueryLanguagesQuery = () => {
})
}

export const useDownloadFormatsQuery = () => {
return useQuery({
queryKey: ['downloads'],
queryFn: () => QueryApi.fetchDownloadFormats(),
placeholderData: keepPreviousData
})
}

export const useQueuesQuery = () => {
return useQuery({
queryKey: ['queues'],
Expand Down Expand Up @@ -123,3 +131,15 @@ export const useVizierQuery = (url, search) => {
enabled: !isEmpty(search)
})
}

export const useDownloadJobQuery = (jobId, downloadKey, downloadJobId) => {
return useQuery({
queryKey: ['downloadJob', jobId, downloadKey, downloadJobId],
queryFn: () => QueryApi.fetchDownloadJob(jobId, downloadKey, downloadJobId),
placeholderData: keepPreviousData,
enabled: !isEmpty(downloadJobId),
refetchInterval: (query) => {
return (query.state.data && ['QUEUED', 'EXECUTING'].includes(query.state.data.phase)) ? refetchInterval : false
}
})
}
4 changes: 2 additions & 2 deletions daiquiri/query/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ class Meta:
verbose_name = _('DownloadJob')
verbose_name_plural = _('DownloadJobs')

@property
@cached_property
def file_path(self):
if not self.owner:
username = 'anonymous'
Expand Down Expand Up @@ -493,7 +493,7 @@ class Meta:
verbose_name = _('QueryArchiveJob')
verbose_name_plural = _('QueryArchiveJob')

@property
@cached_property
def file_path(self):
if not self.owner:
username = 'anonymous'
Expand Down
11 changes: 11 additions & 0 deletions daiquiri/query/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,17 @@ def get_id(self, obj):
return '%(key)s-%(version)s' % obj



class QueryDownloadFormatSerializer(serializers.Serializer):

key = serializers.CharField()
extension = serializers.CharField()
content_type = serializers.CharField()
label = serializers.CharField()
help = serializers.CharField()



class ExampleSerializer(serializers.ModelSerializer):

class Meta:
Expand Down
3 changes: 2 additions & 1 deletion daiquiri/query/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from .views import ExamplesView, JobsView, QueryView, NewQueryView, NewJobsView, NewExamplesView
from .viewsets import (DropdownViewSet, ExampleViewSet, FormViewSet,
PhaseViewSet, QueryJobViewSet, QueryLanguageViewSet,
PhaseViewSet, QueryDownloadFormatViewSet, QueryJobViewSet, QueryLanguageViewSet,
QueueViewSet, StatusViewSet, DownloadViewSet)

app_name = 'query'
Expand All @@ -13,6 +13,7 @@
router.register(r'forms', FormViewSet, basename='form')
router.register(r'dropdowns', DropdownViewSet, basename='dropdown')
router.register(r'downloads', DownloadViewSet, basename='download')
router.register(r'downloadformats', QueryDownloadFormatViewSet, basename='downloadformat')
router.register(r'jobs', QueryJobViewSet, basename='job')
router.register(r'examples', ExampleViewSet, basename='example')
router.register(r'queues', QueueViewSet, basename='queue')
Expand Down
15 changes: 14 additions & 1 deletion daiquiri/query/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from daiquiri.core.paginations import ListPagination
from daiquiri.core.utils import (
get_client_ip,
get_file_size,
fix_for_json,
filter_by_access_level,
handle_file_upload,
Expand All @@ -37,6 +38,7 @@
FormListSerializer,
DropdownSerializer,
DownloadSerializer,
QueryDownloadFormatSerializer,
QueryJobSerializer,
QueryJobListSerializer,
QueryJobRetrieveSerializer,
Expand Down Expand Up @@ -301,7 +303,10 @@ def download(self, request, pk=None, download_key=None, download_job_id=None):
)
return sendfile(request, download_job.file_path, attachment=True)
else:
return Response(download_job.phase)
return Response({
'phase': download_job.phase,
'size': get_file_size(download_job.file_path),
})

@action(detail=True, methods=['post'], url_name='create-download',
url_path=r'download/(?P<download_key>[a-z\-]+)')
Expand Down Expand Up @@ -410,6 +415,14 @@ def get_queryset(self):
return filter_by_access_level(self.request.user, settings.QUERY_LANGUAGES)


class QueryDownloadFormatViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
permission_classes = (HasPermission, )
serializer_class = QueryDownloadFormatSerializer

def get_queryset(self):
return settings.QUERY_DOWNLOAD_FORMATS


class PhaseViewSet(ChoicesViewSet):
permission_classes = (HasPermission, )
authentication_classes = (SessionAuthentication, TokenAuthentication)
Expand Down

0 comments on commit 28caa59

Please sign in to comment.