-
Notifications
You must be signed in to change notification settings - Fork 196
/
Copy pathadmin_resources.py
432 lines (393 loc) · 17.6 KB
/
admin_resources.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
# Copyright 2011 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from google.appengine.ext import db
from google.appengine.api import users
import base64
import cgi
import datetime
import mimetypes
import config
import const
from resources import Resource, ResourceBundle
import utils
# Because this handler is used to administer resources such as templates and
# stylesheets, nothing in this handler depends on those resources.
PREFACE = '''
<style>
body, table, th, td, input { font-family: arial; font-size: 13px; }
body, form, input { margin: 0; padding: 0; }
.nav { padding: 6px 12px; background: #cdf; }
a { text-decoration: none; color: #06c; }
a.sel { color: #000; font-weight: bold; }
a:hover { text-decoration: underline; }
form { display: inline; }
textarea { font-family: courier, courier new, monospace; font-size: 12px; }
img { margin: 12px 0; border: 1px solid #eee; }
.editable .hide-when-editable { display: none; }
.readonly .hide-when-readonly { display: none; }
table { min-width: 400px; margin: 12px; border: 1px solid #ccc; }
tr { vertical-align: baseline; }
th, td { text-align: left; padding: 3px 10px; min-width: 5em; }
th { border-bottom: 1px solid #ccc; }
.active td { background: #afa; }
#show-unaltered { margin-left: 1em; font-weight: normal; color: #aaa; }
div.add { margin: 12px; }
.warning { color: #a00; }
a.bundle { color: #06c; }
a.resource { color: #06c; }
a.file { color: #aaa; }
</style>
'''
def html(s):
"""Converts plain text to HTML."""
try:
s = s.decode('utf-8') # for textarea editing, we need Unicode
except:
s = s.decode('latin-1') # in case the content was uploaded binary data
return s.replace('&','&').replace('<','<').replace('>','>')
def format_datetime(dt):
"""Formats a datetime object for display in a directory listing."""
now = datetime.datetime.utcnow()
delta = now - dt
if delta < datetime.timedelta(days=1):
if delta.seconds < 3600:
return '%d min ago' % int(delta.seconds / 60)
return '%.1f h ago' % (delta.seconds / 3600.0)
else:
return '%04d-%02d-%02d %02d:%02d UTC' % (
dt.year, dt.month, dt.day, dt.hour, dt.minute)
def put_bundle(new_bundle_name, original_bundle_name=None):
"""Puts a new ResourceBundle, optionally copying from another bundle."""
new_bundle = ResourceBundle(key_name=new_bundle_name)
entities = [new_bundle]
if original_bundle_name:
original_bundle = ResourceBundle.get_by_key_name(original_bundle_name)
original_resources = Resource.all().ancestor(original_bundle)
entities += [Resource(parent=new_bundle,
key_name=resource.key().name(),
cache_seconds=resource.cache_seconds,
content=resource.content)
for resource in original_resources]
db.put(entities)
def put_resource(bundle_name, key_name, **kwargs):
"""Puts a Resource in the datastore under the specified ResourceBundle."""
bundle = ResourceBundle(key_name=bundle_name)
Resource(parent=bundle, key_name=key_name, **kwargs).put()
def format_content_html(content, name, editable):
"""Formats HTML to show a Resource's content, optionally for editing."""
type = mimetypes.guess_type(name)[0] or 'text/plain'
if name.endswith('.template') or type.startswith('text/'):
return '<textarea name="content" cols=80 rows=40 %s>%s</textarea>' % (
not editable and 'readonly' or '', html(content))
content_html = '%s data, %d bytes' % (type, len(content))
if type.startswith('image/'):
content_html += '<br><img src="data:%s;base64,%s">' % (
type, base64.encodestring(content))
return content_html
class Handler(utils.BaseHandler):
"""A page that lets app administrators create resource bundles, create
and edit resources, and preview bundles before making them default."""
# Resources apply to all repositories.
repo_required = False
admin_required = True
def get_admin_url(self, bundle_name=None, name=None, lang=None, **params):
"""Constructs a parameterized URL to this page."""
return self.get_url('admin/resources',
resource_bundle=bundle_name,
resource_name=name,
resource_lang=lang,
**params)
def format_nav_html(self, bundle_name, name, lang):
"""Formats the breadcrumb navigation bar."""
crumbs = [('All bundles', ())]
if bundle_name:
crumbs.append(('Bundle: %s' % bundle_name, (bundle_name,)))
if bundle_name and name:
crumbs.append(('Resource: %s' % name, (bundle_name, name)))
if bundle_name and name and lang:
anchor = lang + ': ' + const.LANGUAGE_EXONYMS.get(lang, '?')
crumbs.append((anchor, (bundle_name, name, lang)))
last = crumbs[-1][1]
anchors = (
['<a href="%s">Admin page</a>' % self.get_url('admin')] +
['<a class="%s" href="%s">%s</a>' %
('sel' if args == last else '', self.get_admin_url(*args), html(anchor))
for anchor, args in crumbs])
return '<div class="nav">%s</div>' % (' > '.join(anchors))
def get(self):
self.handle(self.params.operation)
def post(self):
self.handle(self.params.operation)
def handle(self, operation):
"""Handles both GET and POST requests. POST requests include an
'operation' param describing what the user is trying to change."""
bundle_name = self.params.resource_bundle or ''
name = self.params.resource_name or ''
lang = self.params.resource_lang or ''
key_name = name + (lang and ':' + lang)
editable = (bundle_name != self.env.default_resource_bundle)
if not ResourceBundle.get_by_key_name(self.env.default_resource_bundle):
ResourceBundle(key_name=self.env.default_resource_bundle).put()
if not operation:
self.write(PREFACE + self.format_nav_html(bundle_name, name, lang))
if bundle_name and name:
self.show_resource(bundle_name, key_name, name, lang, editable)
elif bundle_name:
self.list_resources(bundle_name, editable)
else:
self.list_bundles()
return
user = users.get_current_user()
xsrf_tool = utils.XsrfTool()
if not (self.params.xsrf_token and xsrf_tool.verify_token(
self.params.xsrf_token, user.user_id(), 'admin_resources')):
return self.error(403)
if operation == 'set_preview':
# Set the resource_bundle cookie. This causes all pages to render
# using the selected bundle (see main.py). We use a cookie so that
# it's possible to preview PF as embedded on external sites.
self.response.headers['Set-Cookie'] = \
'resource_bundle=%s; path=/' % bundle_name
return self.redirect(self.get_admin_url())
if operation == 'set_default':
# Set the default resource bundle.
config.set(default_resource_bundle=
self.params.resource_bundle_default)
return self.redirect(self.get_admin_url())
if operation == 'add_bundle' and editable:
# Add a new bundle, optionally copying from an existing bundle.
put_bundle(bundle_name, self.params.resource_bundle_original)
return self.redirect(self.get_admin_url(bundle_name))
if operation == 'add_resource' and editable:
# Go to the edit page for a new resource (don't create until save).
return self.redirect(self.get_admin_url(bundle_name, name, lang))
if operation == 'delete_resource' and editable:
# Delete a resource.
resource = Resource.get(key_name, bundle_name)
if resource:
resource.delete()
return self.redirect(self.get_admin_url(bundle_name))
if operation == 'put_resource' and editable:
# Store the content of a resource.
if isinstance(self.request.POST.get('file'), cgi.FieldStorage):
content = self.request.get('file') # uploaded file content
elif 'content' in self.request.POST: # edited text
content = self.request.get('content').encode('utf-8')
else: # modify cache_seconds but leave content unchanged
resource = Resource.get(key_name, bundle_name)
content = resource and resource.content or ''
put_resource(bundle_name, key_name, content=content,
cache_seconds=self.params.cache_seconds)
return self.redirect(self.get_admin_url(bundle_name))
def show_resource(self, bundle_name, key_name, name, lang, editable):
"""Displays a single resource, optionally for editing."""
resource = Resource.get(key_name, bundle_name) or Resource()
content = resource.content or ''
self.write('''
<form method="post" class="%(class)s" enctype="multipart/form-data">
<input type="hidden" name="xsrf_token" value="%(xsrf_token)s" />
<input type="hidden" name="operation" value="put_resource">
<input type="hidden" name="resource_bundle" value="%(bundle_name)s">
<input type="hidden" name="resource_name" value="%(name)s">
<input type="hidden" name="resource_lang" value="%(lang)s">
<table cellpadding=0 cellspacing=0>
<tr>
<td class="warning hide-when-editable">
This bundle cannot be edited while it is set as default.
</td>
</tr>
<tr><td colspan=2>%(content_html)s</td></tr>
<tr>
<td style="position: relative">
<button style="position: absolute">Replace with a file</button>
<input type="file" name="file" class="hide-when-readonly"
onchange="document.forms[0].submit()"
style="position: absolute; opacity: 0; z-index: 1">
</td>
<td style="text-align: right">
Cache seconds: <input %(maybe_readonly)s size=4
name="cache_seconds" value="%(cache_seconds).1f">
</td>
</tr>
<tr class="hide-when-readonly">
<td>
<button onclick="delete_resource()">Delete resource</button>
</td>
<td style="text-align: right">
<input type="submit" name="save_content" value="Save resource">
</td>
</tr>
</table>
</form>
<script>
function delete_resource() {
if (confirm('Really delete %(name)s?')) {
document.forms[0].operation.value = 'delete_resource';
document.forms[0].submit();
}
}
</script>''' % {'class': editable and 'editable' or 'readonly',
'bundle_name': bundle_name,
'name': name,
'lang': lang,
'content_html': format_content_html(content, name, editable),
'cache_seconds': resource.cache_seconds,
'maybe_readonly': not editable and 'readonly' or '',
'xsrf_token': self._get_xsrf_token()})
def list_resources(self, bundle_name, editable):
"""Displays a list of the resources in a bundle."""
bundle = ResourceBundle.get_by_key_name(bundle_name)
editable_class = editable and 'editable' or 'readonly'
langs_by_name = {} # Group language variants of each resource together.
for filename in Resource.list_files():
name, lang = (filename.rsplit(':', 1) + [None])[:2]
langs_by_name.setdefault(name, {})[lang] = 'file'
for resource_name in bundle.list_resources():
name, lang = (resource_name.rsplit(':', 1) + [None])[:2]
langs_by_name.setdefault(name, {})[lang] = 'resource'
rows = [] # Each row shows one Resource and its localized variants.
for name in sorted(langs_by_name):
sources_by_lang = langs_by_name[name]
altered = 'resource' in sources_by_lang.values()
generic = '<a class="%s" href="%s">%s</a>' % (
sources_by_lang.pop(None, 'missing'),
self.get_admin_url(bundle_name, name), html(name))
variants = ['<a class="%s" href="%s">%s</a>' % (
sources_by_lang[lang],
self.get_admin_url(bundle_name, name, lang), html(lang))
for lang in sorted(sources_by_lang)]
rows.append('''
<tr class="%(altered)s">
<td>%(generic)s</td>
<td>%(variants)s
<form method="post" class="%(class)s">
<input type="hidden" name="xsrf_token" value="%(xsrf_token)s" />
<input type="hidden" name="operation" value="add_resource">
<input type="hidden" name="resource_name" value="%(name)s">
<input name="resource_lang" size=3 class="hide-when-readonly"
placeholder="lang">
<input type="submit" value="Add" class="hide-when-readonly">
</form>
</td>
</tr>''' % {'altered': altered and 'altered' or 'unaltered',
'generic': generic,
'variants': ', '.join(variants),
'class': editable_class,
'name': name,
'xsrf_token': self._get_xsrf_token()})
self.write('''
<table cellpadding=0 cellspacing=0>
<tr><th>
Resource name
<span id="show-unaltered">
<input id="show-unaltered-checkbox" type="checkbox"
onchange="show_unaltered(this.checked)">
<label for="show-unaltered-checkbox">Show unaltered files</label>
</span>
</th><th>Localized variants</th></tr>
<tr class="add"><td>
<form method="post" class="%(class)s">
<input type="hidden" name="xsrf_token" value="%(xsrf_token)s" />
<input type="hidden" name="operation" value="add_resource">
<input name="resource_name" size="36" class="hide-when-readonly"
placeholder="resource filename">
<input type="submit" value="Add" class="hide-when-readonly">
<div class="warning hide-when-editable">
This bundle cannot be edited while it is set as default.
</div>
</form>
</td><td></td></tr>
%(rows)s
</table>
<script>
function show_unaltered(show) {
var rows = document.getElementsByClassName('unaltered');
for (var r = 0; r < rows.length; r++) {
rows[r].style.display = show ? '' : 'none';
}
}
show_unaltered(false);
</script>
<form method="post" action="%(action)s">
<input type="hidden" name="xsrf_token" value="%(xsrf_token)s" />
<input type="hidden" name="operation" value="add_bundle">
<input type="hidden" name="resource_bundle_original" value="%(bundle_name)s">
<table cellpadding=0 cellspacing=0>
<tr><td>
Copy all these resources to another bundle:
<input name="resource_bundle" size="18"
placeholder="bundle name">
<input type="submit" value="Copy">
</td></tr>
</table>
</form>''' % {'class': editable_class,
'rows': ''.join(rows),
'action': self.get_admin_url(),
'bundle_name': bundle_name,
'xsrf_token': self._get_xsrf_token()})
def list_bundles(self):
"""Displays a list of all the resource bundles."""
bundles = list(ResourceBundle.all().order('-created'))
rows = []
for bundle in bundles:
bundle_name = bundle.key().name()
bundle_name_html = html(bundle_name)
is_default = bundle_name == self.env.default_resource_bundle
is_in_preview = bundle_name == self.env.resource_bundle
if is_default:
bundle_name_html = '<b>%s</b> (default)' % bundle_name_html
elif is_in_preview:
bundle_name_html += ' (in preview)'
rows.append('''
<tr class="%(class)s">
<td><a class="bundle" href="%(link)s">%(bundle_name_html)s</a></td>
<td>%(created)s</td>
<td><a href="%(preview)s"><input type="button" value="Preview"></a></td>
<td><input type="radio" name="resource_bundle_default" value="%(bundle_name)s"
%(default_checked)s></td>
</tr>''' % {
'class': is_in_preview and 'active' or '',
'link': self.get_admin_url(bundle_name),
'bundle_name': bundle_name,
'bundle_name_html': bundle_name_html,
'default_checked': is_default and 'checked' or '',
'created': format_datetime(bundle.created),
'preview': self.get_admin_url(bundle_name, operation='set_preview'),
'xsrf_token': self._get_xsrf_token()})
self.write('''
<form method="post">
<input type="hidden" name="xsrf_token" value="%(xsrf_token)s" />
<input type="hidden" name="operation" value="set_default">
<table cellpadding=0 cellspacing=0>
<tr><th>Bundle name</th><th>Created</th>
<th><a href="%(reset)s"><input type="button" value="Reset to default"
></a></th>
<th><input type="submit" value="Set to default"></th></tr>
%(rows)s
</table>
</form>
<div class="add">
<form method="post">
<input type="hidden" name="xsrf_token" value="%(xsrf_token)s" />
<input type="hidden" name="operation" value="add_bundle">
<input name="resource_bundle" size="18" placeholder="bundle name">
<input type="submit" value="Add bundle">
</form>
</div>''' % {'reset': self.get_admin_url(operation='set_preview'),
'rows': ''.join(rows),
'xsrf_token': self._get_xsrf_token()})
def _get_xsrf_token(self):
user = users.get_current_user()
xsrf_tool = utils.XsrfTool()
return xsrf_tool.generate_token(user.user_id(), 'admin_resources')