2 # -*- coding: utf-8 -*-
4 # Copyright 2004-2006 Zuza Software Foundation
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
47 from xml
.etree
import ElementTree
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
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()
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
83 def setdefaultoptions(self
):
84 """sets the default options in the preferences"""
86 if not hasattr(self
.instance
, "title"):
87 setattr(self
.instance
, "title", "Pootle Demo")
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
)
93 if not hasattr(self
.instance
, "baseurl"):
94 setattr(self
.instance
, "baseurl", "/")
96 if not hasattr(self
.instance
, "enablealtsrc"):
97 setattr(self
.instance
, "enablealtsrc", False)
102 def changeoptions(self
, argdict
):
103 """changes options on the instance"""
104 for key
, value
in argdict
.iteritems():
105 if not key
.startswith("option-"):
107 optionname
= key
.replace("option-", "", 1)
108 setattr(self
.instance
, optionname
, value
)
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
:
118 for langpref
in acceptlanguageheader
.split(","):
119 langpref
= pagelayout
.localelanguage(langpref
)
120 pos
= langpref
.find(";")
122 langpref
= langpref
[:pos
]
123 if langpref
in availablelanguages
:
124 session
.setlanguage(langpref
)
126 elif langpref
.startswith("en"):
127 session
.setlanguage(None)
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'):
142 self
.translation
= self
.potree
.getproject(self
.defaultlanguage
, 'pootle')
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)"""
154 return self
.translation
157 return self
.potree
.getproject(language
, 'pootle')
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..."""
166 def filtererrorhandler(functionname
, str1
, str2
, e
):
167 print "error in filter %s: %r, %r, %s" % (functionname
, str1
, str2
, e
)
169 checkerclasses
= [projects
.checks
.StandardChecker
, projects
.checks
.StandardUnitChecker
]
170 stdchecker
= projects
.checks
.TeeChecker(checkerclasses
=checkerclasses
, errorhandler
=filtererrorhandler
)
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
):
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
, "")
182 fpath
= os
.path
.join(reldirname
, fname
)
183 fullpath
= os
.path
.join(dummyproject
.podir
, fpath
)
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
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
)
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
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
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
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:]
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
):
249 jspage
= widgets
.PlainContents(None)
250 jspage
.content_type
= "application/x-javascript"
251 jspage
.sendfile_path
= jsfile
252 jspage
.allowcaching
= True
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
):
260 csspage
= widgets
.PlainContents(None)
261 csspage
.content_type
= "text/css"
262 csspage
.sendfile_path
= cssfile
263 csspage
.allowcaching
= True
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
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
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
287 elif top
== "robots.txt":
288 robotspage
= widgets
.PlainContents(self
.generaterobotsfile())
289 robotspage
.content_type
= 'text/plain'
290 robotspage
.allowcaching
= True
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":
300 returnurl
= argdict
.get('returnurl', None) or getattr(self
.instance
, 'homepage', 'home/')
301 return server
.Redirect(returnurl
)
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:]
317 if not top
or top
== "index.html":
318 return indexpage
.ProjectsIndex(self
.potree
, session
)
321 if not self
.potree
.hasproject(None, projectcode
):
323 pathwords
= pathwords
[1:]
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:]
338 if not top
or top
== "index.html":
339 return indexpage
.LanguagesIndex(self
.potree
, session
)
341 pathwords
= pathwords
[1:]
346 if not session
.isopen
:
347 templatename
= "redirect"
349 "pagetitle": session
.localize("Redirecting to login..."),
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":
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
)
372 pathwords
= pathwords
[1:]
377 if not session
.isopen
:
378 templatename
= "redirect"
380 "pagetitle": session
.localize("Redirecting to login..."),
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"
390 "pagetitle": session
.localize("Redirecting to home..."),
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
):
415 pathwords
= pathwords
[1:]
418 bottom
= pathwords
[-1]
422 if not top
or top
== "index.html":
423 return indexpage
.LanguageIndex(self
.potree
, languagecode
, session
)
424 if self
.potree
.hasproject(languagecode
, top
):
426 project
= self
.potree
.getproject(languagecode
, projectcode
)
427 pathwords
= pathwords
[1:]
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])
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):
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])
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
)
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
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
):
482 etag
, filepath_or_contents
= project
.convert(pofilename
, extension
)
484 page
= widgets
.SendFile(filepath_or_contents
)
485 page
.etag
= str(etag
)
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"
499 elif bottom
.endswith(".zip"):
500 if not "archive" in project
.getrights(session
):
502 if len(pathwords
) > 1:
503 dirfilter
= os
.path
.join(*pathwords
[:-1])
506 goal
= argdict
.get("goal", None)
508 goalfiles
= project
.getgoalfiles(goal
)
510 for goalfile
in goalfiles
:
511 pofilenames
.extend(project
.browsefiles(goalfile
))
513 pofilenames
= project
.browsefiles(dirfilter
)
514 archivecontents
= project
.getarchive(pofilenames
)
515 page
= widgets
.PlainContents(archivecontents
)
516 page
.content_type
= "application/zip"
518 elif bottom
.endswith(".sdf") or bottom
.endswith(".sgi"):
519 if not "pocompile" in project
.getrights(session
):
521 oocontents
= project
.getoo()
522 page
= widgets
.PlainContents(oocontents
)
523 page
.content_type
= "text/tab-seperated-values"
525 elif bottom
== "index.html":
526 if len(pathwords
) > 1:
527 dirfilter
= os
.path
.join(*pathwords
[:-1])
530 return indexpage
.ProjectIndex(project
, session
, argdict
, dirfilter
)
532 return indexpage
.ProjectIndex(project
, session
, argdict
, os
.path
.join(*pathwords
))
535 class PootleOptionParser(simplewebserver
.WebOptionParser
):
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.")
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
:
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":
567 if options
.psyco
is not None:
568 optrecurse
.RecursiveOptionParser(formats
={}).warning("psyco unavailable", options
, sys
.exc_info())
570 if options
.psyco
is None:
571 options
.psyco
= "full"
572 if options
.psyco
== "full":
574 elif options
.psyco
== "profile":
576 # tell psyco the functions it cannot compile, to prevent warnings
578 psyco
.cannotcompile(encodings
.search_function
)
583 parser
= PootleOptionParser()
584 options
, args
= parser
.parse_args()
585 options
.errorlevel
= options
.logerrors
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__':