~launchpad-pqm/launchpad/devel

14134.5.2 by Jeroen Vermeulen
Request branch scan on the master store, not the slave store.
1
# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
8687.15.34 by Karl Fogel
Add license header blocks to .py, .zcml, and .pt files that don't have it
2
# GNU Affero General Public License version 3 (see the file LICENSE).
8657.8.1 by Jeroen Vermeulen
Script to commit translations to a branch.
3
4
"""Export translation snapshots to bzr branches where requested."""
5
6
__metaclass__ = type
7
__all__ = ['ExportTranslationsToBranch']
8
9
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
10
from datetime import (
11
    datetime,
12
    timedelta,
13
    )
8657.8.1 by Jeroen Vermeulen
Script to commit translations to a branch.
14
import os.path
10100.1.4 by Jonathan Lange
More pytz.
15
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
16
from bzrlib.errors import NotBranchError
13261.7.4 by Jelmer Vernooij
Fix use of deprecated iter_reverse_revision_history in translations code.
17
from bzrlib.revision import NULL_REVISION
10100.1.4 by Jonathan Lange
More pytz.
18
import pytz
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
19
from storm.expr import (
7675.913.8 by j.c.sackett
Lint fixes.
20
    And,
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
21
    Join,
22
    )
8657.8.1 by Jeroen Vermeulen
Script to commit translations to a branch.
23
from zope.component import getUtility
24
14550.1.2 by Steve Kowalik
Revert lib/lp/translations/scripts/translations_to_branch.py
25
# Load the normal plugin set.  Your linter may complain, and automated
26
# imports formatting tools will rearrange this, but keep it above the
27
# other Launchpad imports.
28
import lp.codehosting
29
10914.2.1 by Jeroen Vermeulen
Email notifications for unpushed translations branches.
30
from canonical.config import config
14593.2.12 by Curtis Hovey
Move mail helpers to lp.services.mail.helpers.
31
14593.2.15 by Curtis Hovey
Moved helpers to lp.services.
32
from lp.services.helpers import shortlist
14593.2.12 by Curtis Hovey
Move mail helpers to lp.services.mail.helpers.
33
from lp.services.mail.helpers import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
34
    get_contact_email_addresses,
35
    get_email_template,
36
    )
14560.2.29 by Curtis Hovey
Restored lpstorm module name because it lp engineers know that name.
37
from lp.services.database.lpstorm import IMasterStore
12143.1.2 by Danilo Segan
Clean-up imports.
38
from canonical.launchpad.webapp import errorlog
8657.8.1 by Jeroen Vermeulen
Script to commit translations to a branch.
39
from canonical.launchpad.webapp.interfaces import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
40
    IStoreSelector,
41
    MAIN_STORE,
42
    SLAVE_FLAVOR,
43
    )
7675.913.13 by j.c.sackett
Added comments.
44
from lp.app.enums import ServiceUsage
13084.5.4 by Aaron Bentley
Handle StaleLastMirrored by scheduling scan.
45
from lp.code.errors import StaleLastMirrored
46
from lp.code.interfaces.branch import get_db_branch_info
8657.8.1 by Jeroen Vermeulen
Script to commit translations to a branch.
47
from lp.code.interfaces.branchjob import IRosettaUploadJobSource
14134.5.2 by Jeroen Vermeulen
Request branch scan on the master store, not the slave store.
48
from lp.code.model.branch import Branch
8657.8.1 by Jeroen Vermeulen
Script to commit translations to a branch.
49
from lp.code.model.directbranchcommit import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
50
    ConcurrentUpdateError,
51
    DirectBranchCommit,
52
    )
53
from lp.codehosting.vfs import get_rw_server
54
from lp.services.mail.sendmail import (
55
    format_address,
56
    simple_sendmail,
57
    )
8657.8.1 by Jeroen Vermeulen
Script to commit translations to a branch.
58
from lp.services.scripts.base import LaunchpadCronScript
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
59
from lp.translations.interfaces.potemplate import IPOTemplateSet
8657.8.1 by Jeroen Vermeulen
Script to commit translations to a branch.
60
61
62
class ExportTranslationsToBranch(LaunchpadCronScript):
63
    """Commit translations to translations_branches where requested."""
64
9426.6.1 by Jeroen Vermeulen
Don't export files that clearly haven't changed.
65
    commit_message = "Launchpad automatic translations update."
66
67
    # Don't bother looking for a previous translations commit if it's
68
    # longer than this ago.
69
    previous_commit_cutoff_age = timedelta(days=7)
