Skip to content

Commit bcdc1eb

Browse files
Merge pull request #38 from ekonstantinidis/browsable-api
Live Endpoints - Browsable API
2 parents 5da2ab4 + 81ff7b1 commit bcdc1eb

File tree

7 files changed

+408
-2
lines changed

7 files changed

+408
-2
lines changed

rest_framework_docs/api_endpoint.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import inspect
23
from django.contrib.admindocs.views import simplify_regex
34

@@ -15,6 +16,8 @@ def __init__(self, pattern, parent_pattern=None):
1516
# self.view_name = pattern.callback.__name__
1617
self.errors = None
1718
self.fields = self.__get_serializer_fields__()
19+
self.fields_json = self.__get_serializer_fields_json__()
20+
self.permissions = self.__get_permissions_class__()
1821

1922
def __get_path__(self, parent_pattern):
2023
if parent_pattern:
@@ -27,6 +30,10 @@ def __get_allowed_methods__(self):
2730
def __get_docstring__(self):
2831
return inspect.getdoc(self.callback)
2932

33+
def __get_permissions_class__(self):
34+
for perm_class in self.pattern.callback.cls.permission_classes:
35+
return perm_class.__name__
36+
3037
def __get_serializer_fields__(self):
3138
fields = []
3239

@@ -47,3 +54,8 @@ def __get_serializer_fields__(self):
4754
# Show more attibutes of `field`?
4855

4956
return fields
57+
58+
def __get_serializer_fields_json__(self):
59+
# FIXME:
60+
# Return JSON or not?
61+
return json.dumps(self.fields)

rest_framework_docs/static/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"build-bootstrap-fonts": "cp -r node_modules/bootstrap/fonts rest_framework_docs/",
99
"build-less": "lessc --clean-css rest_framework_docs/less/style.less rest_framework_docs/css/style.css",
1010
"watch-less": "watch 'npm run build-less' rest_framework_docs/less/",
11+
"watch": "npm run watch-less",
1112
"build": "npm run build-font-awesome && npm run build-bootstrap-fonts && npm run build-less",
1213
"start": "npm run build && npm run watch-less",
1314
"test": "echo \"Error: no test specified\" && exit 1"

