/solace/scripts.py
Python | 385 lines | 347 code | 21 blank | 17 comment | 4 complexity | 8ca3086f1fb483121cb9226ac599b22e MD5 | raw file
Possible License(s): BSD-3-Clause
1# -*- coding: utf-8 -*-
2"""
3 solace.scripts
4 ~~~~~~~~~~~~~~
5
6 Provides some setup.py commands. The js-translation compiler is taken
7 from Sphinx, the Python documentation tool.
8
9 :copyright: (c) 2009 by Plurk Inc.
10 (c) 2009 by the Sphinx Team, see AUTHORS for more details.
11 :license: BSD, see LICENSE for more details.
12"""
13# note on imports: This module must not import anything from the
14# solace package, so that the initial import happens in the commands.
15import os
16import sys
17from datetime import datetime, timedelta
18from distutils import log
19from distutils.cmd import Command
20from distutils.errors import DistutilsOptionError, DistutilsSetupError
21from random import randrange, choice, random, shuffle
22from jinja2.utils import generate_lorem_ipsum
23
24from babel.messages.pofile import read_po
25from babel.messages.frontend import compile_catalog
26from simplejson import dump as dump_json
27
28
29class RunserverCommand(Command):
30 description = 'runs the development server'
31 user_options = [
32 ('host=', 'h',
33 'the host of the server, defaults to localhost'),
34 ('port=', 'p',
35 'the port of the server, defaults to 3000'),
36 ('no-reloader', None,
37 'disable the automatic reloader'),
38 ('no-debugger', None,
39 'disable the integrated debugger')
40 ]
41 boolean_options = ['no-reloader', 'no-debugger']
42
43 def initialize_options(self):
44 self.host = 'localhost'
45 self.port = 3000
46 self.no_reloader = False
47 self.no_debugger = False
48
49 def finalize_options(self):
50 if not str(self.port).isdigit():
51 raise DistutilsOptionError('port has to be numeric')
52
53 def run(self):
54 from werkzeug import run_simple
55 def wsgi_app(*a):
56 from solace.application import application
57 return application(*a)
58
59 # werkzeug restarts the interpreter with the same arguments
60 # which would print "running runserver" a second time. Because
61 # of this we force distutils into quiet mode.
62 import sys
63 sys.argv.insert(1, '-q')
64
65 run_simple(self.host, self.port, wsgi_app,
66 use_reloader=not self.no_reloader,
67 use_debugger=not self.no_debugger)
68
69
70class InitDatabaseCommand(Command):
71 description = 'initializes the database'
72 user_options = [
73 ('drop-first', 'D',
74 'drops existing tables first')
75 ]
76 boolean_options = ['drop-first']
77
78 def initialize_options(self):
79 self.drop_first = False
80
81 def finalize_options(self):
82 pass
83
84 def run(self):
85 from solace import database
86 if self.drop_first:
87 database.drop_tables()
88 print 'dropped existing tables'
89 database.init()
90 print 'created database tables'
91
92
93class ResetDatabase(Command):
94 description = 'like initdb, but creates an admin:default user'
95 user_options = [
96 ('username', 'u', 'the admin username'),
97 ('email', 'e', 'the admin email'),
98 ('password', 'p', 'the admin password')
99 ]
100
101 def initialize_options(self):
102 self.username = 'admin'
103 self.email = None
104 self.password = 'default'
105
106 def finalize_options(self):
107 if self.email is None:
108 self.email = self.username + '@localhost'
109
110 def run(self):
111 from solace import database, models
112 database.drop_tables()
113 print 'dropped existing tables'
114 database.init()
115 print 'created database tables'
116 admin = models.User(self.username, self.email, self.password,
117 is_admin=True)
118 database.session.commit()
119 print 'Created %s:%s (%s)' % (self.username, self.password,
120 self.email)
121
122
123class MakeTestData(Command):
124 description = 'adds tons of test data into the database'
125 user_options = [
126 ('data-set-size', 's', 'the size of the dataset '
127 '(small, medium, large)')
128 ]
129
130 USERNAMES = '''
131 asanuma bando chiba ekiguchi erizawa fukuyama inouye ise jo kanada
132 kaneko kasahara kasuse kazuyoshi koyama kumasaka matsushina
133 matsuzawa mazaki miwa momotami morri moto nakamoto nakazawa obinata
134 ohira okakura okano oshima raikatuji saigo sakoda santo sekigawa
135 shibukji sugita tadeshi takahashi takizawa taniguchi tankoshitsu
136 tenshin umehara yamakage yamana yamanouchi yamashita yamura
137 aebru aendra afui asanna callua clesil daev danu eadyel eane efae
138 ettannis fisil frudali glapao glofen grelnor halissa iorran oamira
139 oinnan ondar orirran oudin paenael
140 '''.split()
141 TAGS = '''
142 ajhar amuse animi apiin azoic bacon bala bani bazoo bear bloom bone
143 broke bungo burse caam cento clack clear clog coyly creem cush deity
144 durry ella evan firn grasp gype hance hanky havel hunks ingot javer
145 juno kroo larix lift luke malo marge mart mash nairy nomos noyau
146 papey parch parge parka pheal pint poche pooch puff quit ravin ream
147 remap rotal rowen ruach sadhu saggy saura savor scops seat sere
148 shone shorn sitao skair skep smush snoop soss sprig stalk stiff
149 stipa study swept tang tars taxis terry thirt ulex unkin unmix unsin
150 uprid vire wally wheat woven xylan
151 '''.split()
152 EPOCH = datetime(1930, 1, 1)
153
154 def initialize_options(self):
155 from solace import settings
156 self.data_set_size = 'small'
157 self.highest_date = None
158 self.locales = settings.LANGUAGE_SECTIONS[:]
159
160 def finalize_options(self):
161 if self.data_set_size not in ('small', 'medium', 'large'):
162 raise DistutilsOptionError('invalid value for data-set-size')
163
164 def get_date(self, last=None):
165 secs = randrange(10, 120)
166 d = (last or self.EPOCH) + timedelta(seconds=secs)
167 if self.highest_date is None or d > self.highest_date:
168 self.highest_date = d
169 return d
170
171 def create_users(self):
172 """Creates a bunch of test users."""
173 from solace.models import User
174 num = {'small': 15, 'medium': 30, 'large': 50}[self.data_set_size]
175 result = []
176 used = set()
177 for x in xrange(num):
178 while 1:
179 username = choice(self.USERNAMES)
180 if username not in used:
181 used.add(username)
182 break
183 result.append(User(username, '%s@example.com' % username,
184 'default'))
185 print 'Generated %d users' % num
186 return result
187
188 def create_tags(self):
189 """Creates a bunch of tags."""
190 from solace.models import Tag
191 num = {'small': 10, 'medium': 20, 'large': 50}[self.data_set_size]
192 result = {}
193 tag_count = 0
194 for locale in self.locales:
195 c = result[locale] = []
196 used = set()
197 for x in xrange(randrange(num - 5, num + 5)):
198 while 1:
199 tag = choice(self.TAGS)
200 if tag not in used:
201 used.add(tag)
202 break
203 c.append(Tag(tag, locale).name)
204 tag_count += 1
205 print 'Generated %d tags' % tag_count
206 return result
207
208 def create_topics(self, tags, users):
209 """Generates a bunch of topics."""
210 from solace.models import Topic
211 last_date = None
212 topics = []
213 num, var = {'small': (50, 10), 'medium': (200, 20),
214 'large': (1000, 200)}[self.data_set_size]
215 count = 0
216 for locale in self.locales:
217 for x in xrange(randrange(num - var, num + var)):
218 topic = Topic(locale, generate_lorem_ipsum(1, False, 3, 9),
219 generate_lorem_ipsum(randrange(1, 5), False,
220 40, 140), choice(users),
221 date=self.get_date(last_date))
222 last_date = topic.last_change
223 these_tags = list(tags[locale])
224 shuffle(these_tags)
225 topic.bind_tags(these_tags[:randrange(2, 6)])
226 topics.append(topic)
227 count += 1
228 print 'Generated %d topics in %d locales' % (count, len(self.locales))
229 return topics
230
231 def answer_and_vote(self, topics, users):
232 from solace.models import Post
233 replies = {'small': 4, 'medium': 8, 'large': 12}[self.data_set_size]
234 posts = [x.question for x in topics]
235 last_date = topics[-1].last_change
236 for topic in topics:
237 for x in xrange(randrange(2, replies)):
238 post = Post(topic, choice(users),
239 generate_lorem_ipsum(randrange(1, 3), False,
240 20, 100),
241 self.get_date(last_date))
242 posts.append(post)
243 last_date = post.created
244 print 'Generated %d posts' % len(posts)
245
246 votes = 0
247 for post in posts:
248 for x in xrange(randrange(replies * 4)):
249 post = choice(posts)
250 user = choice(users)
251 if user != post.author:
252 if random() >= 0.05:
253 user.upvote(post)
254 else:
255 user.downvote(post)
256 votes += 1
257
258 print 'Casted %d votes' % votes
259
260 answered = 0
261 for topic in topics:
262 replies = list(topic.replies)
263 if replies:
264 replies.sort(key=lambda x: x.votes)
265 post = choice(replies[:4])
266 if post.votes > 0 and random() > 0.2:
267 topic.accept_answer(post, choice(users))
268 answered += 1
269
270 print 'Answered %d posts' % answered
271 return posts
272
273 def create_comments(self, posts, users):
274 """Creates comments for the posts."""
275 from solace.models import Comment
276 num = {'small': 3, 'medium': 6, 'large': 10}[self.data_set_size]
277 last_date = posts[-1].created
278 comments = 0
279 for post in posts:
280 for x in xrange(randrange(num)):
281 comment = Comment(post, choice(users),
282 generate_lorem_ipsum(1, False, 10, 40),
283 self.get_date(last_date))
284 last_date = comment.date
285 comments += 1
286 print 'Generated %d comments' % comments
287
288 def rebase_dates(self, topics):
289 """Rebase all dates so that they are most recent."""
290 print 'Rebasing dates...',
291 delta = datetime.utcnow() - self.highest_date
292 for topic in topics:
293 topic.last_change += delta
294 topic.date += delta
295 for post in topic.posts:
296 post.updated += delta
297 post.created += delta
298 for comment in post.comments:
299 comment.date += delta
300 topic._update_hotness()
301 print 'done'
302
303 def run(self):
304 from solace.database import session
305 users = self.create_users()
306 tags = self.create_tags()
307 topics = self.create_topics(tags, users)
308 posts = self.answer_and_vote(topics, users)
309 self.create_comments(posts, users)
310 self.rebase_dates(topics)
311 session.commit()
312
313
314class CompileCatalogEx(compile_catalog):
315 """Extends the standard catalog compiler to one that also creates
316 .js files for the strings that are needed in JavaScript.
317 """
318
319 def run(self):
320 compile_catalog.run(self)
321
322 po_files = []
323 js_files = []
324
325 if not self.input_file:
326 if self.locale:
327 po_files.append((self.locale,
328 os.path.join(self.directory, self.locale,
329 'LC_MESSAGES',
330 self.domain + '.po')))
331 js_files.append(os.path.join(self.directory, self.locale,
332 'LC_MESSAGES',
333 self.domain + '.js'))
334 else:
335 for locale in os.listdir(self.directory):
336 po_file = os.path.join(self.directory, locale,
337 'LC_MESSAGES',
338 self.domain + '.po')
339 if os.path.exists(po_file):
340 po_files.append((locale, po_file))
341 js_files.append(os.path.join(self.directory, locale,
342 'LC_MESSAGES',
343 self.domain + '.js'))
344 else:
345 po_files.append((self.locale, self.input_file))
346 if self.output_file:
347 js_files.append(self.output_file)
348 else:
349 js_files.append(os.path.join(self.directory, self.locale,
350 'LC_MESSAGES',
351 self.domain + '.js'))
352
353 for js_file, (locale, po_file) in zip(js_files, po_files):
354 infile = open(po_file, 'r')
355 try:
356 catalog = read_po(infile, locale)
357 finally:
358 infile.close()
359
360 if catalog.fuzzy and not self.use_fuzzy:
361 continue
362
363 log.info('writing JavaScript strings in catalog %r to %r',
364 po_file, js_file)
365
366 jscatalog = {}
367 for message in catalog:
368 if any(x[0].endswith('.js') for x in message.locations):
369 msgid = message.id
370 if isinstance(msgid, (list, tuple)):
371 msgid = msgid[0]
372 jscatalog[msgid] = message.string
373
374 outfile = open(js_file, 'wb')
375 try:
376 outfile.write('Solace.TRANSLATIONS.load(');
377 dump_json(dict(
378 messages=jscatalog,
379 plural_expr=catalog.plural_expr,
380 locale=str(catalog.locale),
381 domain=str(self.domain)
382 ), outfile)
383 outfile.write(');\n')
384 finally:
385 outfile.close()