70
71
    # We can find out when the last translations commit to a branch
72
    # completed, and we can find out when the last transaction changing
73
    # a POFile started.  This is exactly the wrong way around for
74
    # figuring out which POFiles need a fresh export, so assume a fudge
75
    # factor.
76
    fudge_factor = timedelta(hours=6)
77
7675.328.1 by Jeroen Vermeulen
Fix failure in export-to-branches: bzr revision timestamp is a float.
78
    def add_my_options(self):
79
        """See `LaunchpadScript`."""
80
        self.parser.add_option(
81
            '-n', '--no-fudge', action='store_true', dest='no_fudge',
82
            default=False,
83
            help="For testing: no fudge period for POFile changes.")
84
8657.8.1 by Jeroen Vermeulen
Script to commit translations to a branch.
85
    def _checkForObjections(self, source):
86
        """Check for reasons why we can't commit to this branch.
87
88
        Raises `ConcurrentUpdateError` if there is such a reason.
89
90
        :param source: the series being exported to its
91
            translations_branch.
92
        """
93
        if source.translations_branch is None:
94
            raise ConcurrentUpdateError(
95
                "Translations export for %s was just disabled." % (
96
                    source.title))
97
9729.1.1 by Jeroen Vermeulen
Don't block on unfinished branch jobs that are clearly out of date.
98
        branch = source.translations_branch
8657.8.1 by Jeroen Vermeulen
Script to commit translations to a branch.
99
        jobsource = getUtility(IRosettaUploadJobSource)
9729.1.1 by Jeroen Vermeulen
Don't block on unfinished branch jobs that are clearly out of date.
100
        unfinished_jobs = jobsource.findUnfinishedJobs(
10100.1.4 by Jonathan Lange
More pytz.
101
            branch, since=datetime.now(pytz.UTC) - timedelta(days=1))
9729.1.1 by Jeroen Vermeulen
Don't block on unfinished branch jobs that are clearly out of date.
102
103
        if unfinished_jobs.any():
8657.8.1 by Jeroen Vermeulen
Script to commit translations to a branch.
104
            raise ConcurrentUpdateError(
105
                "Translations branch for %s has pending translations "
106
                "changes.  Not committing." % source.title)
107
9426.6.1 by Jeroen Vermeulen
Don't export files that clearly haven't changed.
108
    def _makeDirectBranchCommit(self, db_branch):
8657.8.1 by Jeroen Vermeulen
Script to commit translations to a branch.
109
        """Create a `DirectBranchCommit`.
110
9426.6.1 by Jeroen Vermeulen
Don't export files that clearly haven't changed.
111
        :param db_branch: A `Branch` object as defined in Launchpad.
112
        :return: A `DirectBranchCommit` for `db_branch`.
8657.8.1 by Jeroen Vermeulen
Script to commit translations to a branch.
113
        """
11592.1.5 by Jeroen Vermeulen
Missed a spot on the changes for Noodles.
114
        committer_id = 'Launchpad Translations on behalf of %s' % (
11592.1.3 by Jeroen Vermeulen
Review changes.
115
            db_branch.owner.name)
11592.1.5 by Jeroen Vermeulen
Missed a spot on the changes for Noodles.
116
        return DirectBranchCommit(db_branch, committer_id=committer_id)
8657.8.1 by Jeroen Vermeulen
Script to commit translations to a branch.
117
118
    def _commit(self, source, committer):
119
        """Commit changes to branch.  Check for race conditions."""
120
        self._checkForObjections(source)
9375.3.3 by Jeroen Vermeulen
Merge devel
121
        committer.commit(self.commit_message, txn=self.txn)
9426.6.1 by Jeroen Vermeulen
Don't export files that clearly haven't changed.
122
123
    def _isTranslationsCommit(self, revision):
124
        """Is `revision` an automatic translations commit?"""
125
        return revision.message == self.commit_message
126
7675.328.1 by Jeroen Vermeulen
Fix failure in export-to-branches: bzr revision timestamp is a float.
127
    def _getRevisionTime(self, revision):
128
        """Get timestamp of `revision`."""
129
        # The bzr timestamp is a float representing UTC-based seconds
130
        # since the epoch.  It stores the timezone as well, but we can
131
        # ignore it here.
10100.1.4 by Jonathan Lange
More pytz.
132
        return datetime.fromtimestamp(revision.timestamp, pytz.UTC)
7675.328.1 by Jeroen Vermeulen
Fix failure in export-to-branches: bzr revision timestamp is a float.
133
9426.6.1 by Jeroen Vermeulen
Don't export files that clearly haven't changed.
134
    def _getLatestTranslationsCommit(self, branch):
