Skip to content

Commit

Permalink
Merge pull request #1237 Handle connection error on async exports
Browse files Browse the repository at this point in the history
  • Loading branch information
ukanga authored Feb 8, 2018
2 parents 878aaba + d15dfc4 commit 04d5f74
Show file tree
Hide file tree
Showing 5 changed files with 274 additions and 83 deletions.
5 changes: 3 additions & 2 deletions onadata/apps/api/tests/viewsets/test_xform_viewset.py
Original file line number Diff line number Diff line change
Expand Up @@ -2826,8 +2826,9 @@ def test_export_async_connection_error(self, async_result):
self.assertEqual(response.status_code, 503)
self.assertEqual(response.status_text.upper(),
u'SERVICE UNAVAILABLE')
self.assertEqual(response.data['detail'],
u'Error opening socket: a socket error occurred')
self.assertEqual(
response.data['detail'],
u'Service temporarily unavailable, try again later.')
export = Export.objects.get(task_id=task_id)
self.assertTrue(export.is_successful)

Expand Down
80 changes: 64 additions & 16 deletions onadata/apps/viewer/models/export.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# -*- coding=utf-8 -*-
"""
Export model.
"""
import os
import hashlib
from tempfile import NamedTemporaryFile

from django.core.files.storage import get_storage_class
Expand All @@ -15,17 +18,17 @@
EXPORT_QUERY_KEY = 'query'


# pylint: disable=unused-argument
def export_delete_callback(sender, **kwargs):
"""
Delete export file when an export object is deleted.
"""
export = kwargs['instance']
storage = get_storage_class()()
if export.filepath and storage.exists(export.filepath):
storage.delete(export.filepath)


def md5hash(string):
return hashlib.md5(string).hexdigest()


def get_export_options_query_kwargs(options):
"""
Get dict with options JSONField lookups for export options field
Expand All @@ -47,12 +50,25 @@ class Export(models.Model):
"""

class ExportTypeError(Exception):
"""
ExportTypeError exception class.
"""
def __unicode__(self):
return _(u"Invalid export type specified")

def __str__(self):
return unicode(self).encode('utf-8')

class ExportConnectionError(Exception):
"""
ExportConnectionError exception class.
"""
def __unicode__(self):
return _(u"Export server is down.")

def __str__(self):
return unicode(self).encode('utf-8')

XLS_EXPORT = 'xls'
CSV_EXPORT = 'csv'
KML_EXPORT = 'kml'
Expand Down Expand Up @@ -141,11 +157,13 @@ class Meta:
app_label = "viewer"
unique_together = (("xform", "filename"),)

def save(self, *args, **kwargs):
def __unicode__(self):
return u'%s - %s (%s)' % (self.export_type, self.xform, self.filename)

def save(self, *args, **kwargs): # pylint: disable=arguments-differ
if not self.pk and self.xform:
# if new, check if we've hit our limit for exports for this form,
# if so, delete oldest
# TODO: let user know that last export will be deleted
num_existing_exports = Export.objects.filter(
xform=self.xform, export_type=self.export_type).count()

Expand All @@ -168,49 +186,70 @@ def _delete_oldest_export(cls, xform, export_type):

@property
def is_pending(self):
"""
Return True if an export status is pending.
"""
return self.status == Export.PENDING

@property
def is_successful(self):
"""
Return True if an export status successful.
"""
return self.status == Export.SUCCESSFUL

@property
def status(self):
"""
Return the status [FAILED|PENDING|SUCCESSFUL] of an export.
"""
if self.filename:
# need to have this since existing models will have their
# internal_status set to PENDING - the default
return Export.SUCCESSFUL
elif self.internal_status == Export.FAILED:
return Export.FAILED
else:
return Export.PENDING

return Export.PENDING

def set_filename(self, filename):
"""
Set the filename of an export and mark internal_status as
Export.SUCCESSFUL.
"""
self.filename = filename
self.internal_status = Export.SUCCESSFUL
self._update_filedir()

def _update_filedir(self):
assert(self.filename)
assert self.filename
self.filedir = os.path.join(self.xform.user.username,
'exports', self.xform.id_string,
self.export_type)

@property
def filepath(self):
"""
Return the file path of an export file, None if the file does not
exist.
"""
if self.filedir and self.filename:
return os.path.join(self.filedir, self.filename)
return None

@property
def full_filepath(self):
"""
Return the full filepath of an export file, None if the file does not
exist.
"""
if self.filepath:
default_storage = get_storage_class()()
try:
return default_storage.path(self.filepath)
except NotImplementedError:
# read file from s3
name, ext = os.path.splitext(self.filepath)
_name, ext = os.path.splitext(self.filepath)
tmp = NamedTemporaryFile(suffix=ext, delete=False)
f = default_storage.open(self.filepath)
tmp.write(f.read())
Expand All @@ -219,7 +258,13 @@ def full_filepath(self):
return None

@classmethod
def exports_outdated(cls, xform, export_type, options={}):
def exports_outdated(cls, xform, export_type, options=None):
"""
Return True if export is outdated or there is no export matching the
export_type with the specified options.
"""
if options is None:
options = {}
# get newest export for xform
try:
export_options = get_export_options_query_kwargs(options)
Expand All @@ -234,13 +279,16 @@ def exports_outdated(cls, xform, export_type, options={}):
and xform.time_of_last_submission_update() is not None:
return latest_export.time_of_last_submission <\
xform.time_of_last_submission_update()
else:
# return true if we can't determine the status, to force
# auto-generation
return True

# return true if we can't determine the status, to force
# auto-generation
return True

@classmethod
def is_filename_unique(cls, xform, filename):
"""
Return True if the filename is unique.
"""
return Export.objects.filter(
xform=xform, filename=filename).count() == 0

Expand Down
Loading

0 comments on commit 04d5f74

Please sign in to comment.