Merge lp:~abentley/launchpad/run-via-celery into lp:launchpad

Proposed by Aaron Bentley
Status: Merged
Merged at revision: 15046
Proposed branch: lp:~abentley/launchpad/run-via-celery
Merge into: lp:launchpad
Prerequisite: lp:~abentley/launchpad/lp-lazr.jobrunner
Diff against target: 4851 lines (+701/-2692)
58 files modified
Makefile (+1/-1)
buildout.cfg (+1/-0)
lib/lp/app/templates/base-layout.pt (+0/-5)
lib/lp/bugs/model/bug.py (+0/-123)
lib/lp/bugs/model/bugtask.py (+0/-2)
lib/lp/bugs/scripts/bugimport.py (+0/-22)
lib/lp/bugs/tests/test_bug_mirror_access_triggers.py (+0/-12)
lib/lp/code/interfaces/branch.py (+4/-1)
lib/lp/code/model/branch.py (+12/-2)
lib/lp/code/model/branchjob.py (+67/-61)
lib/lp/code/model/tests/test_branch.py (+3/-2)
lib/lp/code/model/tests/test_branchpuller.py (+2/-2)
lib/lp/code/model/tests/test_branchtarget.py (+9/-5)
lib/lp/code/tests/test_directbranchcommit.py (+3/-2)
lib/lp/code/xmlrpc/tests/test_codehosting.py (+2/-1)
lib/lp/codehosting/bzrutils.py (+16/-3)
lib/lp/registry/browser/pillar.py (+5/-101)
lib/lp/registry/browser/product.py (+0/-6)
lib/lp/registry/browser/tests/test_pillar_sharing.py (+0/-194)
lib/lp/registry/enums.py (+0/-18)
lib/lp/registry/interfaces/accesspolicy.py (+0/-24)
lib/lp/registry/interfaces/productjob.py (+0/-70)
lib/lp/registry/interfaces/sharingservice.py (+0/-15)
lib/lp/registry/javascript/sharing/pillarsharingview.js (+0/-6)
lib/lp/registry/javascript/sharing/shareepicker.js (+0/-96)
lib/lp/registry/javascript/sharing/shareetable.js (+6/-37)
lib/lp/registry/javascript/sharing/tests/test_shareelisting_navigator.html (+0/-65)
lib/lp/registry/javascript/sharing/tests/test_shareepicker.js (+0/-9)
lib/lp/registry/javascript/sharing/tests/test_shareetable.html (+0/-11)
lib/lp/registry/javascript/sharing/tests/test_shareetable.js (+0/-111)
lib/lp/registry/model/accesspolicy.py (+0/-29)
lib/lp/registry/model/person.py (+0/-8)
lib/lp/registry/model/productjob.py (+0/-154)
lib/lp/registry/services/sharingservice.py (+0/-69)
lib/lp/registry/services/tests/test_sharingservice.py (+2/-150)
lib/lp/registry/subscribers.py (+137/-278)
lib/lp/registry/templates/pillar-sharing-details.pt (+0/-17)
lib/lp/registry/templates/pillar-sharing.pt (+0/-15)
lib/lp/registry/tests/test_accesspolicy.py (+0/-55)
lib/lp/registry/tests/test_pillar.py (+5/-14)
lib/lp/registry/tests/test_productjob.py (+0/-189)
lib/lp/registry/tests/test_subscribers.py (+260/-524)
lib/lp/scripts/garbo.py (+0/-42)
lib/lp/scripts/tests/test_garbo.py (+0/-27)
lib/lp/services/features/flags.py (+0/-13)
lib/lp/services/job/celeryconfig.py (+4/-0)
lib/lp/services/job/celeryjob.py (+30/-0)
lib/lp/services/job/interfaces/job.py (+0/-8)
lib/lp/services/job/model/job.py (+61/-2)
lib/lp/services/job/runner.py (+5/-2)
lib/lp/services/job/tests/celeryconfig.py (+7/-0)
lib/lp/services/job/tests/test_celeryjob.py (+42/-0)
lib/lp/services/job/tests/test_job.py (+1/-1)
lib/lp/services/job/tests/test_runner.py (+7/-20)
lib/lp/testing/factory.py (+3/-3)
lib/lp/testing/yuixhr.py (+0/-40)
lib/lp/translations/model/translationsharingjob.py (+6/-17)
versions.cfg (+0/-8)
To merge this branch: bzr merge lp:~abentley/launchpad/run-via-celery
Reviewer Review Type Date Requested Status
Abel Deuring (community) Approve
Review via email: mp+99099@code.launchpad.net

Commit message

Support running jobs via Celery

Description of the change

= Summary =
Support running Jobs via Celery

== Pre-implementation notes ==
Discussed with Deryck, adeuring, lifeless

LOC can be added because this is part of an arc that will remove most job-running code from Launchpad.

== Implementation details ==
Provide a Celery Task, CeleryRunJob, that will run a Job.

As CeleryRunJob's job source, provide UniversalJobSource, which uses Job.id to retrieve the corresponding RunnableJob. It also sets up an environment similar to the script environment for Launchpad code.

Move most code out of BranchScanJob.contextManager and BranchUpgradeJob.contextManager, so that lazr.jobrunner does not need to support per-job contextManagers.

To support UniversalJobSource, rework RegisteredSubclass as EnumeratedSubclass and make it the metaclass for BranchJobDerived.

CeleryRunJob overrides getJobRunner to return a BaseJobRunner, so that it knows how to run Launchpad jobs.

Switch to lazr.jobrunner's LeaseHeld exception, to ensure lease handling works correctly.

Update lp.codehosting.bzrutils.server to permit tests to override the server. This allows the server init code to move into Job.run.

Provide two different celeryconfigs:
- lib/lp/services/job/celeryconfig.py works for production or in the test suite. It gets its rabbitmq settings from lp.services.config.
- lib/lp/services/job/tests/celeryconfig.py works for running celeryd in the test suite. It gets its rabbitmq settings from the environment. It is single-process, to ensure tests are reproducible.

Update versions.cfg to list the current versions of Celery and its dependencies at this time.

== Tests ==
bin/test job -tCelery

== Demo and Q/A ==
None (this functionality is optional.)

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/services/job/tests/test_runner.py
  lib/lp/services/job/runner.py
  lib/lp/services/job/interfaces/job.py
  lib/lp/soyuz/model/packagecopyjob.py
  lib/lp/registry/tests/test_process_job_sources_cronjob.py
  buildout.cfg
  versions.cfg
  setup.py
  lib/lp/services/job/celeryjob.py
  lib/lp/codehosting/bzrutils.py
  lib/lp/translations/model/translationsharingjob.py
  lib/lp/testing/factory.py
  lib/lp/soyuz/tests/test_packagecopyjob.py
  lib/lp/services/job/tests/test_job.py
  lib/lp/services/job/tests/test_celeryjob.py
  lib/lp/services/job/tests/celeryconfig.py
  lib/lp/services/job/celeryconfig.py
  lib/lp/services/job/model/job.py
  lib/lp/code/model/branchjob.py

To post a comment you must log in.
Revision history for this message
Abel Deuring (adeuring) wrote :

Overall, a very nice branch.

As discused on IRC, calling CeleryRunJob.delay(BrachScanJob.create(...)) creates a race condition.

Another remark:

> === modified file 'lib/lp/code/model/branch.py'
> --- lib/lp/code/model/branch.py 2012-03-21 12:34:12 +0000
> +++ lib/lp/code/model/branch.py 2012-03-27 14:40:29 +0000
> @@ -1032,7 +1032,8 @@
> return getUtility(IBranchLookup).getByUniqueName(location)
>
> def branchChanged(self, stacked_on_url, last_revision_id,
> - control_format, branch_format, repository_format):
> + control_format, branch_format, repository_format,
> + skip_celery=False):

This changes needs a related change in the interface class. I think
that the new parameter deserves a short explanation: As I understand it,
skip_celery should be False only in tests.

review: Needs Fixing
Revision history for this message
Aaron Bentley (abentley) wrote :

I've used Transaction.addAfterCommitHook to ensure that CeleryRunJob.delay is invoked on (successful) commit.

I've inverted skip_celery to celery_scan, to make the documentation simpler, and documented it.

Revision history for this message
Abel Deuring (adeuring) wrote :

Looks good. Just one nitpick:

=== modified file 'lib/lp/code/interfaces/branch.py'
--- lib/lp/code/interfaces/branch.py 2012-02-20 02:07:55 +0000
+++ lib/lp/code/interfaces/branch.py 2012-03-27 18:56:12 +0000
@@ -1078,7 +1078,7 @@
         """Create an IBranchUpgradeJob to upgrade this branch."""

     def branchChanged(stacked_on_url, last_revision_id, control_format,
- branch_format, repository_format):
+ branch_format, repository_format, skip_celery=False):

s/skip_celery/celery_scan/

review: Approve
Revision history for this message
Aaron Bentley (abentley) wrote :