135
        """Get date of last translations commit to `branch`, if any."""
10100.1.4 by Jonathan Lange
More pytz.
136
        cutoff_date = datetime.now(pytz.UTC) - self.previous_commit_cutoff_age
9426.6.1 by Jeroen Vermeulen
Don't export files that clearly haven't changed.
137
138
        revno, current_rev = branch.last_revision_info()
139
        repository = branch.repository
13261.7.4 by Jelmer Vernooij
Fix use of deprecated iter_reverse_revision_history in translations code.
140
        graph = repository.get_graph()
141
        for rev_id in graph.iter_lefthand_ancestry(
142
                current_rev, (NULL_REVISION, )):
9426.6.1 by Jeroen Vermeulen
Don't export files that clearly haven't changed.
143
            revision = repository.get_revision(rev_id)
7675.328.1 by Jeroen Vermeulen
Fix failure in export-to-branches: bzr revision timestamp is a float.
144
            revision_date = self._getRevisionTime(revision)
9426.6.1 by Jeroen Vermeulen
Don't export files that clearly haven't changed.
145
            if self._isTranslationsCommit(revision):
146
                return revision_date
147
148
            if revision_date < cutoff_date:
149
                # Going too far back in history.  Give up.
150
                return None
151
152
        return None
8657.8.1 by Jeroen Vermeulen
Script to commit translations to a branch.
153
11942.1.1 by Danilo Segan
Factor out POFile gathering for bzr exports.
154
    def _findChangedPOFiles(self, source, changed_since):
155
        """Return an iterator of POFiles changed since `changed_since`.
156
157
        :param source: a `ProductSeries`.
158
        :param changed_since: a datetime object.
159
        """
160
        subset = getUtility(IPOTemplateSet).getSubset(
161
            productseries=source, iscurrent=True)
162
        for template in subset:
163
            for pofile in template.pofiles:
164
                if (changed_since is None or
11942.1.2 by Danilo Segan
Export all POFiles for a template that has been updated as well.
165
                    pofile.date_changed > changed_since or
166
                    template.date_last_updated > changed_since):
11942.1.1 by Danilo Segan
Factor out POFile gathering for bzr exports.
167
                    yield pofile
168
8657.8.1 by Jeroen Vermeulen
Script to commit translations to a branch.
169
    def _exportToBranch(self, source):
170
        """Export translations for source into source.translations_branch.
171
172
        :param source: a `ProductSeries`.
173
        """
174
        self.logger.info("Exporting %s." % source.title)
175
        self._checkForObjections(source)
14150.1.1 by Jeroen Vermeulen
Remove workaround for old bzr bug with stacked branches.d
176
        branch = source.translations_branch
8657.8.1 by Jeroen Vermeulen
Script to commit translations to a branch.
177
14134.5.2 by Jeroen Vermeulen
Request branch scan on the master store, not the slave store.
178
        branch = source.translations_branch
179
13084.5.4 by Aaron Bentley
Handle StaleLastMirrored by scheduling scan.
180
        try:
14150.1.1 by Jeroen Vermeulen
Remove workaround for old bzr bug with stacked branches.d
181
            committer = self._makeDirectBranchCommit(branch)
13084.5.6 by Aaron Bentley
Update from review.
182
        except StaleLastMirrored as e:
14134.5.2 by Jeroen Vermeulen
Request branch scan on the master store, not the slave store.
183
            # Request a rescan of the branch.  Do this on the master
184
            # store, or we won't be able to modify the branch object.
185
            # (The master copy may also be more recent, in which case
186
            # the rescan won't be necessary).
187
            master_branch = IMasterStore(branch).get(Branch, branch.id)
188
            master_branch.branchChanged(**get_db_branch_info(**e.info))
13084.5.4 by Aaron Bentley
Handle StaleLastMirrored by scheduling scan.
189
            self.logger.warning(
14434.1.1 by Jeroen Vermeulen
Log which branch the translations exporter fails to export to. And cosmetic changes.
190
                "Skipped %s due to stale DB info, and scheduled a new scan.",
14134.5.2 by Jeroen Vermeulen
Request branch scan on the master store, not the slave store.
191
                branch.bzr_identity)
13084.5.4 by Aaron Bentley
Handle StaleLastMirrored by scheduling scan.
192
            if self.txn:
193
                self.txn.commit()
194
            return
9375.3.1 by Jeroen Vermeulen
Extra logging & committing.
195
        self.logger.debug("Created DirectBranchCommit.")
