Merge lp:~wgrant/launchpad/bfj-mixin into lp:launchpad

Proposed by William Grant
Status: Merged
Approved by: Steve Kowalik
Approved revision: no longer in the source branch.
Merged at revision: 16405
Proposed branch: lp:~wgrant/launchpad/bfj-mixin
Merge into: lp:launchpad
Diff against target: 577 lines (+171/-184)
8 files modified
lib/lp/buildmaster/doc/buildfarmjob.txt (+0/-15)
lib/lp/buildmaster/model/buildfarmjob.py (+29/-25)
lib/lp/buildmaster/model/packagebuild.py (+20/-27)
lib/lp/buildmaster/tests/test_buildfarmjob.py (+46/-47)
lib/lp/buildmaster/tests/test_packagebuild.py (+67/-66)
lib/lp/code/model/sourcepackagerecipebuild.py (+2/-1)
lib/lp/soyuz/model/binarypackagebuild.py (+2/-1)
lib/lp/translations/model/translationtemplatesbuild.py (+5/-2)
To merge this branch: bzr merge lp:~wgrant/launchpad/bfj-mixin
Reviewer Review Type Date Requested Status
Steve Kowalik (community) code Approve
Review via email: mp+142055@code.launchpad.net

Commit message

Extract all methods from PackageBuild(Derived)/BuildFarmJob(Derived) to separate mixins. The BFJ/PB classes are now basically free of logic, so we can begin the flattening.

Description of the change

More preparation for the BFJ+PB flattening. BuildFarmJob and PackageBuild are now extraordinarily boring objects, just containing the minimal methods for creating and destroying the basic objects. All the notable logic is now in BuildFarmJobMixin and PackageBuildMixin, permitting us to soon redirect attribute writes on BPB/SPRB/TTB to multiple columns for the migration.

BuildFarmJob(Derived) and PackageBuild(Derived) will die once the migration is complete. *Mixin will be the only remnants.

I had to shuffle some tests around to make them test *Mixin methods on a concrete implementation.