rest_framework_docs/static/rest_framework_docs/css/style.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
var jsonPP = {
2+
// Thanks to http://jsfiddle.net/unlsj/
3+
replacer: function (match, pIndent, pKey, pVal, pEnd) {
4+
var key = '<span class=json-key>';
5+
var val = '<span class=json-value>';
6+
var str = '<span class=json-string>';
7+
var r = pIndent || '';
8+
if (pKey)
9+
r = r + key + pKey.replace(/[": ]/g, '') + '</span>: ';
10+
if (pVal)
11+
r = r + (pVal[0] == '"' ? str : val) + pVal + '</span>';
12+
return r + (pEnd || '');
13+
},
14+
prettyPrint: function (obj) {
15+
var jsonLine = /^( *)("[\w]+": )?("[^"]*"|[\w.+-]*)?([,[{])?$/mg;
16+
return JSON.stringify(obj, null, 3)
17+
.replace(/&/g, '&amp;').replace(/\\"/g, '&quot;')
18+
.replace(/</g, '&lt;').replace(/>/g, '&gt;')
19+
.replace(jsonLine, this.replacer);
20+
}
21+
};
22+
23+
$( document ).ready(function() {
24+
25+
var token = null;
26+
27+
var resetForm = function () {
28+
$('#methods').empty();
29+
$('#fields').empty();
30+
};
31+
32+
var cleanResponse = function () {
33+
$('#responseStatusCode').removeClass(function (index, css) {
34+
return (css.match (/(^|\s)status-code-\S+/g) || []).join(' ');
35+
});
36+
$('#responseStatusText').hide();
37+
$('#responseStatusText').find('span').text('');
38+
$('#responseData').html('');
39+
$('#saveTokenButton').hide();
40+
$('#responseData').parent().hide();
41+
};
42+
43+
var setResponse = function (response) {
44+
var statusCodeFirstChar = String(response.status).charAt(0);
45+
46+
$('#responseStatusCode').text(response.status);
47+
$('#responseStatusCode').addClass('status-code-' + statusCodeFirstChar);
48+
49+
$('#responseStatusText').show();
50+
$('#responseStatusText').find('span').text(response.statusText.toLowerCase());
51+
52+
$('#responseData').parent().show();
53+
$('#responseData').html(jsonPP.prettyPrint(response.responseJSON));
54+
55+
// Setup token store
56+
if (response.responseJSON.hasOwnProperty('token')) {
57+
// If the JSON has a token, show the button to save it
58+
$('#saveTokenButton').show();
59+
60+
$('#saveTokenButton').on('click', function () {
61+
// Save the token to the window
62+
token = response.responseJSON['token'];
63+
});
64+
}
65+
};
66+
67+
var getFormData = function () {
68+
var data = {};
69+
70+
$( '#fields .form-group' ).each(function(){
71+
var input = $(this).find( 'input' );
72+
var name = input.attr( 'id' );
73+
var value = input.val();
74+
data[name] = value;
75+
})
76+
77+
return data;
78+
};
79+
80+
var makeRequest = function () {
81+
// Clean the response
82+
cleanResponse();
83+
84+
var url = $('#requestForm #urlInput').val();
85+
var method = $("#methods").find( ".active" ).text();
86+
var data = getFormData();
87+
88+
var token = $('#headers #authorization').val();
89+
var headers = token ? {
90+
'Authorization' : token
91+
} : null;
92+
93+
$.ajax({
94+
url: url,
95+
method: method,
96+
headers: headers,
97+
context: document.body,
98+
data: data
99+
}).always(function(data, textStatus, jqXHR) {
100+
101+
if (textStatus != 'success') {
102+
jqXHR = data;
103+
}
104+
setResponse(jqXHR);
105+
});
106+
};
107+
108+
var _setupMethods = function (methods) {
109+
// List Methods (Radio Buttons)
110+
// FIXME: Use regex - convert to JSON
111+
var methods = methods.replace("[", "").replace("]", "").replace(/'/g, "").replace(/\s/g, "").split(',');
112+
$.each( methods, function( i, method ) {
113+
var methodClass = "btn btn-sm method " + method.toLowerCase();
114+
$('#methods').append("<button type='button' class='" + methodClass + "'>" + method + "</button>");
115+
});
116+
117+
// Make the first method active
118+
$('#methods').children(".btn").first().addClass( 'active' );
119+
120+
$("#methods .method").on('click', function (evt) {
121+
// Remove 'active' from all methods
122+
$("#methods .method").removeClass( 'active' );
123+
// Add 'active' to the clicked button
124+
$(this).addClass( 'active' );
125+
_toggleFields();
126+
});
127+
};
128+
129+
var _setupAuthorization = function (permissions) {
130+
if (permissions === 'None' || permissions === 'AllowAny') {
131+
$('#headers').hide();
132+
} else {
133+
$('#headers').show();
134+
// Check if token exists
135+
if (token) {
136+
$('#headers #authorization').val('Token ' + token);
137+
}
138+
}
139+
};
140+
141+
var _toggleFields = function () {
142+
// Show/Hide Data depending on method type
143+
var method = $("#methods").find( ".active" ).text();
144+
if (method === 'GET' || method === 'OPTIONS') {
145+
$('#headerData').hide();
146+
$('#headers #authorization').val();
147+
$('#fields').hide();
148+
} else {
149+
$('#headerData').show();
150+
$('#fields').show();
151+
}
152+
};
153+
154+
var _setupFields = function (fields) {
155+
_toggleFields();
156+
157+
$.each( fields, function( i, field ) {
158+
var label = field.name.replace('_', ' ');
159+
$('#fields').append("" +
160+
'<div class="form-group">' +
161+
'<label for="field' + field.name + '" class="col-sm-4 control-label">' + label + '</label>' +
162+
'<div class="col-sm-8">' +
163+
'<input type="text" class="form-control input-sm" id="' + field.name + '" placeholder="' + field.type + '">' +
164+
'</div>' +
165+
'</div>' +
166+
"");
167+
});
168+
};
169+
170+
var setupForm = function (data) {
171+
// Reset Form - Remove Methods & Fields
172+
resetForm();
173+
cleanResponse();
174+
175+
$('#urlInput').val(data.path);
176+
177+
_setupMethods(data.methods);
178+
_setupAuthorization(data.permissions);
179+
_setupFields(data.fields);
180+
181+
$('#requestForm').submit(function (e) {
182+
// Prevent Submit
183+
e.preventDefault();
184+
185+
// Make Request
186+
makeRequest(data);
187+
});
188+
};
189+
190+
$('.plug').bind('click', function(evt) {
191+
// Prevent the accordion from collapsing
192+
evt.stopPropagation();
193+
evt.preventDefault();
194+
195+
// Open Modal
196+
$('#liveAPIModal').modal('toggle');
197+
198+
// Setup the form
199+
var data = $(this).data();
200+
setupForm(data);
201+
});
202+
203+
});

rest_framework_docs/static/rest_framework_docs/less/style.less

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,22 @@
2020
/* @end Colours */
2121

2222

23+
/* @group Mixins */
24+
25+
// Border Radius (Same Corners)
26+
.BorderRadius(@radius: 5px) {
27+
-webkit-border-radius: @radius;
28+
-moz-border-radius: @radius;
29+
border-radius: @radius;
30+
31+
-moz-background-clip: padding;
32+
-webkit-background-clip: padding-box;
33+
background-clip: padding-box;
34+
}
35+
36+
/* @end Mixins */
37+
38+
2339
/* @group Misc */
2440

2541
body {
@@ -40,6 +56,11 @@ body {
4056
margin-left: 5px;
4157
}
4258

59+
.btn:focus,
60+
.btn:active {
61+
outline: none !important;
62+
}
63+
4364
/* @end Misc */
4465

4566

@@ -85,6 +106,16 @@ body {
85106
&.put { background-color: @PatchColor; }
86107
&.delete { background-color: @DeleteColor; }
87108
&.options { background-color: @OptionsColor; }
109+
110+
&.plug {
111+
color: @brand-primary;
112+
border: 1px solid @brand-primary;
113+
.BorderRadius(50%);
114+
font-size: 14px;
115+
width: 30px;
116+
height: 29px;
117+
margin-left: 10px;
118+
}
88119
}
89120
}
90121
}
@@ -136,6 +167,97 @@ body {
136167
/* @end Footer */
137168

138169

170+
/* @group Modal API */
171+
172+
.modal {
173+
174+
h3 {
175+
margin-top: 0;
176+
}
177+
178+
.control-label {
179+
padding-top: 7px;
180+
}
181+
182+
.request {
183+
184+
.section-title {
185+
background-color: @gray-lighter;
186+
text-align: center;
187+
padding: 5px;
188+
.BorderRadius(3px);
189+
}
190+
191+
.methods {
192+
193+
.method {
194+
color: #FFF;
195+
background-color: @gray-light;
196+
border-left: 1px solid @gray;
197+
198+
&:first-child {
199+
border-left: 0;
200+
}
201+
202+
&.get.active { background-color: @GetColor; }
203+
&.post.active { background-color: @PostColor; }
204+
&.patch.active { background-color: @PutColor; }
205+
&.put.active { background-color: @PatchColor; }
206+
&.delete.active { background-color: @DeleteColor; }
207+
&.options.active { background-color: @OptionsColor; }
208+
}
209+
}
210+
211+
.fields {
212+
label {
213+
text-transform: capitalize;
214+
}
215+
}
216+
}
217+
218+
.response {
219+
.status-code {
220+
font-weight: 300;
221+
222+
&.status-code-1 { background-color: @brand-info; }
223+
&.status-code-2 { background-color: @brand-success; }
224+
&.status-code-3 { background-color: @brand-warning; }
225+
&.status-code-4 { background-color: @brand-danger; }
226+
&.status-code-5 { background-color: @brand-primary; }
227+
}
228+
229+
.status-text {
230+
text-transform: capitalize;
231+
}
232+
233+
pre {
234+
font-size: 12px;
235+
margin-top: 20px;
236+
}
237+
}
238+
}
239+
240+
241+
/* @end Modal API */
242+
243+
244+
/* @group jQuery / JSON View */
245+
246+
.json-key {
247+
color: brown;
248+
}
249+
250+
.json-value {
251+
color: navy;
252+
}
253+
254+
.json-string {
255+
color: olive;
256+
}
257+
258+
/* @end jQuery / JSON View */
259+
260+
139261
/* @group Github Ribbon - SVG */
140262

141263
.github-corner:hover .octo-arm {

0 commit comments

Comments
 (0)