196
        if self.txn:
197
            self.txn.commit()
8657.8.1 by Jeroen Vermeulen
Script to commit translations to a branch.
198
9426.6.1 by Jeroen Vermeulen
Don't export files that clearly haven't changed.
199
        bzr_branch = committer.bzrbranch
200
201
        last_commit_date = self._getLatestTranslationsCommit(bzr_branch)
202
203
        if last_commit_date is None:
204
            self.logger.debug("No previous translations commit found.")
205
            changed_since = None
206
        else:
207
            # Export files that have been touched since the last export.
208
            # Subtract a fudge factor because the last-export date marks
209
            # the end of the previous export, and the POFiles'
210
            # last-touched timestamp marks the beginning of the last
211
            # transaction that changed them.
212
            self.logger.debug("Last commit was at %s." % last_commit_date)
213
            changed_since = last_commit_date - self.fudge_factor
214
7675.328.1 by Jeroen Vermeulen
Fix failure in export-to-branches: bzr revision timestamp is a float.
215
        change_count = 0
216
8657.8.1 by Jeroen Vermeulen
Script to commit translations to a branch.
217
        try:
11942.1.1 by Danilo Segan
Factor out POFile gathering for bzr exports.
218
            for pofile in self._findChangedPOFiles(source, changed_since):
219
                base_path = os.path.dirname(pofile.potemplate.path)
220
221
                language_code = pofile.getFullLanguageCode()
222
                self.logger.debug("Exporting %s." % language_code)
223
224
                pofile_path = os.path.join(
225
                    base_path, language_code + '.po')
226
                pofile_contents = pofile.export()
227
228
                committer.writeFile(pofile_path, pofile_contents)
229
                change_count += 1
230
231
                # We're not actually writing any changes to the
232
                # database, but it's not polite to stay in one
233
                # transaction for too long.
234
                if self.txn:
235
                    self.txn.commit()
236
237
                # We're done with this POFile.  Don't bother caching
238
                # anything about it any longer.
239
                pofile.potemplate.clearPOFileCache()
9426.6.1 by Jeroen Vermeulen
Don't export files that clearly haven't changed.
240
7675.328.1 by Jeroen Vermeulen
Fix failure in export-to-branches: bzr revision timestamp is a float.
241
            if change_count > 0:
9375.3.3 by Jeroen Vermeulen
Merge devel
242
                self.logger.debug("Writing to branch.")
7675.328.1 by Jeroen Vermeulen
Fix failure in export-to-branches: bzr revision timestamp is a float.
243
                self._commit(source, committer)
8657.8.1 by Jeroen Vermeulen
Script to commit translations to a branch.
244
        finally:
245
            committer.unlock()
246
8871.3.5 by Jeroen Vermeulen
As per mwhudson's suggestion, keep bzrserver out of DirectBranchCommit entirely.
247
    def _exportToBranches(self, productseries_iter):
248
        """Loop over `productseries_iter` and export their translations."""
8657.8.1 by Jeroen Vermeulen
Script to commit translations to a branch.
249
        items_done = 0
250
        items_failed = 0
10914.2.1 by Jeroen Vermeulen
Email notifications for unpushed translations branches.
251
        unpushed_branches = 0
9019.1.1 by Jeroen Vermeulen
Commit more frequently, and don't iterate over result set across transactions.
252
253
        productseries = shortlist(productseries_iter, longest_expected=2000)
254
255
        for source in productseries:
8657.8.1 by Jeroen Vermeulen
Script to commit translations to a branch.
256
            try:
257
                self._exportToBranch(source)
9019.1.2 by Jeroen Vermeulen
Better keep committing after every productseries as well, just to be sure.
258
259
                if self.txn:
260
                    self.txn.commit()
7675.238.1 by Jeroen Vermeulen
Don't swallow KeyboardInterrupt/SystemExit.
261
            except (KeyboardInterrupt, SystemExit):
262
                raise
10914.2.1 by Jeroen Vermeulen
Email notifications for unpushed translations branches.
263
            except NotBranchError:
264
                unpushed_branches += 1
265
                if self.txn:
266
                    self.txn.abort()
267
                self._handleUnpushedBranch(source)
268
                if self.txn:
269
                    self.txn.commit()
14134.5.2 by Jeroen Vermeulen
Request branch scan on the master store, not the slave store.
270
            except Exception as e:
8657.8.1 by Jeroen Vermeulen
Script to commit translations to a branch.
271
                items_failed += 1
