Merge lp:~sinzui/launchpad/reimport-inactive-templates into lp:launchpad

Proposed by Curtis Hovey on 2012-10-31
Status: Merged
Approved by: j.c.sackett on 2012-11-01
Approved revision: no longer in the source branch.
Merged at revision: 16223
Proposed branch: lp:~sinzui/launchpad/reimport-inactive-templates
Merge into: lp:launchpad
Diff against target: 168 lines (+85/-1)
6 files modified
lib/lp/translations/interfaces/potemplate.py (+14/-1)
lib/lp/translations/model/approver.py (+9/-0)
lib/lp/translations/model/potemplate.py (+19/-0)
lib/lp/translations/tests/test_potemplate.py (+14/-0)
lib/lp/translations/tests/test_translationbranchapprover.py (+12/-0)
lib/lp/translations/tests/test_translationbuildapprover.py (+17/-0)
To merge this branch: bzr merge lp:~sinzui/launchpad/reimport-inactive-templates
Reviewer Review Type Date Requested Status
j.c.sackett (community) 2012-10-31 Approve on 2012-11-01
Review via email: mp+132421@code.launchpad.net

Commit Message

Do not recreate inactive translation templates.

Description of the Change

I created OOPS-91882635e8551f53a0bd423d85dadcd5 while investigating
question #211684. The series has inactive templates
https://translations.launchpad.net/openteacher/3.x/+templates
and translation syncing is not seeing the two new POTS. I requested a
one-time import to see if a rescan would find the missing pots. Instead
the log shows an oops because org.openteacher.dialogs.updates is
inactive.
    IntegrityError: duplicate key value violates unique constraint
        "potemplate__productseries__name__key"
    DETAIL: Key (productseries, name)=(40409, org.openteacher.dialogs.updates)
    already exists

--------------------------------------------------------------------

RULES

    Pre-implementation: no one
    * POTemplates are unique to (productseries, name) and
      (distroseries, sourcepackagename, name). The are created by calling
      POTemplateSubset.new().
    * Calls to new() assumes that the set represents the full
      set of templates in a series, but since the set can be initialised
      with iscurrent=True, all the inactive POTemplates are ignored.
    * A bug was fixed 2 years ago that requires the set to iscurrent=True
      to properly work with the found files, but the calls to make new
      POTemplates still think the set is unfiltered.
    * Add a guard to POTemplateSubset.new() that checks the unfiltered
      series-package set, eg iscurrent=None, and raise ValueError to
      ensure we see the TB, not the Postgresql transaction error.
    * POTemplateSubset.new() is called in several places in approver.py,
      and each call is using a different strategy to determine if it
      must create a new template. Some strategies do not know the name
      unitl late in the process.
    * Before calling new(), the code must check if the proposed series,
      package, name combination exists and choose an exit strategy if
      it does.

QA

    * Visit https://translations.qastaging.launchpad.net/openteacher/3.x
    * Choose "Change synchronization settings", then
      "You can request a one-time import.", then "Request a one-time import"
    * Ask a webops to run cronscripts/rosetta-branches.py on qastaging.
    * Ask for the output of check there there is no oopse for "teacher".

LINT

    lib/lp/translations/interfaces/potemplate.py
    lib/lp/translations/model/approver.py
    lib/lp/translations/model/potemplate.py
    lib/lp/translations/tests/test_potemplate.py
    lib/lp/translations/tests/test_translationbranchapprover.py
    lib/lp/translations/tests/test_translationbuildapprover.py

To post a comment you must log in.
j.c.sackett (jcsackett) wrote :