This branch can be QAed by pushing branches up to be scanned. This should behave normally. The celery side does not need to be tested, since it's not mandatory.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile'
2--- Makefile 2012-03-14 12:01:42 +0000
3+++ Makefile 2012-03-29 14:36:38 +0000
4@@ -305,7 +305,7 @@
5 $(PY) scripts/stop-loggerhead.py
6
7 run_codehosting: build inplace stop
8- bin/run -r librarian,sftp,forker,codebrowse -i $(LPCONFIG)
9+ bin/run -r librarian,sftp,forker,codebrowse,rabbitmq -i $(LPCONFIG)
10
11 start_librarian: compile
12 bin/start_librarian
13
14=== modified file 'buildout.cfg'
15--- buildout.cfg 2012-03-16 11:09:03 +0000
16+++ buildout.cfg 2012-03-29 14:36:38 +0000
17@@ -65,6 +65,7 @@
18 [scripts]
19 recipe = z3c.recipe.scripts
20 eggs = lp
21+ celery
22 funkload
23 zc.zservertracelog
24 pyinotify
25
26=== modified file 'lib/lp/app/templates/base-layout.pt'
27--- lib/lp/app/templates/base-layout.pt 2012-03-29 14:36:36 +0000
28+++ lib/lp/app/templates/base-layout.pt 2012-03-23 20:56:11 +0000
29@@ -79,14 +79,9 @@
30 <tal:login replace="structure context/@@login_status" />
31 </div><!--id="locationbar"-->
32
33-<<<<<<< TREE
34 <div id="watermark" class="watermark-apps-portlet"
35 tal:condition="view/macro:has-watermark">
36 <div>
37-=======
38- <div id="watermark" class="watermark-apps-portlet">
39- <div>
40->>>>>>> MERGE-SOURCE
41 <span tal:replace="structure view/watermark:logo"></span>
42 </div>
43 <div class="wide">
44
45=== modified file 'lib/lp/bugs/model/bug.py'
46--- lib/lp/bugs/model/bug.py 2012-03-29 14:36:36 +0000
47+++ lib/lp/bugs/model/bug.py 2012-03-29 06:02:46 +0000
48@@ -394,7 +394,6 @@
49 heat_last_updated = UtcDateTimeCol(default=None)
50 latest_patch_uploaded = UtcDateTimeCol(default=None)
51
52-<<<<<<< TREE
53 @property
54 def private(self):
55 return self.information_type in PRIVATE_INFORMATION_TYPES
56@@ -403,22 +402,6 @@
57 def security_related(self):
58 return self.information_type in SECURITY_INFORMATION_TYPES
59
60-=======
61- @property
62- def private(self):
63- if self.information_type:
64- return self.information_type in PRIVATE_INFORMATION_TYPES
65- else:
66- return self._private
67-
68- @property
69- def security_related(self):
70- if self.information_type:
71- return self.information_type in SECURITY_INFORMATION_TYPES
72- else:
73- return self._security_related
74-
75->>>>>>> MERGE-SOURCE
76 @cachedproperty
77 def _subscriber_cache(self):
78 """Caches known subscribers."""
79@@ -1727,106 +1710,6 @@
80
81 return bugtask
82
83-<<<<<<< TREE
84-=======
85- def _setInformationType(self):
86- if self._private and self._security_related:
87- self.information_type = InformationType.EMBARGOEDSECURITY
88- elif self._private:
89- self.information_type = InformationType.USERDATA
90- elif self._security_related:
91- self.information_type = InformationType.UNEMBARGOEDSECURITY
92- else:
93- self.information_type = InformationType.PUBLIC
94-
95- def setPrivacyAndSecurityRelated(self, private, security_related, who):
96- """ See `IBug`."""
97- private_changed = False
98- security_related_changed = False
99- bug_before_modification = Snapshot(self, providing=providedBy(self))
100-
101- f_flag_str = 'disclosure.enhanced_private_bug_subscriptions.enabled'
102- f_flag = bool(getFeatureFlag(f_flag_str))
103- if f_flag:
104- # Before we update the privacy or security_related status, we
105- # need to reconcile the subscribers to avoid leaking private
106- # information.
107- if (self.private != private
108- or self.security_related != security_related):
109- self.reconcileSubscribers(private, security_related, who)
110-
111- if self.private != private:
112- # We do not allow multi-pillar private bugs except for those teams
113- # who want to shoot themselves in the foot.
114- if private:
115- allow_multi_pillar_private = bool(getFeatureFlag(
116- 'disclosure.allow_multipillar_private_bugs.enabled'))
117- if (not allow_multi_pillar_private
118- and len(self.affected_pillars) > 1):
119- raise BugCannotBePrivate(
120- "Multi-pillar bugs cannot be private.")
121- private_changed = True
122- self._private = private
123-
124- if private:
125- self.who_made_private = who
126- self.date_made_private = UTC_NOW
127- else:
128- self.who_made_private = None
129- self.date_made_private = None
130-
131- # XXX: This should be a bulk update. RBC 20100827
132- # bug=https://bugs.launchpad.net/storm/+bug/625071
133- for attachment in self.attachments_unpopulated:
134- attachment.libraryfile.restricted = private
135-
136- if self.security_related != security_related:
137- security_related_changed = True
138- self._security_related = security_related
139-
140- if private_changed or security_related_changed:
141- # Correct the heat for the bug immediately, so that we don't have
142- # to wait for the next calculation job for the adjusted heat.
143- self.updateHeat()
144-
145- self._setInformationType()
146-
147- if private_changed or security_related_changed:
148- changed_fields = []
149-
150- if private_changed:
151- changed_fields.append('private')
152- if not f_flag and private:
153- # If we didn't call reconcileSubscribers, we may have
154- # bug supervisors who should be on this bug, but aren't.
155- supervisors = set()
156- for bugtask in self.bugtasks:
157- supervisors.add(bugtask.pillar.bug_supervisor)
158- if None in supervisors:
159- supervisors.remove(None)
160- for s in supervisors:
161- subscriptions = get_structural_subscriptions_for_bug(
162- self, s)
163- if subscriptions != []:
164- self.subscribe(s, who)
165-
166- if security_related_changed:
167- changed_fields.append('security_related')
168- if not f_flag and security_related:
169- # The bug turned out to be security-related, subscribe the
170- # security contact. We do it here only if the feature flag
171- # is not set, otherwise it's done in
172- # reconcileSubscribers().
173- for pillar in self.affected_pillars:
174- if pillar.security_contact is not None:
175- self.subscribe(pillar.security_contact, who)
176-
177- notify(ObjectModifiedEvent(
178- self, bug_before_modification, changed_fields, user=who))
179-
180- return private_changed, security_related_changed
181-
182->>>>>>> MERGE-SOURCE
183 def setPrivate(self, private, who):
184 """See `IBug`.
185
186@@ -2997,15 +2880,9 @@
187 params.information_type in SECURITY_INFORMATION_TYPES)
188 bug = Bug(
189 title=params.title, description=params.description,
190-<<<<<<< TREE
191 owner=params.owner, datecreated=params.datecreated,
192 information_type=params.information_type,
193 _private=private, _security_related=security_related,
194-=======
195- _private=params.private, owner=params.owner,
196- datecreated=params.datecreated,
197- _security_related=params.security_related,
198->>>>>>> MERGE-SOURCE
199 **extra_params)
200
201 if params.subscribe_owner:
202
203=== modified file 'lib/lp/bugs/model/bugtask.py'
204--- lib/lp/bugs/model/bugtask.py 2012-03-29 14:36:36 +0000
205+++ lib/lp/bugs/model/bugtask.py 2012-03-29 00:48:21 +0000
206@@ -43,10 +43,8 @@
207 And,
208 Join,
209 Or,
210- Select,
211 SQL,
212 Sum,
213- Union,
214 )
215 from storm.store import (
216 EmptyResultSet,
217
218=== modified file 'lib/lp/bugs/scripts/bugimport.py'
219--- lib/lp/bugs/scripts/bugimport.py 2012-03-29 14:36:36 +0000
220+++ lib/lp/bugs/scripts/bugimport.py 2012-03-23 07:17:15 +0000
221@@ -293,17 +293,11 @@
222
223 private = get_value(bugnode, 'private') == 'True'
224 security_related = get_value(bugnode, 'security_related') == 'True'
225-<<<<<<< TREE
226 # If the product has private_bugs, we force private to True.
227 if self.product.private_bugs:
228 private = True
229 information_type = convert_to_information_type(
230 private, security_related)
231-=======
232- # If the product has private_bugs, we force private to True.
233- if self.product.private_bugs:
234- private = True
235->>>>>>> MERGE-SOURCE
236
237 if owner is None:
238 owner = self.bug_importer
239@@ -311,17 +305,8 @@
240 msg = self.createMessage(commentnode, defaulttitle=title)
241
242 bug = self.product.createBug(CreateBugParams(
243-<<<<<<< TREE
244 msg=msg, datecreated=datecreated, title=title,
245 information_type=information_type, owner=owner))
246-=======
247- msg=msg,
248- datecreated=datecreated,
249- title=title,
250- private=private or security_related,
251- security_related=security_related,
252- owner=owner))
253->>>>>>> MERGE-SOURCE
254 bugtask = bug.bugtasks[0]
255 self.logger.info('Creating Launchpad bug #%d', bug.id)
256
257@@ -336,18 +321,11 @@
258 bug.linkMessage(msg)
259 self.createAttachments(bug, msg, commentnode)
260
261-<<<<<<< TREE
262 # Security bugs must be created private, so set it correctly.
263 if not self.product.private_bugs:
264 information_type = convert_to_information_type(
265 private, security_related)
266 bug.transitionToInformationType(information_type, owner)
267-=======
268- # Security bugs must be created private, so set it correctly.
269- if not self.product.private_bugs:
270- bug.setPrivacyAndSecurityRelated(
271- private, security_related, owner)
272->>>>>>> MERGE-SOURCE
273 bug.name = get_value(bugnode, 'nickname')
274 description = get_value(bugnode, 'description')
275 if description:
276
277=== modified file 'lib/lp/bugs/tests/test_bug_mirror_access_triggers.py'
278--- lib/lp/bugs/tests/test_bug_mirror_access_triggers.py 2012-03-29 14:36:36 +0000
279+++ lib/lp/bugs/tests/test_bug_mirror_access_triggers.py 2012-03-26 00:12:50 +0000
280@@ -136,11 +136,7 @@
281 bug = self.makeBugAndPolicies(private=True)
282 self.assertIsNot(
283 None, getUtility(IAccessArtifactSource).find([bug]).one())
284-<<<<<<< TREE
285 bug.setPrivate(False, bug.owner)
286-=======
287- removeSecurityProxy(bug).setPrivate(False, bug.owner)
288->>>>>>> MERGE-SOURCE
289 self.assertIs(
290 None, getUtility(IAccessArtifactSource).find([bug]).one())
291
292@@ -148,11 +144,7 @@
293 bug = self.makeBugAndPolicies(private=False)
294 self.assertIs(
295 None, getUtility(IAccessArtifactSource).find([bug]).one())
296-<<<<<<< TREE
297 bug.setPrivate(True, bug.owner)
298-=======
299- removeSecurityProxy(bug).setPrivate(True, bug.owner)
300->>>>>>> MERGE-SOURCE
301 self.assertIsNot(
302 None, getUtility(IAccessArtifactSource).find([bug]).one())
303 self.assertEqual((1, 1), self.assertMirrored(bug))
304@@ -166,11 +158,7 @@
305 self.assertContentEqual(
306 [InformationType.USERDATA],
307 self.getPolicyTypesForArtifact(artifact))
308-<<<<<<< TREE
309 bug.setSecurityRelated(True, bug.owner)
310-=======
311- removeSecurityProxy(bug).setSecurityRelated(True, bug.owner)
312->>>>>>> MERGE-SOURCE
313 self.assertEqual((1, 1), self.assertMirrored(bug))
314 self.assertContentEqual(
315 [InformationType.EMBARGOEDSECURITY],
316
317=== modified file 'lib/lp/code/interfaces/branch.py'
318--- lib/lp/code/interfaces/branch.py 2012-02-20 02:07:55 +0000
319+++ lib/lp/code/interfaces/branch.py 2012-03-29 14:36:38 +0000
320@@ -1078,7 +1078,7 @@
321 """Create an IBranchUpgradeJob to upgrade this branch."""
322
323 def branchChanged(stacked_on_url, last_revision_id, control_format,
324- branch_format, repository_format):
325+ branch_format, repository_format, celery_scan=True):
326 """Record that a branch has been changed.
327
328 This method records the stacked on branch tip revision id and format
329@@ -1092,6 +1092,9 @@
330 :param branch_format: The entry from BranchFormat for the branch.
331 :param repository_format: The entry from RepositoryFormat for the
332 branch.
333+ :param celery_scan: If True, request a branch scan via Celery.
334+ Otherwise, a BranchScanJob may be created, but not requested to
335+ run. Should only be False in certain tests.
336 """
337
338 @export_destructor_operation()
339
340=== modified file 'lib/lp/code/model/branch.py'
341--- lib/lp/code/model/branch.py 2012-03-21 12:34:12 +0000
342+++ lib/lp/code/model/branch.py 2012-03-29 14:36:38 +0000
343@@ -40,6 +40,7 @@
344 Reference,
345 )
346 from storm.store import Store
347+import transaction
348 from zope.component import getUtility
349 from zope.event import notify
350 from zope.interface import implements
351@@ -1032,7 +1033,8 @@
352 return getUtility(IBranchLookup).getByUniqueName(location)
353
354 def branchChanged(self, stacked_on_url, last_revision_id,
355- control_format, branch_format, repository_format):
356+ control_format, branch_format, repository_format,
357+ celery_scan=True):
358 """See `IBranch`."""
359 self.mirror_status_message = None
360 if stacked_on_url == '' or stacked_on_url is None:
361@@ -1057,7 +1059,15 @@
362 self.last_mirrored_id = last_revision_id
363 if self.last_scanned_id != last_revision_id:
364 from lp.code.model.branchjob import BranchScanJob
365- BranchScanJob.create(self)
366+ job_id = BranchScanJob.create(self).job_id
367+ if celery_scan:
368+ # lp.services.job.celery is imported only where needed.
369+ from lp.services.job.celeryjob import CeleryRunJob
370+ current = transaction.get()
371+ def runHook(succeeded):
372+ if succeeded:
373+ CeleryRunJob.delay(job_id)
374+ current.addAfterCommitHook(runHook)
375 self.control_format = control_format
376 self.branch_format = branch_format
377 self.repository_format = repository_format
378
379=== modified file 'lib/lp/code/model/branchjob.py'
380--- lib/lp/code/model/branchjob.py 2012-02-21 19:13:45 +0000
381+++ lib/lp/code/model/branchjob.py 2012-03-29 14:36:38 +0000
382@@ -80,6 +80,7 @@
383 from lp.code.model.branch import Branch
384 from lp.code.model.branchmergeproposal import BranchMergeProposal
385 from lp.code.model.revision import RevisionSet
386+from lp.codehosting.bzrutils import server
387 from lp.codehosting.scanner.bzrsync import BzrSync
388 from lp.codehosting.vfs import (
389 branch_id_to_path,
390@@ -93,7 +94,10 @@
391 from lp.services.database.lpstorm import IStore
392 from lp.services.database.sqlbase import SQLBase
393 from lp.services.job.interfaces.job import JobStatus
394-from lp.services.job.model.job import Job
395+from lp.services.job.model.job import (
396+ EnumeratedSubclass,
397+ Job,
398+ )
399 from lp.services.job.runner import BaseRunnableJob
400 from lp.services.mail.sendmail import format_address_for_person
401 from lp.services.webapp import (
402@@ -212,9 +216,14 @@
403 SQLBase.destroySelf(self)
404 self.job.destroySelf()
405
406+ def makeDerived(self):
407+ return BranchJobDerived.makeSubclass(self)
408+
409
410 class BranchJobDerived(BaseRunnableJob):
411
412+ __metaclass__ = EnumeratedSubclass
413+
414 delegates(IBranchJob)
415
416 def __init__(self, branch_job):
417@@ -287,29 +296,26 @@
418 classProvides(IBranchScanJobSource)
419 class_job_type = BranchJobType.SCAN_BRANCH
420 memory_limit = 2 * (1024 ** 3)
421- server = None
422
423 @classmethod
424 def create(cls, branch):
425 """See `IBranchScanJobSource`."""
426- branch_job = BranchJob(branch, BranchJobType.SCAN_BRANCH, {})
427+ branch_job = BranchJob(branch, cls.class_job_type, {})
428 return cls(branch_job)
429
430 def run(self):
431 """See `IBranchScanJob`."""
432 from lp.services.scripts import log
433- bzrsync = BzrSync(self.branch, log)
434- bzrsync.syncBranchAndClose()
435+ with server(get_ro_server(), no_replace=True):
436+ bzrsync = BzrSync(self.branch, log)
437+ bzrsync.syncBranchAndClose()
438
439 @classmethod
440 @contextlib.contextmanager
441 def contextManager(cls):
442 """See `IBranchScanJobSource`."""
443 errorlog.globalErrorUtility.configure('branchscanner')
444- cls.server = get_ro_server()
445- cls.server.start_server()
446 yield
447- cls.server.stop_server()
448
449
450 class BranchUpgradeJob(BranchJobDerived):
451@@ -330,7 +336,7 @@
452 """See `IBranchUpgradeJobSource`."""
453 branch.checkUpgrade()
454 branch_job = BranchJob(
455- branch, BranchJobType.UPGRADE_BRANCH, {}, requester=requester)
456+ branch, cls.class_job_type, {}, requester=requester)
457 return cls(branch_job)
458
459 @staticmethod
460@@ -338,63 +344,63 @@
461 def contextManager():
462 """See `IBranchUpgradeJobSource`."""
463 errorlog.globalErrorUtility.configure('upgrade_branches')
464- server = get_rw_server()
465- server.start_server()
466 yield
467- server.stop_server()
468
469 def run(self, _check_transaction=False):
470 """See `IBranchUpgradeJob`."""
471 # Set up the new branch structure
472- upgrade_branch_path = tempfile.mkdtemp()
473- try:
474- upgrade_transport = get_transport(upgrade_branch_path)
475- upgrade_transport.mkdir('.bzr')
476- source_branch_transport = get_transport(
477- self.branch.getInternalBzrUrl())
478- source_branch_transport.clone('.bzr').copy_tree_to_transport(
479- upgrade_transport.clone('.bzr'))
480- transaction.commit()
481- upgrade_branch = BzrBranch.open_from_transport(upgrade_transport)
482-
483- # No transactions are open so the DB connection won't be killed.
484- with TransactionFreeOperation():
485- # Perform the upgrade.
486- upgrade(upgrade_branch.base)
487-
488- # Re-open the branch, since its format has changed.
489- upgrade_branch = BzrBranch.open_from_transport(
490- upgrade_transport)
491- source_branch = BzrBranch.open_from_transport(
492- source_branch_transport)
493-
494- source_branch.lock_write()
495- upgrade_branch.pull(source_branch)
496- upgrade_branch.fetch(source_branch)
497- source_branch.unlock()
498-
499- # Move the branch in the old format to backup.bzr
500+ with server(get_rw_server(), no_replace=True):
501+ upgrade_branch_path = tempfile.mkdtemp()
502 try:
503- source_branch_transport.delete_tree('backup.bzr')
504- except NoSuchFile:
505- pass
506- source_branch_transport.rename('.bzr', 'backup.bzr')
507- source_branch_transport.mkdir('.bzr')
508- upgrade_transport.clone('.bzr').copy_tree_to_transport(
509- source_branch_transport.clone('.bzr'))
510-
511- # Re-open the source branch again.
512- source_branch = BzrBranch.open_from_transport(
513- source_branch_transport)
514-
515- formats = get_branch_formats(source_branch)
516-
517- self.branch.branchChanged(
518- self.branch.stacked_on,
519- self.branch.last_scanned_id,
520- *formats)
521- finally:
522- shutil.rmtree(upgrade_branch_path)
523+ upgrade_transport = get_transport(upgrade_branch_path)
524+ upgrade_transport.mkdir('.bzr')
525+ source_branch_transport = get_transport(
526+ self.branch.getInternalBzrUrl())
527+ source_branch_transport.clone('.bzr').copy_tree_to_transport(
528+ upgrade_transport.clone('.bzr'))
529+ transaction.commit()
530+ upgrade_branch = BzrBranch.open_from_transport(
531+ upgrade_transport)
532+
533+ # No transactions are open so the DB connection won't be
534+ # killed.
535+ with TransactionFreeOperation():
536+ # Perform the upgrade.
537+ upgrade(upgrade_branch.base)
538+
539+ # Re-open the branch, since its format has changed.
540+ upgrade_branch = BzrBranch.open_from_transport(
541+ upgrade_transport)
542+ source_branch = BzrBranch.open_from_transport(
543+ source_branch_transport)
544+
545+ source_branch.lock_write()
546+ upgrade_branch.pull(source_branch)
547+ upgrade_branch.fetch(source_branch)
548+ source_branch.unlock()
549+
550+ # Move the branch in the old format to backup.bzr
551+ try:
552+ source_branch_transport.delete_tree('backup.bzr')
553+ except NoSuchFile:
554+ pass
555+ source_branch_transport.rename('.bzr', 'backup.bzr')
556+ source_branch_transport.mkdir('.bzr')
557+ upgrade_transport.clone('.bzr').copy_tree_to_transport(
558+ source_branch_transport.clone('.bzr'))
559+
560+ # Re-open the source branch again.
561+ source_branch = BzrBranch.open_from_transport(
562+ source_branch_transport)
563+
564+ formats = get_branch_formats(source_branch)
565+
566+ self.branch.branchChanged(
567+ self.branch.stacked_on,
568+ self.branch.last_scanned_id,
569+ *formats)
570+ finally:
571+ shutil.rmtree(upgrade_branch_path)
572
573
574 class RevisionMailJob(BranchJobDerived):
575@@ -415,7 +421,7 @@
576 'body': body,
577 'subject': subject,
578 }
579- branch_job = BranchJob(branch, BranchJobType.REVISION_MAIL, metadata)
580+ branch_job = BranchJob(branch, cls.class_job_type, metadata)
581 return cls(branch_job)
582
583 @property
584
585=== modified file 'lib/lp/code/model/tests/test_branch.py'
586--- lib/lp/code/model/tests/test_branch.py 2012-02-15 08:13:51 +0000
587+++ lib/lp/code/model/tests/test_branch.py 2012-03-29 14:36:38 +0000
588@@ -130,6 +130,7 @@
589 from lp.testing.layers import (
590 AppServerLayer,
591 DatabaseFunctionalLayer,
592+ LaunchpadFunctionalLayer,
593 LaunchpadZopelessLayer,
594 )
595 from lp.translations.model.translationtemplatesbuildjob import (
596@@ -159,7 +160,7 @@
597 class TestBranchChanged(TestCaseWithFactory):
598 """Tests for `IBranch.branchChanged`."""
599
600- layer = DatabaseFunctionalLayer
601+ layer = LaunchpadFunctionalLayer
602
603 def setUp(self):
604 TestCaseWithFactory.setUp(self)
605@@ -2144,7 +2145,7 @@
606 class TestPendingWrites(TestCaseWithFactory):
607 """Are there changes to this branch not reflected in the database?"""
608
609- layer = DatabaseFunctionalLayer
610+ layer = LaunchpadFunctionalLayer
611
612 def test_new_branch_no_writes(self):
613 # New branches have no pending writes.
614
615=== modified file 'lib/lp/code/model/tests/test_branchpuller.py'
616--- lib/lp/code/model/tests/test_branchpuller.py 2012-01-06 11:08:30 +0000
617+++ lib/lp/code/model/tests/test_branchpuller.py 2012-03-29 14:36:38 +0000
618@@ -89,7 +89,7 @@
619 transaction.commit()
620 branch.startMirroring()
621 removeSecurityProxy(branch).branchChanged(
622- '', 'rev1', None, None, None)
623+ '', 'rev1', None, None, None, celery_scan=False)
624 self.assertEqual(None, branch.next_mirror_time)
625
626 def test_mirrorFailureResetsMirrorRequest(self):
627@@ -158,7 +158,7 @@
628 transaction.commit()
629 branch.startMirroring()
630 removeSecurityProxy(branch).branchChanged(
631- '', 'rev1', None, None, None)
632+ '', 'rev1', None, None, None, celery_scan=False)
633 self.assertInFuture(branch.next_mirror_time, self.increment)
634 self.assertEqual(0, branch.mirror_failures)
635
636
637=== modified file 'lib/lp/code/model/tests/test_branchtarget.py'
638--- lib/lp/code/model/tests/test_branchtarget.py 2012-01-01 02:58:52 +0000
639+++ lib/lp/code/model/tests/test_branchtarget.py 2012-03-29 14:36:38 +0000
640@@ -129,7 +129,8 @@
641 default_branch = self.factory.makePackageBranch(
642 sourcepackage=development_package)
643 removeSecurityProxy(default_branch).branchChanged(
644- '', self.factory.getUniqueString(), None, None, None)
645+ '', self.factory.getUniqueString(), None, None, None,
646+ celery_scan=False)
647 registrant = development_package.distribution.owner
648 with person_logged_in(registrant):
649 development_package.setBranch(
650@@ -397,7 +398,7 @@
651 branch = self.factory.makeProductBranch(product=self.original)
652 self._setDevelopmentFocus(self.original, branch)
653 removeSecurityProxy(branch).branchChanged(
654- '', 'rev1', None, None, None)
655+ '', 'rev1', None, None, None, celery_scan=False)
656 target = IBranchTarget(self.original)
657 self.assertEqual(branch, target.default_stacked_on_branch)
658
659@@ -537,7 +538,8 @@
660 branch = self.factory.makeAnyBranch(branch_type=BranchType.MIRRORED)
661 branch.startMirroring()
662 removeSecurityProxy(branch).branchChanged(
663- '', self.factory.getUniqueString(), None, None, None)
664+ '', self.factory.getUniqueString(), None, None, None,
665+ celery_scan=False)
666 removeSecurityProxy(branch).branch_type = BranchType.REMOTE
667 self.assertIs(None, check_default_stacked_on(branch))
668
669@@ -553,14 +555,16 @@
670 branch = self.factory.makeAnyBranch(private=True)
671 naked_branch = removeSecurityProxy(branch)
672 naked_branch.branchChanged(
673- '', self.factory.getUniqueString(), None, None, None)
674+ '', self.factory.getUniqueString(), None, None, None,
675+ celery_scan=False)
676 self.assertIs(None, check_default_stacked_on(branch))
677
678 def test_been_mirrored(self):
679 # `check_default_stacked_on` returns the branch if it has revisions.
680 branch = self.factory.makeAnyBranch()
681 removeSecurityProxy(branch).branchChanged(
682- '', self.factory.getUniqueString(), None, None, None)
683+ '', self.factory.getUniqueString(), None, None, None,
684+ celery_scan=False)
685 self.assertEqual(branch, check_default_stacked_on(branch))
686
687
688
689=== modified file 'lib/lp/code/tests/test_directbranchcommit.py'
690--- lib/lp/code/tests/test_directbranchcommit.py 2012-01-01 02:58:52 +0000
691+++ lib/lp/code/tests/test_directbranchcommit.py 2012-03-29 14:36:38 +0000
692@@ -1,4 +1,4 @@
693-# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
694+# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
695 # GNU Affero General Public License version 3 (see the file LICENSE).
696
697 """Tests for `DirectBranchCommit`."""
698@@ -20,6 +20,7 @@
699 from lp.testing.fakemethod import FakeMethod
700 from lp.testing.layers import (
701 DatabaseFunctionalLayer,
702+ LaunchpadZopelessLayer,
703 ZopelessDatabaseLayer,
704 )
705
706@@ -63,7 +64,7 @@
707 class TestDirectBranchCommit(DirectBranchCommitTestCase, TestCaseWithFactory):
708 """Test `DirectBranchCommit`."""
709
710- layer = ZopelessDatabaseLayer
711+ layer = LaunchpadZopelessLayer
712
713 def test_defaults_to_branch_owner(self):
714 # If no committer is given, DirectBranchCommits defaults to
715
716=== modified file 'lib/lp/code/xmlrpc/tests/test_codehosting.py'
717--- lib/lp/code/xmlrpc/tests/test_codehosting.py 2012-02-21 22:46:28 +0000
718+++ lib/lp/code/xmlrpc/tests/test_codehosting.py 2012-03-29 14:36:38 +0000
719@@ -56,6 +56,7 @@
720 from lp.testing.layers import (
721 DatabaseFunctionalLayer,
722 FunctionalLayer,
723+ LaunchpadFunctionalLayer,
724 )
725 from lp.xmlrpc import faults
726
727@@ -1290,7 +1291,7 @@
728 ])
729 scenarios = [
730 ('db', {'frontend': LaunchpadDatabaseFrontend,
731- 'layer': DatabaseFunctionalLayer}),
732+ 'layer': LaunchpadFunctionalLayer}),
733 ('inmemory', {'frontend': InMemoryFrontend,
734 'layer': FunctionalLayer}),
735 ]
736
737=== modified file 'lib/lp/codehosting/bzrutils.py'
738--- lib/lp/codehosting/bzrutils.py 2012-02-24 16:51:25 +0000
739+++ lib/lp/codehosting/bzrutils.py 2012-03-29 14:36:38 +0000
740@@ -19,6 +19,7 @@
741 'identical_formats',
742 'install_oops_handler',
743 'is_branch_stackable',
744+ 'server',
745 'read_locked',
746 'remove_exception_logging_hook',
747 ]
748@@ -33,6 +34,7 @@
749 )
750 from bzrlib.errors import (
751 NotStacked,
752+ UnsupportedProtocol,
753 UnstackableBranchFormat,
754 UnstackableRepositoryFormat,
755 )
756@@ -42,6 +44,7 @@
757 RemoteRepository,
758 )
759 from bzrlib.transport import (
760+ get_transport,
761 register_transport,
762 unregister_transport,
763 )
764@@ -332,9 +335,19 @@
765
766
767 @contextmanager
768-def server(server):
769- server.start_server()
770+def server(server, no_replace=False):
771+ run_server = True
772+ if no_replace:
773+ try:
774+ get_transport(server.get_url())
775+ except UnsupportedProtocol:
776+ pass
777+ else:
778+ run_server = False
779+ if run_server:
780+ server.start_server()
781 try:
782 yield server
783 finally:
784- server.stop_server()
785+ if run_server:
786+ server.stop_server()
787
788=== modified file 'lib/lp/registry/browser/pillar.py'
789--- lib/lp/registry/browser/pillar.py 2012-03-29 14:36:36 +0000
790+++ lib/lp/registry/browser/pillar.py 2012-03-28 01:51:45 +0000
791@@ -20,13 +20,6 @@
792 from lazr.restful import ResourceJSONEncoder
793 from lazr.restful.interfaces import IJSONRequestCache
794 import simplejson
795-<<<<<<< TREE
796-=======
797-
798-from lazr.restful import ResourceJSONEncoder
799-from lazr.restful.interfaces import IJSONRequestCache
800-
801->>>>>>> MERGE-SOURCE
802 from zope.component import getUtility
803 from zope.interface import (
804 implements,
805@@ -48,19 +41,12 @@
806 from lp.bugs.browser.structuralsubscription import (
807 StructuralSubscriptionMenuMixin,
808 )
809-<<<<<<< TREE
810 from lp.bugs.interfaces.bug import IBug
811 from lp.code.interfaces.branch import IBranch
812 from lp.registry.interfaces.accesspolicy import (
813 IAccessPolicyGrantFlatSource,
814 IAccessPolicySource,
815 )
816-=======
817-from lp.registry.interfaces.accesspolicy import (
818- IAccessPolicyGrantFlatSource,
819- IAccessPolicySource,
820- )
821->>>>>>> MERGE-SOURCE
822 from lp.registry.interfaces.distributionsourcepackage import (
823 IDistributionSourcePackage,
824 )
825@@ -68,30 +54,15 @@
826 from lp.registry.interfaces.person import IPersonSet
827 from lp.registry.interfaces.pillar import IPillar
828 from lp.registry.interfaces.projectgroup import IProjectGroup
829-<<<<<<< TREE
830 from lp.registry.model.pillar import PillarPerson
831 from lp.services.config import config
832 from lp.services.features import getFeatureFlag
833-=======
834-from lp.registry.interfaces.person import IPersonSet
835-from lp.registry.model.pillar import PillarPerson
836-from lp.services.config import config
837->>>>>>> MERGE-SOURCE
838 from lp.services.propertycache import cachedproperty
839-<<<<<<< TREE
840-from lp.services.webapp.authorization import check_permission
841-from lp.services.webapp.batching import (
842- BatchNavigator,
843- StormRangeFactory,
844- )
845-=======
846-from lp.services.features import getFeatureFlag
847-from lp.services.webapp.authorization import check_permission
848-from lp.services.webapp.batching import (
849- BatchNavigator,
850- StormRangeFactory,
851- )
852->>>>>>> MERGE-SOURCE
853+from lp.services.webapp.authorization import check_permission
854+from lp.services.webapp.batching import (
855+ BatchNavigator,
856+ StormRangeFactory,
857+ )
858 from lp.services.webapp.menu import (
859 ApplicationMenu,
860 enabled_with_permission,
861@@ -326,7 +297,6 @@
862 return simplejson.dumps(
863 self.sharing_picker_config, cls=ResourceJSONEncoder)
864
865-<<<<<<< TREE
866 def _getBatchNavigator(self, sharees):
867 """Return the batch navigator to be used to batch the sharees."""
868 return BatchNavigator(
869@@ -344,25 +314,6 @@
870
871 def unbatched_sharees(self):
872 """All the sharees for a pillar."""
873-=======
874- def _getBatchNavigator(self, sharees):
875- """Return the batch navigator to be used to batch the sharees."""
876- return BatchNavigator(
877- sharees, self.request,
878- hide_counts=True,
879- size=config.launchpad.default_batch_size,
880- range_factory=StormRangeFactory(sharees))
881-
882- def shareeData(self):
883- """Return an `ITableBatchNavigator` for sharees."""
884- if self._batch_navigator is None:
885- unbatchedSharees = self.unbatchedShareeData()
886- self._batch_navigator = self._getBatchNavigator(unbatchedSharees)
887- return self._batch_navigator
888-
889- def unbatchedShareeData(self):
890- """Return all the sharees for a pillar."""
891->>>>>>> MERGE-SOURCE
892 return self._getSharingService().getPillarSharees(self.context)
893
894 def initialize(self):
895@@ -379,7 +330,6 @@
896 and check_permission('launchpad.Edit', self.context))
897 cache.objects['information_types'] = self.information_types
898 cache.objects['sharing_permissions'] = self.sharing_permissions
899-<<<<<<< TREE
900
901 view_names = set(reg.name for reg
902 in iter_view_registrations(self.__class__))
903@@ -455,49 +405,3 @@
904 branch_name=branch.unique_name,
905 branch_id=branch.id))
906 return branch_data
907-=======
908-
909- view_names = set(reg.name for reg
910- in iter_view_registrations(self.__class__))
911- if len(view_names) != 1:
912- raise AssertionError("Ambiguous view name.")
913- cache.objects['view_name'] = view_names.pop()
914- batch_navigator = self.shareeData()
915- cache.objects['sharee_data'] = (
916- self._getSharingService().getPillarShareeData(
917- self.context, batch_navigator.batch))
918-
919- def _getBatchInfo(batch):
920- if batch is None:
921- return None
922- return {'memo': batch.range_memo,
923- 'start': batch.startNumber() - 1}
924-
925- next_batch = batch_navigator.batch.nextBatch()
926- cache.objects['next'] = _getBatchInfo(next_batch)
927- prev_batch = batch_navigator.batch.prevBatch()
928- cache.objects['prev'] = _getBatchInfo(prev_batch)
929- cache.objects['total'] = batch_navigator.batch.total()
930- cache.objects['forwards'] = batch_navigator.batch.range_forwards
931- last_batch = batch_navigator.batch.lastBatch()
932- cache.objects['last_start'] = last_batch.startNumber() - 1
933- cache.objects.update(_getBatchInfo(batch_navigator.batch))
934-
935-
936-class PillarPersonSharingView(LaunchpadView):
937-
938- page_title = "Person or team"
939- label = "Information shared with person or team"
940-
941- def initialize(self):
942- enabled_flag = 'disclosure.enhanced_sharing.enabled'
943- enabled = bool(getFeatureFlag(enabled_flag))
944- if not enabled:
945- raise Unauthorized("This feature is not yet available.")
946-
947- self.pillar = self.context.pillar
948- self.person = self.context.person
949-
950- self.label = "Information shared with %s" % self.person.displayname
951- self.page_title = "%s" % self.person.displayname
952->>>>>>> MERGE-SOURCE
953
954=== modified file 'lib/lp/registry/browser/product.py'
955--- lib/lp/registry/browser/product.py 2012-03-29 14:36:36 +0000
956+++ lib/lp/registry/browser/product.py 2012-03-22 23:21:24 +0000
957@@ -194,13 +194,7 @@
958 from lp.services.webapp.authorization import check_permission
959 from lp.services.webapp.batching import BatchNavigator
960 from lp.services.webapp.breadcrumb import Breadcrumb
961-<<<<<<< TREE
962 from lp.services.webapp.interfaces import UnsafeFormGetSubmissionError
963-=======
964-from lp.services.webapp.interfaces import (
965- UnsafeFormGetSubmissionError,
966- )
967->>>>>>> MERGE-SOURCE
968 from lp.services.webapp.menu import NavigationMenu
969 from lp.services.worlddata.helpers import browser_languages
970 from lp.services.worlddata.interfaces.country import ICountry
971
972=== modified file 'lib/lp/registry/browser/tests/test_pillar_sharing.py'
973--- lib/lp/registry/browser/tests/test_pillar_sharing.py 2012-03-29 14:36:36 +0000
974+++ lib/lp/registry/browser/tests/test_pillar_sharing.py 2012-03-28 01:51:45 +0000
975@@ -7,7 +7,6 @@
976
977 from BeautifulSoup import BeautifulSoup
978 from lazr.restful.interfaces import IJSONRequestCache
979-<<<<<<< TREE
980 import simplejson
981 from testtools.matchers import (
982 LessThan,
983@@ -15,29 +14,15 @@
984 Not,
985 Raises,
986 )
987-=======
988-from testtools.matchers import (
989- LessThan,
990- MatchesException,
991- Not,
992- Raises,
993- )
994->>>>>>> MERGE-SOURCE
995 from zope.component import getUtility
996 from zope.publisher.interfaces import NotFound
997 from zope.security.interfaces import Unauthorized
998
999 from lp.app.interfaces.services import IService
1000-<<<<<<< TREE
1001 from lp.registry.enums import InformationType
1002 from lp.registry.interfaces.accesspolicy import IAccessPolicyGrantFlatSource
1003 from lp.registry.model.pillar import PillarPerson
1004 from lp.services.config import config
1005-=======
1006-from lp.registry.enums import InformationType
1007-from lp.registry.model.pillar import PillarPerson
1008-from lp.services.config import config
1009->>>>>>> MERGE-SOURCE
1010 from lp.services.features.testing import FeatureFixture
1011 from lp.services.webapp.interfaces import StormRangeFactoryError
1012 from lp.services.webapp.publisher import canonical_url
1013@@ -55,7 +40,6 @@
1014 )
1015
1016
1017-<<<<<<< TREE
1018 DETAILS_ENABLED_FLAG = {'disclosure.enhanced_sharing_details.enabled': 'true'}
1019 ENABLED_FLAG = {'disclosure.enhanced_sharing.enabled': 'true'}
1020 WRITE_FLAG = {'disclosure.enhanced_sharing.writable': 'true'}
1021@@ -149,106 +133,12 @@
1022 owner=self.owner, driver=self.driver)
1023 login_person(self.driver)
1024
1025-=======
1026-ENABLED_FLAG = {'disclosure.enhanced_sharing.enabled': 'true'}
1027-WRITE_FLAG = {'disclosure.enhanced_sharing.writable': 'true'}
1028-
1029-
1030-class PillarSharingDetailsMixin:
1031- """Test the pillar sharing details view."""
1032-
1033- layer = DatabaseFunctionalLayer
1034-
1035- def getPillarPerson(self, person=None, with_sharing=True):
1036- if person is None:
1037- person = self.factory.makePerson()
1038- if with_sharing:
1039- if self.pillar_type == 'product':
1040- bug = self.factory.makeBug(product=self.pillar, private=True)
1041- elif self.pillar_type == 'distribution':
1042- bug = self.factory.makeBug(
1043- distribution=self.pillar, private=True)
1044- artifact = self.factory.makeAccessArtifact(concrete=bug)
1045- policy = self.factory.makeAccessPolicy(pillar=self.pillar)
1046- self.factory.makeAccessPolicyArtifact(
1047- artifact=artifact, policy=policy)
1048- self.factory.makeAccessArtifactGrant(
1049- artifact=artifact, grantee=person, grantor=self.pillar.owner)
1050-
1051- return PillarPerson(self.pillar, person)
1052-
1053- def test_view_traverses_plus_sharingdetails(self):
1054- # The traversed url in the app is pillar/+sharingdetails/person
1055- with FeatureFixture(ENABLED_FLAG):
1056- # We have to do some fun url hacking to force the traversal a user
1057- # encounters.
1058- pillarperson = self.getPillarPerson()
1059- expected = pillarperson.person.displayname
1060- url = 'http://launchpad.dev/%s/+sharingdetails/%s' % (
1061- pillarperson.pillar.name, pillarperson.person.name)
1062- browser = self.getUserBrowser(user=self.driver, url=url)
1063- self.assertEqual(expected, browser.title)
1064-
1065- def test_not_found_without_sharing(self):
1066- # If there is no sharing between pillar and person, NotFound is the
1067- # result.
1068- with FeatureFixture(ENABLED_FLAG):
1069- # We have to do some fun url hacking to force the traversal a user
1070- # encounters.
1071- pillarperson = self.getPillarPerson(with_sharing=False)
1072- url = 'http://launchpad.dev/%s/+sharingdetails/%s' % (
1073- pillarperson.pillar.name, pillarperson.person.name)
1074- browser = self.getUserBrowser(user=self.driver)
1075- self.assertRaises(NotFound, browser.open, url)
1076-
1077- def test_init_without_feature_flag(self):
1078- # We need a feature flag to enable the view.
1079- pillarperson = self.getPillarPerson()
1080- self.assertRaises(
1081- Unauthorized, create_initialized_view, pillarperson, '+index')
1082-
1083- def test_init_with_feature_flag(self):
1084- # The view works with a feature flag.
1085- with FeatureFixture(ENABLED_FLAG):
1086- pillarperson = self.getPillarPerson()
1087- view = create_initialized_view(pillarperson, '+index')
1088- self.assertEqual(pillarperson.person.displayname, view.page_title)
1089-
1090-
1091-class TestProductSharingDetailsView(
1092- TestCaseWithFactory, PillarSharingDetailsMixin):
1093-
1094- pillar_type = 'product'
1095-
1096- def setUp(self):
1097- super(TestProductSharingDetailsView, self).setUp()
1098- self.driver = self.factory.makePerson()
1099- self.owner = self.factory.makePerson()
1100- self.pillar = self.factory.makeProduct(
1101- owner=self.owner, driver=self.driver)
1102- login_person(self.driver)
1103-
1104-
1105-class TestDistributionSharingDetailsView(
1106- TestCaseWithFactory, PillarSharingDetailsMixin):
1107-
1108- pillar_type = 'distribution'
1109-
1110- def setUp(self):
1111- super(TestDistributionSharingDetailsView, self).setUp()
1112- self.driver = self.factory.makePerson()
1113- self.owner = self.factory.makePerson()
1114- self.pillar = self.factory.makeProduct(
1115- owner=self.owner, driver=self.driver)
1116- login_person(self.driver)
1117->>>>>>> MERGE-SOURCE
1118
1119 class PillarSharingViewTestMixin:
1120 """Test the PillarSharingView."""
1121
1122 layer = DatabaseFunctionalLayer
1123
1124-<<<<<<< TREE
1125 def createSharees(self):
1126 login_person(self.owner)
1127 self.access_policy = self.factory.makeAccessPolicy(
1128@@ -271,30 +161,6 @@
1129 for x in range(10):
1130 makeGrants('name%s' % x)
1131
1132-=======
1133- def createSharees(self):
1134- login_person(self.owner)
1135- access_policy = self.factory.makeAccessPolicy(
1136- pillar=self.pillar,
1137- type=InformationType.PROPRIETARY)
1138- self.grantees = []
1139-
1140- def makeGrants(name):
1141- grantee = self.factory.makePerson(name=name)
1142- self.grantees.append(grantee)
1143- # Make access policy grant so that 'All' is returned.
1144- self.factory.makeAccessPolicyGrant(access_policy, grantee)
1145- # Make access artifact grants so that 'Some' is returned.
1146- artifact_grant = self.factory.makeAccessArtifactGrant()
1147- self.factory.makeAccessPolicyArtifact(
1148- artifact=artifact_grant.abstract_artifact,
1149- policy=access_policy)
1150- # Make grants for grantees in ascending order so we can slice off the
1151- # first elements in the pillar observer results to check batching.
1152- for x in range(10):
1153- makeGrants('name%s' % x)
1154-
1155->>>>>>> MERGE-SOURCE
1156 def test_init_without_feature_flag(self):
1157 # We need a feature flag to enable the view.
1158 self.assertRaises(
1159@@ -343,7 +209,6 @@
1160 cache = IJSONRequestCache(view.request)
1161 self.assertIsNotNone(cache.objects.get('information_types'))
1162 self.assertIsNotNone(cache.objects.get('sharing_permissions'))
1163-<<<<<<< TREE
1164 batch_size = config.launchpad.default_batch_size
1165 apgfs = getUtility(IAccessPolicyGrantFlatSource)
1166 sharees = apgfs.findGranteePermissionsByPolicy(
1167@@ -403,65 +268,6 @@
1168 view = create_initialized_view(self.pillar, name='+sharing')
1169 cache = IJSONRequestCache(view.request)
1170 self.assertTrue(cache.objects.get('sharing_write_enabled'))
1171-=======
1172- aps = getUtility(IService, 'sharing')
1173- batch_size = config.launchpad.default_batch_size
1174- observers = aps.getPillarShareeData(
1175- self.pillar, grantees=self.grantees[:batch_size])
1176- self.assertContentEqual(
1177- observers, cache.objects.get('sharee_data'))
1178-
1179- def test_view_batch_data(self):
1180- # Test the expected batching data is in the json request cache.
1181- with FeatureFixture(ENABLED_FLAG):
1182- view = create_initialized_view(self.pillar, name='+sharing')
1183- cache = IJSONRequestCache(view.request)
1184- # Test one expected data value (there are many).
1185- next_batch = view.shareeData().batch.nextBatch()
1186- self.assertContentEqual(
1187- next_batch.range_memo, cache.objects.get('next')['memo'])
1188-
1189- def test_view_range_factory(self):
1190- # Test the view range factory is properly configured.
1191- with FeatureFixture(ENABLED_FLAG):
1192- view = create_initialized_view(self.pillar, name='+sharing')
1193- range_factory = view.shareeData().batch.range_factory
1194-
1195- def test_range_factory():
1196- row = range_factory.resultset.get_plain_result_set()[0]
1197- range_factory.getOrderValuesFor(row)
1198-
1199- self.assertThat(
1200- test_range_factory,
1201- Not(Raises(MatchesException(StormRangeFactoryError))))
1202-
1203- def test_view_query_count(self):
1204- # Test the query count is within expected limit.
1205- with FeatureFixture(ENABLED_FLAG):
1206- view = create_view(self.pillar, name='+sharing')
1207- with StormStatementRecorder() as recorder:
1208- view.initialize()
1209- self.assertThat(recorder, HasQueryCount(LessThan(7)))
1210-
1211- def test_view_write_enabled_without_feature_flag(self):
1212- # Test that sharing_write_enabled is not set without the feature flag.
1213- with FeatureFixture(ENABLED_FLAG):
1214- login_person(self.owner)
1215- view = create_initialized_view(self.pillar, name='+sharing')
1216- cache = IJSONRequestCache(view.request)
1217- self.assertFalse(cache.objects.get('sharing_write_enabled'))
1218-
1219- def test_view_write_enabled_with_feature_flag(self):
1220- # Test that sharing_write_enabled is set when required.
1221- with FeatureFixture(WRITE_FLAG):
1222- view = create_initialized_view(self.pillar, name='+sharing')
1223- cache = IJSONRequestCache(view.request)
1224- self.assertFalse(cache.objects.get('sharing_write_enabled'))
1225- login_person(self.owner)
1226- view = create_initialized_view(self.pillar, name='+sharing')
1227- cache = IJSONRequestCache(view.request)
1228- self.assertTrue(cache.objects.get('sharing_write_enabled'))
1229->>>>>>> MERGE-SOURCE
1230
1231
1232 class TestProductSharingView(PillarSharingViewTestMixin,
1233
1234=== modified file 'lib/lp/registry/enums.py'
1235--- lib/lp/registry/enums.py 2012-03-29 14:36:36 +0000
1236+++ lib/lp/registry/enums.py 2012-03-29 00:48:21 +0000
1237@@ -9,16 +9,10 @@
1238 'DistroSeriesDifferenceType',
1239 'InformationType',
1240 'PersonTransferJobType',
1241-<<<<<<< TREE
1242 'PRIVATE_INFORMATION_TYPES',
1243 'PUBLIC_INFORMATION_TYPES',
1244 'ProductJobType',
1245 'SECURITY_INFORMATION_TYPES',
1246-=======
1247- 'PRIVATE_INFORMATION_TYPES',
1248- 'ProductJobType',
1249- 'SECURITY_INFORMATION_TYPES',
1250->>>>>>> MERGE-SOURCE
1251 'SharingPermission',
1252 ]
1253
1254@@ -70,7 +64,6 @@
1255 """)
1256
1257
1258-<<<<<<< TREE
1259 PUBLIC_INFORMATION_TYPES = (
1260 InformationType.PUBLIC, InformationType.UNEMBARGOEDSECURITY)
1261
1262@@ -84,17 +77,6 @@
1263 InformationType.UNEMBARGOEDSECURITY, InformationType.EMBARGOEDSECURITY)
1264
1265
1266-=======
1267-PRIVATE_INFORMATION_TYPES = (
1268- InformationType.EMBARGOEDSECURITY, InformationType.USERDATA,
1269- InformationType.PROPRIETARY)
1270-
1271-
1272-SECURITY_INFORMATION_TYPES = (
1273- InformationType.UNEMBARGOEDSECURITY, InformationType.EMBARGOEDSECURITY)
1274-
1275-
1276->>>>>>> MERGE-SOURCE
1277 class SharingPermission(DBEnumeratedType):
1278 """Sharing permission.
1279
1280
1281=== modified file 'lib/lp/registry/interfaces/accesspolicy.py'
1282--- lib/lp/registry/interfaces/accesspolicy.py 2012-03-29 14:36:36 +0000
1283+++ lib/lp/registry/interfaces/accesspolicy.py 2012-03-22 09:00:36 +0000
1284@@ -215,7 +215,6 @@
1285 """Experimental query utility to search through the flattened schema."""
1286
1287 def findGranteesByPolicy(policies):
1288-<<<<<<< TREE
1289 """Find teams or users with access grants for the policies.
1290
1291 This includes grants for artifacts in the policies.
1292@@ -237,29 +236,6 @@
1293 ALL means the person has an access policy grant and can see all
1294 artifacts for the associated pillar.
1295 SOME means the person only has specified access artifact grants.
1296-=======
1297- """Find teams or users with access grants for the policies.
1298-
1299- This includes grants for artifacts in the policies.
1300-
1301- :param policies: a collection of `IAccesPolicy`s.
1302- :return: a collection of `IPerson`.
1303- """
1304-
1305- def findGranteePermissionsByPolicy(policies, grantees=None):
1306- """Find teams or users with access grants for the policies.
1307-
1308- This includes grants for artifacts in the policies.
1309-
1310- :param policies: a collection of `IAccesPolicy`s.
1311- :param grantees: if not None, the result only includes people in the
1312- specified list of grantees.
1313- :return: a collection of (`IPerson`, `IAccessPolicy`, permission)
1314- where permission is a SharingPermission value.
1315- 'ALL' means the person has an access policy grant and can see all
1316- artifacts for the associated pillar.
1317- 'SOME' means the person only has specified access artifact grants.
1318->>>>>>> MERGE-SOURCE
1319 """
1320
1321 def findArtifactsByGrantee(grantee, policies):
1322
1323=== modified file 'lib/lp/registry/interfaces/productjob.py'
1324--- lib/lp/registry/interfaces/productjob.py 2012-03-29 14:36:36 +0000
1325+++ lib/lp/registry/interfaces/productjob.py 2012-03-24 12:41:36 +0000
1326@@ -1,4 +1,3 @@
1327-<<<<<<< TREE
1328 # Copyright 2012 Canonical Ltd. This software is licensed under the
1329 # GNU Affero General Public License version 3 (see the file LICENSE).
1330
1331@@ -120,72 +119,3 @@
1332 :param reply_to_commercial: Set the reply_to property to the
1333 commercial email address.
1334 """
1335-=======
1336-# Copyright 2012 Canonical Ltd. This software is licensed under the
1337-# GNU Affero General Public License version 3 (see the file LICENSE).
1338-
1339-"""Interfaces for the Jobs system to update products and send notifications."""
1340-
1341-__metaclass__ = type
1342-__all__ = [
1343- 'IProductJob',
1344- 'IProductJobSource',
1345- ]
1346-
1347-from zope.interface import Attribute
1348-from zope.schema import (
1349- Int,
1350- Object,
1351- )
1352-
1353-from lp import _
1354-from lp.registry.interfaces.product import IProduct
1355-
1356-from lp.services.job.interfaces.job import (
1357- IJob,
1358- IJobSource,
1359- IRunnableJob,
1360- )
1361-
1362-
1363-class IProductJob(IRunnableJob):
1364- """A Job related to an `IProduct`."""
1365-
1366- id = Int(
1367- title=_('DB ID'), required=True, readonly=True,
1368- description=_("The tracking number of this job."))
1369-
1370- job = Object(
1371- title=_('The common Job attributes'),
1372- schema=IJob,
1373- required=True)
1374-
1375- product = Object(
1376- title=_('The product the job is for'),
1377- schema=IProduct,
1378- required=True)
1379-
1380- metadata = Attribute('A dict of data for the job')
1381-
1382-
1383-class IProductJobSource(IJobSource):
1384- """An interface for creating and finding `IProductJob`s."""
1385-
1386- def create(product, metadata):
1387- """Create a new `IProductJob`.
1388-
1389- :param product: An IProduct.
1390- :param metadata: a dict of configuration data for the job.
1391- The data must be JSON compatible keys and values.
1392- """
1393-
1394- def find(product=None, date_since=None, job_type=None):
1395- """Find `IProductJob`s that match the specified criteria.
1396-
1397- :param product: Match jobs for specific product.
1398- :param date_since: Match jobs since the specified date.
1399- :param job_type: Match jobs of a specific type. Type is expected
1400- to be a class name.
1401- :return: A `ResultSet` yielding `IProductJob`.
1402- """
1403->>>>>>> MERGE-SOURCE
1404
1405=== modified file 'lib/lp/registry/interfaces/sharingservice.py'
1406--- lib/lp/registry/interfaces/sharingservice.py 2012-03-29 14:36:36 +0000
1407+++ lib/lp/registry/interfaces/sharingservice.py 2012-03-26 14:10:32 +0000
1408@@ -59,7 +59,6 @@
1409 def getPillarSharees(pillar):
1410 """Return people/teams who can see pillar artifacts."""
1411
1412-<<<<<<< TREE
1413 @export_read_operation()
1414 @operation_parameters(
1415 pillar=Reference(IPillar, title=_('Pillar'), required=True))
1416@@ -83,20 +82,6 @@
1417 - permissions they have for each information type.
1418 """
1419
1420-=======
1421- @export_read_operation()
1422- @operation_parameters(
1423- pillar=Reference(IPillar, title=_('Pillar'), required=True))
1424- @operation_for_version('devel')
1425- def getPillarShareeData(pillar, grantees=None):
1426- """Return people/teams who can see pillar artifacts.
1427-
1428- The result records are json data which includes:
1429- - person name
1430- - permissions they have for each information type.
1431- """
1432-
1433->>>>>>> MERGE-SOURCE
1434 @export_write_operation()
1435 @call_with(user=REQUEST_USER)
1436 @operation_parameters(
1437
1438=== modified file 'lib/lp/registry/javascript/sharing/pillarsharingview.js'
1439--- lib/lp/registry/javascript/sharing/pillarsharingview.js 2012-03-29 14:36:36 +0000
1440+++ lib/lp/registry/javascript/sharing/pillarsharingview.js 2012-03-26 01:11:56 +0000
1441@@ -103,16 +103,10 @@
1442 var otns = Y.lp.registry.sharing.shareetable;
1443 var sharee_table = new otns.ShareeTableWidget({
1444 sharees: sharee_data,
1445-<<<<<<< TREE
1446 sharing_permissions:
1447 this.get('sharing_permissions_by_value'),
1448 information_types: this.get('information_types_by_value'),
1449 write_enabled: this.get('write_enabled')
1450-=======
1451- sharing_permissions: sharing_permissions,
1452- information_types: this.get('information_types_by_value'),
1453- write_enabled: this.get('write_enabled')
1454->>>>>>> MERGE-SOURCE
1455 });
1456 this.set('sharee_table', sharee_table);
1457 sharee_table.render();
1458
1459=== modified file 'lib/lp/registry/javascript/sharing/shareepicker.js'
1460--- lib/lp/registry/javascript/sharing/shareepicker.js 2012-03-29 14:36:36 +0000
1461+++ lib/lp/registry/javascript/sharing/shareepicker.js 2012-03-28 00:44:36 +0000
1462@@ -51,12 +51,8 @@
1463 }
1464 }
1465 this.set('information_types', information_types);
1466-<<<<<<< TREE
1467 this.set('sharing_permissions', sharing_permissions);
1468 this.step_one_header = this.get('headerContent');
1469-=======
1470- this.set('sharing_permissions', sharing_permissions);
1471->>>>>>> MERGE-SOURCE
1472 var self = this;
1473 this.subscribe('save', function (e) {
1474 e.halt();
1475@@ -173,67 +169,10 @@
1476 var step_one_content = contentBox.one('.yui3-widget-bd');
1477 var step_two_content = contentBox.one('.picker-content-two');
1478 if (step_two_content === null) {
1479-<<<<<<< TREE
1480 step_two_content = this._render_step_two(
1481 data.back_enabled, data.allowed_permissions);
1482-=======
1483- var step_two_html = [
1484- '<div class="picker-content-two transparent">',
1485- '<div class="step-links">',
1486- '<a class="prev js-action" href="#">Back</a>',
1487- '<button class="next lazr-pos lazr-btn"></button>',
1488- '<a class="next js-action" href="#">Select</a>',
1489- '</div></div>'
1490- ].join(' ');
1491- step_two_content = Y.Node.create(step_two_html);
1492- var self = this;
1493- // Remove the back link if required.
1494- if (Y.Lang.isBoolean(data.back_enabled)
1495- && !data.back_enabled ) {
1496- step_two_content.one('a.prev').remove(true);
1497- } else {
1498- step_two_content.one('a.prev').on('click', function(e) {
1499- e.halt();
1500- self._display_step_one();
1501- });
1502- }
1503- // Wire up the next (ie submit) links.
1504- step_two_content.all('.next').on('click', function(e) {
1505- e.halt();
1506- // Only submit if at least one info type is selected.
1507- if (!self._all_info_choices_unticked(step_two_content)) {
1508- self.fire('save', data, 2);
1509- }
1510- });
1511- // By default, we only show All or Nothing.
1512- var allowed_permissions = ['ALL', 'NOTHING'];
1513- if (Y.Lang.isValue(data.allowed_permissions)) {
1514- allowed_permissions = data.allowed_permissions;
1515- }
1516- var sharing_permissions = [];
1517- Y.Array.each(this.get('sharing_permissions'),
1518- function(permission) {
1519- if (Y.Array.indexOf(
1520- allowed_permissions, permission.value) >=0) {
1521- sharing_permissions.push(permission);
1522- }
1523- });
1524- var policy_selector = self._make_policy_selector(
1525- sharing_permissions);
1526- step_two_content.one('div.step-links')
1527- .insert(policy_selector, 'before');
1528->>>>>>> MERGE-SOURCE
1529 step_one_content.insert(step_two_content, 'after');
1530-<<<<<<< TREE
1531-=======
1532- step_two_content.all('input[name^=field.permission]')
1533- .on('click', function(e) {
1534- self._disable_select_if_all_info_choices_unticked(
1535- step_two_content);
1536- });
1537->>>>>>> MERGE-SOURCE
1538 }
1539-<<<<<<< TREE
1540 // Wire up the next (ie submit) links.
1541 step_two_content.detach('click');
1542 step_two_content.delegate('click', function(e) {
1543@@ -252,17 +191,6 @@
1544 // sharee_permissions.
1545 if (Y.Lang.isObject(data.sharee_permissions)) {
1546 Y.each(data.sharee_permissions, function(perm, type) {
1547-=======
1548- // Initially set all radio buttons to Nothing.
1549- step_two_content.all('input[name^=field.permission][value=NOTHING]')
1550- .each(function(radio_button) {
1551- radio_button.set('checked', true);
1552- });
1553- // Ensure the correct radio buttons are ticked according to the
1554- // sharee_permissions.
1555- if (Y.Lang.isObject(data.sharee_permissions)) {
1556- Y.each(data.sharee_permissions, function(perm, type) {
1557->>>>>>> MERGE-SOURCE
1558 var cb = step_two_content.one(
1559 'input[name=field.permission.'+type+']' +
1560 '[value="' + perm + '"]');
1561@@ -326,7 +254,6 @@
1562 this.fire('save', data, 0);
1563 },
1564
1565-<<<<<<< TREE
1566 _sharing_permission_template: function() {
1567 return [
1568 '<table class="radio-button-widget"><tbody>',
1569@@ -348,29 +275,6 @@
1570 },
1571
1572 _make_policy_selector: function(allowed_permissions) {
1573-=======
1574- _sharing_permission_template: function() {
1575- return [
1576- '<table class="radio-button-widget"><tbody>',
1577- '{{#permissions}}',
1578- '<tr>',
1579- ' <input type="radio"',
1580- ' value="{{value}}"',
1581- ' name="field.permission.{{info_type}}"',
1582- ' id="field.permission.{{info_type}}.{{index}}"',
1583- ' class="radioType">',
1584- ' <label for="field.permission.{{info_type}}"',
1585- ' title="{{description}}">',
1586- ' {{title}}',
1587- ' </label>',
1588- '</tr>',
1589- '{{/permissions}}',
1590- '</tbody></table>'
1591- ].join('');
1592- },
1593-
1594- _make_policy_selector: function(allowed_permissions) {
1595->>>>>>> MERGE-SOURCE
1596 // The policy selector is a set of radio buttons.
1597 var sharing_permissions_template = this._sharing_permission_template();
1598 var html = Y.lp.mustache.to_html([
1599
1600=== modified file 'lib/lp/registry/javascript/sharing/shareetable.js'
1601--- lib/lp/registry/javascript/sharing/shareetable.js 2012-03-29 14:36:36 +0000
1602+++ lib/lp/registry/javascript/sharing/shareetable.js 2012-03-28 03:50:33 +0000
1603@@ -49,7 +49,7 @@
1604 },
1605 // The sharing permission choices: all, some, nothing etc.
1606 sharing_permissions: {
1607- value: []
1608+ value: {}
1609 },
1610 // The node holding the sharee table.
1611 sharee_table: {
1612@@ -273,25 +273,11 @@
1613 },
1614
1615 renderUI: function() {
1616-<<<<<<< TREE
1617- this._render_sharees(this.get('sharees'));
1618- },
1619-
1620- _render_sharees: function(sharees) {
1621- var sharee_table = this.get('sharee_table');
1622-=======
1623- this._render_sharees(this.get('sharees'));
1624- },
1625-
1626- _render_sharees: function(sharees) {
1627- var sharee_table = this.get('sharee_table');
1628- if (sharees.length === 0) {
1629- sharee_table.one('tr#sharee-table-loading td')
1630- .setContent("This project's private information is " +
1631- "not shared with anyone.");
1632- return;
1633- }
1634->>>>>>> MERGE-SOURCE
1635+ this._render_sharees(this.get('sharees'));
1636+ },
1637+
1638+ _render_sharees: function(sharees) {
1639+ var sharee_table = this.get('sharee_table');
1640 var partials = {
1641 sharee_access_policies:
1642 this.get('sharee_policy_template'),
1643@@ -302,7 +288,6 @@
1644 this.get('sharee_table_template'),
1645 {sharees: sharees}, partials);
1646 var table_node = Y.Node.create(html);
1647-<<<<<<< TREE
1648 if (sharees.length === 0) {
1649 table_node.one('tbody').appendChild(
1650 Y.Node.create(this.get('sharee_table_empty_row')));
1651@@ -311,12 +296,6 @@
1652 this.render_sharing_info(sharees);
1653 this._update_editable_status();
1654 this.set('sharees', sharees);
1655-=======
1656- sharee_table.replace(table_node);
1657- this.render_sharing_info(sharees);
1658- this._update_editable_status();
1659- this.set('sharees', sharees);
1660->>>>>>> MERGE-SOURCE
1661 },
1662
1663 bindUI: function() {
1664@@ -521,7 +500,6 @@
1665 var rows_to_delete = sharee_table.all(deleted_row_selectors.join(','));
1666 var delete_rows = function() {
1667 rows_to_delete.remove(true);
1668-<<<<<<< TREE
1669 if (all_rows_deleted === true) {
1670 sharee_table.one('tbody')
1671 .appendChild('<tr id="sharee-table-not-shared"></tr>')
1672@@ -529,15 +507,6 @@
1673 .setContent("This project's private information " +
1674 "is not shared with anyone.");
1675 }
1676-=======
1677- if (all_rows_deleted === true) {
1678- sharee_table.one('tbody')
1679- .appendChild('<tr id="sharee-table-loading"></tr>')
1680- .appendChild('<td></td>')
1681- .setContent("This project's private information " +
1682- "is not shared with anyone.");
1683- }
1684->>>>>>> MERGE-SOURCE
1685 };
1686 var anim_duration = this.get('anim_duration');
1687 if (anim_duration === 0 ) {
1688
1689=== modified file 'lib/lp/registry/javascript/sharing/tests/test_shareelisting_navigator.html'
1690--- lib/lp/registry/javascript/sharing/tests/test_shareelisting_navigator.html 2012-03-29 14:36:36 +0000
1691+++ lib/lp/registry/javascript/sharing/tests/test_shareelisting_navigator.html 2012-03-28 03:50:33 +0000
1692@@ -1,4 +1,3 @@
1693-<<<<<<< TREE
1694 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
1695 "http://www.w3.org/TR/html4/strict.dtd">
1696 <!--
1697@@ -61,67 +60,3 @@
1698 </script>
1699 </body>
1700 </html>
1701-=======
1702-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
1703- "http://www.w3.org/TR/html4/strict.dtd">
1704-<!--
1705-Copyright 2012 Canonical Ltd. This software is licensed under the
1706-GNU Affero General Public License version 3 (see the file LICENSE).
1707--->
1708-
1709-<html>
1710- <head>
1711- <title>Sharee Listing Navigator Tests</title>
1712-
1713- <!-- YUI and test setup -->
1714- <script type="text/javascript"
1715- src="../../../../../../build/js/yui/yui/yui.js">
1716- </script>
1717- <link rel="stylesheet"
1718- href="../../../../../../build/js/yui/console/assets/console-core.css" />
1719- <link rel="stylesheet"
1720- href="../../../../../../build/js/yui/console/assets/skins/sam/console.css" />
1721- <link rel="stylesheet"
1722- href="../../../../../../build/js/yui/test/assets/skins/sam/test.css" />
1723-
1724- <script type="text/javascript"
1725- src="../../../../../../build/js/lp/app/testing/testrunner.js"></script>
1726-
1727- <link rel="stylesheet" href="../../../../app/javascript/testing/test.css" />
1728-
1729- <!-- Dependencies -->
1730- <script type="text/javascript"
1731- src="../../../../../../build/js/lp/app/client.js"></script>
1732- <script type="text/javascript"
1733- src="../../../../../../build/js/lp/app/lp.js"></script>
1734- <script type="text/javascript"
1735- src="../../../../../../build/js/lp/app/mustache.js"></script>
1736- <script type="text/javascript"
1737- src="../../../../../../build/js/lp/app/indicator/indicator.js"></script>
1738- <script type="text/javascript"
1739- src="../../../../../../build/js/lp/app/overlay/overlay.js"></script>
1740- <script type="text/javascript"
1741- src="../../../../../../build/js/lp/app/listing_navigator.js"></script>
1742-
1743- <!-- The module under test. -->
1744- <script type="text/javascript" src="../shareelisting_navigator.js"></script>
1745-
1746- <!-- The test suite -->
1747- <script type="text/javascript" src="test_shareelisting_navigator.js"></script>
1748-
1749- </head>
1750- <body class="yui3-skin-sam">
1751- <ul id="suites">
1752- <li>lp.registry.sharing.shareelisting_navigator.test</li>
1753- </ul>
1754- <div id="fixture"></div>
1755- <script type="text/x-template" id="sharee-table-template">
1756- <table id='sharee-table'>
1757- <tr id='sharee-table-loading'><td>
1758- Loading...
1759- </td></tr>
1760- </table>
1761- </script>
1762- </body>
1763-</html>
1764->>>>>>> MERGE-SOURCE
1765
1766=== modified file 'lib/lp/registry/javascript/sharing/tests/test_shareepicker.js'
1767--- lib/lp/registry/javascript/sharing/tests/test_shareepicker.js 2012-03-29 14:36:36 +0000
1768+++ lib/lp/registry/javascript/sharing/tests/test_shareepicker.js 2012-03-27 02:27:21 +0000
1769@@ -181,7 +181,6 @@
1770 // Check the title and step title are correct.
1771 var steptitle = cb.one('.contains-steptitle h2').getContent();
1772 Y.Assert.areEqual(
1773-<<<<<<< TREE
1774 'Update sharing policies',
1775 this.picker.get('headerContent').get('text'));
1776 Y.Assert.areEqual(
1777@@ -191,14 +190,6 @@
1778 Y.Assert.isNotNull(cb.one('input[value=ALL]'));
1779 Y.Assert.isNotNull(cb.one('input[value=NOTHING]'));
1780 Y.Assert.isNull(cb.one('input[value=SOME]'));
1781-=======
1782- 'Select sharing policies for Fred', steptitle);
1783- // By default, selections only for ALL and NOTHING are available
1784- // (and no others).
1785- Y.Assert.isNotNull(cb.one('input[value=ALL]'));
1786- Y.Assert.isNotNull(cb.one('input[value=NOTHING]'));
1787- Y.Assert.isNull(cb.one('input[value=SOME]'));
1788->>>>>>> MERGE-SOURCE
1789 // Selected permission checkboxes should be ticked.
1790 cb.all('input[name=field.permission.P1]')
1791 .each(function(node) {
1792
1793=== modified file 'lib/lp/registry/javascript/sharing/tests/test_shareetable.html'
1794--- lib/lp/registry/javascript/sharing/tests/test_shareetable.html 2012-03-29 14:36:36 +0000
1795+++ lib/lp/registry/javascript/sharing/tests/test_shareetable.html 2012-03-28 03:50:33 +0000
1796@@ -73,7 +73,6 @@
1797 <ul id="suites">
1798 <li>lp.registry.sharing.shareetable.test</li>
1799 </ul>
1800-<<<<<<< TREE
1801 <div id="fixture"></div>
1802 <script type="text/x-template" id="sharee-table-template">
1803 <table id='sharee-table'>
1804@@ -82,15 +81,5 @@
1805 </td></tr>
1806 </table>
1807 </script>
1808-=======
1809- <div id="fixture"></div>
1810- <script type="text/x-template" id="sharee-table-template">
1811- <table id='sharee-table'>
1812- <tr id='sharee-table-loading'><td>
1813- Loading...
1814- </td></tr>
1815- </table>
1816- </script>
1817->>>>>>> MERGE-SOURCE
1818 </body>
1819 </html>
1820
1821=== modified file 'lib/lp/registry/javascript/sharing/tests/test_shareetable.js'
1822--- lib/lp/registry/javascript/sharing/tests/test_shareetable.js 2012-03-29 14:36:36 +0000
1823+++ lib/lp/registry/javascript/sharing/tests/test_shareetable.js 2012-03-28 03:50:33 +0000
1824@@ -25,7 +25,6 @@
1825 ]
1826 }
1827 };
1828-<<<<<<< TREE
1829 this.sharing_permissions = {
1830 s1: 'S1',
1831 s2: 'S2'
1832@@ -40,27 +39,13 @@
1833 Y.one('#sharee-table-template').getContent());
1834 this.fixture.appendChild(sharee_table);
1835
1836-=======
1837- this.fixture = Y.one('#fixture');
1838- var sharee_table = Y.Node.create(
1839- Y.one('#sharee-table-template').getContent());
1840- this.fixture.appendChild(sharee_table);
1841-
1842->>>>>>> MERGE-SOURCE
1843 },
1844
1845 tearDown: function () {
1846-<<<<<<< TREE
1847 if (this.fixture !== null) {
1848 this.fixture.empty(true);
1849 }
1850 delete this.fixture;
1851-=======
1852- if (this.fixture !== null) {
1853- this.fixture.empty();
1854- }
1855- delete this.fixture;
1856->>>>>>> MERGE-SOURCE
1857 delete window.LP;
1858 },
1859
1860@@ -72,7 +57,6 @@
1861 sharee_table: Y.one('#sharee-table'),
1862 anim_duration: 0,
1863 sharees: window.LP.cache.sharee_data,
1864-<<<<<<< TREE
1865 sharing_permissions: this.sharing_permissions,
1866 information_types: this.information_types,
1867 write_enabled: true
1868@@ -80,14 +64,6 @@
1869 window.LP.cache.sharee_data = config.sharees;
1870 var ns = Y.lp.registry.sharing.shareetable;
1871 return new ns.ShareeTableWidget(config);
1872-=======
1873- sharing_permissions: window.LP.cache.sharing_permissions,
1874- information_types: window.LP.cache.information_types,
1875- write_enabled: true
1876- }, overrides);
1877- var ns = Y.lp.registry.sharing.shareetable;
1878- return new ns.ShareeTableWidget(config);
1879->>>>>>> MERGE-SOURCE
1880 },
1881
1882 test_library_exists: function () {
1883@@ -104,7 +80,6 @@
1884 "Sharee table failed to be instantiated");
1885 },
1886
1887-<<<<<<< TREE
1888 // Read only mode disables the correct things.
1889 test_readonly: function() {
1890 this.sharee_table = this._create_Widget({
1891@@ -146,34 +121,6 @@
1892 Y.Assert.isNull(Y.one('tr#sharee-table-not-shared'));
1893 },
1894
1895-=======
1896- // Read only mode disables the correct things.
1897- test_readonly: function() {
1898- this.sharee_table = this._create_Widget({
1899- write_enabled: false
1900- });
1901- this.sharee_table.render();
1902- Y.all('#sharee-table ' +
1903- '.sprite.add, .sprite.edit, .sprite.remove a')
1904- .each(function(link) {
1905- Y.Assert.isTrue(link.hasClass('unseen'));
1906- });
1907- },
1908-
1909- // When there are no sharees, the table contains an informative message.
1910- test_no_sharee_message: function() {
1911- this.sharee_table = this._create_Widget({
1912- sharees: []
1913- });
1914- this.sharee_table.render();
1915- Y.Assert.areEqual(
1916- "This project's private information is not shared " +
1917- "with anyone.",
1918- Y.one('#sharee-table tr#sharee-table-loading td')
1919- .getContent());
1920- },
1921-
1922->>>>>>> MERGE-SOURCE
1923 // The given sharee is correctly rendered.
1924 _test_sharee_rendered: function(sharee) {
1925 // The sharee row
1926@@ -344,7 +291,6 @@
1927 this.sharee_table.syncUI();
1928 // Check the results.
1929 var self = this;
1930-<<<<<<< TREE
1931 Y.Array.each(sharee_data, function(sharee) {
1932 self._test_sharee_rendered(sharee);
1933 });
1934@@ -400,63 +346,6 @@
1935 'permissions': {'P1': 's2'}};
1936 this.sharee_table.navigator.fire('updateContent', [new_sharee]);
1937 this._test_sharee_rendered(new_sharee);
1938-=======
1939- Y.Array.each(sharee_data, function(sharee) {
1940- self._test_sharee_rendered(sharee);
1941- });
1942- var deleted_row = '#sharee-table tr[id=permission-fred]';
1943- Y.Assert.isNull(Y.one(deleted_row));
1944- },
1945-
1946- // The navigator model total attribute is updated when the currently
1947- // displayed sharee data changes.
1948- test_navigation_totals_updated: function() {
1949- this.sharee_table = this._create_Widget();
1950- this.sharee_table.render();
1951- // We manipulate the cached model data - delete, add and update
1952- var sharee_data = window.LP.cache.sharee_data;
1953- // Insert a new record.
1954- var new_sharee = {
1955- 'name': 'joe', 'display_name': 'Joe Smith',
1956- 'role': '(Maintainer)', web_link: '~joe',
1957- 'self_link': '~joe',
1958- 'permissions': {'P1': 's2'}};
1959- sharee_data.splice(0, 0, new_sharee);
1960- this.sharee_table.syncUI();
1961- // Check the results.
1962- Y.Assert.areEqual(
1963- 3, this.sharee_table.navigator.get('model').get('total'));
1964- },
1965-
1966- // When all rows are deleted, the table contains an informative message.
1967- test_delete_all: function() {
1968- this.sharee_table = this._create_Widget();
1969- this.sharee_table.render();
1970- // We manipulate the cached model data.
1971- var sharee_data = window.LP.cache.sharee_data;
1972- // Delete all the records.
1973- sharee_data.splice(0, 2);
1974- this.sharee_table.syncUI();
1975- // Check the results.
1976- Y.Assert.areEqual(
1977- "This project's private information is not shared " +
1978- "with anyone.",
1979- Y.one('#sharee-table tr#sharee-table-loading td')
1980- .getContent());
1981- },
1982-
1983- // A batch update is correctly rendered.
1984- test_navigator_content_update: function() {
1985- this.sharee_table = this._create_Widget();
1986- this.sharee_table.render();
1987- var new_sharee = {
1988- 'name': 'joe', 'display_name': 'Joe Smith',
1989- 'role': '(Maintainer)', web_link: '~joe',
1990- 'self_link': '~joe',
1991- 'permissions': {'P1': 's2'}};
1992- this.sharee_table.navigator.fire('updateContent', [new_sharee]);
1993- this._test_sharee_rendered(new_sharee);
1994->>>>>>> MERGE-SOURCE
1995 }
1996 }));
1997
1998
1999=== modified file 'lib/lp/registry/model/accesspolicy.py'
2000--- lib/lp/registry/model/accesspolicy.py 2012-03-29 14:36:36 +0000
2001+++ lib/lp/registry/model/accesspolicy.py 2012-03-23 04:12:59 +0000
2002@@ -18,12 +18,8 @@
2003 And,
2004 In,
2005 Or,
2006-<<<<<<< TREE
2007 Select,
2008 SQL,
2009-=======
2010- SQL,
2011->>>>>>> MERGE-SOURCE
2012 )
2013 from storm.properties import (
2014 DateTime,
2015@@ -359,7 +355,6 @@
2016 Person, Person.id == cls.grantee_id, cls.policy_id.is_in(ids))
2017
2018 @classmethod
2019-<<<<<<< TREE
2020 def findGranteePermissionsByPolicy(cls, policies, grantees=None):
2021 """See `IAccessPolicyGrantFlatSource`."""
2022 policies_by_id = dict((policy.id, policy) for policy in policies)
2023@@ -409,30 +404,6 @@
2024 result_decorator=set_permission, pre_iter_hook=load_permissions)
2025
2026 @classmethod
2027-=======
2028- def findGranteePermissionsByPolicy(cls, policies, grantees=None):
2029- """See `IAccessPolicyGrantFlatSource`."""
2030- ids = [policy.id for policy in policies]
2031- sharing_permission_term = SQL("""
2032- CASE(
2033- MIN(COALESCE(artifact, 0)))
2034- WHEN 0 THEN 'ALL'
2035- ELSE 'SOME'
2036- END
2037- """)
2038- constraints = [
2039- Person.id == cls.grantee_id,
2040- AccessPolicy.id == cls.policy_id,
2041- cls.policy_id.is_in(ids)]
2042- if grantees:
2043- grantee_ids = [grantee.id for grantee in grantees]
2044- constraints.append(cls.grantee_id.is_in(grantee_ids))
2045- return IStore(cls).find(
2046- (Person, AccessPolicy, sharing_permission_term),
2047- *constraints).group_by(Person, AccessPolicy)
2048-
2049- @classmethod
2050->>>>>>> MERGE-SOURCE
2051 def findArtifactsByGrantee(cls, grantee, policies):
2052 """See `IAccessPolicyGrantFlatSource`."""
2053 ids = [policy.id for policy in policies]
2054
2055=== modified file 'lib/lp/registry/model/person.py'
2056--- lib/lp/registry/model/person.py 2012-03-29 14:36:36 +0000
2057+++ lib/lp/registry/model/person.py 2012-03-29 06:02:46 +0000
2058@@ -1850,24 +1850,16 @@
2059 Bug,
2060 Join(BugSubscription, BugSubscription.bug_id == Bug.id)),
2061 where=And(
2062-<<<<<<< TREE
2063 Bug.information_type.is_in(PRIVATE_INFORMATION_TYPES),
2064-=======
2065- Bug._private == True,
2066->>>>>>> MERGE-SOURCE
2067 BugSubscription.person_id == self.id)),
2068 Select(
2069 Bug.id,
2070 tables=(
2071 Bug,
2072 Join(BugTask, BugTask.bugID == Bug.id)),
2073-<<<<<<< TREE
2074 where=And(Bug.information_type.is_in(
2075 PRIVATE_INFORMATION_TYPES),
2076 BugTask.assignee == self.id)),
2077-=======
2078- where=And(Bug._private == True, BugTask.assignee == self.id)),
2079->>>>>>> MERGE-SOURCE
2080 limit=1))
2081 if private_bugs_involved.rowcount:
2082 raise TeamSubscriptionPolicyError(
2083
2084=== modified file 'lib/lp/registry/model/productjob.py'
2085--- lib/lp/registry/model/productjob.py 2012-03-29 14:36:36 +0000
2086+++ lib/lp/registry/model/productjob.py 2012-03-24 12:36:13 +0000
2087@@ -1,4 +1,3 @@
2088-<<<<<<< TREE
2089 # Copyright 2012 Canonical Ltd. This software is licensed under the
2090 # GNU Affero General Public License version 3 (see the file LICENSE).
2091
2092@@ -283,156 +282,3 @@
2093 'Launchpad', config.canonical.noreply_from_address)
2094 self.sendEmailToMaintainer(
2095 self.email_template_name, self.subject, from_address)
2096-=======
2097-# Copyright 2012 Canonical Ltd. This software is licensed under the
2098-# GNU Affero General Public License version 3 (see the file LICENSE).
2099-
2100-"""Jobs classes to update products and send notifications."""
2101-
2102-__metaclass__ = type
2103-__all__ = [
2104- 'ProductJob',
2105- ]
2106-
2107-from lazr.delegates import delegates
2108-import simplejson
2109-from storm.expr import (
2110- And,
2111- )
2112-from storm.locals import (
2113- Int,
2114- Reference,
2115- Unicode,
2116- )
2117-from zope.interface import (
2118- classProvides,
2119- implements,
2120- )
2121-
2122-from lp.registry.enums import ProductJobType
2123-from lp.registry.interfaces.product import (
2124- IProduct,
2125- )
2126-from lp.registry.interfaces.productjob import (
2127- IProductJob,
2128- IProductJobSource,
2129- )
2130-from lp.registry.model.product import Product
2131-from lp.services.database.decoratedresultset import DecoratedResultSet
2132-from lp.services.database.enumcol import EnumCol
2133-from lp.services.database.lpstorm import (
2134- IMasterStore,
2135- IStore,
2136- )
2137-from lp.services.database.stormbase import StormBase
2138-from lp.services.job.model.job import Job
2139-from lp.services.job.runner import BaseRunnableJob
2140-
2141-
2142-class ProductJob(StormBase):
2143- """Base class for product jobs."""
2144-
2145- implements(IProductJob)
2146-
2147- __storm_table__ = 'ProductJob'
2148-
2149- id = Int(primary=True)
2150-
2151- job_id = Int(name='job')
2152- job = Reference(job_id, Job.id)
2153-
2154- product_id = Int(name='product')
2155- product = Reference(product_id, Product.id)
2156-
2157- job_type = EnumCol(enum=ProductJobType, notNull=True)
2158-
2159- _json_data = Unicode('json_data')
2160-
2161- @property
2162- def metadata(self):
2163- return simplejson.loads(self._json_data)
2164-
2165- def __init__(self, product, job_type, metadata):
2166- """Constructor.
2167-
2168- :param product: The product the job is for.
2169- :param job_type: The type job the product needs run.
2170- :param metadata: A dict of JSON-compatible data to pass to the job.
2171- """
2172- super(ProductJob, self).__init__()
2173- self.job = Job()
2174- self.product = product
2175- self.job_type = job_type
2176- json_data = simplejson.dumps(metadata)
2177- self._json_data = json_data.decode('utf-8')
2178-
2179-
2180-class ProductJobDerived(BaseRunnableJob):
2181- """Intermediate class for deriving from ProductJob.
2182-
2183- Storm classes can't simply be subclassed or you can end up with
2184- multiple objects referencing the same row in the db. This class uses
2185- lazr.delegates, which is a little bit simpler than storm's
2186- inheritance solution to the problem. Subclasses need to override
2187- the run() method.
2188- """
2189-
2190- delegates(IProductJob)
2191- classProvides(IProductJobSource)
2192-
2193- def __init__(self, job):
2194- self.context = job
2195-
2196- def __repr__(self):
2197- return (
2198- "<{self.__class__.__name__} for {self.product.name} "
2199- "status={self.job.status}>").format(self=self)
2200-
2201- @classmethod
2202- def create(cls, product, metadata):
2203- """See `IProductJob`."""
2204- if not IProduct.providedBy(product):
2205- raise TypeError("Product must be an IProduct: %s" % repr(product))
2206- job = ProductJob(
2207- product=product, job_type=cls.class_job_type, metadata=metadata)
2208- return cls(job)
2209-
2210- @classmethod
2211- def find(cls, product, date_since=None, job_type=None):
2212- """See `IPersonMergeJobSource`."""
2213- conditions = [
2214- ProductJob.job_id == Job.id,
2215- ProductJob.product == product.id,
2216- ]
2217- if date_since is not None:
2218- conditions.append(
2219- Job.date_created >= date_since)
2220- if job_type is not None:
2221- conditions.append(
2222- ProductJob.job_type == job_type)
2223- return DecoratedResultSet(
2224- IStore(ProductJob).find(
2225- ProductJob, *conditions), cls)
2226-
2227- @classmethod
2228- def iterReady(cls):
2229- """Iterate through all ready ProductJobs."""
2230- store = IMasterStore(ProductJob)
2231- jobs = store.find(
2232- ProductJob,
2233- And(ProductJob.job_type == cls.class_job_type,
2234- ProductJob.job_id.is_in(Job.ready_jobs)))
2235- return (cls(job) for job in jobs)
2236-
2237- @property
2238- def log_name(self):
2239- return self.__class__.__name__
2240-
2241- def getOopsVars(self):
2242- """See `IRunnableJob`."""
2243- vars = BaseRunnableJob.getOopsVars(self)
2244- vars.extend([
2245- ('product', self.context.product.name),
2246- ])
2247- return vars
2248->>>>>>> MERGE-SOURCE
2249
2250=== modified file 'lib/lp/registry/services/sharingservice.py'
2251--- lib/lp/registry/services/sharingservice.py 2012-03-29 14:36:36 +0000
2252+++ lib/lp/registry/services/sharingservice.py 2012-03-26 20:49:13 +0000
2253@@ -27,14 +27,9 @@
2254 )
2255 from lp.registry.interfaces.product import IProduct
2256 from lp.registry.interfaces.projectgroup import IProjectGroup
2257-<<<<<<< TREE
2258 from lp.registry.interfaces.sharingservice import ISharingService
2259 from lp.registry.model.person import Person
2260 from lp.services.features import getFeatureFlag
2261-=======
2262-from lp.registry.model.person import Person
2263-from lp.services.features import getFeatureFlag
2264->>>>>>> MERGE-SOURCE
2265 from lp.services.webapp.authorization import available_with_permission
2266
2267
2268@@ -52,7 +47,6 @@
2269 """See `IService`."""
2270 return 'sharing'
2271
2272-<<<<<<< TREE
2273 @property
2274 def write_enabled(self):
2275 return bool(getFeatureFlag(
2276@@ -64,13 +58,6 @@
2277 return [a for a in
2278 flat_source.findArtifactsByGrantee(person, policies)]
2279
2280-=======
2281- @property
2282- def write_enabled(self):
2283- return bool(getFeatureFlag(
2284- 'disclosure.enhanced_sharing.writable'))
2285-
2286->>>>>>> MERGE-SOURCE
2287 def getInformationTypes(self, pillar):
2288 """See `ISharingService`."""
2289 allowed_types = [
2290@@ -115,7 +102,6 @@
2291 @available_with_permission('launchpad.Driver', 'pillar')
2292 def getPillarSharees(self, pillar):
2293 """See `ISharingService`."""
2294-<<<<<<< TREE
2295 policies = getUtility(IAccessPolicySource).findByPillar([pillar])
2296 ap_grant_flat = getUtility(IAccessPolicyGrantFlatSource)
2297 # XXX 2012-03-22 wallyworld bug 961836
2298@@ -135,31 +121,8 @@
2299
2300 def jsonShareeData(self, grant_permissions):
2301 """See `ISharingService`."""
2302-=======
2303- policies = getUtility(IAccessPolicySource).findByPillar([pillar])
2304- ap_grant_flat = getUtility(IAccessPolicyGrantFlatSource)
2305- # XXX 2012-03-22 wallyworld bug 961836
2306- # We want to use person_sort_key(Person.displayname, Person.name) but
2307- # StormRangeFactory doesn't support that yet.
2308- grantees = ap_grant_flat.findGranteesByPolicy(
2309- policies).order_by(Person.displayname, Person.name)
2310- return grantees
2311-
2312- @available_with_permission('launchpad.Driver', 'pillar')
2313- def getPillarShareeData(self, pillar, grantees=None):
2314- """See `ISharingService`."""
2315- policies = getUtility(IAccessPolicySource).findByPillar([pillar])
2316- ap_grant_flat = getUtility(IAccessPolicyGrantFlatSource)
2317- # XXX 2012-03-22 wallyworld bug 961836
2318- # We want to use person_sort_key(Person.displayname, Person.name) but
2319- # StormRangeFactory doesn't support that yet.
2320- grant_permissions = ap_grant_flat.findGranteePermissionsByPolicy(
2321- policies, grantees).order_by(Person.displayname, Person.name)
2322-
2323->>>>>>> MERGE-SOURCE
2324 result = []
2325 request = get_current_web_service_request()
2326-<<<<<<< TREE
2327 browser_request = IWebBrowserOriginatingRequest(request)
2328 for (grantee, permissions) in grant_permissions:
2329 result.append({
2330@@ -171,22 +134,6 @@
2331 'permissions': dict(
2332 (policy.type.name, permission.name)
2333 for (policy, permission) in permissions.iteritems())})
2334-=======
2335- browser_request = IWebBrowserOriginatingRequest(request)
2336- for (grantee, policy, sharing_permission) in grant_permissions:
2337- if not grantee.id in person_by_id:
2338- person_data = {
2339- 'name': grantee.name,
2340- 'meta': 'team' if grantee.is_team else 'person',
2341- 'display_name': grantee.displayname,
2342- 'self_link': absoluteURL(grantee, request),
2343- 'permissions': {}}
2344- person_data['web_link'] = absoluteURL(grantee, browser_request)
2345- person_by_id[grantee.id] = person_data
2346- result.append(person_data)
2347- person_data = person_by_id[grantee.id]
2348- person_data['permissions'][policy.type.name] = sharing_permission
2349->>>>>>> MERGE-SOURCE
2350 return result
2351
2352 @available_with_permission('launchpad.Edit', 'pillar')
2353@@ -251,7 +198,6 @@
2354 self.deletePillarSharee(pillar, sharee, info_types_for_nothing)
2355
2356 # Return sharee data to the caller.
2357-<<<<<<< TREE
2358 ap_grant_flat = getUtility(IAccessPolicyGrantFlatSource)
2359 grant_permissions = list(ap_grant_flat.findGranteePermissionsByPolicy(
2360 all_pillar_policies, [sharee]))
2361@@ -259,12 +205,6 @@
2362 return None
2363 [sharee] = self.jsonShareeData(grant_permissions)
2364 return sharee
2365-=======
2366- sharees = self.getPillarShareeData(pillar, [sharee])
2367- if not sharees:
2368- return None
2369- return sharees[0]
2370->>>>>>> MERGE-SOURCE
2371
2372 @available_with_permission('launchpad.Edit', 'pillar')
2373 def deletePillarSharee(self, pillar, sharee,
2374@@ -296,18 +236,9 @@
2375
2376 # Second delete any access artifact grants.
2377 ap_grant_flat = getUtility(IAccessPolicyGrantFlatSource)
2378-<<<<<<< TREE
2379 to_delete = list(ap_grant_flat.findArtifactsByGrantee(
2380 sharee, pillar_policies))
2381 if len(to_delete) > 0:
2382 accessartifact_grant_source = getUtility(
2383 IAccessArtifactGrantSource)
2384 accessartifact_grant_source.revokeByArtifact(to_delete)
2385-=======
2386- to_delete = ap_grant_flat.findArtifactsByGrantee(
2387- sharee, pillar_policies)
2388- if to_delete.count() > 0:
2389- accessartifact_grant_source = getUtility(
2390- IAccessArtifactGrantSource)
2391- accessartifact_grant_source.revokeByArtifact(to_delete)
2392->>>>>>> MERGE-SOURCE
2393
2394=== modified file 'lib/lp/registry/services/tests/test_sharingservice.py'
2395--- lib/lp/registry/services/tests/test_sharingservice.py 2012-03-29 14:36:36 +0000
2396+++ lib/lp/registry/services/tests/test_sharingservice.py 2012-03-23 04:00:29 +0000
2397@@ -123,7 +123,6 @@
2398 distro,
2399 [InformationType.EMBARGOEDSECURITY, InformationType.USERDATA])
2400
2401-<<<<<<< TREE
2402 def test_jsonShareeData(self):
2403 # jsonShareeData returns the expected data.
2404 product = self.factory.makeProduct()
2405@@ -246,123 +245,6 @@
2406 self._assert_getPillarShareeDataUnauthorized(product)
2407
2408 def _assert_getPillarSharees(self, pillar):
2409-=======
2410- def _assert_getPillarShareeData(self, pillar):
2411- # getPillarShareeData returns the expected data.
2412- access_policy = self.factory.makeAccessPolicy(
2413- pillar=pillar,
2414- type=InformationType.PROPRIETARY)
2415- grantee = self.factory.makePerson()
2416- # Make access policy grant so that 'All' is returned.
2417- self.factory.makeAccessPolicyGrant(access_policy, grantee)
2418- # Make access artifact grants so that 'Some' is returned.
2419- artifact_grant = self.factory.makeAccessArtifactGrant()
2420- self.factory.makeAccessPolicyArtifact(
2421- artifact=artifact_grant.abstract_artifact, policy=access_policy)
2422-
2423- sharees = self.service.getPillarShareeData(pillar)
2424- expected_sharees = [
2425- self._makeShareeData(
2426- grantee,
2427- [(InformationType.PROPRIETARY, SharingPermission.ALL)]),
2428- self._makeShareeData(
2429- artifact_grant.grantee,
2430- [(InformationType.PROPRIETARY, SharingPermission.SOME)])]
2431- self.assertContentEqual(expected_sharees, sharees)
2432-
2433- def test_getProductShareeData(self):
2434- # Users with launchpad.Driver can view sharees.
2435- driver = self.factory.makePerson()
2436- product = self.factory.makeProduct(driver=driver)
2437- login_person(driver)
2438- self._assert_getPillarShareeData(product)
2439-
2440- def test_getDistroShareeData(self):
2441- # Users with launchpad.Driver can view sharees.
2442- driver = self.factory.makePerson()
2443- distro = self.factory.makeDistribution(driver=driver)
2444- login_person(driver)
2445- self._assert_getPillarShareeData(distro)
2446-
2447- def test_getPillarShareeDataQueryCount(self):
2448- # getPillarShareeData only should use 2 queries regardless of how many
2449- # sharees are returned.
2450- driver = self.factory.makePerson()
2451- product = self.factory.makeProduct(driver=driver)
2452- login_person(driver)
2453- access_policy = self.factory.makeAccessPolicy(
2454- pillar=product,
2455- type=InformationType.PROPRIETARY)
2456-
2457- def makeGrants():
2458- grantee = self.factory.makePerson()
2459- # Make access policy grant so that 'All' is returned.
2460- self.factory.makeAccessPolicyGrant(access_policy, grantee)
2461- # Make access artifact grants so that 'Some' is returned.
2462- artifact_grant = self.factory.makeAccessArtifactGrant()
2463- self.factory.makeAccessPolicyArtifact(
2464- artifact=artifact_grant.abstract_artifact,
2465- policy=access_policy)
2466-
2467- # Make some grants and check the count.
2468- for x in range(5):
2469- makeGrants()
2470- with StormStatementRecorder() as recorder:
2471- sharees = self.service.getPillarShareeData(product)
2472- self.assertEqual(10, len(sharees))
2473- self.assertThat(recorder, HasQueryCount(Equals(2)))
2474- # Make some more grants and check again.
2475- for x in range(5):
2476- makeGrants()
2477- with StormStatementRecorder() as recorder:
2478- sharees = self.service.getPillarShareeData(product)
2479- self.assertEqual(20, len(sharees))
2480- self.assertThat(recorder, HasQueryCount(Equals(2)))
2481-
2482- def test_getPillarShareeData_filter_grantees(self):
2483- # getPillarShareeData only returns grantees in the specified list.
2484- driver = self.factory.makePerson()
2485- pillar = self.factory.makeProduct(driver=driver)
2486- login_person(driver)
2487- access_policy = self.factory.makeAccessPolicy(
2488- pillar=pillar,
2489- type=InformationType.PROPRIETARY)
2490- grantee_in_result = self.factory.makePerson()
2491- grantee_not_in_result = self.factory.makePerson()
2492- self.factory.makeAccessPolicyGrant(access_policy, grantee_in_result)
2493- self.factory.makeAccessPolicyGrant(
2494- access_policy, grantee_not_in_result)
2495-
2496- sharees = self.service.getPillarShareeData(pillar, [grantee_in_result])
2497- expected_sharees = [
2498- self._makeShareeData(
2499- grantee_in_result,
2500- [(InformationType.PROPRIETARY, SharingPermission.ALL)])]
2501- self.assertContentEqual(expected_sharees, sharees)
2502-
2503- def _assert_getPillarShareeDataUnauthorized(self, pillar):
2504- # getPillarShareeData raises an Unauthorized exception if the user is
2505- # not permitted to do so.
2506- access_policy = self.factory.makeAccessPolicy(pillar=pillar)
2507- grantee = self.factory.makePerson()
2508- self.factory.makeAccessPolicyGrant(access_policy, grantee)
2509- self.assertRaises(
2510- Unauthorized, self.service.getPillarShareeData, pillar)
2511-
2512- def test_getPillarShareeDataAnonymous(self):
2513- # Anonymous users are not allowed.
2514- product = self.factory.makeProduct()
2515- login(ANONYMOUS)
2516- self._assert_getPillarShareeDataUnauthorized(product)
2517-
2518- def test_getPillarShareeDataAnyone(self):
2519- # Unauthorized users are not allowed.
2520- product = self.factory.makeProduct()
2521- login_person(self.factory.makePerson())
2522- self._assert_getPillarShareeDataUnauthorized(product)
2523-
2524- def _assert_getPillarSharees(self, pillar):
2525->>>>>>> MERGE-SOURCE
2526 # getPillarSharees returns the expected data.
2527 access_policy = self.factory.makeAccessPolicy(
2528 pillar=pillar,
2529@@ -370,7 +252,6 @@
2530 grantee = self.factory.makePerson()
2531 # Make access policy grant so that 'All' is returned.
2532 self.factory.makeAccessPolicyGrant(access_policy, grantee)
2533-<<<<<<< TREE
2534 # Make access artifact grants so that 'Some' is returned.
2535 artifact_grant = self.factory.makeAccessArtifactGrant()
2536 self.factory.makeAccessPolicyArtifact(
2537@@ -381,16 +262,6 @@
2538 (grantee, {access_policy: SharingPermission.ALL}),
2539 (artifact_grant.grantee, {access_policy: SharingPermission.SOME})]
2540 self.assertContentEqual(expected_sharees, sharees)
2541-=======
2542- # Make access artifact grants so that 'Some' is returned.
2543- artifact_grant = self.factory.makeAccessArtifactGrant()
2544- self.factory.makeAccessPolicyArtifact(
2545- artifact=artifact_grant.abstract_artifact, policy=access_policy)
2546-
2547- sharees = self.service.getPillarSharees(pillar)
2548- expected_sharees = [grantee, artifact_grant.grantee]
2549- self.assertContentEqual(expected_sharees, sharees)
2550->>>>>>> MERGE-SOURCE
2551
2552 def test_getProductSharees(self):
2553 # Users with launchpad.Driver can view sharees.
2554@@ -483,14 +354,7 @@
2555 (InformationType.EMBARGOEDSECURITY, SharingPermission.ALL),
2556 (InformationType.USERDATA, SharingPermission.SOME)]
2557 expected_sharee_data = self._makeShareeData(
2558-<<<<<<< TREE
2559- sharee, expected_permissions)
2560-=======
2561- sharee, expected_permissions)
2562- self.assertEqual(expected_sharee_data, sharee_data)
2563- # Check that getPillarShareeData returns what we expect.
2564- [sharee_data] = self.service.getPillarShareeData(pillar)
2565->>>>>>> MERGE-SOURCE
2566+ sharee, expected_permissions)
2567 self.assertEqual(expected_sharee_data, sharee_data)
2568 # Check that getPillarSharees returns what we expect.
2569 expected_sharee_grants = [
2570@@ -604,7 +468,6 @@
2571 if types_to_delete is not None:
2572 expected_information_types = (
2573 set(information_types).difference(types_to_delete))
2574-<<<<<<< TREE
2575 expected_policies = [
2576 access_policy for access_policy in access_policies
2577 if access_policy.type in expected_information_types]
2578@@ -614,20 +477,9 @@
2579 # Add the expected data for the other sharee.
2580 another_person_data = (
2581 another, {access_policies[0]: SharingPermission.ALL})
2582-=======
2583- remaining_grantee_person_data = self._makeShareeData(
2584- grantee,
2585- [(info_type, SharingPermission.ALL)
2586- for info_type in expected_information_types])
2587-
2588- expected_data.append(remaining_grantee_person_data)
2589- # Add the data for the other sharee.
2590- another_person_data = self._makeShareeData(
2591- another, [(information_types[0], SharingPermission.ALL)])
2592->>>>>>> MERGE-SOURCE
2593 expected_data.append(another_person_data)
2594 self.assertContentEqual(
2595- expected_data, self.service.getPillarShareeData(pillar))
2596+ expected_data, self.service.getPillarSharees(pillar))
2597
2598 def test_deleteProductShareeAll(self):
2599 # Users with launchpad.Edit can delete all access for a sharee.
2600
2601=== modified file 'lib/lp/registry/subscribers.py'
2602--- lib/lp/registry/subscribers.py 2012-03-29 14:36:36 +0000
2603+++ lib/lp/registry/subscribers.py 2012-03-22 23:21:24 +0000
2604@@ -8,281 +8,140 @@
2605 'product_licenses_modified',
2606 ]
2607
2608-<<<<<<< TREE
2609-from datetime import datetime
2610-import textwrap
2611-
2612-import pytz
2613-from zope.security.proxy import removeSecurityProxy
2614-
2615-from lp.registry.interfaces.person import IPerson
2616-from lp.registry.interfaces.product import License
2617-from lp.services.config import config
2618-from lp.services.mail.helpers import get_email_template
2619-from lp.services.mail.sendmail import (
2620- format_address,
2621- simple_sendmail,
2622- )
2623-from lp.services.webapp.menu import structured
2624-from lp.services.webapp.publisher import (
2625- canonical_url,
2626- get_current_browser_request,
2627- )
2628-
2629-
2630-def product_licenses_modified(product, event):
2631- """Send a notification if licenses changed and a license is special."""
2632- if not event.edited_fields:
2633- return
2634- licenses_changed = 'licenses' in event.edited_fields
2635- needs_notification = LicenseNotification.needs_notification(product)
2636- if licenses_changed and needs_notification:
2637- user = IPerson(event.user)
2638- notification = LicenseNotification(product, user)
2639- notification.send()
2640- notification.display()
2641-
2642-
2643-class LicenseNotification:
2644- """Send notification about special licenses to the user."""
2645-
2646- def __init__(self, product, user):
2647- self.product = product
2648- self.user = user
2649-
2650- @staticmethod
2651- def needs_notification(product):
2652- licenses = list(product.licenses)
2653- return (
2654- License.OTHER_PROPRIETARY in licenses
2655- or License.OTHER_OPEN_SOURCE in licenses
2656- or [License.DONT_KNOW] == licenses)
2657-
2658- def getTemplateName(self):
2659- """Return the name of the email template for the licensing case."""
2660- licenses = list(self.product.licenses)
2661- if [License.DONT_KNOW] == licenses:
2662- template_name = 'product-license-dont-know.txt'
2663- elif License.OTHER_PROPRIETARY in licenses:
2664- template_name = 'product-license-other-proprietary.txt'
2665- else:
2666- template_name = 'product-license-other-open-source.txt'
2667- return template_name
2668-
2669- def getCommercialUseMessage(self):
2670- """Return a message explaining the current commercial subscription."""
2671- commercial_subscription = self.product.commercial_subscription
2672- if commercial_subscription is None:
2673- return ''
2674- iso_date = commercial_subscription.date_expires.date().isoformat()
2675- if not self.product.has_current_commercial_subscription:
2676- message = "%s's commercial subscription expired on %s."
2677- elif 'complimentary' in commercial_subscription.sales_system_id:
2678- message = (
2679- "%s's complimentary commercial subscription expires on %s.")
2680- else:
2681- message = "%s's commercial subscription expires on %s."
2682- message = message % (self.product.displayname, iso_date)
2683- return textwrap.fill(message, 72)
2684-
2685- def send(self):
2686- """Send a message to the user about the product's license."""
2687- if not self.needs_notification(self.product):
2688- # The project has a common license.
2689- return False
2690- user_address = format_address(
2691- self.user.displayname, self.user.preferredemail.email)
2692- from_address = format_address(
2693- "Launchpad", config.canonical.noreply_from_address)
2694- commercial_address = format_address(
2695- 'Commercial', 'commercial@launchpad.net')
2696- substitutions = dict(
2697- user_displayname=self.user.displayname,
2698- user_name=self.user.name,
2699- product_name=self.product.name,
2700- product_url=canonical_url(self.product),
2701- commercial_use_expiration=self.getCommercialUseMessage(),
2702- )
2703- # Email the user about license policy.
2704- subject = (
2705- "License information for %(product_name)s "
2706- "in Launchpad" % substitutions)
2707- template = get_email_template(
2708- self.getTemplateName(), app='registry')
2709- message = template % substitutions
2710- simple_sendmail(
2711- from_address, user_address,
2712- subject, message, headers={'Reply-To': commercial_address})
2713- # Inform that Launchpad recognized the license change.
2714- self._addLicenseChangeToReviewWhiteboard()
2715- return True
2716-
2717- def display(self):
2718- """Show a message in a browser page about the product's license."""
2719- request = get_current_browser_request()
2720- message = self.getCommercialUseMessage()
2721- if request is None or message == '':
2722- return False
2723- safe_message = structured(
2724- '%s<br />Learn more about '
2725- '<a href="https://help.launchpad.net/CommercialHosting">'
2726- 'commercial subscriptions</a>', message)
2727- request.response.addNotification(safe_message)
2728- return True
2729-
2730- @staticmethod
2731- def _formatDate(now=None):
2732- """Return the date formatted for messages."""
2733- if now is None:
2734- now = datetime.now(tz=pytz.UTC)
2735- return now.strftime('%Y-%m-%d')
2736-
2737- def _addLicenseChangeToReviewWhiteboard(self):
2738- """Update the whiteboard for the reviewer's benefit."""
2739- now = self._formatDate()
2740- whiteboard = 'User notified of license policy on %s.' % now
2741- naked_product = removeSecurityProxy(self.product)
2742- if naked_product.reviewer_whiteboard is None:
2743- naked_product.reviewer_whiteboard = whiteboard
2744- else:
2745- naked_product.reviewer_whiteboard += '\n' + whiteboard
2746-=======
2747-from datetime import datetime
2748-import textwrap
2749-
2750-import pytz
2751-
2752-from zope.security.proxy import removeSecurityProxy
2753-
2754-from lp.registry.interfaces.person import IPerson
2755-from lp.registry.interfaces.product import License
2756-from lp.services.config import config
2757-from lp.services.mail.helpers import get_email_template
2758-from lp.services.mail.sendmail import (
2759- format_address,
2760- simple_sendmail,
2761- )
2762-from lp.services.webapp.menu import structured
2763-from lp.services.webapp.publisher import (
2764- canonical_url,
2765- get_current_browser_request,
2766- )
2767-
2768-
2769-def product_licenses_modified(product, event):
2770- """Send a notification if licenses changed and a license is special."""
2771- if not event.edited_fields:
2772- return
2773- licenses_changed = 'licenses' in event.edited_fields
2774- needs_notification = LicenseNotification.needs_notification(product)
2775- if licenses_changed and needs_notification:
2776- user = IPerson(event.user)
2777- notification = LicenseNotification(product, user)
2778- notification.send()
2779- notification.display()
2780-
2781-
2782-class LicenseNotification:
2783- """Send notification about special licenses to the user."""
2784-
2785- def __init__(self, product, user):
2786- self.product = product
2787- self.user = user
2788-
2789- @staticmethod
2790- def needs_notification(product):
2791- licenses = list(product.licenses)
2792- return (
2793- License.OTHER_PROPRIETARY in licenses
2794- or License.OTHER_OPEN_SOURCE in licenses
2795- or [License.DONT_KNOW] == licenses)
2796-
2797- def getTemplateName(self):
2798- """Return the name of the email template for the licensing case."""
2799- licenses = list(self.product.licenses)
2800- if [License.DONT_KNOW] == licenses:
2801- template_name = 'product-license-dont-know.txt'
2802- elif License.OTHER_PROPRIETARY in licenses:
2803- template_name = 'product-license-other-proprietary.txt'
2804- else:
2805- template_name = 'product-license-other-open-source.txt'
2806- return template_name
2807-
2808- def getCommercialUseMessage(self):
2809- """Return a message explaining the current commercial subscription."""
2810- commercial_subscription = self.product.commercial_subscription
2811- if commercial_subscription is None:
2812- return ''
2813- iso_date = commercial_subscription.date_expires.date().isoformat()
2814- if not self.product.has_current_commercial_subscription:
2815- message = "%s's commercial subscription expired on %s."
2816- elif 'complimentary' in commercial_subscription.sales_system_id:
2817- message = (
2818- "%s's complimentary commercial subscription expires on %s.")
2819- else:
2820- message = "%s's commercial subscription expires on %s."
2821- message = message % (self.product.displayname, iso_date)
2822- return textwrap.fill(message, 72)
2823-
2824- def send(self):
2825- """Send a message to the user about the product's license."""
2826- if not self.needs_notification(self.product):
2827- # The project has a common license.
2828- return False
2829- user_address = format_address(
2830- self.user.displayname, self.user.preferredemail.email)
2831- from_address = format_address(
2832- "Launchpad", config.canonical.noreply_from_address)
2833- commercial_address = format_address(
2834- 'Commercial', 'commercial@launchpad.net')
2835- substitutions = dict(
2836- user_displayname=self.user.displayname,
2837- user_name=self.user.name,
2838- product_name=self.product.name,
2839- product_url=canonical_url(self.product),
2840- commercial_use_expiration=self.getCommercialUseMessage(),
2841- )
2842- # Email the user about license policy.
2843- subject = (
2844- "License information for %(product_name)s "
2845- "in Launchpad" % substitutions)
2846- template = get_email_template(
2847- self.getTemplateName(), app='registry')
2848- message = template % substitutions
2849- simple_sendmail(
2850- from_address, user_address,
2851- subject, message, headers={'Reply-To': commercial_address})
2852- # Inform that Launchpad recognized the license change.
2853- self._addLicenseChangeToReviewWhiteboard()
2854- return True
2855-
2856- def display(self):
2857- """Show a message in a browser page about the product's license."""
2858- request = get_current_browser_request()
2859- message = self.getCommercialUseMessage()
2860- if request is None or message == '':
2861- return False
2862- safe_message = structured(
2863- '%s<br />Learn more about '
2864- '<a href="https://help.launchpad.net/CommercialHosting">'
2865- 'commercial subscriptions</a>', message)
2866- request.response.addNotification(safe_message)
2867- return True
2868-
2869- @staticmethod
2870- def _formatDate(now=None):
2871- """Return the date formatted for messages."""
2872- if now is None:
2873- now = datetime.now(tz=pytz.UTC)
2874- return now.strftime('%Y-%m-%d')
2875-
2876- def _addLicenseChangeToReviewWhiteboard(self):
2877- """Update the whiteboard for the reviewer's benefit."""
2878- now = self._formatDate()
2879- whiteboard = 'User notified of license policy on %s.' % now
2880- naked_product = removeSecurityProxy(self.product)
2881- if naked_product.reviewer_whiteboard is None:
2882- naked_product.reviewer_whiteboard = whiteboard
2883- else:
2884- naked_product.reviewer_whiteboard += '\n' + whiteboard
2885->>>>>>> MERGE-SOURCE
2886+from datetime import datetime
2887+import textwrap
2888+
2889+import pytz
2890+from zope.security.proxy import removeSecurityProxy
2891+
2892+from lp.registry.interfaces.person import IPerson
2893+from lp.registry.interfaces.product import License
2894+from lp.services.config import config
2895+from lp.services.mail.helpers import get_email_template
2896+from lp.services.mail.sendmail import (
2897+ format_address,
2898+ simple_sendmail,
2899+ )
2900+from lp.services.webapp.menu import structured
2901+from lp.services.webapp.publisher import (
2902+ canonical_url,
2903+ get_current_browser_request,
2904+ )
2905+
2906+
2907+def product_licenses_modified(product, event):
2908+ """Send a notification if licenses changed and a license is special."""
2909+ if not event.edited_fields:
2910+ return
2911+ licenses_changed = 'licenses' in event.edited_fields
2912+ needs_notification = LicenseNotification.needs_notification(product)
2913+ if licenses_changed and needs_notification:
2914+ user = IPerson(event.user)
2915+ notification = LicenseNotification(product, user)
2916+ notification.send()
2917+ notification.display()
2918+
2919+
2920+class LicenseNotification:
2921+ """Send notification about special licenses to the user."""
2922+
2923+ def __init__(self, product, user):
2924+ self.product = product
2925+ self.user = user
2926+
2927+ @staticmethod
2928+ def needs_notification(product):
2929+ licenses = list(product.licenses)
2930+ return (
2931+ License.OTHER_PROPRIETARY in licenses
2932+ or License.OTHER_OPEN_SOURCE in licenses
2933+ or [License.DONT_KNOW] == licenses)
2934+
2935+ def getTemplateName(self):
2936+ """Return the name of the email template for the licensing case."""
2937+ licenses = list(self.product.licenses)
2938+ if [License.DONT_KNOW] == licenses:
2939+ template_name = 'product-license-dont-know.txt'
2940+ elif License.OTHER_PROPRIETARY in licenses:
2941+ template_name = 'product-license-other-proprietary.txt'
2942+ else:
2943+ template_name = 'product-license-other-open-source.txt'
2944+ return template_name
2945+
2946+ def getCommercialUseMessage(self):
2947+ """Return a message explaining the current commercial subscription."""
2948+ commercial_subscription = self.product.commercial_subscription
2949+ if commercial_subscription is None:
2950+ return ''
2951+ iso_date = commercial_subscription.date_expires.date().isoformat()
2952+ if not self.product.has_current_commercial_subscription:
2953+ message = "%s's commercial subscription expired on %s."
2954+ elif 'complimentary' in commercial_subscription.sales_system_id:
2955+ message = (
2956+ "%s's complimentary commercial subscription expires on %s.")
2957+ else:
2958+ message = "%s's commercial subscription expires on %s."
2959+ message = message % (self.product.displayname, iso_date)
2960+ return textwrap.fill(message, 72)
2961+
2962+ def send(self):
2963+ """Send a message to the user about the product's license."""
2964+ if not self.needs_notification(self.product):
2965+ # The project has a common license.
2966+ return False
2967+ user_address = format_address(
2968+ self.user.displayname, self.user.preferredemail.email)
2969+ from_address = format_address(
2970+ "Launchpad", config.canonical.noreply_from_address)
2971+ commercial_address = format_address(
2972+ 'Commercial', 'commercial@launchpad.net')
2973+ substitutions = dict(
2974+ user_displayname=self.user.displayname,
2975+ user_name=self.user.name,
2976+ product_name=self.product.name,
2977+ product_url=canonical_url(self.product),
2978+ commercial_use_expiration=self.getCommercialUseMessage(),
2979+ )
2980+ # Email the user about license policy.
2981+ subject = (
2982+ "License information for %(product_name)s "
2983+ "in Launchpad" % substitutions)
2984+ template = get_email_template(
2985+ self.getTemplateName(), app='registry')
2986+ message = template % substitutions
2987+ simple_sendmail(
2988+ from_address, user_address,
2989+ subject, message, headers={'Reply-To': commercial_address})
2990+ # Inform that Launchpad recognized the license change.
2991+ self._addLicenseChangeToReviewWhiteboard()
2992+ return True
2993+
2994+ def display(self):
2995+ """Show a message in a browser page about the product's license."""
2996+ request = get_current_browser_request()
2997+ message = self.getCommercialUseMessage()
2998+ if request is None or message == '':
2999+ return False
3000+ safe_message = structured(
3001+ '%s<br />Learn more about '
3002+ '<a href="https://help.launchpad.net/CommercialHosting">'
3003+ 'commercial subscriptions</a>', message)
3004+ request.response.addNotification(safe_message)
3005+ return True
3006+
3007+ @staticmethod
3008+ def _formatDate(now=None):
3009+ """Return the date formatted for messages."""
3010+ if now is None:
3011+ now = datetime.now(tz=pytz.UTC)
3012+ return now.strftime('%Y-%m-%d')
3013+
3014+ def _addLicenseChangeToReviewWhiteboard(self):
3015+ """Update the whiteboard for the reviewer's benefit."""
3016+ now = self._formatDate()
3017+ whiteboard = 'User notified of license policy on %s.' % now
3018+ naked_product = removeSecurityProxy(self.product)
3019+ if naked_product.reviewer_whiteboard is None:
3020+ naked_product.reviewer_whiteboard = whiteboard
3021+ else:
3022+ naked_product.reviewer_whiteboard += '\n' + whiteboard
3023
3024=== modified file 'lib/lp/registry/templates/pillar-sharing-details.pt'
3025--- lib/lp/registry/templates/pillar-sharing-details.pt 2012-03-29 14:36:36 +0000
3026+++ lib/lp/registry/templates/pillar-sharing-details.pt 2012-03-26 20:49:13 +0000
3027@@ -1,4 +1,3 @@
3028-<<<<<<< TREE
3029 <html
3030 xmlns="http://www.w3.org/1999/xhtml"
3031 xmlns:tal="http://xml.zope.org/namespaces/tal"
3032@@ -61,19 +60,3 @@
3033 </div>
3034 </body>
3035 </html>
3036-=======
3037-<html
3038- xmlns="http://www.w3.org/1999/xhtml"
3039- xmlns:tal="http://xml.zope.org/namespaces/tal"
3040- xmlns:metal="http://xml.zope.org/namespaces/metal"
3041- xmlns:i18n="http://xml.zope.org/namespaces/i18n"
3042- metal:use-macro="view/macro:page/main_only"
3043- i18n:domain="launchpad"
3044->
3045-
3046-<body>
3047- <div metal:fill-slot="main">
3048- </div>
3049-</body>
3050-</html>
3051->>>>>>> MERGE-SOURCE
3052
3053=== modified file 'lib/lp/registry/templates/pillar-sharing.pt'
3054--- lib/lp/registry/templates/pillar-sharing.pt 2012-03-29 14:36:36 +0000
3055+++ lib/lp/registry/templates/pillar-sharing.pt 2012-03-22 09:00:36 +0000
3056@@ -28,7 +28,6 @@
3057 Proprietary, embargoed security, or user-data information is
3058 shared with these users and teams.
3059 </p>
3060-<<<<<<< TREE
3061 <ul class="horizontal">
3062 <li><a id='add-sharee-link' class='sprite add js-action' href="#">Share
3063 with someone</a></li>
3064@@ -41,19 +40,5 @@
3065
3066 </div>
3067
3068-=======
3069- <ul class="horizontal">
3070- <li><a id='add-sharee-link' class='sprite add js-action' href="#">Share
3071- with someone</a></li>
3072- <li><a id="audit-link" class="sprite info" href='#'>Audit sharing</a></li>
3073- </ul>
3074-
3075- <div tal:define="batch_navigator view/shareeData">
3076- <tal:shareelisting content="structure batch_navigator/@@+sharee-table-view" />
3077- </div>
3078-
3079- </div>
3080-
3081->>>>>>> MERGE-SOURCE
3082 </body>
3083 </html>
3084
3085=== modified file 'lib/lp/registry/tests/test_accesspolicy.py'
3086--- lib/lp/registry/tests/test_accesspolicy.py 2012-03-29 14:36:36 +0000
3087+++ lib/lp/registry/tests/test_accesspolicy.py 2012-03-23 03:40:17 +0000
3088@@ -468,7 +468,6 @@
3089 artifact=artifact_grant.abstract_artifact, policy=another_policy)
3090 self.assertContentEqual(
3091 [policy_grant.grantee, artifact_grant.grantee],
3092-<<<<<<< TREE
3093 apgfs.findGranteesByPolicy([
3094 policy, another_policy, policy_with_no_grantees]))
3095
3096@@ -537,60 +536,6 @@
3097 [(policy_grant.grantee, {policy: SharingPermission.ALL})],
3098 apgfs.findGranteePermissionsByPolicy(
3099 [policy], [grantee_in_result]))
3100-=======
3101- apgfs.findGranteesByPolicy([
3102- policy, another_policy, policy_with_no_grantees]))
3103-
3104- def findGranteePermissionsByPolicy(self):
3105- # findGranteePermissionsByPolicy() returns anyone with a grant for any
3106- # of the policies or the policies' artifacts.
3107- apgfs = getUtility(IAccessPolicyGrantFlatSource)
3108-
3109- # People with grants on the policy show up.
3110- policy_with_no_grantees = self.factory.makeAccessPolicy()
3111- policy = self.factory.makeAccessPolicy()
3112- policy_grant = self.factory.makeAccessPolicyGrant(policy=policy)
3113- self.assertContentEqual(
3114- [(policy_grant.grantee, policy, 'ALL')],
3115- apgfs.findGranteePermissionsByPolicy(
3116- [policy, policy_with_no_grantees]))
3117-
3118- # But not people with grants on artifacts.
3119- artifact_grant = self.factory.makeAccessArtifactGrant()
3120- self.assertContentEqual(
3121- [(policy_grant.grantee, policy, 'ALL')],
3122- apgfs.findGranteePermissionsByPolicy(
3123- [policy, policy_with_no_grantees]))
3124-
3125- # Unless the artifacts are linked to the policy.
3126- another_policy = self.factory.makeAccessPolicy()
3127- self.factory.makeAccessPolicyArtifact(
3128- artifact=artifact_grant.abstract_artifact, policy=another_policy)
3129- self.assertContentEqual(
3130- [(policy_grant.grantee, policy, 'ALL'),
3131- (artifact_grant.grantee, another_policy, 'SOME')],
3132- apgfs.findGranteePermissionsByPolicy([
3133- policy, another_policy, policy_with_no_grantees]))
3134-
3135- def test_findGranteePermissionsByPolicy_filter_grantees(self):
3136- # findGranteePermissionsByPolicy() returns anyone with a grant for any
3137- # of the policies or the policies' artifacts so long as the grantee is
3138- # in the specified list of grantees.
3139- apgfs = getUtility(IAccessPolicyGrantFlatSource)
3140-
3141- # People with grants on the policy show up.
3142- policy = self.factory.makeAccessPolicy()
3143- grantee_in_result = self.factory.makePerson()
3144- grantee_not_in_result = self.factory.makePerson()
3145- policy_grant = self.factory.makeAccessPolicyGrant(
3146- policy=policy, grantee=grantee_in_result)
3147- self.factory.makeAccessPolicyGrant(
3148- policy=policy, grantee=grantee_not_in_result)
3149- self.assertContentEqual(
3150- [(policy_grant.grantee, policy, 'ALL')],
3151- apgfs.findGranteePermissionsByPolicy(
3152- [policy], [grantee_in_result]))
3153->>>>>>> MERGE-SOURCE
3154
3155 def test_findArtifactsByGrantee(self):
3156 # findArtifactsByGrantee() returns the artifacts for grantee for any of
3157
3158=== modified file 'lib/lp/registry/tests/test_pillar.py'
3159--- lib/lp/registry/tests/test_pillar.py 2012-03-29 14:36:36 +0000
3160+++ lib/lp/registry/tests/test_pillar.py 2012-03-22 23:21:24 +0000
3161@@ -5,20 +5,11 @@
3162
3163 from zope.component import getUtility
3164
3165-<<<<<<< TREE
3166-from lp.registry.interfaces.pillar import (
3167- IPillarNameSet,
3168- IPillarPerson,
3169- )
3170-from lp.registry.model.pillar import PillarPerson
3171-=======
3172-from lp.registry.interfaces.pillar import (
3173- IPillarNameSet,
3174- IPillarPerson,
3175- )
3176-from lp.registry.model.pillar import PillarPerson
3177-
3178->>>>>>> MERGE-SOURCE
3179+from lp.registry.interfaces.pillar import (
3180+ IPillarNameSet,
3181+ IPillarPerson,
3182+ )
3183+from lp.registry.model.pillar import PillarPerson
3184 from lp.testing import (
3185 login,
3186 TestCaseWithFactory,
3187
3188=== modified file 'lib/lp/registry/tests/test_productjob.py'
3189--- lib/lp/registry/tests/test_productjob.py 2012-03-29 14:36:36 +0000
3190+++ lib/lp/registry/tests/test_productjob.py 2012-03-24 12:41:36 +0000
3191@@ -1,4 +1,3 @@
3192-<<<<<<< TREE
3193 # Copyright 2010 Canonical Ltd. This software is licensed under the
3194 # GNU Affero General Public License version 3 (see the file LICENSE).
3195
3196@@ -370,191 +369,3 @@
3197 self.assertEqual(subject, notifications[0]['Subject'])
3198 self.assertIn(
3199 'Launchpad <noreply@launchpad.net>', notifications[0]['From'])
3200-=======
3201-# Copyright 2010 Canonical Ltd. This software is licensed under the
3202-# GNU Affero General Public License version 3 (see the file LICENSE).
3203-
3204-"""Tests for ProductJobs."""
3205-
3206-__metaclass__ = type
3207-
3208-from datetime import (
3209- datetime,
3210- timedelta,
3211- )
3212-
3213-import pytz
3214-
3215-from zope.interface import (
3216- classProvides,
3217- implements,
3218- )
3219-from zope.security.proxy import removeSecurityProxy
3220-
3221-from lp.registry.enums import ProductJobType
3222-from lp.registry.interfaces.productjob import (
3223- IProductJob,
3224- IProductJobSource,
3225- )
3226-from lp.registry.model.productjob import (
3227- ProductJob,
3228- ProductJobDerived,
3229- )
3230-from lp.testing import TestCaseWithFactory
3231-from lp.testing.layers import (
3232- DatabaseFunctionalLayer,
3233- LaunchpadZopelessLayer,
3234- )
3235-
3236-
3237-class ProductJobTestCase(TestCaseWithFactory):
3238- """Test case for basic ProductJob class."""
3239-
3240- layer = LaunchpadZopelessLayer
3241-
3242- def test_init(self):
3243- product = self.factory.makeProduct()
3244- metadata = ('some', 'arbitrary', 'metadata')
3245- product_job = ProductJob(
3246- product, ProductJobType.REVIEWER_NOTIFICATION, metadata)
3247- self.assertEqual(product, product_job.product)
3248- self.assertEqual(
3249- ProductJobType.REVIEWER_NOTIFICATION, product_job.job_type)
3250- expected_json_data = '["some", "arbitrary", "metadata"]'
3251- self.assertEqual(expected_json_data, product_job._json_data)
3252-
3253- def test_metadata(self):
3254- # The python structure stored as json is returned as python.
3255- product = self.factory.makeProduct()
3256- metadata = {
3257- 'a_list': ('some', 'arbitrary', 'metadata'),
3258- 'a_number': 1,
3259- 'a_string': 'string',
3260- }
3261- product_job = ProductJob(
3262- product, ProductJobType.REVIEWER_NOTIFICATION, metadata)
3263- metadata['a_list'] = list(metadata['a_list'])
3264- self.assertEqual(metadata, product_job.metadata)
3265-
3266-
3267-class IProductThingJob(IProductJob):
3268- """An interface for testing derived job classes."""
3269-
3270-
3271-class IProductThingJobSource(IProductJobSource):
3272- """An interface for testing derived job source classes."""
3273-
3274-
3275-class FakeProductJob(ProductJobDerived):
3276- """A class that reuses other interfaces and types for testing."""
3277- class_job_type = ProductJobType.REVIEWER_NOTIFICATION
3278- implements(IProductThingJob)
3279- classProvides(IProductThingJobSource)
3280-
3281-
3282-class OtherFakeProductJob(ProductJobDerived):
3283- """A class that reuses other interfaces and types for testing."""
3284- class_job_type = ProductJobType.COMMERCIAL_EXPIRED
3285- implements(IProductThingJob)
3286- classProvides(IProductThingJobSource)
3287-
3288-
3289-class ProductJobDerivedTestCase(TestCaseWithFactory):
3290- """Test case for the ProductJobDerived class."""
3291-
3292- layer = DatabaseFunctionalLayer
3293-
3294- def test_repr(self):
3295- product = self.factory.makeProduct('fnord')
3296- metadata = {'foo': 'bar'}
3297- job = FakeProductJob.create(product, metadata)
3298- self.assertEqual(
3299- '<FakeProductJob for fnord status=Waiting>', repr(job))
3300-
3301- def test_create_success(self):
3302- # Create an instance of ProductJobDerived that delegates to
3303- # ProductJob.
3304- product = self.factory.makeProduct()
3305- metadata = {'foo': 'bar'}
3306- self.assertIs(True, IProductJobSource.providedBy(ProductJobDerived))
3307- job = FakeProductJob.create(product, metadata)
3308- self.assertIsInstance(job, ProductJobDerived)
3309- self.assertIs(True, IProductJob.providedBy(job))
3310- self.assertIs(True, IProductJob.providedBy(job.context))
3311-
3312- def test_create_raises_error(self):
3313- # ProductJobDerived.create() raises an error because it
3314- # needs to be subclassed to work properly.
3315- product = self.factory.makeProduct()
3316- metadata = {'foo': 'bar'}
3317- self.assertRaises(
3318- AttributeError, ProductJobDerived.create, product, metadata)
3319-
3320- def test_iterReady(self):
3321- # iterReady finds job in the READY status that are of the same type.
3322- product = self.factory.makeProduct()
3323- metadata = {'foo': 'bar'}
3324- job_1 = FakeProductJob.create(product, metadata)
3325- job_2 = FakeProductJob.create(product, metadata)
3326- job_2.start()
3327- OtherFakeProductJob.create(product, metadata)
3328- jobs = list(FakeProductJob.iterReady())
3329- self.assertEqual(1, len(jobs))
3330- self.assertEqual(job_1, jobs[0])
3331-
3332- def test_find_product(self):
3333- # Find all the jobs for a product regardless of date or job type.
3334- product = self.factory.makeProduct()
3335- metadata = {'foo': 'bar'}
3336- job_1 = FakeProductJob.create(product, metadata)
3337- job_2 = OtherFakeProductJob.create(product, metadata)
3338- FakeProductJob.create(self.factory.makeProduct(), metadata)
3339- jobs = list(ProductJobDerived.find(product=product))
3340- self.assertEqual(2, len(jobs))
3341- self.assertContentEqual([job_1.id, job_2.id], [job.id for job in jobs])
3342-
3343- def test_find_job_type(self):
3344- # Find all the jobs for a product and job_type regardless of date.
3345- product = self.factory.makeProduct()
3346- metadata = {'foo': 'bar'}
3347- job_1 = FakeProductJob.create(product, metadata)
3348- job_2 = FakeProductJob.create(product, metadata)
3349- OtherFakeProductJob.create(product, metadata)
3350- jobs = list(ProductJobDerived.find(
3351- product, job_type=ProductJobType.REVIEWER_NOTIFICATION))
3352- self.assertEqual(2, len(jobs))
3353- self.assertContentEqual([job_1.id, job_2.id], [job.id for job in jobs])
3354-
3355- def test_find_date_since(self):
3356- # Find all the jobs for a product since a date regardless of job_type.
3357- now = datetime.now(pytz.utc)
3358- seven_days_ago = now - timedelta(7)
3359- thirty_days_ago = now - timedelta(30)
3360- product = self.factory.makeProduct()
3361- metadata = {'foo': 'bar'}
3362- job_1 = FakeProductJob.create(product, metadata)
3363- removeSecurityProxy(job_1.job).date_created = thirty_days_ago
3364- job_2 = FakeProductJob.create(product, metadata)
3365- removeSecurityProxy(job_2.job).date_created = seven_days_ago
3366- job_3 = OtherFakeProductJob.create(product, metadata)
3367- removeSecurityProxy(job_3.job).date_created = now
3368- jobs = list(ProductJobDerived.find(product, date_since=seven_days_ago))
3369- self.assertEqual(2, len(jobs))
3370- self.assertContentEqual([job_2.id, job_3.id], [job.id for job in jobs])
3371-
3372- def test_log_name(self):
3373- # The log_name is the name of the implementing class.
3374- product = self.factory.makeProduct('fnord')
3375- metadata = {'foo': 'bar'}
3376- job = FakeProductJob.create(product, metadata)
3377- self.assertEqual('FakeProductJob', job.log_name)
3378-
3379- def test_getOopsVars(self):
3380- # The project name is added to the oops vars.
3381- product = self.factory.makeProduct('fnord')
3382- metadata = {'foo': 'bar'}
3383- job = FakeProductJob.create(product, metadata)
3384- oops_vars = job.getOopsVars()
3385- self.assertIs(True, len(oops_vars) > 1)
3386- self.assertIn(('product', product.name), oops_vars)
3387->>>>>>> MERGE-SOURCE
3388
3389=== modified file 'lib/lp/registry/tests/test_subscribers.py'
3390--- lib/lp/registry/tests/test_subscribers.py 2012-03-29 14:36:36 +0000
3391+++ lib/lp/registry/tests/test_subscribers.py 2012-03-22 23:21:24 +0000
3392@@ -1,524 +1,260 @@
3393-<<<<<<< TREE
3394-# Copyright 2012 Canonical Ltd. This software is licensed under the
3395-# GNU Affero General Public License version 3 (see the file LICENSE).
3396-
3397-"""Test subscruber classes and functions."""
3398-
3399-__metaclass__ = type
3400-
3401-from datetime import datetime
3402-
3403-from lazr.lifecycle.event import ObjectModifiedEvent
3404-import pytz
3405-from zope.security.proxy import removeSecurityProxy
3406-
3407-from lp.registry.interfaces.product import License
3408-from lp.registry.subscribers import (
3409- LicenseNotification,
3410- product_licenses_modified,
3411- )
3412-from lp.services.webapp.publisher import get_current_browser_request
3413-from lp.testing import (
3414- login_person,
3415- logout,
3416- TestCaseWithFactory,
3417- )
3418-from lp.testing.layers import DatabaseFunctionalLayer
3419-from lp.testing.mail_helpers import pop_notifications
3420-
3421-
3422-class ProductLicensesModifiedTestCase(TestCaseWithFactory):
3423-
3424- layer = DatabaseFunctionalLayer
3425-
3426- def make_product_event(self, licenses, edited_fields='licenses'):
3427- product = self.factory.makeProduct(licenses=licenses)
3428- pop_notifications()
3429- login_person(product.owner)
3430- event = ObjectModifiedEvent(
3431- product, product, edited_fields, user=product.owner)
3432- return product, event
3433-
3434- def test_product_licenses_modified_licenses_not_edited(self):
3435- product, event = self.make_product_event(
3436- [License.OTHER_PROPRIETARY], edited_fields='_owner')
3437- product_licenses_modified(product, event)
3438- notifications = pop_notifications()
3439- self.assertEqual(0, len(notifications))
3440-
3441- def test_product_licenses_modified_licenses_common_license(self):
3442- product, event = self.make_product_event([License.MIT])
3443- product_licenses_modified(product, event)
3444- notifications = pop_notifications()
3445- self.assertEqual(0, len(notifications))
3446- request = get_current_browser_request()
3447- self.assertEqual(0, len(request.response.notifications))
3448-
3449- def test_product_licenses_modified_licenses_other_proprietary(self):
3450- product, event = self.make_product_event([License.OTHER_PROPRIETARY])
3451- product_licenses_modified(product, event)
3452- notifications = pop_notifications()
3453- self.assertEqual(1, len(notifications))
3454- request = get_current_browser_request()
3455- self.assertEqual(1, len(request.response.notifications))
3456-
3457- def test_product_licenses_modified_licenses_other_open_source(self):
3458- product, event = self.make_product_event([License.OTHER_OPEN_SOURCE])
3459- product_licenses_modified(product, event)
3460- notifications = pop_notifications()
3461- self.assertEqual(1, len(notifications))
3462- request = get_current_browser_request()
3463- self.assertEqual(0, len(request.response.notifications))
3464-
3465- def test_product_licenses_modified_licenses_other_dont_know(self):
3466- product, event = self.make_product_event([License.DONT_KNOW])
3467- product_licenses_modified(product, event)
3468- notifications = pop_notifications()
3469- self.assertEqual(1, len(notifications))
3470- request = get_current_browser_request()
3471- self.assertEqual(0, len(request.response.notifications))
3472-
3473-
3474-class LicenseNotificationTestCase(TestCaseWithFactory):
3475-
3476- layer = DatabaseFunctionalLayer
3477-
3478- def make_product_user(self, licenses):
3479- # Setup an a view that implements ProductLicenseMixin.
3480- super(LicenseNotificationTestCase, self).setUp()
3481- user = self.factory.makePerson(
3482- name='registrant', email='registrant@launchpad.dev')
3483- login_person(user)
3484- product = self.factory.makeProduct(
3485- name='ball', owner=user, licenses=licenses)
3486- pop_notifications()
3487- return product, user
3488-
3489- def verify_whiteboard(self, product):
3490- # Verify that the review whiteboard was updated.
3491- naked_product = removeSecurityProxy(product)
3492- entries = naked_product.reviewer_whiteboard.split('\n')
3493- whiteboard, stamp = entries[-1].rsplit(' ', 1)
3494- self.assertEqual(
3495- 'User notified of license policy on', whiteboard)
3496-
3497- def verify_user_email(self, notification):
3498- # Verify that the user was sent an email about the license change.
3499- self.assertEqual(
3500- 'License information for ball in Launchpad',
3501- notification['Subject'])
3502- self.assertEqual(
3503- 'Registrant <registrant@launchpad.dev>',
3504- notification['To'])
3505- self.assertEqual(
3506- 'Commercial <commercial@launchpad.net>',
3507- notification['Reply-To'])
3508-
3509- def test_send_known_license(self):
3510- # A known license does not generate an email.
3511- product, user = self.make_product_user([License.GNU_GPL_V2])
3512- notification = LicenseNotification(product, user)
3513- result = notification.send()
3514- self.assertIs(False, result)
3515- self.assertEqual(0, len(pop_notifications()))
3516-
3517- def test_send_other_dont_know(self):
3518- # An Other/I don't know license sends one email.
3519- product, user = self.make_product_user([License.DONT_KNOW])
3520- notification = LicenseNotification(product, user)
3521- result = notification.send()
3522- self.assertIs(True, result)
3523- self.verify_whiteboard(product)
3524- notifications = pop_notifications()
3525- self.assertEqual(1, len(notifications))
3526- self.verify_user_email(notifications.pop())
3527-
3528- def test_send_other_open_source(self):
3529- # An Other/Open Source license sends one email.
3530- product, user = self.make_product_user([License.OTHER_OPEN_SOURCE])
3531- notification = LicenseNotification(product, user)
3532- result = notification.send()
3533- self.assertIs(True, result)
3534- self.verify_whiteboard(product)
3535- notifications = pop_notifications()
3536- self.assertEqual(1, len(notifications))
3537- self.verify_user_email(notifications.pop())
3538-
3539- def test_send_other_proprietary(self):
3540- # An Other/Proprietary license sends one email.
3541- product, user = self.make_product_user([License.OTHER_PROPRIETARY])
3542- notification = LicenseNotification(product, user)
3543- result = notification.send()
3544- self.assertIs(True, result)
3545- self.verify_whiteboard(product)
3546- notifications = pop_notifications()
3547- self.assertEqual(1, len(notifications))
3548- self.verify_user_email(notifications.pop())
3549-
3550- def test_display_no_request(self):
3551- # If there is no request, there is no reason to show a message in
3552- # the browser.
3553- product, user = self.make_product_user([License.GNU_GPL_V2])
3554- notification = LicenseNotification(product, user)
3555- logout()
3556- result = notification.display()
3557- self.assertIs(False, result)
3558-
3559- def test_display_no_message(self):
3560- # A notification is not added if there is no message to show.
3561- product, user = self.make_product_user([License.GNU_GPL_V2])
3562- notification = LicenseNotification(product, user)
3563- result = notification.display()
3564- self.assertEqual('', notification.getCommercialUseMessage())
3565- self.assertIs(False, result)
3566-
3567- def test_display_has_message(self):
3568- # A notification is added if there is a message to show.
3569- product, user = self.make_product_user([License.OTHER_PROPRIETARY])
3570- notification = LicenseNotification(product, user)
3571- result = notification.display()
3572- message = notification.getCommercialUseMessage()
3573- self.assertIs(True, result)
3574- request = get_current_browser_request()
3575- self.assertEqual(1, len(request.response.notifications))
3576- self.assertIn(message, request.response.notifications[0].message)
3577- self.assertIn(
3578- '<a href="https://help.launchpad.net/CommercialHosting">',
3579- request.response.notifications[0].message)
3580-
3581- def test_display_escapee_user_data(self):
3582- # A notification is added if there is a message to show.
3583- product, user = self.make_product_user([License.OTHER_PROPRIETARY])
3584- product.displayname = '<b>Look</b>'
3585- notification = LicenseNotification(product, user)
3586- result = notification.display()
3587- self.assertIs(True, result)
3588- request = get_current_browser_request()
3589- self.assertEqual(1, len(request.response.notifications))
3590- self.assertIn(
3591- '&lt;b&gt;Look&lt;/b&gt;',
3592- request.response.notifications[0].message)
3593-
3594- def test_formatDate(self):
3595- # Verify the date format.
3596- now = datetime(2005, 6, 15, 0, 0, 0, 0, pytz.UTC)
3597- result = LicenseNotification._formatDate(now)
3598- self.assertEqual('2005-06-15', result)
3599-
3600- def test_getTemplateName_other_dont_know(self):
3601- product, user = self.make_product_user([License.DONT_KNOW])
3602- notification = LicenseNotification(product, user)
3603- self.assertEqual(
3604- 'product-license-dont-know.txt',
3605- notification.getTemplateName())
3606-
3607- def test_getTemplateName_propietary(self):
3608- product, user = self.make_product_user([License.OTHER_PROPRIETARY])
3609- notification = LicenseNotification(product, user)
3610- self.assertEqual(
3611- 'product-license-other-proprietary.txt',
3612- notification.getTemplateName())
3613-
3614- def test_getTemplateName_other_open_source(self):
3615- product, user = self.make_product_user([License.OTHER_OPEN_SOURCE])
3616- notification = LicenseNotification(product, user)
3617- self.assertEqual(
3618- 'product-license-other-open-source.txt',
3619- notification.getTemplateName())
3620-
3621- def test_getCommercialUseMessage_without_commercial_subscription(self):
3622- product, user = self.make_product_user([License.MIT])
3623- notification = LicenseNotification(product, user)
3624- self.assertEqual('', notification.getCommercialUseMessage())
3625-
3626- def test_getCommercialUseMessage_with_complimentary_cs(self):
3627- product, user = self.make_product_user([License.OTHER_PROPRIETARY])
3628- notification = LicenseNotification(product, user)
3629- message = (
3630- "Ball's complimentary commercial subscription expires on %s." %
3631- product.commercial_subscription.date_expires.date().isoformat())
3632- self.assertEqual(message, notification.getCommercialUseMessage())
3633-
3634- def test_getCommercialUseMessage_with_commercial_subscription(self):
3635- product, user = self.make_product_user([License.MIT])
3636- self.factory.makeCommercialSubscription(product)
3637- product.licenses = [License.MIT, License.OTHER_PROPRIETARY]
3638- notification = LicenseNotification(product, user)
3639- message = (
3640- "Ball's commercial subscription expires on %s." %
3641- product.commercial_subscription.date_expires.date().isoformat())
3642- self.assertEqual(message, notification.getCommercialUseMessage())
3643-
3644- def test_getCommercialUseMessage_with_expired_cs(self):
3645- product, user = self.make_product_user([License.MIT])
3646- self.factory.makeCommercialSubscription(product, expired=True)
3647- product.licenses = [License.MIT, License.OTHER_PROPRIETARY]
3648- notification = LicenseNotification(product, user)
3649- message = (
3650- "Ball's commercial subscription expired on %s." %
3651- product.commercial_subscription.date_expires.date().isoformat())
3652- self.assertEqual(message, notification.getCommercialUseMessage())
3653- self.assertEqual(message, notification.getCommercialUseMessage())
3654-=======
3655-# Copyright 2012 Canonical Ltd. This software is licensed under the
3656-# GNU Affero General Public License version 3 (see the file LICENSE).
3657-
3658-"""Test subscruber classes and functions."""
3659-
3660-__metaclass__ = type
3661-
3662-from datetime import datetime
3663-
3664-import pytz
3665-
3666-from zope.security.proxy import removeSecurityProxy
3667-from lazr.lifecycle.event import ObjectModifiedEvent
3668-
3669-from lp.registry.interfaces.product import License
3670-from lp.registry.subscribers import (
3671- LicenseNotification,
3672- product_licenses_modified,
3673- )
3674-from lp.services.webapp.publisher import get_current_browser_request
3675-from lp.testing import (
3676- login_person,
3677- logout,
3678- TestCaseWithFactory,
3679- )
3680-from lp.testing.layers import DatabaseFunctionalLayer
3681-from lp.testing.mail_helpers import pop_notifications
3682-
3683-
3684-class ProductLicensesModifiedTestCase(TestCaseWithFactory):
3685-
3686- layer = DatabaseFunctionalLayer
3687-
3688- def make_product_event(self, licenses, edited_fields='licenses'):
3689- product = self.factory.makeProduct(licenses=licenses)
3690- pop_notifications()
3691- login_person(product.owner)
3692- event = ObjectModifiedEvent(
3693- product, product, edited_fields, user=product.owner)
3694- return product, event
3695-
3696- def test_product_licenses_modified_licenses_not_edited(self):
3697- product, event = self.make_product_event(
3698- [License.OTHER_PROPRIETARY], edited_fields='_owner')
3699- product_licenses_modified(product, event)
3700- notifications = pop_notifications()
3701- self.assertEqual(0, len(notifications))
3702-
3703- def test_product_licenses_modified_licenses_common_license(self):
3704- product, event = self.make_product_event([License.MIT])
3705- product_licenses_modified(product, event)
3706- notifications = pop_notifications()
3707- self.assertEqual(0, len(notifications))
3708- request = get_current_browser_request()
3709- self.assertEqual(0, len(request.response.notifications))
3710-
3711- def test_product_licenses_modified_licenses_other_proprietary(self):
3712- product, event = self.make_product_event([License.OTHER_PROPRIETARY])
3713- product_licenses_modified(product, event)
3714- notifications = pop_notifications()
3715- self.assertEqual(1, len(notifications))
3716- request = get_current_browser_request()
3717- self.assertEqual(1, len(request.response.notifications))
3718-
3719- def test_product_licenses_modified_licenses_other_open_source(self):
3720- product, event = self.make_product_event([License.OTHER_OPEN_SOURCE])
3721- product_licenses_modified(product, event)
3722- notifications = pop_notifications()
3723- self.assertEqual(1, len(notifications))
3724- request = get_current_browser_request()
3725- self.assertEqual(0, len(request.response.notifications))
3726-
3727- def test_product_licenses_modified_licenses_other_dont_know(self):
3728- product, event = self.make_product_event([License.DONT_KNOW])
3729- product_licenses_modified(product, event)
3730- notifications = pop_notifications()
3731- self.assertEqual(1, len(notifications))
3732- request = get_current_browser_request()
3733- self.assertEqual(0, len(request.response.notifications))
3734-
3735-
3736-class LicenseNotificationTestCase(TestCaseWithFactory):
3737-
3738- layer = DatabaseFunctionalLayer
3739-
3740- def make_product_user(self, licenses):
3741- # Setup an a view that implements ProductLicenseMixin.
3742- super(LicenseNotificationTestCase, self).setUp()
3743- user = self.factory.makePerson(
3744- name='registrant', email='registrant@launchpad.dev')
3745- login_person(user)
3746- product = self.factory.makeProduct(
3747- name='ball', owner=user, licenses=licenses)
3748- pop_notifications()
3749- return product, user
3750-
3751- def verify_whiteboard(self, product):
3752- # Verify that the review whiteboard was updated.
3753- naked_product = removeSecurityProxy(product)
3754- entries = naked_product.reviewer_whiteboard.split('\n')
3755- whiteboard, stamp = entries[-1].rsplit(' ', 1)
3756- self.assertEqual(
3757- 'User notified of license policy on', whiteboard)
3758-
3759- def verify_user_email(self, notification):
3760- # Verify that the user was sent an email about the license change.
3761- self.assertEqual(
3762- 'License information for ball in Launchpad',
3763- notification['Subject'])
3764- self.assertEqual(
3765- 'Registrant <registrant@launchpad.dev>',
3766- notification['To'])
3767- self.assertEqual(
3768- 'Commercial <commercial@launchpad.net>',
3769- notification['Reply-To'])
3770-
3771- def test_send_known_license(self):
3772- # A known license does not generate an email.
3773- product, user = self.make_product_user([License.GNU_GPL_V2])
3774- notification = LicenseNotification(product, user)
3775- result = notification.send()
3776- self.assertIs(False, result)
3777- self.assertEqual(0, len(pop_notifications()))
3778-
3779- def test_send_other_dont_know(self):
3780- # An Other/I don't know license sends one email.
3781- product, user = self.make_product_user([License.DONT_KNOW])
3782- notification = LicenseNotification(product, user)
3783- result = notification.send()
3784- self.assertIs(True, result)
3785- self.verify_whiteboard(product)
3786- notifications = pop_notifications()
3787- self.assertEqual(1, len(notifications))
3788- self.verify_user_email(notifications.pop())
3789-
3790- def test_send_other_open_source(self):
3791- # An Other/Open Source license sends one email.
3792- product, user = self.make_product_user([License.OTHER_OPEN_SOURCE])
3793- notification = LicenseNotification(product, user)
3794- result = notification.send()
3795- self.assertIs(True, result)
3796- self.verify_whiteboard(product)
3797- notifications = pop_notifications()
3798- self.assertEqual(1, len(notifications))
3799- self.verify_user_email(notifications.pop())
3800-
3801- def test_send_other_proprietary(self):
3802- # An Other/Proprietary license sends one email.
3803- product, user = self.make_product_user([License.OTHER_PROPRIETARY])
3804- notification = LicenseNotification(product, user)
3805- result = notification.send()
3806- self.assertIs(True, result)
3807- self.verify_whiteboard(product)
3808- notifications = pop_notifications()
3809- self.assertEqual(1, len(notifications))
3810- self.verify_user_email(notifications.pop())
3811-
3812- def test_display_no_request(self):
3813- # If there is no request, there is no reason to show a message in
3814- # the browser.
3815- product, user = self.make_product_user([License.GNU_GPL_V2])
3816- notification = LicenseNotification(product, user)
3817- logout()
3818- result = notification.display()
3819- self.assertIs(False, result)
3820-
3821- def test_display_no_message(self):
3822- # A notification is not added if there is no message to show.
3823- product, user = self.make_product_user([License.GNU_GPL_V2])
3824- notification = LicenseNotification(product, user)
3825- result = notification.display()
3826- self.assertEqual('', notification.getCommercialUseMessage())
3827- self.assertIs(False, result)
3828-
3829- def test_display_has_message(self):
3830- # A notification is added if there is a message to show.
3831- product, user = self.make_product_user([License.OTHER_PROPRIETARY])
3832- notification = LicenseNotification(product, user)
3833- result = notification.display()
3834- message = notification.getCommercialUseMessage()
3835- self.assertIs(True, result)
3836- request = get_current_browser_request()
3837- self.assertEqual(1, len(request.response.notifications))
3838- self.assertIn(message, request.response.notifications[0].message)
3839- self.assertIn(
3840- '<a href="https://help.launchpad.net/CommercialHosting">',
3841- request.response.notifications[0].message)
3842-
3843- def test_display_escapee_user_data(self):
3844- # A notification is added if there is a message to show.
3845- product, user = self.make_product_user([License.OTHER_PROPRIETARY])
3846- product.displayname = '<b>Look</b>'
3847- notification = LicenseNotification(product, user)
3848- result = notification.display()
3849- self.assertIs(True, result)
3850- request = get_current_browser_request()
3851- self.assertEqual(1, len(request.response.notifications))
3852- self.assertIn(
3853- '&lt;b&gt;Look&lt;/b&gt;',
3854- request.response.notifications[0].message)
3855-
3856- def test_formatDate(self):
3857- # Verify the date format.
3858- now = datetime(2005, 6, 15, 0, 0, 0, 0, pytz.UTC)
3859- result = LicenseNotification._formatDate(now)
3860- self.assertEqual('2005-06-15', result)
3861-
3862- def test_getTemplateName_other_dont_know(self):
3863- product, user = self.make_product_user([License.DONT_KNOW])
3864- notification = LicenseNotification(product, user)
3865- self.assertEqual(
3866- 'product-license-dont-know.txt',
3867- notification.getTemplateName())
3868-
3869- def test_getTemplateName_propietary(self):
3870- product, user = self.make_product_user([License.OTHER_PROPRIETARY])
3871- notification = LicenseNotification(product, user)
3872- self.assertEqual(
3873- 'product-license-other-proprietary.txt',
3874- notification.getTemplateName())
3875-
3876- def test_getTemplateName_other_open_source(self):
3877- product, user = self.make_product_user([License.OTHER_OPEN_SOURCE])
3878- notification = LicenseNotification(product, user)
3879- self.assertEqual(
3880- 'product-license-other-open-source.txt',
3881- notification.getTemplateName())
3882-
3883- def test_getCommercialUseMessage_without_commercial_subscription(self):
3884- product, user = self.make_product_user([License.MIT])
3885- notification = LicenseNotification(product, user)
3886- self.assertEqual('', notification.getCommercialUseMessage())
3887-
3888- def test_getCommercialUseMessage_with_complimentary_cs(self):
3889- product, user = self.make_product_user([License.OTHER_PROPRIETARY])
3890- notification = LicenseNotification(product, user)
3891- message = (
3892- "Ball's complimentary commercial subscription expires on %s." %
3893- product.commercial_subscription.date_expires.date().isoformat())
3894- self.assertEqual(message, notification.getCommercialUseMessage())
3895-
3896- def test_getCommercialUseMessage_with_commercial_subscription(self):
3897- product, user = self.make_product_user([License.MIT])
3898- self.factory.makeCommercialSubscription(product)
3899- product.licenses = [License.MIT, License.OTHER_PROPRIETARY]
3900- notification = LicenseNotification(product, user)
3901- message = (
3902- "Ball's commercial subscription expires on %s." %
3903- product.commercial_subscription.date_expires.date().isoformat())
3904- self.assertEqual(message, notification.getCommercialUseMessage())
3905-
3906- def test_getCommercialUseMessage_with_expired_cs(self):
3907- product, user = self.make_product_user([License.MIT])
3908- self.factory.makeCommercialSubscription(product, expired=True)
3909- product.licenses = [License.MIT, License.OTHER_PROPRIETARY]
3910- notification = LicenseNotification(product, user)
3911- message = (
3912- "Ball's commercial subscription expired on %s." %
3913- product.commercial_subscription.date_expires.date().isoformat())
3914- self.assertEqual(message, notification.getCommercialUseMessage())
3915- self.assertEqual(message, notification.getCommercialUseMessage())
3916->>>>>>> MERGE-SOURCE
3917+# Copyright 2012 Canonical Ltd. This software is licensed under the
3918+# GNU Affero General Public License version 3 (see the file LICENSE).
3919+
3920+"""Test subscruber classes and functions."""
3921+
3922+__metaclass__ = type
3923+
3924+from datetime import datetime
3925+
3926+from lazr.lifecycle.event import ObjectModifiedEvent
3927+import pytz
3928+from zope.security.proxy import removeSecurityProxy
3929+
3930+from lp.registry.interfaces.product import License
3931+from lp.registry.subscribers import (
3932+ LicenseNotification,
3933+ product_licenses_modified,
3934+ )
3935+from lp.services.webapp.publisher import get_current_browser_request
3936+from lp.testing import (
3937+ login_person,
3938+ logout,
3939+ TestCaseWithFactory,
3940+ )
3941+from lp.testing.layers import DatabaseFunctionalLayer
3942+from lp.testing.mail_helpers import pop_notifications
3943+
3944+
3945+class ProductLicensesModifiedTestCase(TestCaseWithFactory):
3946+
3947+ layer = DatabaseFunctionalLayer
3948+
3949+ def make_product_event(self, licenses, edited_fields='licenses'):
3950+ product = self.factory.makeProduct(licenses=licenses)
3951+ pop_notifications()
3952+ login_person(product.owner)
3953+ event = ObjectModifiedEvent(
3954+ product, product, edited_fields, user=product.owner)
3955+ return product, event
3956+
3957+ def test_product_licenses_modified_licenses_not_edited(self):
3958+ product, event = self.make_product_event(
3959+ [License.OTHER_PROPRIETARY], edited_fields='_owner')
3960+ product_licenses_modified(product, event)
3961+ notifications = pop_notifications()
3962+ self.assertEqual(0, len(notifications))
3963+
3964+ def test_product_licenses_modified_licenses_common_license(self):
3965+ product, event = self.make_product_event([License.MIT])
3966+ product_licenses_modified(product, event)
3967+ notifications = pop_notifications()
3968+ self.assertEqual(0, len(notifications))
3969+ request = get_current_browser_request()
3970+ self.assertEqual(0, len(request.response.notifications))
3971+
3972+ def test_product_licenses_modified_licenses_other_proprietary(self):
3973+ product, event = self.make_product_event([License.OTHER_PROPRIETARY])
3974+ product_licenses_modified(product, event)
3975+ notifications = pop_notifications()
3976+ self.assertEqual(1, len(notifications))
3977+ request = get_current_browser_request()
3978+ self.assertEqual(1, len(request.response.notifications))
3979+
3980+ def test_product_licenses_modified_licenses_other_open_source(self):
3981+ product, event = self.make_product_event([License.OTHER_OPEN_SOURCE])
3982+ product_licenses_modified(product, event)
3983+ notifications = pop_notifications()
3984+ self.assertEqual(1, len(notifications))
3985+ request = get_current_browser_request()
3986+ self.assertEqual(0, len(request.response.notifications))
3987+
3988+ def test_product_licenses_modified_licenses_other_dont_know(self):
3989+ product, event = self.make_product_event([License.DONT_KNOW])
3990+ product_licenses_modified(product, event)
3991+ notifications = pop_notifications()
3992+ self.assertEqual(1, len(notifications))
3993+ request = get_current_browser_request()
3994+ self.assertEqual(0, len(request.response.notifications))
3995+
3996+
3997+class LicenseNotificationTestCase(TestCaseWithFactory):
3998+
3999+ layer = DatabaseFunctionalLayer
4000+
4001+ def make_product_user(self, licenses):
4002+ # Setup an a view that implements ProductLicenseMixin.
4003+ super(LicenseNotificationTestCase, self).setUp()
4004+ user = self.factory.makePerson(
4005+ name='registrant', email='registrant@launchpad.dev')
4006+ login_person(user)
4007+ product = self.factory.makeProduct(
4008+ name='ball', owner=user, licenses=licenses)
4009+ pop_notifications()
4010+ return product, user
4011+
4012+ def verify_whiteboard(self, product):
4013+ # Verify that the review whiteboard was updated.
4014+ naked_product = removeSecurityProxy(product)
4015+ entries = naked_product.reviewer_whiteboard.split('\n')
4016+ whiteboard, stamp = entries[-1].rsplit(' ', 1)
4017+ self.assertEqual(
4018+ 'User notified of license policy on', whiteboard)
4019+
4020+ def verify_user_email(self, notification):
4021+ # Verify that the user was sent an email about the license change.
4022+ self.assertEqual(
4023+ 'License information for ball in Launchpad',
4024+ notification['Subject'])
4025+ self.assertEqual(
4026+ 'Registrant <registrant@launchpad.dev>',
4027+ notification['To'])
4028+ self.assertEqual(
4029+ 'Commercial <commercial@launchpad.net>',
4030+ notification['Reply-To'])
4031+
4032+ def test_send_known_license(self):
4033+ # A known license does not generate an email.
4034+ product, user = self.make_product_user([License.GNU_GPL_V2])
4035+ notification = LicenseNotification(product, user)
4036+ result = notification.send()
4037+ self.assertIs(False, result)
4038+ self.assertEqual(0, len(pop_notifications()))
4039+
4040+ def test_send_other_dont_know(self):
4041+ # An Other/I don't know license sends one email.
4042+ product, user = self.make_product_user([License.DONT_KNOW])
4043+ notification = LicenseNotification(product, user)
4044+ result = notification.send()
4045+ self.assertIs(True, result)
4046+ self.verify_whiteboard(product)
4047+ notifications = pop_notifications()
4048+ self.assertEqual(1, len(notifications))
4049+ self.verify_user_email(notifications.pop())
4050+
4051+ def test_send_other_open_source(self):
4052+ # An Other/Open Source license sends one email.
4053+ product, user = self.make_product_user([License.OTHER_OPEN_SOURCE])
4054+ notification = LicenseNotification(product, user)
4055+ result = notification.send()
4056+ self.assertIs(True, result)
4057+ self.verify_whiteboard(product)
4058+ notifications = pop_notifications()
4059+ self.assertEqual(1, len(notifications))
4060+ self.verify_user_email(notifications.pop())
4061+
4062+ def test_send_other_proprietary(self):
4063+ # An Other/Proprietary license sends one email.
4064+ product, user = self.make_product_user([License.OTHER_PROPRIETARY])
4065+ notification = LicenseNotification(product, user)
4066+ result = notification.send()
4067+ self.assertIs(True, result)
4068+ self.verify_whiteboard(product)
4069+ notifications = pop_notifications()
4070+ self.assertEqual(1, len(notifications))
4071+ self.verify_user_email(notifications.pop())
4072+
4073+ def test_display_no_request(self):
4074+ # If there is no request, there is no reason to show a message in
4075+ # the browser.
4076+ product, user = self.make_product_user([License.GNU_GPL_V2])
4077+ notification = LicenseNotification(product, user)
4078+ logout()
4079+ result = notification.display()
4080+ self.assertIs(False, result)
4081+
4082+ def test_display_no_message(self):
4083+ # A notification is not added if there is no message to show.
4084+ product, user = self.make_product_user([License.GNU_GPL_V2])
4085+ notification = LicenseNotification(product, user)
4086+ result = notification.display()
4087+ self.assertEqual('', notification.getCommercialUseMessage())
4088+ self.assertIs(False, result)
4089+
4090+ def test_display_has_message(self):
4091+ # A notification is added if there is a message to show.
4092+ product, user = self.make_product_user([License.OTHER_PROPRIETARY])
4093+ notification = LicenseNotification(product, user)
4094+ result = notification.display()
4095+ message = notification.getCommercialUseMessage()
4096+ self.assertIs(True, result)
4097+ request = get_current_browser_request()
4098+ self.assertEqual(1, len(request.response.notifications))
4099+ self.assertIn(message, request.response.notifications[0].message)
4100+ self.assertIn(
4101+ '<a href="https://help.launchpad.net/CommercialHosting">',
4102+ request.response.notifications[0].message)
4103+
4104+ def test_display_escapee_user_data(self):
4105+ # A notification is added if there is a message to show.
4106+ product, user = self.make_product_user([License.OTHER_PROPRIETARY])
4107+ product.displayname = '<b>Look</b>'
4108+ notification = LicenseNotification(product, user)
4109+ result = notification.display()
4110+ self.assertIs(True, result)
4111+ request = get_current_browser_request()
4112+ self.assertEqual(1, len(request.response.notifications))
4113+ self.assertIn(
4114+ '&lt;b&gt;Look&lt;/b&gt;',
4115+ request.response.notifications[0].message)
4116+
4117+ def test_formatDate(self):
4118+ # Verify the date format.
4119+ now = datetime(2005, 6, 15, 0, 0, 0, 0, pytz.UTC)
4120+ result = LicenseNotification._formatDate(now)
4121+ self.assertEqual('2005-06-15', result)
4122+
4123+ def test_getTemplateName_other_dont_know(self):
4124+ product, user = self.make_product_user([License.DONT_KNOW])
4125+ notification = LicenseNotification(product, user)
4126+ self.assertEqual(
4127+ 'product-license-dont-know.txt',
4128+ notification.getTemplateName())
4129+
4130+ def test_getTemplateName_propietary(self):
4131+ product, user = self.make_product_user([License.OTHER_PROPRIETARY])
4132+ notification = LicenseNotification(product, user)
4133+ self.assertEqual(
4134+ 'product-license-other-proprietary.txt',
4135+ notification.getTemplateName())
4136+
4137+ def test_getTemplateName_other_open_source(self):
4138+ product, user = self.make_product_user([License.OTHER_OPEN_SOURCE])
4139+ notification = LicenseNotification(product, user)
4140+ self.assertEqual(
4141+ 'product-license-other-open-source.txt',
4142+ notification.getTemplateName())
4143+
4144+ def test_getCommercialUseMessage_without_commercial_subscription(self):
4145+ product, user = self.make_product_user([License.MIT])
4146+ notification = LicenseNotification(product, user)
4147+ self.assertEqual('', notification.getCommercialUseMessage())
4148+
4149+ def test_getCommercialUseMessage_with_complimentary_cs(self):
4150+ product, user = self.make_product_user([License.OTHER_PROPRIETARY])
4151+ notification = LicenseNotification(product, user)
4152+ message = (
4153+ "Ball's complimentary commercial subscription expires on %s." %
4154+ product.commercial_subscription.date_expires.date().isoformat())
4155+ self.assertEqual(message, notification.getCommercialUseMessage())
4156+
4157+ def test_getCommercialUseMessage_with_commercial_subscription(self):
4158+ product, user = self.make_product_user([License.MIT])
4159+ self.factory.makeCommercialSubscription(product)
4160+ product.licenses = [License.MIT, License.OTHER_PROPRIETARY]
4161+ notification = LicenseNotification(product, user)
4162+ message = (
4163+ "Ball's commercial subscription expires on %s." %
4164+ product.commercial_subscription.date_expires.date().isoformat())
4165+ self.assertEqual(message, notification.getCommercialUseMessage())
4166+
4167+ def test_getCommercialUseMessage_with_expired_cs(self):
4168+ product, user = self.make_product_user([License.MIT])
4169+ self.factory.makeCommercialSubscription(product, expired=True)
4170+ product.licenses = [License.MIT, License.OTHER_PROPRIETARY]
4171+ notification = LicenseNotification(product, user)
4172+ message = (
4173+ "Ball's commercial subscription expired on %s." %
4174+ product.commercial_subscription.date_expires.date().isoformat())
4175+ self.assertEqual(message, notification.getCommercialUseMessage())
4176+ self.assertEqual(message, notification.getCommercialUseMessage())
4177
4178=== modified file 'lib/lp/scripts/garbo.py'
4179--- lib/lp/scripts/garbo.py 2012-03-29 14:36:36 +0000
4180+++ lib/lp/scripts/garbo.py 2012-03-26 22:47:47 +0000
4181@@ -81,7 +81,6 @@
4182 from lp.services.librarian.model import TimeLimitedToken
4183 from lp.services.log.logger import PrefixFilter
4184 from lp.services.looptuner import TunableLoop
4185-from lp.services.memcache.interfaces import IMemcacheClient
4186 from lp.services.oauth.model import OAuthNonce
4187 from lp.services.openid.model.openidconsumer import OpenIDConsumerNonce
4188 from lp.services.propertycache import cachedproperty
4189@@ -1092,43 +1091,6 @@
4190 self.offset += chunk_size
4191
4192
4193-<<<<<<< TREE
4194-=======
4195-class BugLegacyAccessMirrorer(TunableLoop):
4196- """A `TunableLoop` to populate the access policy schema for all bugs."""
4197-
4198- maximum_chunk_size = 5000
4199-
4200- def __init__(self, log, abort_time=None):
4201- super(BugLegacyAccessMirrorer, self).__init__(log, abort_time)
4202- watermark = getUtility(IMemcacheClient).get(
4203- '%s:bug-legacy-access-mirrorer' % config.instance_name)
4204- self.start_at = watermark or 0
4205-
4206- def findBugIDs(self):
4207- return IMasterStore(Bug).find(
4208- (Bug.id,), Bug.id >= self.start_at).order_by(Bug.id)
4209-
4210- def isDone(self):
4211- return self.findBugIDs().is_empty()
4212-
4213- def __call__(self, chunk_size):
4214- ids = [row[0] for row in self.findBugIDs()[:chunk_size]]
4215- list(IMasterStore(Bug).using(Bug).find(
4216- SQL('bug_mirror_legacy_access(Bug.id)'),
4217- Bug.id.is_in(ids)))
4218-
4219- self.start_at = ids[-1] + 1
4220- result = getUtility(IMemcacheClient).set(
4221- '%s:bug-legacy-access-mirrorer' % config.instance_name,
4222- self.start_at)
4223- if not result:
4224- self.log.warning('Failed to set start_at in memcache.')
4225-
4226- transaction.commit()
4227-
4228-
4229->>>>>>> MERGE-SOURCE
4230 class BaseDatabaseGarbageCollector(LaunchpadCronScript):
4231 """Abstract base class to run a collection of TunableLoops."""
4232 script_name = None # Script name for locking and database user. Override.
4233@@ -1380,10 +1342,6 @@
4234 UnusedSessionPruner,
4235 DuplicateSessionPruner,
4236 BugHeatUpdater,
4237-<<<<<<< TREE
4238-=======
4239- BugLegacyAccessMirrorer,
4240->>>>>>> MERGE-SOURCE
4241 ]
4242 experimental_tunable_loops = []
4243
4244
4245=== modified file 'lib/lp/scripts/tests/test_garbo.py'
4246--- lib/lp/scripts/tests/test_garbo.py 2012-03-29 14:36:36 +0000
4247+++ lib/lp/scripts/tests/test_garbo.py 2012-03-26 22:47:47 +0000
4248@@ -54,10 +54,6 @@
4249 )
4250 from lp.code.model.codeimportevent import CodeImportEvent
4251 from lp.code.model.codeimportresult import CodeImportResult
4252-<<<<<<< TREE
4253-=======
4254-from lp.registry.interfaces.accesspolicy import IAccessArtifactSource
4255->>>>>>> MERGE-SOURCE
4256 from lp.registry.interfaces.distribution import IDistributionSet
4257 from lp.registry.interfaces.person import IPersonSet
4258 from lp.scripts.garbo import (
4259@@ -1108,29 +1104,6 @@
4260 self.assertEqual(whiteboard, spec.whiteboard)
4261 self.assertEqual(0, spec.work_items.count())
4262
4263-<<<<<<< TREE
4264-=======
4265- def test_BugLegacyAccessMirrorer(self):
4266- # Private bugs without corresponding data in the access policy
4267- # schema get mirrored.
4268- switch_dbuser('testadmin')
4269- bug = self.factory.makeBug(private=True)
4270- # Remove the existing mirrored data.
4271- getUtility(IAccessArtifactSource).delete([bug])
4272- transaction.commit()
4273- self.runHourly()
4274- # Check that there's an artifact again, and delete it.
4275- switch_dbuser('testadmin')
4276- [artifact] = getUtility(IAccessArtifactSource).find([bug])
4277- getUtility(IAccessArtifactSource).delete([bug])
4278- transaction.commit()
4279- self.runHourly()
4280- # A watermark is kept in memcache, so a second run doesn't
4281- # consider the same bug.
4282- self.assertContentEqual(
4283- [], getUtility(IAccessArtifactSource).find([bug]))
4284-
4285->>>>>>> MERGE-SOURCE
4286
4287 class TestGarboTasks(TestCaseWithFactory):
4288 layer = LaunchpadZopelessLayer
4289
4290=== modified file 'lib/lp/services/features/flags.py'
4291--- lib/lp/services/features/flags.py 2012-03-29 14:36:36 +0000
4292+++ lib/lp/services/features/flags.py 2012-03-26 21:23:40 +0000
4293@@ -290,7 +290,6 @@
4294 ('disclosure.enhanced_sharing.enabled',
4295 'boolean',
4296 ('If true, will allow the use of the new sharing view and apis used '
4297-<<<<<<< TREE
4298 'for the new disclosure data model to view but not write data.'),
4299 '',
4300 'Sharing overview',
4301@@ -308,18 +307,6 @@
4302 'to edit the new disclosure data model.'),
4303 '',
4304 'Sharing management',
4305-=======
4306- 'for the new disclosure data model to view but not write data.'),
4307- '',
4308- 'Sharing overview',
4309- ''),
4310- ('disclosure.enhanced_sharing.writable',
4311- 'boolean',
4312- ('If true, will allow the use of the new sharing view and apis used '
4313- 'to edit the new disclosure data model.'),
4314- '',
4315- 'Sharing management',
4316->>>>>>> MERGE-SOURCE
4317 ''),
4318 ('garbo.workitem_migrator.enabled',
4319 'boolean',
4320
4321=== added file 'lib/lp/services/job/celeryconfig.py'
4322--- lib/lp/services/job/celeryconfig.py 1970-01-01 00:00:00 +0000
4323+++ lib/lp/services/job/celeryconfig.py 2012-03-29 14:36:38 +0000
4324@@ -0,0 +1,4 @@
4325+from lp.services.config import config
4326+BROKER_URL = "amqplib://%s" % config.rabbitmq.host
4327+CELERY_IMPORTS = ("lp.services.job.celeryjob", )
4328+CELERY_RESULT_BACKEND = "amqp"
4329
4330=== added file 'lib/lp/services/job/celeryjob.py'
4331--- lib/lp/services/job/celeryjob.py 1970-01-01 00:00:00 +0000
4332+++ lib/lp/services/job/celeryjob.py 2012-03-29 14:36:38 +0000
4333@@ -0,0 +1,30 @@
4334+# Copyright 2012 Canonical Ltd. This software is licensed under the
4335+# GNU Affero General Public License version 3 (see the file LICENSE).
4336+
4337+"""Celery-specific Job code.
4338+
4339+Because celery sets up configuration at import time, code that is not designed
4340+to use Celery may break if this is used.
4341+"""
4342+
4343+__metaclass__ = type
4344+
4345+__all__ = ['CeleryRunJob']
4346+
4347+import os
4348+
4349+os.environ.setdefault('CELERY_CONFIG_MODULE', 'lp.services.job.celeryconfig')
4350+from lazr.jobrunner.celerytask import RunJob
4351+
4352+from lp.services.job.model.job import UniversalJobSource
4353+from lp.services.job.runner import BaseJobRunner
4354+
4355+
4356+class CeleryRunJob(RunJob):
4357+ """The Celery Task that runs a job."""
4358+
4359+ job_source = UniversalJobSource
4360+
4361+ def getJobRunner(self):
4362+ """Return a BaseJobRunner, to support customization."""
4363+ return BaseJobRunner()
4364
4365=== modified file 'lib/lp/services/job/interfaces/job.py'
4366--- lib/lp/services/job/interfaces/job.py 2012-02-24 21:46:11 +0000
4367+++ lib/lp/services/job/interfaces/job.py 2012-03-29 14:36:38 +0000
4368@@ -13,7 +13,6 @@
4369 'IRunnableJob',
4370 'ITwistedJobSource',
4371 'JobStatus',
4372- 'LeaseHeld',
4373 ]
4374
4375
4376@@ -38,13 +37,6 @@
4377 from lp.registry.interfaces.person import IPerson
4378
4379
4380-class LeaseHeld(Exception):
4381- """Raised when attempting to acquire a list that is already held."""
4382-
4383- def __init__(self):
4384- Exception.__init__(self, 'Lease is already held.')
4385-
4386-
4387 class JobStatus(DBEnumeratedType):
4388 """Values that ICodeImportJob.state can take."""
4389
4390
4391=== modified file 'lib/lp/services/job/model/job.py'
4392--- lib/lp/services/job/model/job.py 2012-03-14 16:32:59 +0000
4393+++ lib/lp/services/job/model/job.py 2012-03-29 14:36:38 +0000
4394@@ -4,13 +4,21 @@
4395 """ORM object representing jobs."""
4396
4397 __metaclass__ = type
4398-__all__ = ['InvalidTransition', 'Job', 'JobStatus']
4399+__all__ = [
4400+ 'EnumeratedSubclass',
4401+ 'InvalidTransition',
4402+ 'Job',
4403+ 'JobStatus',
4404+ 'UniversalJobSource',
4405+ ]
4406
4407
4408 from calendar import timegm
4409 import datetime
4410 import time
4411
4412+from lazr.jobrunner.jobrunner import LeaseHeld
4413+
4414 import pytz
4415 from sqlobject import (
4416 IntCol,
4417@@ -28,16 +36,18 @@
4418 import transaction
4419 from zope.interface import implements
4420
4421+from lp.services.config import dbconfig
4422 from lp.services.database import bulk
4423 from lp.services.database.constants import UTC_NOW
4424 from lp.services.database.datetimecol import UtcDateTimeCol
4425 from lp.services.database.enumcol import EnumCol
4426+from lp.services.database.lpstorm import IStore
4427 from lp.services.database.sqlbase import SQLBase
4428 from lp.services.job.interfaces.job import (
4429 IJob,
4430 JobStatus,
4431- LeaseHeld,
4432 )
4433+from lp.services import scripts
4434
4435
4436 UTC = pytz.timezone('UTC')
4437@@ -201,6 +211,29 @@
4438 self.lease_expires = None
4439
4440
4441+class EnumeratedSubclass(type):
4442+ """Metaclass for when subclasses are assigned enums."""
4443+
4444+ def __init__(cls, name, bases, dict_):
4445+ if getattr(cls, '_subclass', None) is None:
4446+ cls._subclass = {}
4447+ job_type = dict_.get('class_job_type')
4448+ if job_type is not None:
4449+ value = cls._subclass.setdefault(job_type, cls)
4450+ assert value is cls, (
4451+ '%s already registered to %s.' % (
4452+ job_type.name, value.__name__))
4453+ # Perform any additional set-up requested by class.
4454+ cls._register_subclass(cls)
4455+
4456+ @staticmethod
4457+ def _register_subclass(cls):
4458+ pass
4459+
4460+ def makeSubclass(cls, job):
4461+ return cls._subclass[job.job_type](job)
4462+
4463+
4464 Job.ready_jobs = Select(
4465 Job.id,
4466 And(
4467@@ -208,3 +241,29 @@
4468 Or(Job.lease_expires == None, Job.lease_expires < UTC_NOW),
4469 Or(Job.scheduled_start == None, Job.scheduled_start <= UTC_NOW),
4470 ))
4471+
4472+
4473+class UniversalJobSource:
4474+ """Returns the RunnableJob associated with a Job.id.
4475+
4476+ Only BranchJobs are supported at present.
4477+ """
4478+
4479+ memory_limit = 2 * (1024 ** 3)
4480+
4481+ needs_init = True
4482+
4483+ @classmethod
4484+ def get(cls, job_id):
4485+ if cls.needs_init:
4486+ scripts.execute_zcml_for_scripts(use_web_security=False)
4487+ cls.needs_init = False
4488+
4489+ dbconfig.override(
4490+ dbuser='branchscanner', isolation_level='read_committed')
4491+ from lp.code.model.branchjob import (
4492+ BranchJob,
4493+ )
4494+ store = IStore(BranchJob)
4495+ branch_job = store.find(BranchJob, BranchJob.job == job_id).one()
4496+ return branch_job.makeDerived()
4497
4498=== modified file 'lib/lp/services/job/runner.py'
4499--- lib/lp/services/job/runner.py 2012-03-22 11:58:42 +0000
4500+++ lib/lp/services/job/runner.py 2012-03-29 14:36:38 +0000
4501@@ -6,6 +6,7 @@
4502 __metaclass__ = type
4503
4504 __all__ = [
4505+ 'BaseJobRunner',
4506 'BaseRunnableJob',
4507 'BaseRunnableJobSource',
4508 'JobCronScript',
4509@@ -38,7 +39,10 @@
4510 pool,
4511 )
4512 from lazr.delegates import delegates
4513-from lazr.jobrunner.jobrunner import JobRunner as LazrJobRunner
4514+from lazr.jobrunner.jobrunner import (
4515+ JobRunner as LazrJobRunner,
4516+ LeaseHeld,
4517+ )
4518 import transaction
4519 from twisted.internet import reactor
4520 from twisted.internet.defer import (
4521@@ -61,7 +65,6 @@
4522 from lp.services.job.interfaces.job import (
4523 IJob,
4524 IRunnableJob,
4525- LeaseHeld,
4526 )
4527 from lp.services.mail.sendmail import (
4528 MailController,
4529
4530=== added file 'lib/lp/services/job/tests/celeryconfig.py'
4531--- lib/lp/services/job/tests/celeryconfig.py 1970-01-01 00:00:00 +0000
4532+++ lib/lp/services/job/tests/celeryconfig.py 2012-03-29 14:36:38 +0000
4533@@ -0,0 +1,7 @@
4534+import os
4535+BROKER_VHOST = "/"
4536+CELERY_RESULT_BACKEND = "amqp"
4537+CELERY_IMPORTS = ("lp.services.job.celeryjob", )
4538+CELERYD_LOG_LEVEL = 'INFO'
4539+CELERYD_CONCURRENCY = 1
4540+BROKER_URL = os.environ['BROKER_URL']
4541
4542=== added file 'lib/lp/services/job/tests/test_celeryjob.py'
4543--- lib/lp/services/job/tests/test_celeryjob.py 1970-01-01 00:00:00 +0000
4544+++ lib/lp/services/job/tests/test_celeryjob.py 2012-03-29 14:36:38 +0000
4545@@ -0,0 +1,42 @@
4546+# Copyright 2009 Canonical Ltd. This software is licensed under the
4547+# GNU Affero General Public License version 3 (see the file LICENSE).
4548+
4549+
4550+import os
4551+
4552+import transaction
4553+
4554+from lp.code.model.branchjob import BranchScanJob
4555+from lp.testing import TestCaseWithFactory
4556+from lp.testing.layers import ZopelessAppServerLayer
4557+
4558+
4559+class TestCelery(TestCaseWithFactory):
4560+
4561+ layer = ZopelessAppServerLayer
4562+
4563+ def test_run_scan_job(self):
4564+ """Running a job via Celery succeeds and emits expected output."""
4565+ # Delay importing anything that uses Celery until RabbitMQLayer is
4566+ # running, so that config.rabbitmq.host is defined when
4567+ # lp.services.job.celeryconfig is loaded.
4568+ from lp.services.job.celeryjob import CeleryRunJob
4569+ from celery.exceptions import TimeoutError
4570+ from lazr.jobrunner.tests.test_celerytask import running
4571+ cmd_args = ('--config', 'lp.services.job.tests.celeryconfig')
4572+ env = dict(os.environ)
4573+ env['BROKER_URL'] = CeleryRunJob.app.conf['BROKER_URL']
4574+ with running('bin/celeryd', cmd_args, env=env) as proc:
4575+ self.useBzrBranches()
4576+ db_branch, bzr_tree = self.create_branch_and_tree()
4577+ bzr_tree.commit(
4578+ 'First commit', rev_id='rev1', committer='me@example.org')
4579+ job = BranchScanJob.create(db_branch)
4580+ transaction.commit()
4581+ try:
4582+ CeleryRunJob.delay(job.job_id).wait(30)
4583+ except TimeoutError:
4584+ pass
4585+ self.assertIn(
4586+ 'Updating branch scanner status: 1 revs', proc.stderr.read())
4587+ self.assertEqual(db_branch.revision_count, 1)
4588
4589=== modified file 'lib/lp/services/job/tests/test_job.py'
4590--- lib/lp/services/job/tests/test_job.py 2011-12-30 06:14:56 +0000
4591+++ lib/lp/services/job/tests/test_job.py 2012-03-29 14:36:38 +0000
4592@@ -7,6 +7,7 @@
4593 import time
4594
4595 import pytz
4596+from lazr.jobrunner.jobrunner import LeaseHeld
4597 from storm.locals import Store
4598
4599 from lp.services.database.constants import UTC_NOW
4600@@ -18,7 +19,6 @@
4601 from lp.services.job.model.job import (
4602 InvalidTransition,
4603 Job,
4604- LeaseHeld,
4605 )
4606 from lp.services.webapp.testing import verifyObject
4607 from lp.testing import (
4608
4609=== modified file 'lib/lp/services/job/tests/test_runner.py'
4610--- lib/lp/services/job/tests/test_runner.py 2012-03-29 14:36:36 +0000
4611+++ lib/lp/services/job/tests/test_runner.py 2012-03-29 14:36:38 +0000
4612@@ -8,7 +8,10 @@
4613 from textwrap import dedent
4614 from time import sleep
4615
4616-from lazr.jobrunner.jobrunner import SuspendJobException
4617+from lazr.jobrunner.jobrunner import (
4618+ LeaseHeld,
4619+ SuspendJobException,
4620+ )
4621 from testtools.matchers import MatchesRegex
4622 from testtools.testcase import ExpectedException
4623 import transaction
4624@@ -19,7 +22,6 @@
4625 from lp.services.job.interfaces.job import (
4626 IRunnableJob,
4627 JobStatus,
4628- LeaseHeld,
4629 )
4630 from lp.services.job.model.job import Job
4631 from lp.services.job.runner import (
4632@@ -554,20 +556,11 @@
4633 self.assertEqual(expected_exception, (oops['type'], oops['value']))
4634 self.assertThat(logger.getLogBuffer(), MatchesRegex(
4635 dedent("""\
4636-<<<<<<< TREE
4637 INFO Running through Twisted.
4638 INFO Running <StuckJob.*?> \(ID .*?\).
4639 INFO Running <StuckJob.*?> \(ID .*?\).
4640 INFO Job resulted in OOPS: .*
4641 """)))
4642-=======
4643- INFO Running through Twisted.
4644- INFO Running StuckJob \(ID .*\).
4645- INFO Running StuckJob \(ID .*\).
4646- ERROR OOPS while executing job \d+: \['OOPS-.*\] %s\('%s',\)
4647- INFO Job resulted in OOPS: .*
4648- """ % (expected_exception))))
4649->>>>>>> MERGE-SOURCE
4650
4651 def test_timeout_short(self):
4652 """When a job exceeds its lease, an exception is raised.
4653@@ -590,16 +583,10 @@
4654 logger.getLogBuffer(), MatchesRegex(
4655 dedent("""\
4656 INFO Running through Twisted.
4657-<<<<<<< TREE
4658- INFO Running <ShorterStuckJob.*?> \(ID .*?\).
4659- INFO Running <ShorterStuckJob.*?> \(ID .*?\).
4660-=======
4661- INFO Running ShorterStuckJob \(ID .*\).
4662- INFO Running ShorterStuckJob \(ID .*\).
4663- ERROR OOPS while executing job \d+: \['OOPS-.*\] %s\('%s',\)
4664->>>>>>> MERGE-SOURCE
4665+ INFO Running <ShorterStuckJob.*?> \(ID .*?\).
4666+ INFO Running <ShorterStuckJob.*?> \(ID .*?\).
4667 INFO Job resulted in OOPS: %s
4668- """) % ('TimeoutError', 'Job ran too long.', oops['id'])))
4669+ """) % oops['id']))
4670 self.assertEqual(('TimeoutError', 'Job ran too long.'),
4671 (oops['type'], oops['value']))
4672
4673
4674=== modified file 'lib/lp/testing/factory.py'
4675--- lib/lp/testing/factory.py 2012-03-29 14:36:36 +0000
4676+++ lib/lp/testing/factory.py 2012-03-29 14:36:38 +0000
4677@@ -1407,7 +1407,7 @@
4678 # We just remove the security proxies to be able to change the objects
4679 # here.
4680 removeSecurityProxy(branch).branchChanged(
4681- '', 'rev1', None, None, None)
4682+ '', 'rev1', None, None, None, celery_scan=False)
4683 naked_series = removeSecurityProxy(product.development_focus)
4684 naked_series.branch = branch
4685 return branch
4686@@ -1422,7 +1422,7 @@
4687 # We just remove the security proxies to be able to change the branch
4688 # here.
4689 removeSecurityProxy(branch).branchChanged(
4690- '', 'rev1', None, None, None)
4691+ '', 'rev1', None, None, None, celery_scan=False)
4692 with person_logged_in(package.distribution.owner):
4693 package.development_version.setBranch(
4694 PackagePublishingPocket.RELEASE, branch,
4695@@ -1628,7 +1628,7 @@
4696 if branch.branch_type not in (BranchType.REMOTE, BranchType.HOSTED):
4697 branch.startMirroring()
4698 removeSecurityProxy(branch).branchChanged(
4699- '', parent.revision_id, None, None, None)
4700+ '', parent.revision_id, None, None, None, celery_scan=False)
4701 branch.updateScannedDetails(parent, sequence)
4702
4703 def makeBranchRevision(self, branch, revision_id=None, sequence=None,
4704
4705=== modified file 'lib/lp/testing/yuixhr.py'
4706--- lib/lp/testing/yuixhr.py 2012-03-29 14:36:36 +0000
4707+++ lib/lp/testing/yuixhr.py 2012-03-23 18:07:23 +0000
4708@@ -235,7 +235,6 @@
4709 <!DOCTYPE html>
4710 <html>
4711 <head>
4712-<<<<<<< TREE
4713 <title>Test</title>
4714 %(javascript_block)s
4715 <script type="text/javascript">
4716@@ -253,45 +252,6 @@
4717 <link rel="stylesheet" href="/+icing/rev%(revno)s/combo.css"/>
4718 <script type="text/javascript" src="%(test_module)s"></script>
4719 </head>
4720-=======
4721- <title>Test</title>
4722- <script type="text/javascript"
4723- src="/+icing/rev%(revno)s/build/launchpad.js"></script>
4724- <script type="text/javascript">
4725- YUI.GlobalConfig = {
4726- fetchCSS: false,
4727- timeout: 50,
4728- ignore: [
4729- 'yui2-yahoo', 'yui2-event', 'yui2-dom',
4730- 'yui2-calendar','yui2-dom-event'
4731- ]
4732- }
4733- </script>
4734- <link rel="stylesheet"
4735- href="/+icing/yui/assets/skins/sam/skin.css"/>
4736- <link rel="stylesheet" href="/+icing/rev%(revno)s/combo.css"/>
4737- <style>
4738- /* Taken and customized from testlogger.css */
4739- .yui-console-entry-src { display:none; }
4740- .yui-console-entry.yui-console-entry-pass .yui-console-entry-cat {
4741- background-color: green;
4742- font-weight: bold;
4743- color: white;
4744- }
4745- .yui-console-entry.yui-console-entry-fail .yui-console-entry-cat {
4746- background-color: red;
4747- font-weight: bold;
4748- color: white;
4749- }
4750- .yui-console-entry.yui-console-entry-ignore .yui-console-entry-cat {
4751- background-color: #666;
4752- font-weight: bold;
4753- color: white;
4754- }
4755- </style>
4756- <script type="text/javascript" src="%(test_module)s"></script>
4757- </head>
4758->>>>>>> MERGE-SOURCE
4759 <body class="yui3-skin-sam">
4760 <div id="log"></div>
4761 <p>Want to re-run your test?</p>
4762
4763=== modified file 'lib/lp/translations/model/translationsharingjob.py'
4764--- lib/lp/translations/model/translationsharingjob.py 2011-12-30 06:14:56 +0000
4765+++ lib/lp/translations/model/translationsharingjob.py 2012-03-29 14:36:38 +0000
4766@@ -31,7 +31,10 @@
4767 IJob,
4768 JobStatus,
4769 )
4770-from lp.services.job.model.job import Job
4771+from lp.services.job.model.job import (
4772+ EnumeratedSubclass,
4773+ Job,
4774+ )
4775 from lp.translations.interfaces.translationsharingjob import (
4776 ITranslationSharingJob,
4777 )
4778@@ -108,21 +111,13 @@
4779 self.potemplate = potemplate
4780
4781
4782-class RegisteredSubclass(type):
4783- """Metaclass for when subclasses should be registered."""
4784-
4785- def __init__(cls, name, bases, dict_):
4786- cls._register_subclass(cls)
4787-
4788-
4789 class TranslationSharingJobDerived:
4790 """Base class for specialized TranslationTemplate Job types."""
4791
4792- __metaclass__ = RegisteredSubclass
4793+ __metaclass__ = EnumeratedSubclass
4794
4795 delegates(ITranslationSharingJob, 'job')
4796
4797- _subclass = {}
4798 _event_types = {}
4799
4800 @property
4801@@ -136,12 +131,6 @@
4802 # TranslationPackagingJob) need to be able to override it and call
4803 # into it, and there's no syntax to call a base class's version of a
4804 # classmethod with the subclass as the first parameter.
4805- job_type = getattr(cls, 'class_job_type', None)
4806- if job_type is not None:
4807- value = cls._subclass.setdefault(job_type, cls)
4808- assert value is cls, (
4809- '%s already registered to %s.' % (
4810- job_type.name, value.__name__))
4811 event_type = getattr(cls, 'create_on_event', None)
4812 if event_type is not None:
4813 cls._event_types.setdefault(event_type, []).append(cls)
4814@@ -215,7 +204,7 @@
4815 TranslationSharingJob.job == Job.id,
4816 Job.id.is_in(Job.ready_jobs),
4817 *extra_clauses)
4818- return (cls._subclass[job.job_type](job) for job in jobs)
4819+ return (cls.makeSubclass(job) for job in jobs)
4820
4821 @classmethod
4822 def getNextJobStatus(cls, packaging):
4823
4824=== modified file 'versions.cfg'
4825--- versions.cfg 2012-03-29 14:36:36 +0000
4826+++ versions.cfg 2012-03-27 17:29:47 +0000
4827@@ -42,11 +42,7 @@
4828 lazr.config = 1.1.3
4829 lazr.delegates = 1.2.0
4830 lazr.enum = 1.1.3
4831-<<<<<<< TREE
4832 lazr.jobrunner = 0.2
4833-=======
4834-lazr.jobrunner = 0.1
4835->>>>>>> MERGE-SOURCE
4836 lazr.lifecycle = 1.1
4837 lazr.restful = 0.19.6
4838 lazr.restfulclient = 0.12.0
4839@@ -87,12 +83,8 @@
4840 pymongo = 2.1.1
4841 pyOpenSSL = 0.10
4842 pystache = 0.3.1
4843-<<<<<<< TREE
4844 python-dateutil = 1.5
4845 python-memcached = 1.48
4846-=======
4847-python-memcached = 1.48
4848->>>>>>> MERGE-SOURCE
4849 # 2.2.1 with the one-liner Expect: 100-continue fix from
4850 # lp:~wgrant/python-openid/python-openid-2.2.1-fix676372.
4851 python-openid = 2.2.1-fix676372