To post a comment you must log in.
Revision history for this message
Steve Kowalik (stevenk) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== removed file 'lib/lp/buildmaster/doc/buildfarmjob.txt'
2--- lib/lp/buildmaster/doc/buildfarmjob.txt 2013-01-04 06:20:06 +0000
3+++ lib/lp/buildmaster/doc/buildfarmjob.txt 1970-01-01 00:00:00 +0000
4@@ -1,15 +0,0 @@
5-BuildFarmJob
6-============
7-
8- >>> from lp.buildmaster.interfaces.buildfarmjob import (
9- ... IBuildFarmJob)
10- >>> from lp.buildmaster.enums import BuildFarmJobType
11- >>> from lp.buildmaster.model.buildfarmjob import BuildFarmJob
12-
13-BuildFarmJob provides a basic implementation of IBuildFarmJob. The
14-specific build farm job classes will automatically delegate to
15-BuildFarmJob by inheriting BuildFarmJobDerived.
16-
17- >>> buildfarmjob = BuildFarmJob(BuildFarmJobType.PACKAGEBUILD)
18- >>> verifyObject(IBuildFarmJob, buildfarmjob)
19- True
20
21=== modified file 'lib/lp/buildmaster/model/buildfarmjob.py'
22--- lib/lp/buildmaster/model/buildfarmjob.py 2013-01-04 07:52:40 +0000
23+++ lib/lp/buildmaster/model/buildfarmjob.py 2013-01-07 04:25:24 +0000
24@@ -5,6 +5,7 @@
25 __all__ = [
26 'BuildFarmJob',
27 'BuildFarmJobDerived',
28+ 'BuildFarmJobMixin',
29 'BuildFarmJobOld',
30 'BuildFarmJobOldDerived',
31 ]
32@@ -278,6 +279,34 @@
33 store.add(build_farm_job)
34 return build_farm_job
35
36+ def getSpecificJob(self):
37+ """See `IBuild`"""
38+ # Adapt ourselves based on our job type.
39+ try:
40+ source = getUtility(
41+ ISpecificBuildFarmJobSource, self.job_type.name)
42+ except ComponentLookupError:
43+ raise InconsistentBuildFarmJobError(
44+ "No source was found for the build farm job type %s." % (
45+ self.job_type.name))
46+
47+ build = source.getByBuildFarmJob(self)
48+
49+ if build is None:
50+ raise InconsistentBuildFarmJobError(
51+ "There is no related specific job for the build farm "
52+ "job with id %d." % self.id)
53+
54+ # Just to be on the safe side, make sure the build is still
55+ # proxied before returning it.
56+ assert isProxy(build), (
57+ "Unproxied result returned from ISpecificBuildFarmJobSource.")
58+
59+ return build
60+
61+
62+class BuildFarmJobMixin:
63+
64 @property
65 def title(self):
66 """See `IBuildFarmJob`."""
67@@ -327,31 +356,6 @@
68 BuildStatus.UPLOADING,
69 BuildStatus.SUPERSEDED]
70
71- def getSpecificJob(self):
72- """See `IBuild`"""
73- # Adapt ourselves based on our job type.
74- try:
75- source = getUtility(
76- ISpecificBuildFarmJobSource, self.job_type.name)
77- except ComponentLookupError:
78- raise InconsistentBuildFarmJobError(
79- "No source was found for the build farm job type %s." % (
80- self.job_type.name))
81-
82- build = source.getByBuildFarmJob(self)
83-
84- if build is None:
85- raise InconsistentBuildFarmJobError(
86- "There is no related specific job for the build farm "
87- "job with id %d." % self.id)
88-
89- # Just to be on the safe side, make sure the build is still
90- # proxied before returning it.
91- assert isProxy(build), (
92- "Unproxied result returned from ISpecificBuildFarmJobSource.")
93-
94- return build
95-
96 def gotFailure(self):
97 """See `IBuildFarmJob`."""
98 self.failure_count += 1
99
100=== modified file 'lib/lp/buildmaster/model/packagebuild.py'
101--- lib/lp/buildmaster/model/packagebuild.py 2012-10-31 23:03:26 +0000
102+++ lib/lp/buildmaster/model/packagebuild.py 2013-01-07 04:25:24 +0000
103@@ -5,6 +5,7 @@
104 __all__ = [
105 'PackageBuild',
106 'PackageBuildDerived',
107+ 'PackageBuildMixin',
108 'PackageBuildSet',
109 ]
110
111@@ -43,6 +44,7 @@
112 )
113 from lp.buildmaster.model.buildfarmjob import (
114 BuildFarmJob,
115+ BuildFarmJobMixin,
116 BuildFarmJobDerived,
117 )
118 from lp.buildmaster.model.buildqueue import BuildQueue
119@@ -128,6 +130,15 @@
120 store.remove(self)
121 store.remove(build_farm_job)
122
123+
124+class PackageBuildMixin(BuildFarmJobMixin):
125+
126+ # The list of build status values for which email notifications are
127+ # allowed to be sent. It is up to each callback as to whether it will
128+ # consider sending a notification but it won't do so if the status is not
129+ # in this list.
130+ ALLOWED_STATUS_NOTIFICATIONS = ['OK', 'PACKAGEFAIL', 'CHROOTFAIL']
131+
132 @property
133 def current_component(self):
134 """See `IPackageBuild`."""
135@@ -234,37 +245,10 @@
136 """See `IPackageBuild`."""
137 raise NotImplementedError
138
139- def handleStatus(self, status, librarian, slave_status):
140- """See `IPackageBuild`."""
141- raise NotImplementedError
142-
143- def queueBuild(self, suspended=False):
144- """See `IPackageBuild`."""
145- raise NotImplementedError
146-
147- def getBuildCookie(self):
148- """See `IPackageBuild`."""
149- raise NotImplementedError
150-
151 def getUploader(self, changes):
152 """See `IPackageBuild`."""
153 raise NotImplementedError
154
155-
156-class PackageBuildDerived:
157- """Setup the delegation for package build.
158-
159- This class also provides some common implementation for handling
160- build status.
161- """
162- delegates(IPackageBuild, context="package_build")
163-
164- # The list of build status values for which email notifications are
165- # allowed to be sent. It is up to each callback as to whether it will
166- # consider sending a notification but it won't do so if the status is not
167- # in this list.
168- ALLOWED_STATUS_NOTIFICATIONS = ['OK', 'PACKAGEFAIL', 'CHROOTFAIL']
169-
170 def getBuildCookie(self):
171 """See `IPackageBuild`."""
172 return '%s-%s' % (self.job_type.name, self.id)
173@@ -520,6 +504,15 @@
174 return d.addCallback(build_info_stored)
175
176
177+class PackageBuildDerived:
178+ """Setup the delegation for package build.
179+
180+ This class also provides some common implementation for handling
181+ build status.
182+ """
183+ delegates(IPackageBuild, context="package_build")
184+
185+
186 class PackageBuildSet:
187 implements(IPackageBuildSet)
188
189
190=== modified file 'lib/lp/buildmaster/tests/test_buildfarmjob.py'
191--- lib/lp/buildmaster/tests/test_buildfarmjob.py 2013-01-04 06:25:25 +0000
192+++ lib/lp/buildmaster/tests/test_buildfarmjob.py 2013-01-07 04:25:24 +0000
193@@ -40,13 +40,13 @@
194 )
195
196
197-class TestBuildFarmJobMixin:
198+class TestBuildFarmJobBase:
199
200 layer = DatabaseFunctionalLayer
201
202 def setUp(self):
203 """Create a build farm job with which to test."""
204- super(TestBuildFarmJobMixin, self).setUp()
205+ super(TestBuildFarmJobBase, self).setUp()
206 self.build_farm_job = self.makeBuildFarmJob()
207
208 def makeBuildFarmJob(self, builder=None,
209@@ -68,13 +68,9 @@
210 return build_farm_job
211
212
213-class TestBuildFarmJob(TestBuildFarmJobMixin, TestCaseWithFactory):
214+class TestBuildFarmJob(TestBuildFarmJobBase, TestCaseWithFactory):
215 """Tests for the build farm job object."""
216
217- def test_providesInterface(self):
218- # BuildFarmJob provides IBuildFarmJob
219- self.assertProvides(self.build_farm_job, IBuildFarmJob)
220-
221 def test_saves_record(self):
222 # A build farm job can be stored in the database.
223 flush_database_updates()
224@@ -105,19 +101,46 @@
225 self.assertEqual(None, self.build_farm_job.date_first_dispatched)
226 self.assertEqual(None, self.build_farm_job.builder)
227 self.assertEqual(None, self.build_farm_job.log)
228- self.assertEqual(None, self.build_farm_job.log_url)
229- self.assertEqual(None, self.build_farm_job.buildqueue_record)
230-
231- def test_unimplemented_methods(self):
232- # A build farm job leaves the implementation of various
233- # methods for derived classes.
234- self.assertRaises(NotImplementedError, self.build_farm_job.makeJob)
235-
236- def test_title(self):
237- # The default title simply uses the job type's title.
238- self.assertEqual(
239- self.build_farm_job.job_type.title,
240- self.build_farm_job.title)
241+
242+ def test_getSpecificJob_none(self):
243+ # An exception is raised if there is no related specific job.
244+ self.assertRaises(
245+ InconsistentBuildFarmJobError, self.build_farm_job.getSpecificJob)
246+
247+ def test_getSpecificJob_unimplemented_type(self):
248+ # An `IBuildFarmJob` with an unimplemented type results in an
249+ # exception.
250+ removeSecurityProxy(self.build_farm_job).job_type = (
251+ BuildFarmJobType.RECIPEBRANCHBUILD)
252+
253+ self.assertRaises(
254+ InconsistentBuildFarmJobError, self.build_farm_job.getSpecificJob)
255+
256+ def test_date_created(self):
257+ # date_created can be passed optionally when creating a
258+ # bulid farm job to ensure we don't get identical timestamps
259+ # when transactions are committed.
260+ ten_years_ago = datetime.now(pytz.UTC) - timedelta(365 * 10)
261+ build_farm_job = getUtility(IBuildFarmJobSource).new(
262+ job_type=BuildFarmJobType.PACKAGEBUILD,
263+ date_created=ten_years_ago)
264+ self.failUnlessEqual(ten_years_ago, build_farm_job.date_created)
265+
266+
267+class TestBuildFarmJobMixin(TestCaseWithFactory):
268+ """Test methods provided by BuildFarmJobMixin."""
269+
270+ layer = DatabaseFunctionalLayer
271+
272+ def setUp(self):
273+ super(TestBuildFarmJobMixin, self).setUp()
274+ # BuildFarmJobMixin only operates as part of a concrete
275+ # IBuildFarmJob implementation. Here we use BinaryPackageBuild.
276+ self.build_farm_job = self.factory.makeBinaryPackageBuild()
277+
278+ def test_providesInterface(self):
279+ # BuildFarmJobMixin derivatives provide IBuildFarmJob
280+ self.assertProvides(self.build_farm_job, IBuildFarmJob)
281
282 def test_duration_none(self):
283 # If either start or finished is none, the duration will be
284@@ -141,32 +164,8 @@
285 naked_bfj.date_finished = now + duration
286 self.failUnlessEqual(duration, self.build_farm_job.duration)
287
288- def test_date_created(self):
289- # date_created can be passed optionally when creating a
290- # bulid farm job to ensure we don't get identical timestamps
291- # when transactions are committed.
292- ten_years_ago = datetime.now(pytz.UTC) - timedelta(365 * 10)
293- build_farm_job = getUtility(IBuildFarmJobSource).new(
294- job_type=BuildFarmJobType.PACKAGEBUILD,
295- date_created=ten_years_ago)
296- self.failUnlessEqual(ten_years_ago, build_farm_job.date_created)
297-
298- def test_getSpecificJob_none(self):
299- # An exception is raised if there is no related specific job.
300- self.assertRaises(
301- InconsistentBuildFarmJobError, self.build_farm_job.getSpecificJob)
302-
303- def test_getSpecificJob_unimplemented_type(self):
304- # An `IBuildFarmJob` with an unimplemented type results in an
305- # exception.
306- removeSecurityProxy(self.build_farm_job).job_type = (
307- BuildFarmJobType.RECIPEBRANCHBUILD)
308-
309- self.assertRaises(
310- InconsistentBuildFarmJobError, self.build_farm_job.getSpecificJob)
311-
312-
313-class TestBuildFarmJobSecurity(TestBuildFarmJobMixin, TestCaseWithFactory):
314+
315+class TestBuildFarmJobSecurity(TestBuildFarmJobBase, TestCaseWithFactory):
316
317 def test_view_build_farm_job(self):
318 # Anonymous access can read public builds, but not edit.
319@@ -184,7 +183,7 @@
320 BuildStatus.FULLYBUILT, self.build_farm_job.status)
321
322
323-class TestBuildFarmJobSet(TestBuildFarmJobMixin, TestCaseWithFactory):
324+class TestBuildFarmJobSet(TestBuildFarmJobBase, TestCaseWithFactory):
325
326 layer = LaunchpadFunctionalLayer
327
328
329=== modified file 'lib/lp/buildmaster/tests/test_packagebuild.py'
330--- lib/lp/buildmaster/tests/test_packagebuild.py 2013-01-04 06:25:25 +0000
331+++ lib/lp/buildmaster/tests/test_packagebuild.py 2013-01-07 04:25:24 +0000
332@@ -78,10 +78,6 @@
333 joes_ppa = self.factory.makeArchive(owner=joe, name="ppa")
334 self.package_build = self.makePackageBuild(archive=joes_ppa)
335
336- def test_providesInterface(self):
337- # PackageBuild provides IPackageBuild
338- self.assertProvides(self.package_build, IPackageBuild)
339-
340 def test_saves_record(self):
341 # A package build can be stored in the database.
342 store = Store.of(self.package_build)
343@@ -91,25 +87,74 @@
344 PackageBuild.id == self.package_build.id).one()
345 self.assertEqual(self.package_build, retrieved_build)
346
347- def test_unimplemented_methods(self):
348- # Classes deriving from PackageBuild must provide various
349- # methods.
350- self.assertRaises(
351- NotImplementedError, self.package_build.estimateDuration)
352- self.assertRaises(
353- NotImplementedError, self.package_build.verifySuccessfulUpload)
354- self.assertRaises(NotImplementedError, self.package_build.notify)
355- self.assertRaises(
356- NotImplementedError, self.package_build.handleStatus,
357- None, None, None)
358-
359 def test_default_values(self):
360 # PackageBuild has a number of default values.
361- self.failUnlessEqual(
362- 'multiverse', self.package_build.current_component.name)
363 self.failUnlessEqual(None, self.package_build.distribution)
364 self.failUnlessEqual(None, self.package_build.distro_series)
365
366+ def test_destroySelf_removes_BuildFarmJob(self):
367+ # Destroying a packagebuild also destroys the BuildFarmJob it
368+ # references.
369+ naked_build = removeSecurityProxy(self.package_build)
370+ store = Store.of(self.package_build)
371+ # Ensure build_farm_job_id is set.
372+ store.flush()
373+ build_farm_job_id = naked_build.build_farm_job_id
374+ naked_build.destroySelf()
375+ result = store.find(
376+ BuildFarmJob, BuildFarmJob.id == build_farm_job_id)
377+ self.assertIs(None, result.one())
378+
379+ def test_view_package_build(self):
380+ # Anonymous access can read public builds, but not edit.
381+ self.failUnlessEqual(
382+ None, self.package_build.dependencies)
383+ self.assertRaises(
384+ Unauthorized, setattr, self.package_build,
385+ 'dependencies', u'my deps')
386+
387+ def test_edit_package_build(self):
388+ # An authenticated user who belongs to the owning archive team
389+ # can edit the build.
390+ login_person(self.package_build.archive.owner)
391+ self.package_build.dependencies = u'My deps'
392+ self.failUnlessEqual(
393+ u'My deps', self.package_build.dependencies)
394+
395+ # But other users cannot.
396+ other_person = self.factory.makePerson()
397+ login_person(other_person)
398+ self.assertRaises(
399+ Unauthorized, setattr, self.package_build,
400+ 'dependencies', u'my deps')
401+
402+ def test_admin_package_build(self):
403+ # Users with edit access can update attributes.
404+ login('admin@canonical.com')
405+ self.package_build.dependencies = u'My deps'
406+ self.failUnlessEqual(
407+ u'My deps', self.package_build.dependencies)
408+
409+
410+class TestPackageBuildMixin(TestCaseWithFactory):
411+ """Test methods provided by PackageBuildMixin."""
412+
413+ layer = LaunchpadFunctionalLayer
414+
415+ def setUp(self):
416+ super(TestPackageBuildMixin, self).setUp()
417+ # BuildFarmJobMixin only operates as part of a concrete
418+ # IBuildFarmJob implementation. Here we use
419+ # SourcePackageRecipeBuild.
420+ joe = self.factory.makePerson(name="joe")
421+ joes_ppa = self.factory.makeArchive(owner=joe, name="ppa")
422+ self.package_build = self.factory.makeSourcePackageRecipeBuild(
423+ archive=joes_ppa)
424+
425+ def test_providesInterface(self):
426+ # PackageBuild provides IPackageBuild
427+ self.assertProvides(self.package_build, IPackageBuild)
428+
429 def test_log_url(self):
430 # The url of the build log file is determined by the PackageBuild.
431 lfa = self.factory.makeLibraryFileAlias('mybuildlog.txt')
432@@ -117,8 +162,8 @@
433 log_url = self.package_build.log_url
434 self.failUnlessEqual(
435 'http://launchpad.dev/~joe/'
436- '+archive/ppa/+build/%d/+files/mybuildlog.txt' % (
437- self.package_build.build_farm_job.id),
438+ '+archive/ppa/+recipebuild/%d/+files/mybuildlog.txt' % (
439+ self.package_build.id),
440 log_url)
441
442 def test_storeUploadLog(self):
443@@ -152,45 +197,14 @@
444 def test_upload_log_url(self):
445 # The url of the upload log file is determined by the PackageBuild.
446 Store.of(self.package_build).flush()
447- build_id = self.package_build.build_farm_job.id
448 self.package_build.storeUploadLog("Some content")
449 log_url = self.package_build.upload_log_url
450 self.failUnlessEqual(
451 'http://launchpad.dev/~joe/'
452- '+archive/ppa/+build/%d/+files/upload_%d_log.txt' % (
453- build_id, build_id),
454+ '+archive/ppa/+recipebuild/%d/+files/upload_%d_log.txt' % (
455+ self.package_build.id, self.package_build.build_farm_job.id),
456 log_url)
457
458- def test_view_package_build(self):
459- # Anonymous access can read public builds, but not edit.
460- self.failUnlessEqual(
461- None, self.package_build.dependencies)
462- self.assertRaises(
463- Unauthorized, setattr, self.package_build,
464- 'dependencies', u'my deps')
465-
466- def test_edit_package_build(self):
467- # An authenticated user who belongs to the owning archive team
468- # can edit the build.
469- login_person(self.package_build.archive.owner)
470- self.package_build.dependencies = u'My deps'
471- self.failUnlessEqual(
472- u'My deps', self.package_build.dependencies)
473-
474- # But other users cannot.
475- other_person = self.factory.makePerson()
476- login_person(other_person)
477- self.assertRaises(
478- Unauthorized, setattr, self.package_build,
479- 'dependencies', u'my deps')
480-
481- def test_admin_package_build(self):
482- # Users with edit access can update attributes.
483- login('admin@canonical.com')
484- self.package_build.dependencies = u'My deps'
485- self.failUnlessEqual(
486- u'My deps', self.package_build.dependencies)
487-
488 def test_getUploadDirLeaf(self):
489 # getUploadDirLeaf returns the current time, followed by the build
490 # cookie.
491@@ -202,19 +216,6 @@
492 '%s-%s' % (now.strftime("%Y%m%d-%H%M%S"), build_cookie),
493 upload_leaf)
494
495- def test_destroySelf_removes_BuildFarmJob(self):
496- # Destroying a packagebuild also destroys the BuildFarmJob it
497- # references.
498- naked_build = removeSecurityProxy(self.package_build)
499- store = Store.of(self.package_build)
500- # Ensure build_farm_job_id is set.
501- store.flush()
502- build_farm_job_id = naked_build.build_farm_job_id
503- naked_build.destroySelf()
504- result = store.find(
505- BuildFarmJob, BuildFarmJob.id == build_farm_job_id)
506- self.assertIs(None, result.one())
507-
508
509 class TestPackageBuildSet(TestPackageBuildBase):
510
511
512=== modified file 'lib/lp/code/model/sourcepackagerecipebuild.py'
513--- lib/lp/code/model/sourcepackagerecipebuild.py 2013-01-07 02:40:55 +0000
514+++ lib/lp/code/model/sourcepackagerecipebuild.py 2013-01-07 04:25:24 +0000
515@@ -41,6 +41,7 @@
516 from lp.buildmaster.model.packagebuild import (
517 PackageBuild,
518 PackageBuildDerived,
519+ PackageBuildMixin,
520 )
521 from lp.code.errors import (
522 BuildAlreadyPending,
523@@ -74,7 +75,7 @@
524 from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease
525
526
527-class SourcePackageRecipeBuild(PackageBuildDerived, Storm):
528+class SourcePackageRecipeBuild(PackageBuildMixin, PackageBuildDerived, Storm):
529
530 __storm_table__ = 'SourcePackageRecipeBuild'
531
532
533=== modified file 'lib/lp/soyuz/model/binarypackagebuild.py'
534--- lib/lp/soyuz/model/binarypackagebuild.py 2013-01-02 06:05:27 +0000
535+++ lib/lp/soyuz/model/binarypackagebuild.py 2013-01-07 04:25:24 +0000
536@@ -45,6 +45,7 @@
537 from lp.buildmaster.model.packagebuild import (
538 PackageBuild,
539 PackageBuildDerived,
540+ PackageBuildMixin,
541 )
542 from lp.services.config import config
543 from lp.services.database.bulk import load_related
544@@ -92,7 +93,7 @@
545 )
546
547
548-class BinaryPackageBuild(PackageBuildDerived, SQLBase):
549+class BinaryPackageBuild(PackageBuildMixin, PackageBuildDerived, SQLBase):
550 implements(IBinaryPackageBuild)
551 _table = 'BinaryPackageBuild'
552 _defaultOrder = 'id'
553
554=== modified file 'lib/lp/translations/model/translationtemplatesbuild.py'
555--- lib/lp/translations/model/translationtemplatesbuild.py 2012-01-02 18:22:20 +0000
556+++ lib/lp/translations/model/translationtemplatesbuild.py 2013-01-07 04:25:24 +0000
557@@ -18,7 +18,10 @@
558 implements,
559 )
560
561-from lp.buildmaster.model.buildfarmjob import BuildFarmJobDerived
562+from lp.buildmaster.model.buildfarmjob import (
563+ BuildFarmJobDerived,
564+ BuildFarmJobMixin,
565+ )
566 from lp.code.model.branch import Branch
567 from lp.code.model.branchcollection import GenericBranchCollection
568 from lp.code.model.branchjob import (
569@@ -38,7 +41,7 @@
570 )
571
572
573-class TranslationTemplatesBuild(BuildFarmJobDerived, Storm):
574+class TranslationTemplatesBuild(BuildFarmJobMixin, BuildFarmJobDerived, Storm):
575 """A `BuildFarmJob` extension for translation templates builds."""
576
577 implements(ITranslationTemplatesBuild)