modinfo in the translate toolkit is now a 2-tuple containing the mtime and
[pootle.git] / pootle.py
blob35270bd89486ad63da4215420e4c51ed552013d1
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 #
4 # Copyright 2004-2006 Zuza Software Foundation
5 #
6 # This file is part of translate.
8 # translate is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 2 of the License, or
11 # (at your option) any later version.
13 # translate is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with translate; if not, write to the Free Software
20 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
22 from jToolkit.web import server
23 from jToolkit.web import templateserver
24 from jToolkit.web import session
25 from jToolkit import prefs
26 from jToolkit import localize
27 from jToolkit.widgets import widgets
28 from jToolkit.widgets import spellui
29 from jToolkit.widgets import thumbgallery
30 from jToolkit.web import simplewebserver
31 from Pootle import indexpage
32 from Pootle import adminpages
33 from Pootle import translatepage
34 from Pootle import pagelayout
35 from Pootle import projects
36 from Pootle import potree
37 from Pootle import pootlefile
38 from Pootle import users
39 from Pootle import filelocations
40 from translate.misc import optrecurse
41 # Versioning information
42 from Pootle import __version__ as pootleversion
43 from translate import __version__ as toolkitversion
44 from jToolkit import __version__ as jtoolkitversion
45 from Pootle import statistics
46 try:
47 from xml.etree import ElementTree
48 except ImportError:
49 from elementtree import ElementTree
50 # We don't need kid in this file, but this will show quickly if it is not
51 # installed. jToolkit won't complain, so we have to stop here if we don't have kid
52 import kid
53 import sys
54 import os
55 import re
56 import random
57 import pprint
59 class PootleServer(users.OptionalLoginAppServer, templateserver.TemplateServer):
60 """the Server that serves the Pootle Pages"""
61 def __init__(self, instance, webserver, sessioncache=None, errorhandler=None, loginpageclass=users.LoginPage):
62 if sessioncache is None:
63 sessioncache = session.SessionCache(sessionclass=users.PootleSession)
64 self.potree = potree.POTree(instance)
65 super(PootleServer, self).__init__(instance, webserver, sessioncache, errorhandler, loginpageclass)
66 self.templatedir = filelocations.templatedir
67 self.setdefaultoptions()
69 def loadurl(self, filename, context):
70 """loads a url internally for overlay code"""
71 # print "call to load %s with context:\n%s" % (filename, pprint.pformat(context))
72 filename = os.path.join(self.templatedir, filename+os.extsep+"html")
73 if os.path.exists(filename):
74 return open(filename, "r").read()
75 return None
77 def saveprefs(self):
78 """saves any changes made to the preferences"""
79 # TODO: this is a hack, fix it up nicely :-)
80 prefsfile = self.instance.__root__.__dict__["_setvalue"].im_self
81 prefsfile.savefile()
83 def setdefaultoptions(self):
84 """sets the default options in the preferences"""
85 changed = False
86 if not hasattr(self.instance, "title"):
87 setattr(self.instance, "title", "Pootle Demo")
88 changed = True
89 if not hasattr(self.instance, "description"):
90 defaultdescription = "This is a demo installation of pootle. The administrator can customize the description in the preferences."
91 setattr(self.instance, "description", defaultdescription)
92 changed = True
93 if not hasattr(self.instance, "baseurl"):
94 setattr(self.instance, "baseurl", "/")
95 changed = True
96 if not hasattr(self.instance, "enablealtsrc"):
97 setattr(self.instance, "enablealtsrc", False)
98 changed = True
99 if changed:
100 self.saveprefs()
102 def changeoptions(self, argdict):
103 """changes options on the instance"""
104 for key, value in argdict.iteritems():
105 if not key.startswith("option-"):
106 continue
107 optionname = key.replace("option-", "", 1)
108 setattr(self.instance, optionname, value)
109 self.saveprefs()
111 def initlanguage(self, req, session):
112 """Initialises the session language from the request"""
113 availablelanguages = self.potree.getlanguagecodes('pootle')
114 acceptlanguageheader = req.headers_in.getheader('Accept-Language')
115 if not acceptlanguageheader:
116 return
118 for langpref in acceptlanguageheader.split(","):
119 langpref = pagelayout.localelanguage(langpref)
120 pos = langpref.find(";")
121 if pos >= 0:
122 langpref = langpref[:pos]
123 if langpref in availablelanguages:
124 session.setlanguage(langpref)
125 return
126 elif langpref.startswith("en"):
127 session.setlanguage(None)
128 return
129 session.setlanguage(None)
131 def inittranslation(self, localedir=None, localedomains=None, defaultlanguage=None):
132 """initializes live translations using the Pootle PO files"""
133 self.localedomains = ['jToolkit', 'pootle']
134 self.localedir = None
135 self.languagelist = self.potree.getlanguagecodes('pootle')
136 self.languagenames = self.potree.getlanguages()
137 self.defaultlanguage = defaultlanguage
138 if self.defaultlanguage is None:
139 self.defaultlanguage = getattr(self.instance, "defaultlanguage", "en")
140 if self.potree.hasproject(self.defaultlanguage, 'pootle'):
141 try:
142 self.translation = self.potree.getproject(self.defaultlanguage, 'pootle')
143 return
144 except Exception, e:
145 self.errorhandler.logerror("Could not initialize translation:\n%s" % str(e))
146 # if no translation available, set up a blank translation
147 super(PootleServer, self).inittranslation()
148 # the inherited method overwrites self.languagenames, so we have to redo it
149 self.languagenames = self.potree.getlanguages()
151 def gettranslation(self, language):
152 """returns a translation object for the given language (or default if language is None)"""
153 if language is None:
154 return self.translation
155 else:
156 try:
157 return self.potree.getproject(language, 'pootle')
158 except Exception, e:
159 if not language.startswith('en'):
160 self.errorhandler.logerror("Could not get translation for language %r:\n%s" % (language,str(e)))
161 return self.translation
163 def refreshstats(self, args):
164 """refreshes all the available statistics..."""
165 if args:
166 def filtererrorhandler(functionname, str1, str2, e):
167 print "error in filter %s: %r, %r, %s" % (functionname, str1, str2, e)
168 return False
169 checkerclasses = [projects.checks.StandardChecker, projects.checks.StandardUnitChecker]
170 stdchecker = projects.checks.TeeChecker(checkerclasses=checkerclasses, errorhandler=filtererrorhandler)
171 for arg in args:
172 if not os.path.exists(arg):
173 print "file not found:", arg
174 if os.path.isdir(arg):
175 if not arg.endswith(os.sep):
176 arg += os.sep
177 projectcode, languagecode = self.potree.getcodesfordir(arg)
178 dummyproject = projects.DummyStatsProject(arg, stdchecker, projectcode, languagecode)
179 def refreshdir(dummy, dirname, fnames):
180 reldirname = dirname.replace(dummyproject.podir, "")
181 for fname in fnames:
182 fpath = os.path.join(reldirname, fname)
183 fullpath = os.path.join(dummyproject.podir, fpath)
184 #TODO: PO specific
185 if fname.endswith(".po") and not os.path.isdir(fullpath):
186 if not os.path.exists(fullpath):
187 print "file does not exist:", fullpath
188 return
189 print "refreshing stats for", fpath
190 pootlefile.pootlefile(dummyproject, fpath).statistics.updatequickstats()
191 os.path.walk(arg, refreshdir, None)
192 if projectcode and languagecode:
193 dummyproject.savequickstats()
194 elif os.path.isfile(arg):
195 dummyproject = projects.DummyStatsProject(".", stdchecker)
196 print "refreshing stats for", arg
197 projects.pootlefile.pootlefile(dummyproject, arg)
198 else:
199 print "refreshing stats for all files in all projects"
200 self.potree.refreshstats()
202 def generateactivationcode(self):
203 """generates a unique activation code"""
204 return "".join(["%02x" % int(random.random()*0x100) for i in range(16)])
206 def generaterobotsfile(self):
207 """generates the robots.txt file"""
208 langcodes = self.potree.getlanguagecodes()
209 excludedfiles = ["login.html", "register.html", "activate.html"]
210 content = "User-agent: *\n"
211 for excludedfile in excludedfiles:
212 content += "Disallow: /%s\n" % excludedfile
213 for langcode in langcodes:
214 content += "Disallow: /%s/\n" % langcode
215 return content
217 def getpage(self, pathwords, session, argdict):
218 """return a page that will be sent to the user"""
219 #Ensure we get unicode from argdict
220 #TODO: remove when jToolkit does this
221 newargdict = {}
222 for key, value in argdict.iteritems():
223 if isinstance(key, str):
224 key = key.decode("utf-8")
225 if isinstance(value, str):
226 value = value.decode("utf-8")
227 newargdict[key] = value
228 argdict = newargdict
230 # Strip of the base url
231 baseurl = re.sub('http://[^/]', '', self.instance.baseurl)
232 # Split up and remove empty parts
233 basepathwords = filter(None, baseurl.split('/'))
234 while pathwords and basepathwords and basepathwords[0] == pathwords[0]:
235 basepathwords = basepathwords[1:]
236 pathwords = pathwords[1:]
238 if pathwords:
239 top = pathwords[0]
240 else:
241 top = ""
242 if top == 'js':
243 pathwords = pathwords[1:]
244 jsfile = os.path.join(filelocations.htmldir, 'js', *pathwords)
245 if not os.path.exists(jsfile):
246 jsfile = os.path.join(filelocations.jtoolkitdir, 'js', *pathwords)
247 if not os.path.exists(jsfile):
248 return None
249 jspage = widgets.PlainContents(None)
250 jspage.content_type = "application/x-javascript"
251 jspage.sendfile_path = jsfile
252 jspage.allowcaching = True
253 return jspage
254 elif pathwords and pathwords[-1].endswith(".css"):
255 cssfile = os.path.join(filelocations.htmldir, *pathwords)
256 if not os.path.exists(cssfile):
257 cssfile = os.path.join(filelocations.jtoolkitdir, *pathwords)
258 if not os.path.exists(cssfile):
259 return None
260 csspage = widgets.PlainContents(None)
261 csspage.content_type = "text/css"
262 csspage.sendfile_path = cssfile
263 csspage.allowcaching = True
264 return csspage
265 elif top in ['selenium', 'tests']:
266 picturefile = os.path.join(filelocations.htmldir, *pathwords)
267 picture = widgets.SendFile(picturefile)
268 if picturefile.endswith(".html"):
269 picture.content_type = 'text/html'
270 elif picturefile.endswith(".js"):
271 picture.content_type = 'text/javascript'
272 picture.allowcaching = True
273 return picture
274 elif top == 'images':
275 pathwords = pathwords[1:]
276 picturefile = os.path.join(filelocations.htmldir, 'images', *pathwords)
277 picture = widgets.SendFile(picturefile)
278 picture.content_type = thumbgallery.getcontenttype(pathwords[-1])
279 picture.allowcaching = True
280 return picture
281 elif pathwords and pathwords[-1].endswith(".ico"):
282 picturefile = os.path.join(filelocations.htmldir, *pathwords)
283 picture = widgets.SendFile(picturefile)
284 picture.content_type = 'image/ico'
285 picture.allowcaching = True
286 return picture
287 elif top == "robots.txt":
288 robotspage = widgets.PlainContents(self.generaterobotsfile())
289 robotspage.content_type = 'text/plain'
290 robotspage.allowcaching = True
291 return robotspage
292 elif top == "testtemplates.html":
293 return templateserver.TemplateServer.getpage(self, pathwords, session, argdict)
294 elif not top or top == "index.html":
295 return indexpage.PootleIndex(self.potree, session)
296 elif top == 'about.html':
297 return indexpage.AboutPage(session)
298 elif top == "login.html":
299 if session.isopen:
300 returnurl = argdict.get('returnurl', None) or getattr(self.instance, 'homepage', 'home/')
301 return server.Redirect(returnurl)
302 message = None
303 if 'username' in argdict:
304 session.username = argdict["username"]
305 message = session.localize("Login failed")
306 return users.LoginPage(session, languagenames=self.languagenames, message=message)
307 elif top == "register.html":
308 return self.registerpage(session, argdict)
309 elif top == "activate.html":
310 return self.activatepage(session, argdict)
311 elif top == "projects":
312 pathwords = pathwords[1:]
313 if pathwords:
314 top = pathwords[0]
315 else:
316 top = ""
317 if not top or top == "index.html":
318 return indexpage.ProjectsIndex(self.potree, session)
319 else:
320 projectcode = top
321 if not self.potree.hasproject(None, projectcode):
322 return None
323 pathwords = pathwords[1:]
324 if pathwords:
325 top = pathwords[0]
326 else:
327 top = ""
328 if not top or top == "index.html":
329 return indexpage.ProjectLanguageIndex(self.potree, projectcode, session)
330 elif top == "admin.html":
331 return adminpages.ProjectAdminPage(self.potree, projectcode, session, argdict)
332 elif top == "languages":
333 pathwords = pathwords[1:]
334 if pathwords:
335 top = pathwords[0]
336 else:
337 top = ""
338 if not top or top == "index.html":
339 return indexpage.LanguagesIndex(self.potree, session)
340 elif top == "home":
341 pathwords = pathwords[1:]
342 if pathwords:
343 top = pathwords[0]
344 else:
345 top = ""
346 if not session.isopen:
347 templatename = "redirect"
348 templatevars = {
349 "pagetitle": session.localize("Redirecting to login..."),
350 "refresh": 1,
351 "refreshurl": "login.html",
352 "message": session.localize("Need to log in to access home page"),
354 pagelayout.completetemplatevars(templatevars, session)
355 return server.Redirect("../login.html", withtemplate=(templatename, templatevars))
356 if not top or top == "index.html":
357 return indexpage.UserIndex(self.potree, session)
358 elif top == "options.html":
359 message = None
360 try:
361 if "changeoptions" in argdict:
362 session.setoptions(argdict)
363 if "changepersonal" in argdict:
364 session.setpersonaloptions(argdict)
365 message = session.localize("Personal details updated")
366 if "changeinterface" in argdict:
367 session.setinterfaceoptions(argdict)
368 except users.RegistrationError, errormessage:
369 message = errormessage
370 return users.UserOptions(self.potree, session, message)
371 elif top == "admin":
372 pathwords = pathwords[1:]
373 if pathwords:
374 top = pathwords[0]
375 else:
376 top = ""
377 if not session.isopen:
378 templatename = "redirect"
379 templatevars = {
380 "pagetitle": session.localize("Redirecting to login..."),
381 "refresh": 1,
382 "refreshurl": "login.html",
383 "message": session.localize("Need to log in to access admin page"),
385 pagelayout.completetemplatevars(templatevars, session)
386 return server.Redirect("../login.html", withtemplate=(templatename, templatevars))
387 if not session.issiteadmin():
388 templatename = "redirect"
389 templatevars = {
390 "pagetitle": session.localize("Redirecting to home..."),
391 "refresh": 1,
392 "refreshurl": "login.html",
393 "message": self.localize("You do not have the rights to administer pootle."),
395 pagelayout.completetemplatevars(templatevars, session)
396 return server.Redirect("../index.html", withtemplate=(templatename, templatevars))
397 if not top or top == "index.html":
398 if "changegeneral" in argdict:
399 self.changeoptions(argdict)
400 return adminpages.AdminPage(self.potree, session, self.instance)
401 elif top == "users.html":
402 if "changeusers" in argdict:
403 self.changeusers(session, argdict)
404 return adminpages.UsersAdminPage(self, session.loginchecker.users, session, self.instance)
405 elif top == "languages.html":
406 if "changelanguages" in argdict:
407 self.potree.changelanguages(argdict)
408 return adminpages.LanguagesAdminPage(self.potree, session, self.instance)
409 elif top == "projects.html":
410 if "changeprojects" in argdict:
411 self.potree.changeprojects(argdict)
412 return adminpages.ProjectsAdminPage(self.potree, session, self.instance)
413 elif top == "templates" or self.potree.haslanguage(top):
414 languagecode = top
415 pathwords = pathwords[1:]
416 if pathwords:
417 top = pathwords[0]
418 bottom = pathwords[-1]
419 else:
420 top = ""
421 bottom = ""
422 if not top or top == "index.html":
423 return indexpage.LanguageIndex(self.potree, languagecode, session)
424 if self.potree.hasproject(languagecode, top):
425 projectcode = top
426 project = self.potree.getproject(languagecode, projectcode)
427 pathwords = pathwords[1:]
428 if pathwords:
429 top = pathwords[0]
430 else:
431 top = ""
432 if not top or top == "index.html":
433 return indexpage.ProjectIndex(project, session, argdict)
434 elif top == "admin.html":
435 return adminpages.TranslationProjectAdminPage(self.potree, project, session, argdict)
436 elif bottom == "translate.html":
437 if len(pathwords) > 1:
438 dirfilter = os.path.join(*pathwords[:-1])
439 else:
440 dirfilter = ""
441 try:
442 return translatepage.TranslatePage(project, session, argdict, dirfilter)
443 except projects.RightsError, stoppedby:
444 argdict["message"] = str(stoppedby)
445 return indexpage.ProjectIndex(project, session, argdict, dirfilter)
446 elif bottom == "spellcheck.html":
447 # the full review page
448 argdict["spellchecklang"] = languagecode
449 return spellui.SpellingReview(session, argdict, js_url="/js/spellui.js")
450 elif bottom == "spellingstandby.html":
451 # a simple 'loading' page
452 return spellui.SpellingStandby()
453 elif bottom.endswith("." + project.fileext):
454 pofilename = os.path.join(*pathwords)
455 if argdict.get("translate", 0):
456 try:
457 return translatepage.TranslatePage(project, session, argdict, dirfilter=pofilename)
458 except projects.RightsError, stoppedby:
459 if len(pathwords) > 1:
460 dirfilter = os.path.join(*pathwords[:-1])
461 else:
462 dirfilter = ""
463 argdict["message"] = str(stoppedby)
464 return indexpage.ProjectIndex(project, session, argdict, dirfilter=dirfilter)
465 elif argdict.get("index", 0):
466 return indexpage.ProjectIndex(project, session, argdict, dirfilter=pofilename)
467 else:
468 pofile = project.getpofile(pofilename, freshen=False)
469 page = widgets.SendFile(pofile.filename)
470 page.etag = str(pofile.pomtime)
471 encoding = getattr(pofile, "encoding", "UTF-8")
472 page.content_type = "text/plain; charset=%s" % encoding
473 return page
474 elif bottom.endswith(".csv") or bottom.endswith(".xlf") or bottom.endswith(".ts") or bottom.endswith(".po") or bottom.endswith(".mo"):
475 destfilename = os.path.join(*pathwords)
476 basename, extension = os.path.splitext(destfilename)
477 pofilename = basename + os.extsep + project.fileext
478 extension = extension[1:]
479 if extension == "mo":
480 if not "pocompile" in project.getrights(session):
481 return None
482 etag, filepath_or_contents = project.convert(pofilename, extension)
483 if etag:
484 page = widgets.SendFile(filepath_or_contents)
485 page.etag = str(etag)
486 else:
487 page = widgets.PlainContents(filepath_or_contents)
488 if extension == "po":
489 page.content_type = "text/x-gettext-translation; charset=UTF-8"
490 elif extension == "csv":
491 page.content_type = "text/csv; charset=UTF-8"
492 elif extension == "xlf":
493 page.content_type = "application/x-xliff; charset=UTF-8"
494 elif extension == "ts":
495 page.content_type = "application/x-linguist; charset=UTF-8"
496 elif extension == "mo":
497 page.content_type = "application/x-gettext-translation"
498 return page
499 elif bottom.endswith(".zip"):
500 if not "archive" in project.getrights(session):
501 return None
502 if len(pathwords) > 1:
503 dirfilter = os.path.join(*pathwords[:-1])
504 else:
505 dirfilter = None
506 goal = argdict.get("goal", None)
507 if goal:
508 goalfiles = project.getgoalfiles(goal)
509 pofilenames = []
510 for goalfile in goalfiles:
511 pofilenames.extend(project.browsefiles(goalfile))
512 else:
513 pofilenames = project.browsefiles(dirfilter)
514 archivecontents = project.getarchive(pofilenames)
515 page = widgets.PlainContents(archivecontents)
516 page.content_type = "application/zip"
517 return page
518 elif bottom.endswith(".sdf") or bottom.endswith(".sgi"):
519 if not "pocompile" in project.getrights(session):
520 return None
521 oocontents = project.getoo()
522 page = widgets.PlainContents(oocontents)
523 page.content_type = "text/tab-seperated-values"
524 return page
525 elif bottom == "index.html":
526 if len(pathwords) > 1:
527 dirfilter = os.path.join(*pathwords[:-1])
528 else:
529 dirfilter = None
530 return indexpage.ProjectIndex(project, session, argdict, dirfilter)
531 else:
532 return indexpage.ProjectIndex(project, session, argdict, os.path.join(*pathwords))
533 return None
535 class PootleOptionParser(simplewebserver.WebOptionParser):
536 def __init__(self):
537 versionstring = "%%prog %s\njToolkit %s\nTranslate Toolkit %s\nKid %s\nElementTree %s\nPython %s (on %s/%s)" % (pootleversion.ver, jtoolkitversion.ver, toolkitversion.ver, kid.__version__, ElementTree.VERSION, sys.version, sys.platform, os.name)
538 simplewebserver.WebOptionParser.__init__(self, version=versionstring)
539 self.set_default('prefsfile', filelocations.prefsfile)
540 self.set_default('instance', 'Pootle')
541 self.set_default('htmldir', filelocations.htmldir)
542 self.add_option('', "--refreshstats", dest="action", action="store_const", const="refreshstats",
543 default="runwebserver", help="refresh the stats files instead of running the webserver")
544 psycomodes=["none", "full", "profile"]
545 self.add_option('', "--statsdb_file", action="store", type="string", dest="statsdb_file",
546 default=None, help="Specifies the location of the SQLite stats db file.")
547 try:
548 import psyco
549 self.add_option('', "--psyco", dest="psyco", default=None, choices=psycomodes, metavar="MODE",
550 help="use psyco to speed up the operation, modes: %s" % (", ".join(psycomodes)))
551 except ImportError, e:
552 return
554 def checkversions():
555 """Checks that version dependencies are met"""
556 if not hasattr(toolkitversion, "build") or toolkitversion.build < 11000:
557 raise RuntimeError("requires Translate Toolkit version >= 1.1. Current installed version is: %s" % toolkitversion.ver)
559 def usepsyco(options):
560 # options.psyco == None means the default, which is "full", but don't give a warning...
561 # options.psyco == "none" means don't use psyco at all...
562 if getattr(options, "psyco", "none") == "none":
563 return
564 try:
565 import psyco
566 except ImportError:
567 if options.psyco is not None:
568 optrecurse.RecursiveOptionParser(formats={}).warning("psyco unavailable", options, sys.exc_info())
569 return
570 if options.psyco is None:
571 options.psyco = "full"
572 if options.psyco == "full":
573 psyco.full()
574 elif options.psyco == "profile":
575 psyco.profile()
576 # tell psyco the functions it cannot compile, to prevent warnings
577 import encodings
578 psyco.cannotcompile(encodings.search_function)
580 def main():
581 # run the web server
582 checkversions()
583 parser = PootleOptionParser()
584 options, args = parser.parse_args()
585 options.errorlevel = options.logerrors
586 usepsyco(options)
587 statistics.STATS_DB_FILE = options.statsdb_file
588 if options.action != "runwebserver":
589 options.servertype = "dummy"
590 server = parser.getserver(options)
591 server.options = options
592 if options.action == "runwebserver":
593 simplewebserver.run(server, options)
594 elif options.action == "refreshstats":
595 server.refreshstats(args)
597 if __name__ == '__main__':
598 main()