14434.1.1 by Jeroen Vermeulen
Log which branch the translations exporter fails to export to. And cosmetic changes.
272
                self.logger.error(
273
                    "Failure in %s/%s: %s", source.product.name, source.name,
274
                    repr(e))
8657.8.1 by Jeroen Vermeulen
Script to commit translations to a branch.
275
                if self.txn:
276
                    self.txn.abort()
277
278
            items_done += 1
279
10914.2.1 by Jeroen Vermeulen
Email notifications for unpushed translations branches.
280
        self.logger.info(
281
            "Processed %d item(s); %d failure(s), %d unpushed branch(es)." % (
282
                items_done, items_failed, unpushed_branches))
283
284
    def _sendMail(self, sender, recipients, subject, text):
10914.2.2 by Jeroen Vermeulen
Integration test.
285
        """Wrapper for `simple_sendmail`.  Fakeable for easy testing."""
10914.2.1 by Jeroen Vermeulen
Email notifications for unpushed translations branches.
286
        simple_sendmail(sender, recipients, subject, text)
287
288
    def _handleUnpushedBranch(self, productseries):
289
        """Branch has never been scanned.  Notify owner.
290
291
        This means that as far as the Launchpad database knows, there is
292
        no actual bzr branch behind this `IBranch` yet.
293
        """
294
        branch = productseries.translations_branch
10914.2.3 by Jeroen Vermeulen
More verbose logging.
295
        self.logger.info("Notifying %s of unpushed branch %s." % (
296
            branch.owner.name, branch.bzr_identity))
297
10914.2.1 by Jeroen Vermeulen
Email notifications for unpushed translations branches.
298
        template = get_email_template('unpushed-branch.txt', 'translations')
299
        text = template % {
300
            'productseries': productseries.title,
301
            'branch_url': branch.bzr_identity,
302
        }
303
        recipients = get_contact_email_addresses(branch.owner)
304
        sender = format_address(
305
            "Launchpad Translations", config.canonical.noreply_from_address)
306
        subject = "Launchpad: translations branch has not been set up."
307
        self._sendMail(sender, recipients, subject, text)
8871.3.1 by Jeroen Vermeulen
Implemented changes whispered in my ear by mwhudson.
308
309
    def main(self):
310
        """See `LaunchpadScript`."""
311
        # Avoid circular imports.
312
        from lp.registry.model.product import Product
313
        from lp.registry.model.productseries import ProductSeries
314
12143.1.1 by Danilo Segan
Set config section and OOPS prefix for translations_export_to_branch.
315
        errorlog.globalErrorUtility.configure(self.config_name)
7675.328.1 by Jeroen Vermeulen
Fix failure in export-to-branches: bzr revision timestamp is a float.
316
        if self.options.no_fudge:
317
            self.fudge_factor = timedelta(0)
318
12143.1.2 by Danilo Segan
Clean-up imports.
319
        self.logger.info("Exporting to translations branches.")
8871.3.1 by Jeroen Vermeulen
Implemented changes whispered in my ear by mwhudson.
320
321
        self.store = getUtility(IStoreSelector).get(MAIN_STORE, SLAVE_FLAVOR)
322
323
        product_join = Join(
324
            ProductSeries, Product, ProductSeries.product == Product.id)
325
        productseries = self.store.using(product_join).find(
7675.909.5 by j.c.sackett
Slew of fixes per review.
326
            ProductSeries,
7675.913.9 by j.c.sackett
Lint fixes.
327
            And(
14120.2.4 by Danilo Segan
Replace all references to _translation_usage with translation_usage.
328
                Product.translations_usage == ServiceUsage.LAUNCHPAD,
7675.913.15 by j.c.sackett
Fixed conditions on one method.
329
                ProductSeries.translations_branch != None))
7675.909.14 by j.c.sackett
Fixed a test.
330
8871.3.1 by Jeroen Vermeulen
Implemented changes whispered in my ear by mwhudson.
331
        # Anything deterministic will do, and even that is only for
332
        # testing.
333
        productseries = productseries.order_by(ProductSeries.id)
334
9590.1.103 by Michael Hudson
fix the other use of map_branch_contents
335
        bzrserver = get_rw_server()
10197.5.10 by Michael Hudson
more...
336
        bzrserver.start_server()
8871.3.1 by Jeroen Vermeulen
Implemented changes whispered in my ear by mwhudson.
337
        try:
338
            self._exportToBranches(productseries)
339
        finally:
10197.5.10 by Michael Hudson
more...
340
            bzrserver.stop_server()