Looks good. Thanks, Curtis.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/translations/interfaces/potemplate.py'
2--- lib/lp/translations/interfaces/potemplate.py 2011-12-24 16:54:44 +0000
3+++ lib/lp/translations/interfaces/potemplate.py 2012-11-01 15:53:21 +0000
4@@ -599,8 +599,21 @@
5 def __getitem__(name):
6 """Get a POTemplate by its name."""
7
8+ def isNameUnique(name):
9+ """Is the IPOTemplate name unique to the series (and package).
10+
11+ The subset may only include active `IPOTemplate` objects
12+ (iscurrent=True), but the full set that constrains creating new
13+ templates includes inactive templates too. Use this method to
14+ verify that an `IPOTemplate` can be created before calling new().
15+ """
16+
17 def new(name, translation_domain, path, owner, copy_pofiles=True):
18- """Create a new template for the context of this Subset."""
19+ """Create a new template for the context of this Subset.
20+
21+ The name must be unique to the full subset of active and inactive
22+ templates in a series (and package). See `isNameUnique`.
23+ """
24
25 def getPOTemplateByName(name):
26 """Return the `IPOTemplate` with the given name or None.
27
28=== modified file 'lib/lp/translations/model/approver.py'
29--- lib/lp/translations/model/approver.py 2011-05-27 19:53:20 +0000
30+++ lib/lp/translations/model/approver.py 2012-11-01 15:53:21 +0000
31@@ -141,6 +141,9 @@
32 return entry
33 # No (possibly) matching entry found: create one.
34 name = make_name(domain)
35+ if not self._potemplateset.isNameUnique(name):
36+ # The name probably matches an inactive template.
37+ return entry
38 potemplate = self._potemplateset.new(
39 name, domain, entry.path, entry.importer)
40 self._potemplates[entry.path] = potemplate
41@@ -206,6 +209,9 @@
42 else:
43 domain = self._potemplateset.sourcepackagename.name
44 name = domain
45+ if not self._potemplateset.isNameUnique(name):
46+ # The name probably matches an inactive template.
47+ return None
48 return self._potemplateset.new(name, domain, path, self.owner)
49 elif potemplateset_size == 1:
50 # Use the one template that is there.
51@@ -240,6 +246,9 @@
52 potemplate = self._potemplateset.getPOTemplateByName(name)
53 if potemplate is None:
54 # Still no template found, create a new one.
55+ if not self._potemplateset.isNameUnique(name):
56+ # The name probably matches an inactive template.
57+ return None
58 potemplate = self._potemplateset.new(
59 name, domain, path, self.owner)
60 return potemplate
61
62=== modified file 'lib/lp/translations/model/potemplate.py'
63--- lib/lp/translations/model/potemplate.py 2012-09-26 08:09:31 +0000
64+++ lib/lp/translations/model/potemplate.py 2012-11-01 15:53:21 +0000
65@@ -1181,8 +1181,27 @@
66 # Do not continue, else it would trigger an existingpo assertion.
67 return
68
69+ def _getSuperSet(self):
70+ """Return the set of all POTemplates for this series and package."""
71+ if self.iscurrent is None:
72+ return self
73+ else:
74+ return getUtility(IPOTemplateSet).getSubset(
75+ productseries=self.productseries,
76+ distroseries=self.distroseries,
77+ sourcepackagename=self.sourcepackagename)
78+
79+ def isNameUnique(self, name):
80+ """See `IPOTemplateSubset`."""
81+ return self._getSuperSet().getPOTemplateByName(name) is None
82+
83 def new(self, name, translation_domain, path, owner, copy_pofiles=True):
84 """See `IPOTemplateSubset`."""
85+ existing_template = self._getSuperSet().getPOTemplateByName(name)
86+ if existing_template is not None:
87+ raise ValueError(
88+ 'POTempate %s already exists and is iscurrent=%s' %
89+ (name, existing_template.iscurrent))
90 header_params = {
91 'origin': 'PACKAGE VERSION',
92 'templatedate': datetime.datetime.now(),
93
94=== modified file 'lib/lp/translations/tests/test_potemplate.py'
95--- lib/lp/translations/tests/test_potemplate.py 2012-01-01 02:58:52 +0000
96+++ lib/lp/translations/tests/test_potemplate.py 2012-11-01 15:53:21 +0000
97@@ -847,6 +847,20 @@
98
99 self.assertEqual(templates, found_templates)
100
101+ def test_isNameUnique(self):
102+ # The isNameUnique method ignored the iscurrent filter to provide
103+ # an authoritative answer to whether a new template can be created
104+ # with the name.
105+ series = self.factory.makeProductSeries()
106+ self.factory.makePOTemplate(productseries=series, name='cat')
107+ self.factory.makePOTemplate(
108+ productseries=series, name='dog', iscurrent=False)
109+ potset = getUtility(IPOTemplateSet)
110+ subset = potset.getSubset(productseries=series, iscurrent=True)
111+ self.assertFalse(subset.isNameUnique('cat'))
112+ self.assertFalse(subset.isNameUnique('dog'))
113+ self.assertTrue(subset.isNameUnique('fnord'))
114+
115 def test_getPOTemplatesByTranslationDomain_returns_result_set(self):
116 subset = getUtility(IPOTemplateSet).getSubset(
117 productseries=self.factory.makeProductSeries())
118
119=== modified file 'lib/lp/translations/tests/test_translationbranchapprover.py'
120--- lib/lp/translations/tests/test_translationbranchapprover.py 2012-01-20 15:42:44 +0000
121+++ lib/lp/translations/tests/test_translationbranchapprover.py 2012-11-01 15:53:21 +0000
122@@ -151,6 +151,18 @@
123 self._createApprover(template_path).approve(entry)
124 self.assertEqual(potemplate, entry.potemplate)
125
126+ def test_ignore_existing_inactive_potemplate(self):
127+ # When replacing an existing inactive template, the entry is not
128+ # approved and no template is created for it.
129+ translation_domain = self.factory.getUniqueString()
130+ template_path = translation_domain + u'.pot'
131+ potemplate = self._createTemplate(template_path, translation_domain)
132+ potemplate.setActive(False)
133+ entry = self._upload_file(template_path)
134+ self._createApprover(template_path).approve(entry)
135+ self.assertEqual(RosettaImportStatus.NEEDS_REVIEW, entry.status)
136+ self.assertEqual(None, entry.potemplate)
137+
138 def test_replace_existing_any_path(self):
139 # If just one template file is found in the tree and just one
140 # POTemplate is in the database, the upload is always approved.
141
142=== modified file 'lib/lp/translations/tests/test_translationbuildapprover.py'
143--- lib/lp/translations/tests/test_translationbuildapprover.py 2012-01-20 16:11:11 +0000
144+++ lib/lp/translations/tests/test_translationbuildapprover.py 2012-11-01 15:53:21 +0000
145@@ -127,6 +127,23 @@
146 self.assertEqual('domain3', entries[2].potemplate.name)
147 self.assertEqual('domain4', entries[3].potemplate.name)
148
149+ def test_approve_inactive_existing(self):
150+ # Inactive templates are approved, but they remain inactive.
151+ filenames = [
152+ 'po-domain1/domain1.pot',
153+ ]
154+ series = self.factory.makeProductSeries()
155+ domain1_pot = self.factory.makePOTemplate(
156+ productseries=series, name='domain1', iscurrent=False)
157+ self._becomeBuilddMaster()
158+ approver = TranslationBuildApprover(filenames, productseries=series)
159+ entries = self._makeApprovedEntries(series, approver, filenames)
160+ self.assertEqual(
161+ [RosettaImportStatus.APPROVED],
162+ [entry.status for entry in entries])
163+ self.assertEqual(
164+ [domain1_pot], [entry.potemplate for entry in entries])
165+
166 def test_approve_generic_name_one_new(self):
167 # Generic names are OK, if there is only one.
168 filenames = [