Skip to content
This repository has been archived by the owner on Feb 1, 2018. It is now read-only.

Commit

Permalink
fixes bug 858244 - documentation for public api, r=rhelmer
Browse files Browse the repository at this point in the history
  • Loading branch information
peterbe committed May 8, 2013
1 parent d9011c7 commit b0c7aab
Show file tree
Hide file tree
Showing 7 changed files with 460 additions and 7 deletions.
52 changes: 52 additions & 0 deletions crashstats/api/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import urllib
import warnings
import datetime
from jingo import register
import jinja2


@register.function
def describe_friendly_type(type_):
if type_ is basestring:
return "String"
if type_ is int:
return "Integer"
if type_ is list:
return "List of strings"
if type_ is datetime.date:
return "Date"
if type_ is datetime.datetime:
return "Date and time"
warnings.warn("Don't know how to describe type %r" % type_)
return type_


@register.function
def make_test_input(parameter, defaults):
template = u'<input type="%(type)s" name="%(name)s"'
data = {
'name': parameter['name'],
}
classes = []
if parameter['required']:
classes.append('required')

if parameter['type'] is datetime.date:
data['type'] = 'date'
else:
data['type'] = 'text'
if parameter['type'] is not basestring:
classes.append('validate-%s' % parameter['type'].__name__)
if defaults.get(parameter['name']):
data['value'] = urllib.quote(unicode(defaults.get(parameter['name'])))
else:
data['value'] = ''

data['classes'] = ' '.join(classes)
if data['classes']:
template += ' class="%(classes)s"'
if data['value']:
template += ' value="%(value)s"'
template += '>'
html = template % data
return jinja2.Markup(html)
158 changes: 158 additions & 0 deletions crashstats/api/static/api/js/testdrive.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
(function ($, document) {
'use strict';

$.fn.serializeExclusive = function() {
var o = {};
var a = this.serializeArray();
$.each(a, function() {
var value;
if (o[this.name] !== undefined) {
if (!o[this.name].push) {
o[this.name] = [o[this.name]];
}
if (this.value)
o[this.name].push(this.value || '');
} else {
if (this.value)
o[this.name] = this.value || '';
}
});
return o;
};

function one_more(element) {
var container = $(element).parents('td');
var previous = $('input', container).eq(-1);
var clone = previous.clone();
clone.val('');
clone.insertAfter(previous);
if ($('input', container).length > 1) {
$('a.collapse-list', container).css('display', 'inline');
}
}

function one_less(element) {
var container = $(element).parents('td');
var last = $('input', container).eq(-1);
last.remove();
if ($('input', container).length < 2) {
$('a.collapse-list', container).css('display', 'none');
}
}

function validate_form(form) {

function is_int(x) {
var y = parseInt(x, 10);
if (isNaN(y)) return false;
return x==y && x.toString() == y.toString();
}
var all_valid = true;
$('input', form).each(function() {
var valid = true;
var element = $(this);
var value = element.val();
if (element.hasClass('required') && !value) {
valid = false;
} else if (element.hasClass('validate-int') && !is_int(value)) {
valid = false;
} else {
// we can do more validation but let's not go too crazy yet
}
if (!valid) {
all_valid = false;
element.addClass('error');
} else {
element.removeClass('error');
}
});
return all_valid;
}

function submit_form(form) {
var url = $('p.url code', form).text();
// unlike regular, form.serialize() by doing it this way we get a
// query string that only contains actual values
// The second parameter (`true`) is so that things like
// `{products: ["Firefox", "Thunderbird"]}`
// becomes: `products=Firefox&products=Thunderbird`
var qs = $.param(form.serializeExclusive(), true);
if (qs) {
url += '?' + qs;
}
var ajax_url = url;
if (ajax_url.search(/\?/) == -1) ajax_url += '?';
else ajax_url += '&';
ajax_url += 'pretty=print';

$.ajax({
url: ajax_url,
method: 'GET',
dataType: 'text',
success: function(response, textStatus, jqXHR) {
var container = $('.test-drive', form);
$('.used-url code', container).text(url);
$('.used-url a', container).attr('href', url);
$('pre', container).text(response);
$('.status code', container).hide();
$('.status-error', container).hide();
container.show();
$('button.close', form).show();
},
error: function(jqXHR, textStatus, errorThrown) {
var container = $('.test-drive', form);
$('pre', container).text(jqXHR.responseText);
$('.status code', container).text(jqXHR.status).show();
$('.status-error', container).show();

container.show();
$('button.close', form).show();
}
});
}

$(document).ready(function () {
$('input.validate-list').each(function() {
$('<a href="#">-</a>')
.hide()
.addClass('collapse-list')
.attr('title', 'Click to remove the last added input field')
.click(function(e) {
e.preventDefault();
one_less(this);
})
.insertAfter($(this));
$('<a href="#">+</a>')
.addClass('expand-list')
.attr('title', 'Click to create another input field')
.click(function(e) {
e.preventDefault();
one_more(this);
})
.insertAfter($(this));
});

$('form.testdrive').submit(function(event) {
var $form = $(this);
event.preventDefault();
if (validate_form($form)) {
submit_form($form);
} else {
}
});

// Can we PLEASE upgrade to modern jquery soon?
$('input.error').live('change', function() {
$(this).removeClass('error');
});

$('button.close').click(function(event) {
event.preventDefault();
$('.test-drive', $(this).parents('form')).hide();
$(this).hide();
});

});


}($, document));
131 changes: 131 additions & 0 deletions crashstats/api/templates/api/documentation.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
{% extends "crashstats_base.html" %}

