Merge lp:~jpds/launchpad/fix_361650_model_changes into lp:launchpad
- fix_361650_model_changes
- Merge into devel
Status: | Merged |
---|---|
Approved by: | Curtis Hovey |
Approved revision: | no longer in the source branch. |
Merged at revision: | 10619 |
Proposed branch: | lp:~jpds/launchpad/fix_361650_model_changes |
Merge into: | lp:launchpad |
Diff against target: |
857 lines (+470/-93) 11 files modified
lib/lp/registry/configure.zcml (+47/-5) lib/lp/registry/doc/distribution-mirror.txt (+119/-1) lib/lp/registry/interfaces/distribution.py (+9/-1) lib/lp/registry/interfaces/distributionmirror.py (+96/-47) lib/lp/registry/model/distribution.py (+11/-0) lib/lp/registry/model/distributionmirror.py (+63/-1) lib/lp/registry/stories/distributionmirror/xx-distribution-mirrors.txt (+11/-11) lib/lp/registry/stories/webservice/xx-distribution-mirror.txt (+20/-11) lib/lp/registry/stories/webservice/xx-distribution.txt (+74/-0) lib/lp/registry/tests/test_distributionmirror.py (+4/-14) lib/lp/testing/factory.py (+16/-2) |
To merge this branch: | bzr merge lp:~jpds/launchpad/fix_361650_model_changes |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Curtis Hovey (community) | code | Approve | |
Review via email: mp+20785@code.launchpad.net |
Commit message
Added models and tests for DistributionMir
Description of the change
= Summary =
This branch builds on the schema changes introduced for bug #361650. Adding stuff to our models.
It also adds the API parts and mirror checks when marking mirrors as country mirrors with complete test suite.
Curtis Hovey (sinzui) wrote : | # |
Hi Jonathan.
I have some trivial suggestions to improve this branch. I think the implementation
is good and ready to land.
I can land the branch at the end of this week after these changes are made.
make lint reported these problems that need fixing:
lib/lp/
339: [C0301] Line too long (83/78)
436: [W0311] Bad indentation. Found 7 spaces, expected 8
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
@@ -814,3 +814,107 @@
> +Country DNS mirrors
> +------
> +
> +Country DNS mirrors are mirrors which have been assigned $CC.archive.
> +or $CC.releases.
Wrap the narrative at 78 characters.
> + >>> login('<email address hidden>')
> + >>> ubuntu_distro = getUtility(
> + >>> de_archive_mirror = factory.
> + ... "Technische Universitaet Dresden", country=82,
> + ... http_url="http://
> + ... official_
> + >>> davis_station_
> + ... "Davis Station", country=9,
> + ... http_url="http://
> + ... official_
> + >>> de_archive_
> + >>> de_archive_
> + >>> logout()
Wrap the code at 78 characters.
...
> +Mirrors which are not official or do not have an HTTP URL may not be set as
> +country mirrors:
> +
> + >>> login('<email address hidden>')
> + >>> osuosl_mirror = factory.
> + ... country=226,
> + ... ftp_url="ftp://ubuntu.
> + ... official_
> + >>> osuosl_
> + >>> print osuosl_
> + None
Wrap the code at 78 characters.
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
>
> @@ -321,6 +321,14 @@
> if it's not found.
> """
>
> + @operation_
> + country=
> + mirror_
> + @operation_
> + @export_
> + def getCountryMirro
> + """Return the country DNS mirror for acountry and content type."""
grammar: s/acountry/a country/
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
> @@ -6,21 +6,24 @@
> __metaclass__ = type
__all__ = [
...
> + ...
Curtis Hovey (sinzui) : | # |
Preview Diff
1 | === modified file 'lib/lp/registry/configure.zcml' |
2 | --- lib/lp/registry/configure.zcml 2010-03-19 11:33:44 +0000 |
3 | +++ lib/lp/registry/configure.zcml 2010-03-31 15:03:35 +0000 |
4 | @@ -1644,18 +1644,60 @@ |
5 | <!-- DistributionMirror --> |
6 | <class class="lp.registry.model.distributionmirror.DistributionMirror"> |
7 | <allow |
8 | - interface="lp.registry.interfaces.distributionmirror.IDistributionMirrorPublic" /> |
9 | + attributes=" |
10 | + id |
11 | + name |
12 | + displayname |
13 | + description |
14 | + distribution |
15 | + http_base_url |
16 | + ftp_base_url |
17 | + rsync_base_url |
18 | + enabled |
19 | + speed |
20 | + status |
21 | + country |
22 | + content |
23 | + owner |
24 | + title |
25 | + cdimage_series |
26 | + source_series |
27 | + arch_series |
28 | + last_probe_record |
29 | + all_probe_records |
30 | + has_ftp_or_rsync_base_url |
31 | + base_url |
32 | + date_created |
33 | + country_dns_mirror |
34 | + mirrorMustHaveHTTPOrFTPURL |
35 | + getSummarizedMirroredSourceSeries |
36 | + getSummarizedMirroredArchSeries |
37 | + getOverallFreshness |
38 | + isOfficial |
39 | + shouldDisable |
40 | + disable |
41 | + newProbeRecord |
42 | + deleteMirrorDistroArchSeries |
43 | + ensureMirrorDistroArchSeries |
44 | + ensureMirrorDistroSeriesSource |
45 | + deleteMirrorDistroSeriesSource |
46 | + ensureMirrorCDImageSeries |
47 | + deleteMirrorCDImageSeries |
48 | + deleteAllMirrorCDImageSeries |
49 | + getExpectedPackagesPaths |
50 | + getExpectedSourcesPaths |
51 | + canTransitionToCountryMirror" /> |
52 | <require |
53 | permission="launchpad.Edit" |
54 | - interface="lp.registry.interfaces.distributionmirror.IDistributionMirrorEditRestricted" |
55 | set_attributes="name displayname description whiteboard |
56 | http_base_url ftp_base_url rsync_base_url enabled |
57 | - speed country content official_candidate owner" /> |
58 | + speed country content official_candidate owner" |
59 | + attributes="official_candidate whiteboard" /> |
60 | <require |
61 | permission="launchpad.Admin" |
62 | - interface="lp.registry.interfaces.distributionmirror.IDistributionMirrorAdminRestricted" |
63 | set_attributes="status reviewer date_reviewed" |
64 | - attributes="destroySelf" /> |
65 | + attributes="reviewer date_reviewed destroySelf |
66 | + transitionToCountryMirror" /> |
67 | </class> |
68 | |
69 | <adapter |
70 | |
71 | === modified file 'lib/lp/registry/doc/distribution-mirror.txt' |
72 | --- lib/lp/registry/doc/distribution-mirror.txt 2009-12-09 10:03:20 +0000 |
73 | +++ lib/lp/registry/doc/distribution-mirror.txt 2010-03-31 15:03:35 +0000 |
74 | @@ -8,7 +8,7 @@ |
75 | >>> from canonical.launchpad.interfaces import ( |
76 | ... ICountrySet, IDistributionSet, IDistributionMirrorSet, |
77 | ... IDistroArchSeriesSet, IDistroSeriesSet, ILibraryFileAliasSet, |
78 | - ... IPersonSet, MirrorContent, MirrorSpeed) |
79 | + ... IPersonSet, MirrorContent, MirrorSpeed, MirrorStatus) |
80 | >>> from lp.registry.interfaces.pocket import PackagePublishingPocket |
81 | >>> mirrorset = getUtility(IDistributionMirrorSet) |
82 | >>> distroset = getUtility(IDistributionSet) |
83 | @@ -814,3 +814,121 @@ |
84 | >>> mirrorset.getByName('invalid-mirror') is None |
85 | True |
86 | |
87 | +Country DNS mirrors |
88 | +------------------- |
89 | + |
90 | +Country DNS mirrors are mirrors which have been assigned |
91 | +$CC.archive.ubuntu.com or $CC.releases.ubuntu.com. These assignments are |
92 | +tracked in Launchpad. |
93 | + |
94 | + >>> login('admin@canonical.com') |
95 | + >>> ubuntu_distro = getUtility(IDistributionSet).getByName('ubuntu') |
96 | + >>> de_archive_mirror = factory.makeMirror(ubuntu_distro, |
97 | + ... "Technische Universitaet Dresden", country=82, |
98 | + ... http_url="http://ubuntu.mirror.tudos.de/ubuntu/", |
99 | + ... official_candidate=True) |
100 | + >>> davis_station_archive = factory.makeMirror(ubuntu_distro, |
101 | + ... "Davis Station", country=9, |
102 | + ... http_url="http://mirror.davis.antarctica.org/ubuntu", |
103 | + ... official_candidate=True) |
104 | + >>> de_archive_mirror.status = MirrorStatus.OFFICIAL |
105 | + >>> de_archive_prober_log = factory.makeMirrorProbeRecord(de_archive_mirror) |
106 | + >>> logout() |
107 | + |
108 | +Normal users can access country_dns_mirror, can see if a mirror is eligible |
109 | +for the status, however, they may not change it: |
110 | + |
111 | + >>> login('test@canonical.com') |
112 | + >>> de_archive_mirror.canTransitionToCountryMirror() |
113 | + True |
114 | + >>> de_archive_mirror.transitionToCountryMirror(True) |
115 | + Traceback (most recent call last): |
116 | + ... |
117 | + Unauthorized: (<DistributionMirror at ...>, 'transitionToCountryMirror', |
118 | + 'launchpad.Admin') |
119 | + >>> logout() |
120 | + |
121 | +Mirror listing administrators may change the status however: |
122 | + |
123 | + >>> login('karl@canonical.com') |
124 | + >>> de_archive_mirror.transitionToCountryMirror(True) |
125 | + |
126 | +Mirrors which are already set as country mirrors can't be 'set' as such |
127 | +again: |
128 | + |
129 | + >>> de_archive_mirror.canTransitionToCountryMirror() |
130 | + False |
131 | + >>> de_archive_mirror.transitionToCountryMirror(True) |
132 | + >>> logout() |
133 | + |
134 | +There cannot be multiple country mirrors of one type for one country: |
135 | + |
136 | + >>> login('karl@canonical.com') |
137 | + >>> proberecord = factory.makeMirrorProbeRecord(davis_station_archive) |
138 | + |
139 | + >>> print davis_station_archive.content.name |
140 | + ARCHIVE |
141 | + >>> print davis_station_archive.country_dns_mirror |
142 | + False |
143 | + >>> print davis_station_archive.country.name |
144 | + Antarctica |
145 | + |
146 | + >>> archive_mirror2 = getUtility(IDistributionMirrorSet).getByName( |
147 | + ... 'archive-mirror2') |
148 | + >>> print archive_mirror2.content.name |
149 | + ARCHIVE |
150 | + >>> print archive_mirror2.country_dns_mirror |
151 | + False |
152 | + >>> print archive_mirror2.country.name |
153 | + Antarctica |
154 | + |
155 | + >>> davis_station_archive.status = MirrorStatus.OFFICIAL |
156 | + |
157 | + >>> davis_station_archive.transitionToCountryMirror(True) |
158 | + >>> archive_mirror2.transitionToCountryMirror(True) |
159 | + Traceback (most recent call last): |
160 | + ... |
161 | + CountryMirrorAlreadySet: Antarctica already has a country Archive mirror |
162 | + set. |
163 | + |
164 | +Mirrors which have not been probed may not be marked as country mirrors: |
165 | + |
166 | + >>> linux_au_mirror = factory.makeMirror(ubuntu_distro, |
167 | + ... "Linux.org.au", country=14, |
168 | + ... http_url="http://mirror.linux.org.au/ubuntu", |
169 | + ... official_candidate=True) |
170 | + >>> linux_au_mirror.status = MirrorStatus.OFFICIAL |
171 | + >>> linux_au_mirror.transitionToCountryMirror(True) |
172 | + Traceback (most recent call last): |
173 | + ... |
174 | + MirrorNotProbed: This mirror may not be set as a country mirror as it has |
175 | + not been probed. |
176 | + >>> logout() |
177 | + |
178 | +Mirrors which are not official or do not have an HTTP URL may not be set as |
179 | +country mirrors: |
180 | + |
181 | + >>> login('admin@canonical.com') |
182 | + >>> osuosl_mirror = factory.makeMirror(ubuntu_distro, |
183 | + ... "OSU Open Source Lab", country=226, |
184 | + ... ftp_url="ftp://ubuntu.osuosl.org/pub/ubuntu/", |
185 | + ... official_candidate=True) |
186 | + >>> osuosl_mirror.status = MirrorStatus.OFFICIAL |
187 | + >>> print osuosl_mirror.http_base_url |
188 | + None |
189 | + |
190 | + >>> osuosl_mirror.canTransitionToCountryMirror() |
191 | + False |
192 | + |
193 | + >>> osuosl_mirror.transitionToCountryMirror(None) |
194 | + Traceback (most recent call last): |
195 | + ... |
196 | + NoneError: None isn't acceptable as a value for |
197 | + DistributionMirror.country_dns_mirror |
198 | + |
199 | + >>> osuosl_mirror.transitionToCountryMirror(True) |
200 | + Traceback (most recent call last): |
201 | + ... |
202 | + MirrorHasNoHTTPURL: This mirror may not be set as a country mirror as it |
203 | + does not have an HTTP URL set. |
204 | + >>> logout() |
205 | |
206 | === modified file 'lib/lp/registry/interfaces/distribution.py' |
207 | --- lib/lp/registry/interfaces/distribution.py 2010-03-24 21:59:58 +0000 |
208 | +++ lib/lp/registry/interfaces/distribution.py 2010-03-31 15:03:35 +0000 |
209 | @@ -25,7 +25,7 @@ |
210 | |
211 | from lazr.restful.fields import CollectionField, Reference |
212 | from lazr.restful.declarations import ( |
213 | - collection_default_content, export_as_webservice_collection, |
214 | + collection_default_content, copy_field, export_as_webservice_collection, |
215 | export_as_webservice_entry, export_operation_as, |
216 | export_read_operation, exported, operation_parameters, |
217 | operation_returns_collection_of, operation_returns_entry, |
218 | @@ -321,6 +321,14 @@ |
219 | if it's not found. |
220 | """ |
221 | |
222 | + @operation_parameters( |
223 | + country=copy_field(IDistributionMirror['country'], required=True), |
224 | + mirror_type=copy_field(IDistributionMirror['content'], required=True)) |
225 | + @operation_returns_entry(IDistributionMirror) |
226 | + @export_read_operation() |
227 | + def getCountryMirror(country, mirror_type): |
228 | + """Return the country DNS mirror for a country and content type.""" |
229 | + |
230 | def newMirror(owner, speed, country, content, displayname=None, |
231 | description=None, http_base_url=None, |
232 | ftp_base_url=None, rsync_base_url=None, enabled=False, |
233 | |
234 | === modified file 'lib/lp/registry/interfaces/distributionmirror.py' |
235 | --- lib/lp/registry/interfaces/distributionmirror.py 2010-02-22 15:50:06 +0000 |
236 | +++ lib/lp/registry/interfaces/distributionmirror.py 2010-03-31 15:03:35 +0000 |
237 | @@ -6,21 +6,24 @@ |
238 | __metaclass__ = type |
239 | |
240 | __all__ = [ |
241 | -'IDistributionMirror', |
242 | -'IDistributionMirrorAdminRestricted', |
243 | -'IDistributionMirrorEditRestricted', |
244 | -'IDistributionMirrorPublic', |
245 | -'IMirrorDistroArchSeries', |
246 | -'IMirrorDistroSeriesSource', |
247 | -'IMirrorProbeRecord', |
248 | -'IDistributionMirrorSet', |
249 | -'IMirrorCDImageDistroSeries', |
250 | -'PROBE_INTERVAL', |
251 | -'UnableToFetchCDImageFileList', |
252 | -'MirrorContent', |
253 | -'MirrorFreshness', |
254 | -'MirrorSpeed', |
255 | -'MirrorStatus'] |
256 | + 'CannotTransitionToCountryMirror', |
257 | + 'CountryMirrorAlreadySet', |
258 | + 'IDistributionMirror', |
259 | + 'IMirrorDistroArchSeries', |
260 | + 'IMirrorDistroSeriesSource', |
261 | + 'IMirrorProbeRecord', |
262 | + 'IDistributionMirrorSet', |
263 | + 'IMirrorCDImageDistroSeries', |
264 | + 'PROBE_INTERVAL', |
265 | + 'MirrorContent', |
266 | + 'MirrorFreshness', |
267 | + 'MirrorHasNoHTTPURL', |
268 | + 'MirrorNotOfficial', |
269 | + 'MirrorNotProbed', |
270 | + 'MirrorSpeed', |
271 | + 'MirrorStatus', |
272 | + 'UnableToFetchCDImageFileList', |
273 | + ] |
274 | |
275 | from cgi import escape |
276 | |
277 | @@ -31,8 +34,11 @@ |
278 | from zope.component import getUtility |
279 | from lazr.enum import DBEnumeratedType, DBItem |
280 | from lazr.restful.declarations import ( |
281 | - export_as_webservice_entry, export_read_operation, exported) |
282 | + export_as_webservice_entry, export_read_operation, |
283 | + export_write_operation, exported, mutator_for, operation_parameters, |
284 | + webservice_error) |
285 | from lazr.restful.fields import Reference, ReferenceChoice |
286 | +from lazr.restful.interface import copy_field |
287 | |
288 | from canonical.launchpad import _ |
289 | from canonical.launchpad.fields import ( |
290 | @@ -47,6 +53,43 @@ |
291 | PROBE_INTERVAL = 23 |
292 | |
293 | |
294 | +class CannotTransitionToCountryMirror(Exception): |
295 | + """Root exception for transitions to country mirrors.""" |
296 | + webservice_error(400) |
297 | + |
298 | + |
299 | +class CountryMirrorAlreadySet(CannotTransitionToCountryMirror): |
300 | + """Distribution mirror cannot be set as a country mirror. |
301 | + |
302 | + Raised when a user tries to change set a distribution mirror as a country |
303 | + mirror, however there is already one set for that country. |
304 | + """ |
305 | + |
306 | + |
307 | +class MirrorNotOfficial(CannotTransitionToCountryMirror): |
308 | + """Distribution mirror is not permitted to become a country mirror. |
309 | + |
310 | + Raised when a user tries to change set a distribution mirror as a country |
311 | + mirror, however the mirror in question is not official. |
312 | + """ |
313 | + |
314 | + |
315 | +class MirrorHasNoHTTPURL(CannotTransitionToCountryMirror): |
316 | + """Distribution mirror has no HTTP URL. |
317 | + |
318 | + Raised when a user tries to make an official mirror a country mirror, |
319 | + however the mirror has not HTTP URL set. |
320 | + """ |
321 | + |
322 | + |
323 | +class MirrorNotProbed(CannotTransitionToCountryMirror): |
324 | + """Distribution mirror has not been probed. |
325 | + |
326 | + Raised when a user tries to set an official mirror as a country mirror, |
327 | + however the mirror has not been probed yet. |
328 | + """ |
329 | + |
330 | + |
331 | class MirrorContent(DBEnumeratedType): |
332 | """The content that is mirrored.""" |
333 | |
334 | @@ -284,33 +327,10 @@ |
335 | def getMirrorByURI(self, url): |
336 | return getUtility(IDistributionMirrorSet).getByRsyncUrl(url) |
337 | |
338 | -class IDistributionMirrorAdminRestricted(Interface): |
339 | - """IDistributionMirror properties requiring launchpad.Admin permission.""" |
340 | - |
341 | - reviewer = exported(PublicPersonChoice( |
342 | - title=_('Reviewer'), required=False, readonly=True, |
343 | - vocabulary='ValidPersonOrTeam', description=_( |
344 | - "The person who last reviewed this mirror."))) |
345 | - date_reviewed = exported(Datetime( |
346 | - title=_('Date reviewed'), required=False, readonly=True, |
347 | - description=_( |
348 | - "The date on which this mirror was last reviewed by a mirror admin."))) |
349 | - |
350 | - |
351 | -class IDistributionMirrorEditRestricted(Interface): |
352 | - """IDistributionMirror properties requiring launchpad.Edit permission.""" |
353 | - |
354 | - official_candidate = exported(Bool( |
355 | - title=_('Apply to be an official mirror of this distribution'), |
356 | - required=False, readonly=False, default=True)) |
357 | - whiteboard = exported(Whiteboard( |
358 | - title=_('Whiteboard'), required=False, readonly=False, |
359 | - description=_("Notes on the current status of the mirror (only " |
360 | - "visible to admins and the mirror's registrant)."))) |
361 | - |
362 | - |
363 | -class IDistributionMirrorPublic(Interface): |
364 | - """Public IDistributionMirror properties.""" |
365 | + |
366 | +class IDistributionMirror(Interface): |
367 | + """A mirror of a given distribution.""" |
368 | + export_as_webservice_entry() |
369 | |
370 | id = Int(title=_('The unique id'), required=True, readonly=True) |
371 | owner = exported(PublicPersonChoice( |
372 | @@ -386,6 +406,39 @@ |
373 | date_created = exported(Datetime( |
374 | title=_('Date Created'), required=True, readonly=True, |
375 | description=_("The date on which this mirror was registered."))) |
376 | + country_dns_mirror = exported(Bool( |
377 | + title=_('Country DNS Mirror'), |
378 | + description=_('Whether this is a country mirror in DNS.'), |
379 | + required=False, readonly=True, default=False)) |
380 | + |
381 | + reviewer = exported(PublicPersonChoice( |
382 | + title=_('Reviewer'), required=False, readonly=True, |
383 | + vocabulary='ValidPersonOrTeam', description=_( |
384 | + "The person who last reviewed this mirror."))) |
385 | + date_reviewed = exported(Datetime( |
386 | + title=_('Date reviewed'), required=False, readonly=True, |
387 | + description=_( |
388 | + "The date on which this mirror was last reviewed by a mirror " |
389 | + "admin."))) |
390 | + |
391 | + official_candidate = exported(Bool( |
392 | + title=_('Apply to be an official mirror of this distribution'), |
393 | + required=False, readonly=False, default=True)) |
394 | + whiteboard = exported(Whiteboard( |
395 | + title=_('Whiteboard'), required=False, readonly=False, |
396 | + description=_("Notes on the current status of the mirror (only " |
397 | + "visible to admins and the mirror's registrant)."))) |
398 | + |
399 | + @export_read_operation() |
400 | + def canTransitionToCountryMirror(): |
401 | + """Verify if a mirror can be set as a country mirror or return |
402 | + False.""" |
403 | + |
404 | + @mutator_for(country_dns_mirror) |
405 | + @operation_parameters(country_dns_mirror=copy_field(country_dns_mirror)) |
406 | + @export_write_operation() |
407 | + def transitionToCountryMirror(country_dns_mirror): |
408 | + """Method run on changing country_dns_mirror.""" |
409 | |
410 | @invariant |
411 | def mirrorMustHaveHTTPOrFTPURL(mirror): |
412 | @@ -521,10 +574,6 @@ |
413 | Sources.gz file refer to and the path to the file itself. |
414 | """ |
415 | |
416 | -class IDistributionMirror(IDistributionMirrorAdminRestricted, |
417 | - IDistributionMirrorEditRestricted, IDistributionMirrorPublic): |
418 | - """A mirror of a given distribution.""" |
419 | - export_as_webservice_entry() |
420 | |
421 | |
422 | class UnableToFetchCDImageFileList(Exception): |
423 | |
424 | === modified file 'lib/lp/registry/model/distribution.py' |
425 | --- lib/lp/registry/model/distribution.py 2010-03-24 02:53:42 +0000 |
426 | +++ lib/lp/registry/model/distribution.py 2010-03-31 15:03:35 +0000 |
427 | @@ -402,6 +402,17 @@ |
428 | """See `IDistribution`.""" |
429 | return DistributionMirror.selectOneBy(distribution=self, name=name) |
430 | |
431 | + def getCountryMirror(self, country, mirror_type): |
432 | + """See `IDistribution`.""" |
433 | + store = Store.of(self) |
434 | + results = store.find( |
435 | + DistributionMirror, |
436 | + DistributionMirror.distribution == self, |
437 | + DistributionMirror.country == country, |
438 | + DistributionMirror.content == mirror_type, |
439 | + DistributionMirror.country_dns_mirror == True) |
440 | + return results.one() |
441 | + |
442 | def newMirror(self, owner, speed, country, content, displayname=None, |
443 | description=None, http_base_url=None, |
444 | ftp_base_url=None, rsync_base_url=None, |
445 | |
446 | === modified file 'lib/lp/registry/model/distributionmirror.py' |
447 | --- lib/lp/registry/model/distributionmirror.py 2010-03-23 20:42:23 +0000 |
448 | +++ lib/lp/registry/model/distributionmirror.py 2010-03-31 15:03:35 +0000 |
449 | @@ -45,9 +45,11 @@ |
450 | from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities |
451 | from lp.soyuz.interfaces.publishing import PackagePublishingStatus |
452 | from lp.registry.interfaces.distributionmirror import ( |
453 | + CannotTransitionToCountryMirror, CountryMirrorAlreadySet, |
454 | IDistributionMirror, IDistributionMirrorSet, IMirrorCDImageDistroSeries, |
455 | IMirrorDistroArchSeries, IMirrorDistroSeriesSource, IMirrorProbeRecord, |
456 | - MirrorContent, MirrorFreshness, MirrorSpeed, MirrorStatus, PROBE_INTERVAL) |
457 | + MirrorContent, MirrorFreshness, MirrorHasNoHTTPURL, MirrorNotOfficial, |
458 | + MirrorNotProbed, MirrorSpeed, MirrorStatus, PROBE_INTERVAL) |
459 | from lp.registry.interfaces.distroseries import IDistroSeries |
460 | from lp.registry.interfaces.sourcepackage import SourcePackageFileType |
461 | from canonical.launchpad.mail import simple_sendmail, format_address |
462 | @@ -100,6 +102,8 @@ |
463 | date_reviewed = UtcDateTimeCol(default=None) |
464 | whiteboard = StringCol( |
465 | notNull=False, default=None) |
466 | + country_dns_mirror = BoolCol( |
467 | + notNull=True, default=False) |
468 | |
469 | @property |
470 | def base_url(self): |
471 | @@ -145,6 +149,64 @@ |
472 | "This mirror has been probed and thus can't be removed.") |
473 | SQLBase.destroySelf(self) |
474 | |
475 | + def verifyTransitionToCountryMirror(self): |
476 | + """Verify that a mirror can be set as a country mirror. |
477 | + |
478 | + Return True if valid, otherwise raise a subclass of |
479 | + CannotTransitionToCountryMirror. |
480 | + """ |
481 | + |
482 | + current_country_mirror = self.distribution.getCountryMirror( |
483 | + self.country, self.content) |
484 | + |
485 | + if current_country_mirror is not None: |
486 | + # Country already has a country mirror. |
487 | + raise CountryMirrorAlreadySet( |
488 | + "%s already has a country %s mirror set." % ( |
489 | + self.country.name, self.content)) |
490 | + |
491 | + if not self.isOfficial(): |
492 | + # Only official mirrors may be set as country mirrors. |
493 | + raise MirrorNotOfficial( |
494 | + "This mirror may not be set as a country mirror as it is not " |
495 | + "an official mirror.") |
496 | + |
497 | + if self.http_base_url is None: |
498 | + # Country mirrors must have HTTP URLs set. |
499 | + raise MirrorHasNoHTTPURL( |
500 | + "This mirror may not be set as a country mirror as it does " |
501 | + "not have an HTTP URL set.") |
502 | + |
503 | + if not self.last_probe_record: |
504 | + # Only mirrors which have been probed may be set as country |
505 | + # mirrors. |
506 | + raise MirrorNotProbed( |
507 | + "This mirror may not be set as a country mirror as it has " |
508 | + "not been probed.") |
509 | + |
510 | + # Verification done. |
511 | + return True |
512 | + |
513 | + def canTransitionToCountryMirror(self): |
514 | + """See `IDistributionMirror`.""" |
515 | + try: |
516 | + return self.verifyTransitionToCountryMirror() |
517 | + except CannotTransitionToCountryMirror: |
518 | + return False |
519 | + |
520 | + def transitionToCountryMirror(self, country_dns_mirror): |
521 | + """See `IDistributionMirror`.""" |
522 | + |
523 | + # country_dns_mirror has not been changed, do nothing. |
524 | + if self.country_dns_mirror == country_dns_mirror: |
525 | + return |
526 | + |
527 | + # Environment sanity checks. |
528 | + if country_dns_mirror: |
529 | + self.verifyTransitionToCountryMirror() |
530 | + |
531 | + self.country_dns_mirror = country_dns_mirror |
532 | + |
533 | def getOverallFreshness(self): |
534 | """See IDistributionMirror""" |
535 | # XXX Guilherme Salgado 2006-08-16: |
536 | |
537 | === modified file 'lib/lp/registry/stories/distributionmirror/xx-distribution-mirrors.txt' |
538 | --- lib/lp/registry/stories/distributionmirror/xx-distribution-mirrors.txt 2009-12-10 23:32:13 +0000 |
539 | +++ lib/lp/registry/stories/distributionmirror/xx-distribution-mirrors.txt 2010-03-31 15:03:35 +0000 |
540 | @@ -40,13 +40,14 @@ |
541 | >>> print browser.title |
542 | Mirrors of Ubuntu Linux... |
543 | >>> print_mirrors_by_countries(browser.contents) |
544 | - Antarctica: |
545 | - [(u'Archive-mirror2', u'http', u'128 Kbps', u'Six hours behind')] |
546 | + Antarctica: [(u'Archive-mirror2', u'http', u'128 Kbps', |
547 | + u'Six hours behind')] |
548 | France: |
549 | [(u'Archive-404-mirror', u'http', u'512 Kbps', u'Last update unknown'), |
550 | - (u'Archive-mirror', u'http', u'128 Kbps', u'Last update unknown')] |
551 | - United Kingdom: |
552 | - [(u'Canonical-archive', u'http', u'100 Mbps', u'Last update unknown')] |
553 | + (u'Archive-mirror', u'http', u'128 Kbps', u'Last update unknown')] |
554 | + United Kingdom: [(u'Canonical-archive', u'http', u'100 Mbps', |
555 | + u'Last update unknown')] |
556 | + |
557 | >>> find_tags_by_class(browser.contents, 'distromirrorstatusSIXHOURSBEHIND') |
558 | [<span class="distromirrorstatusSIXHOURSBEHIND">Six hours behind</span>] |
559 | >>> find_tags_by_class(browser.contents, 'distromirrorstatusUNKNOWN')[0] |
560 | @@ -59,13 +60,12 @@ |
561 | >>> browser.url |
562 | 'http://launchpad.dev/ubuntu/+cdmirrors' |
563 | >>> print_mirrors_by_countries(browser.contents) |
564 | - France: |
565 | - [(u'Releases-mirror', u'http', u'2 Mbps'), |
566 | + France: |
567 | + [(u'Releases-mirror', u'http', u'2 Mbps'), |
568 | (u'Unreachable-mirror', u'http', u'512 Kbps')] |
569 | - Germany: |
570 | - [(u'Releases-mirror2', u'http', u'2 Mbps')] |
571 | - United Kingdom: |
572 | - [(u'Canonical-releases', u'http', u'100 Mbps')] |
573 | + Germany: [(u'Releases-mirror2', u'http', u'2 Mbps')] |
574 | + United Kingdom: [(u'Canonical-releases', u'http', u'100 Mbps')] |
575 | + |
576 | |
577 | === Disabled mirrors === |
578 | |
579 | |
580 | === modified file 'lib/lp/registry/stories/webservice/xx-distribution-mirror.txt' |
581 | --- lib/lp/registry/stories/webservice/xx-distribution-mirror.txt 2010-02-23 19:40:45 +0000 |
582 | +++ lib/lp/registry/stories/webservice/xx-distribution-mirror.txt 2010-03-31 15:03:35 +0000 |
583 | @@ -8,10 +8,12 @@ |
584 | >>> distro = distros['entries'][0] |
585 | >>> ubuntu = webservice.get(distro['self_link']).jsonBody() |
586 | >>> ubuntu_archive_mirrors = webservice.get(ubuntu['archive_mirrors_collection_link']).jsonBody() |
587 | - >>> canonical_archive = ubuntu_archive_mirrors['entries'][0] |
588 | - >>> canonical_archive_json = webservice.get(canonical_archive['self_link']).jsonBody() |
589 | - >>> pprint_entry(canonical_archive_json) |
590 | + >>> canonical_archive = webservice.named_get( |
591 | + ... ubuntu['self_link'], 'getMirrorByName', |
592 | + ... name='canonical-archive').jsonBody() |
593 | + >>> pprint_entry(canonical_archive) |
594 | content: u'Archive' |
595 | + country_dns_mirror: False |
596 | country_link: u'http://.../+countries/GB' |
597 | date_created: u'2006-10-16T18:31:43.434567+00:00' |
598 | date_reviewed: None |
599 | @@ -39,6 +41,7 @@ |
600 | >>> canonical_releases_json = webservice.get(canonical_releases['self_link']).jsonBody() |
601 | >>> pprint_entry(canonical_releases_json) |
602 | content: u'CD Image' |
603 | + country_dns_mirror: False |
604 | country_link: u'http://.../+countries/GB' |
605 | date_created: u'2006-10-16T18:31:43.434567+00:00' |
606 | date_reviewed: None |
607 | @@ -73,12 +76,12 @@ |
608 | >>> karl_db = getUtility(IPersonSet).getByName('karl') |
609 | >>> test_db = getUtility(IPersonSet).getByName('name12') |
610 | >>> no_priv_db = getUtility(IPersonSet).getByName('no-priv') |
611 | - >>> karl_webservice = webservice_for_person(karl_db, |
612 | - ... permission=OAuthPermission.WRITE_PUBLIC) |
613 | - >>> test_webservice = webservice_for_person(test_db, |
614 | - ... permission=OAuthPermission.WRITE_PUBLIC) |
615 | - >>> no_priv_webservice = webservice_for_person(no_priv_db, |
616 | - ... permission=OAuthPermission.READ_PUBLIC) |
617 | + >>> karl_webservice = webservice_for_person( |
618 | + ... karl_db, permission=OAuthPermission.WRITE_PUBLIC) |
619 | + >>> test_webservice = webservice_for_person( |
620 | + ... test_db, permission=OAuthPermission.WRITE_PUBLIC) |
621 | + >>> no_priv_webservice = webservice_for_person( |
622 | + ... no_priv_db, permission=OAuthPermission.READ_PUBLIC) |
623 | >>> logout() |
624 | |
625 | Ensure that anonymous API sessions can view mirror listings; archive/releases. |
626 | @@ -97,7 +100,9 @@ |
627 | |
628 | One must have special permissions to access certain attributes: |
629 | |
630 | - >>> archive_404_mirror = ubuntu_archive_mirrors['entries'][1] |
631 | + >>> archive_404_mirror = webservice.named_get( |
632 | + ... ubuntu['self_link'], 'getMirrorByName', |
633 | + ... name="archive-404-mirror").jsonBody() |
634 | >>> response = no_priv_webservice.get( |
635 | ... archive_404_mirror['self_link']).jsonBody() |
636 | >>> pprint_entry(response) |
637 | @@ -128,6 +133,7 @@ |
638 | ... archive_404_mirror['self_link']).jsonBody() |
639 | >>> pprint_entry(response) |
640 | content: u'Archive' |
641 | + country_dns_mirror: False |
642 | country_link: u'http://.../+countries/FR' |
643 | date_created: u'2006-10-16T18:31:43.438573+00:00' |
644 | date_reviewed: None |
645 | @@ -209,6 +215,7 @@ |
646 | ... canonical_releases['self_link'], 'application/json', dumps(patch)).jsonBody() |
647 | >>> pprint_entry(response) |
648 | content: u'CD Image' |
649 | + country_dns_mirror: False |
650 | country_link: u'http://.../+countries/GL' |
651 | date_created: u'2006-10-16T18:31:43.434567+00:00' |
652 | date_reviewed: None |
653 | @@ -244,7 +251,9 @@ |
654 | "getOverallFreshness" returns the freshness of the mirror determined by the |
655 | mirror prober from the mirror's last probe. |
656 | |
657 | - >>> releases_mirror2 = ubuntu_cd_mirrors['entries'][2] |
658 | + >>> releases_mirror2 = webservice.named_get( |
659 | + ... ubuntu['self_link'], 'getMirrorByName', |
660 | + ... name='releases-mirror2').jsonBody() |
661 | >>> freshness = webservice.named_get(releases_mirror2['self_link'], |
662 | ... 'getOverallFreshness').jsonBody() |
663 | >>> print freshness |
664 | |
665 | === modified file 'lib/lp/registry/stories/webservice/xx-distribution.txt' |
666 | --- lib/lp/registry/stories/webservice/xx-distribution.txt 2010-02-23 17:36:27 +0000 |
667 | +++ lib/lp/registry/stories/webservice/xx-distribution.txt 2010-03-31 15:03:35 +0000 |
668 | @@ -123,6 +123,7 @@ |
669 | ... name='canonical-releases').jsonBody() |
670 | >>> pprint_entry(canonical_releases) |
671 | content: u'CD Image' |
672 | + country_dns_mirror: False |
673 | country_link: u'http://.../+countries/GB' |
674 | date_created: u'2006-10-16T18:31:43.434567+00:00' |
675 | date_reviewed: None |
676 | @@ -142,3 +143,76 @@ |
677 | speed: u'100 Mbps' |
678 | status: u'Official' |
679 | whiteboard: None |
680 | + |
681 | +"getCountryMirror" returns the country DNS mirror for a given country; |
682 | +returning None if there isn't one. |
683 | + |
684 | + >>> # Prepare stuff. |
685 | + >>> from lp.registry.interfaces.distribution import IDistributionSet |
686 | + >>> from zope.component import getUtility |
687 | + >>> from canonical.launchpad.testing.pages import webservice_for_person |
688 | + >>> from canonical.launchpad.webapp.interfaces import OAuthPermission |
689 | + >>> from lp.registry.interfaces.person import IPersonSet |
690 | + >>> from simplejson import dumps |
691 | + |
692 | + >>> login('admin@canonical.com') |
693 | + >>> ubuntu_distro = getUtility(IDistributionSet).getByName('ubuntu') |
694 | + >>> showa_station = factory.makeMirror(ubuntu_distro, |
695 | + ... "Showa Station", country=9, |
696 | + ... http_url="http://mirror.showa.antarctica.org/ubuntu", |
697 | + ... official_candidate=True) |
698 | + >>> showa_station_log = factory.makeMirrorProbeRecord(showa_station) |
699 | + >>> logout() |
700 | + |
701 | + >>> login(ANONYMOUS) |
702 | + >>> karl_db = getUtility(IPersonSet).getByName('karl') |
703 | + >>> karl_webservice = webservice_for_person(karl_db, |
704 | + ... permission=OAuthPermission.WRITE_PUBLIC) |
705 | + >>> logout() |
706 | + |
707 | + >>> # Mark new mirror as official and a country mirror. |
708 | + >>> patch = { |
709 | + ... u'status': 'Official', |
710 | + ... u'country_dns_mirror': True |
711 | + ... } |
712 | + |
713 | + >>> antarctica_patch_target = webservice.named_get( |
714 | + ... ubuntu['self_link'], 'getMirrorByName', |
715 | + ... name='mirror.showa.antarctica.org-archive').jsonBody() |
716 | + ... ) |
717 | + |
718 | + >>> response = karl_webservice.patch( |
719 | + ... antarctica_patch_target['self_link'], 'application/json', |
720 | + ... dumps(patch)) |
721 | + |
722 | + >>> antarctica = webservice.get("/+countries/AQ").jsonBody() |
723 | + >>> antarctica_country_mirror_archive = webservice.named_get( |
724 | + ... ubuntu['self_link'], 'getCountryMirror', |
725 | + ... country=antarctica['self_link'], |
726 | + ... mirror_type="Archive").jsonBody() |
727 | + >>> pprint_entry(antarctica_country_mirror_archive) |
728 | + content: u'Archive' |
729 | + country_dns_mirror: True |
730 | + country_link: u'http://.../+countries/AQ' |
731 | + ... |
732 | + |
733 | + >>> uk = webservice.get("/+countries/GB").jsonBody() |
734 | + >>> uk_country_mirror_archive = webservice.named_get( |
735 | + ... ubuntu['self_link'], 'getCountryMirror', |
736 | + ... country=uk['self_link'], |
737 | + ... mirror_type="Archive") |
738 | + >>> print uk_country_mirror_archive.jsonBody() |
739 | + None |
740 | + |
741 | +For "getCountryMirror", the mirror_type parameter must be "Archive" or |
742 | +"CD Images": |
743 | + |
744 | + >>> uk_country_mirror_archive = webservice.named_get( |
745 | + ... ubuntu['self_link'], 'getCountryMirror', |
746 | + ... country=uk['self_link'], |
747 | + ... mirror_type="Bogus") |
748 | + >>> print uk_country_mirror_archive.jsonBody() |
749 | + Traceback (most recent call last): |
750 | + ... |
751 | + ValueError: mirror_type: Invalid value "Bogus". Acceptable values are: |
752 | + Archive, CD Image |
753 | |
754 | === modified file 'lib/lp/registry/tests/test_distributionmirror.py' |
755 | --- lib/lp/registry/tests/test_distributionmirror.py 2009-10-26 18:40:04 +0000 |
756 | +++ lib/lp/registry/tests/test_distributionmirror.py 2010-03-31 15:03:35 +0000 |
757 | @@ -3,7 +3,6 @@ |
758 | |
759 | __metaclass__ = type |
760 | |
761 | -from StringIO import StringIO |
762 | import unittest |
763 | |
764 | import transaction |
765 | @@ -17,19 +16,19 @@ |
766 | from lp.registry.interfaces.distributionmirror import ( |
767 | IDistributionMirrorSet, MirrorContent, MirrorFreshness) |
768 | from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities |
769 | -from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet |
770 | from lp.registry.interfaces.pocket import PackagePublishingPocket |
771 | from lp.registry.interfaces.distribution import IDistributionSet |
772 | from lp.services.mail import stub |
773 | +from lp.testing.factory import LaunchpadObjectFactory |
774 | |
775 | from canonical.testing import LaunchpadFunctionalLayer |
776 | |
777 | - |
778 | class TestDistributionMirror(unittest.TestCase): |
779 | layer = LaunchpadFunctionalLayer |
780 | |
781 | def setUp(self): |
782 | login('test@canonical.com') |
783 | + self.factory = LaunchpadObjectFactory() |
784 | mirrorset = getUtility(IDistributionMirrorSet) |
785 | self.cdimage_mirror = getUtility(IDistributionMirrorSet).getByName( |
786 | 'releases-mirror') |
787 | @@ -132,15 +131,6 @@ |
788 | self.archive_mirror.getOverallFreshness(), |
789 | MirrorFreshness.TWODAYSBEHIND) |
790 | |
791 | - def _create_probe_record(self, mirror): |
792 | - log_file = StringIO() |
793 | - log_file.write("Fake probe, nothing useful here.") |
794 | - log_file.seek(0) |
795 | - library_alias = getUtility(ILibraryFileAliasSet).create( |
796 | - name='foo', size=len(log_file.getvalue()), |
797 | - file=log_file, contentType='text/plain') |
798 | - proberecord = mirror.newProbeRecord(library_alias) |
799 | - |
800 | def test_disabling_mirror_and_notifying_owner(self): |
801 | login('karl@canonical.com') |
802 | |
803 | @@ -148,7 +138,7 @@ |
804 | # If a mirror has been probed only once, the owner will always be |
805 | # notified when it's disabled --it doesn't matter whether it was |
806 | # previously enabled or disabled. |
807 | - self._create_probe_record(mirror) |
808 | + self.factory.makeMirrorProbeRecord(mirror) |
809 | self.failUnless(mirror.enabled) |
810 | log = 'Got a 404 on http://foo/baz' |
811 | mirror.disable(notify_owner=True, log=log) |
812 | @@ -166,7 +156,7 @@ |
813 | |
814 | # For mirrors that have been probed more than once, we'll only notify |
815 | # the owner if the mirror was previously enabled. |
816 | - self._create_probe_record(mirror) |
817 | + self.factory.makeMirrorProbeRecord(mirror) |
818 | mirror.enabled = True |
819 | mirror.disable(notify_owner=True, log=log) |
820 | # A notification was sent to the owner and other to the mirror admins. |
821 | |
822 | === modified file 'lib/lp/testing/factory.py' |
823 | --- lib/lp/testing/factory.py 2010-03-25 02:21:15 +0000 |
824 | +++ lib/lp/testing/factory.py 2010-03-31 15:03:35 +0000 |
825 | @@ -1981,8 +1981,22 @@ |
826 | team_list = self.makeMailingList(team, owner) |
827 | return team, team_list |
828 | |
829 | + def makeMirrorProbeRecord(self, mirror): |
830 | + """Create a probe record for a mirror of a distribution.""" |
831 | + log_file = StringIO() |
832 | + log_file.write("Fake probe, nothing useful here.") |
833 | + log_file.seek(0) |
834 | + |
835 | + library_alias = getUtility(ILibraryFileAliasSet).create( |
836 | + name='foo', size=len(log_file.getvalue()), |
837 | + file=log_file, contentType='text/plain') |
838 | + |
839 | + proberecord = mirror.newProbeRecord(library_alias) |
840 | + return proberecord |
841 | + |
842 | def makeMirror(self, distribution, displayname, country=None, |
843 | - http_url=None, ftp_url=None, rsync_url=None): |
844 | + http_url=None, ftp_url=None, rsync_url=None, |
845 | + official_candidate=False): |
846 | """Create a mirror for the distribution.""" |
847 | # If no URL is specified create an HTTP URL. |
848 | if http_url is None and ftp_url is None and rsync_url is None: |
849 | @@ -2001,7 +2015,7 @@ |
850 | http_base_url=http_url, |
851 | ftp_base_url=ftp_url, |
852 | rsync_base_url=rsync_url, |
853 | - official_candidate=False) |
854 | + official_candidate=official_candidate) |
855 | return mirror |
856 | |
857 | def makeUniqueRFC822MsgId(self): |
Your merge proposal should show the output of make lint to verify your
changes did not have any cruft. It will also inform you of style mistakes
that must be fixed before making a merge proposal.
I have some ideas to improve the code, and I think the interface and model
are missing essential documentation and unit tests. I suspect that the
story tests (which are integration tests) are test what should be testing
as documentation and unit tests.
> === modified file 'lib/lp/ registry/ interfaces/ distribution. py' registry/ interfaces/ distribution. py 2010-02-27 10:19:18 +0000 registry/ interfaces/ distribution. py 2010-03-06 00:09:35 +0000 parameters( copy_field( IDistributionMi rror['country' ], required=True), type=copy_ field(IDistribu tionMirror[ 'content' ], required=True)) returns_ entry(IDistribu tionMirror) read_operation( ) r(country, mirror_type):
> --- lib/lp/
> +++ lib/lp/
> @@ -318,6 +318,16 @@
> if it's not found.
> """
>
> + @operation_
> + country=
> + mirror_
> + @operation_
> + @export_
> + def getCountryMirro
> + """Return the country DNS mirror for a given country and content
> + type.
> + """
> +
This docstring does not follow PEP 257. The first sentence must be one line. www.python. org/dev/ peps/pep- 0257/
Subsequent sentences may follow after a blank line:
http://
Think this fixes the issue:
"""Return the country DNS mirror for a country and content type."""
> === modified file 'lib/lp/ registry/ interfaces/ distributionmir ror.py' registry/ interfaces/ distributionmir ror.py 2010-02-22 15:50:06 +0000 registry/ interfaces/ distributionmir ror.py 2010-03-06 00:09:35 +0000 ionToCountryMir ror', AlreadySet' , irror', MirrorAdminRest ricted' , MirrorEditRestr icted', MirrorPublic' , rchSeries' , eriesSource' , cord', irrorSet' , DistroSeries' , CDImageFileList ', TPUrl', cial', CDImageFileList ']
> --- lib/lp/
> +++ lib/lp/
> @@ -6,21 +6,23 @@
> __metaclass__ = type
>
> __all__ = [
> +'CannotTransit
> +'CountryMirror
> 'IDistributionM
> -'IDistribution
> -'IDistribution
> -'IDistribution
> 'IMirrorDistroA
> 'IMirrorDistroS
> 'IMirrorProbeRe
> 'IDistributionM
> 'IMirrorCDImage
> 'PROBE_INTERVAL',
> -'UnableToFetch
> 'MirrorContent',
> 'MirrorFreshness',
> +'MirrorHasNoHT
> +'MirrorNotOffi
> +'MirrorNotProbed',
> 'MirrorSpeed',
> -'MirrorStatus']
> +'MirrorStatus',
> +'UnableToFetch
Per PEP 8, this list of single entries must each be indented and required a
trailing comma; the closing bracket on on a separate line to minimise diffs,
which I can see that this diff is already a victim:
'MirrorStatus', tchCDImageFileL ist',
'UnableToFe
]
> @@ -47,6 +52,44 @@ nToCountryMirro r(Exception) :
> PROBE_INTERVAL = 23
>
> +class CannotTransitio
> + """Root exception for transitions to country mirrors.
> + """
The closing quotes belong on the previous line PEP 257.
> + webservice_ error(400) # HTTP Error: 'Bad Request'.
Launchpad style does not use trailing comments because they interfere with
refactoring. I do not think comments about HTTP codes are informative;
we are expect to know them.
> @@ -386,6 +406,33 @@
> date_created = exported(Datetime(
> title=_('Date Crea...