root/translations/lib/types/pot.py @ 528:25c1783c0111

Revision 528:25c1783c0111, 14.8 KB (checked in by Diego Búrigo Zacarão <diegobz@…>, 17 months ago)

Delete cache and POFiles entries when a file is deleted from upstream

Line 
1import os, commands, re
2from django.contrib.contenttypes.models import ContentType
3from django.conf import settings
4from translations.lib.types import (TransManagerMixin, TransManagerError)
5from translations.models import POFile, Language
6from translations.lib.utils import (run_command, CommandError)
7
8class POTStatsError(Exception):
9
10    def __init__(self, language):
11        self.language = language
12
13    def __str__(self):
14        return "Could not calculate the statistics using the '%s' " \
15               "language." % (self.language)
16
17class FileFilterError(Exception):
18
19    def __str__(self):
20        return "The file filter should allows the POTFILES.in file" \
21               " for intltool POT-based projects."
22
23class POTManager(TransManagerMixin):
24    """ A browser class for POT files. """
25
26    def __init__(self, file_set, path, source_lang, file_filter,
27        filepath=None):
28        self.file_set = file_set
29        if filepath is None:
30            filepath = path
31        self.path = filepath
32        self.source_lang = source_lang
33        self.file_filter = file_filter
34        self.msgmerge_path = os.path.join(settings.MSGMERGE_DIR, 
35                                     os.path.basename(path))
36
37    def get_file_path(self, filename, is_msgmerged=False):
38        # All the files should be in the file_set, except the intltool
39        # POT file that is created by the system
40        if filename in self.file_set or \
41           filename.endswith('.pot') and is_msgmerged:
42            if is_msgmerged:
43                file_path = os.path.join(self.msgmerge_path, filename)
44            else:
45                file_path = os.path.join(self.path, filename)
46        else:
47            raise IOError("File not found.")
48        return file_path
49
50    def get_file_content(self, filename, is_msgmerged=False):
51        file_path = self.get_file_path(filename, is_msgmerged)
52        filef = file(file_path, 'rb')
53        file_content = filef.read()
54        filef.close()
55        return file_content
56
57    def get_po_files(self):
58        """ Return a list of PO filenames """
59
60        po_files = []
61        for filename in self.file_set:
62            if filename.endswith('.po'):
63                po_files.append(filename)
64        po_files.sort()
65        return po_files
66
67    def get_langfiles(self, lang):
68        """ Return a list with the PO filenames for a specificy language """
69
70        files=[]
71        for filepath in self.get_po_files():
72            if self.guess_language(filepath) == lang:
73                files.append(filepath)
74        return files
75
76    def guess_language(self, filepath):
77        """ Guess a language from a filepath """
78
79        if 'LC_MESSAGES' in filepath:
80            fp = filepath.split('LC_MESSAGES')
81            return os.path.basename(fp[0][:-1:])
82        else:
83            return os.path.basename(filepath[:-3:])
84
85    def get_langs(self):
86        """ Return all langs tha have a po file for a object """
87
88        langs = []
89        for filepath in self.get_po_files():
90            langs.append(self.guess_language(filepath))
91        langs.sort()
92        return langs
93
94
95    def po_file_stats(self, pofile):
96        """ Calculate stats for a POT/PO file """
97        error = False
98        pofile = os.path.join(self.msgmerge_path, pofile)
99
100        command = "LC_ALL=C LANG=C LANGUAGE=C msgfmt --statistics" \
101                  " -o /dev/null %s" % pofile
102        (error, output) = commands.getstatusoutput(command)
103
104        if error:
105            error = True
106   
107        r_tr = re.search(r"([0-9]+) translated", output)
108        r_un = re.search(r"([0-9]+) untranslated", output)
109        r_fz = re.search(r"([0-9]+) fuzzy", output)
110
111        if r_tr: translated = r_tr.group(1)
112        else: translated = 0
113        if r_un: untranslated = r_un.group(1)
114        else: untranslated = 0
115        if r_fz: fuzzy = r_fz.group(1)
116        else: fuzzy = 0
117
118        return {'translated' : int(translated),
119                'fuzzy' : int(fuzzy),
120                'untranslated' : int(untranslated),
121                'error' : error,}
122
123    def calculate_file_stats(self, filename, try_to_merge):
124        """
125        Return the statistics of a specificy file for an object after
126        merging the file with the source translation file (POT), if possible.
127        """
128        # We might want to skip the msgmerge setting try_to_merge as False
129        if try_to_merge:
130            source_file = self.get_source_file_for_pofile(filename)
131            (is_msgmerged, file_path) = self.msgmerge(filename, source_file)
132        else:
133            is_msgmerged=False
134            file_path = os.path.join(self.path, filename)
135
136        #Copy the current file (non-msgmerged) to the static dir
137        if not is_msgmerged:
138            self.copy_file_to_static_dir(filename)
139
140        postats = self.po_file_stats(file_path)
141
142        return {'trans': postats['translated'],
143                'fuzzy': postats['fuzzy'],
144                'untrans': postats['untranslated'],
145                'error': postats['error'],
146                'is_msgmerged': is_msgmerged}
147
148    def create_lang_stats(self, lang, object, try_to_merge=True):
149        """Set the statistics of a specificy language for an object."""
150
151        for filename in self.get_langfiles(lang):
152            self.create_file_stats(filename, object, try_to_merge)
153
154    def create_file_stats(self, filename, object, try_to_merge=True):
155        """Set the statistics of a specificy file for an object."""
156        lang_code = self.guess_language(filename)
157        try:
158            stats = self.calculate_file_stats(filename, try_to_merge)
159            ctype = ContentType.objects.get_for_model(object)
160            s = POFile.objects.get(object_id=object.id, content_type=ctype,
161                                   filename=filename)
162            if not s.language:
163                try:
164                    l = Language.objects.by_code_or_alias(code=lang_code)
165                    s.language=l
166                except Language.DoesNotExist:
167                    pass
168        except POTStatsError:
169            # TODO: It should probably be raised when a checkout of a
170            # module has a problem. Needs to decide what to do when it
171            # happens
172            pass
173        except POFile.DoesNotExist:
174            try:
175                l = Language.objects.by_code_or_alias(code=lang_code)
176            except Language.DoesNotExist:
177                l = None
178            s = POFile.objects.create(language=l, filename=filename,
179                                        object=object)
180        s.set_stats(trans=stats['trans'], fuzzy=stats['fuzzy'], 
181                    untrans=stats['untrans'], error=stats['error'])
182        s.is_msgmerged = stats['is_msgmerged']
183        s.language_code = lang_code
184        return s.save()
185
186    def stats_for_lang_object(self, lang, object):
187        """Return statistics for an object in a specific language."""
188        try:
189            ctype = ContentType.objects.get_for_model(object)
190            return POFile.objects.filter(language=lang, content_type=ctype, 
191                                         object_id=object.id)[0]
192        except IndexError:
193            return None
194
195    def get_stats(self, object):
196        """ Return a list of statistics of languages for an object."""
197        return POFile.objects.by_object_total(object)
198
199    def delete_stats_for_object(self, object):
200        """ Delete all lang statistics of an object."""
201        ctype = ContentType.objects.get_for_model(object)
202        POFile.objects.filter(object_id=object.id, content_type=ctype).delete()
203
204    def delete_stats_for_file_object(self, filename, object):
205        """Delete a specific pofile of an object"""
206        ctype = ContentType.objects.get_for_model(object)
207        POFile.objects.filter(filename=filename, object_id=object.id, 
208            content_type=ctype).delete()
209        self.delete_file_from_static_dir(filename)
210
211    def set_source_stats(self, object, is_msgmerged):
212        """Set the source file (pot) in the database"""
213
214        ctype = ContentType.objects.get_for_model(object)
215        potfiles=self.get_source_files()
216        for potfile in potfiles:
217            p, created = POFile.objects.get_or_create(filename=potfile,
218                                                      is_pot=True,
219                                                      content_type=ctype,
220                                                      object_id=object.id,
221                                                      is_msgmerged=is_msgmerged)
222            stats = self.po_file_stats(potfile)
223            p.set_stats(trans=stats['translated'], 
224                        fuzzy=stats['fuzzy'], 
225                        untrans=stats['untranslated'], 
226                        error=stats['error'])
227
228            p.save()
229
230    def get_source_stats(self, object):
231        """
232        Return a list of the source file (pot) statistics from the database
233        """
234        try:
235            ctype = ContentType.objects.get_for_model(object)
236            return POFile.objects.filter(object_id=object.id, 
237                                         content_type=ctype, is_pot=True)
238        except POFile.DoesNotExist:
239            return None
240
241    def get_source_files(self):
242        """
243        Return a list with the source files (pot) paths
244
245        Try to find it in the file_set passed to the PO file instace.
246        If it still fauls, try to find the POT file in the filesystem.
247        """
248        pofiles=[]
249        for filename in self.file_set:
250            if filename.endswith('.pot'):
251                pofiles.append(filename)
252
253        # If there is no POT in the file_set, try to find it in
254        # the file system
255        if not pofiles:
256            filename = self.get_intltool_source_file(self.msgmerge_path)
257            if filename:
258                pofiles.append(filename)
259
260        return pofiles
261
262    def get_intltool_source_file(self, po_dir):
263        """Return the POT file that might be created by intltool"""
264        for root, dirs, files in os.walk(po_dir):
265            for filename in files:
266                if filename.endswith('.pot'):
267                    # Get the relative path
268                    rel_path = root.split(os.path.basename(self.path))[1]
269                    # Return the relative path of the POT file without
270                    # the / in the start of the POT file path
271                    return os.path.join(rel_path, filename)[1:]
272
273    def get_source_file_for_pofile(self, filename):
274        """
275        Find the related source file (POT) for a pofile when it has multiple
276        source files.
277
278        This method gets a filename as parameter and tries to discover the
279        related POT file using two methods:
280        1. Trying to find a POT file with the same base path that the pofile.
281           Example: /foo/bar.pot and /foo/baz.po match on this method.
282
283        2. Trying to find a POT file with the same domain that the pofile in any
284           directory.
285           Example: /foo/bar.pot and /foo/baz/bar.po match on this method.
286           The domain in this case is 'bar'.
287
288        If no POT is found the method returns None.
289        """
290
291        # For filename='/foo/bar.po'
292        fb = os.path.basename(filename) # 'bar.po'
293        fp = filename.split(fb)[0]        # '/foo/'
294
295        source_files = self.get_source_files()
296
297        # Find the POT with the same domain or path that the filename,
298        # if the component has more that one POT file
299        if len(source_files) > 1:
300            for source in source_files:
301                sb = os.path.basename(source)[:-1] # *.po instead *.pot
302                pb = source.split(sb)[0]
303                if pb==fp or sb==fb:
304                    return source
305        elif len(source_files) == 1:
306            return source_files[0]
307        else:
308            return None
309
310    def copy_file_to_static_dir(self, filename):
311        """Copy a file to the destination"""
312        import shutil
313
314        dest = os.path.join(self.msgmerge_path, filename)
315
316        if not os.path.exists(os.path.dirname(dest)):
317            os.makedirs(os.path.dirname(dest))
318
319        shutil.copyfile(os.path.join(self.path, filename), dest)
320
321    def delete_file_from_static_dir(self, filename):
322        """Delete a file from the static cache dir"""
323        dest = os.path.join(self.msgmerge_path, filename)
324        try:
325            os.remove(dest)
326        except OSError:
327            pass
328
329    def msgmerge(self, pofile, potfile):
330        """
331        Merge two files and save the output at the settings.MSGMERGE_DIR.
332        In case that error, copy the source file (pofile) to the
333        destination without merging.
334        """
335        is_msgmerged = True
336        outpo = os.path.join(self.msgmerge_path, pofile)
337
338        try:
339        # TODO: Find a library to avoid call msgmerge by command
340            command = "msgmerge -o %(outpo)s %(pofile)s %(potfile)s" % {
341                    'outpo' : outpo,
342                    'pofile' : os.path.join(self.path, pofile),
343                    'potfile' : os.path.join(self.msgmerge_path, potfile),}
344           
345            (error, output) = commands.getstatusoutput(command)
346        except:
347            error = True
348
349        if error:
350            # TODO: Log this. output var can be used.
351            is_msgmerged = False
352
353        return (is_msgmerged, outpo)
354
355    def guess_po_dir(self):
356        """ Guess the po/ diretory to run intltool """
357        for filename in self.file_set:
358            if 'POTFILES.in' in filename:
359                if self.file_filter:
360                    if re.compile(self.file_filter).match(filename):
361                        return os.path.join(self.path, 
362                                      os.path.dirname(filename))
363        raise FileFilterError
364
365    def intltool_update(self):
366        """
367        Create a new POT file using "intltool-update -p" from the
368        source files. Return False if it fails.
369        """
370        po_dir = self.guess_po_dir()
371        try:
372            command = "cd \"%(dir)s\" && rm -f missing notexist && " \
373                      "intltool-update -p" % { "dir" : po_dir, }
374            (error, output) = commands.getstatusoutput(command)
375        except:
376            error = True
377
378        # Copy the potfile if it exist to the merged files directory
379        potfile = self.get_intltool_source_file(po_dir)
380        if potfile:
381            self.copy_file_to_static_dir(potfile)
382
383        if error:
384            # TODO: Log this. output var can be used.
385            return False
386
387        return True
388
389    def msgfmt_check(self, po_contents):
390        """
391        Run a `msgfmt -c` on a file (file object).
392        Raises a ValueError in case the file has errors.
393        """
394        try:
395            p = run_command('msgfmt -o /dev/null -c -', _input=po_contents)
396        except CommandError:
397            # TODO: Figure out why gettext is not working here
398            raise ValueError, "Your file does not" \
399                            " pass by the check for correctness" \
400                            " (msgfmt -c). Please run this command" \
401                            " on your system to see the errors."
Note: See TracBrowser for help on using the browser.