julen / pootle
- Source
- Commits
- Network (0)
- Issues (0)
- Downloads (0)
- Wiki (1)
- Graphs
-
Tree:
4292355
pootle / pootlefile.py
| a079afa1 » | davidfraser | 2005-04-20 | 1 | #!/usr/bin/env python | |
| b353992c » | davidfraser | 2005-07-01 | 2 | # -*- coding: utf-8 -*- | |
| 5833449c » | dwaynebailey | 2006-06-15 | 3 | # | |
| 4 | # Copyright 2004-2006 Zuza Software Foundation | ||||
| 5 | # | ||||
| 6 | # This file is part of translate. | ||||
| 7 | # | ||||
| 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. | ||||
| 12 | # | ||||
| 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. | ||||
| 17 | # | ||||
| 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 | ||||
| a079afa1 » | davidfraser | 2005-04-20 | 21 | ||
| d07801d0 » | friedelwolff | 2006-10-12 | 22 | """manages a translation file and its associated files""" | |
| a079afa1 » | davidfraser | 2005-04-20 | 23 | ||
| 0d42d7f4 » | friedelwolff | 2006-10-03 | 24 | from translate.storage import base | |
| a079afa1 » | davidfraser | 2005-04-20 | 25 | from translate.storage import po | |
| bb29a717 » | friedelwolff | 2006-12-27 | 26 | from translate.storage import xliff | |
| d07801d0 » | friedelwolff | 2006-10-12 | 27 | from translate.storage import factory | |
| 574eec06 » | friedelwolff | 2006-03-03 | 28 | from translate.misc.multistring import multistring | |
| 944295bd » | davidfraser | 2005-06-28 | 29 | from Pootle import __version__ | |
| e71ee4eb » | dwaynebailey | 2006-11-02 | 30 | from Pootle import statistics | |
| a079afa1 » | davidfraser | 2005-04-20 | 31 | from jToolkit import timecache | |
| 76fa87f0 » | friedelwolff | 2006-10-10 | 32 | from jToolkit import glock | |
| a079afa1 » | davidfraser | 2005-04-20 | 33 | import time | |
| 34 | import os | ||||
| 35 | |||||
| 0d42d7f4 » | friedelwolff | 2006-10-03 | 36 | class Wrapper(object): | |
| 37 | """An object which wraps an inner object, delegating to the encapsulated methods, etc""" | ||||
| 38 | def __getattr__(self, attrname, *args): | ||||
| 39 | if attrname in self.__dict__: | ||||
| 40 | return self.__dict__[attrname] | ||||
| 41 | return getattr(self.__dict__["__innerobj__"], attrname, *args) | ||||
| 42 | |||||
| 43 | def __setattr__(self, attrname, value): | ||||
| 44 | if attrname == "__innerobj__": | ||||
| 45 | self.__dict__[attrname] = value | ||||
| 46 | elif attrname in self.__dict__: | ||||
| 47 | if isinstance(self.__dict__[attrname], property): | ||||
| 48 | self.__dict__[attrname].fset(value) | ||||
| 49 | else: | ||||
| 50 | self.__dict__[attrname] = value | ||||
| 51 | elif attrname in self.__class__.__dict__: | ||||
| 52 | if isinstance(self.__class__.__dict__[attrname], property): | ||||
| 53 | self.__class__.__dict__[attrname].fset(self, value) | ||||
| 54 | else: | ||||
| 55 | self.__dict__[attrname] = value | ||||
| 56 | else: | ||||
| 57 | return setattr(self.__dict__["__innerobj__"], attrname, value) | ||||
| 58 | |||||
| 76fa87f0 » | friedelwolff | 2006-10-10 | 59 | class LockedFile: | |
| 60 | """locked interaction with a filesystem file""" | ||||
| 2877ffa0 » | friedelwolff | 2007-03-16 | 61 | #Locking is disabled for now since it impacts performance negatively and was | |
| 62 | #not complete yet anyway. Reverse svn revision 5271 to regain the locking | ||||
| 63 | #code here. | ||||
| 76fa87f0 » | friedelwolff | 2006-10-10 | 64 | def __init__(self, filename): | |
| 65 | self.filename = filename | ||||
| a1aeb64b » | friedelwolff | 2007-03-16 | 66 | self.lock = None | |
| 67 | |||||
| 68 | def initlock(self): | ||||
| 76fa87f0 » | friedelwolff | 2006-10-10 | 69 | self.lock = glock.GlobalLock(self.filename + os.extsep + "lock") | |
| 70 | |||||
| a1aeb64b » | friedelwolff | 2007-03-16 | 71 | def dellock(self): | |
| 72 | del self.lock | ||||
| 73 | self.lock = None | ||||
| 74 | |||||
| 76fa87f0 » | friedelwolff | 2006-10-10 | 75 | def readmodtime(self): | |
| 76 | """returns the modification time of the file (locked operation)""" | ||||
| 2877ffa0 » | friedelwolff | 2007-03-16 | 77 | return statistics.getmodtime(self.filename) | |
| 76fa87f0 » | friedelwolff | 2006-10-10 | 78 | ||
| 79 | def getcontents(self): | ||||
| 80 | """returns modtime, contents tuple (locked operation)""" | ||||
| 2877ffa0 » | friedelwolff | 2007-03-16 | 81 | pomtime = statistics.getmodtime(self.filename) | |
| 82 | fp = open(self.filename, 'r') | ||||
| 83 | filecontents = fp.read() | ||||
| 84 | fp.close() | ||||
| 85 | return pomtime, filecontents | ||||
| 76fa87f0 » | friedelwolff | 2006-10-10 | 86 | ||
| 87 | def writecontents(self, contents): | ||||
| 88 | """writes contents to file, returning modification time (locked operation)""" | ||||
| 2877ffa0 » | friedelwolff | 2007-03-16 | 89 | f = open(self.filename, 'w') | |
| 90 | f.write(contents) | ||||
| 91 | f.close() | ||||
| 92 | pomtime = statistics.getmodtime(self.filename) | ||||
| 93 | return pomtime | ||||
| 76fa87f0 » | friedelwolff | 2006-10-10 | 94 | ||
| 95 | class pootleassigns: | ||||
| 96 | """this represents the assignments for a file""" | ||||
| 97 | def __init__(self, basefile): | ||||
| 98 | """constructs assignments object for the given file""" | ||||
| 99 | # TODO: try and remove circular references between basefile and this class | ||||
| 100 | self.basefile = basefile | ||||
| 101 | self.assignsfilename = self.basefile.filename + os.extsep + "assigns" | ||||
| 102 | self.getassigns() | ||||
| 7e84177f » | friedelwolff | 2006-10-06 | 103 | ||
| a079afa1 » | davidfraser | 2005-04-20 | 104 | def getassigns(self): | |
| 105 | """reads the assigns if neccessary or returns them from the cache""" | ||||
| 106 | if os.path.exists(self.assignsfilename): | ||||
| 107 | self.assigns = self.readassigns() | ||||
| 108 | else: | ||||
| 109 | self.assigns = {} | ||||
| 110 | return self.assigns | ||||
| 111 | |||||
| 112 | def readassigns(self): | ||||
| 113 | """reads the assigns from the associated assigns file, returning the assigns | ||||
| 114 | the format is a number of lines consisting of | ||||
| 115 | username: action: itemranges | ||||
| 116 | where itemranges is a comma-separated list of item numbers or itemranges like 3-5 | ||||
| 117 | e.g. pootlewizz: review: 2-99,101""" | ||||
| e71ee4eb » | dwaynebailey | 2006-11-02 | 118 | assignsmtime = statistics.getmodtime(self.assignsfilename) | |
| a079afa1 » | davidfraser | 2005-04-20 | 119 | if assignsmtime == getattr(self, "assignsmtime", None): | |
| 120 | return | ||||
| 609fc3b0 » | andreaspauley | 2007-02-05 | 121 | assignsfile = open(self.assignsfilename, "r") | |
| 122 | assignsstring = assignsfile.read() | ||||
| 123 | assignsfile.close() | ||||
| a079afa1 » | davidfraser | 2005-04-20 | 124 | poassigns = {} | |
| 53becf1a » | davidfraser | 2005-05-20 | 125 | itemcount = len(getattr(self, "classify", {}).get("total", [])) | |
| a079afa1 » | davidfraser | 2005-04-20 | 126 | for line in assignsstring.split("\n"): | |
| 127 | if not line.strip(): | ||||
| 128 | continue | ||||
| 129 | if not line.count(":") == 2: | ||||
| 2713b3cf » | davidfraser | 2005-05-20 | 130 | print "invalid assigns line in %s: %r" % (self.assignsfilename, line) | |
| a079afa1 » | davidfraser | 2005-04-20 | 131 | continue | |
| 132 | username, action, itemranges = line.split(":", 2) | ||||
| 78e88287 » | friedelwolff | 2006-08-31 | 133 | username, action = username.strip().decode('utf-8'), action.strip().decode('utf-8') | |
| a079afa1 » | davidfraser | 2005-04-20 | 134 | if not username in poassigns: | |
| 135 | poassigns[username] = {} | ||||
| 136 | userassigns = poassigns[username] | ||||
| 137 | if not action in userassigns: | ||||
| 138 | userassigns[action] = [] | ||||
| 139 | items = userassigns[action] | ||||
| 140 | for itemrange in itemranges.split(","): | ||||
| 141 | if "-" in itemrange: | ||||
| 2713b3cf » | davidfraser | 2005-05-20 | 142 | if not itemrange.count("-") == 1: | |
| 143 | print "invalid assigns range in %s: %r (from line %r)" % (self.assignsfilename, itemrange, line) | ||||
| a079afa1 » | davidfraser | 2005-04-20 | 144 | continue | |
| 145 | itemstart, itemstop = [int(item.strip()) for item in itemrange.split("-", 1)] | ||||
| 146 | items.extend(range(itemstart, itemstop+1)) | ||||
| 147 | else: | ||||
| 148 | item = int(itemrange.strip()) | ||||
| 149 | items.append(item) | ||||
| 53becf1a » | davidfraser | 2005-05-20 | 150 | if itemcount: | |
| 151 | items = [item for item in items if 0 <= item < itemcount] | ||||
| a079afa1 » | davidfraser | 2005-04-20 | 152 | userassigns[action] = items | |
| 153 | return poassigns | ||||
| 154 | |||||
| 155 | def assignto(self, item, username, action): | ||||
| 156 | """assigns the item to the given username for the given action""" | ||||
| 157 | userassigns = self.assigns.setdefault(username, {}) | ||||
| 158 | items = userassigns.setdefault(action, []) | ||||
| 159 | if item not in items: | ||||
| 160 | items.append(item) | ||||
| 161 | self.saveassigns() | ||||
| 162 | |||||
| 65dbd9a1 » | davidfraser | 2005-05-20 | 163 | def unassign(self, item, username=None, action=None): | |
| 164 | """removes assignments of the item to the given username (or all users) for the given action (or all actions)""" | ||||
| 165 | if username is None: | ||||
| 166 | usernames = self.assigns.keys() | ||||
| a079afa1 » | davidfraser | 2005-04-20 | 167 | else: | |
| 65dbd9a1 » | davidfraser | 2005-05-20 | 168 | usernames = [username] | |
| 169 | for username in usernames: | ||||
| 170 | userassigns = self.assigns.setdefault(username, {}) | ||||
| 171 | if action is None: | ||||
| 172 | itemlist = [userassigns.get(action, []) for action in userassigns] | ||||
| 173 | else: | ||||
| 174 | itemlist = [userassigns.get(action, [])] | ||||
| 175 | for items in itemlist: | ||||
| 176 | if item in items: | ||||
| 177 | items.remove(item) | ||||
| a079afa1 » | davidfraser | 2005-04-20 | 178 | self.saveassigns() | |
| 179 | |||||
| 180 | def saveassigns(self): | ||||
| 181 | """saves the current assigns to file""" | ||||
| 182 | # assumes self.assigns is up to date | ||||
| 183 | assignstrings = [] | ||||
| 184 | usernames = self.assigns.keys() | ||||
| 185 | usernames.sort() | ||||
| 186 | for username in usernames: | ||||
| 187 | actions = self.assigns[username].keys() | ||||
| 188 | actions.sort() | ||||
| 189 | for action in actions: | ||||
| 190 | items = self.assigns[username][action] | ||||
| 191 | items.sort() | ||||
| 192 | if items: | ||||
| 193 | lastitem = None | ||||
| 194 | rangestart = None | ||||
| 78e88287 » | friedelwolff | 2006-08-31 | 195 | assignstring = "%s: %s: " % (username.encode('utf-8'), action.encode('utf-8')) | |
| a079afa1 » | davidfraser | 2005-04-20 | 196 | for item in items: | |
| 197 | if item - 1 == lastitem: | ||||
| 198 | if rangestart is None: | ||||
| 199 | rangestart = lastitem | ||||
| 200 | else: | ||||
| 201 | if rangestart is not None: | ||||
| 202 | assignstring += "-%d" % lastitem | ||||
| 203 | rangestart = None | ||||
| 204 | if lastitem is None: | ||||
| 205 | assignstring += "%d" % item | ||||
| 206 | else: | ||||
| 207 | assignstring += ",%d" % item | ||||
| 208 | lastitem = item | ||||
| 209 | if rangestart is not None: | ||||
| 210 | assignstring += "-%d" % lastitem | ||||
| 211 | assignstrings.append(assignstring + "\n") | ||||
| 212 | assignsfile = open(self.assignsfilename, "w") | ||||
| 213 | assignsfile.writelines(assignstrings) | ||||
| 214 | assignsfile.close() | ||||
| 215 | |||||
| 7e84177f » | friedelwolff | 2006-10-06 | 216 | def getunassigned(self, action=None): | |
| 217 | """gets all strings that are unassigned (for the given action if given)""" | ||||
| 76fa87f0 » | friedelwolff | 2006-10-10 | 218 | unassigneditems = range(0, self.basefile.statistics.getitemslen()) | |
| 78cf21ad » | andreaspauley | 2007-01-16 | 219 | self.assigns = self.getassigns() | |
| 7e84177f » | friedelwolff | 2006-10-06 | 220 | for username in self.assigns: | |
| 221 | if action is not None: | ||||
| 222 | assigneditems = self.assigns[username].get(action, []) | ||||
| 223 | else: | ||||
| 224 | assigneditems = [] | ||||
| 225 | for action, actionitems in self.assigns[username].iteritems(): | ||||
| 226 | assigneditems += actionitems | ||||
| 227 | unassigneditems = [item for item in unassigneditems if item not in assigneditems] | ||||
| 228 | return unassigneditems | ||||
| 229 | |||||
| 76fa87f0 » | friedelwolff | 2006-10-10 | 230 | def finditems(self, search): | |
| 231 | """returns items that match the .assignedto and/or .assignedaction criteria in the searchobject""" | ||||
| 232 | # search.assignedto == [None] means assigned to nobody | ||||
| 233 | if search.assignedto == [None]: | ||||
| 234 | assignitems = self.getunassigned(search.assignedaction) | ||||
| 235 | else: | ||||
| 236 | # filter based on assign criteria | ||||
| 237 | assigns = self.getassigns() | ||||
| 238 | if search.assignedto: | ||||
| 239 | usernames = [search.assignedto] | ||||
| 240 | else: | ||||
| 241 | usernames = assigns.iterkeys() | ||||
| 242 | assignitems = [] | ||||
| 243 | for username in usernames: | ||||
| 244 | if search.assignedaction: | ||||
| 245 | actionitems = assigns[username].get(search.assignedaction, []) | ||||
| 246 | assignitems.extend(actionitems) | ||||
| 247 | else: | ||||
| 248 | for actionitems in assigns[username].itervalues(): | ||||
| 249 | assignitems.extend(actionitems) | ||||
| 250 | return assignitems | ||||
| 251 | |||||
| f008f9b5 » | friedelwolff | 2006-11-07 | 252 | class pootlefile(Wrapper): | |
| d07801d0 » | friedelwolff | 2006-10-12 | 253 | """this represents a pootle-managed file and its associated files""" | |
| 0800d3d2 » | friedelwolff | 2006-11-06 | 254 | innerclass = po.pofile | |
| 76fa87f0 » | friedelwolff | 2006-10-10 | 255 | x_generator = "Pootle %s" % __version__.ver | |
| 256 | def __init__(self, project=None, pofilename=None, generatestats=True): | ||||
| d07801d0 » | friedelwolff | 2006-10-12 | 257 | if pofilename: | |
| 02d5fde3 » | friedelwolff | 2006-11-16 | 258 | innerclass = factory.getclass(pofilename) | |
| 259 | innerobj = innerclass() | ||||
| f008f9b5 » | friedelwolff | 2006-11-07 | 260 | self.__innerobj__ = innerobj | |
| 261 | self.UnitClass = innerobj.UnitClass | ||||
| d07801d0 » | friedelwolff | 2006-10-12 | 262 | ||
| 76fa87f0 » | friedelwolff | 2006-10-10 | 263 | self.pofilename = pofilename | |
| 264 | if project is None: | ||||
| 265 | from Pootle import projects | ||||
| 266 | self.project = projects.DummyProject(None) | ||||
| 267 | self.checker = None | ||||
| 268 | self.filename = self.pofilename | ||||
| 269 | else: | ||||
| 270 | self.project = project | ||||
| 271 | self.checker = self.project.checker | ||||
| 272 | self.filename = os.path.join(self.project.podir, self.pofilename) | ||||
| 273 | |||||
| 274 | self.lockedfile = LockedFile(self.filename) | ||||
| 275 | # we delay parsing until it is required | ||||
| 276 | self.pomtime = None | ||||
| 277 | self.assigns = pootleassigns(self) | ||||
| 278 | |||||
| 279 | self.pendingfilename = self.filename + os.extsep + "pending" | ||||
| 280 | self.pendingfile = None | ||||
| e71ee4eb » | dwaynebailey | 2006-11-02 | 281 | self.statistics = statistics.pootlestatistics(self, generatestats) | |
| 76fa87f0 » | friedelwolff | 2006-10-10 | 282 | self.tmfilename = self.filename + os.extsep + "tm" | |
| 283 | # we delay parsing until it is required | ||||
| 284 | self.pomtime = None | ||||
| 285 | self.tracker = timecache.timecache(20*60) | ||||
| 286 | |||||
| 287 | def __str__(self): | ||||
| 288 | return self.__innerobj__.__str__() | ||||
| 289 | |||||
| 290 | def parsestring(cls, storestring): | ||||
| 291 | newstore = cls() | ||||
| 292 | newstore.parse(storestring) | ||||
| 293 | return newstore | ||||
| 294 | parsestring = classmethod(parsestring) | ||||
| 295 | |||||
| f008f9b5 » | friedelwolff | 2006-11-07 | 296 | def parsefile(cls, storefile): | |
| 297 | """Reads the given file (or opens the given filename) and parses back to an object""" | ||||
| 298 | if isinstance(storefile, basestring): | ||||
| 299 | storefile = open(storefile, "r") | ||||
| 300 | if "r" in getattr(storefile, "mode", "r"): | ||||
| 301 | storestring = storefile.read() | ||||
| 302 | else: | ||||
| 303 | storestring = "" | ||||
| 304 | return cls.parsestring(storestring) | ||||
| 305 | parsefile = classmethod(parsefile) | ||||
| 76fa87f0 » | friedelwolff | 2006-10-10 | 306 | ||
| 5a398d38 » | friedelwolff | 2006-11-13 | 307 | def getheaderplural(self): | |
| 308 | """returns values for nplural and plural values. It tries to see if the | ||||
| 309 | file has it specified (in a po header or similar).""" | ||||
| 310 | method = getattr(self.__innerobj__, "getheaderplural", None) | ||||
| 311 | if method and callable(method): | ||||
| 312 | return self.__innerobj__.getheaderplural() | ||||
| 313 | else: | ||||
| 314 | return None, None | ||||
| 315 | |||||
| c7571240 » | friedelwolff | 2006-11-14 | 316 | def updateheaderplural(self, *args, **kwargs): | |
| 317 | """updates the file header. If there is an updateheader function in the | ||||
| 318 | underlying store it will be delegated there.""" | ||||
| 319 | method = getattr(self.__innerobj__, "updateheaderplural", None) | ||||
| 320 | if method and callable(method): | ||||
| 321 | self.__innerobj__.updateheaderplural(*args, **kwargs) | ||||
| 322 | |||||
| 323 | def updateheader(self, **kwargs): | ||||
| 324 | """updates the file header. If there is an updateheader function in the | ||||
| 325 | underlying store it will be delegated there.""" | ||||
| 326 | method = getattr(self.__innerobj__, "updateheader", None) | ||||
| 327 | if method and callable(method): | ||||
| 328 | self.__innerobj__.updateheader(**kwargs) | ||||
| 329 | |||||
| c87a05b0 » | friedelwolff | 2006-10-06 | 330 | def readpendingfile(self): | |
| d07801d0 » | friedelwolff | 2006-10-12 | 331 | """reads and parses the pending file corresponding to this file""" | |
| c87a05b0 » | friedelwolff | 2006-10-06 | 332 | if os.path.exists(self.pendingfilename): | |
| e71ee4eb » | dwaynebailey | 2006-11-02 | 333 | pendingmtime = statistics.getmodtime(self.pendingfilename) | |
| c87a05b0 » | friedelwolff | 2006-10-06 | 334 | if pendingmtime == getattr(self, "pendingmtime", None): | |
| 335 | return | ||||
| 336 | inputfile = open(self.pendingfilename, "r") | ||||
| d07801d0 » | friedelwolff | 2006-10-12 | 337 | self.pendingmtime, self.pendingfile = pendingmtime, factory.getobject(inputfile, ignore=".pending") | |
| c87a05b0 » | friedelwolff | 2006-10-06 | 338 | if self.pomtime: | |
| 339 | self.reclassifysuggestions() | ||||
| 340 | else: | ||||
| 0800d3d2 » | friedelwolff | 2006-11-06 | 341 | self.pendingfile = po.pofile() | |
| c87a05b0 » | friedelwolff | 2006-10-06 | 342 | self.savependingfile() | |
| 7e84177f » | friedelwolff | 2006-10-06 | 343 | ||
| c87a05b0 » | friedelwolff | 2006-10-06 | 344 | def savependingfile(self): | |
| 345 | """saves changes to disk...""" | ||||
| 346 | output = str(self.pendingfile) | ||||
| 0c632acf » | friedelwolff | 2007-05-07 | 347 | outputfile = open(self.pendingfilename, "w") | |
| 348 | outputfile.write(output) | ||||
| 349 | outputfile.close() | ||||
| e71ee4eb » | dwaynebailey | 2006-11-02 | 350 | self.pendingmtime = statistics.getmodtime(self.pendingfilename) | |
| 7e84177f » | friedelwolff | 2006-10-06 | 351 | ||
| c87a05b0 » | friedelwolff | 2006-10-06 | 352 | def readtmfile(self): | |
| d07801d0 » | friedelwolff | 2006-10-12 | 353 | """reads and parses the tm file corresponding to this file""" | |
| c87a05b0 » | friedelwolff | 2006-10-06 | 354 | if os.path.exists(self.tmfilename): | |
| e71ee4eb » | dwaynebailey | 2006-11-02 | 355 | tmmtime = statistics.getmodtime(self.tmfilename) | |
| c87a05b0 » | friedelwolff | 2006-10-06 | 356 | if tmmtime == getattr(self, "tmmtime", None): | |
| 357 | return | ||||
| 358 | inputfile = open(self.tmfilename, "r") | ||||
| d07801d0 » | friedelwolff | 2006-10-12 | 359 | self.tmmtime, self.tmfile = tmmtime, factory.getobject(inputfile, ignore=".tm") | |
| c87a05b0 » | friedelwolff | 2006-10-06 | 360 | else: | |
| 0800d3d2 » | friedelwolff | 2006-11-06 | 361 | self.tmfile = po.pofile() | |
| a079afa1 » | davidfraser | 2005-04-20 | 362 | ||
| 363 | def reclassifysuggestions(self): | ||||
| 364 | """shortcut to only update classification of has-suggestion for all items""" | ||||
| 365 | suggitems = [] | ||||
| f82628f7 » | friedelwolff | 2006-04-28 | 366 | sugglocations = {} | |
| dbcc8d96 » | friedelwolff | 2006-02-20 | 367 | for thesugg in self.pendingfile.units: | |
| f82628f7 » | friedelwolff | 2006-04-28 | 368 | locations = tuple(thesugg.getlocations()) | |
| 369 | sugglocations[locations] = thesugg | ||||
| c4d6f5be » | friedelwolff | 2006-10-03 | 370 | suggitems = [item for item in self.transunits if tuple(item.getlocations()) in sugglocations] | |
| bb29a717 » | friedelwolff | 2006-12-27 | 371 | havesuggestions = self.statistics.classify["has-suggestion"] | |
| c4d6f5be » | friedelwolff | 2006-10-03 | 372 | for item, poel in enumerate(self.transunits): | |
| a079afa1 » | davidfraser | 2005-04-20 | 373 | if (poel in suggitems) != (item in havesuggestions): | |
| 374 | if poel in suggitems: | ||||
| 375 | havesuggestions.append(item) | ||||
| 376 | else: | ||||
| 377 | havesuggestions.remove(item) | ||||
| 378 | havesuggestions.sort() | ||||
| bb29a717 » | friedelwolff | 2006-12-27 | 379 | self.statistics.calcstats() | |
| 380 | self.statistics.savestats() | ||||
| a079afa1 » | davidfraser | 2005-04-20 | 381 | ||
| 382 | def getsuggestions(self, item): | ||||
| bb29a717 » | friedelwolff | 2006-12-27 | 383 | """find all the suggestion items submitted for the given item""" | |
| b9c4dfe0 » | andreaspauley | 2006-11-23 | 384 | unit = self.transunits[item] | |
| bb29a717 » | friedelwolff | 2006-12-27 | 385 | if isinstance(unit, xliff.xliffunit): | |
| 386 | return unit.getalttrans() | ||||
| 387 | |||||
| b9c4dfe0 » | andreaspauley | 2006-11-23 | 388 | locations = unit.getlocations() | |
| bb29a717 » | friedelwolff | 2006-12-27 | 389 | self.readpendingfile() | |
| a079afa1 » | davidfraser | 2005-04-20 | 390 | # TODO: review the matching method | |
| f82628f7 » | friedelwolff | 2006-04-28 | 391 | suggestpos = [suggestpo for suggestpo in self.pendingfile.units if suggestpo.getlocations() == locations] | |
| a079afa1 » | davidfraser | 2005-04-20 | 392 | return suggestpos | |
| 393 | |||||
| f5aa1184 » | varsist | 2006-09-18 | 394 | def addsuggestion(self, item, suggtarget, username): | |
| bb29a717 » | friedelwolff | 2006-12-27 | 395 | """adds a new suggestion for the given item""" | |
| b9c4dfe0 » | andreaspauley | 2006-11-23 | 396 | unit = self.transunits[item] | |
| bb29a717 » | friedelwolff | 2006-12-27 | 397 | if isinstance(unit, xliff.xliffunit): | |
| a57b46db » | andreaspauley | 2007-03-29 | 398 | if isinstance(suggtarget, list) and (len(suggtarget) > 0): | |
| 399 | suggtarget = suggtarget[0] | ||||
| bb29a717 » | friedelwolff | 2006-12-27 | 400 | unit.addalttrans(suggtarget, origin=username) | |
| 401 | self.statistics.reclassifyunit(item) | ||||
| 402 | self.savepofile() | ||||
| 403 | return | ||||
| 404 | |||||
| 405 | self.readpendingfile() | ||||
| b9c4dfe0 » | andreaspauley | 2006-11-23 | 406 | newpo = unit.copy() | |
| a079afa1 » | davidfraser | 2005-04-20 | 407 | if username is not None: | |
| 2a84ba23 » | friedelwolff | 2006-08-15 | 408 | newpo.msgidcomments.append('"_: suggested by %s\\n"' % username) | |
| f5aa1184 » | varsist | 2006-09-18 | 409 | newpo.target = suggtarget | |
| a079afa1 » | davidfraser | 2005-04-20 | 410 | newpo.markfuzzy(False) | |
| 951a2852 » | friedelwolff | 2008-03-13 | 411 | self.pendingfile.addunit(newpo) | |
| a079afa1 » | davidfraser | 2005-04-20 | 412 | self.savependingfile() | |
| 76fa87f0 » | friedelwolff | 2006-10-10 | 413 | self.statistics.reclassifyunit(item) | |
| a079afa1 » | davidfraser | 2005-04-20 | 414 | ||
| 415 | def deletesuggestion(self, item, suggitem): | ||||
| 416 | """removes the suggestion from the pending file""" | ||||
| b9c4dfe0 » | andreaspauley | 2006-11-23 | 417 | unit = self.transunits[item] | |
| bb29a717 » | friedelwolff | 2006-12-27 | 418 | if hasattr(unit, "xmlelement"): | |
| 419 | suggestions = self.getsuggestions(item) | ||||
| 420 | unit.delalttrans(suggestions[suggitem]) | ||||
| f2e9e34a » | andreaspauley | 2007-03-29 | 421 | self.savepofile() | |
| bb29a717 » | friedelwolff | 2006-12-27 | 422 | else: | |
| 423 | self.readpendingfile() | ||||
| 424 | locations = unit.getlocations() | ||||
| 425 | # TODO: remove the suggestion in a less brutal manner | ||||
| 426 | pendingitems = [pendingitem for pendingitem, suggestpo in enumerate(self.pendingfile.units) if suggestpo.getlocations() == locations] | ||||
| 427 | pendingitem = pendingitems[suggitem] | ||||
| 428 | del self.pendingfile.units[pendingitem] | ||||
| 429 | self.savependingfile() | ||||
| 76fa87f0 » | friedelwolff | 2006-10-10 | 430 | self.statistics.reclassifyunit(item) | |
| a079afa1 » | davidfraser | 2005-04-20 | 431 | ||
| bb29a717 » | friedelwolff | 2006-12-27 | 432 | def getsuggester(self, item, suggitem): | |
| 433 | """returns who suggested the given item's suggitem if recorded, else None""" | ||||
| 434 | unit = self.getsuggestions(item)[suggitem] | ||||
| 435 | if hasattr(unit, "xmlelement"): | ||||
| 436 | return unit.xmlelement.getAttribute("origin") | ||||
| 437 | |||||
| 438 | for msgidcomment in unit.msgidcomments: | ||||
| 439 | if msgidcomment.find("suggested by ") != -1: | ||||
| 440 | suggestedby = po.unquotefrompo([msgidcomment]).replace("_:", "", 1).replace("suggested by ", "", 1).strip() | ||||
| 441 | return suggestedby | ||||
| 442 | return None | ||||
| 443 | |||||
| 6c404e8c » | friedelwolff | 2006-06-27 | 444 | def gettmsuggestions(self, item): | |
| af8592b1 » | friedelwolff | 2006-07-26 | 445 | """find all the tmsuggestion items submitted for the given item""" | |
| 6c404e8c » | friedelwolff | 2006-06-27 | 446 | self.readtmfile() | |
| b9c4dfe0 » | andreaspauley | 2006-11-23 | 447 | unit = self.transunits[item] | |
| 448 | locations = unit.getlocations() | ||||
| 6c404e8c » | friedelwolff | 2006-06-27 | 449 | # TODO: review the matching method | |
| af8592b1 » | friedelwolff | 2006-07-26 | 450 | # Can't simply use the location index, because we want multiple matches | |
| 6c404e8c » | friedelwolff | 2006-06-27 | 451 | suggestpos = [suggestpo for suggestpo in self.tmfile.units if suggestpo.getlocations() == locations] | |
| 452 | return suggestpos | ||||
| 453 | |||||
| c87a05b0 » | friedelwolff | 2006-10-06 | 454 | def track(self, item, message): | |
| 455 | """sets the tracker message for the given item""" | ||||
| 456 | self.tracker[item] = message | ||||
| 457 | |||||
| 458 | def readpofile(self): | ||||
| d07801d0 » | friedelwolff | 2006-10-12 | 459 | """reads and parses the main file""" | |
| c87a05b0 » | friedelwolff | 2006-10-06 | 460 | # make sure encoding is reset so it is read from the file | |
| 461 | self.encoding = None | ||||
| 462 | self.units = [] | ||||
| 76fa87f0 » | friedelwolff | 2006-10-10 | 463 | pomtime, filecontents = self.lockedfile.getcontents() | |
| 464 | # note: we rely on this not resetting the filename, which we set earlier, when given a string | ||||
| 465 | self.parse(filecontents) | ||||
| c87a05b0 » | friedelwolff | 2006-10-06 | 466 | # we ignore all the headers by using this filtered set | |
| 467 | self.transunits = [poel for poel in self.units if not (poel.isheader() or poel.isblank())] | ||||
| 76fa87f0 » | friedelwolff | 2006-10-10 | 468 | self.statistics.classifyunits() | |
| c87a05b0 » | friedelwolff | 2006-10-06 | 469 | self.pomtime = pomtime | |
| 470 | |||||
| 471 | def savepofile(self): | ||||
| 472 | """saves changes to the main file to disk...""" | ||||
| 473 | output = str(self) | ||||
| 76fa87f0 » | friedelwolff | 2006-10-10 | 474 | self.pomtime = self.lockedfile.writecontents(output) | |
| c87a05b0 » | friedelwolff | 2006-10-06 | 475 | ||
| 476 | def pofreshen(self): | ||||
| 477 | """makes sure we have a freshly parsed pofile""" | ||||
| 478 | if not os.path.exists(self.filename): | ||||
| 479 | # the file has been removed, update the project index (and fail below) | ||||
| 480 | self.project.scanpofiles() | ||||
| 76fa87f0 » | friedelwolff | 2006-10-10 | 481 | if self.pomtime != self.lockedfile.readmodtime(): | |
| c87a05b0 » | friedelwolff | 2006-10-06 | 482 | self.readpofile() | |
| 483 | |||||
| 484 | def getoutput(self): | ||||
| 485 | """returns pofile output""" | ||||
| 486 | self.pofreshen() | ||||
| 487 | return super(pootlefile, self).getoutput() | ||||
| 488 | |||||
| 7a7bac4b » | andreaspauley | 2006-11-02 | 489 | def updateunit(self, item, newvalues, userprefs, languageprefs): | |
| c87a05b0 » | friedelwolff | 2006-10-06 | 490 | """updates a translation with a new target value""" | |
| 491 | self.pofreshen() | ||||
| b9c4dfe0 » | andreaspauley | 2006-11-23 | 492 | unit = self.transunits[item] | |
| 7a7bac4b » | andreaspauley | 2006-11-02 | 493 | ||
| 494 | if newvalues.has_key("target"): | ||||
| b9c4dfe0 » | andreaspauley | 2006-11-23 | 495 | unit.target = newvalues["target"] | |
| 7a7bac4b » | andreaspauley | 2006-11-02 | 496 | if newvalues.has_key("fuzzy"): | |
| b9c4dfe0 » | andreaspauley | 2006-11-23 | 497 | unit.markfuzzy(newvalues["fuzzy"]) | |
| 3b2d571f » | andreaspauley | 2006-11-10 | 498 | if newvalues.has_key("translator_comments"): | |
| b9c4dfe0 » | andreaspauley | 2006-11-23 | 499 | unit.removenotes() | |
| 3b2d571f » | andreaspauley | 2006-11-10 | 500 | if newvalues["translator_comments"]: | |
| b9c4dfe0 » | andreaspauley | 2006-11-23 | 501 | unit.addnote(newvalues["translator_comments"]) | |
| 7a7bac4b » | andreaspauley | 2006-11-02 | 502 | ||
| c87a05b0 » | friedelwolff | 2006-10-06 | 503 | po_revision_date = time.strftime("%F %H:%M%z") | |
| 504 | headerupdates = {"PO_Revision_Date": po_revision_date, "X_Generator": self.x_generator} | ||||
| 505 | if userprefs: | ||||
| 506 | if getattr(userprefs, "name", None) and getattr(userprefs, "email", None): | ||||
| 507 | headerupdates["Last_Translator"] = "%s <%s>" % (userprefs.name, userprefs.email) | ||||
| 508 | self.updateheader(add=True, **headerupdates) | ||||
| 509 | if languageprefs: | ||||
| 510 | nplurals = getattr(languageprefs, "nplurals", None) | ||||
| 511 | pluralequation = getattr(languageprefs, "pluralequation", None) | ||||
| 512 | if nplurals and pluralequation: | ||||
| 513 | self.updateheaderplural(nplurals, pluralequation) | ||||
| 514 | self.savepofile() | ||||
| 76fa87f0 » | friedelwolff | 2006-10-10 | 515 | self.statistics.reclassifyunit(item) | |
| c87a05b0 » | friedelwolff | 2006-10-06 | 516 | ||
| a079afa1 » | davidfraser | 2005-04-20 | 517 | def iteritems(self, search, lastitem=None): | |
| 518 | """iterates through the items in this pofile starting after the given lastitem, using the given search""" | ||||
| 519 | # update stats if required | ||||
| 76fa87f0 » | friedelwolff | 2006-10-10 | 520 | self.statistics.getstats() | |
| a079afa1 » | davidfraser | 2005-04-20 | 521 | if lastitem is None: | |
| 522 | minitem = 0 | ||||
| 523 | else: | ||||
| 524 | minitem = lastitem + 1 | ||||
| c4d6f5be » | friedelwolff | 2006-10-03 | 525 | maxitem = len(self.transunits) | |
| a079afa1 » | davidfraser | 2005-04-20 | 526 | validitems = range(minitem, maxitem) | |
| c4d6f5be » | friedelwolff | 2006-10-03 | 527 | if search.assignedto or search.assignedaction: | |
| 76fa87f0 » | friedelwolff | 2006-10-10 | 528 | assignitems = self.assigns.finditems(search) | |
| a079afa1 » | davidfraser | 2005-04-20 | 529 | validitems = [item for item in validitems if item in assignitems] | |
| 530 | # loop through, filtering on matchnames if required | ||||
| 531 | for item in validitems: | ||||
| 532 | if not search.matchnames: | ||||
| 533 | yield item | ||||
| 534 | for name in search.matchnames: | ||||
| 76fa87f0 » | friedelwolff | 2006-10-10 | 535 | if item in self.statistics.classify[name]: | |
| a079afa1 » | davidfraser | 2005-04-20 | 536 | yield item | |
| 537 | |||||
| e0296a1e » | friedelwolff | 2006-07-27 | 538 | def matchitems(self, newfile, uselocations=False): | |
| 539 | """matches up corresponding items in this pofile with the given newfile, and returns tuples of matching poitems (None if no match found)""" | ||||
| f82628f7 » | friedelwolff | 2006-04-28 | 540 | if not hasattr(self, "sourceindex"): | |
| a079afa1 » | davidfraser | 2005-04-20 | 541 | self.makeindex() | |
| e0296a1e » | friedelwolff | 2006-07-27 | 542 | if not hasattr(newfile, "sourceindex"): | |
| 543 | newfile.makeindex() | ||||
| a079afa1 » | davidfraser | 2005-04-20 | 544 | matches = [] | |
| e0296a1e » | friedelwolff | 2006-07-27 | 545 | for newpo in newfile.units: | |
| 0ba87081 » | friedelwolff | 2006-04-05 | 546 | if newpo.isheader(): | |
| 547 | continue | ||||
| abcfacfe » | friedelwolff | 2006-03-13 | 548 | foundid = False | |
| f82628f7 » | friedelwolff | 2006-04-28 | 549 | if uselocations: | |
| 550 | newlocations = newpo.getlocations() | ||||
| 551 | mergedlocations = [] | ||||
| 552 | for location in newlocations: | ||||
| 553 | if location in mergedlocations: | ||||
| a079afa1 » | davidfraser | 2005-04-20 | 554 | continue | |
| f82628f7 » | friedelwolff | 2006-04-28 | 555 | if location in self.locationindex: | |
| 556 | oldpo = self.locationindex[location] | ||||
| a079afa1 » | davidfraser | 2005-04-20 | 557 | if oldpo is not None: | |
| abcfacfe » | friedelwolff | 2006-03-13 | 558 | foundid = True | |
| a079afa1 » | davidfraser | 2005-04-20 | 559 | matches.append((oldpo, newpo)) | |
| f82628f7 » | friedelwolff | 2006-04-28 | 560 | mergedlocations.append(location) | |
| a079afa1 » | davidfraser | 2005-04-20 | 561 | continue | |
| abcfacfe » | friedelwolff | 2006-03-13 | 562 | if not foundid: | |
| 25a5e6c0 » | friedelwolff | 2006-07-27 | 563 | # We can't use the multistring, because it might contain more than two | |
| 564 | # entries in a PO xliff file. Rather use the singular. | ||||
| f5aa1184 » | varsist | 2006-09-18 | 565 | source = unicode(newpo.source) | |
| 566 | if source in self.sourceindex: | ||||
| 567 | oldpo = self.sourceindex[source] | ||||
| a079afa1 » | davidfraser | 2005-04-20 | 568 | matches.append((oldpo, newpo)) | |
| 569 | else: | ||||
| 570 | matches.append((None, newpo)) | ||||
| 571 | # find items that have been removed | ||||
| 572 | matcheditems = [oldpo for oldpo, newpo in matches if oldpo] | ||||
| dbcc8d96 » | friedelwolff | 2006-02-20 | 573 | for oldpo in self.units: | |
| a079afa1 » | davidfraser | 2005-04-20 | 574 | if not oldpo in matcheditems: | |
| 575 | matches.append((oldpo, None)) | ||||
| 576 | return matches | ||||
| 577 | |||||
| 2d2ea731 » | friedelwolff | 2008-02-29 | 578 | def mergeitem(self, oldpo, newpo, username, suggest=False): | |
| a079afa1 » | davidfraser | 2005-04-20 | 579 | """merges any changes from newpo into oldpo""" | |
| 574eec06 » | friedelwolff | 2006-03-03 | 580 | unchanged = oldpo.target == newpo.target | |
| 2d2ea731 » | friedelwolff | 2008-02-29 | 581 | ||
| 582 | if not suggest and (not oldpo.target or not newpo.target or oldpo.isheader() or newpo.isheader() or unchanged): | ||||
| a079afa1 » | davidfraser | 2005-04-20 | 583 | oldpo.merge(newpo) | |
| 2d2ea731 » | friedelwolff | 2008-02-29 | 584 | elif not unchanged: | |
| 951a2852 » | friedelwolff | 2008-03-13 | 585 | #XXX: this is very inefficient! | |
| c4d6f5be » | friedelwolff | 2006-10-03 | 586 | for item, matchpo in enumerate(self.transunits): | |
| a079afa1 » | davidfraser | 2005-04-20 | 587 | if matchpo == oldpo: | |
| e0296a1e » | friedelwolff | 2006-07-27 | 588 | strings = getattr(newpo.target, "strings", [newpo.target]) | |
| 589 | self.addsuggestion(item, strings, username) | ||||
| a079afa1 » | davidfraser | 2005-04-20 | 590 | return | |
| 591 | raise KeyError("Could not find item for merge") | ||||
| 592 | |||||
| 2d2ea731 » | friedelwolff | 2008-02-29 | 593 | def mergefile(self, newfile, username, allownewstrings=True, suggestions=False): | |
| a079afa1 » | davidfraser | 2005-04-20 | 594 | """make sure each msgid is unique ; merge comments etc from duplicates into original""" | |
| 595 | self.makeindex() | ||||
| e0296a1e » | friedelwolff | 2006-07-27 | 596 | matches = self.matchitems(newfile) | |
| a079afa1 » | davidfraser | 2005-04-20 | 597 | for oldpo, newpo in matches: | |
| 2d2ea731 » | friedelwolff | 2008-02-29 | 598 | if suggestions: | |
| 599 | if oldpo and newpo: | ||||
| 600 | self.mergeitem(oldpo, newpo, username, suggest=True) | ||||
| 601 | continue | ||||
| 602 | |||||
| a079afa1 » | davidfraser | 2005-04-20 | 603 | if oldpo is None: | |
| 604 | if allownewstrings: | ||||
| e0296a1e » | friedelwolff | 2006-07-27 | 605 | if isinstance(newpo, po.pounit): | |
| 951a2852 » | friedelwolff | 2008-03-13 | 606 | self.addunit(newpo) | |
| e0296a1e » | friedelwolff | 2006-07-27 | 607 | else: | |
| 951a2852 » | friedelwolff | 2008-03-13 | 608 | self.addunit(self.UnitClass.buildfromunit(newpo)) | |
| a079afa1 » | davidfraser | 2005-04-20 | 609 | elif newpo is None: | |
| 610 | # TODO: mark the old one as obsolete | ||||
| 611 | pass | ||||
| 612 | else: | ||||
| 613 | self.mergeitem(oldpo, newpo, username) | ||||
| abcfacfe » | friedelwolff | 2006-03-13 | 614 | # we invariably want to get the ids (source locations) from the newpo | |
| e0296a1e » | friedelwolff | 2006-07-27 | 615 | if hasattr(newpo, "sourcecomments"): | |
| 616 | oldpo.sourcecomments = newpo.sourcecomments | ||||
| 617 | |||||
| 2d2ea731 » | friedelwolff | 2008-02-29 | 618 | if not isinstance(newfile, po.pofile) or suggestions: | |
| e0296a1e » | friedelwolff | 2006-07-27 | 619 | #TODO: We don't support updating the header yet. | |
| 620 | self.savepofile() | ||||
| 621 | # the easiest way to recalculate everything | ||||
| 622 | self.readpofile() | ||||
| 623 | return | ||||
| f9512f37 » | friedelwolff | 2006-04-05 | 624 | ||
| 625 | #Let's update selected header entries. Only the ones listed below, and ones | ||||
| 626 | #that are empty in self can be updated. The check in header_order is just | ||||
| 627 | #a basic sanity check so that people don't insert garbage. | ||||
| 628 | updatekeys = ['Content-Type', | ||||
| 629 | 'POT-Creation-Date', | ||||
| 630 | 'Last-Translator', | ||||
| 631 | 'Project-Id-Version', | ||||
| 632 | 'PO-Revision-Date', | ||||
| 633 | 'Language-Team'] | ||||
| 634 | headerstoaccept = {} | ||||
| 635 | ownheader = self.parseheader() | ||||
| e0296a1e » | friedelwolff | 2006-07-27 | 636 | for (key, value) in newfile.parseheader().items(): | |
| d8ecb7e9 » | andreaspauley | 2007-01-19 | 637 | if key in updatekeys or (not key in ownheader or not ownheader[key]) and key in po.pofile.header_order: | |
| f9512f37 » | friedelwolff | 2006-04-05 | 638 | headerstoaccept[key] = value | |
| 639 | self.updateheader(add=True, **headerstoaccept) | ||||
| 640 | |||||
| 641 | #Now update the comments above the header: | ||||
| 642 | header = self.header() | ||||
| e0296a1e » | friedelwolff | 2006-07-27 | 643 | newheader = newfile.header() | |
| f9512f37 » | friedelwolff | 2006-04-05 | 644 | if header is None and not newheader is None: | |
| 1f9c3413 » | friedelwolff | 2006-06-20 | 645 | header = self.UnitClass("", encoding=self.encoding) | |
| f9512f37 » | friedelwolff | 2006-04-05 | 646 | header.target = "" | |
| c4d6f5be » | friedelwolff | 2006-10-03 | 647 | if header: | |
| f10f15db » | friedelwolff | 2007-11-13 | 648 | header._initallcomments(blankall=True) | |
| f9512f37 » | friedelwolff | 2006-04-05 | 649 | if newheader: | |
| 650 | for i in range(len(header.allcomments)): | ||||
| 651 | header.allcomments[i].extend(newheader.allcomments[i]) | ||||
| 652 | |||||
| a079afa1 » | davidfraser | 2005-04-20 | 653 | self.savepofile() | |
| fa5719d4 » | friedelwolff | 2006-02-24 | 654 | # the easiest way to recalculate everything | |
| a079afa1 » | davidfraser | 2005-04-20 | 655 | self.readpofile() | |
| 656 | |||||
| 657 | class Search: | ||||
| 20a2bb24 » | davidfraser | 2006-02-02 | 658 | """an object containing all the searching information""" | |
| a079afa1 » | davidfraser | 2005-04-20 | 659 | def __init__(self, dirfilter=None, matchnames=[], assignedto=None, assignedaction=None, searchtext=None): | |
| 660 | self.dirfilter = dirfilter | ||||
| 661 | self.matchnames = matchnames | ||||
| 662 | self.assignedto = assignedto | ||||
| 663 | self.assignedaction = assignedaction | ||||
| 664 | self.searchtext = searchtext | ||||
| 665 | |||||
| 6daa67b7 » | davidfraser | 2005-06-23 | 666 | def copy(self): | |
| 667 | """returns a copy of this search""" | ||||
| 668 | return Search(self.dirfilter, self.matchnames, self.assignedto, self.assignedaction, self.searchtext) | ||||
| 669 | |||||
