modinfo in the translate toolkit is now a 2-tuple containing the mtime and
[pootle.git] / indexpage.py
blob325aedabd68d23bc7687f6e9c9e266a2ce607173
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 #
4 # Copyright 2004-2007 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 Pootle import pagelayout
23 from Pootle import projects
24 from Pootle import pootlefile
25 from translate.storage import versioncontrol
26 # Versioning information
27 from Pootle import __version__ as pootleversion
28 from translate import __version__ as toolkitversion
29 from jToolkit import __version__ as jtoolkitversion
30 from kid import __version__ as kidversion
31 try:
32 # ElementTree is part of Python 2.5, so let's try that first
33 from xml.etree import ElementTree
34 except ImportError:
35 from elementtree import ElementTree
36 import os
37 import sys
38 import re
39 import locale
41 def shortdescription(descr):
42 """Returns a short description by removing markup and only including up
43 to the first br-tag"""
44 stopsign = descr.find("<br")
45 if stopsign >= 0:
46 descr = descr[:stopsign]
47 return re.sub("<[^>]*>", "", descr).strip()
49 class AboutPage(pagelayout.PootlePage):
50 """the bar at the side describing current login details etc"""
51 def __init__(self, session):
52 self.localize = session.localize
53 pagetitle = getattr(session.instance, "title")
54 description = getattr(session.instance, "description")
55 meta_description = shortdescription(description)
56 keywords = ["Pootle", "WordForge", "translate", "translation", "localisation",
57 "localization", "l10n", "traduction", "traduire"]
58 abouttitle = self.localize("About Pootle")
59 # l10n: Take care to use HTML tags correctly. A markup error could cause a display error.
60 introtext = self.localize("<strong>Pootle</strong> is a simple web portal that should allow you to <strong>translate</strong>! Since Pootle is <strong>Free Software</strong>, you can download it and run your own copy if you like. You can also help participate in the development in many ways (you don't have to be able to program).")
61 hosttext = self.localize('The Pootle project itself is hosted at <a href="http://translate.sourceforge.net/">translate.sourceforge.net</a> where you can find the details about source code, mailing lists etc.')
62 # l10n: If your language uses right-to-left layout and you leave the English untranslated, consider enclosing the necessary text with <span dir="ltr">.......</span> to help browsers to display it correctly.
63 # l10n: Take care to use HTML tags correctly. A markup error could cause a display error.
64 nametext = self.localize('The name stands for <b>PO</b>-based <b>O</b>nline <b>T</b>ranslation / <b>L</b>ocalization <b>E</b>ngine, but you may need to read <a href="http://www.thechestnut.com/flumps.htm">this</a>.')
65 versiontitle = self.localize("Versions")
66 # l10n: If your language uses right-to-left layout and you leave the English untranslated, consider enclosing the necessary text with <span dir="ltr">.......</span> to help browsers to display it correctly.
67 # l10n: Take care to use HTML tags correctly. A markup error could cause a display error.
68 versiontext = self.localize("This site is running:<br />Pootle %s<br />Translate Toolkit %s<br />jToolkit %s<br />Kid %s<br />ElementTree %s<br />Python %s (on %s/%s)", pootleversion.ver, toolkitversion.ver, jtoolkitversion.ver, kidversion, ElementTree.VERSION, sys.version, sys.platform, os.name)
69 templatename = "about"
70 instancetitle = getattr(session.instance, "title", session.localize("Pootle Demo"))
71 sessionvars = {"status": session.status, "isopen": session.isopen, "issiteadmin": session.issiteadmin()}
72 templatevars = {"pagetitle": pagetitle, "description": description,
73 "meta_description": meta_description, "keywords": keywords,
74 "abouttitle": abouttitle, "introtext": introtext,
75 "hosttext": hosttext, "nametext": nametext, "versiontitle": versiontitle, "versiontext": versiontext,
76 "session": sessionvars, "instancetitle": instancetitle}
77 pagelayout.PootlePage.__init__(self, templatename, templatevars, session)
79 class PootleIndex(pagelayout.PootlePage):
80 """The main page listing projects and languages. It is also reused for
81 LanguagesIndex and ProjectsIndex"""
82 def __init__(self, potree, session):
83 self.potree = potree
84 self.localize = session.localize
85 self.nlocalize = session.nlocalize
86 self.tr_lang = session.tr_lang
87 self.listseperator = session.lang.listseperator
88 templatename = "index"
89 description = getattr(session.instance, "description")
90 meta_description = shortdescription(description)
91 keywords = ["Pootle", "WordForge", "translate", "translation", "localisation", "localization",
92 "l10n", "traduction", "traduire"] + self.getprojectnames()
93 languagelink = self.localize('Languages')
94 projectlink = self.localize('Projects')
95 instancetitle = getattr(session.instance, "title", session.localize("Pootle Demo"))
96 pagetitle = instancetitle
97 sessionvars = {"status": session.status, "isopen": session.isopen, "issiteadmin": session.issiteadmin()}
98 languages = [{"code": code, "name": self.tr_lang(name), "sep": self.listseperator} for code, name in self.potree.getlanguages()]
99 # rewritten for compatibility with Python 2.3
100 # languages.sort(cmp=locale.strcoll, key=lambda dict: dict["name"])
101 languages.sort(lambda x,y: locale.strcoll(x["name"], y["name"]))
102 if languages:
103 languages[-1]["sep"] = ""
104 templatevars = {"pagetitle": pagetitle, "description": description,
105 "meta_description": meta_description, "keywords": keywords,
106 "languagelink": languagelink, "languages": languages,
107 "projectlink": projectlink, "projects": self.getprojects(),
108 "session": sessionvars, "instancetitle": instancetitle}
109 pagelayout.PootlePage.__init__(self, templatename, templatevars, session)
111 def getprojects(self):
112 """gets the options for the projects"""
113 projects = []
114 for projectcode in self.potree.getprojectcodes():
115 projectname = self.potree.getprojectname(projectcode)
116 description = shortdescription(self.potree.getprojectdescription(projectcode))
117 projects.append({"code": projectcode, "name": projectname, "description": description, "sep": self.listseperator})
118 if projects:
119 projects[-1]["sep"] = ""
120 return projects
122 def getprojectnames(self):
123 return [self.potree.getprojectname(projectcode) for projectcode in self.potree.getprojectcodes()]
125 class UserIndex(pagelayout.PootlePage):
126 """home page for a given user"""
127 def __init__(self, potree, session):
128 self.potree = potree
129 self.session = session
130 self.tr_lang = session.tr_lang
131 self.localize = session.localize
132 self.nlocalize = session.nlocalize
133 pagetitle = self.localize("User Page for: %s", session.username)
134 templatename = "home"
135 optionslink = self.localize("Change options")
136 adminlink = self.localize("Admin page")
137 admintext = self.localize("Administrate")
138 quicklinkstitle = self.localize("Quick Links")
139 instancetitle = getattr(session.instance, "title", session.localize("Pootle Demo"))
140 sessionvars = {"status": session.status, "isopen": session.isopen, "issiteadmin": session.issiteadmin()}
141 quicklinks = self.getquicklinks()
142 setoptionstext = self.localize("Please click on 'Change options' and select some languages and projects")
143 templatevars = {"pagetitle": pagetitle, "optionslink": optionslink,
144 "adminlink": adminlink, "admintext": admintext,
145 "quicklinkstitle": quicklinkstitle,
146 "quicklinks": quicklinks, "setoptionstext": setoptionstext,
147 "session": sessionvars, "instancetitle": instancetitle}
148 pagelayout.PootlePage.__init__(self, templatename, templatevars, session)
150 def getquicklinks(self):
151 """gets a set of quick links to user's project-languages"""
152 quicklinks = []
153 for languagecode in self.session.getlanguages():
154 if not self.potree.haslanguage(languagecode):
155 continue
156 languagename = self.potree.getlanguagename(languagecode)
157 langlinks = []
158 for projectcode in self.session.getprojects():
159 if self.potree.hasproject(languagecode, projectcode):
160 projecttitle = self.potree.getprojectname(projectcode)
161 project = self.potree.getproject(languagecode, projectcode)
162 isprojectadmin = "admin" in project.getrights(session=self.session) \
163 or self.session.issiteadmin()
164 langlinks.append({"code": projectcode, "name": projecttitle,
165 "isprojectadmin": isprojectadmin, "sep": "<br />"})
166 if langlinks:
167 langlinks[-1]["sep"] = ""
168 quicklinks.append({"code": languagecode, "name": self.tr_lang(languagename), "projects": langlinks})
169 # rewritten for compatibility with Python 2.3
170 # quicklinks.sort(cmp=locale.strcoll, key=lambda dict: dict["name"])
171 quicklinks.sort(lambda x,y: locale.strcoll(x["name"], y["name"]))
172 return quicklinks
174 class ProjectsIndex(PootleIndex):
175 """the list of languages"""
176 def __init__(self, potree, session):
177 PootleIndex.__init__(self, potree, session)
178 self.templatename = "projects"
180 class LanguagesIndex(PootleIndex):
181 """the list of languages"""
182 def __init__(self, potree, session):
183 PootleIndex.__init__(self, potree, session)
184 self.templatename = "languages"
186 class LanguageIndex(pagelayout.PootleNavPage):
187 """The main page for a language, listing all the projects in it"""
188 def __init__(self, potree, languagecode, session):
189 self.potree = potree
190 self.languagecode = languagecode
191 self.localize = session.localize
192 self.nlocalize = session.nlocalize
193 self.tr_lang = session.tr_lang
194 self.languagename = self.potree.getlanguagename(self.languagecode)
195 self.initpagestats()
196 languageprojects = self.getprojects()
197 self.projectcount = len(languageprojects)
198 average = self.getpagestats()
199 languagestats = self.nlocalize("%d project, average %d%% translated", "%d projects, average %d%% translated", self.projectcount, self.projectcount, average)
200 languageinfo = self.getlanguageinfo()
201 instancetitle = getattr(session.instance, "title", session.localize("Pootle Demo"))
202 # l10n: The first parameter is the name of the installation
203 # l10n: The second parameter is the name of the project/language
204 # l10n: This is used as a page title. Most languages won't need to change this
205 pagetitle = self.localize("%s: %s", instancetitle, self.tr_lang(self.languagename))
206 templatename = "language"
207 adminlink = self.localize("Admin")
208 sessionvars = {"status": session.status, "isopen": session.isopen, "issiteadmin": session.issiteadmin()}
209 templatevars = {"pagetitle": pagetitle,
210 "language": {"code": languagecode, "name": self.tr_lang(self.languagename), "stats": languagestats, "info": languageinfo},
211 "projects": languageprojects,
212 "statsheadings": self.getstatsheadings(),
213 "session": sessionvars, "instancetitle": instancetitle}
214 pagelayout.PootleNavPage.__init__(self, templatename, templatevars, session, bannerheight=80)
216 def getlanguageinfo(self):
217 """returns information defined for the language"""
218 # specialchars = self.potree.getlanguagespecialchars(self.languagecode)
219 nplurals = self.potree.getlanguagenplurals(self.languagecode)
220 pluralequation = self.potree.getlanguagepluralequation(self.languagecode)
221 infoparts = [(self.localize("Language Code"), self.languagecode),
222 (self.localize("Language Name"), self.tr_lang(self.languagename)),
223 # (self.localize("Special Characters"), specialchars),
224 (self.localize("Number of Plurals"), str(nplurals)),
225 (self.localize("Plural Equation"), pluralequation),
227 return [{"title": title, "value": value} for title, value in infoparts]
229 def getprojects(self):
230 """gets the info on the projects"""
231 projectcodes = self.potree.getprojectcodes(self.languagecode)
232 self.projectcount = len(projectcodes)
233 projectitems = [self.getprojectitem(projectcode) for projectcode in projectcodes]
234 for n, item in enumerate(projectitems):
235 item["parity"] = ["even", "odd"][n % 2]
236 return projectitems
238 def getprojectitem(self, projectcode):
239 href = '%s/' % projectcode
240 projectname = self.potree.getprojectname(projectcode)
241 projectdescription = shortdescription(self.potree.getprojectdescription(projectcode))
242 project = self.potree.getproject(self.languagecode, projectcode)
243 pofilenames = project.browsefiles()
244 projectstats = project.getquickstats()
245 projectdata = self.getstats(project, projectstats)
246 self.updatepagestats(projectdata["translatedsourcewords"], projectdata["totalsourcewords"])
247 return {"code": projectcode, "href": href, "icon": "folder", "title": projectname, "description": projectdescription, "data": projectdata, "isproject": True}
249 class ProjectLanguageIndex(pagelayout.PootleNavPage):
250 """The main page for a project, listing all the languages belonging to it"""
251 def __init__(self, potree, projectcode, session):
252 self.potree = potree
253 self.projectcode = projectcode
254 self.localize = session.localize
255 self.nlocalize = session.nlocalize
256 self.tr_lang = session.tr_lang
257 self.initpagestats()
258 languages = self.getlanguages()
259 average = self.getpagestats()
260 projectstats = self.nlocalize("%d language, average %d%% translated", "%d languages, average %d%% translated", self.languagecount, self.languagecount, average)
261 projectname = self.potree.getprojectname(self.projectcode)
262 description = self.potree.getprojectdescription(projectcode)
263 meta_description = shortdescription(description)
264 instancetitle = getattr(session.instance, "title", session.localize("Pootle Demo"))
265 # l10n: The first parameter is the name of the installation
266 # l10n: The second parameter is the name of the project/language
267 # l10n: This is used as a page title. Most languages won't need to change this
268 pagetitle = self.localize("%s: %s", instancetitle, projectname)
269 templatename = "project"
270 adminlink = self.localize("Admin")
271 sessionvars = {"status": session.status, "isopen": session.isopen, "issiteadmin": session.issiteadmin()}
272 statsheadings = self.getstatsheadings()
273 statsheadings["name"] = self.localize("Language")
274 templatevars = {"pagetitle": pagetitle,
275 "project": {"code": projectcode, "name": projectname, "stats": projectstats},
276 "description": description, "meta_description": meta_description,
277 "adminlink": adminlink, "languages": languages,
278 "session": sessionvars, "instancetitle": instancetitle,
279 "statsheadings": statsheadings}
280 pagelayout.PootleNavPage.__init__(self, templatename, templatevars, session, bannerheight=80)
282 def getlanguages(self):
283 """gets the stats etc of the languages"""
284 languages = self.potree.getlanguages(self.projectcode)
285 self.languagecount = len(languages)
286 languageitems = [self.getlanguageitem(languagecode, languagename) for languagecode, languagename in languages]
287 # rewritten for compatibility with Python 2.3
288 # languageitems.sort(cmp=locale.strcoll, key=lambda dict: dict["title"])
289 languageitems.sort(lambda x,y: locale.strcoll(x["title"], y["title"]))
290 for n, item in enumerate(languageitems):
291 item["parity"] = ["even", "odd"][n % 2]
292 return languageitems
294 def getlanguageitem(self, languagecode, languagename):
295 language = self.potree.getproject(languagecode, self.projectcode)
296 href = "../../%s/%s/" % (languagecode, self.projectcode)
297 quickstats = language.getquickstats()
298 data = self.getstats(language, quickstats)
299 self.updatepagestats(data["translatedsourcewords"], data["totalsourcewords"])
300 return {"code": languagecode, "icon": "language", "href": href, "title": self.tr_lang(languagename), "data": data}
302 class ProjectIndex(pagelayout.PootleNavPage):
303 """The main page of a project in a specific language"""
304 def __init__(self, project, session, argdict, dirfilter=None):
305 self.project = project
306 self.session = session
307 self.localize = session.localize
308 self.nlocalize = session.nlocalize
309 self.tr_lang = session.tr_lang
310 self.rights = self.project.getrights(self.session)
311 message = argdict.get("message", "")
312 if dirfilter == "":
313 dirfilter = None
314 self.dirfilter = dirfilter
315 if dirfilter and dirfilter.endswith(".po"):
316 self.dirname = os.path.dirname(dirfilter)
317 else:
318 self.dirname = dirfilter or ""
319 self.argdict = argdict
320 # handle actions before generating URLs, so we strip unneccessary parameters out of argdict
321 self.handleactions()
322 # generate the navigation bar maintaining state
323 navbarpath_dict = self.makenavbarpath_dict(project=self.project, session=self.session, currentfolder=dirfilter, argdict=self.argdict)
324 self.showtracks = self.getboolarg("showtracks")
325 self.showchecks = self.getboolarg("showchecks")
326 self.showassigns = self.getboolarg("showassigns")
327 self.showgoals = self.getboolarg("showgoals")
328 self.editing = self.getboolarg("editing")
329 self.currentgoal = self.argdict.pop("goal", None)
330 if dirfilter and dirfilter.endswith(".po"):
331 actionlinks = []
332 mainstats = ""
333 mainicon = "file"
334 else:
335 pofilenames = self.project.browsefiles(dirfilter)
336 projecttotals = self.project.getquickstats(pofilenames)
337 if self.editing or self.showassigns or self.showchecks:
338 # we need the complete stats
339 projectstats = self.project.combinestats(pofilenames)
340 else:
341 projectstats = projecttotals
342 if self.editing:
343 actionlinks = self.getactionlinks("", projectstats, ["editing", "mine", "review", "check", "assign", "goal", "quick", "all", "zip", "sdf"], dirfilter)
344 else:
345 actionlinks = self.getactionlinks("", projectstats, ["editing", "goal", "zip", "sdf"])
346 mainstats = self.getitemstats("", pofilenames, len(pofilenames))
347 mainstats["summary"] = self.describestats(self.project, projecttotals, len(pofilenames))
348 if self.showgoals:
349 childitems = self.getgoalitems(dirfilter)
350 else:
351 childitems = self.getchilditems(dirfilter)
352 instancetitle = getattr(session.instance, "title", session.localize("Pootle Demo"))
353 # l10n: The first parameter is the name of the installation (like "Pootle")
354 pagetitle = self.localize("%s: Project %s, Language %s", instancetitle, self.project.projectname, self.tr_lang(self.project.languagename))
355 templatename = "fileindex"
356 sessionvars = {"status": session.status, "isopen": session.isopen, "issiteadmin": session.issiteadmin()}
357 templatevars = {"pagetitle": pagetitle,
358 "project": {"code": self.project.projectcode, "name": self.project.projectname},
359 "language": {"code": self.project.languagecode, "name": self.tr_lang(self.project.languagename)},
360 # optional sections, will appear if these values are replaced
361 "assign": None, "goals": None, "upload": None,
362 "search": {"title": self.localize("Search")}, "message": message,
363 # navigation bar
364 "navitems": [{"icon": "folder", "path": navbarpath_dict, "actions": actionlinks, "stats": mainstats}],
365 # children
366 "children": childitems,
367 # are we in editing mode (otherwise stats)
368 "editing": self.editing,
369 # stats table headings
370 "statsheadings": self.getstatsheadings(),
371 # general vars
372 "session": sessionvars, "instancetitle": instancetitle}
373 pagelayout.PootleNavPage.__init__(self, templatename, templatevars, session, bannerheight=80)
374 if self.showassigns and "assign" in self.rights:
375 self.templatevars["assign"] = self.getassignbox()
376 if "admin" in self.rights:
377 if self.showgoals:
378 self.templatevars["goals"] = self.getgoalbox()
379 if "admin" in self.rights or "translate" in self.rights or "suggest" in self.rights:
380 self.templatevars["upload"] = self.getuploadbox()
382 def handleactions(self):
383 """handles the given actions that must be taken (changing operations)"""
384 if "doassign" in self.argdict:
385 assignto = self.argdict.pop("assignto", None)
386 action = self.argdict.pop("action", None)
387 if not assignto and action:
388 raise ValueError("cannot doassign, need assignto and action")
389 search = pootlefile.Search(dirfilter=self.dirfilter)
390 assigncount = self.project.assignpoitems(self.session, search, assignto, action)
391 print "assigned %d strings to %s for %s" % (assigncount, assignto, action)
392 del self.argdict["doassign"]
393 if self.getboolarg("removeassigns"):
394 assignedto = self.argdict.pop("assignedto", None)
395 removefilter = self.argdict.pop("removefilter", "")
396 if removefilter:
397 if self.dirfilter:
398 removefilter = self.dirfilter + removefilter
399 else:
400 removefilter = self.dirfilter
401 search = pootlefile.Search(dirfilter=removefilter)
402 search.assignedto = assignedto
403 assigncount = self.project.unassignpoitems(self.session, search, assignedto)
404 print "removed %d assigns from %s" % (assigncount, assignedto)
405 del self.argdict["removeassigns"]
406 if "doupload" in self.argdict:
407 extensiontypes = ("xlf", "xlff", "xliff", "po")
408 if "Yes" in self.argdict.pop("dooverwrite", []):
409 overwrite = True
410 else:
411 overwrite = False
412 uploadfile = self.argdict.pop("uploadfile", None)
413 # multiple translation file extensions check
414 if filter(uploadfile.filename.endswith, extensiontypes):
415 transfiles = True
416 else:
417 transfiles = False
418 if not uploadfile.filename:
419 raise ValueError(self.localize("Cannot upload file, no file attached"))
420 if transfiles:
421 self.project.uploadfile(self.session, self.dirname, uploadfile.filename, uploadfile.contents, overwrite)
422 elif uploadfile.filename.endswith(".zip"):
423 self.project.uploadarchive(self.session, self.dirname, uploadfile.contents)
424 else:
425 raise ValueError(self.localize("Can only upload PO files and zips of PO files"))
426 del self.argdict["doupload"]
427 if "doupdate" in self.argdict:
428 updatefile = self.argdict.pop("updatefile", None)
429 if not updatefile:
430 raise ValueError("cannot update file, no file specified")
431 if updatefile.endswith("." + self.project.fileext):
432 self.project.updatepofile(self.session, self.dirname, updatefile)
433 else:
434 raise ValueError("can only update files with extension ." + self.project.fileext)
435 del self.argdict["doupdate"]
436 if "docommit" in self.argdict:
437 commitfile = self.argdict.pop("commitfile", None)
438 if not commitfile:
439 raise ValueError("cannot commit file, no file specified")
440 if commitfile.endswith("." + self.project.fileext):
441 self.project.commitpofile(self.session, self.dirname, commitfile)
442 else:
443 raise ValueError("can only commit files with extension ." + self.project.fileext)
444 del self.argdict["docommit"]
445 if "doaddgoal" in self.argdict:
446 goalname = self.argdict.pop("newgoal", None)
447 if not goalname:
448 raise ValueError("cannot add goal, no name given")
449 self.project.setgoalfiles(self.session, goalname.strip(), "")
450 del self.argdict["doaddgoal"]
451 if "doeditgoal" in self.argdict:
452 goalnames = self.argdict.pop("editgoal", None)
453 goalfile = self.argdict.pop("editgoalfile", None)
454 if not goalfile:
455 raise ValueError("cannot add goal, no filename given")
456 if self.dirname:
457 goalfile = os.path.join(self.dirname, goalfile)
458 if not isinstance(goalnames, list):
459 goalnames = [goalnames]
460 goalnames = [goalname.strip() for goalname in goalnames if goalname.strip()]
461 self.project.setfilegoals(self.session, goalnames, goalfile)
462 del self.argdict["doeditgoal"]
463 if "doeditgoalusers" in self.argdict:
464 goalname = self.argdict.pop("editgoalname", "").strip()
465 if not goalname:
466 raise ValueError("cannot edit goal, no name given")
467 goalusers = self.project.getgoalusers(goalname)
468 addusername = self.argdict.pop("newgoaluser", "").strip()
469 if addusername:
470 self.project.addusertogoal(self.session, goalname, addusername)
471 del self.argdict["doeditgoalusers"]
472 if "doedituser" in self.argdict:
473 goalnames = self.argdict.pop("editgoal", None)
474 goalusers = self.argdict.pop("editfileuser", "")
475 goalfile = self.argdict.pop("editgoalfile", None)
476 assignwhich = self.argdict.pop("edituserwhich", "all")
477 if not goalfile:
478 raise ValueError("cannot add user to file for goal, no filename given")
479 if self.dirname:
480 goalfile = os.path.join(self.dirname, goalfile)
481 if not isinstance(goalusers, list):
482 goalusers = [goalusers]
483 goalusers = [goaluser.strip() for goaluser in goalusers if goaluser.strip()]
484 if not isinstance(goalnames, list):
485 goalnames = [goalnames]
486 goalnames = [goalname.strip() for goalname in goalnames if goalname.strip()]
487 search = pootlefile.Search(dirfilter=goalfile)
488 if assignwhich == "all":
489 pass
490 elif assignwhich == "untranslated":
491 search.matchnames = ["fuzzy", "untranslated"]
492 elif assignwhich == "unassigned":
493 search.assignedto = [None]
494 elif assignwhich == "unassigneduntranslated":
495 search.matchnames = ["fuzzy", "untranslated"]
496 search.assignedto = [None]
497 else:
498 raise ValueError("unexpected assignwhich")
499 for goalname in goalnames:
500 action = "goal-" + goalname
501 self.project.reassignpoitems(self.session, search, goalusers, action)
502 del self.argdict["doedituser"]
503 # pop arguments we don't want to propogate through inadvertently...
504 for argname in ("assignto", "action", "assignedto", "removefilter",
505 "uploadfile", "updatefile", "commitfile",
506 "newgoal", "editgoal", "editgoalfile", "editgoalname",
507 "newgoaluser", "editfileuser", "edituserwhich"):
508 self.argdict.pop(argname, "")
510 def getboolarg(self, argname, default=False):
511 """gets a boolean argument from self.argdict"""
512 value = self.argdict.get(argname, default)
513 if isinstance(value, bool):
514 return value
515 elif isinstance(value, int):
516 return bool(value)
517 elif isinstance(value, (str, unicode)):
518 value = value.lower()
519 if value.isdigit():
520 return bool(int(value))
521 if value == "true":
522 return True
523 if value == "false":
524 return False
525 raise ValueError("Invalid boolean value for %s: %r" % (argname, value))
527 def getassignbox(self):
528 """adds a box that lets the user assign strings"""
529 users = self.project.getuserswithinterest(self.session)
530 return {
531 "users": users,
532 "title": self.localize("Assign Strings"),
533 "action_text": self.localize("Assign Action"),
534 "users_text": self.localize("Assign to User"),
535 "button": self.localize("Assign Strings")
538 def getgoalbox(self):
539 """adds a box that lets the user add a new goal"""
540 return {"title": self.localize('goals'),
541 "name-title": self.localize("Enter goal name"),
542 "button": self.localize("Add Goal")}
544 def getuploadbox(self):
545 """adds a box that lets the user assign strings"""
546 uploadbox = {
547 "title": self.localize("Upload File"),
548 "file_title": self.localize("Select file to upload"),
549 "upload_button": self.localize("Upload File")
551 if "admin" in self.rights or "overwrite" in self.rights:
552 uploadbox.update({
553 #l10n: radio button text
554 "overwrite": self.localize("Overwrite"),
555 #l10n: tooltip
556 "overwrite_title": self.localize("Overwrite the current file if it exists"),
557 #l10n: radio button text
558 "merge": self.localize("Merge"),
559 #l10n: tooltip
560 "merge_title": self.localize("Merge the file with the current file and turn conflicts into suggestions"),
562 return uploadbox
564 def getchilditems(self, dirfilter):
565 """get all the items for directories and files viewable at this level"""
566 if dirfilter is None:
567 depth = 0
568 else:
569 depth = dirfilter.count(os.path.sep)
570 if not dirfilter.endswith(os.path.extsep + self.project.fileext):
571 depth += 1
572 diritems = []
573 for childdir in self.project.browsefiles(dirfilter=dirfilter, depth=depth, includedirs=True, includefiles=False):
574 diritem = self.getdiritem(childdir)
575 diritems.append((childdir, diritem))
576 diritems.sort()
577 fileitems = []
578 for childfile in self.project.browsefiles(dirfilter=dirfilter, depth=depth, includefiles=True, includedirs=False):
579 fileitem = self.getfileitem(childfile)
580 fileitems.append((childfile, fileitem))
581 fileitems.sort()
582 childitems = [diritem for childdir, diritem in diritems] + [fileitem for childfile, fileitem in fileitems]
583 self.polarizeitems(childitems)
584 return childitems
586 def getitems(self, itempaths, linksrequired=None, **newargs):
587 """gets the listed dir and fileitems"""
588 diritems, fileitems = [], []
589 for item in itempaths:
590 if item.endswith(os.path.extsep + self.project.fileext):
591 fileitem = self.getfileitem(item, linksrequired=linksrequired, **newargs)
592 fileitems.append((item, fileitem))
593 else:
594 if item.endswith(os.path.sep):
595 item = item.rstrip(os.path.sep)
596 diritem = self.getdiritem(item, linksrequired=linksrequired, **newargs)
597 diritems.append((item, diritem))
598 diritems.sort()
599 fileitems.sort()
600 childitems = [diritem for childdir, diritem in diritems] + [fileitem for childfile, fileitem in fileitems]
601 self.polarizeitems(childitems)
602 return childitems
604 def getgoalitems(self, dirfilter):
605 """get all the items for directories and files viewable at this level"""
606 if dirfilter is None:
607 depth = 0
608 else:
609 depth = dirfilter.count(os.path.sep)
610 if not dirfilter.endswith(os.path.extsep + self.project.fileext):
611 depth += 1
612 allitems = []
613 goalchildren = {}
614 allchildren = []
615 for childname in self.project.browsefiles(dirfilter=dirfilter, depth=depth, includedirs=True, includefiles=False):
616 allchildren.append(childname + os.path.sep)
617 for childname in self.project.browsefiles(dirfilter=dirfilter, depth=depth, includedirs=False, includefiles=True):
618 allchildren.append(childname)
619 initial = dirfilter
620 if initial and not initial.endswith(os.path.extsep + self.project.fileext):
621 initial += os.path.sep
622 if initial:
623 maxdepth = initial.count(os.path.sep)
624 else:
625 maxdepth = 0
626 # using a goal of "" means that the file has no goal
627 nogoal = ""
628 if self.currentgoal is None:
629 goalnames = self.project.getgoalnames() + [nogoal]
630 else:
631 goalnames = [self.currentgoal]
632 goalfiledict = {}
633 for goalname in goalnames:
634 goalfiles = self.project.getgoalfiles(goalname, dirfilter, maxdepth=maxdepth, expanddirs=True, includepartial=True)
635 goalfiles = [goalfile for goalfile in goalfiles if goalfile != initial]
636 goalfiledict[goalname] = goalfiles
637 for goalfile in goalfiles:
638 goalchildren[goalfile] = True
639 goalless = []
640 for item in allchildren:
641 itemgoals = self.project.getfilegoals(item)
642 if not itemgoals:
643 goalless.append(item)
644 goalfiledict[nogoal] = goalless
645 for goalname in goalnames:
646 goalfiles = goalfiledict[goalname]
647 goalusers = self.project.getgoalusers(goalname)
648 goalitem = self.getgoalitem(goalname, dirfilter, goalusers)
649 allitems.append(goalitem)
650 if self.currentgoal == goalname:
651 goalchilditems = self.getitems(goalfiles, linksrequired=["editgoal"], goal=self.currentgoal)
652 allitems.extend(goalchilditems)
653 return allitems
655 def getgoalitem(self, goalname, dirfilter, goalusers):
656 """returns an item showing a goal entry"""
657 pofilenames = self.project.getgoalfiles(goalname, dirfilter, expanddirs=True, includedirs=False)
658 projectstats = self.project.combinestats(pofilenames)
659 goal = {"actions": None, "icon": "goal", "isgoal": True, "goal": {"name": goalname}}
660 if goalname:
661 goal["title"] = goalname
662 else:
663 goal["title"] = self.localize("Not in a goal")
664 goal["href"] = self.makelink("index.html", goal=goalname)
665 if pofilenames:
666 actionlinks = self.getactionlinks("", projectstats, linksrequired=["mine", "review", "translate", "zip"], goal=goalname)
667 goal["actions"] = actionlinks
668 goaluserslist = []
669 if goalusers:
670 goalusers.sort()
671 goaluserslist = [{"name": goaluser, "sep": ", "} for goaluser in goalusers]
672 if goaluserslist:
673 goaluserslist[-1]["sep"] = ""
674 goal["goal"]["users"] = goaluserslist
675 if goalname and self.currentgoal == goalname:
676 if "admin" in self.rights:
677 unassignedusers = self.project.getuserswithinterest(self.session)
678 for user in goalusers:
679 if user in unassignedusers:
680 unassignedusers.pop(user)
681 goal["goal"]["show_adduser"] = True
682 goal["goal"]["otherusers"] = unassignedusers
683 goal["goal"]["adduser_title"] = self.localize("Add User")
684 goal["stats"] = self.getitemstats("", pofilenames, len(pofilenames))
685 projectstats = self.project.getquickstats(pofilenames)
686 goal["data"] = self.getstats(self.project, projectstats)
687 return goal
689 def getdiritem(self, direntry, linksrequired=None, **newargs):
690 """returns an item showing a directory entry"""
691 pofilenames = self.project.browsefiles(direntry)
692 if self.showgoals and "goal" in self.argdict:
693 goalfilenames = self.project.getgoalfiles(self.currentgoal, dirfilter=direntry, includedirs=False, expanddirs=True)
694 projectstats = self.project.combinestats(goalfilenames)
695 else:
696 projectstats = self.project.combine_totals(pofilenames)
697 basename = os.path.basename(direntry)
698 browseurl = self.getbrowseurl("%s/" % basename, **newargs)
699 diritem = {"href": browseurl, "title": basename, "icon": "folder", "isdir": True}
700 basename += "/"
701 actionlinks = self.getactionlinks(basename, projectstats, linksrequired=linksrequired)
702 diritem["actions"] = actionlinks
703 if self.showgoals and "goal" in self.argdict:
704 diritem["stats"] = self.getitemstats(basename, goalfilenames, (len(goalfilenames), len(pofilenames)))
705 projectstats = self.project.getquickstats(goalfilenames)
706 diritem["data"] = self.getstats(self.projects, projectstats)
707 else:
708 diritem["stats"] = self.getitemstats(basename, pofilenames, len(pofilenames))
709 projectstats = self.project.getquickstats(pofilenames)
710 diritem["data"] = self.getstats(self.project, projectstats)
711 return diritem
713 def getfileitem(self, fileentry, linksrequired=None, **newargs):
714 """returns an item showing a file entry"""
715 if linksrequired is None:
716 if fileentry.endswith('.po'):
717 linksrequired = ["mine", "review", "quick", "all", "po", "xliff", "ts", "csv", "mo", "update", "commit"]
718 else:
719 linksrequired = ["mine", "review", "quick", "all", "po", "xliff", "update", "commit"]
720 basename = os.path.basename(fileentry)
721 projectstats = self.project.combine_totals([fileentry])
722 browseurl = self.getbrowseurl(basename, **newargs)
723 fileitem = {"href": browseurl, "title": basename, "icon": "file", "isfile": True}
724 actions = self.getactionlinks(basename, projectstats, linksrequired=linksrequired)
725 actionlinks = actions["extended"]
726 if "po" in linksrequired:
727 poname = basename.replace(".xlf", ".po")
728 polink = {"href": poname, "text": self.localize('PO file')}
729 actionlinks.append(polink)
730 if "xliff" in linksrequired and "translate" in self.rights:
731 xliffname = basename.replace(".po", ".xlf")
732 xlifflink = {"href": xliffname, "text": self.localize('XLIFF file')}
733 actionlinks.append(xlifflink)
734 if "ts" in linksrequired and "translate" in self.rights:
735 tsname = basename.replace(".po", ".ts")
736 tslink = {"href": tsname, "text": self.localize('Qt .ts file')}
737 actionlinks.append(tslink)
738 if "csv" in linksrequired and "translate" in self.rights:
739 csvname = basename.replace(".po", ".csv")
740 csvlink = {"href": csvname, "text": self.localize('CSV file')}
741 actionlinks.append(csvlink)
742 if "mo" in linksrequired:
743 if self.project.hascreatemofiles(self.project.projectcode) and "pocompile" in self.rights:
744 moname = basename.replace(".po", ".mo")
745 molink = {"href": moname, "text": self.localize('MO file')}
746 actionlinks.append(molink)
747 if "update" in linksrequired and "admin" in self.rights:
748 if versioncontrol.hasversioning(os.path.join(self.project.podir,
749 self.dirname, basename)):
750 # l10n: Update from version control (like CVS or Subversion)
751 updatelink = {"href": "index.html?editing=1&doupdate=1&updatefile=%s" % (basename), "text": self.localize('Update')}
752 actionlinks.append(updatelink)
753 if "commit" in linksrequired and "commit" in self.rights:
754 if versioncontrol.hasversioning(os.path.join(self.project.podir,
755 self.dirname, basename)):
756 # l10n: Commit to version control (like CVS or Subversion)
757 commitlink = {"href": "index.html?editing=1&docommit=1&commitfile=%s" % (basename), "text": self.localize('Commit')}
758 actionlinks.append(commitlink)
759 # update the separators
760 for n, actionlink in enumerate(actionlinks):
761 if n < len(actionlinks)-1:
762 actionlink["sep"] = " | "
763 else:
764 actionlink["sep"] = ""
765 fileitem["actions"] = actions
766 fileitem["stats"] = self.getitemstats(basename, [fileentry], None)
767 projectstats = self.project.getquickstats([fileentry])
768 fileitem["data"] = self.getstats(self.project, projectstats)
769 return fileitem
771 def getgoalform(self, basename, goalfile, filegoals):
772 """Returns a form for adjusting goals"""
773 goalformname = "goal_%s" % (basename.replace("/", "_").replace(".", "_"))
774 goalnames = self.project.getgoalnames()
775 useroptions = []
776 for goalname in filegoals:
777 useroptions += self.project.getgoalusers(goalname)
778 multifiles = None
779 if len(filegoals) > 1:
780 multifiles = "multiple"
781 multiusers = None
782 assignusers = []
783 assignwhich = []
784 if len(useroptions) > 1:
785 assignfilenames = self.project.browsefiles(dirfilter=goalfile)
786 if self.currentgoal:
787 action = "goal-" + self.currentgoal
788 else:
789 action = None
790 assignstats = self.project.combineassignstats(assignfilenames, action)
791 assignusers = list(assignstats.iterkeys())
792 useroptions += [username for username in assignusers if username not in useroptions]
793 if len(assignusers) > 1:
794 multiusers = "multiple"
795 assignwhich = [('all', self.localize("All Strings")),
796 ('untranslated', self.localize("Untranslated")),
797 ('unassigned', self.localize('Unassigned')),
798 ('unassigneduntranslated', self.localize("Unassigned and Untranslated"))]
799 return {
800 "name": goalformname,
801 "filename": basename,
802 "goalnames": goalnames,
803 "filegoals": dict([(goalname, goalname in filegoals or None) for goalname in goalnames]),
804 "multifiles": multifiles,
805 "setgoal_text": self.localize("Set Goal"),
806 "users": useroptions,
807 "assignusers": dict([(username, username in assignusers or None) for username in useroptions]),
808 "multiusers": multiusers,
809 "selectmultiple_text": self.localize("Select Multiple"),
810 "assignwhich": [{"value": value, "text": text} for value, text in assignwhich],
811 "assignto_text": self.localize("Assign To"),
814 def getactionlinks(self, basename, projectstats, linksrequired=None, filepath=None, goal=None):
815 """get links to the actions that can be taken on an item (directory / file)"""
816 if linksrequired is None:
817 linksrequired = ["mine", "review", "quick", "all"]
818 actionlinks = []
819 actions = {}
820 actions["goalform"] = None
821 if not basename or basename.endswith("/"):
822 baseactionlink = basename + "translate.html?"
823 baseindexlink = basename + "index.html?"
824 else:
825 baseactionlink = "%s?translate=1" % basename
826 baseindexlink = "%s?index=1" % basename
827 if goal:
828 baseactionlink += "&goal=%s" % goal
829 baseindexlink += "&goal=%s" % goal
830 def addoptionlink(linkname, rightrequired, attrname, showtext, hidetext):
831 if linkname in linksrequired:
832 if rightrequired and not rightrequired in self.rights:
833 return
834 if getattr(self, attrname, False):
835 link = {"href": self.makelink(baseindexlink, **{attrname:0}), "text": hidetext}
836 else:
837 link = {"href": self.makelink(baseindexlink, **{attrname:1}), "text": showtext}
838 link["sep"] = " | "
839 actionlinks.append(link)
840 addoptionlink("editing", None, "editing", self.localize("Show Editing Functions"),
841 self.localize("Show Statistics"))
842 addoptionlink("track", None, "showtracks", self.localize("Show Tracks"), self.localize("Hide Tracks"))
843 # l10n: "Checks" are quality checks that Pootle performs on translations to test for common mistakes
844 addoptionlink("check", "translate", "showchecks", self.localize("Show Checks"), self.localize("Hide Checks"))
845 addoptionlink("goal", None, "showgoals", self.localize("Show Goals"), self.localize("Hide Goals"))
846 addoptionlink("assign", "translate", "showassigns", self.localize("Show Assigns"), self.localize("Hide Assigns"))
847 actions["basic"] = actionlinks
848 actionlinks = []
849 if not goal:
850 goalfile = os.path.join(self.dirname, basename)
851 filegoals = self.project.getfilegoals(goalfile)
852 if self.showgoals:
853 if len(filegoals) > 1:
854 #TODO: This is not making sense. For now make it an unclickable link
855 allgoalslink = {"href": "", "text": self.localize("All Goals: %s", (", ".join(filegoals)))}
856 actionlinks.append(allgoalslink)
857 if "editgoal" in linksrequired and "admin" in self.rights:
858 actions["goalform"] = self.getgoalform(basename, goalfile, filegoals)
859 if "mine" in linksrequired and self.session.isopen:
860 if "translate" in self.rights:
861 minelink = self.localize("Translate My Strings")
862 else:
863 minelink = self.localize("View My Strings")
864 mystats = projectstats.get('assign', {}).get(self.session.username, [])
865 if len(mystats):
866 minelink = {"href": self.makelink(baseactionlink, assignedto=self.session.username), "text": minelink}
867 else:
868 minelink = {"title": self.localize("No strings assigned to you"), "text": minelink}
869 actionlinks.append(minelink)
870 if "quick" in linksrequired and "translate" in self.rights:
871 mytranslatedstats = [statsitem for statsitem in mystats if statsitem in projectstats["units"].get("translated", [])]
872 quickminelink = self.localize("Quick Translate My Strings")
873 if len(mytranslatedstats) < len(mystats):
874 quickminelink = {"href": self.makelink(baseactionlink, assignedto=self.session.username, fuzzy=1, untranslated=1), "text": quickminelink}
875 else:
876 quickminelink = {"title": self.localize("No untranslated strings assigned to you"), "text": quickminelink}
877 actionlinks.append(quickminelink)
878 if "review" in linksrequired and projectstats.get("units", {}).get("check-hassuggestion", []):
879 if "review" in self.rights:
880 reviewlink = self.localize("Review Suggestions")
881 else:
882 reviewlink = self.localize("View Suggestions")
883 reviewlink = {"href": self.makelink(baseactionlink, review=1, **{"hassuggestion": 1}), "text": reviewlink}
884 actionlinks.append(reviewlink)
885 if "quick" in linksrequired:
886 if "translate" in self.rights:
887 quicklink = self.localize("Quick Translate")
888 else:
889 quicklink = self.localize("View Untranslated")
890 if projectstats.get("translated", 0) < projectstats.get("total", 0):
891 quicklink = {"href": self.makelink(baseactionlink, fuzzy=1, untranslated=1), "text": quicklink}
892 else:
893 quicklink = {"title": self.localize("No untranslated items"), "text": quicklink}
894 actionlinks.append(quicklink)
895 if "all" in linksrequired and "translate" in self.rights:
896 translatelink = {"href": self.makelink(baseactionlink), "text": self.localize('Translate All')}
897 actionlinks.append(translatelink)
898 if "zip" in linksrequired and "archive" in self.rights:
899 if filepath and filepath.endswith(".po"):
900 currentfolder = os.path.dirname(filepath)
901 else:
902 currentfolder = filepath
903 archivename = "%s-%s" % (self.project.projectcode, self.project.languagecode)
904 if currentfolder:
905 archivename += "-%s" % currentfolder.replace(os.path.sep, "-")
906 if goal:
907 archivename += "-%s" % goal
908 archivename += ".zip"
909 if goal:
910 archivename += "?goal=%s" % goal
911 linktext = self.localize('ZIP of goal')
912 else:
913 linktext = self.localize('ZIP of folder')
914 ziplink = {"href": archivename, "text": linktext, "title": archivename}
915 actionlinks.append(ziplink)
917 if "sdf" in linksrequired and "pocompile" in self.rights and \
918 self.project.ootemplate() and not (basename or filepath):
919 archivename = self.project.languagecode + ".sdf"
920 linktext = self.localize('Generate SDF')
921 oolink = {"href": archivename, "text": linktext, "title": archivename}
922 actionlinks.append(oolink)
923 for n, actionlink in enumerate(actionlinks):
924 if n < len(actionlinks)-1:
925 actionlink["sep"] = " | "
926 else:
927 actionlink["sep"] = ""
928 actions["extended"] = actionlinks
929 if not actions["extended"] and not actions["goalform"] and actions["basic"]:
930 actions["basic"][-1]["sep"] = ""
931 return actions
933 def getitemstats(self, basename, pofilenames, numfiles):
934 """returns a widget summarizing item statistics"""
935 stats = {"checks": [], "tracks": [], "assigns": []}
936 if not basename or basename.endswith("/"):
937 linkbase = basename + "translate.html?"
938 else:
939 linkbase = basename + "?translate=1"
940 if pofilenames:
941 if self.showchecks:
942 stats["checks"] = self.getcheckdetails(pofilenames, linkbase)
943 if self.showtracks:
944 trackfilter = (self.dirfilter or "") + basename
945 trackpofilenames = self.project.browsefiles(trackfilter)
946 projecttracks = self.project.gettracks(trackpofilenames)
947 stats["tracks"] = self.gettrackdetails(projecttracks, linkbase)
948 if self.showassigns:
949 if not basename or basename.endswith("/"):
950 removelinkbase = "?showassigns=1&removeassigns=1"
951 else:
952 removelinkbase = "?showassigns=1&removeassigns=1&removefilter=%s" % basename
953 stats["assigns"] = self.getassigndetails(pofilenames, linkbase, removelinkbase)
954 return stats
956 def gettrackdetails(self, projecttracks, linkbase):
957 """return a list of strings describing the results of tracks"""
958 return [trackmessage for trackmessage in projecttracks]
960 def getcheckdetails(self, pofilenames, linkbase):
961 """return a list of strings describing the results of checks"""
962 projectstats = self.project.combine_unit_stats(pofilenames)
963 total = max(len(projectstats.get("total", [])), 1)
964 checklinks = []
965 keys = projectstats.keys()
966 keys.sort()
967 for checkname in keys:
968 if not checkname.startswith("check-"):
969 continue
970 checkcount = len(projectstats[checkname])
971 checkname = checkname.replace("check-", "", 1)
972 if total and checkcount:
973 stats = self.nlocalize("%d string (%d%%) failed", "%d strings (%d%%) failed", checkcount, checkcount, (checkcount * 100 / total))
974 checklink = {"href": self.makelink(linkbase, **{str(checkname):1}), "text": checkname, "stats": stats}
975 checklinks += [checklink]
976 return checklinks
978 def getassigndetails(self, pofilenames, linkbase, removelinkbase):
979 """return a list of strings describing the assigned strings"""
980 # TODO: allow setting of action, so goals can only show the appropriate action assigns
981 # quick lookup of what has been translated
982 projectstats = self.project.combinestats(pofilenames)
983 totalcount = projectstats.get("total", 0)
984 totalwords = projectstats.get("totalsourcewords", 0)
985 translated = projectstats['units'].get("translated", [])
986 assignlinks = []
987 keys = projectstats['assign'].keys()
988 keys.sort()
989 for assignname in keys:
990 assigned = projectstats['assign'][assignname]
991 assigncount = len(assigned)
992 assignwords = self.project.countwords(assigned)
993 complete = [statsitem for statsitem in assigned if statsitem in translated]
994 completecount = len(complete)
995 completewords = self.project.countwords(complete)
996 if totalcount and assigncount:
997 assignlink = {"href": self.makelink(linkbase, assignedto=assignname), "text": assignname}
998 percentassigned = assignwords * 100 / max(totalwords, 1)
999 percentcomplete = completewords * 100 / max(assignwords, 1)
1000 stats = self.localize("%d/%d words (%d%%) assigned", assignwords, totalwords, percentassigned)
1001 stringstats = self.localize("[%d/%d strings]", assigncount, totalcount)
1002 completestats = self.localize("%d/%d words (%d%%) translated", completewords, assignwords, percentcomplete)
1003 completestringstats = self.localize("[%d/%d strings]", completecount, assigncount)
1004 if "assign" in self.rights:
1005 removetext = self.localize("Remove")
1006 removelink = {"href": self.makelink(removelinkbase, assignedto=assignname), "text": removetext}
1007 else:
1008 removelink = None
1009 assignlinks.append({"assign": assignlink, "stats": stats, "stringstats": stringstats, "completestats": completestats, "completestringstats": completestringstats, "remove": removelink})
1010 return assignlinks