-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathasknot_lib.py
298 lines (236 loc) · 9.96 KB
/
asknot_lib.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
#!/usr/bin/env python
""" Utilities module used by the asknot-ng.py script. """
import hashlib
import os
import random
import subprocess as sp
import sys
import copy
import mako.template
import pkg_resources
import yaml
# Lists of translatable strings so we know what to extract at extraction time
# and so we know what to translate at render time.
translatable_collections = ['negatives', 'affirmatives', 'backlinks']
translatable_fields = ['title', 'description', 'segue1', 'segue2', 'subtitle', 'negative', 'affirmative', 'nextChildLink']
if sys.version_info[0] == 2:
string_types = (basestring,)
else:
string_types = (str, bytes,)
def asknot_version():
try:
return pkg_resources.get_distribution('asknot-ng').version
except pkg_resources.DistributionNotFound:
try:
stdout = sp.check_output(['git', 'rev-parse', 'HEAD'])
return stdout[:8] # Short hash
except:
return 'unknown'
defaults = {
'title': 'asknot-ng',
'author': 'Ralph Bean',
'description': (
'Ask not what $ORG can do for you, '
'but what you can do for $ORG'
),
'asknot_version': asknot_version(),
'favicon': 'whatever',
'googlesiteverification': 'n/a',
'navlinks': [],
'negatives': ['No, thanks'],
'affirmatives': ['Yes, please'],
'backlinks': ['I was wrong, take me back'],
'SEP': '#', # Make this '/' for cool prod environments
}
def load_yaml(filename):
""" Simply load our yaml file from disk. """
with open(filename, 'r') as f:
data = yaml.load(f.read(), Loader=yaml.BaseLoader)
basedir = os.path.dirname(filename)
try:
validate_yaml(data, basedir)
except:
print("Problem with %r due to..." % filename)
raise
return data
def validate_yaml(data, basedir):
""" Sanity check used to make sure the root question file is valid. """
assert 'tree' in data
assert 'children' in data['tree']
validate_tree(data['tree'], basedir)
def validate_tree(node, basedir):
""" Sanity check used to make sure the question tree is valid. """
if not 'children' in node:
if not 'link' in node:
raise ValueError('%r must have either a "href" value or '
'a "children" list' % node)
else:
# Handle recursive includes in yaml files. The children of a node
# may be defined in a separate file
if isinstance(node['children'], string_types):
include_file = node['children']
if not os.path.isabs(include_file):
include_file = os.path.join(basedir, include_file)
node['children'] = load_yaml(include_file)['tree']['children']
# Finally, validate all the children whether they are from a separately
# included file, or not.
for child in node['children']:
validate_tree(child, basedir)
def slugify(title, seen):
""" Return a unique id for a node given its title. """
idx = title.lower()
replacements = {
' ': '-',
'+': 'plus',
'!': 'exclamation',
',': 'comma',
'?': 'question',
'/': 'forwardslash',
'\'': 'apostrophe',
}
for left, right in replacements.items():
idx = idx.replace(left, right)
while idx in seen:
idx = idx + hashlib.md5(idx.encode('utf-8')).hexdigest()[0]
return idx
def prepare_code_items(data):
""" Utility method for cloning items from causes and placing them in code categories automatically.
"""
for codeGroup in data['tree']['children'][1]['children']:
for causeGroup in data['tree']['children'][0]['children']:
for causeCodeGroup in causeGroup['children']:
if causeCodeGroup['title'] == codeGroup['title']:
newCodeCauseGroupChildren = []
if causeCodeGroup.get('children'):
for group in causeCodeGroup['children']:
newCodeCauseGroupChildren.append(copy.deepcopy(group))
newCodeCauseGroup = causeGroup.copy()
codeGroup['children'].append(newCodeCauseGroup)
if newCodeCauseGroupChildren:
newCodeCauseGroup['children'] = newCodeCauseGroupChildren
def prepare_tree(data, node, parent=None, seen=None, _=lambda x: x):
""" Utility method for "enhancing" the data in the question tree.
This is called typically before rendering the mako template with data.
A few things happen here:
- Translatable strings are marked up so they can be translated.
- Unique ids are assigned to each node in the tree for use by JS.
- Texts for 'yes', 'no', and 'go back' are assigned at random per node.
- For each node that doesn't have an image defined, propagate the image
defined by its parent node.
"""
# Markup strings for translation
if node is data.get('tree'):
for collection in translatable_collections:
if collection in data:
data[collection] = [_(s) for s in data[collection]]
for field in translatable_fields:
if field in node:
node[field] = _(node[field])
# Assign a unique id to this node.
seen = seen or []
node['id'] = slugify(node.get('title', 'foo'), seen)
seen.append(node['id'])
# Choose random text for our navigation buttons for this node.
if not node.get('affirmative'):
node['affirmative'] = random.choice(data['affirmatives'])
if not node.get('negative'):
node['negative'] = random.choice(data['negatives'])
node['backlink'] = random.choice(data['backlinks'])
# Propagate parent images to children unless otherwise specified.
if parent and not 'image' in node and 'image' in parent:
node['image'] = parent['image']
# Recursively apply this logic to all children of this node.
for i, child in enumerate(node.get('children', [])):
node['children'][i] = prepare_tree(data, child, parent=node, seen=seen, _=_)
return node
def prepare_next_child(data, node, parent=None, seen=None, _=lambda x: x):
""" Utility method for "enhancing" the data in the question tree with the id of the next child.
"""
# Recursively apply this logic to all children of this node.
for i, child in enumerate(node.get('children', [])):
node['children'][i] = prepare_next_child(data, child, parent=node, seen=seen, _=_)
if parent and parent.get('children') and len(parent.get('children')) > 1 and parent.get('children')[1].get('children'):
node['nextChild'] = parent['children'][1]['children'][0].get('id')
if node.get('nextChildLink') == None:
node['nextChildLink'] = False
return node
def gather_ids(node):
""" Yields all the unique ids in the question tree recursively. """
yield node['id']
for child in node.get('children', []):
for idx in gather_ids(child):
yield idx
def produce_graph(tree, dot=None):
""" Given a question tree, returns a pygraphviz object
for later rendering.
"""
import pygraphviz
dot = dot or pygraphviz.AGraph(directed=True)
idx = tree.get('id', 'root')
dot.add_node(idx, label=tree.get('title', 'Root'))
for child in tree.get('children', []):
dot = produce_graph(child, dot)
dot.add_edge(idx, child['id'])
return dot
def load_template(filename):
""" Load a mako template and return it for later rendering. """
return mako.template.Template(
filename=filename,
strict_undefined=True,
output_encoding='utf-8',
)
def translatable_strings(data):
""" A generator that yields tuples containing translatable strings from a
question tree.
The yielded tuples are of the form (linenumber, string, comment).
"""
for key in translatable_fields:
if key in data:
yield data['__line__'], data[key], key
for key in translatable_collections:
if key in data:
for string in data[key]:
yield data['__line__'], string, key[:-1]
for item in data.get('navlinks', []):
yield data['__line__'], item['name'], 'navlink'
if 'tree' in data:
for items in translatable_strings(data['tree']):
yield items
children = data.get('children', [])
if isinstance(children, str):
pass
else:
for child in children:
for items in translatable_strings(child):
yield items
def load_yaml_with_linenumbers(fileobj):
""" Return yaml with line numbers included in the dict.
This is similar to our mundane ``load_yaml`` function, except that it
modifies the yaml loader to include line numbers in the data. Our babel
extension which is used to extract translatable strings from our yaml files
uses those line numbers to make things easier on translators.
"""
loader = yaml.Loader(fileobj.read())
def compose_node(parent, index):
# the line number where the previous token has ended (plus empty lines)
line = loader.line
node = yaml.composer.Composer.compose_node(loader, parent, index)
node.__line__ = line + 1
return node
def construct_mapping(node, deep=False):
constructor = yaml.constructor.Constructor.construct_mapping
mapping = constructor(loader, node, deep=deep)
mapping['__line__'] = node.__line__
return mapping
loader.compose_node = compose_node
loader.construct_mapping = construct_mapping
return loader.get_single_data()
def extract(fileobj, keywords, comment_tags, options):
""" Babel entry-point for extracting translatable strings from our yaml.
This gets called by 'python setup.py extract_messages' when it encounters a
yaml file. (See setup.py for where we declare the existence of this
function for bable using setuptools 'entry-points').
"""
data = load_yaml_with_linenumbers(fileobj)
for lineno, string, comment in translatable_strings(data):
yield lineno, None, [string], [comment]