Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

[bug 972414] Add csv export to analyzer search

* add csv export to analyzer search
* clean up some things by moving some functionality to the Response
  model
  • Loading branch information...
commit 4ffe4f0d94ec2dcac3fa5abb40ddd87b8a1d76ba 1 parent d7bffd8
@willkg willkg authored
View
55 fjord/analytics/analyzer_views.py
@@ -16,10 +16,13 @@
from collections import defaultdict
from datetime import datetime, timedelta
+import csv
from elasticutils.contrib.django import F, es_required_or_50x
+from django.http import HttpResponse
from django.shortcuts import render
+from django.utils.encoding import force_bytes
from fjord.analytics.forms import OccurrencesComparisonForm
from fjord.analytics.tools import (
@@ -261,12 +264,59 @@ def analytics_duplicates(request):
})
+def _analytics_search_export(request, opinions_s):
+ """Handles CSV export for analytics search
+
+ This only exports MAX_OPINIONS amount. It adds a note to the top
+ about that if the results are truncated.
+
+ """
+ MAX_OPINIONS = 1000
+
+ # Create the HttpResponse object with the appropriate CSV header.
+ response = HttpResponse(content_type='text/csv')
+ response['Content-Disposition'] = 'attachment; filename="{0}"'.format(
+ datetime.now().strftime('%Y%m%d_%H%M_search_export.csv'))
+
+ keys = Response.get_export_keys(confidential=True)
+ total_opinions = opinions_s.count()
+
+ opinions_s = opinions_s.values_list('id')[:MAX_OPINIONS]
+
+ # We convert what we get back from ES to what's in the db so we
+ # can get all the information.
+ opinions = Response.objects.filter(id__in=[mem[0] for mem in opinions_s])
+
+ writer = csv.writer(response)
+
+ # Specify what this search is
+ writer.writerow(['URL: {0}'.format(request.get_full_path())])
+ writer.writerow(['Params: ' +
+ ' '.join(['{0}: {1}'.format(key, val)
+ for key, val in request.GET.items()])])
+
+ # Add note if we truncated.
+ if total_opinions > MAX_OPINIONS:
+ writer.writerow(['Truncated {0} rows.'.format(
+ total_opinions - MAX_OPINIONS)])
+
+ # Write headers row.
+ writer.writerow(keys)
+
+ # Write opinion rows.
+ for op in opinions:
+ writer.writerow([force_bytes(getattr(op, key)) for key in keys])
+
+ return response
+
+
@check_new_user
@analyzer_required
@es_required_or_50x(error_template='analytics/es_down.html')
def analytics_search(request):
template = 'analytics/analyzer/search.html'
+ output_format = request.GET.get('format', None)
page = smart_int(request.GET.get('page', 1), 1)
# Note: If we add additional querystring fields, we need to add
@@ -369,7 +419,10 @@ def unknown_to_empty(text):
search = search.filter(f).order_by('-created')
- # FIXME - Links to output formats here
+ # If they're asking for a CSV export, then send them to the export
+ # screen.
+ if output_format == 'csv':
+ return _analytics_search_export(request, search)
# Search results and pagination
if page < 1:
View
12 fjord/analytics/templates/analytics/analyzer/search.html
@@ -12,8 +12,8 @@
<dl>
<dt>Email:</dt>
<dd>
- {% if feedback.responseemail_set.count() > 0 %}
- {{ feedback.responseemail_set.all()[0].email }}
+ {% if feedback.user_email %}
+ {{ feedback.user_email }}
{% else %}
&mdash;
{% endif %}
@@ -64,10 +64,10 @@
<a href="{{ request.get_full_path()|urlparams(platform=feedback.platform) }}">{{ feedback.platform|unknown }}</a>
</li>
<li>
- <a href="{{ request.get_full_path()|urlparams(locale=feedback.locale) }}">{{ feedback.locale|locale_name }}</a>
+ <a href="{{ request.get_full_path()|urlparams(locale=feedback.locale) }}">{{ feedback.locale_name }}</a>
</li>
<li>
- {{ feedback.country|country_name }}
+ {{ feedback.country_name }}
</li>
<li>
{{ feedback.manufacturer|unknown }}
@@ -169,6 +169,10 @@
information. Please act responsibly with it.
</p>
+ <p>
+ <a href="{{ request.get_full_path()|urlparams(format='csv') }}">CSV export</a>
+ </p>
+
<ul class="search_results">
{% for opinion in opinions %}
{{ feedback_block(opinion) }}
View
10 fjord/analytics/tests/test_analyzer_views.py
@@ -362,3 +362,13 @@ def test_invalid_search(self):
eq_(r.status_code, 200)
pq = PyQuery(r.content)
eq_(len(pq('li.opinion')), 7)
+
+ def test_search_export_csv(self):
+ r = self.client.get(self.url, {'format': 'csv'})
+ eq_(r.status_code, 200)
+
+ # Check that it parses in csv with n rows.
+ lines = r.content.splitlines()
+
+ # URL row, params row, header row and one row for every opinion
+ eq_(len(lines), 10)
View
67 fjord/feedback/models.py
@@ -7,9 +7,11 @@
from elasticutils.contrib.django import Indexable
from rest_framework import serializers
from tower import ugettext_lazy as _
+from product_details import product_details
from fjord.base.models import ModelBase
from fjord.base.util import smart_truncate
+from fjord.feedback.config import CODE_TO_COUNTRY
from fjord.feedback.utils import compute_grams
from fjord.search.index import (
register_mapping_type, FjordMappingType,
@@ -113,11 +115,54 @@ def __unicode__(self):
def __repr__(self):
return self.__unicode__().encode('ascii', 'ignore')
+ @classmethod
+ def get_export_keys(cls, confidential=False):
+ """Returns set of keys that are interesting for export
+
+ Some parts of the Response aren't very interesting. This lets
+ us explicitly state what is available for export.
+
+ Note: This returns the name of *properties* of Response which
+ aren't all database fields. Some of them are finessed.
+
+ :arg confidential: Whether or not to include confidential data
+
+ """
+ keys = [
+ 'id',
+ 'created',
+ 'sentiment',
+ 'description',
+ 'translated_description',
+ 'product',
+ 'channel',
+ 'version',
+ 'locale_name',
+ 'manufacturer',
+ 'device',
+ 'platform',
+ ]
+
+ if confidential:
+ keys.extend([
+ 'url',
+ 'country_name',
+ 'user_email',
+ ])
+ return keys
+
def save(self, *args, **kwargs):
self.description = self.description.strip()[:TRUNCATE_LENGTH]
super(Response, self).save(*args, **kwargs)
@property
+ def user_email(self):
+ """Associated email address or u''"""
+ if self.responseemail_set.count() > 0:
+ return self.responseemail_set.all()[0].email
+ return u''
+
+ @property
def sentiment(self):
if self.happy:
return _(u'Happy')
@@ -128,6 +173,26 @@ def truncated_description(self):
"""Shorten feedback for list display etc."""
return smart_truncate(self.description, length=70)
+ @property
+ def locale_name(self, native=False):
+ """Convert a locale code into a human readable locale name"""
+ locale = self.locale
+ if locale in product_details.languages:
+ display_locale = 'native' if native else 'English'
+ return product_details.languages[locale][display_locale]
+
+ return locale
+
+ @property
+ def country_name(self, native=False):
+ """Convert a country code into a human readable country name"""
+ country = self.country
+ if country in CODE_TO_COUNTRY:
+ display_locale = 'native' if native else 'English'
+ return CODE_TO_COUNTRY[country][display_locale]
+
+ return country
+
@classmethod
def get_mapping_type(self):
return ResponseMappingType
@@ -189,7 +254,7 @@ def empty_to_unknown(text):
'prodchan': obj.prodchan,
'happy': obj.happy,
'url': obj.url,
- 'has_email': obj.responseemail_set.count() > 0,
+ 'has_email': bool(obj.user_email),
'description': obj.description,
'user_agent': obj.user_agent,
'product': obj.product,
Please sign in to comment.
Something went wrong with that request. Please try again.