{% block page_title %}API Documentation{% endblock %}

{% block product_nav_filter %}&nbsp;{% endblock %}

{% block site_css %}
{{ super() }}
<style type="text/css">
p.url { margin: 15px; font-size: 1.6em; }
p.url code { padding-left: 25px; }
th.fixed, td.fixed { width: 15%; }
div.run-test { text-align: right; margin: 10px; }
a.collapse-list, a.expand-list { text-decoration: none; font-weight: bold; padding: 3px; }
input.error { border: 2px solid red; }
div.test-drive { display: none; }
button.close { display: none; }
.status-error { display: none; color: red; font-weight: bold; }
pre.docstring { background-color: rgb(225,225,225); padding: 6px; }
</style>
{% endblock %}


{% block site_js %}
{{ super() }}
{% compress js %}
<script type="text/javascript" src="{{ static('api/js/testdrive.js') }}"></script>
{% endcompress %}
{% endblock %}


{% block content %}
<div id="mainbody">
<div class="page-heading">
<h2>API Documentation</h2>
</div>

<div class="panel">
<div class="body notitle">
<p>
These API endpoints are publically available. The parameters to some endpoints
are non-trivial as some might require deeper understanding of the Soccoro backend.
</p>
<p>
<b>Note:</b> All Personal Identifiable Information
will be <b>removed or scrubbed</b> from all responses.
</p>
</div>
</div>

{% for endpoint in endpoints %}
<div class="panel">
<div class="title">
<h2>{{ endpoint.name }}</h2>
</div>
<div class="body">
<form class="testdrive">
<p class="url">
<strong>{{ ' | '.join(endpoint.methods) }}</strong>
<code>{{ base_url }}{{ endpoint.url }}</code>
</p>

{% if endpoint.docstring %}
<p class="docstring">Documentation notes</p>
<pre class="docstring">{{ endpoint.docstring }}</pre>
{% endif %}

{% if endpoint.parameters %}
<table class="data-table vertical">
<thead>
<tr>
<th class="fixed">Parameter key</th>
<th class="fixed">Required?</th>
<th class="fixed">Type</th>
<th class="fixed">Default</th>
<th>Test drive</th>
</tr>
</thead>
<tbody>
{% for parameter in endpoint.parameters %}
<tr>
<td class="fixed"><strong>{{ parameter.name }}</strong></td>
<td class="fixed">
{% if parameter.required %}Required
{% else %}Optional
{% endif %}
</td>
<td class="fixed">
{{ describe_friendly_type(parameter['type']) }}
</td>
<td class="fixed">
<code>{{ endpoint['defaults'].get(parameter['name'], '') }}</code>
</td>
<td>
{{ make_test_input(parameter, endpoint['defaults']) }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p><em>Takes no parameters</em></p>
{% endif %}

<!-- for starting a test drive -->
<div class="run-test">
<button type="submit">Run Test Drive!</button>
<button type="button" class="close">&times; Close</button>
</div>

<div class="test-drive">
<p class="status-error">
An error happened on the server when trying this URL.
</p>
<p class="used-url">
<strong>Using</strong><br>
<a href=""><code></code></a>
</p>
<p class="status">
Status <code></code>
</p>
<p><strong>Output:</strong></p>
<pre></pre>

</div>
</form>
</div>
</div>
{% endfor %}
</div>
{% endblock %}
29 changes: 29 additions & 0 deletions crashstats/api/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import re
import json
import unittest

from django.core.urlresolvers import reverse

Expand All @@ -12,6 +13,34 @@
)


class TestDedentLeft(unittest.TestCase):

def test_dedent_left(self):
from crashstats.api.views import dedent_left
eq_(dedent_left('Hello', 2), 'Hello')
eq_(dedent_left(' Hello', 2), ' Hello')
eq_(dedent_left(' Hello ', 2), ' Hello ')

text = """Line 1
Line 2
Line 3
""".rstrip()
# because this code right above is indented with 2 * 4 spaces
eq_(dedent_left(text, 8), 'Line 1\nLine 2\nLine 3')


class TestDocumentationViews(BaseTestViews):

def test_documentation_home_page(self):
url = reverse('api:documentation')
response = self.client.get(url)
eq_(response.status_code, 200)

from crashstats.api import views
for each in views.BLACKLIST:
ok_(each not in response.content)


class TestViews(BaseTestViews):

def test_invalid_url(self):
Expand Down
1 change: 1 addition & 0 deletions crashstats/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

urlpatterns = patterns(
'', # prefix
url('^$', views.documentation, name='documentation'),
url('^(?P<model_name>\w+)/$',
views.model_wrapper,
name='model_wrapper'),
Expand Down
Loading

0 comments on commit b0c7aab

Please sign in to comment.