Permalink
Browse files

Bug 1020348 - Added a tab to signature report that lists all user com…

…ments.
  • Loading branch information...
1 parent 27a25b5 commit 85990f765b7a64e5597754db15bf5deca62cd8ce @adngdb adngdb committed Jul 8, 2014
View
20 webapp-django/crashstats/base/templates/macros/pagination.html
@@ -5,7 +5,10 @@
{% if report.total_pages > 1 %}
…
{% if current_page > 1 %}
- <a href="{{ current_url }}{{ separator }}page={{ current_page - 1 }}{{ tab }}">&larr; Prev</a>
+ <a
+ href="{{ current_url }}{{ separator }}page={{ current_page - 1 }}{{ tab }}"
+ data-page="{{ current_page - 1 }}"
+ >&larr; Prev</a>
{% endif %}
{% for page in range(1, report.total_pages + 1) %}
{% if page == current_page %}
@@ -16,16 +19,25 @@
{% elif current_page > 6 and page == 3 %}
&hellip;
{% elif page > (current_page - 3) and page < (current_page + 3) %}
- <a href="{{ current_url }}{{ separator }}page={{ page }}{{ tab }}">{{ page }}</a>
+ <a
+ href="{{ current_url }}{{ separator }}page={{ page }}{{ tab }}"
+ data-page="{{ page }}"
+ >{{ page }}</a>
{% elif page == (report.total_pages - 3) %}
&hellip;
{% endif %}
{% else %}
- <a href="{{ current_url }}{{ separator }}page={{ page }}{{ tab }}">{{ page }}</a>
+ <a
+ href="{{ current_url }}{{ separator }}page={{ page }}{{ tab }}"
+ data-page="{{ page }}"
+ >{{ page }}</a>
{% endif %}
{% endfor %}
{% if report.total_pages > current_page %}
- <a href="{{ current_url }}{{ separator }}page={{ current_page + 1 }}{{ tab }}">Next &rarr;</a>
+ <a
+ href="{{ current_url }}{{ separator }}page={{ current_page + 1 }}{{ tab }}"
+ data-page="{{ current_page + 1 }}"
+ >Next &rarr;</a>
{% endif %}
{% endif %}
</div>
View
54 webapp-django/crashstats/signature/static/signature/js/signature_report.js
@@ -80,6 +80,15 @@ $(function () {
elt.append($('<div>', {class: 'loader'}));
}
+ function bindPaginationLinks(panel, callback) {
+ $('.pagination a', panel).click(function (e) {
+ e.preventDefault();
+
+ var page = $(this).data('page');
+ callback(page);
+ });
+ }
+
function handleError(contentElt, jqXHR, textStatus, errorThrown) {
var errorContent = $('<div>', {class: 'error'});
@@ -151,7 +160,7 @@ $(function () {
var dataUrl = reportsPanel.data('source-url');
- function prepareResultsQueryString(params) {
+ function prepareResultsQueryString(params, page) {
var i;
var len;
@@ -164,25 +173,26 @@ $(function () {
}
// Add the page number.
- params.page = pageNum;
+ params.page = page || pageNum;
var queryString = $.param(params, true);
return '?' + queryString;
}
- function showReports() {
+ function showReports(page) {
// Remove previous results and show loader.
contentElt.empty();
addLoaderToElt(contentElt);
var params = getParamsWithSignature();
- var url = dataUrl + prepareResultsQueryString(params);
+ var url = dataUrl + prepareResultsQueryString(params, page);
$.ajax({
url: url,
success: function(data) {
contentElt.empty().append($(data));
$('.tablesorter').tablesorter();
+ bindPaginationLinks(reportsPanel, showReports);
},
error: function(jqXHR, textStatus, errorThrown) {
handleError(contentElt, jqXHR, textStatus, errorThrown);
@@ -264,6 +274,42 @@ $(function () {
showAggregation('build_id');
};
+ tabsLoadFunctions.comments = function () {
+ // Initialize the comments tab, bind all events and start loading
+ // default data.
+ var commentsPanel = $('#comments-panel');
+ var contentElt = $('.content', commentsPanel);
+
+ var dataUrl = commentsPanel.data('source-url');
+
+ function showComments(page) {
+ // Remove previous results and show loader.
+ contentElt.empty();
+ addLoaderToElt(contentElt);
+
+ var params = getParamsWithSignature();
+ params.page = page || pageNum;
+
+ var queryString = $.param(params, true);
+ var url = dataUrl + '?' + queryString;
+
+ $.ajax({
+ url: url,
+ success: function(data) {
+ contentElt.empty().append($(data));
+ $('.tablesorter').tablesorter();
+ bindPaginationLinks(commentsPanel, showComments);
+ },
+ error: function(jqXHR, textStatus, errorThrown) {
+ handleError(contentElt, jqXHR, textStatus, errorThrown);
+ },
+ dataType: 'HTML'
+ });
+ }
+
+ showComments();
+ };
+
// Finally start the damn thing.
bindEvents();
startSearchForm(loadInitialTab);
View
32 webapp-django/crashstats/signature/templates/signature/signature_comments.html
@@ -0,0 +1,32 @@
+{% from "macros/pagination.html" import pagination %}
+
+{% if query.total > 0 %}
+
+<div>
+ {{ pagination(query, current_url, current_page) }}
+ <table class="tablesorter data-table">
+ <thead>
+ <tr>
+ <th scope="col">Crash ID</th>
+ <th scope="col">Comment</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for crash in query.hits %}
+ <tr>
+ <td><a href="{{ url('crashstats:report_index', crash.uuid) }}" class="external-link">{{ crash.uuid }}</a></td>
+ <td>
+ {{ crash.user_comments }}
+ </td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ {{ pagination(query, current_url, current_page) }}
+</div>
+
+{% else %}
+
+<p>No comments were found.</p>
+
+{% endif %}
View
19 webapp-django/crashstats/signature/templates/signature/signature_report.html
@@ -27,6 +27,7 @@
<ul class="tabs">
<li><a href="#reports" class="reports" data-tab-name="reports">Reports</a></li>
<li><a href="#aggregations" class="aggregations" data-tab-name="aggregations">Aggregations</a></li>
+ <li><a href="#comments" class="comments" data-tab-name="comments">Comments</a></li>
</ul>
</nav>
@@ -91,6 +92,24 @@
</div>
</section>
+ <!-- Comments panel.
+ Displays a list of user comments from crash reports that have the
+ current signature.
+ -->
+ <section class="panel" id="comments-panel"
+ data-source-url="{{ url('signature:signature_comments') }}"
+ >
+ <header class="title">
+ <h2>User Comments</h2>
+ </header>
+
+ <div class="body">
+ <div class="content">
+ <div class="loader"></div>
+ </div>
+ </div>
+ </section>
+
</div>
{% endblock %}
View
138 webapp-django/crashstats/signature/tests/test_views.py
@@ -334,3 +334,141 @@ def mocked_get(url, params, **options):
ok_(str(1337 / 1382 * 100) in response.content)
ok_('windows' in response.content)
ok_('mac' in response.content)
+
+ @mock.patch('requests.get')
+ def test_signature_comments(self, rget):
+ def mocked_get(url, params, **options):
+ assert 'supersearch' in url
+
+ if 'supersearch/fields' in url:
+ return Response(SUPERSEARCH_FIELDS_MOCKED_RESULTS)
+
+ ok_('signature' in params)
+ eq_(params['signature'], '=' + DUMB_SIGNATURE)
+
+ ok_('user_comments' in params)
+ eq_(params['user_comments'], '!__null__')
+
+ if 'product' in params:
+ return Response({
+ "hits": [
+ {
+ "date": "2017-01-31T23:12:57",
+ "uuid": "aaaaaaaaaaaaa1",
+ "product": "WaterWolf",
+ "version": "1.0",
+ "platform": "Linux",
+ "user_comments": "hello there people!"
+ },
+ {
+ "date": "2017-01-31T23:12:57",
+ "uuid": "aaaaaaaaaaaaa2",
+ "product": "WaterWolf",
+ "version": "1.0",
+ "platform": "Linux",
+ "user_comments": "I love Mozilla"
+ },
+ {
+ "date": "2017-01-31T23:12:57",
+ "uuid": "aaaaaaaaaaaaa3",
+ "product": "WaterWolf",
+ "version": "1.0",
+ "platform": "Linux",
+ "user_comments": "this product is awesome"
+ },
+ {
+ "date": "2017-01-31T23:12:57",
+ "uuid": "aaaaaaaaaaaaa4",
+ "product": "WaterWolf",
+ "version": "1.0",
+ "platform": "Linux",
+ "user_comments": "WaterWolf Y U SO GOOD?"
+ }
+ ],
+ "total": 4
+ })
+
+ return Response({"hits": [], "total": 0})
+
+ rget.side_effect = mocked_get
+
+ url = reverse('signature:signature_comments')
+
+ # Test with no results.
+ response = self.client.get(url, {
+ 'signature': DUMB_SIGNATURE,
+ })
+ eq_(response.status_code, 200)
+ ok_('Crash ID' not in response.content)
+ ok_('No comments were found' in response.content)
+
+ # Test with results.
+ response = self.client.get(url, {
+ 'signature': DUMB_SIGNATURE,
+ 'product': 'WaterWolf'
+ })
+ eq_(response.status_code, 200)
+ ok_('aaaaaaaaaaaaa1' in response.content)
+ ok_('Crash ID' in response.content)
+ ok_('hello there' in response.content)
+ ok_('WaterWolf Y U SO GOOD' in response.content)
+
+ @mock.patch('requests.get')
+ def test_signature_comments_pagination(self, rget):
+ """Test that the pagination of comments works as expected. """
+
+ def mocked_get(url, params, **options):
+ assert 'supersearch' in url
+
+ if 'supersearch/fields' in url:
+ return Response(SUPERSEARCH_FIELDS_MOCKED_RESULTS)
+
+ if '_results_offset' in params:
+ hits_range = range(100, 140)
+ else:
+ hits_range = range(100)
+
+ hits = []
+ for i in hits_range:
+ hits.append({
+ "date": "2017-01-31T23:12:57",
+ "uuid": i,
+ "user_comments": "hi",
+ })
+
+ return Response({
+ "hits": hits,
+ "total": 140
+ })
+
+ rget.side_effect = mocked_get
+
+ url = reverse('signature:signature_comments')
+
+ response = self.client.get(
+ url,
+ {
+ 'signature': DUMB_SIGNATURE,
+ 'product': ['WaterWolf'],
+ }
+ )
+
+ eq_(response.status_code, 200)
+ ok_('140' in response.content)
+ ok_('99' in response.content)
+ ok_('139' not in response.content)
+
+ # Check that the pagination URL contains all expected parameters.
+ doc = pyquery.PyQuery(response.content)
+ next_page_url = str(doc('.pagination a').eq(0))
+ ok_('product=WaterWolf' in next_page_url)
+ ok_('page=2' in next_page_url)
+
+ response = self.client.get(url, {
+ 'signature': DUMB_SIGNATURE,
+ 'page': '2',
+ })
+ eq_(response.status_code, 200)
+ ok_('140' in response.content)
+ ok_('99' not in response.content)
+ ok_('139' in response.content)
View
5 webapp-django/crashstats/signature/urls.py
@@ -10,6 +10,11 @@
name='signature_reports',
),
url(
+ r'^comments/$',
+ views.signature_comments,
+ name='signature_comments',
+ ),
+ url(
r'^aggregation/(?P<aggregation>\w+)/$',
views.signature_aggregation,
name='signature_aggregation',
View
110 webapp-django/crashstats/signature/views.py
@@ -30,6 +30,26 @@
)
+def get_validated_params(request):
+ params = get_params(request)
+ if isinstance(params, http.HttpResponseBadRequest):
+ # There was an error in the form, let's return it.
+ return params
+
+ if len(params['signature']) > 1:
+ return http.HttpResponseBadRequest(
+ 'Invalid value for "signature" parameter, '
+ 'only one value is accepted'
+ )
+
+ if not params['signature'][0]:
+ return http.HttpResponseBadRequest(
+ '"signature" parameter is mandatory'
+ )
+
+ return params
+
+
@waffle_switch('signature-report')
@pass_default_context
def signature_report(request, default_context=None):
@@ -61,23 +81,13 @@ def signature_report(request, default_context=None):
@waffle_switch('signature-report')
def signature_reports(request):
'''Return the results of a search. '''
- params = get_params(request)
+ params = get_validated_params(request)
if isinstance(params, http.HttpResponseBadRequest):
# There was an error in the form, let's return it.
return params
- if len(params['signature']) > 1:
- return http.HttpResponseBadRequest(
- 'Invalid value for "signature" parameter, '
- 'only one value is accepted'
- )
signature = params['signature'][0]
- if not signature:
- return http.HttpResponseBadRequest(
- '"signature" parameter is mandatory'
- )
-
data = {}
data['query'] = {
'total': 0,
@@ -148,23 +158,13 @@ def signature_reports(request):
@waffle_switch('signature-report')
def signature_aggregation(request, aggregation):
'''Return the aggregation of a field. '''
- params = get_params(request)
+ params = get_validated_params(request)
if isinstance(params, http.HttpResponseBadRequest):
# There was an error in the form, let's return it.
return params
- if len(params['signature']) > 1:
- return http.HttpResponseBadRequest(
- 'Invalid value for "signature" parameter, '
- 'only one value is accepted'
- )
signature = params['signature'][0]
- if not signature:
- return http.HttpResponseBadRequest(
- '"signature" parameter is mandatory'
- )
-
data = {}
data['aggregation'] = aggregation
@@ -206,3 +206,69 @@ def signature_aggregation(request, aggregation):
data['total_count'] = search_results['total']
return render(request, 'signature/signature_aggregation.html', data)
+
+
+@waffle_switch('signature-report')
+def signature_comments(request):
+ '''Return a list of non-empty comments. '''
+ params = get_validated_params(request)
+ if isinstance(params, http.HttpResponseBadRequest):
+ # There was an error in the form, let's return it.
+ return params
+
+ signature = params['signature'][0]
+
+ data = {}
+ data['query'] = {
+ 'total': 0,
+ 'total_count': 0,
+ 'total_pages': 0
+ }
+
+ current_query = request.GET.copy()
+ if 'page' in current_query:
+ del current_query['page']
+
+ data['params'] = current_query.copy()
+
+ try:
+ current_page = int(request.GET.get('page', 1))
+ except ValueError:
+ return http.HttpResponseBadRequest('Invalid page')
+
+ if current_page <= 0:
+ current_page = 1
+
+ results_per_page = 50
+ data['current_page'] = current_page
+ data['results_offset'] = results_per_page * (current_page - 1)
+
+ params['signature'] = '=' + signature
+ params['user_comments'] = '!__null__'
+ params['_results_number'] = results_per_page
+ params['_results_offset'] = data['results_offset']
+ params['_facets'] = [] # We don't need no facets.
+
+ data['current_url'] = '%s?%s' % (
+ reverse('signature:signature_report'),
+ current_query.urlencode()
+ )
+
+ api = SuperSearchUnredacted()
+ try:
+ search_results = api.get(**params)
+ except models.BadStatusCodeError, e:
+ # We need to return the error message in some HTML form for jQuery to
+ # pick it up.
+ return http.HttpResponseBadRequest('<ul><li>%s</li></ul>' % e)
+
+ search_results['total_pages'] = int(
+ math.ceil(
+ search_results['total'] / float(results_per_page)
+ )
+ )
+ search_results['total_count'] = search_results['total']
+
+ data['query'] = search_results
+
+ return render(request, 'signature/signature_comments.html', data)
View
12 webapp-django/crashstats/supersearch/tests/test_views.py
@@ -184,6 +184,18 @@
'is_returned': True,
'is_mandatory': False,
},
+ 'user_comments': {
+ 'name': 'user_comments',
+ 'query_type': 'str',
+ 'namespace': 'processed_crash',
+ 'form_field_type': 'MultipleValueField',
+ 'form_field_choices': [],
+ 'permissions_needed': [],
+ 'default_value': None,
+ 'is_exposed': True,
+ 'is_returned': True,
+ 'is_mandatory': False,
+ },
}

0 comments on commit 85990f7

Please sign in to comment.