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
=== modified file 'Makefile'
--- Makefile 2012-03-14 12:01:42 +0000
+++ Makefile 2012-03-29 14:36:38 +0000
@@ -305,7 +305,7 @@
305 $(PY) scripts/stop-loggerhead.py305 $(PY) scripts/stop-loggerhead.py
306306
307run_codehosting: build inplace stop307run_codehosting: build inplace stop
308 bin/run -r librarian,sftp,forker,codebrowse -i $(LPCONFIG)308 bin/run -r librarian,sftp,forker,codebrowse,rabbitmq -i $(LPCONFIG)
309309
310start_librarian: compile310start_librarian: compile
311 bin/start_librarian311 bin/start_librarian
312312
=== modified file 'buildout.cfg'
--- buildout.cfg 2012-03-16 11:09:03 +0000
+++ buildout.cfg 2012-03-29 14:36:38 +0000
@@ -65,6 +65,7 @@
65[scripts]65[scripts]
66recipe = z3c.recipe.scripts66recipe = z3c.recipe.scripts
67eggs = lp67eggs = lp
68 celery
68 funkload69 funkload
69 zc.zservertracelog70 zc.zservertracelog
70 pyinotify71 pyinotify
7172
=== modified file 'lib/lp/app/templates/base-layout.pt'
--- lib/lp/app/templates/base-layout.pt 2012-03-29 14:36:36 +0000
+++ lib/lp/app/templates/base-layout.pt 2012-03-23 20:56:11 +0000
@@ -79,14 +79,9 @@
79 <tal:login replace="structure context/@@login_status" />79 <tal:login replace="structure context/@@login_status" />
80 </div><!--id="locationbar"-->80 </div><!--id="locationbar"-->
8181
82<<<<<<< TREE
83 <div id="watermark" class="watermark-apps-portlet"82 <div id="watermark" class="watermark-apps-portlet"
84 tal:condition="view/macro:has-watermark">83 tal:condition="view/macro:has-watermark">
85 <div>84 <div>
86=======
87 <div id="watermark" class="watermark-apps-portlet">
88 <div>
89>>>>>>> MERGE-SOURCE
90 <span tal:replace="structure view/watermark:logo"></span>85 <span tal:replace="structure view/watermark:logo"></span>
91 </div>86 </div>
92 <div class="wide">87 <div class="wide">
9388
=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py 2012-03-29 14:36:36 +0000
+++ lib/lp/bugs/model/bug.py 2012-03-29 06:02:46 +0000
@@ -394,7 +394,6 @@
394 heat_last_updated = UtcDateTimeCol(default=None)394 heat_last_updated = UtcDateTimeCol(default=None)
395 latest_patch_uploaded = UtcDateTimeCol(default=None)395 latest_patch_uploaded = UtcDateTimeCol(default=None)
396396
397<<<<<<< TREE
398 @property397 @property
399 def private(self):398 def private(self):
400 return self.information_type in PRIVATE_INFORMATION_TYPES399 return self.information_type in PRIVATE_INFORMATION_TYPES
@@ -403,22 +402,6 @@
403 def security_related(self):402 def security_related(self):
404 return self.information_type in SECURITY_INFORMATION_TYPES403 return self.information_type in SECURITY_INFORMATION_TYPES
405404
406=======
407 @property
408 def private(self):
409 if self.information_type:
410 return self.information_type in PRIVATE_INFORMATION_TYPES
411 else:
412 return self._private
413
414 @property
415 def security_related(self):
416 if self.information_type:
417 return self.information_type in SECURITY_INFORMATION_TYPES
418 else:
419 return self._security_related
420
421>>>>>>> MERGE-SOURCE
422 @cachedproperty405 @cachedproperty
423 def _subscriber_cache(self):406 def _subscriber_cache(self):
424 """Caches known subscribers."""407 """Caches known subscribers."""
@@ -1727,106 +1710,6 @@
17271710
1728 return bugtask1711 return bugtask
17291712
1730<<<<<<< TREE
1731=======
1732 def _setInformationType(self):
1733 if self._private and self._security_related:
1734 self.information_type = InformationType.EMBARGOEDSECURITY
1735 elif self._private:
1736 self.information_type = InformationType.USERDATA
1737 elif self._security_related:
1738 self.information_type = InformationType.UNEMBARGOEDSECURITY
1739 else:
1740 self.information_type = InformationType.PUBLIC
1741
1742 def setPrivacyAndSecurityRelated(self, private, security_related, who):
1743 """ See `IBug`."""
1744 private_changed = False
1745 security_related_changed = False
1746 bug_before_modification = Snapshot(self, providing=providedBy(self))
1747
1748 f_flag_str = 'disclosure.enhanced_private_bug_subscriptions.enabled'
1749 f_flag = bool(getFeatureFlag(f_flag_str))
1750 if f_flag:
1751 # Before we update the privacy or security_related status, we
1752 # need to reconcile the subscribers to avoid leaking private
1753 # information.
1754 if (self.private != private
1755 or self.security_related != security_related):
1756 self.reconcileSubscribers(private, security_related, who)
1757
1758 if self.private != private:
1759 # We do not allow multi-pillar private bugs except for those teams
1760 # who want to shoot themselves in the foot.
1761 if private:
1762 allow_multi_pillar_private = bool(getFeatureFlag(
1763 'disclosure.allow_multipillar_private_bugs.enabled'))
1764 if (not allow_multi_pillar_private
1765 and len(self.affected_pillars) > 1):
1766 raise BugCannotBePrivate(
1767 "Multi-pillar bugs cannot be private.")
1768 private_changed = True
1769 self._private = private
1770
1771 if private:
1772 self.who_made_private = who
1773 self.date_made_private = UTC_NOW
1774 else:
1775 self.who_made_private = None
1776 self.date_made_private = None
1777
1778 # XXX: This should be a bulk update. RBC 20100827
1779 # bug=https://bugs.launchpad.net/storm/+bug/625071
1780 for attachment in self.attachments_unpopulated:
1781 attachment.libraryfile.restricted = private
1782
1783 if self.security_related != security_related:
1784 security_related_changed = True
1785 self._security_related = security_related
1786
1787 if private_changed or security_related_changed:
1788 # Correct the heat for the bug immediately, so that we don't have
1789 # to wait for the next calculation job for the adjusted heat.
1790 self.updateHeat()
1791
1792 self._setInformationType()
1793
1794 if private_changed or security_related_changed:
1795 changed_fields = []
1796
1797 if private_changed:
1798 changed_fields.append('private')
1799 if not f_flag and private:
1800 # If we didn't call reconcileSubscribers, we may have
1801 # bug supervisors who should be on this bug, but aren't.
1802 supervisors = set()
1803 for bugtask in self.bugtasks:
1804 supervisors.add(bugtask.pillar.bug_supervisor)
1805 if None in supervisors:
1806 supervisors.remove(None)
1807 for s in supervisors:
1808 subscriptions = get_structural_subscriptions_for_bug(
1809 self, s)
1810 if subscriptions != []:
1811 self.subscribe(s, who)
1812
1813 if security_related_changed:
1814 changed_fields.append('security_related')
1815 if not f_flag and security_related:
1816 # The bug turned out to be security-related, subscribe the
1817 # security contact. We do it here only if the feature flag
1818 # is not set, otherwise it's done in
1819 # reconcileSubscribers().
1820 for pillar in self.affected_pillars:
1821 if pillar.security_contact is not None:
1822 self.subscribe(pillar.security_contact, who)
1823
1824 notify(ObjectModifiedEvent(
1825 self, bug_before_modification, changed_fields, user=who))
1826
1827 return private_changed, security_related_changed
1828
1829>>>>>>> MERGE-SOURCE
1830 def setPrivate(self, private, who):1713 def setPrivate(self, private, who):
1831 """See `IBug`.1714 """See `IBug`.
18321715
@@ -2997,15 +2880,9 @@
2997 params.information_type in SECURITY_INFORMATION_TYPES)2880 params.information_type in SECURITY_INFORMATION_TYPES)
2998 bug = Bug(2881 bug = Bug(
2999 title=params.title, description=params.description,2882 title=params.title, description=params.description,
3000<<<<<<< TREE
3001 owner=params.owner, datecreated=params.datecreated,2883 owner=params.owner, datecreated=params.datecreated,
3002 information_type=params.information_type,2884 information_type=params.information_type,
3003 _private=private, _security_related=security_related,2885 _private=private, _security_related=security_related,
3004=======
3005 _private=params.private, owner=params.owner,
3006 datecreated=params.datecreated,
3007 _security_related=params.security_related,
3008>>>>>>> MERGE-SOURCE
3009 **extra_params)2886 **extra_params)
30102887
3011 if params.subscribe_owner:2888 if params.subscribe_owner:
30122889
=== modified file 'lib/lp/bugs/model/bugtask.py'
--- lib/lp/bugs/model/bugtask.py 2012-03-29 14:36:36 +0000
+++ lib/lp/bugs/model/bugtask.py 2012-03-29 00:48:21 +0000
@@ -43,10 +43,8 @@
43 And,43 And,
44 Join,44 Join,
45 Or,45 Or,
46 Select,
47 SQL,46 SQL,
48 Sum,47 Sum,
49 Union,
50 )48 )
51from storm.store import (49from storm.store import (
52 EmptyResultSet,50 EmptyResultSet,
5351
=== modified file 'lib/lp/bugs/scripts/bugimport.py'
--- lib/lp/bugs/scripts/bugimport.py 2012-03-29 14:36:36 +0000
+++ lib/lp/bugs/scripts/bugimport.py 2012-03-23 07:17:15 +0000
@@ -293,17 +293,11 @@
293293
294 private = get_value(bugnode, 'private') == 'True'294 private = get_value(bugnode, 'private') == 'True'
295 security_related = get_value(bugnode, 'security_related') == 'True'295 security_related = get_value(bugnode, 'security_related') == 'True'
296<<<<<<< TREE
297 # If the product has private_bugs, we force private to True.296 # If the product has private_bugs, we force private to True.
298 if self.product.private_bugs:297 if self.product.private_bugs:
299 private = True298 private = True
300 information_type = convert_to_information_type(299 information_type = convert_to_information_type(
301 private, security_related)300 private, security_related)
302=======
303 # If the product has private_bugs, we force private to True.
304 if self.product.private_bugs:
305 private = True
306>>>>>>> MERGE-SOURCE
307301
308 if owner is None:302 if owner is None:
309 owner = self.bug_importer303 owner = self.bug_importer
@@ -311,17 +305,8 @@
311 msg = self.createMessage(commentnode, defaulttitle=title)305 msg = self.createMessage(commentnode, defaulttitle=title)
312306
313 bug = self.product.createBug(CreateBugParams(307 bug = self.product.createBug(CreateBugParams(
314<<<<<<< TREE
315 msg=msg, datecreated=datecreated, title=title,308 msg=msg, datecreated=datecreated, title=title,
316 information_type=information_type, owner=owner))309 information_type=information_type, owner=owner))
317=======
318 msg=msg,
319 datecreated=datecreated,
320 title=title,
321 private=private or security_related,
322 security_related=security_related,
323 owner=owner))
324>>>>>>> MERGE-SOURCE
325 bugtask = bug.bugtasks[0]310 bugtask = bug.bugtasks[0]
326 self.logger.info('Creating Launchpad bug #%d', bug.id)311 self.logger.info('Creating Launchpad bug #%d', bug.id)
327312
@@ -336,18 +321,11 @@
336 bug.linkMessage(msg)321 bug.linkMessage(msg)
337 self.createAttachments(bug, msg, commentnode)322 self.createAttachments(bug, msg, commentnode)
338323
339<<<<<<< TREE
340 # Security bugs must be created private, so set it correctly.324 # Security bugs must be created private, so set it correctly.
341 if not self.product.private_bugs:325 if not self.product.private_bugs:
342 information_type = convert_to_information_type(326 information_type = convert_to_information_type(
343 private, security_related)327 private, security_related)
344 bug.transitionToInformationType(information_type, owner)328 bug.transitionToInformationType(information_type, owner)
345=======
346 # Security bugs must be created private, so set it correctly.
347 if not self.product.private_bugs:
348 bug.setPrivacyAndSecurityRelated(
349 private, security_related, owner)
350>>>>>>> MERGE-SOURCE
351 bug.name = get_value(bugnode, 'nickname')329 bug.name = get_value(bugnode, 'nickname')
352 description = get_value(bugnode, 'description')330 description = get_value(bugnode, 'description')
353 if description:331 if description:
354332
=== modified file 'lib/lp/bugs/tests/test_bug_mirror_access_triggers.py'
--- lib/lp/bugs/tests/test_bug_mirror_access_triggers.py 2012-03-29 14:36:36 +0000
+++ lib/lp/bugs/tests/test_bug_mirror_access_triggers.py 2012-03-26 00:12:50 +0000
@@ -136,11 +136,7 @@
136 bug = self.makeBugAndPolicies(private=True)136 bug = self.makeBugAndPolicies(private=True)
137 self.assertIsNot(137 self.assertIsNot(
138 None, getUtility(IAccessArtifactSource).find([bug]).one())138 None, getUtility(IAccessArtifactSource).find([bug]).one())
139<<<<<<< TREE
140 bug.setPrivate(False, bug.owner)139 bug.setPrivate(False, bug.owner)
141=======
142 removeSecurityProxy(bug).setPrivate(False, bug.owner)
143>>>>>>> MERGE-SOURCE
144 self.assertIs(140 self.assertIs(
145 None, getUtility(IAccessArtifactSource).find([bug]).one())141 None, getUtility(IAccessArtifactSource).find([bug]).one())
146142
@@ -148,11 +144,7 @@
148 bug = self.makeBugAndPolicies(private=False)144 bug = self.makeBugAndPolicies(private=False)
149 self.assertIs(145 self.assertIs(
150 None, getUtility(IAccessArtifactSource).find([bug]).one())146 None, getUtility(IAccessArtifactSource).find([bug]).one())
151<<<<<<< TREE
152 bug.setPrivate(True, bug.owner)147 bug.setPrivate(True, bug.owner)
153=======
154 removeSecurityProxy(bug).setPrivate(True, bug.owner)
155>>>>>>> MERGE-SOURCE
156 self.assertIsNot(148 self.assertIsNot(
157 None, getUtility(IAccessArtifactSource).find([bug]).one())149 None, getUtility(IAccessArtifactSource).find([bug]).one())
158 self.assertEqual((1, 1), self.assertMirrored(bug))150 self.assertEqual((1, 1), self.assertMirrored(bug))
@@ -166,11 +158,7 @@
166 self.assertContentEqual(158 self.assertContentEqual(
167 [InformationType.USERDATA],159 [InformationType.USERDATA],
168 self.getPolicyTypesForArtifact(artifact))160 self.getPolicyTypesForArtifact(artifact))
169<<<<<<< TREE
170 bug.setSecurityRelated(True, bug.owner)161 bug.setSecurityRelated(True, bug.owner)
171=======
172 removeSecurityProxy(bug).setSecurityRelated(True, bug.owner)
173>>>>>>> MERGE-SOURCE
174 self.assertEqual((1, 1), self.assertMirrored(bug))162 self.assertEqual((1, 1), self.assertMirrored(bug))
175 self.assertContentEqual(163 self.assertContentEqual(
176 [InformationType.EMBARGOEDSECURITY],164 [InformationType.EMBARGOEDSECURITY],
177165
=== 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-29 14:36:38 +0000
@@ -1078,7 +1078,7 @@
1078 """Create an IBranchUpgradeJob to upgrade this branch."""1078 """Create an IBranchUpgradeJob to upgrade this branch."""
10791079
1080 def branchChanged(stacked_on_url, last_revision_id, control_format,1080 def branchChanged(stacked_on_url, last_revision_id, control_format,
1081 branch_format, repository_format):1081 branch_format, repository_format, celery_scan=True):
1082 """Record that a branch has been changed.1082 """Record that a branch has been changed.
10831083
1084 This method records the stacked on branch tip revision id and format1084 This method records the stacked on branch tip revision id and format
@@ -1092,6 +1092,9 @@
1092 :param branch_format: The entry from BranchFormat for the branch.1092 :param branch_format: The entry from BranchFormat for the branch.
1093 :param repository_format: The entry from RepositoryFormat for the1093 :param repository_format: The entry from RepositoryFormat for the
1094 branch.1094 branch.
1095 :param celery_scan: If True, request a branch scan via Celery.
1096 Otherwise, a BranchScanJob may be created, but not requested to
1097 run. Should only be False in certain tests.
1095 """1098 """
10961099
1097 @export_destructor_operation()1100 @export_destructor_operation()
10981101
=== 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-29 14:36:38 +0000
@@ -40,6 +40,7 @@
40 Reference,40 Reference,
41 )41 )
42from storm.store import Store42from storm.store import Store
43import transaction
43from zope.component import getUtility44from zope.component import getUtility
44from zope.event import notify45from zope.event import notify
45from zope.interface import implements46from zope.interface import implements
@@ -1032,7 +1033,8 @@
1032 return getUtility(IBranchLookup).getByUniqueName(location)1033 return getUtility(IBranchLookup).getByUniqueName(location)
10331034
1034 def branchChanged(self, stacked_on_url, last_revision_id,1035 def branchChanged(self, stacked_on_url, last_revision_id,
1035 control_format, branch_format, repository_format):1036 control_format, branch_format, repository_format,
1037 celery_scan=True):
1036 """See `IBranch`."""1038 """See `IBranch`."""
1037 self.mirror_status_message = None1039 self.mirror_status_message = None
1038 if stacked_on_url == '' or stacked_on_url is None:1040 if stacked_on_url == '' or stacked_on_url is None:
@@ -1057,7 +1059,15 @@
1057 self.last_mirrored_id = last_revision_id1059 self.last_mirrored_id = last_revision_id
1058 if self.last_scanned_id != last_revision_id:1060 if self.last_scanned_id != last_revision_id:
1059 from lp.code.model.branchjob import BranchScanJob1061 from lp.code.model.branchjob import BranchScanJob
1060 BranchScanJob.create(self)1062 job_id = BranchScanJob.create(self).job_id
1063 if celery_scan:
1064 # lp.services.job.celery is imported only where needed.
1065 from lp.services.job.celeryjob import CeleryRunJob
1066 current = transaction.get()
1067 def runHook(succeeded):
1068 if succeeded:
1069 CeleryRunJob.delay(job_id)
1070 current.addAfterCommitHook(runHook)
1061 self.control_format = control_format1071 self.control_format = control_format
1062 self.branch_format = branch_format1072 self.branch_format = branch_format
1063 self.repository_format = repository_format1073 self.repository_format = repository_format
10641074
=== modified file 'lib/lp/code/model/branchjob.py'
--- lib/lp/code/model/branchjob.py 2012-02-21 19:13:45 +0000
+++ lib/lp/code/model/branchjob.py 2012-03-29 14:36:38 +0000
@@ -80,6 +80,7 @@
80from lp.code.model.branch import Branch80from lp.code.model.branch import Branch
81from lp.code.model.branchmergeproposal import BranchMergeProposal81from lp.code.model.branchmergeproposal import BranchMergeProposal
82from lp.code.model.revision import RevisionSet82from lp.code.model.revision import RevisionSet
83from lp.codehosting.bzrutils import server
83from lp.codehosting.scanner.bzrsync import BzrSync84from lp.codehosting.scanner.bzrsync import BzrSync
84from lp.codehosting.vfs import (85from lp.codehosting.vfs import (
85 branch_id_to_path,86 branch_id_to_path,
@@ -93,7 +94,10 @@
93from lp.services.database.lpstorm import IStore94from lp.services.database.lpstorm import IStore
94from lp.services.database.sqlbase import SQLBase95from lp.services.database.sqlbase import SQLBase
95from lp.services.job.interfaces.job import JobStatus96from lp.services.job.interfaces.job import JobStatus
96from lp.services.job.model.job import Job97from lp.services.job.model.job import (
98 EnumeratedSubclass,
99 Job,
100 )
97from lp.services.job.runner import BaseRunnableJob101from lp.services.job.runner import BaseRunnableJob
98from lp.services.mail.sendmail import format_address_for_person102from lp.services.mail.sendmail import format_address_for_person
99from lp.services.webapp import (103from lp.services.webapp import (
@@ -212,9 +216,14 @@
212 SQLBase.destroySelf(self)216 SQLBase.destroySelf(self)
213 self.job.destroySelf()217 self.job.destroySelf()
214218
219 def makeDerived(self):
220 return BranchJobDerived.makeSubclass(self)
221
215222
216class BranchJobDerived(BaseRunnableJob):223class BranchJobDerived(BaseRunnableJob):
217224
225 __metaclass__ = EnumeratedSubclass
226
218 delegates(IBranchJob)227 delegates(IBranchJob)
219228
220 def __init__(self, branch_job):229 def __init__(self, branch_job):
@@ -287,29 +296,26 @@
287 classProvides(IBranchScanJobSource)296 classProvides(IBranchScanJobSource)
288 class_job_type = BranchJobType.SCAN_BRANCH297 class_job_type = BranchJobType.SCAN_BRANCH
289 memory_limit = 2 * (1024 ** 3)298 memory_limit = 2 * (1024 ** 3)
290 server = None
291299
292 @classmethod300 @classmethod
293 def create(cls, branch):301 def create(cls, branch):
294 """See `IBranchScanJobSource`."""302 """See `IBranchScanJobSource`."""
295 branch_job = BranchJob(branch, BranchJobType.SCAN_BRANCH, {})303 branch_job = BranchJob(branch, cls.class_job_type, {})
296 return cls(branch_job)304 return cls(branch_job)
297305
298 def run(self):306 def run(self):
299 """See `IBranchScanJob`."""307 """See `IBranchScanJob`."""
300 from lp.services.scripts import log308 from lp.services.scripts import log
301 bzrsync = BzrSync(self.branch, log)309 with server(get_ro_server(), no_replace=True):
302 bzrsync.syncBranchAndClose()310 bzrsync = BzrSync(self.branch, log)
311 bzrsync.syncBranchAndClose()
303312
304 @classmethod313 @classmethod
305 @contextlib.contextmanager314 @contextlib.contextmanager
306 def contextManager(cls):315 def contextManager(cls):
307 """See `IBranchScanJobSource`."""316 """See `IBranchScanJobSource`."""
308 errorlog.globalErrorUtility.configure('branchscanner')317 errorlog.globalErrorUtility.configure('branchscanner')
309 cls.server = get_ro_server()
310 cls.server.start_server()
311 yield318 yield
312 cls.server.stop_server()
313319
314320
315class BranchUpgradeJob(BranchJobDerived):321class BranchUpgradeJob(BranchJobDerived):
@@ -330,7 +336,7 @@
330 """See `IBranchUpgradeJobSource`."""336 """See `IBranchUpgradeJobSource`."""
331 branch.checkUpgrade()337 branch.checkUpgrade()
332 branch_job = BranchJob(338 branch_job = BranchJob(
333 branch, BranchJobType.UPGRADE_BRANCH, {}, requester=requester)339 branch, cls.class_job_type, {}, requester=requester)
334 return cls(branch_job)340 return cls(branch_job)
335341
336 @staticmethod342 @staticmethod
@@ -338,63 +344,63 @@
338 def contextManager():344 def contextManager():
339 """See `IBranchUpgradeJobSource`."""345 """See `IBranchUpgradeJobSource`."""
340 errorlog.globalErrorUtility.configure('upgrade_branches')346 errorlog.globalErrorUtility.configure('upgrade_branches')
341 server = get_rw_server()
342 server.start_server()
343 yield347 yield
344 server.stop_server()
345348
346 def run(self, _check_transaction=False):349 def run(self, _check_transaction=False):
347 """See `IBranchUpgradeJob`."""350 """See `IBranchUpgradeJob`."""
348 # Set up the new branch structure351 # Set up the new branch structure
349 upgrade_branch_path = tempfile.mkdtemp()352 with server(get_rw_server(), no_replace=True):
350 try:353 upgrade_branch_path = tempfile.mkdtemp()
351 upgrade_transport = get_transport(upgrade_branch_path)
352 upgrade_transport.mkdir('.bzr')
353 source_branch_transport = get_transport(
354 self.branch.getInternalBzrUrl())
355 source_branch_transport.clone('.bzr').copy_tree_to_transport(
356 upgrade_transport.clone('.bzr'))
357 transaction.commit()
358 upgrade_branch = BzrBranch.open_from_transport(upgrade_transport)
359
360 # No transactions are open so the DB connection won't be killed.
361 with TransactionFreeOperation():
362 # Perform the upgrade.
363 upgrade(upgrade_branch.base)
364
365 # Re-open the branch, since its format has changed.
366 upgrade_branch = BzrBranch.open_from_transport(
367 upgrade_transport)
368 source_branch = BzrBranch.open_from_transport(
369 source_branch_transport)
370
371 source_branch.lock_write()
372 upgrade_branch.pull(source_branch)
373 upgrade_branch.fetch(source_branch)
374 source_branch.unlock()
375
376 # Move the branch in the old format to backup.bzr
377 try:354 try:
378 source_branch_transport.delete_tree('backup.bzr')355 upgrade_transport = get_transport(upgrade_branch_path)
379 except NoSuchFile:356 upgrade_transport.mkdir('.bzr')
380 pass357 source_branch_transport = get_transport(
381 source_branch_transport.rename('.bzr', 'backup.bzr')358 self.branch.getInternalBzrUrl())
382 source_branch_transport.mkdir('.bzr')359 source_branch_transport.clone('.bzr').copy_tree_to_transport(
383 upgrade_transport.clone('.bzr').copy_tree_to_transport(360 upgrade_transport.clone('.bzr'))
384 source_branch_transport.clone('.bzr'))361 transaction.commit()
385362 upgrade_branch = BzrBranch.open_from_transport(
386 # Re-open the source branch again.363 upgrade_transport)
387 source_branch = BzrBranch.open_from_transport(364
388 source_branch_transport)365 # No transactions are open so the DB connection won't be
389366 # killed.
390 formats = get_branch_formats(source_branch)367 with TransactionFreeOperation():
391368 # Perform the upgrade.
392 self.branch.branchChanged(369 upgrade(upgrade_branch.base)
393 self.branch.stacked_on,370
394 self.branch.last_scanned_id,371 # Re-open the branch, since its format has changed.
395 *formats)372 upgrade_branch = BzrBranch.open_from_transport(
396 finally:373 upgrade_transport)
397 shutil.rmtree(upgrade_branch_path)374 source_branch = BzrBranch.open_from_transport(
375 source_branch_transport)
376
377 source_branch.lock_write()
378 upgrade_branch.pull(source_branch)
379 upgrade_branch.fetch(source_branch)
380 source_branch.unlock()
381
382 # Move the branch in the old format to backup.bzr
383 try:
384 source_branch_transport.delete_tree('backup.bzr')
385 except NoSuchFile:
386 pass
387 source_branch_transport.rename('.bzr', 'backup.bzr')
388 source_branch_transport.mkdir('.bzr')
389 upgrade_transport.clone('.bzr').copy_tree_to_transport(
390 source_branch_transport.clone('.bzr'))
391
392 # Re-open the source branch again.
393 source_branch = BzrBranch.open_from_transport(
394 source_branch_transport)
395
396 formats = get_branch_formats(source_branch)
397
398 self.branch.branchChanged(
399 self.branch.stacked_on,
400 self.branch.last_scanned_id,
401 *formats)
402 finally:
403 shutil.rmtree(upgrade_branch_path)
398404
399405
400class RevisionMailJob(BranchJobDerived):406class RevisionMailJob(BranchJobDerived):
@@ -415,7 +421,7 @@
415 'body': body,421 'body': body,
416 'subject': subject,422 'subject': subject,
417 }423 }
418 branch_job = BranchJob(branch, BranchJobType.REVISION_MAIL, metadata)424 branch_job = BranchJob(branch, cls.class_job_type, metadata)
419 return cls(branch_job)425 return cls(branch_job)
420426
421 @property427 @property
422428
=== modified file 'lib/lp/code/model/tests/test_branch.py'
--- lib/lp/code/model/tests/test_branch.py 2012-02-15 08:13:51 +0000
+++ lib/lp/code/model/tests/test_branch.py 2012-03-29 14:36:38 +0000
@@ -130,6 +130,7 @@
130from lp.testing.layers import (130from lp.testing.layers import (
131 AppServerLayer,131 AppServerLayer,
132 DatabaseFunctionalLayer,132 DatabaseFunctionalLayer,
133 LaunchpadFunctionalLayer,
133 LaunchpadZopelessLayer,134 LaunchpadZopelessLayer,
134 )135 )
135from lp.translations.model.translationtemplatesbuildjob import (136from lp.translations.model.translationtemplatesbuildjob import (
@@ -159,7 +160,7 @@
159class TestBranchChanged(TestCaseWithFactory):160class TestBranchChanged(TestCaseWithFactory):
160 """Tests for `IBranch.branchChanged`."""161 """Tests for `IBranch.branchChanged`."""
161162
162 layer = DatabaseFunctionalLayer163 layer = LaunchpadFunctionalLayer
163164
164 def setUp(self):165 def setUp(self):
165 TestCaseWithFactory.setUp(self)166 TestCaseWithFactory.setUp(self)
@@ -2144,7 +2145,7 @@
2144class TestPendingWrites(TestCaseWithFactory):2145class TestPendingWrites(TestCaseWithFactory):
2145 """Are there changes to this branch not reflected in the database?"""2146 """Are there changes to this branch not reflected in the database?"""
21462147
2147 layer = DatabaseFunctionalLayer2148 layer = LaunchpadFunctionalLayer
21482149
2149 def test_new_branch_no_writes(self):2150 def test_new_branch_no_writes(self):
2150 # New branches have no pending writes.2151 # New branches have no pending writes.
21512152
=== modified file 'lib/lp/code/model/tests/test_branchpuller.py'
--- lib/lp/code/model/tests/test_branchpuller.py 2012-01-06 11:08:30 +0000
+++ lib/lp/code/model/tests/test_branchpuller.py 2012-03-29 14:36:38 +0000
@@ -89,7 +89,7 @@
89 transaction.commit()89 transaction.commit()
90 branch.startMirroring()90 branch.startMirroring()
91 removeSecurityProxy(branch).branchChanged(91 removeSecurityProxy(branch).branchChanged(
92 '', 'rev1', None, None, None)92 '', 'rev1', None, None, None, celery_scan=False)
93 self.assertEqual(None, branch.next_mirror_time)93 self.assertEqual(None, branch.next_mirror_time)
9494
95 def test_mirrorFailureResetsMirrorRequest(self):95 def test_mirrorFailureResetsMirrorRequest(self):
@@ -158,7 +158,7 @@
158 transaction.commit()158 transaction.commit()
159 branch.startMirroring()159 branch.startMirroring()
160 removeSecurityProxy(branch).branchChanged(160 removeSecurityProxy(branch).branchChanged(
161 '', 'rev1', None, None, None)161 '', 'rev1', None, None, None, celery_scan=False)
162 self.assertInFuture(branch.next_mirror_time, self.increment)162 self.assertInFuture(branch.next_mirror_time, self.increment)
163 self.assertEqual(0, branch.mirror_failures)163 self.assertEqual(0, branch.mirror_failures)
164164
165165
=== modified file 'lib/lp/code/model/tests/test_branchtarget.py'
--- lib/lp/code/model/tests/test_branchtarget.py 2012-01-01 02:58:52 +0000
+++ lib/lp/code/model/tests/test_branchtarget.py 2012-03-29 14:36:38 +0000
@@ -129,7 +129,8 @@
129 default_branch = self.factory.makePackageBranch(129 default_branch = self.factory.makePackageBranch(
130 sourcepackage=development_package)130 sourcepackage=development_package)
131 removeSecurityProxy(default_branch).branchChanged(131 removeSecurityProxy(default_branch).branchChanged(
132 '', self.factory.getUniqueString(), None, None, None)132 '', self.factory.getUniqueString(), None, None, None,
133 celery_scan=False)
133 registrant = development_package.distribution.owner134 registrant = development_package.distribution.owner
134 with person_logged_in(registrant):135 with person_logged_in(registrant):
135 development_package.setBranch(136 development_package.setBranch(
@@ -397,7 +398,7 @@
397 branch = self.factory.makeProductBranch(product=self.original)398 branch = self.factory.makeProductBranch(product=self.original)
398 self._setDevelopmentFocus(self.original, branch)399 self._setDevelopmentFocus(self.original, branch)
399 removeSecurityProxy(branch).branchChanged(400 removeSecurityProxy(branch).branchChanged(
400 '', 'rev1', None, None, None)401 '', 'rev1', None, None, None, celery_scan=False)
401 target = IBranchTarget(self.original)402 target = IBranchTarget(self.original)
402 self.assertEqual(branch, target.default_stacked_on_branch)403 self.assertEqual(branch, target.default_stacked_on_branch)
403404
@@ -537,7 +538,8 @@
537 branch = self.factory.makeAnyBranch(branch_type=BranchType.MIRRORED)538 branch = self.factory.makeAnyBranch(branch_type=BranchType.MIRRORED)
538 branch.startMirroring()539 branch.startMirroring()
539 removeSecurityProxy(branch).branchChanged(540 removeSecurityProxy(branch).branchChanged(
540 '', self.factory.getUniqueString(), None, None, None)541 '', self.factory.getUniqueString(), None, None, None,
542 celery_scan=False)
541 removeSecurityProxy(branch).branch_type = BranchType.REMOTE543 removeSecurityProxy(branch).branch_type = BranchType.REMOTE
542 self.assertIs(None, check_default_stacked_on(branch))544 self.assertIs(None, check_default_stacked_on(branch))
543545
@@ -553,14 +555,16 @@
553 branch = self.factory.makeAnyBranch(private=True)555 branch = self.factory.makeAnyBranch(private=True)
554 naked_branch = removeSecurityProxy(branch)556 naked_branch = removeSecurityProxy(branch)
555 naked_branch.branchChanged(557 naked_branch.branchChanged(
556 '', self.factory.getUniqueString(), None, None, None)558 '', self.factory.getUniqueString(), None, None, None,
559 celery_scan=False)
557 self.assertIs(None, check_default_stacked_on(branch))560 self.assertIs(None, check_default_stacked_on(branch))
558561
559 def test_been_mirrored(self):562 def test_been_mirrored(self):
560 # `check_default_stacked_on` returns the branch if it has revisions.563 # `check_default_stacked_on` returns the branch if it has revisions.
561 branch = self.factory.makeAnyBranch()564 branch = self.factory.makeAnyBranch()
562 removeSecurityProxy(branch).branchChanged(565 removeSecurityProxy(branch).branchChanged(
563 '', self.factory.getUniqueString(), None, None, None)566 '', self.factory.getUniqueString(), None, None, None,
567 celery_scan=False)
564 self.assertEqual(branch, check_default_stacked_on(branch))568 self.assertEqual(branch, check_default_stacked_on(branch))
565569
566570
567571
=== modified file 'lib/lp/code/tests/test_directbranchcommit.py'
--- lib/lp/code/tests/test_directbranchcommit.py 2012-01-01 02:58:52 +0000
+++ lib/lp/code/tests/test_directbranchcommit.py 2012-03-29 14:36:38 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2010 Canonical Ltd. This software is licensed under the1# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Tests for `DirectBranchCommit`."""4"""Tests for `DirectBranchCommit`."""
@@ -20,6 +20,7 @@
20from lp.testing.fakemethod import FakeMethod20from lp.testing.fakemethod import FakeMethod
21from lp.testing.layers import (21from lp.testing.layers import (
22 DatabaseFunctionalLayer,22 DatabaseFunctionalLayer,
23 LaunchpadZopelessLayer,
23 ZopelessDatabaseLayer,24 ZopelessDatabaseLayer,
24 )25 )
2526
@@ -63,7 +64,7 @@
63class TestDirectBranchCommit(DirectBranchCommitTestCase, TestCaseWithFactory):64class TestDirectBranchCommit(DirectBranchCommitTestCase, TestCaseWithFactory):
64 """Test `DirectBranchCommit`."""65 """Test `DirectBranchCommit`."""
6566
66 layer = ZopelessDatabaseLayer67 layer = LaunchpadZopelessLayer
6768
68 def test_defaults_to_branch_owner(self):69 def test_defaults_to_branch_owner(self):
69 # If no committer is given, DirectBranchCommits defaults to70 # If no committer is given, DirectBranchCommits defaults to
7071
=== modified file 'lib/lp/code/xmlrpc/tests/test_codehosting.py'
--- lib/lp/code/xmlrpc/tests/test_codehosting.py 2012-02-21 22:46:28 +0000
+++ lib/lp/code/xmlrpc/tests/test_codehosting.py 2012-03-29 14:36:38 +0000
@@ -56,6 +56,7 @@
56from lp.testing.layers import (56from lp.testing.layers import (
57 DatabaseFunctionalLayer,57 DatabaseFunctionalLayer,
58 FunctionalLayer,58 FunctionalLayer,
59 LaunchpadFunctionalLayer,
59 )60 )
60from lp.xmlrpc import faults61from lp.xmlrpc import faults
6162
@@ -1290,7 +1291,7 @@
1290 ])1291 ])
1291 scenarios = [1292 scenarios = [
1292 ('db', {'frontend': LaunchpadDatabaseFrontend,1293 ('db', {'frontend': LaunchpadDatabaseFrontend,
1293 'layer': DatabaseFunctionalLayer}),1294 'layer': LaunchpadFunctionalLayer}),
1294 ('inmemory', {'frontend': InMemoryFrontend,1295 ('inmemory', {'frontend': InMemoryFrontend,
1295 'layer': FunctionalLayer}),1296 'layer': FunctionalLayer}),
1296 ]1297 ]
12971298
=== modified file 'lib/lp/codehosting/bzrutils.py'
--- lib/lp/codehosting/bzrutils.py 2012-02-24 16:51:25 +0000
+++ lib/lp/codehosting/bzrutils.py 2012-03-29 14:36:38 +0000
@@ -19,6 +19,7 @@
19 'identical_formats',19 'identical_formats',
20 'install_oops_handler',20 'install_oops_handler',
21 'is_branch_stackable',21 'is_branch_stackable',
22 'server',
22 'read_locked',23 'read_locked',
23 'remove_exception_logging_hook',24 'remove_exception_logging_hook',
24 ]25 ]
@@ -33,6 +34,7 @@
33 )34 )
34from bzrlib.errors import (35from bzrlib.errors import (
35 NotStacked,36 NotStacked,
37 UnsupportedProtocol,
36 UnstackableBranchFormat,38 UnstackableBranchFormat,
37 UnstackableRepositoryFormat,39 UnstackableRepositoryFormat,
38 )40 )
@@ -42,6 +44,7 @@
42 RemoteRepository,44 RemoteRepository,
43 )45 )
44from bzrlib.transport import (46from bzrlib.transport import (
47 get_transport,
45 register_transport,48 register_transport,
46 unregister_transport,49 unregister_transport,
47 )50 )
@@ -332,9 +335,19 @@
332335
333336
334@contextmanager337@contextmanager
335def server(server):338def server(server, no_replace=False):
336 server.start_server()339 run_server = True
340 if no_replace:
341 try:
342 get_transport(server.get_url())
343 except UnsupportedProtocol:
344 pass
345 else:
346 run_server = False
347 if run_server:
348 server.start_server()
337 try:349 try:
338 yield server350 yield server
339 finally:351 finally:
340 server.stop_server()352 if run_server:
353 server.stop_server()
341354
=== modified file 'lib/lp/registry/browser/pillar.py'
--- lib/lp/registry/browser/pillar.py 2012-03-29 14:36:36 +0000
+++ lib/lp/registry/browser/pillar.py 2012-03-28 01:51:45 +0000
@@ -20,13 +20,6 @@
20from lazr.restful import ResourceJSONEncoder20from lazr.restful import ResourceJSONEncoder
21from lazr.restful.interfaces import IJSONRequestCache21from lazr.restful.interfaces import IJSONRequestCache
22import simplejson22import simplejson
23<<<<<<< TREE
24=======
25
26from lazr.restful import ResourceJSONEncoder
27from lazr.restful.interfaces import IJSONRequestCache
28
29>>>>>>> MERGE-SOURCE
30from zope.component import getUtility23from zope.component import getUtility
31from zope.interface import (24from zope.interface import (
32 implements,25 implements,
@@ -48,19 +41,12 @@
48from lp.bugs.browser.structuralsubscription import (41from lp.bugs.browser.structuralsubscription import (
49 StructuralSubscriptionMenuMixin,42 StructuralSubscriptionMenuMixin,
50 )43 )
51<<<<<<< TREE
52from lp.bugs.interfaces.bug import IBug44from lp.bugs.interfaces.bug import IBug
53from lp.code.interfaces.branch import IBranch45from lp.code.interfaces.branch import IBranch
54from lp.registry.interfaces.accesspolicy import (46from lp.registry.interfaces.accesspolicy import (
55 IAccessPolicyGrantFlatSource,47 IAccessPolicyGrantFlatSource,
56 IAccessPolicySource,48 IAccessPolicySource,
57 )49 )
58=======
59from lp.registry.interfaces.accesspolicy import (
60 IAccessPolicyGrantFlatSource,
61 IAccessPolicySource,
62 )
63>>>>>>> MERGE-SOURCE
64from lp.registry.interfaces.distributionsourcepackage import (50from lp.registry.interfaces.distributionsourcepackage import (
65 IDistributionSourcePackage,51 IDistributionSourcePackage,
66 )52 )
@@ -68,30 +54,15 @@
68from lp.registry.interfaces.person import IPersonSet54from lp.registry.interfaces.person import IPersonSet
69from lp.registry.interfaces.pillar import IPillar55from lp.registry.interfaces.pillar import IPillar
70from lp.registry.interfaces.projectgroup import IProjectGroup56from lp.registry.interfaces.projectgroup import IProjectGroup
71<<<<<<< TREE
72from lp.registry.model.pillar import PillarPerson57from lp.registry.model.pillar import PillarPerson
73from lp.services.config import config58from lp.services.config import config
74from lp.services.features import getFeatureFlag59from lp.services.features import getFeatureFlag
75=======
76from lp.registry.interfaces.person import IPersonSet
77from lp.registry.model.pillar import PillarPerson
78from lp.services.config import config
79>>>>>>> MERGE-SOURCE
80from lp.services.propertycache import cachedproperty60from lp.services.propertycache import cachedproperty
81<<<<<<< TREE61from lp.services.webapp.authorization import check_permission
82from lp.services.webapp.authorization import check_permission62from lp.services.webapp.batching import (
83from lp.services.webapp.batching import (63 BatchNavigator,
84 BatchNavigator,64 StormRangeFactory,
85 StormRangeFactory,65 )
86 )
87=======
88from lp.services.features import getFeatureFlag
89from lp.services.webapp.authorization import check_permission
90from lp.services.webapp.batching import (
91 BatchNavigator,
92 StormRangeFactory,
93 )
94>>>>>>> MERGE-SOURCE
95from lp.services.webapp.menu import (66from lp.services.webapp.menu import (
96 ApplicationMenu,67 ApplicationMenu,
97 enabled_with_permission,68 enabled_with_permission,
@@ -326,7 +297,6 @@
326 return simplejson.dumps(297 return simplejson.dumps(
327 self.sharing_picker_config, cls=ResourceJSONEncoder)298 self.sharing_picker_config, cls=ResourceJSONEncoder)
328299
329<<<<<<< TREE
330 def _getBatchNavigator(self, sharees):300 def _getBatchNavigator(self, sharees):
331 """Return the batch navigator to be used to batch the sharees."""301 """Return the batch navigator to be used to batch the sharees."""
332 return BatchNavigator(302 return BatchNavigator(
@@ -344,25 +314,6 @@
344314
345 def unbatched_sharees(self):315 def unbatched_sharees(self):
346 """All the sharees for a pillar."""316 """All the sharees for a pillar."""
347=======
348 def _getBatchNavigator(self, sharees):
349 """Return the batch navigator to be used to batch the sharees."""
350 return BatchNavigator(
351 sharees, self.request,
352 hide_counts=True,
353 size=config.launchpad.default_batch_size,
354 range_factory=StormRangeFactory(sharees))
355
356 def shareeData(self):
357 """Return an `ITableBatchNavigator` for sharees."""
358 if self._batch_navigator is None:
359 unbatchedSharees = self.unbatchedShareeData()
360 self._batch_navigator = self._getBatchNavigator(unbatchedSharees)
361 return self._batch_navigator
362
363 def unbatchedShareeData(self):
364 """Return all the sharees for a pillar."""
365>>>>>>> MERGE-SOURCE
366 return self._getSharingService().getPillarSharees(self.context)317 return self._getSharingService().getPillarSharees(self.context)
367318
368 def initialize(self):319 def initialize(self):
@@ -379,7 +330,6 @@
379 and check_permission('launchpad.Edit', self.context))330 and check_permission('launchpad.Edit', self.context))
380 cache.objects['information_types'] = self.information_types331 cache.objects['information_types'] = self.information_types
381 cache.objects['sharing_permissions'] = self.sharing_permissions332 cache.objects['sharing_permissions'] = self.sharing_permissions
382<<<<<<< TREE
383333
384 view_names = set(reg.name for reg334 view_names = set(reg.name for reg
385 in iter_view_registrations(self.__class__))335 in iter_view_registrations(self.__class__))
@@ -455,49 +405,3 @@
455 branch_name=branch.unique_name,405 branch_name=branch.unique_name,
456 branch_id=branch.id))406 branch_id=branch.id))
457 return branch_data407 return branch_data
458=======
459
460 view_names = set(reg.name for reg
461 in iter_view_registrations(self.__class__))
462 if len(view_names) != 1:
463 raise AssertionError("Ambiguous view name.")
464 cache.objects['view_name'] = view_names.pop()
465 batch_navigator = self.shareeData()
466 cache.objects['sharee_data'] = (
467 self._getSharingService().getPillarShareeData(
468 self.context, batch_navigator.batch))
469
470 def _getBatchInfo(batch):
471 if batch is None:
472 return None
473 return {'memo': batch.range_memo,
474 'start': batch.startNumber() - 1}
475
476 next_batch = batch_navigator.batch.nextBatch()
477 cache.objects['next'] = _getBatchInfo(next_batch)
478 prev_batch = batch_navigator.batch.prevBatch()
479 cache.objects['prev'] = _getBatchInfo(prev_batch)
480 cache.objects['total'] = batch_navigator.batch.total()
481 cache.objects['forwards'] = batch_navigator.batch.range_forwards
482 last_batch = batch_navigator.batch.lastBatch()
483 cache.objects['last_start'] = last_batch.startNumber() - 1
484 cache.objects.update(_getBatchInfo(batch_navigator.batch))
485
486
487class PillarPersonSharingView(LaunchpadView):
488
489 page_title = "Person or team"
490 label = "Information shared with person or team"
491
492 def initialize(self):
493 enabled_flag = 'disclosure.enhanced_sharing.enabled'
494 enabled = bool(getFeatureFlag(enabled_flag))
495 if not enabled:
496 raise Unauthorized("This feature is not yet available.")
497
498 self.pillar = self.context.pillar
499 self.person = self.context.person
500
501 self.label = "Information shared with %s" % self.person.displayname
502 self.page_title = "%s" % self.person.displayname
503>>>>>>> MERGE-SOURCE
504408
=== modified file 'lib/lp/registry/browser/product.py'
--- lib/lp/registry/browser/product.py 2012-03-29 14:36:36 +0000
+++ lib/lp/registry/browser/product.py 2012-03-22 23:21:24 +0000
@@ -194,13 +194,7 @@
194from lp.services.webapp.authorization import check_permission194from lp.services.webapp.authorization import check_permission
195from lp.services.webapp.batching import BatchNavigator195from lp.services.webapp.batching import BatchNavigator
196from lp.services.webapp.breadcrumb import Breadcrumb196from lp.services.webapp.breadcrumb import Breadcrumb
197<<<<<<< TREE
198from lp.services.webapp.interfaces import UnsafeFormGetSubmissionError197from lp.services.webapp.interfaces import UnsafeFormGetSubmissionError
199=======
200from lp.services.webapp.interfaces import (
201 UnsafeFormGetSubmissionError,
202 )
203>>>>>>> MERGE-SOURCE
204from lp.services.webapp.menu import NavigationMenu198from lp.services.webapp.menu import NavigationMenu
205from lp.services.worlddata.helpers import browser_languages199from lp.services.worlddata.helpers import browser_languages
206from lp.services.worlddata.interfaces.country import ICountry200from lp.services.worlddata.interfaces.country import ICountry
207201
=== modified file 'lib/lp/registry/browser/tests/test_pillar_sharing.py'
--- lib/lp/registry/browser/tests/test_pillar_sharing.py 2012-03-29 14:36:36 +0000
+++ lib/lp/registry/browser/tests/test_pillar_sharing.py 2012-03-28 01:51:45 +0000
@@ -7,7 +7,6 @@
77
8from BeautifulSoup import BeautifulSoup8from BeautifulSoup import BeautifulSoup
9from lazr.restful.interfaces import IJSONRequestCache9from lazr.restful.interfaces import IJSONRequestCache
10<<<<<<< TREE
11import simplejson10import simplejson
12from testtools.matchers import (11from testtools.matchers import (
13 LessThan,12 LessThan,
@@ -15,29 +14,15 @@
15 Not,14 Not,
16 Raises,15 Raises,
17 )16 )
18=======
19from testtools.matchers import (
20 LessThan,
21 MatchesException,
22 Not,
23 Raises,
24 )
25>>>>>>> MERGE-SOURCE
26from zope.component import getUtility17from zope.component import getUtility
27from zope.publisher.interfaces import NotFound18from zope.publisher.interfaces import NotFound
28from zope.security.interfaces import Unauthorized19from zope.security.interfaces import Unauthorized
2920
30from lp.app.interfaces.services import IService21from lp.app.interfaces.services import IService
31<<<<<<< TREE
32from lp.registry.enums import InformationType22from lp.registry.enums import InformationType
33from lp.registry.interfaces.accesspolicy import IAccessPolicyGrantFlatSource23from lp.registry.interfaces.accesspolicy import IAccessPolicyGrantFlatSource
34from lp.registry.model.pillar import PillarPerson24from lp.registry.model.pillar import PillarPerson
35from lp.services.config import config25from lp.services.config import config
36=======
37from lp.registry.enums import InformationType
38from lp.registry.model.pillar import PillarPerson
39from lp.services.config import config
40>>>>>>> MERGE-SOURCE
41from lp.services.features.testing import FeatureFixture26from lp.services.features.testing import FeatureFixture
42from lp.services.webapp.interfaces import StormRangeFactoryError27from lp.services.webapp.interfaces import StormRangeFactoryError
43from lp.services.webapp.publisher import canonical_url28from lp.services.webapp.publisher import canonical_url
@@ -55,7 +40,6 @@
55 )40 )
5641
5742
58<<<<<<< TREE
59DETAILS_ENABLED_FLAG = {'disclosure.enhanced_sharing_details.enabled': 'true'}43DETAILS_ENABLED_FLAG = {'disclosure.enhanced_sharing_details.enabled': 'true'}
60ENABLED_FLAG = {'disclosure.enhanced_sharing.enabled': 'true'}44ENABLED_FLAG = {'disclosure.enhanced_sharing.enabled': 'true'}
61WRITE_FLAG = {'disclosure.enhanced_sharing.writable': 'true'}45WRITE_FLAG = {'disclosure.enhanced_sharing.writable': 'true'}
@@ -149,106 +133,12 @@
149 owner=self.owner, driver=self.driver)133 owner=self.owner, driver=self.driver)
150 login_person(self.driver)134 login_person(self.driver)
151135
152=======
153ENABLED_FLAG = {'disclosure.enhanced_sharing.enabled': 'true'}
154WRITE_FLAG = {'disclosure.enhanced_sharing.writable': 'true'}
155
156
157class PillarSharingDetailsMixin:
158 """Test the pillar sharing details view."""
159
160 layer = DatabaseFunctionalLayer
161
162 def getPillarPerson(self, person=None, with_sharing=True):
163 if person is None:
164 person = self.factory.makePerson()
165 if with_sharing:
166 if self.pillar_type == 'product':
167 bug = self.factory.makeBug(product=self.pillar, private=True)
168 elif self.pillar_type == 'distribution':
169 bug = self.factory.makeBug(
170 distribution=self.pillar, private=True)
171 artifact = self.factory.makeAccessArtifact(concrete=bug)
172 policy = self.factory.makeAccessPolicy(pillar=self.pillar)
173 self.factory.makeAccessPolicyArtifact(
174 artifact=artifact, policy=policy)
175 self.factory.makeAccessArtifactGrant(
176 artifact=artifact, grantee=person, grantor=self.pillar.owner)
177
178 return PillarPerson(self.pillar, person)
179
180 def test_view_traverses_plus_sharingdetails(self):
181 # The traversed url in the app is pillar/+sharingdetails/person
182 with FeatureFixture(ENABLED_FLAG):
183 # We have to do some fun url hacking to force the traversal a user
184 # encounters.
185 pillarperson = self.getPillarPerson()
186 expected = pillarperson.person.displayname
187 url = 'http://launchpad.dev/%s/+sharingdetails/%s' % (
188 pillarperson.pillar.name, pillarperson.person.name)
189 browser = self.getUserBrowser(user=self.driver, url=url)
190 self.assertEqual(expected, browser.title)
191
192 def test_not_found_without_sharing(self):
193 # If there is no sharing between pillar and person, NotFound is the
194 # result.
195 with FeatureFixture(ENABLED_FLAG):
196 # We have to do some fun url hacking to force the traversal a user
197 # encounters.
198 pillarperson = self.getPillarPerson(with_sharing=False)
199 url = 'http://launchpad.dev/%s/+sharingdetails/%s' % (
200 pillarperson.pillar.name, pillarperson.person.name)
201 browser = self.getUserBrowser(user=self.driver)
202 self.assertRaises(NotFound, browser.open, url)
203
204 def test_init_without_feature_flag(self):
205 # We need a feature flag to enable the view.
206 pillarperson = self.getPillarPerson()
207 self.assertRaises(
208 Unauthorized, create_initialized_view, pillarperson, '+index')
209
210 def test_init_with_feature_flag(self):
211 # The view works with a feature flag.
212 with FeatureFixture(ENABLED_FLAG):
213 pillarperson = self.getPillarPerson()
214 view = create_initialized_view(pillarperson, '+index')
215 self.assertEqual(pillarperson.person.displayname, view.page_title)
216
217
218class TestProductSharingDetailsView(
219 TestCaseWithFactory, PillarSharingDetailsMixin):
220
221 pillar_type = 'product'
222
223 def setUp(self):
224 super(TestProductSharingDetailsView, self).setUp()
225 self.driver = self.factory.makePerson()
226 self.owner = self.factory.makePerson()
227 self.pillar = self.factory.makeProduct(
228 owner=self.owner, driver=self.driver)
229 login_person(self.driver)
230
231
232class TestDistributionSharingDetailsView(
233 TestCaseWithFactory, PillarSharingDetailsMixin):
234
235 pillar_type = 'distribution'
236
237 def setUp(self):
238 super(TestDistributionSharingDetailsView, self).setUp()
239 self.driver = self.factory.makePerson()
240 self.owner = self.factory.makePerson()
241 self.pillar = self.factory.makeProduct(
242 owner=self.owner, driver=self.driver)
243 login_person(self.driver)
244>>>>>>> MERGE-SOURCE
245136
246class PillarSharingViewTestMixin:137class PillarSharingViewTestMixin:
247 """Test the PillarSharingView."""138 """Test the PillarSharingView."""
248139
249 layer = DatabaseFunctionalLayer140 layer = DatabaseFunctionalLayer
250141
251<<<<<<< TREE
252 def createSharees(self):142 def createSharees(self):
253 login_person(self.owner)143 login_person(self.owner)
254 self.access_policy = self.factory.makeAccessPolicy(144 self.access_policy = self.factory.makeAccessPolicy(
@@ -271,30 +161,6 @@
271 for x in range(10):161 for x in range(10):
272 makeGrants('name%s' % x)162 makeGrants('name%s' % x)
273163
274=======
275 def createSharees(self):
276 login_person(self.owner)
277 access_policy = self.factory.makeAccessPolicy(
278 pillar=self.pillar,
279 type=InformationType.PROPRIETARY)
280 self.grantees = []
281
282 def makeGrants(name):
283 grantee = self.factory.makePerson(name=name)
284 self.grantees.append(grantee)
285 # Make access policy grant so that 'All' is returned.
286 self.factory.makeAccessPolicyGrant(access_policy, grantee)
287 # Make access artifact grants so that 'Some' is returned.
288 artifact_grant = self.factory.makeAccessArtifactGrant()
289 self.factory.makeAccessPolicyArtifact(
290 artifact=artifact_grant.abstract_artifact,
291 policy=access_policy)
292 # Make grants for grantees in ascending order so we can slice off the
293 # first elements in the pillar observer results to check batching.
294 for x in range(10):
295 makeGrants('name%s' % x)
296
297>>>>>>> MERGE-SOURCE
298 def test_init_without_feature_flag(self):164 def test_init_without_feature_flag(self):
299 # We need a feature flag to enable the view.165 # We need a feature flag to enable the view.
300 self.assertRaises(166 self.assertRaises(
@@ -343,7 +209,6 @@
343 cache = IJSONRequestCache(view.request)209 cache = IJSONRequestCache(view.request)
344 self.assertIsNotNone(cache.objects.get('information_types'))210 self.assertIsNotNone(cache.objects.get('information_types'))
345 self.assertIsNotNone(cache.objects.get('sharing_permissions'))211 self.assertIsNotNone(cache.objects.get('sharing_permissions'))
346<<<<<<< TREE
347 batch_size = config.launchpad.default_batch_size212 batch_size = config.launchpad.default_batch_size
348 apgfs = getUtility(IAccessPolicyGrantFlatSource)213 apgfs = getUtility(IAccessPolicyGrantFlatSource)
349 sharees = apgfs.findGranteePermissionsByPolicy(214 sharees = apgfs.findGranteePermissionsByPolicy(
@@ -403,65 +268,6 @@
403 view = create_initialized_view(self.pillar, name='+sharing')268 view = create_initialized_view(self.pillar, name='+sharing')
404 cache = IJSONRequestCache(view.request)269 cache = IJSONRequestCache(view.request)
405 self.assertTrue(cache.objects.get('sharing_write_enabled'))270 self.assertTrue(cache.objects.get('sharing_write_enabled'))
406=======
407 aps = getUtility(IService, 'sharing')
408 batch_size = config.launchpad.default_batch_size
409 observers = aps.getPillarShareeData(
410 self.pillar, grantees=self.grantees[:batch_size])
411 self.assertContentEqual(
412 observers, cache.objects.get('sharee_data'))
413
414 def test_view_batch_data(self):
415 # Test the expected batching data is in the json request cache.
416 with FeatureFixture(ENABLED_FLAG):
417 view = create_initialized_view(self.pillar, name='+sharing')
418 cache = IJSONRequestCache(view.request)
419 # Test one expected data value (there are many).
420 next_batch = view.shareeData().batch.nextBatch()
421 self.assertContentEqual(
422 next_batch.range_memo, cache.objects.get('next')['memo'])
423
424 def test_view_range_factory(self):
425 # Test the view range factory is properly configured.
426 with FeatureFixture(ENABLED_FLAG):
427 view = create_initialized_view(self.pillar, name='+sharing')
428 range_factory = view.shareeData().batch.range_factory
429
430 def test_range_factory():
431 row = range_factory.resultset.get_plain_result_set()[0]
432 range_factory.getOrderValuesFor(row)
433
434 self.assertThat(
435 test_range_factory,
436 Not(Raises(MatchesException(StormRangeFactoryError))))
437
438 def test_view_query_count(self):
439 # Test the query count is within expected limit.
440 with FeatureFixture(ENABLED_FLAG):
441 view = create_view(self.pillar, name='+sharing')
442 with StormStatementRecorder() as recorder:
443 view.initialize()
444 self.assertThat(recorder, HasQueryCount(LessThan(7)))
445
446 def test_view_write_enabled_without_feature_flag(self):
447 # Test that sharing_write_enabled is not set without the feature flag.
448 with FeatureFixture(ENABLED_FLAG):
449 login_person(self.owner)
450 view = create_initialized_view(self.pillar, name='+sharing')
451 cache = IJSONRequestCache(view.request)
452 self.assertFalse(cache.objects.get('sharing_write_enabled'))
453
454 def test_view_write_enabled_with_feature_flag(self):
455 # Test that sharing_write_enabled is set when required.
456 with FeatureFixture(WRITE_FLAG):
457 view = create_initialized_view(self.pillar, name='+sharing')
458 cache = IJSONRequestCache(view.request)
459 self.assertFalse(cache.objects.get('sharing_write_enabled'))
460 login_person(self.owner)
461 view = create_initialized_view(self.pillar, name='+sharing')
462 cache = IJSONRequestCache(view.request)
463 self.assertTrue(cache.objects.get('sharing_write_enabled'))
464>>>>>>> MERGE-SOURCE
465271
466272
467class TestProductSharingView(PillarSharingViewTestMixin,273class TestProductSharingView(PillarSharingViewTestMixin,
468274
=== modified file 'lib/lp/registry/enums.py'
--- lib/lp/registry/enums.py 2012-03-29 14:36:36 +0000
+++ lib/lp/registry/enums.py 2012-03-29 00:48:21 +0000
@@ -9,16 +9,10 @@
9 'DistroSeriesDifferenceType',9 'DistroSeriesDifferenceType',
10 'InformationType',10 'InformationType',
11 'PersonTransferJobType',11 'PersonTransferJobType',
12<<<<<<< TREE
13 'PRIVATE_INFORMATION_TYPES',12 'PRIVATE_INFORMATION_TYPES',
14 'PUBLIC_INFORMATION_TYPES',13 'PUBLIC_INFORMATION_TYPES',
15 'ProductJobType',14 'ProductJobType',
16 'SECURITY_INFORMATION_TYPES',15 'SECURITY_INFORMATION_TYPES',
17=======
18 'PRIVATE_INFORMATION_TYPES',
19 'ProductJobType',
20 'SECURITY_INFORMATION_TYPES',
21>>>>>>> MERGE-SOURCE
22 'SharingPermission',16 'SharingPermission',
23 ]17 ]
2418
@@ -70,7 +64,6 @@
70 """)64 """)
7165
7266
73<<<<<<< TREE
74PUBLIC_INFORMATION_TYPES = (67PUBLIC_INFORMATION_TYPES = (
75 InformationType.PUBLIC, InformationType.UNEMBARGOEDSECURITY)68 InformationType.PUBLIC, InformationType.UNEMBARGOEDSECURITY)
7669
@@ -84,17 +77,6 @@
84 InformationType.UNEMBARGOEDSECURITY, InformationType.EMBARGOEDSECURITY)77 InformationType.UNEMBARGOEDSECURITY, InformationType.EMBARGOEDSECURITY)
8578
8679
87=======
88PRIVATE_INFORMATION_TYPES = (
89 InformationType.EMBARGOEDSECURITY, InformationType.USERDATA,
90 InformationType.PROPRIETARY)
91
92
93SECURITY_INFORMATION_TYPES = (
94 InformationType.UNEMBARGOEDSECURITY, InformationType.EMBARGOEDSECURITY)
95
96
97>>>>>>> MERGE-SOURCE
98class SharingPermission(DBEnumeratedType):80class SharingPermission(DBEnumeratedType):
99 """Sharing permission.81 """Sharing permission.
10082
10183
=== modified file 'lib/lp/registry/interfaces/accesspolicy.py'
--- lib/lp/registry/interfaces/accesspolicy.py 2012-03-29 14:36:36 +0000
+++ lib/lp/registry/interfaces/accesspolicy.py 2012-03-22 09:00:36 +0000
@@ -215,7 +215,6 @@
215 """Experimental query utility to search through the flattened schema."""215 """Experimental query utility to search through the flattened schema."""
216216
217 def findGranteesByPolicy(policies):217 def findGranteesByPolicy(policies):
218<<<<<<< TREE
219 """Find teams or users with access grants for the policies.218 """Find teams or users with access grants for the policies.
220219
221 This includes grants for artifacts in the policies.220 This includes grants for artifacts in the policies.
@@ -237,29 +236,6 @@
237 ALL means the person has an access policy grant and can see all236 ALL means the person has an access policy grant and can see all
238 artifacts for the associated pillar.237 artifacts for the associated pillar.
239 SOME means the person only has specified access artifact grants.238 SOME means the person only has specified access artifact grants.
240=======
241 """Find teams or users with access grants for the policies.
242
243 This includes grants for artifacts in the policies.
244
245 :param policies: a collection of `IAccesPolicy`s.
246 :return: a collection of `IPerson`.
247 """
248
249 def findGranteePermissionsByPolicy(policies, grantees=None):
250 """Find teams or users with access grants for the policies.
251
252 This includes grants for artifacts in the policies.
253
254 :param policies: a collection of `IAccesPolicy`s.
255 :param grantees: if not None, the result only includes people in the
256 specified list of grantees.
257 :return: a collection of (`IPerson`, `IAccessPolicy`, permission)
258 where permission is a SharingPermission value.
259 'ALL' means the person has an access policy grant and can see all
260 artifacts for the associated pillar.
261 'SOME' means the person only has specified access artifact grants.
262>>>>>>> MERGE-SOURCE
263 """239 """
264240
265 def findArtifactsByGrantee(grantee, policies):241 def findArtifactsByGrantee(grantee, policies):
266242
=== modified file 'lib/lp/registry/interfaces/productjob.py'
--- lib/lp/registry/interfaces/productjob.py 2012-03-29 14:36:36 +0000
+++ lib/lp/registry/interfaces/productjob.py 2012-03-24 12:41:36 +0000
@@ -1,4 +1,3 @@
1<<<<<<< TREE
2# Copyright 2012 Canonical Ltd. This software is licensed under the1# Copyright 2012 Canonical Ltd. This software is licensed under the
3# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
43
@@ -120,72 +119,3 @@
120 :param reply_to_commercial: Set the reply_to property to the119 :param reply_to_commercial: Set the reply_to property to the
121 commercial email address.120 commercial email address.
122 """121 """
123=======
124# Copyright 2012 Canonical Ltd. This software is licensed under the
125# GNU Affero General Public License version 3 (see the file LICENSE).
126
127"""Interfaces for the Jobs system to update products and send notifications."""
128
129__metaclass__ = type
130__all__ = [
131 'IProductJob',
132 'IProductJobSource',
133 ]
134
135from zope.interface import Attribute
136from zope.schema import (
137 Int,
138 Object,
139 )
140
141from lp import _
142from lp.registry.interfaces.product import IProduct
143
144from lp.services.job.interfaces.job import (
145 IJob,
146 IJobSource,
147 IRunnableJob,
148 )
149
150
151class IProductJob(IRunnableJob):
152 """A Job related to an `IProduct`."""
153
154 id = Int(
155 title=_('DB ID'), required=True, readonly=True,
156 description=_("The tracking number of this job."))
157
158 job = Object(
159 title=_('The common Job attributes'),
160 schema=IJob,
161 required=True)
162
163 product = Object(
164 title=_('The product the job is for'),
165 schema=IProduct,
166 required=True)
167
168 metadata = Attribute('A dict of data for the job')
169
170
171class IProductJobSource(IJobSource):
172 """An interface for creating and finding `IProductJob`s."""
173
174 def create(product, metadata):
175 """Create a new `IProductJob`.
176
177 :param product: An IProduct.
178 :param metadata: a dict of configuration data for the job.
179 The data must be JSON compatible keys and values.
180 """
181
182 def find(product=None, date_since=None, job_type=None):
183 """Find `IProductJob`s that match the specified criteria.
184
185 :param product: Match jobs for specific product.
186 :param date_since: Match jobs since the specified date.
187 :param job_type: Match jobs of a specific type. Type is expected
188 to be a class name.
189 :return: A `ResultSet` yielding `IProductJob`.
190 """
191>>>>>>> MERGE-SOURCE
192122
=== modified file 'lib/lp/registry/interfaces/sharingservice.py'
--- lib/lp/registry/interfaces/sharingservice.py 2012-03-29 14:36:36 +0000
+++ lib/lp/registry/interfaces/sharingservice.py 2012-03-26 14:10:32 +0000
@@ -59,7 +59,6 @@
59 def getPillarSharees(pillar):59 def getPillarSharees(pillar):
60 """Return people/teams who can see pillar artifacts."""60 """Return people/teams who can see pillar artifacts."""
6161
62<<<<<<< TREE
63 @export_read_operation()62 @export_read_operation()
64 @operation_parameters(63 @operation_parameters(
65 pillar=Reference(IPillar, title=_('Pillar'), required=True))64 pillar=Reference(IPillar, title=_('Pillar'), required=True))
@@ -83,20 +82,6 @@
83 - permissions they have for each information type.82 - permissions they have for each information type.
84 """83 """
8584
86=======
87 @export_read_operation()
88 @operation_parameters(
89 pillar=Reference(IPillar, title=_('Pillar'), required=True))
90 @operation_for_version('devel')
91 def getPillarShareeData(pillar, grantees=None):
92 """Return people/teams who can see pillar artifacts.
93
94 The result records are json data which includes:
95 - person name
96 - permissions they have for each information type.
97 """
98
99>>>>>>> MERGE-SOURCE
100 @export_write_operation()85 @export_write_operation()
101 @call_with(user=REQUEST_USER)86 @call_with(user=REQUEST_USER)
102 @operation_parameters(87 @operation_parameters(
10388
=== modified file 'lib/lp/registry/javascript/sharing/pillarsharingview.js'
--- lib/lp/registry/javascript/sharing/pillarsharingview.js 2012-03-29 14:36:36 +0000
+++ lib/lp/registry/javascript/sharing/pillarsharingview.js 2012-03-26 01:11:56 +0000
@@ -103,16 +103,10 @@
103 var otns = Y.lp.registry.sharing.shareetable;103 var otns = Y.lp.registry.sharing.shareetable;
104 var sharee_table = new otns.ShareeTableWidget({104 var sharee_table = new otns.ShareeTableWidget({
105 sharees: sharee_data,105 sharees: sharee_data,
106<<<<<<< TREE
107 sharing_permissions:106 sharing_permissions:
108 this.get('sharing_permissions_by_value'),107 this.get('sharing_permissions_by_value'),
109 information_types: this.get('information_types_by_value'),108 information_types: this.get('information_types_by_value'),
110 write_enabled: this.get('write_enabled')109 write_enabled: this.get('write_enabled')
111=======
112 sharing_permissions: sharing_permissions,
113 information_types: this.get('information_types_by_value'),
114 write_enabled: this.get('write_enabled')
115>>>>>>> MERGE-SOURCE
116 });110 });
117 this.set('sharee_table', sharee_table);111 this.set('sharee_table', sharee_table);
118 sharee_table.render();112 sharee_table.render();
119113
=== modified file 'lib/lp/registry/javascript/sharing/shareepicker.js'
--- lib/lp/registry/javascript/sharing/shareepicker.js 2012-03-29 14:36:36 +0000
+++ lib/lp/registry/javascript/sharing/shareepicker.js 2012-03-28 00:44:36 +0000
@@ -51,12 +51,8 @@
51 }51 }
52 }52 }
53 this.set('information_types', information_types);53 this.set('information_types', information_types);
54<<<<<<< TREE
55 this.set('sharing_permissions', sharing_permissions);54 this.set('sharing_permissions', sharing_permissions);
56 this.step_one_header = this.get('headerContent');55 this.step_one_header = this.get('headerContent');
57=======
58 this.set('sharing_permissions', sharing_permissions);
59>>>>>>> MERGE-SOURCE
60 var self = this;56 var self = this;
61 this.subscribe('save', function (e) {57 this.subscribe('save', function (e) {
62 e.halt();58 e.halt();
@@ -173,67 +169,10 @@
173 var step_one_content = contentBox.one('.yui3-widget-bd');169 var step_one_content = contentBox.one('.yui3-widget-bd');
174 var step_two_content = contentBox.one('.picker-content-two');170 var step_two_content = contentBox.one('.picker-content-two');
175 if (step_two_content === null) {171 if (step_two_content === null) {
176<<<<<<< TREE
177 step_two_content = this._render_step_two(172 step_two_content = this._render_step_two(
178 data.back_enabled, data.allowed_permissions);173 data.back_enabled, data.allowed_permissions);
179=======
180 var step_two_html = [
181 '<div class="picker-content-two transparent">',
182 '<div class="step-links">',
183 '<a class="prev js-action" href="#">Back</a>',
184 '<button class="next lazr-pos lazr-btn"></button>',
185 '<a class="next js-action" href="#">Select</a>',
186 '</div></div>'
187 ].join(' ');
188 step_two_content = Y.Node.create(step_two_html);
189 var self = this;
190 // Remove the back link if required.
191 if (Y.Lang.isBoolean(data.back_enabled)
192 && !data.back_enabled ) {
193 step_two_content.one('a.prev').remove(true);
194 } else {
195 step_two_content.one('a.prev').on('click', function(e) {
196 e.halt();
197 self._display_step_one();
198 });
199 }
200 // Wire up the next (ie submit) links.
201 step_two_content.all('.next').on('click', function(e) {
202 e.halt();
203 // Only submit if at least one info type is selected.
204 if (!self._all_info_choices_unticked(step_two_content)) {
205 self.fire('save', data, 2);
206 }
207 });
208 // By default, we only show All or Nothing.
209 var allowed_permissions = ['ALL', 'NOTHING'];
210 if (Y.Lang.isValue(data.allowed_permissions)) {
211 allowed_permissions = data.allowed_permissions;
212 }
213 var sharing_permissions = [];
214 Y.Array.each(this.get('sharing_permissions'),
215 function(permission) {
216 if (Y.Array.indexOf(
217 allowed_permissions, permission.value) >=0) {
218 sharing_permissions.push(permission);
219 }
220 });
221 var policy_selector = self._make_policy_selector(
222 sharing_permissions);
223 step_two_content.one('div.step-links')
224 .insert(policy_selector, 'before');
225>>>>>>> MERGE-SOURCE
226 step_one_content.insert(step_two_content, 'after');174 step_one_content.insert(step_two_content, 'after');
227<<<<<<< TREE
228=======
229 step_two_content.all('input[name^=field.permission]')
230 .on('click', function(e) {
231 self._disable_select_if_all_info_choices_unticked(
232 step_two_content);
233 });
234>>>>>>> MERGE-SOURCE
235 }175 }
236<<<<<<< TREE
237 // Wire up the next (ie submit) links.176 // Wire up the next (ie submit) links.
238 step_two_content.detach('click');177 step_two_content.detach('click');
239 step_two_content.delegate('click', function(e) {178 step_two_content.delegate('click', function(e) {
@@ -252,17 +191,6 @@
252 // sharee_permissions.191 // sharee_permissions.
253 if (Y.Lang.isObject(data.sharee_permissions)) {192 if (Y.Lang.isObject(data.sharee_permissions)) {
254 Y.each(data.sharee_permissions, function(perm, type) {193 Y.each(data.sharee_permissions, function(perm, type) {
255=======
256 // Initially set all radio buttons to Nothing.
257 step_two_content.all('input[name^=field.permission][value=NOTHING]')
258 .each(function(radio_button) {
259 radio_button.set('checked', true);
260 });
261 // Ensure the correct radio buttons are ticked according to the
262 // sharee_permissions.
263 if (Y.Lang.isObject(data.sharee_permissions)) {
264 Y.each(data.sharee_permissions, function(perm, type) {
265>>>>>>> MERGE-SOURCE
266 var cb = step_two_content.one(194 var cb = step_two_content.one(
267 'input[name=field.permission.'+type+']' +195 'input[name=field.permission.'+type+']' +
268 '[value="' + perm + '"]');196 '[value="' + perm + '"]');
@@ -326,7 +254,6 @@
326 this.fire('save', data, 0);254 this.fire('save', data, 0);
327 },255 },
328256
329<<<<<<< TREE
330 _sharing_permission_template: function() {257 _sharing_permission_template: function() {
331 return [258 return [
332 '<table class="radio-button-widget"><tbody>',259 '<table class="radio-button-widget"><tbody>',
@@ -348,29 +275,6 @@
348 },275 },
349276
350 _make_policy_selector: function(allowed_permissions) {277 _make_policy_selector: function(allowed_permissions) {
351=======
352 _sharing_permission_template: function() {
353 return [
354 '<table class="radio-button-widget"><tbody>',
355 '{{#permissions}}',
356 '<tr>',
357 ' <input type="radio"',
358 ' value="{{value}}"',
359 ' name="field.permission.{{info_type}}"',
360 ' id="field.permission.{{info_type}}.{{index}}"',
361 ' class="radioType">',
362 ' <label for="field.permission.{{info_type}}"',
363 ' title="{{description}}">',
364 ' {{title}}',
365 ' </label>',
366 '</tr>',
367 '{{/permissions}}',
368 '</tbody></table>'
369 ].join('');
370 },
371
372 _make_policy_selector: function(allowed_permissions) {
373>>>>>>> MERGE-SOURCE
374 // The policy selector is a set of radio buttons.278 // The policy selector is a set of radio buttons.
375 var sharing_permissions_template = this._sharing_permission_template();279 var sharing_permissions_template = this._sharing_permission_template();
376 var html = Y.lp.mustache.to_html([280 var html = Y.lp.mustache.to_html([
377281
=== modified file 'lib/lp/registry/javascript/sharing/shareetable.js'
--- lib/lp/registry/javascript/sharing/shareetable.js 2012-03-29 14:36:36 +0000
+++ lib/lp/registry/javascript/sharing/shareetable.js 2012-03-28 03:50:33 +0000
@@ -49,7 +49,7 @@
49 },49 },
50 // The sharing permission choices: all, some, nothing etc.50 // The sharing permission choices: all, some, nothing etc.
51 sharing_permissions: {51 sharing_permissions: {
52 value: []52 value: {}
53 },53 },
54 // The node holding the sharee table.54 // The node holding the sharee table.
55 sharee_table: {55 sharee_table: {
@@ -273,25 +273,11 @@
273 },273 },
274274
275 renderUI: function() {275 renderUI: function() {
276<<<<<<< TREE276 this._render_sharees(this.get('sharees'));
277 this._render_sharees(this.get('sharees'));277 },
278 },278
279279 _render_sharees: function(sharees) {
280 _render_sharees: function(sharees) {280 var sharee_table = this.get('sharee_table');
281 var sharee_table = this.get('sharee_table');
282=======
283 this._render_sharees(this.get('sharees'));
284 },
285
286 _render_sharees: function(sharees) {
287 var sharee_table = this.get('sharee_table');
288 if (sharees.length === 0) {
289 sharee_table.one('tr#sharee-table-loading td')
290 .setContent("This project's private information is " +
291 "not shared with anyone.");
292 return;
293 }
294>>>>>>> MERGE-SOURCE
295 var partials = {281 var partials = {
296 sharee_access_policies:282 sharee_access_policies:
297 this.get('sharee_policy_template'),283 this.get('sharee_policy_template'),
@@ -302,7 +288,6 @@
302 this.get('sharee_table_template'),288 this.get('sharee_table_template'),
303 {sharees: sharees}, partials);289 {sharees: sharees}, partials);
304 var table_node = Y.Node.create(html);290 var table_node = Y.Node.create(html);
305<<<<<<< TREE
306 if (sharees.length === 0) {291 if (sharees.length === 0) {
307 table_node.one('tbody').appendChild(292 table_node.one('tbody').appendChild(
308 Y.Node.create(this.get('sharee_table_empty_row')));293 Y.Node.create(this.get('sharee_table_empty_row')));
@@ -311,12 +296,6 @@
311 this.render_sharing_info(sharees);296 this.render_sharing_info(sharees);
312 this._update_editable_status();297 this._update_editable_status();
313 this.set('sharees', sharees);298 this.set('sharees', sharees);
314=======
315 sharee_table.replace(table_node);
316 this.render_sharing_info(sharees);
317 this._update_editable_status();
318 this.set('sharees', sharees);
319>>>>>>> MERGE-SOURCE
320 },299 },
321300
322 bindUI: function() {301 bindUI: function() {
@@ -521,7 +500,6 @@
521 var rows_to_delete = sharee_table.all(deleted_row_selectors.join(','));500 var rows_to_delete = sharee_table.all(deleted_row_selectors.join(','));
522 var delete_rows = function() {501 var delete_rows = function() {
523 rows_to_delete.remove(true);502 rows_to_delete.remove(true);
524<<<<<<< TREE
525 if (all_rows_deleted === true) {503 if (all_rows_deleted === true) {
526 sharee_table.one('tbody')504 sharee_table.one('tbody')
527 .appendChild('<tr id="sharee-table-not-shared"></tr>')505 .appendChild('<tr id="sharee-table-not-shared"></tr>')
@@ -529,15 +507,6 @@
529 .setContent("This project's private information " +507 .setContent("This project's private information " +
530 "is not shared with anyone.");508 "is not shared with anyone.");
531 }509 }
532=======
533 if (all_rows_deleted === true) {
534 sharee_table.one('tbody')
535 .appendChild('<tr id="sharee-table-loading"></tr>')
536 .appendChild('<td></td>')
537 .setContent("This project's private information " +
538 "is not shared with anyone.");
539 }
540>>>>>>> MERGE-SOURCE
541 };510 };
542 var anim_duration = this.get('anim_duration');511 var anim_duration = this.get('anim_duration');
543 if (anim_duration === 0 ) {512 if (anim_duration === 0 ) {
544513
=== modified file 'lib/lp/registry/javascript/sharing/tests/test_shareelisting_navigator.html'
--- lib/lp/registry/javascript/sharing/tests/test_shareelisting_navigator.html 2012-03-29 14:36:36 +0000
+++ lib/lp/registry/javascript/sharing/tests/test_shareelisting_navigator.html 2012-03-28 03:50:33 +0000
@@ -1,4 +1,3 @@
1<<<<<<< TREE
2<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"1<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
3 "http://www.w3.org/TR/html4/strict.dtd">2 "http://www.w3.org/TR/html4/strict.dtd">
4<!--3<!--
@@ -61,67 +60,3 @@
61 </script>60 </script>
62 </body>61 </body>
63</html>62</html>
64=======
65<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
66 "http://www.w3.org/TR/html4/strict.dtd">
67<!--
68Copyright 2012 Canonical Ltd. This software is licensed under the
69GNU Affero General Public License version 3 (see the file LICENSE).
70-->
71
72<html>
73 <head>
74 <title>Sharee Listing Navigator Tests</title>
75
76 <!-- YUI and test setup -->
77 <script type="text/javascript"
78 src="../../../../../../build/js/yui/yui/yui.js">
79 </script>
80 <link rel="stylesheet"
81 href="../../../../../../build/js/yui/console/assets/console-core.css" />
82 <link rel="stylesheet"
83 href="../../../../../../build/js/yui/console/assets/skins/sam/console.css" />
84 <link rel="stylesheet"
85 href="../../../../../../build/js/yui/test/assets/skins/sam/test.css" />
86
87 <script type="text/javascript"
88 src="../../../../../../build/js/lp/app/testing/testrunner.js"></script>
89
90 <link rel="stylesheet" href="../../../../app/javascript/testing/test.css" />
91
92 <!-- Dependencies -->
93 <script type="text/javascript"
94 src="../../../../../../build/js/lp/app/client.js"></script>
95 <script type="text/javascript"
96 src="../../../../../../build/js/lp/app/lp.js"></script>
97 <script type="text/javascript"
98 src="../../../../../../build/js/lp/app/mustache.js"></script>
99 <script type="text/javascript"
100 src="../../../../../../build/js/lp/app/indicator/indicator.js"></script>
101 <script type="text/javascript"
102 src="../../../../../../build/js/lp/app/overlay/overlay.js"></script>
103 <script type="text/javascript"
104 src="../../../../../../build/js/lp/app/listing_navigator.js"></script>
105
106 <!-- The module under test. -->
107 <script type="text/javascript" src="../shareelisting_navigator.js"></script>
108
109 <!-- The test suite -->
110 <script type="text/javascript" src="test_shareelisting_navigator.js"></script>
111
112 </head>
113 <body class="yui3-skin-sam">
114 <ul id="suites">
115 <li>lp.registry.sharing.shareelisting_navigator.test</li>
116 </ul>
117 <div id="fixture"></div>
118 <script type="text/x-template" id="sharee-table-template">
119 <table id='sharee-table'>
120 <tr id='sharee-table-loading'><td>
121 Loading...
122 </td></tr>
123 </table>
124 </script>
125 </body>
126</html>
127>>>>>>> MERGE-SOURCE
12863
=== modified file 'lib/lp/registry/javascript/sharing/tests/test_shareepicker.js'
--- lib/lp/registry/javascript/sharing/tests/test_shareepicker.js 2012-03-29 14:36:36 +0000
+++ lib/lp/registry/javascript/sharing/tests/test_shareepicker.js 2012-03-27 02:27:21 +0000
@@ -181,7 +181,6 @@
181 // Check the title and step title are correct.181 // Check the title and step title are correct.
182 var steptitle = cb.one('.contains-steptitle h2').getContent();182 var steptitle = cb.one('.contains-steptitle h2').getContent();
183 Y.Assert.areEqual(183 Y.Assert.areEqual(
184<<<<<<< TREE
185 'Update sharing policies',184 'Update sharing policies',
186 this.picker.get('headerContent').get('text'));185 this.picker.get('headerContent').get('text'));
187 Y.Assert.areEqual(186 Y.Assert.areEqual(
@@ -191,14 +190,6 @@
191 Y.Assert.isNotNull(cb.one('input[value=ALL]'));190 Y.Assert.isNotNull(cb.one('input[value=ALL]'));
192 Y.Assert.isNotNull(cb.one('input[value=NOTHING]'));191 Y.Assert.isNotNull(cb.one('input[value=NOTHING]'));
193 Y.Assert.isNull(cb.one('input[value=SOME]'));192 Y.Assert.isNull(cb.one('input[value=SOME]'));
194=======
195 'Select sharing policies for Fred', steptitle);
196 // By default, selections only for ALL and NOTHING are available
197 // (and no others).
198 Y.Assert.isNotNull(cb.one('input[value=ALL]'));
199 Y.Assert.isNotNull(cb.one('input[value=NOTHING]'));
200 Y.Assert.isNull(cb.one('input[value=SOME]'));
201>>>>>>> MERGE-SOURCE
202 // Selected permission checkboxes should be ticked.193 // Selected permission checkboxes should be ticked.
203 cb.all('input[name=field.permission.P1]')194 cb.all('input[name=field.permission.P1]')
204 .each(function(node) {195 .each(function(node) {
205196
=== modified file 'lib/lp/registry/javascript/sharing/tests/test_shareetable.html'
--- lib/lp/registry/javascript/sharing/tests/test_shareetable.html 2012-03-29 14:36:36 +0000
+++ lib/lp/registry/javascript/sharing/tests/test_shareetable.html 2012-03-28 03:50:33 +0000
@@ -73,7 +73,6 @@
73 <ul id="suites">73 <ul id="suites">
74 <li>lp.registry.sharing.shareetable.test</li>74 <li>lp.registry.sharing.shareetable.test</li>
75 </ul>75 </ul>
76<<<<<<< TREE
77 <div id="fixture"></div>76 <div id="fixture"></div>
78 <script type="text/x-template" id="sharee-table-template">77 <script type="text/x-template" id="sharee-table-template">
79 <table id='sharee-table'>78 <table id='sharee-table'>
@@ -82,15 +81,5 @@
82 </td></tr>81 </td></tr>
83 </table>82 </table>
84 </script>83 </script>
85=======
86 <div id="fixture"></div>
87 <script type="text/x-template" id="sharee-table-template">
88 <table id='sharee-table'>
89 <tr id='sharee-table-loading'><td>
90 Loading...
91 </td></tr>
92 </table>
93 </script>
94>>>>>>> MERGE-SOURCE
95 </body>84 </body>
96</html>85</html>
9786
=== modified file 'lib/lp/registry/javascript/sharing/tests/test_shareetable.js'
--- lib/lp/registry/javascript/sharing/tests/test_shareetable.js 2012-03-29 14:36:36 +0000
+++ lib/lp/registry/javascript/sharing/tests/test_shareetable.js 2012-03-28 03:50:33 +0000
@@ -25,7 +25,6 @@
25 ]25 ]
26 }26 }
27 };27 };
28<<<<<<< TREE
29 this.sharing_permissions = {28 this.sharing_permissions = {
30 s1: 'S1',29 s1: 'S1',
31 s2: 'S2'30 s2: 'S2'
@@ -40,27 +39,13 @@
40 Y.one('#sharee-table-template').getContent());39 Y.one('#sharee-table-template').getContent());
41 this.fixture.appendChild(sharee_table);40 this.fixture.appendChild(sharee_table);
4241
43=======
44 this.fixture = Y.one('#fixture');
45 var sharee_table = Y.Node.create(
46 Y.one('#sharee-table-template').getContent());
47 this.fixture.appendChild(sharee_table);
48
49>>>>>>> MERGE-SOURCE
50 },42 },
5143
52 tearDown: function () {44 tearDown: function () {
53<<<<<<< TREE
54 if (this.fixture !== null) {45 if (this.fixture !== null) {
55 this.fixture.empty(true);46 this.fixture.empty(true);
56 }47 }
57 delete this.fixture;48 delete this.fixture;
58=======
59 if (this.fixture !== null) {
60 this.fixture.empty();
61 }
62 delete this.fixture;
63>>>>>>> MERGE-SOURCE
64 delete window.LP;49 delete window.LP;
65 },50 },
6651
@@ -72,7 +57,6 @@
72 sharee_table: Y.one('#sharee-table'),57 sharee_table: Y.one('#sharee-table'),
73 anim_duration: 0,58 anim_duration: 0,
74 sharees: window.LP.cache.sharee_data,59 sharees: window.LP.cache.sharee_data,
75<<<<<<< TREE
76 sharing_permissions: this.sharing_permissions,60 sharing_permissions: this.sharing_permissions,
77 information_types: this.information_types,61 information_types: this.information_types,
78 write_enabled: true62 write_enabled: true
@@ -80,14 +64,6 @@
80 window.LP.cache.sharee_data = config.sharees;64 window.LP.cache.sharee_data = config.sharees;
81 var ns = Y.lp.registry.sharing.shareetable;65 var ns = Y.lp.registry.sharing.shareetable;
82 return new ns.ShareeTableWidget(config);66 return new ns.ShareeTableWidget(config);
83=======
84 sharing_permissions: window.LP.cache.sharing_permissions,
85 information_types: window.LP.cache.information_types,
86 write_enabled: true
87 }, overrides);
88 var ns = Y.lp.registry.sharing.shareetable;
89 return new ns.ShareeTableWidget(config);
90>>>>>>> MERGE-SOURCE
91 },67 },
9268
93 test_library_exists: function () {69 test_library_exists: function () {
@@ -104,7 +80,6 @@
104 "Sharee table failed to be instantiated");80 "Sharee table failed to be instantiated");
105 },81 },
10682
107<<<<<<< TREE
108 // Read only mode disables the correct things.83 // Read only mode disables the correct things.
109 test_readonly: function() {84 test_readonly: function() {
110 this.sharee_table = this._create_Widget({85 this.sharee_table = this._create_Widget({
@@ -146,34 +121,6 @@
146 Y.Assert.isNull(Y.one('tr#sharee-table-not-shared'));121 Y.Assert.isNull(Y.one('tr#sharee-table-not-shared'));
147 },122 },
148123
149=======
150 // Read only mode disables the correct things.
151 test_readonly: function() {
152 this.sharee_table = this._create_Widget({
153 write_enabled: false
154 });
155 this.sharee_table.render();
156 Y.all('#sharee-table ' +
157 '.sprite.add, .sprite.edit, .sprite.remove a')
158 .each(function(link) {
159 Y.Assert.isTrue(link.hasClass('unseen'));
160 });
161 },
162
163 // When there are no sharees, the table contains an informative message.
164 test_no_sharee_message: function() {
165 this.sharee_table = this._create_Widget({
166 sharees: []
167 });
168 this.sharee_table.render();
169 Y.Assert.areEqual(
170 "This project's private information is not shared " +
171 "with anyone.",
172 Y.one('#sharee-table tr#sharee-table-loading td')
173 .getContent());
174 },
175
176>>>>>>> MERGE-SOURCE
177 // The given sharee is correctly rendered.124 // The given sharee is correctly rendered.
178 _test_sharee_rendered: function(sharee) {125 _test_sharee_rendered: function(sharee) {
179 // The sharee row126 // The sharee row
@@ -344,7 +291,6 @@
344 this.sharee_table.syncUI();291 this.sharee_table.syncUI();
345 // Check the results.292 // Check the results.
346 var self = this;293 var self = this;
347<<<<<<< TREE
348 Y.Array.each(sharee_data, function(sharee) {294 Y.Array.each(sharee_data, function(sharee) {
349 self._test_sharee_rendered(sharee);295 self._test_sharee_rendered(sharee);
350 });296 });
@@ -400,63 +346,6 @@
400 'permissions': {'P1': 's2'}};346 'permissions': {'P1': 's2'}};
401 this.sharee_table.navigator.fire('updateContent', [new_sharee]);347 this.sharee_table.navigator.fire('updateContent', [new_sharee]);
402 this._test_sharee_rendered(new_sharee);348 this._test_sharee_rendered(new_sharee);
403=======
404 Y.Array.each(sharee_data, function(sharee) {
405 self._test_sharee_rendered(sharee);
406 });
407 var deleted_row = '#sharee-table tr[id=permission-fred]';
408 Y.Assert.isNull(Y.one(deleted_row));
409 },
410
411 // The navigator model total attribute is updated when the currently
412 // displayed sharee data changes.
413 test_navigation_totals_updated: function() {
414 this.sharee_table = this._create_Widget();
415 this.sharee_table.render();
416 // We manipulate the cached model data - delete, add and update
417 var sharee_data = window.LP.cache.sharee_data;
418 // Insert a new record.
419 var new_sharee = {
420 'name': 'joe', 'display_name': 'Joe Smith',
421 'role': '(Maintainer)', web_link: '~joe',
422 'self_link': '~joe',
423 'permissions': {'P1': 's2'}};
424 sharee_data.splice(0, 0, new_sharee);
425 this.sharee_table.syncUI();
426 // Check the results.
427 Y.Assert.areEqual(
428 3, this.sharee_table.navigator.get('model').get('total'));
429 },
430
431 // When all rows are deleted, the table contains an informative message.
432 test_delete_all: function() {
433 this.sharee_table = this._create_Widget();
434 this.sharee_table.render();
435 // We manipulate the cached model data.
436 var sharee_data = window.LP.cache.sharee_data;
437 // Delete all the records.
438 sharee_data.splice(0, 2);
439 this.sharee_table.syncUI();
440 // Check the results.
441 Y.Assert.areEqual(
442 "This project's private information is not shared " +
443 "with anyone.",
444 Y.one('#sharee-table tr#sharee-table-loading td')
445 .getContent());
446 },
447
448 // A batch update is correctly rendered.
449 test_navigator_content_update: function() {
450 this.sharee_table = this._create_Widget();
451 this.sharee_table.render();
452 var new_sharee = {
453 'name': 'joe', 'display_name': 'Joe Smith',
454 'role': '(Maintainer)', web_link: '~joe',
455 'self_link': '~joe',
456 'permissions': {'P1': 's2'}};
457 this.sharee_table.navigator.fire('updateContent', [new_sharee]);
458 this._test_sharee_rendered(new_sharee);
459>>>>>>> MERGE-SOURCE
460 }349 }
461 }));350 }));
462351
463352
=== modified file 'lib/lp/registry/model/accesspolicy.py'
--- lib/lp/registry/model/accesspolicy.py 2012-03-29 14:36:36 +0000
+++ lib/lp/registry/model/accesspolicy.py 2012-03-23 04:12:59 +0000
@@ -18,12 +18,8 @@
18 And,18 And,
19 In,19 In,
20 Or,20 Or,
21<<<<<<< TREE
22 Select,21 Select,
23 SQL,22 SQL,
24=======
25 SQL,
26>>>>>>> MERGE-SOURCE
27 )23 )
28from storm.properties import (24from storm.properties import (
29 DateTime,25 DateTime,
@@ -359,7 +355,6 @@
359 Person, Person.id == cls.grantee_id, cls.policy_id.is_in(ids))355 Person, Person.id == cls.grantee_id, cls.policy_id.is_in(ids))
360356
361 @classmethod357 @classmethod
362<<<<<<< TREE
363 def findGranteePermissionsByPolicy(cls, policies, grantees=None):358 def findGranteePermissionsByPolicy(cls, policies, grantees=None):
364 """See `IAccessPolicyGrantFlatSource`."""359 """See `IAccessPolicyGrantFlatSource`."""
365 policies_by_id = dict((policy.id, policy) for policy in policies)360 policies_by_id = dict((policy.id, policy) for policy in policies)
@@ -409,30 +404,6 @@
409 result_decorator=set_permission, pre_iter_hook=load_permissions)404 result_decorator=set_permission, pre_iter_hook=load_permissions)
410405
411 @classmethod406 @classmethod
412=======
413 def findGranteePermissionsByPolicy(cls, policies, grantees=None):
414 """See `IAccessPolicyGrantFlatSource`."""
415 ids = [policy.id for policy in policies]
416 sharing_permission_term = SQL("""
417 CASE(
418 MIN(COALESCE(artifact, 0)))
419 WHEN 0 THEN 'ALL'
420 ELSE 'SOME'
421 END
422 """)
423 constraints = [
424 Person.id == cls.grantee_id,
425 AccessPolicy.id == cls.policy_id,
426 cls.policy_id.is_in(ids)]
427 if grantees:
428 grantee_ids = [grantee.id for grantee in grantees]
429 constraints.append(cls.grantee_id.is_in(grantee_ids))
430 return IStore(cls).find(
431 (Person, AccessPolicy, sharing_permission_term),
432 *constraints).group_by(Person, AccessPolicy)
433
434 @classmethod
435>>>>>>> MERGE-SOURCE
436 def findArtifactsByGrantee(cls, grantee, policies):407 def findArtifactsByGrantee(cls, grantee, policies):
437 """See `IAccessPolicyGrantFlatSource`."""408 """See `IAccessPolicyGrantFlatSource`."""
438 ids = [policy.id for policy in policies]409 ids = [policy.id for policy in policies]
439410
=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2012-03-29 14:36:36 +0000
+++ lib/lp/registry/model/person.py 2012-03-29 06:02:46 +0000
@@ -1850,24 +1850,16 @@
1850 Bug,1850 Bug,
1851 Join(BugSubscription, BugSubscription.bug_id == Bug.id)),1851 Join(BugSubscription, BugSubscription.bug_id == Bug.id)),
1852 where=And(1852 where=And(
1853<<<<<<< TREE
1854 Bug.information_type.is_in(PRIVATE_INFORMATION_TYPES),1853 Bug.information_type.is_in(PRIVATE_INFORMATION_TYPES),
1855=======
1856 Bug._private == True,
1857>>>>>>> MERGE-SOURCE
1858 BugSubscription.person_id == self.id)),1854 BugSubscription.person_id == self.id)),
1859 Select(1855 Select(
1860 Bug.id,1856 Bug.id,
1861 tables=(1857 tables=(
1862 Bug,1858 Bug,
1863 Join(BugTask, BugTask.bugID == Bug.id)),1859 Join(BugTask, BugTask.bugID == Bug.id)),
1864<<<<<<< TREE
1865 where=And(Bug.information_type.is_in(1860 where=And(Bug.information_type.is_in(
1866 PRIVATE_INFORMATION_TYPES),1861 PRIVATE_INFORMATION_TYPES),
1867 BugTask.assignee == self.id)),1862 BugTask.assignee == self.id)),
1868=======
1869 where=And(Bug._private == True, BugTask.assignee == self.id)),
1870>>>>>>> MERGE-SOURCE
1871 limit=1))1863 limit=1))
1872 if private_bugs_involved.rowcount:1864 if private_bugs_involved.rowcount:
1873 raise TeamSubscriptionPolicyError(1865 raise TeamSubscriptionPolicyError(
18741866
=== modified file 'lib/lp/registry/model/productjob.py'
--- lib/lp/registry/model/productjob.py 2012-03-29 14:36:36 +0000
+++ lib/lp/registry/model/productjob.py 2012-03-24 12:36:13 +0000
@@ -1,4 +1,3 @@
1<<<<<<< TREE
2# Copyright 2012 Canonical Ltd. This software is licensed under the1# Copyright 2012 Canonical Ltd. This software is licensed under the
3# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
43
@@ -283,156 +282,3 @@
283 'Launchpad', config.canonical.noreply_from_address)282 'Launchpad', config.canonical.noreply_from_address)
284 self.sendEmailToMaintainer(283 self.sendEmailToMaintainer(
285 self.email_template_name, self.subject, from_address)284 self.email_template_name, self.subject, from_address)
286=======
287# Copyright 2012 Canonical Ltd. This software is licensed under the
288# GNU Affero General Public License version 3 (see the file LICENSE).
289
290"""Jobs classes to update products and send notifications."""
291
292__metaclass__ = type
293__all__ = [
294 'ProductJob',
295 ]
296
297from lazr.delegates import delegates
298import simplejson
299from storm.expr import (
300 And,
301 )
302from storm.locals import (
303 Int,
304 Reference,
305 Unicode,
306 )
307from zope.interface import (
308 classProvides,
309 implements,
310 )
311
312from lp.registry.enums import ProductJobType
313from lp.registry.interfaces.product import (
314 IProduct,
315 )
316from lp.registry.interfaces.productjob import (
317 IProductJob,
318 IProductJobSource,
319 )
320from lp.registry.model.product import Product
321from lp.services.database.decoratedresultset import DecoratedResultSet
322from lp.services.database.enumcol import EnumCol
323from lp.services.database.lpstorm import (
324 IMasterStore,
325 IStore,
326 )
327from lp.services.database.stormbase import StormBase
328from lp.services.job.model.job import Job
329from lp.services.job.runner import BaseRunnableJob
330
331
332class ProductJob(StormBase):
333 """Base class for product jobs."""
334
335 implements(IProductJob)
336
337 __storm_table__ = 'ProductJob'
338
339 id = Int(primary=True)
340
341 job_id = Int(name='job')
342 job = Reference(job_id, Job.id)
343
344 product_id = Int(name='product')
345 product = Reference(product_id, Product.id)
346
347 job_type = EnumCol(enum=ProductJobType, notNull=True)
348
349 _json_data = Unicode('json_data')
350
351 @property
352 def metadata(self):
353 return simplejson.loads(self._json_data)
354
355 def __init__(self, product, job_type, metadata):
356 """Constructor.
357
358 :param product: The product the job is for.
359 :param job_type: The type job the product needs run.
360 :param metadata: A dict of JSON-compatible data to pass to the job.
361 """
362 super(ProductJob, self).__init__()
363 self.job = Job()
364 self.product = product
365 self.job_type = job_type
366 json_data = simplejson.dumps(metadata)
367 self._json_data = json_data.decode('utf-8')
368
369
370class ProductJobDerived(BaseRunnableJob):
371 """Intermediate class for deriving from ProductJob.
372
373 Storm classes can't simply be subclassed or you can end up with
374 multiple objects referencing the same row in the db. This class uses
375 lazr.delegates, which is a little bit simpler than storm's
376 inheritance solution to the problem. Subclasses need to override
377 the run() method.
378 """
379
380 delegates(IProductJob)
381 classProvides(IProductJobSource)
382
383 def __init__(self, job):
384 self.context = job
385
386 def __repr__(self):
387 return (
388 "<{self.__class__.__name__} for {self.product.name} "
389 "status={self.job.status}>").format(self=self)
390
391 @classmethod
392 def create(cls, product, metadata):
393 """See `IProductJob`."""
394 if not IProduct.providedBy(product):
395 raise TypeError("Product must be an IProduct: %s" % repr(product))
396 job = ProductJob(
397 product=product, job_type=cls.class_job_type, metadata=metadata)
398 return cls(job)
399
400 @classmethod
401 def find(cls, product, date_since=None, job_type=None):
402 """See `IPersonMergeJobSource`."""
403 conditions = [
404 ProductJob.job_id == Job.id,
405 ProductJob.product == product.id,
406 ]
407 if date_since is not None:
408 conditions.append(
409 Job.date_created >= date_since)
410 if job_type is not None:
411 conditions.append(
412 ProductJob.job_type == job_type)
413 return DecoratedResultSet(
414 IStore(ProductJob).find(
415 ProductJob, *conditions), cls)
416
417 @classmethod
418 def iterReady(cls):
419 """Iterate through all ready ProductJobs."""
420 store = IMasterStore(ProductJob)
421 jobs = store.find(
422 ProductJob,
423 And(ProductJob.job_type == cls.class_job_type,
424 ProductJob.job_id.is_in(Job.ready_jobs)))
425 return (cls(job) for job in jobs)
426
427 @property
428 def log_name(self):
429 return self.__class__.__name__
430
431 def getOopsVars(self):
432 """See `IRunnableJob`."""
433 vars = BaseRunnableJob.getOopsVars(self)
434 vars.extend([
435 ('product', self.context.product.name),
436 ])
437 return vars
438>>>>>>> MERGE-SOURCE
439285
=== modified file 'lib/lp/registry/services/sharingservice.py'
--- lib/lp/registry/services/sharingservice.py 2012-03-29 14:36:36 +0000
+++ lib/lp/registry/services/sharingservice.py 2012-03-26 20:49:13 +0000
@@ -27,14 +27,9 @@
27 )27 )
28from lp.registry.interfaces.product import IProduct28from lp.registry.interfaces.product import IProduct
29from lp.registry.interfaces.projectgroup import IProjectGroup29from lp.registry.interfaces.projectgroup import IProjectGroup
30<<<<<<< TREE
31from lp.registry.interfaces.sharingservice import ISharingService30from lp.registry.interfaces.sharingservice import ISharingService
32from lp.registry.model.person import Person31from lp.registry.model.person import Person
33from lp.services.features import getFeatureFlag32from lp.services.features import getFeatureFlag
34=======
35from lp.registry.model.person import Person
36from lp.services.features import getFeatureFlag
37>>>>>>> MERGE-SOURCE
38from lp.services.webapp.authorization import available_with_permission33from lp.services.webapp.authorization import available_with_permission
3934
4035
@@ -52,7 +47,6 @@
52 """See `IService`."""47 """See `IService`."""
53 return 'sharing'48 return 'sharing'
5449
55<<<<<<< TREE
56 @property50 @property
57 def write_enabled(self):51 def write_enabled(self):
58 return bool(getFeatureFlag(52 return bool(getFeatureFlag(
@@ -64,13 +58,6 @@
64 return [a for a in58 return [a for a in
65 flat_source.findArtifactsByGrantee(person, policies)]59 flat_source.findArtifactsByGrantee(person, policies)]
6660
67=======
68 @property
69 def write_enabled(self):
70 return bool(getFeatureFlag(
71 'disclosure.enhanced_sharing.writable'))
72
73>>>>>>> MERGE-SOURCE
74 def getInformationTypes(self, pillar):61 def getInformationTypes(self, pillar):
75 """See `ISharingService`."""62 """See `ISharingService`."""
76 allowed_types = [63 allowed_types = [
@@ -115,7 +102,6 @@
115 @available_with_permission('launchpad.Driver', 'pillar')102 @available_with_permission('launchpad.Driver', 'pillar')
116 def getPillarSharees(self, pillar):103 def getPillarSharees(self, pillar):
117 """See `ISharingService`."""104 """See `ISharingService`."""
118<<<<<<< TREE
119 policies = getUtility(IAccessPolicySource).findByPillar([pillar])105 policies = getUtility(IAccessPolicySource).findByPillar([pillar])
120 ap_grant_flat = getUtility(IAccessPolicyGrantFlatSource)106 ap_grant_flat = getUtility(IAccessPolicyGrantFlatSource)
121 # XXX 2012-03-22 wallyworld bug 961836107 # XXX 2012-03-22 wallyworld bug 961836
@@ -135,31 +121,8 @@
135121
136 def jsonShareeData(self, grant_permissions):122 def jsonShareeData(self, grant_permissions):
137 """See `ISharingService`."""123 """See `ISharingService`."""
138=======
139 policies = getUtility(IAccessPolicySource).findByPillar([pillar])
140 ap_grant_flat = getUtility(IAccessPolicyGrantFlatSource)
141 # XXX 2012-03-22 wallyworld bug 961836
142 # We want to use person_sort_key(Person.displayname, Person.name) but
143 # StormRangeFactory doesn't support that yet.
144 grantees = ap_grant_flat.findGranteesByPolicy(
145 policies).order_by(Person.displayname, Person.name)
146 return grantees
147
148 @available_with_permission('launchpad.Driver', 'pillar')
149 def getPillarShareeData(self, pillar, grantees=None):
150 """See `ISharingService`."""
151 policies = getUtility(IAccessPolicySource).findByPillar([pillar])
152 ap_grant_flat = getUtility(IAccessPolicyGrantFlatSource)
153 # XXX 2012-03-22 wallyworld bug 961836
154 # We want to use person_sort_key(Person.displayname, Person.name) but
155 # StormRangeFactory doesn't support that yet.
156 grant_permissions = ap_grant_flat.findGranteePermissionsByPolicy(
157 policies, grantees).order_by(Person.displayname, Person.name)
158
159>>>>>>> MERGE-SOURCE
160 result = []124 result = []
161 request = get_current_web_service_request()125 request = get_current_web_service_request()
162<<<<<<< TREE
163 browser_request = IWebBrowserOriginatingRequest(request)126 browser_request = IWebBrowserOriginatingRequest(request)
164 for (grantee, permissions) in grant_permissions:127 for (grantee, permissions) in grant_permissions:
165 result.append({128 result.append({
@@ -171,22 +134,6 @@
171 'permissions': dict(134 'permissions': dict(
172 (policy.type.name, permission.name)135 (policy.type.name, permission.name)
173 for (policy, permission) in permissions.iteritems())})136 for (policy, permission) in permissions.iteritems())})
174=======
175 browser_request = IWebBrowserOriginatingRequest(request)
176 for (grantee, policy, sharing_permission) in grant_permissions:
177 if not grantee.id in person_by_id:
178 person_data = {
179 'name': grantee.name,
180 'meta': 'team' if grantee.is_team else 'person',
181 'display_name': grantee.displayname,
182 'self_link': absoluteURL(grantee, request),
183 'permissions': {}}
184 person_data['web_link'] = absoluteURL(grantee, browser_request)
185 person_by_id[grantee.id] = person_data
186 result.append(person_data)
187 person_data = person_by_id[grantee.id]
188 person_data['permissions'][policy.type.name] = sharing_permission
189>>>>>>> MERGE-SOURCE
190 return result137 return result
191138
192 @available_with_permission('launchpad.Edit', 'pillar')139 @available_with_permission('launchpad.Edit', 'pillar')
@@ -251,7 +198,6 @@
251 self.deletePillarSharee(pillar, sharee, info_types_for_nothing)198 self.deletePillarSharee(pillar, sharee, info_types_for_nothing)
252199
253 # Return sharee data to the caller.200 # Return sharee data to the caller.
254<<<<<<< TREE
255 ap_grant_flat = getUtility(IAccessPolicyGrantFlatSource)201 ap_grant_flat = getUtility(IAccessPolicyGrantFlatSource)
256 grant_permissions = list(ap_grant_flat.findGranteePermissionsByPolicy(202 grant_permissions = list(ap_grant_flat.findGranteePermissionsByPolicy(
257 all_pillar_policies, [sharee]))203 all_pillar_policies, [sharee]))
@@ -259,12 +205,6 @@
259 return None205 return None
260 [sharee] = self.jsonShareeData(grant_permissions)206 [sharee] = self.jsonShareeData(grant_permissions)
261 return sharee207 return sharee
262=======
263 sharees = self.getPillarShareeData(pillar, [sharee])
264 if not sharees:
265 return None
266 return sharees[0]
267>>>>>>> MERGE-SOURCE
268208
269 @available_with_permission('launchpad.Edit', 'pillar')209 @available_with_permission('launchpad.Edit', 'pillar')
270 def deletePillarSharee(self, pillar, sharee,210 def deletePillarSharee(self, pillar, sharee,
@@ -296,18 +236,9 @@
296236
297 # Second delete any access artifact grants.237 # Second delete any access artifact grants.
298 ap_grant_flat = getUtility(IAccessPolicyGrantFlatSource)238 ap_grant_flat = getUtility(IAccessPolicyGrantFlatSource)
299<<<<<<< TREE
300 to_delete = list(ap_grant_flat.findArtifactsByGrantee(239 to_delete = list(ap_grant_flat.findArtifactsByGrantee(
301 sharee, pillar_policies))240 sharee, pillar_policies))
302 if len(to_delete) > 0:241 if len(to_delete) > 0:
303 accessartifact_grant_source = getUtility(242 accessartifact_grant_source = getUtility(
304 IAccessArtifactGrantSource)243 IAccessArtifactGrantSource)
305 accessartifact_grant_source.revokeByArtifact(to_delete)244 accessartifact_grant_source.revokeByArtifact(to_delete)
306=======
307 to_delete = ap_grant_flat.findArtifactsByGrantee(
308 sharee, pillar_policies)
309 if to_delete.count() > 0:
310 accessartifact_grant_source = getUtility(
311 IAccessArtifactGrantSource)
312 accessartifact_grant_source.revokeByArtifact(to_delete)
313>>>>>>> MERGE-SOURCE
314245
=== modified file 'lib/lp/registry/services/tests/test_sharingservice.py'
--- lib/lp/registry/services/tests/test_sharingservice.py 2012-03-29 14:36:36 +0000
+++ lib/lp/registry/services/tests/test_sharingservice.py 2012-03-23 04:00:29 +0000
@@ -123,7 +123,6 @@
123 distro,123 distro,
124 [InformationType.EMBARGOEDSECURITY, InformationType.USERDATA])124 [InformationType.EMBARGOEDSECURITY, InformationType.USERDATA])
125125
126<<<<<<< TREE
127 def test_jsonShareeData(self):126 def test_jsonShareeData(self):
128 # jsonShareeData returns the expected data.127 # jsonShareeData returns the expected data.
129 product = self.factory.makeProduct()128 product = self.factory.makeProduct()
@@ -246,123 +245,6 @@
246 self._assert_getPillarShareeDataUnauthorized(product)245 self._assert_getPillarShareeDataUnauthorized(product)
247246
248 def _assert_getPillarSharees(self, pillar):247 def _assert_getPillarSharees(self, pillar):
249=======
250 def _assert_getPillarShareeData(self, pillar):
251 # getPillarShareeData returns the expected data.
252 access_policy = self.factory.makeAccessPolicy(
253 pillar=pillar,
254 type=InformationType.PROPRIETARY)
255 grantee = self.factory.makePerson()
256 # Make access policy grant so that 'All' is returned.
257 self.factory.makeAccessPolicyGrant(access_policy, grantee)
258 # Make access artifact grants so that 'Some' is returned.
259 artifact_grant = self.factory.makeAccessArtifactGrant()
260 self.factory.makeAccessPolicyArtifact(
261 artifact=artifact_grant.abstract_artifact, policy=access_policy)
262
263 sharees = self.service.getPillarShareeData(pillar)
264 expected_sharees = [
265 self._makeShareeData(
266 grantee,
267 [(InformationType.PROPRIETARY, SharingPermission.ALL)]),
268 self._makeShareeData(
269 artifact_grant.grantee,
270 [(InformationType.PROPRIETARY, SharingPermission.SOME)])]
271 self.assertContentEqual(expected_sharees, sharees)
272
273 def test_getProductShareeData(self):
274 # Users with launchpad.Driver can view sharees.
275 driver = self.factory.makePerson()
276 product = self.factory.makeProduct(driver=driver)
277 login_person(driver)
278 self._assert_getPillarShareeData(product)
279
280 def test_getDistroShareeData(self):
281 # Users with launchpad.Driver can view sharees.
282 driver = self.factory.makePerson()
283 distro = self.factory.makeDistribution(driver=driver)
284 login_person(driver)
285 self._assert_getPillarShareeData(distro)
286
287 def test_getPillarShareeDataQueryCount(self):
288 # getPillarShareeData only should use 2 queries regardless of how many
289 # sharees are returned.
290 driver = self.factory.makePerson()
291 product = self.factory.makeProduct(driver=driver)
292 login_person(driver)
293 access_policy = self.factory.makeAccessPolicy(
294 pillar=product,
295 type=InformationType.PROPRIETARY)
296
297 def makeGrants():
298 grantee = self.factory.makePerson()
299 # Make access policy grant so that 'All' is returned.
300 self.factory.makeAccessPolicyGrant(access_policy, grantee)
301 # Make access artifact grants so that 'Some' is returned.
302 artifact_grant = self.factory.makeAccessArtifactGrant()
303 self.factory.makeAccessPolicyArtifact(
304 artifact=artifact_grant.abstract_artifact,
305 policy=access_policy)
306
307 # Make some grants and check the count.
308 for x in range(5):
309 makeGrants()
310 with StormStatementRecorder() as recorder:
311 sharees = self.service.getPillarShareeData(product)
312 self.assertEqual(10, len(sharees))
313 self.assertThat(recorder, HasQueryCount(Equals(2)))
314 # Make some more grants and check again.
315 for x in range(5):
316 makeGrants()
317 with StormStatementRecorder() as recorder:
318 sharees = self.service.getPillarShareeData(product)
319 self.assertEqual(20, len(sharees))
320 self.assertThat(recorder, HasQueryCount(Equals(2)))
321
322 def test_getPillarShareeData_filter_grantees(self):
323 # getPillarShareeData only returns grantees in the specified list.
324 driver = self.factory.makePerson()
325 pillar = self.factory.makeProduct(driver=driver)
326 login_person(driver)
327 access_policy = self.factory.makeAccessPolicy(
328 pillar=pillar,
329 type=InformationType.PROPRIETARY)
330 grantee_in_result = self.factory.makePerson()
331 grantee_not_in_result = self.factory.makePerson()
332 self.factory.makeAccessPolicyGrant(access_policy, grantee_in_result)
333 self.factory.makeAccessPolicyGrant(
334 access_policy, grantee_not_in_result)
335
336 sharees = self.service.getPillarShareeData(pillar, [grantee_in_result])
337 expected_sharees = [
338 self._makeShareeData(
339 grantee_in_result,
340 [(InformationType.PROPRIETARY, SharingPermission.ALL)])]
341 self.assertContentEqual(expected_sharees, sharees)
342
343 def _assert_getPillarShareeDataUnauthorized(self, pillar):
344 # getPillarShareeData raises an Unauthorized exception if the user is
345 # not permitted to do so.
346 access_policy = self.factory.makeAccessPolicy(pillar=pillar)
347 grantee = self.factory.makePerson()
348 self.factory.makeAccessPolicyGrant(access_policy, grantee)
349 self.assertRaises(
350 Unauthorized, self.service.getPillarShareeData, pillar)
351
352 def test_getPillarShareeDataAnonymous(self):
353 # Anonymous users are not allowed.
354 product = self.factory.makeProduct()
355 login(ANONYMOUS)
356 self._assert_getPillarShareeDataUnauthorized(product)
357
358 def test_getPillarShareeDataAnyone(self):
359 # Unauthorized users are not allowed.
360 product = self.factory.makeProduct()
361 login_person(self.factory.makePerson())
362 self._assert_getPillarShareeDataUnauthorized(product)
363
364 def _assert_getPillarSharees(self, pillar):
365>>>>>>> MERGE-SOURCE
366 # getPillarSharees returns the expected data.248 # getPillarSharees returns the expected data.
367 access_policy = self.factory.makeAccessPolicy(249 access_policy = self.factory.makeAccessPolicy(
368 pillar=pillar,250 pillar=pillar,
@@ -370,7 +252,6 @@
370 grantee = self.factory.makePerson()252 grantee = self.factory.makePerson()
371 # Make access policy grant so that 'All' is returned.253 # Make access policy grant so that 'All' is returned.
372 self.factory.makeAccessPolicyGrant(access_policy, grantee)254 self.factory.makeAccessPolicyGrant(access_policy, grantee)
373<<<<<<< TREE
374 # Make access artifact grants so that 'Some' is returned.255 # Make access artifact grants so that 'Some' is returned.
375 artifact_grant = self.factory.makeAccessArtifactGrant()256 artifact_grant = self.factory.makeAccessArtifactGrant()
376 self.factory.makeAccessPolicyArtifact(257 self.factory.makeAccessPolicyArtifact(
@@ -381,16 +262,6 @@
381 (grantee, {access_policy: SharingPermission.ALL}),262 (grantee, {access_policy: SharingPermission.ALL}),
382 (artifact_grant.grantee, {access_policy: SharingPermission.SOME})]263 (artifact_grant.grantee, {access_policy: SharingPermission.SOME})]
383 self.assertContentEqual(expected_sharees, sharees)264 self.assertContentEqual(expected_sharees, sharees)
384=======
385 # Make access artifact grants so that 'Some' is returned.
386 artifact_grant = self.factory.makeAccessArtifactGrant()
387 self.factory.makeAccessPolicyArtifact(
388 artifact=artifact_grant.abstract_artifact, policy=access_policy)
389
390 sharees = self.service.getPillarSharees(pillar)
391 expected_sharees = [grantee, artifact_grant.grantee]
392 self.assertContentEqual(expected_sharees, sharees)
393>>>>>>> MERGE-SOURCE
394265
395 def test_getProductSharees(self):266 def test_getProductSharees(self):
396 # Users with launchpad.Driver can view sharees.267 # Users with launchpad.Driver can view sharees.
@@ -483,14 +354,7 @@
483 (InformationType.EMBARGOEDSECURITY, SharingPermission.ALL),354 (InformationType.EMBARGOEDSECURITY, SharingPermission.ALL),
484 (InformationType.USERDATA, SharingPermission.SOME)]355 (InformationType.USERDATA, SharingPermission.SOME)]
485 expected_sharee_data = self._makeShareeData(356 expected_sharee_data = self._makeShareeData(
486<<<<<<< TREE357 sharee, expected_permissions)
487 sharee, expected_permissions)
488=======
489 sharee, expected_permissions)
490 self.assertEqual(expected_sharee_data, sharee_data)
491 # Check that getPillarShareeData returns what we expect.
492 [sharee_data] = self.service.getPillarShareeData(pillar)
493>>>>>>> MERGE-SOURCE
494 self.assertEqual(expected_sharee_data, sharee_data)358 self.assertEqual(expected_sharee_data, sharee_data)
495 # Check that getPillarSharees returns what we expect.359 # Check that getPillarSharees returns what we expect.
496 expected_sharee_grants = [360 expected_sharee_grants = [
@@ -604,7 +468,6 @@
604 if types_to_delete is not None:468 if types_to_delete is not None:
605 expected_information_types = (469 expected_information_types = (
606 set(information_types).difference(types_to_delete))470 set(information_types).difference(types_to_delete))
607<<<<<<< TREE
608 expected_policies = [471 expected_policies = [
609 access_policy for access_policy in access_policies472 access_policy for access_policy in access_policies
610 if access_policy.type in expected_information_types]473 if access_policy.type in expected_information_types]
@@ -614,20 +477,9 @@
614 # Add the expected data for the other sharee.477 # Add the expected data for the other sharee.
615 another_person_data = (478 another_person_data = (
616 another, {access_policies[0]: SharingPermission.ALL})479 another, {access_policies[0]: SharingPermission.ALL})
617=======
618 remaining_grantee_person_data = self._makeShareeData(
619 grantee,
620 [(info_type, SharingPermission.ALL)
621 for info_type in expected_information_types])
622
623 expected_data.append(remaining_grantee_person_data)
624 # Add the data for the other sharee.
625 another_person_data = self._makeShareeData(
626 another, [(information_types[0], SharingPermission.ALL)])
627>>>>>>> MERGE-SOURCE
628 expected_data.append(another_person_data)480 expected_data.append(another_person_data)
629 self.assertContentEqual(481 self.assertContentEqual(
630 expected_data, self.service.getPillarShareeData(pillar))482 expected_data, self.service.getPillarSharees(pillar))
631483
632 def test_deleteProductShareeAll(self):484 def test_deleteProductShareeAll(self):
633 # Users with launchpad.Edit can delete all access for a sharee.485 # Users with launchpad.Edit can delete all access for a sharee.
634486
=== modified file 'lib/lp/registry/subscribers.py'
--- lib/lp/registry/subscribers.py 2012-03-29 14:36:36 +0000
+++ lib/lp/registry/subscribers.py 2012-03-22 23:21:24 +0000
@@ -8,281 +8,140 @@
8 'product_licenses_modified',8 'product_licenses_modified',
9 ]9 ]
1010
11<<<<<<< TREE11from datetime import datetime
12from datetime import datetime12import textwrap
13import textwrap13
1414import pytz
15import pytz15from zope.security.proxy import removeSecurityProxy
16from zope.security.proxy import removeSecurityProxy16
1717from lp.registry.interfaces.person import IPerson
18from lp.registry.interfaces.person import IPerson18from lp.registry.interfaces.product import License
19from lp.registry.interfaces.product import License19from lp.services.config import config
20from lp.services.config import config20from lp.services.mail.helpers import get_email_template
21from lp.services.mail.helpers import get_email_template21from lp.services.mail.sendmail import (
22from lp.services.mail.sendmail import (22 format_address,
23 format_address,23 simple_sendmail,
24 simple_sendmail,24 )
25 )25from lp.services.webapp.menu import structured
26from lp.services.webapp.menu import structured26from lp.services.webapp.publisher import (
27from lp.services.webapp.publisher import (27 canonical_url,
28 canonical_url,28 get_current_browser_request,
29 get_current_browser_request,29 )
30 )30
3131
3232def product_licenses_modified(product, event):
33def product_licenses_modified(product, event):33 """Send a notification if licenses changed and a license is special."""
34 """Send a notification if licenses changed and a license is special."""34 if not event.edited_fields:
35 if not event.edited_fields:35 return
36 return36 licenses_changed = 'licenses' in event.edited_fields
37 licenses_changed = 'licenses' in event.edited_fields37 needs_notification = LicenseNotification.needs_notification(product)
38 needs_notification = LicenseNotification.needs_notification(product)38 if licenses_changed and needs_notification:
39 if licenses_changed and needs_notification:39 user = IPerson(event.user)
40 user = IPerson(event.user)40 notification = LicenseNotification(product, user)
41 notification = LicenseNotification(product, user)41 notification.send()
42 notification.send()42 notification.display()
43 notification.display()43
4444
4545class LicenseNotification:
46class LicenseNotification:46 """Send notification about special licenses to the user."""
47 """Send notification about special licenses to the user."""47
4848 def __init__(self, product, user):
49 def __init__(self, product, user):49 self.product = product
50 self.product = product50 self.user = user
51 self.user = user51
5252 @staticmethod
53 @staticmethod53 def needs_notification(product):
54 def needs_notification(product):54 licenses = list(product.licenses)
55 licenses = list(product.licenses)55 return (
56 return (56 License.OTHER_PROPRIETARY in licenses
57 License.OTHER_PROPRIETARY in licenses57 or License.OTHER_OPEN_SOURCE in licenses
58 or License.OTHER_OPEN_SOURCE in licenses58 or [License.DONT_KNOW] == licenses)
59 or [License.DONT_KNOW] == licenses)59
6060 def getTemplateName(self):
61 def getTemplateName(self):61 """Return the name of the email template for the licensing case."""
62 """Return the name of the email template for the licensing case."""62 licenses = list(self.product.licenses)
63 licenses = list(self.product.licenses)63 if [License.DONT_KNOW] == licenses:
64 if [License.DONT_KNOW] == licenses:64 template_name = 'product-license-dont-know.txt'
65 template_name = 'product-license-dont-know.txt'65 elif License.OTHER_PROPRIETARY in licenses:
66 elif License.OTHER_PROPRIETARY in licenses:66 template_name = 'product-license-other-proprietary.txt'
67 template_name = 'product-license-other-proprietary.txt'67 else:
68 else:68 template_name = 'product-license-other-open-source.txt'
69 template_name = 'product-license-other-open-source.txt'69 return template_name
70 return template_name70
7171 def getCommercialUseMessage(self):
72 def getCommercialUseMessage(self):72 """Return a message explaining the current commercial subscription."""
73 """Return a message explaining the current commercial subscription."""73 commercial_subscription = self.product.commercial_subscription
74 commercial_subscription = self.product.commercial_subscription74 if commercial_subscription is None:
75 if commercial_subscription is None:75 return ''
76 return ''76 iso_date = commercial_subscription.date_expires.date().isoformat()
77 iso_date = commercial_subscription.date_expires.date().isoformat()77 if not self.product.has_current_commercial_subscription:
78 if not self.product.has_current_commercial_subscription:78 message = "%s's commercial subscription expired on %s."
79 message = "%s's commercial subscription expired on %s."79 elif 'complimentary' in commercial_subscription.sales_system_id:
80 elif 'complimentary' in commercial_subscription.sales_system_id:80 message = (
81 message = (81 "%s's complimentary commercial subscription expires on %s.")
82 "%s's complimentary commercial subscription expires on %s.")82 else:
83 else:83 message = "%s's commercial subscription expires on %s."
84 message = "%s's commercial subscription expires on %s."84 message = message % (self.product.displayname, iso_date)
85 message = message % (self.product.displayname, iso_date)85 return textwrap.fill(message, 72)
86 return textwrap.fill(message, 72)86
8787 def send(self):
88 def send(self):88 """Send a message to the user about the product's license."""
89 """Send a message to the user about the product's license."""89 if not self.needs_notification(self.product):
90 if not self.needs_notification(self.product):90 # The project has a common license.
91 # The project has a common license.91 return False
92 return False92 user_address = format_address(
93 user_address = format_address(93 self.user.displayname, self.user.preferredemail.email)
94 self.user.displayname, self.user.preferredemail.email)94 from_address = format_address(
95 from_address = format_address(95 "Launchpad", config.canonical.noreply_from_address)
96 "Launchpad", config.canonical.noreply_from_address)96 commercial_address = format_address(
97 commercial_address = format_address(97 'Commercial', 'commercial@launchpad.net')
98 'Commercial', 'commercial@launchpad.net')98 substitutions = dict(
99 substitutions = dict(99 user_displayname=self.user.displayname,
100 user_displayname=self.user.displayname,100 user_name=self.user.name,
101 user_name=self.user.name,101 product_name=self.product.name,
102 product_name=self.product.name,102 product_url=canonical_url(self.product),
103 product_url=canonical_url(self.product),103 commercial_use_expiration=self.getCommercialUseMessage(),
104 commercial_use_expiration=self.getCommercialUseMessage(),104 )
105 )105 # Email the user about license policy.
106 # Email the user about license policy.106 subject = (
107 subject = (107 "License information for %(product_name)s "
108 "License information for %(product_name)s "108 "in Launchpad" % substitutions)
109 "in Launchpad" % substitutions)109 template = get_email_template(
110 template = get_email_template(110 self.getTemplateName(), app='registry')
111 self.getTemplateName(), app='registry')111 message = template % substitutions
112 message = template % substitutions112 simple_sendmail(
113 simple_sendmail(113 from_address, user_address,
114 from_address, user_address,114 subject, message, headers={'Reply-To': commercial_address})
115 subject, message, headers={'Reply-To': commercial_address})115 # Inform that Launchpad recognized the license change.
116 # Inform that Launchpad recognized the license change.116 self._addLicenseChangeToReviewWhiteboard()
117 self._addLicenseChangeToReviewWhiteboard()117 return True
118 return True118
119119 def display(self):
120 def display(self):120 """Show a message in a browser page about the product's license."""
121 """Show a message in a browser page about the product's license."""121 request = get_current_browser_request()
122 request = get_current_browser_request()122 message = self.getCommercialUseMessage()
123 message = self.getCommercialUseMessage()123 if request is None or message == '':
124 if request is None or message == '':124 return False
125 return False125 safe_message = structured(
126 safe_message = structured(126 '%s<br />Learn more about '
127 '%s<br />Learn more about '127 '<a href="https://help.launchpad.net/CommercialHosting">'
128 '<a href="https://help.launchpad.net/CommercialHosting">'128 'commercial subscriptions</a>', message)
129 'commercial subscriptions</a>', message)129 request.response.addNotification(safe_message)
130 request.response.addNotification(safe_message)130 return True
131 return True131
132132 @staticmethod
133 @staticmethod133 def _formatDate(now=None):
134 def _formatDate(now=None):134 """Return the date formatted for messages."""
135 """Return the date formatted for messages."""135 if now is None:
136 if now is None:136 now = datetime.now(tz=pytz.UTC)
137 now = datetime.now(tz=pytz.UTC)137 return now.strftime('%Y-%m-%d')
138 return now.strftime('%Y-%m-%d')138
139139 def _addLicenseChangeToReviewWhiteboard(self):
140 def _addLicenseChangeToReviewWhiteboard(self):140 """Update the whiteboard for the reviewer's benefit."""
141 """Update the whiteboard for the reviewer's benefit."""141 now = self._formatDate()
142 now = self._formatDate()142 whiteboard = 'User notified of license policy on %s.' % now
143 whiteboard = 'User notified of license policy on %s.' % now143 naked_product = removeSecurityProxy(self.product)
144 naked_product = removeSecurityProxy(self.product)144 if naked_product.reviewer_whiteboard is None:
145 if naked_product.reviewer_whiteboard is None:145 naked_product.reviewer_whiteboard = whiteboard
146 naked_product.reviewer_whiteboard = whiteboard146 else:
147 else:147 naked_product.reviewer_whiteboard += '\n' + whiteboard
148 naked_product.reviewer_whiteboard += '\n' + whiteboard
149=======
150from datetime import datetime
151import textwrap
152
153import pytz
154
155from zope.security.proxy import removeSecurityProxy
156
157from lp.registry.interfaces.person import IPerson
158from lp.registry.interfaces.product import License
159from lp.services.config import config
160from lp.services.mail.helpers import get_email_template
161from lp.services.mail.sendmail import (
162 format_address,
163 simple_sendmail,
164 )
165from lp.services.webapp.menu import structured
166from lp.services.webapp.publisher import (
167 canonical_url,
168 get_current_browser_request,
169 )
170
171
172def product_licenses_modified(product, event):
173 """Send a notification if licenses changed and a license is special."""
174 if not event.edited_fields:
175 return
176 licenses_changed = 'licenses' in event.edited_fields
177 needs_notification = LicenseNotification.needs_notification(product)
178 if licenses_changed and needs_notification:
179 user = IPerson(event.user)
180 notification = LicenseNotification(product, user)
181 notification.send()
182 notification.display()
183
184
185class LicenseNotification:
186 """Send notification about special licenses to the user."""
187
188 def __init__(self, product, user):
189 self.product = product
190 self.user = user
191
192 @staticmethod
193 def needs_notification(product):
194 licenses = list(product.licenses)
195 return (
196 License.OTHER_PROPRIETARY in licenses
197 or License.OTHER_OPEN_SOURCE in licenses
198 or [License.DONT_KNOW] == licenses)
199
200 def getTemplateName(self):
201 """Return the name of the email template for the licensing case."""
202 licenses = list(self.product.licenses)
203 if [License.DONT_KNOW] == licenses:
204 template_name = 'product-license-dont-know.txt'
205 elif License.OTHER_PROPRIETARY in licenses:
206 template_name = 'product-license-other-proprietary.txt'
207 else:
208 template_name = 'product-license-other-open-source.txt'
209 return template_name
210
211 def getCommercialUseMessage(self):
212 """Return a message explaining the current commercial subscription."""
213 commercial_subscription = self.product.commercial_subscription
214 if commercial_subscription is None:
215 return ''
216 iso_date = commercial_subscription.date_expires.date().isoformat()
217 if not self.product.has_current_commercial_subscription:
218 message = "%s's commercial subscription expired on %s."
219 elif 'complimentary' in commercial_subscription.sales_system_id:
220 message = (
221 "%s's complimentary commercial subscription expires on %s.")
222 else:
223 message = "%s's commercial subscription expires on %s."
224 message = message % (self.product.displayname, iso_date)
225 return textwrap.fill(message, 72)
226
227 def send(self):
228 """Send a message to the user about the product's license."""
229 if not self.needs_notification(self.product):
230 # The project has a common license.
231 return False
232 user_address = format_address(
233 self.user.displayname, self.user.preferredemail.email)
234 from_address = format_address(
235 "Launchpad", config.canonical.noreply_from_address)
236 commercial_address = format_address(
237 'Commercial', 'commercial@launchpad.net')
238 substitutions = dict(
239 user_displayname=self.user.displayname,
240 user_name=self.user.name,
241 product_name=self.product.name,
242 product_url=canonical_url(self.product),
243 commercial_use_expiration=self.getCommercialUseMessage(),
244 )
245 # Email the user about license policy.
246 subject = (
247 "License information for %(product_name)s "
248 "in Launchpad" % substitutions)
249 template = get_email_template(
250 self.getTemplateName(), app='registry')
251 message = template % substitutions
252 simple_sendmail(
253 from_address, user_address,
254 subject, message, headers={'Reply-To': commercial_address})
255 # Inform that Launchpad recognized the license change.
256 self._addLicenseChangeToReviewWhiteboard()
257 return True
258
259 def display(self):
260 """Show a message in a browser page about the product's license."""
261 request = get_current_browser_request()
262 message = self.getCommercialUseMessage()
263 if request is None or message == '':
264 return False
265 safe_message = structured(
266 '%s<br />Learn more about '
267 '<a href="https://help.launchpad.net/CommercialHosting">'
268 'commercial subscriptions</a>', message)
269 request.response.addNotification(safe_message)
270 return True
271
272 @staticmethod
273 def _formatDate(now=None):
274 """Return the date formatted for messages."""
275 if now is None:
276 now = datetime.now(tz=pytz.UTC)
277 return now.strftime('%Y-%m-%d')
278
279 def _addLicenseChangeToReviewWhiteboard(self):
280 """Update the whiteboard for the reviewer's benefit."""
281 now = self._formatDate()
282 whiteboard = 'User notified of license policy on %s.' % now
283 naked_product = removeSecurityProxy(self.product)
284 if naked_product.reviewer_whiteboard is None:
285 naked_product.reviewer_whiteboard = whiteboard
286 else:
287 naked_product.reviewer_whiteboard += '\n' + whiteboard
288>>>>>>> MERGE-SOURCE
289148
=== modified file 'lib/lp/registry/templates/pillar-sharing-details.pt'
--- lib/lp/registry/templates/pillar-sharing-details.pt 2012-03-29 14:36:36 +0000
+++ lib/lp/registry/templates/pillar-sharing-details.pt 2012-03-26 20:49:13 +0000
@@ -1,4 +1,3 @@
1<<<<<<< TREE
2<html1<html
3 xmlns="http://www.w3.org/1999/xhtml"2 xmlns="http://www.w3.org/1999/xhtml"
4 xmlns:tal="http://xml.zope.org/namespaces/tal"3 xmlns:tal="http://xml.zope.org/namespaces/tal"
@@ -61,19 +60,3 @@
61 </div>60 </div>
62</body>61</body>
63</html>62</html>
64=======
65<html
66 xmlns="http://www.w3.org/1999/xhtml"
67 xmlns:tal="http://xml.zope.org/namespaces/tal"
68 xmlns:metal="http://xml.zope.org/namespaces/metal"
69 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
70 metal:use-macro="view/macro:page/main_only"
71 i18n:domain="launchpad"
72>
73
74<body>
75 <div metal:fill-slot="main">
76 </div>
77</body>
78</html>
79>>>>>>> MERGE-SOURCE
8063
=== modified file 'lib/lp/registry/templates/pillar-sharing.pt'
--- lib/lp/registry/templates/pillar-sharing.pt 2012-03-29 14:36:36 +0000
+++ lib/lp/registry/templates/pillar-sharing.pt 2012-03-22 09:00:36 +0000
@@ -28,7 +28,6 @@
28 Proprietary, embargoed security, or user-data information is28 Proprietary, embargoed security, or user-data information is
29 shared with these users and teams.29 shared with these users and teams.
30 </p>30 </p>
31<<<<<<< TREE
32 <ul class="horizontal">31 <ul class="horizontal">
33 <li><a id='add-sharee-link' class='sprite add js-action' href="#">Share32 <li><a id='add-sharee-link' class='sprite add js-action' href="#">Share
34 with someone</a></li>33 with someone</a></li>
@@ -41,19 +40,5 @@
4140
42 </div>41 </div>
4342
44=======
45 <ul class="horizontal">
46 <li><a id='add-sharee-link' class='sprite add js-action' href="#">Share
47 with someone</a></li>
48 <li><a id="audit-link" class="sprite info" href='#'>Audit sharing</a></li>
49 </ul>
50
51 <div tal:define="batch_navigator view/shareeData">
52 <tal:shareelisting content="structure batch_navigator/@@+sharee-table-view" />
53 </div>
54
55 </div>
56
57>>>>>>> MERGE-SOURCE
58</body>43</body>
59</html>44</html>
6045
=== modified file 'lib/lp/registry/tests/test_accesspolicy.py'
--- lib/lp/registry/tests/test_accesspolicy.py 2012-03-29 14:36:36 +0000
+++ lib/lp/registry/tests/test_accesspolicy.py 2012-03-23 03:40:17 +0000
@@ -468,7 +468,6 @@
468 artifact=artifact_grant.abstract_artifact, policy=another_policy)468 artifact=artifact_grant.abstract_artifact, policy=another_policy)
469 self.assertContentEqual(469 self.assertContentEqual(
470 [policy_grant.grantee, artifact_grant.grantee],470 [policy_grant.grantee, artifact_grant.grantee],
471<<<<<<< TREE
472 apgfs.findGranteesByPolicy([471 apgfs.findGranteesByPolicy([
473 policy, another_policy, policy_with_no_grantees]))472 policy, another_policy, policy_with_no_grantees]))
474473
@@ -537,60 +536,6 @@
537 [(policy_grant.grantee, {policy: SharingPermission.ALL})],536 [(policy_grant.grantee, {policy: SharingPermission.ALL})],
538 apgfs.findGranteePermissionsByPolicy(537 apgfs.findGranteePermissionsByPolicy(
539 [policy], [grantee_in_result]))538 [policy], [grantee_in_result]))
540=======
541 apgfs.findGranteesByPolicy([
542 policy, another_policy, policy_with_no_grantees]))
543
544 def findGranteePermissionsByPolicy(self):
545 # findGranteePermissionsByPolicy() returns anyone with a grant for any
546 # of the policies or the policies' artifacts.
547 apgfs = getUtility(IAccessPolicyGrantFlatSource)
548
549 # People with grants on the policy show up.
550 policy_with_no_grantees = self.factory.makeAccessPolicy()
551 policy = self.factory.makeAccessPolicy()
552 policy_grant = self.factory.makeAccessPolicyGrant(policy=policy)
553 self.assertContentEqual(
554 [(policy_grant.grantee, policy, 'ALL')],
555 apgfs.findGranteePermissionsByPolicy(
556 [policy, policy_with_no_grantees]))
557
558 # But not people with grants on artifacts.
559 artifact_grant = self.factory.makeAccessArtifactGrant()
560 self.assertContentEqual(
561 [(policy_grant.grantee, policy, 'ALL')],
562 apgfs.findGranteePermissionsByPolicy(
563 [policy, policy_with_no_grantees]))
564
565 # Unless the artifacts are linked to the policy.
566 another_policy = self.factory.makeAccessPolicy()
567 self.factory.makeAccessPolicyArtifact(
568 artifact=artifact_grant.abstract_artifact, policy=another_policy)
569 self.assertContentEqual(
570 [(policy_grant.grantee, policy, 'ALL'),
571 (artifact_grant.grantee, another_policy, 'SOME')],
572 apgfs.findGranteePermissionsByPolicy([
573 policy, another_policy, policy_with_no_grantees]))
574
575 def test_findGranteePermissionsByPolicy_filter_grantees(self):
576 # findGranteePermissionsByPolicy() returns anyone with a grant for any
577 # of the policies or the policies' artifacts so long as the grantee is
578 # in the specified list of grantees.
579 apgfs = getUtility(IAccessPolicyGrantFlatSource)
580
581 # People with grants on the policy show up.
582 policy = self.factory.makeAccessPolicy()
583 grantee_in_result = self.factory.makePerson()
584 grantee_not_in_result = self.factory.makePerson()
585 policy_grant = self.factory.makeAccessPolicyGrant(
586 policy=policy, grantee=grantee_in_result)
587 self.factory.makeAccessPolicyGrant(
588 policy=policy, grantee=grantee_not_in_result)
589 self.assertContentEqual(
590 [(policy_grant.grantee, policy, 'ALL')],
591 apgfs.findGranteePermissionsByPolicy(
592 [policy], [grantee_in_result]))
593>>>>>>> MERGE-SOURCE
594539
595 def test_findArtifactsByGrantee(self):540 def test_findArtifactsByGrantee(self):
596 # findArtifactsByGrantee() returns the artifacts for grantee for any of541 # findArtifactsByGrantee() returns the artifacts for grantee for any of
597542
=== modified file 'lib/lp/registry/tests/test_pillar.py'
--- lib/lp/registry/tests/test_pillar.py 2012-03-29 14:36:36 +0000
+++ lib/lp/registry/tests/test_pillar.py 2012-03-22 23:21:24 +0000
@@ -5,20 +5,11 @@
55
6from zope.component import getUtility6from zope.component import getUtility
77
8<<<<<<< TREE8from lp.registry.interfaces.pillar import (
9from lp.registry.interfaces.pillar import (9 IPillarNameSet,
10 IPillarNameSet,10 IPillarPerson,
11 IPillarPerson,11 )
12 )12from lp.registry.model.pillar import PillarPerson
13from lp.registry.model.pillar import PillarPerson
14=======
15from lp.registry.interfaces.pillar import (
16 IPillarNameSet,
17 IPillarPerson,
18 )
19from lp.registry.model.pillar import PillarPerson
20
21>>>>>>> MERGE-SOURCE
22from lp.testing import (13from lp.testing import (
23 login,14 login,
24 TestCaseWithFactory,15 TestCaseWithFactory,
2516
=== modified file 'lib/lp/registry/tests/test_productjob.py'
--- lib/lp/registry/tests/test_productjob.py 2012-03-29 14:36:36 +0000
+++ lib/lp/registry/tests/test_productjob.py 2012-03-24 12:41:36 +0000
@@ -1,4 +1,3 @@
1<<<<<<< TREE
2# Copyright 2010 Canonical Ltd. This software is licensed under the1# Copyright 2010 Canonical Ltd. This software is licensed under the
3# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
43
@@ -370,191 +369,3 @@
370 self.assertEqual(subject, notifications[0]['Subject'])369 self.assertEqual(subject, notifications[0]['Subject'])
371 self.assertIn(370 self.assertIn(
372 'Launchpad <noreply@launchpad.net>', notifications[0]['From'])371 'Launchpad <noreply@launchpad.net>', notifications[0]['From'])
373=======
374# Copyright 2010 Canonical Ltd. This software is licensed under the
375# GNU Affero General Public License version 3 (see the file LICENSE).
376
377"""Tests for ProductJobs."""
378
379__metaclass__ = type
380
381from datetime import (
382 datetime,
383 timedelta,
384 )
385
386import pytz
387
388from zope.interface import (
389 classProvides,
390 implements,
391 )
392from zope.security.proxy import removeSecurityProxy
393
394from lp.registry.enums import ProductJobType
395from lp.registry.interfaces.productjob import (
396 IProductJob,
397 IProductJobSource,
398 )
399from lp.registry.model.productjob import (
400 ProductJob,
401 ProductJobDerived,
402 )
403from lp.testing import TestCaseWithFactory
404from lp.testing.layers import (
405 DatabaseFunctionalLayer,
406 LaunchpadZopelessLayer,
407 )
408
409
410class ProductJobTestCase(TestCaseWithFactory):
411 """Test case for basic ProductJob class."""
412
413 layer = LaunchpadZopelessLayer
414
415 def test_init(self):
416 product = self.factory.makeProduct()
417 metadata = ('some', 'arbitrary', 'metadata')
418 product_job = ProductJob(
419 product, ProductJobType.REVIEWER_NOTIFICATION, metadata)
420 self.assertEqual(product, product_job.product)
421 self.assertEqual(
422 ProductJobType.REVIEWER_NOTIFICATION, product_job.job_type)
423 expected_json_data = '["some", "arbitrary", "metadata"]'
424 self.assertEqual(expected_json_data, product_job._json_data)
425
426 def test_metadata(self):
427 # The python structure stored as json is returned as python.
428 product = self.factory.makeProduct()
429 metadata = {
430 'a_list': ('some', 'arbitrary', 'metadata'),
431 'a_number': 1,
432 'a_string': 'string',
433 }
434 product_job = ProductJob(
435 product, ProductJobType.REVIEWER_NOTIFICATION, metadata)
436 metadata['a_list'] = list(metadata['a_list'])
437 self.assertEqual(metadata, product_job.metadata)
438
439
440class IProductThingJob(IProductJob):
441 """An interface for testing derived job classes."""
442
443
444class IProductThingJobSource(IProductJobSource):
445 """An interface for testing derived job source classes."""
446
447
448class FakeProductJob(ProductJobDerived):
449 """A class that reuses other interfaces and types for testing."""
450 class_job_type = ProductJobType.REVIEWER_NOTIFICATION
451 implements(IProductThingJob)
452 classProvides(IProductThingJobSource)
453
454
455class OtherFakeProductJob(ProductJobDerived):
456 """A class that reuses other interfaces and types for testing."""
457 class_job_type = ProductJobType.COMMERCIAL_EXPIRED
458 implements(IProductThingJob)
459 classProvides(IProductThingJobSource)
460
461
462class ProductJobDerivedTestCase(TestCaseWithFactory):
463 """Test case for the ProductJobDerived class."""
464
465 layer = DatabaseFunctionalLayer
466
467 def test_repr(self):
468 product = self.factory.makeProduct('fnord')
469 metadata = {'foo': 'bar'}
470 job = FakeProductJob.create(product, metadata)
471 self.assertEqual(
472 '<FakeProductJob for fnord status=Waiting>', repr(job))
473
474 def test_create_success(self):
475 # Create an instance of ProductJobDerived that delegates to
476 # ProductJob.
477 product = self.factory.makeProduct()
478 metadata = {'foo': 'bar'}
479 self.assertIs(True, IProductJobSource.providedBy(ProductJobDerived))
480 job = FakeProductJob.create(product, metadata)
481 self.assertIsInstance(job, ProductJobDerived)
482 self.assertIs(True, IProductJob.providedBy(job))
483 self.assertIs(True, IProductJob.providedBy(job.context))
484
485 def test_create_raises_error(self):
486 # ProductJobDerived.create() raises an error because it
487 # needs to be subclassed to work properly.
488 product = self.factory.makeProduct()
489 metadata = {'foo': 'bar'}
490 self.assertRaises(
491 AttributeError, ProductJobDerived.create, product, metadata)
492
493 def test_iterReady(self):
494 # iterReady finds job in the READY status that are of the same type.
495 product = self.factory.makeProduct()
496 metadata = {'foo': 'bar'}
497 job_1 = FakeProductJob.create(product, metadata)
498 job_2 = FakeProductJob.create(product, metadata)
499 job_2.start()
500 OtherFakeProductJob.create(product, metadata)
501 jobs = list(FakeProductJob.iterReady())
502 self.assertEqual(1, len(jobs))
503 self.assertEqual(job_1, jobs[0])
504
505 def test_find_product(self):
506 # Find all the jobs for a product regardless of date or job type.
507 product = self.factory.makeProduct()
508 metadata = {'foo': 'bar'}
509 job_1 = FakeProductJob.create(product, metadata)
510 job_2 = OtherFakeProductJob.create(product, metadata)
511 FakeProductJob.create(self.factory.makeProduct(), metadata)
512 jobs = list(ProductJobDerived.find(product=product))
513 self.assertEqual(2, len(jobs))
514 self.assertContentEqual([job_1.id, job_2.id], [job.id for job in jobs])
515
516 def test_find_job_type(self):
517 # Find all the jobs for a product and job_type regardless of date.
518 product = self.factory.makeProduct()
519 metadata = {'foo': 'bar'}
520 job_1 = FakeProductJob.create(product, metadata)
521 job_2 = FakeProductJob.create(product, metadata)
522 OtherFakeProductJob.create(product, metadata)
523 jobs = list(ProductJobDerived.find(
524 product, job_type=ProductJobType.REVIEWER_NOTIFICATION))
525 self.assertEqual(2, len(jobs))
526 self.assertContentEqual([job_1.id, job_2.id], [job.id for job in jobs])
527
528 def test_find_date_since(self):
529 # Find all the jobs for a product since a date regardless of job_type.
530 now = datetime.now(pytz.utc)
531 seven_days_ago = now - timedelta(7)
532 thirty_days_ago = now - timedelta(30)
533 product = self.factory.makeProduct()
534 metadata = {'foo': 'bar'}
535 job_1 = FakeProductJob.create(product, metadata)
536 removeSecurityProxy(job_1.job).date_created = thirty_days_ago
537 job_2 = FakeProductJob.create(product, metadata)
538 removeSecurityProxy(job_2.job).date_created = seven_days_ago
539 job_3 = OtherFakeProductJob.create(product, metadata)
540 removeSecurityProxy(job_3.job).date_created = now
541 jobs = list(ProductJobDerived.find(product, date_since=seven_days_ago))
542 self.assertEqual(2, len(jobs))
543 self.assertContentEqual([job_2.id, job_3.id], [job.id for job in jobs])
544
545 def test_log_name(self):
546 # The log_name is the name of the implementing class.
547 product = self.factory.makeProduct('fnord')
548 metadata = {'foo': 'bar'}
549 job = FakeProductJob.create(product, metadata)
550 self.assertEqual('FakeProductJob', job.log_name)
551
552 def test_getOopsVars(self):
553 # The project name is added to the oops vars.
554 product = self.factory.makeProduct('fnord')
555 metadata = {'foo': 'bar'}
556 job = FakeProductJob.create(product, metadata)
557 oops_vars = job.getOopsVars()
558 self.assertIs(True, len(oops_vars) > 1)
559 self.assertIn(('product', product.name), oops_vars)
560>>>>>>> MERGE-SOURCE
561372
=== modified file 'lib/lp/registry/tests/test_subscribers.py'
--- lib/lp/registry/tests/test_subscribers.py 2012-03-29 14:36:36 +0000
+++ lib/lp/registry/tests/test_subscribers.py 2012-03-22 23:21:24 +0000
@@ -1,524 +1,260 @@
1<<<<<<< TREE1# Copyright 2012 Canonical Ltd. This software is licensed under the
2# Copyright 2012 Canonical Ltd. This software is licensed under the2# GNU Affero General Public License version 3 (see the file LICENSE).
3# GNU Affero General Public License version 3 (see the file LICENSE).3
44"""Test subscruber classes and functions."""
5"""Test subscruber classes and functions."""5
66__metaclass__ = type
7__metaclass__ = type7
88from datetime import datetime
9from datetime import datetime9
1010from lazr.lifecycle.event import ObjectModifiedEvent
11from lazr.lifecycle.event import ObjectModifiedEvent11import pytz
12import pytz12from zope.security.proxy import removeSecurityProxy
13from zope.security.proxy import removeSecurityProxy13
1414from lp.registry.interfaces.product import License
15from lp.registry.interfaces.product import License15from lp.registry.subscribers import (
16from lp.registry.subscribers import (16 LicenseNotification,
17 LicenseNotification,17 product_licenses_modified,
18 product_licenses_modified,18 )
19 )19from lp.services.webapp.publisher import get_current_browser_request
20from lp.services.webapp.publisher import get_current_browser_request20from lp.testing import (
21from lp.testing import (21 login_person,
22 login_person,22 logout,
23 logout,23 TestCaseWithFactory,
24 TestCaseWithFactory,24 )
25 )25from lp.testing.layers import DatabaseFunctionalLayer
26from lp.testing.layers import DatabaseFunctionalLayer26from lp.testing.mail_helpers import pop_notifications
27from lp.testing.mail_helpers import pop_notifications27
2828
2929class ProductLicensesModifiedTestCase(TestCaseWithFactory):
30class ProductLicensesModifiedTestCase(TestCaseWithFactory):30
3131 layer = DatabaseFunctionalLayer
32 layer = DatabaseFunctionalLayer32
3333 def make_product_event(self, licenses, edited_fields='licenses'):
34 def make_product_event(self, licenses, edited_fields='licenses'):34 product = self.factory.makeProduct(licenses=licenses)
35 product = self.factory.makeProduct(licenses=licenses)35 pop_notifications()
36 pop_notifications()36 login_person(product.owner)
37 login_person(product.owner)37 event = ObjectModifiedEvent(
38 event = ObjectModifiedEvent(38 product, product, edited_fields, user=product.owner)
39 product, product, edited_fields, user=product.owner)39 return product, event
40 return product, event40
4141 def test_product_licenses_modified_licenses_not_edited(self):
42 def test_product_licenses_modified_licenses_not_edited(self):42 product, event = self.make_product_event(
43 product, event = self.make_product_event(43 [License.OTHER_PROPRIETARY], edited_fields='_owner')
44 [License.OTHER_PROPRIETARY], edited_fields='_owner')44 product_licenses_modified(product, event)
45 product_licenses_modified(product, event)45 notifications = pop_notifications()
46 notifications = pop_notifications()46 self.assertEqual(0, len(notifications))
47 self.assertEqual(0, len(notifications))47
4848 def test_product_licenses_modified_licenses_common_license(self):
49 def test_product_licenses_modified_licenses_common_license(self):49 product, event = self.make_product_event([License.MIT])
50 product, event = self.make_product_event([License.MIT])50 product_licenses_modified(product, event)
51 product_licenses_modified(product, event)51 notifications = pop_notifications()
52 notifications = pop_notifications()52 self.assertEqual(0, len(notifications))
53 self.assertEqual(0, len(notifications))53 request = get_current_browser_request()
54 request = get_current_browser_request()54 self.assertEqual(0, len(request.response.notifications))
55 self.assertEqual(0, len(request.response.notifications))55
5656 def test_product_licenses_modified_licenses_other_proprietary(self):
57 def test_product_licenses_modified_licenses_other_proprietary(self):57 product, event = self.make_product_event([License.OTHER_PROPRIETARY])
58 product, event = self.make_product_event([License.OTHER_PROPRIETARY])58 product_licenses_modified(product, event)
59 product_licenses_modified(product, event)59 notifications = pop_notifications()
60 notifications = pop_notifications()60 self.assertEqual(1, len(notifications))
61 self.assertEqual(1, len(notifications))61 request = get_current_browser_request()
62 request = get_current_browser_request()62 self.assertEqual(1, len(request.response.notifications))
63 self.assertEqual(1, len(request.response.notifications))63
6464 def test_product_licenses_modified_licenses_other_open_source(self):
65 def test_product_licenses_modified_licenses_other_open_source(self):65 product, event = self.make_product_event([License.OTHER_OPEN_SOURCE])
66 product, event = self.make_product_event([License.OTHER_OPEN_SOURCE])66 product_licenses_modified(product, event)
67 product_licenses_modified(product, event)67 notifications = pop_notifications()
68 notifications = pop_notifications()68 self.assertEqual(1, len(notifications))
69 self.assertEqual(1, len(notifications))69 request = get_current_browser_request()
70 request = get_current_browser_request()70 self.assertEqual(0, len(request.response.notifications))
71 self.assertEqual(0, len(request.response.notifications))71
7272 def test_product_licenses_modified_licenses_other_dont_know(self):
73 def test_product_licenses_modified_licenses_other_dont_know(self):73 product, event = self.make_product_event([License.DONT_KNOW])
74 product, event = self.make_product_event([License.DONT_KNOW])74 product_licenses_modified(product, event)
75 product_licenses_modified(product, event)75 notifications = pop_notifications()
76 notifications = pop_notifications()76 self.assertEqual(1, len(notifications))
77 self.assertEqual(1, len(notifications))77 request = get_current_browser_request()
78 request = get_current_browser_request()78 self.assertEqual(0, len(request.response.notifications))
79 self.assertEqual(0, len(request.response.notifications))79
8080
8181class LicenseNotificationTestCase(TestCaseWithFactory):
82class LicenseNotificationTestCase(TestCaseWithFactory):82
8383 layer = DatabaseFunctionalLayer
84 layer = DatabaseFunctionalLayer84
8585 def make_product_user(self, licenses):
86 def make_product_user(self, licenses):86 # Setup an a view that implements ProductLicenseMixin.
87 # Setup an a view that implements ProductLicenseMixin.87 super(LicenseNotificationTestCase, self).setUp()
88 super(LicenseNotificationTestCase, self).setUp()88 user = self.factory.makePerson(
89 user = self.factory.makePerson(89 name='registrant', email='registrant@launchpad.dev')
90 name='registrant', email='registrant@launchpad.dev')90 login_person(user)
91 login_person(user)91 product = self.factory.makeProduct(
92 product = self.factory.makeProduct(92 name='ball', owner=user, licenses=licenses)
93 name='ball', owner=user, licenses=licenses)93 pop_notifications()
94 pop_notifications()94 return product, user
95 return product, user95
9696 def verify_whiteboard(self, product):
97 def verify_whiteboard(self, product):97 # Verify that the review whiteboard was updated.
98 # Verify that the review whiteboard was updated.98 naked_product = removeSecurityProxy(product)
99 naked_product = removeSecurityProxy(product)99 entries = naked_product.reviewer_whiteboard.split('\n')
100 entries = naked_product.reviewer_whiteboard.split('\n')100 whiteboard, stamp = entries[-1].rsplit(' ', 1)
101 whiteboard, stamp = entries[-1].rsplit(' ', 1)101 self.assertEqual(
102 self.assertEqual(102 'User notified of license policy on', whiteboard)
103 'User notified of license policy on', whiteboard)103
104104 def verify_user_email(self, notification):
105 def verify_user_email(self, notification):105 # Verify that the user was sent an email about the license change.
106 # Verify that the user was sent an email about the license change.106 self.assertEqual(
107 self.assertEqual(107 'License information for ball in Launchpad',
108 'License information for ball in Launchpad',108 notification['Subject'])
109 notification['Subject'])109 self.assertEqual(
110 self.assertEqual(110 'Registrant <registrant@launchpad.dev>',
111 'Registrant <registrant@launchpad.dev>',111 notification['To'])
112 notification['To'])112 self.assertEqual(
113 self.assertEqual(113 'Commercial <commercial@launchpad.net>',
114 'Commercial <commercial@launchpad.net>',114 notification['Reply-To'])
115 notification['Reply-To'])115
116116 def test_send_known_license(self):
117 def test_send_known_license(self):117 # A known license does not generate an email.
118 # A known license does not generate an email.118 product, user = self.make_product_user([License.GNU_GPL_V2])
119 product, user = self.make_product_user([License.GNU_GPL_V2])119 notification = LicenseNotification(product, user)
120 notification = LicenseNotification(product, user)120 result = notification.send()
121 result = notification.send()121 self.assertIs(False, result)
122 self.assertIs(False, result)122 self.assertEqual(0, len(pop_notifications()))
123 self.assertEqual(0, len(pop_notifications()))123
124124 def test_send_other_dont_know(self):
125 def test_send_other_dont_know(self):125 # An Other/I don't know license sends one email.
126 # An Other/I don't know license sends one email.126 product, user = self.make_product_user([License.DONT_KNOW])
127 product, user = self.make_product_user([License.DONT_KNOW])127 notification = LicenseNotification(product, user)
128 notification = LicenseNotification(product, user)128 result = notification.send()
129 result = notification.send()129 self.assertIs(True, result)
130 self.assertIs(True, result)130 self.verify_whiteboard(product)
131 self.verify_whiteboard(product)131 notifications = pop_notifications()
132 notifications = pop_notifications()132 self.assertEqual(1, len(notifications))
133 self.assertEqual(1, len(notifications))133 self.verify_user_email(notifications.pop())
134 self.verify_user_email(notifications.pop())134
135135 def test_send_other_open_source(self):
136 def test_send_other_open_source(self):136 # An Other/Open Source license sends one email.
137 # An Other/Open Source license sends one email.137 product, user = self.make_product_user([License.OTHER_OPEN_SOURCE])
138 product, user = self.make_product_user([License.OTHER_OPEN_SOURCE])138 notification = LicenseNotification(product, user)
139 notification = LicenseNotification(product, user)139 result = notification.send()
140 result = notification.send()140 self.assertIs(True, result)
141 self.assertIs(True, result)141 self.verify_whiteboard(product)
142 self.verify_whiteboard(product)142 notifications = pop_notifications()
143 notifications = pop_notifications()143 self.assertEqual(1, len(notifications))
144 self.assertEqual(1, len(notifications))144 self.verify_user_email(notifications.pop())
145 self.verify_user_email(notifications.pop())145
146146 def test_send_other_proprietary(self):
147 def test_send_other_proprietary(self):147 # An Other/Proprietary license sends one email.
148 # An Other/Proprietary license sends one email.148 product, user = self.make_product_user([License.OTHER_PROPRIETARY])
149 product, user = self.make_product_user([License.OTHER_PROPRIETARY])149 notification = LicenseNotification(product, user)
150 notification = LicenseNotification(product, user)150 result = notification.send()
151 result = notification.send()151 self.assertIs(True, result)
152 self.assertIs(True, result)152 self.verify_whiteboard(product)
153 self.verify_whiteboard(product)153 notifications = pop_notifications()
154 notifications = pop_notifications()154 self.assertEqual(1, len(notifications))
155 self.assertEqual(1, len(notifications))155 self.verify_user_email(notifications.pop())
156 self.verify_user_email(notifications.pop())156
157157 def test_display_no_request(self):
158 def test_display_no_request(self):158 # If there is no request, there is no reason to show a message in
159 # If there is no request, there is no reason to show a message in159 # the browser.
160 # the browser.160 product, user = self.make_product_user([License.GNU_GPL_V2])
161 product, user = self.make_product_user([License.GNU_GPL_V2])161 notification = LicenseNotification(product, user)
162 notification = LicenseNotification(product, user)162 logout()
163 logout()163 result = notification.display()
164 result = notification.display()164 self.assertIs(False, result)
165 self.assertIs(False, result)165
166166 def test_display_no_message(self):
167 def test_display_no_message(self):167 # A notification is not added if there is no message to show.
168 # A notification is not added if there is no message to show.168 product, user = self.make_product_user([License.GNU_GPL_V2])
169 product, user = self.make_product_user([License.GNU_GPL_V2])169 notification = LicenseNotification(product, user)
170 notification = LicenseNotification(product, user)170 result = notification.display()
171 result = notification.display()171 self.assertEqual('', notification.getCommercialUseMessage())
172 self.assertEqual('', notification.getCommercialUseMessage())172 self.assertIs(False, result)
173 self.assertIs(False, result)173
174174 def test_display_has_message(self):
175 def test_display_has_message(self):175 # A notification is added if there is a message to show.
176 # A notification is added if there is a message to show.176 product, user = self.make_product_user([License.OTHER_PROPRIETARY])
177 product, user = self.make_product_user([License.OTHER_PROPRIETARY])177 notification = LicenseNotification(product, user)
178 notification = LicenseNotification(product, user)178 result = notification.display()
179 result = notification.display()179 message = notification.getCommercialUseMessage()
180 message = notification.getCommercialUseMessage()180 self.assertIs(True, result)
181 self.assertIs(True, result)181 request = get_current_browser_request()
182 request = get_current_browser_request()182 self.assertEqual(1, len(request.response.notifications))
183 self.assertEqual(1, len(request.response.notifications))183 self.assertIn(message, request.response.notifications[0].message)
184 self.assertIn(message, request.response.notifications[0].message)184 self.assertIn(
185 self.assertIn(185 '<a href="https://help.launchpad.net/CommercialHosting">',
186 '<a href="https://help.launchpad.net/CommercialHosting">',186 request.response.notifications[0].message)
187 request.response.notifications[0].message)187
188188 def test_display_escapee_user_data(self):
189 def test_display_escapee_user_data(self):189 # A notification is added if there is a message to show.
190 # A notification is added if there is a message to show.190 product, user = self.make_product_user([License.OTHER_PROPRIETARY])
191 product, user = self.make_product_user([License.OTHER_PROPRIETARY])191 product.displayname = '<b>Look</b>'
192 product.displayname = '<b>Look</b>'192 notification = LicenseNotification(product, user)
193 notification = LicenseNotification(product, user)193 result = notification.display()
194 result = notification.display()194 self.assertIs(True, result)
195 self.assertIs(True, result)195 request = get_current_browser_request()
196 request = get_current_browser_request()196 self.assertEqual(1, len(request.response.notifications))
197 self.assertEqual(1, len(request.response.notifications))197 self.assertIn(
198 self.assertIn(198 '&lt;b&gt;Look&lt;/b&gt;',
199 '&lt;b&gt;Look&lt;/b&gt;',199 request.response.notifications[0].message)
200 request.response.notifications[0].message)200
201201 def test_formatDate(self):
202 def test_formatDate(self):202 # Verify the date format.
203 # Verify the date format.203 now = datetime(2005, 6, 15, 0, 0, 0, 0, pytz.UTC)
204 now = datetime(2005, 6, 15, 0, 0, 0, 0, pytz.UTC)204 result = LicenseNotification._formatDate(now)
205 result = LicenseNotification._formatDate(now)205 self.assertEqual('2005-06-15', result)
206 self.assertEqual('2005-06-15', result)206
207207 def test_getTemplateName_other_dont_know(self):
208 def test_getTemplateName_other_dont_know(self):208 product, user = self.make_product_user([License.DONT_KNOW])
209 product, user = self.make_product_user([License.DONT_KNOW])209 notification = LicenseNotification(product, user)
210 notification = LicenseNotification(product, user)210 self.assertEqual(
211 self.assertEqual(211 'product-license-dont-know.txt',
212 'product-license-dont-know.txt',212 notification.getTemplateName())
213 notification.getTemplateName())213
214214 def test_getTemplateName_propietary(self):
215 def test_getTemplateName_propietary(self):215 product, user = self.make_product_user([License.OTHER_PROPRIETARY])
216 product, user = self.make_product_user([License.OTHER_PROPRIETARY])216 notification = LicenseNotification(product, user)
217 notification = LicenseNotification(product, user)217 self.assertEqual(
218 self.assertEqual(218 'product-license-other-proprietary.txt',
219 'product-license-other-proprietary.txt',219 notification.getTemplateName())
220 notification.getTemplateName())220
221221 def test_getTemplateName_other_open_source(self):
222 def test_getTemplateName_other_open_source(self):222 product, user = self.make_product_user([License.OTHER_OPEN_SOURCE])
223 product, user = self.make_product_user([License.OTHER_OPEN_SOURCE])223 notification = LicenseNotification(product, user)
224 notification = LicenseNotification(product, user)224 self.assertEqual(
225 self.assertEqual(225 'product-license-other-open-source.txt',
226 'product-license-other-open-source.txt',226 notification.getTemplateName())
227 notification.getTemplateName())227
228228 def test_getCommercialUseMessage_without_commercial_subscription(self):
229 def test_getCommercialUseMessage_without_commercial_subscription(self):229 product, user = self.make_product_user([License.MIT])
230 product, user = self.make_product_user([License.MIT])230 notification = LicenseNotification(product, user)
231 notification = LicenseNotification(product, user)231 self.assertEqual('', notification.getCommercialUseMessage())
232 self.assertEqual('', notification.getCommercialUseMessage())232
233233 def test_getCommercialUseMessage_with_complimentary_cs(self):
234 def test_getCommercialUseMessage_with_complimentary_cs(self):234 product, user = self.make_product_user([License.OTHER_PROPRIETARY])
235 product, user = self.make_product_user([License.OTHER_PROPRIETARY])235 notification = LicenseNotification(product, user)
236 notification = LicenseNotification(product, user)236 message = (
237 message = (237 "Ball's complimentary commercial subscription expires on %s." %
238 "Ball's complimentary commercial subscription expires on %s." %238 product.commercial_subscription.date_expires.date().isoformat())
239 product.commercial_subscription.date_expires.date().isoformat())239 self.assertEqual(message, notification.getCommercialUseMessage())
240 self.assertEqual(message, notification.getCommercialUseMessage())240
241241 def test_getCommercialUseMessage_with_commercial_subscription(self):
242 def test_getCommercialUseMessage_with_commercial_subscription(self):242 product, user = self.make_product_user([License.MIT])
243 product, user = self.make_product_user([License.MIT])243 self.factory.makeCommercialSubscription(product)
244 self.factory.makeCommercialSubscription(product)244 product.licenses = [License.MIT, License.OTHER_PROPRIETARY]
245 product.licenses = [License.MIT, License.OTHER_PROPRIETARY]245 notification = LicenseNotification(product, user)
246 notification = LicenseNotification(product, user)246 message = (
247 message = (247 "Ball's commercial subscription expires on %s." %
248 "Ball's commercial subscription expires on %s." %248 product.commercial_subscription.date_expires.date().isoformat())
249 product.commercial_subscription.date_expires.date().isoformat())249 self.assertEqual(message, notification.getCommercialUseMessage())
250 self.assertEqual(message, notification.getCommercialUseMessage())250
251251 def test_getCommercialUseMessage_with_expired_cs(self):
252 def test_getCommercialUseMessage_with_expired_cs(self):252 product, user = self.make_product_user([License.MIT])
253 product, user = self.make_product_user([License.MIT])253 self.factory.makeCommercialSubscription(product, expired=True)
254 self.factory.makeCommercialSubscription(product, expired=True)254 product.licenses = [License.MIT, License.OTHER_PROPRIETARY]
255 product.licenses = [License.MIT, License.OTHER_PROPRIETARY]255 notification = LicenseNotification(product, user)
256 notification = LicenseNotification(product, user)256 message = (
257 message = (257 "Ball's commercial subscription expired on %s." %
258 "Ball's commercial subscription expired on %s." %258 product.commercial_subscription.date_expires.date().isoformat())
259 product.commercial_subscription.date_expires.date().isoformat())259 self.assertEqual(message, notification.getCommercialUseMessage())
260 self.assertEqual(message, notification.getCommercialUseMessage())260 self.assertEqual(message, notification.getCommercialUseMessage())
261 self.assertEqual(message, notification.getCommercialUseMessage())
262=======
263# Copyright 2012 Canonical Ltd. This software is licensed under the
264# GNU Affero General Public License version 3 (see the file LICENSE).
265
266"""Test subscruber classes and functions."""
267
268__metaclass__ = type
269
270from datetime import datetime
271
272import pytz
273
274from zope.security.proxy import removeSecurityProxy
275from lazr.lifecycle.event import ObjectModifiedEvent
276
277from lp.registry.interfaces.product import License
278from lp.registry.subscribers import (
279 LicenseNotification,
280 product_licenses_modified,
281 )
282from lp.services.webapp.publisher import get_current_browser_request
283from lp.testing import (
284 login_person,
285 logout,
286 TestCaseWithFactory,
287 )
288from lp.testing.layers import DatabaseFunctionalLayer
289from lp.testing.mail_helpers import pop_notifications
290
291
292class ProductLicensesModifiedTestCase(TestCaseWithFactory):
293
294 layer = DatabaseFunctionalLayer
295
296 def make_product_event(self, licenses, edited_fields='licenses'):
297 product = self.factory.makeProduct(licenses=licenses)
298 pop_notifications()
299 login_person(product.owner)
300 event = ObjectModifiedEvent(
301 product, product, edited_fields, user=product.owner)
302 return product, event
303
304 def test_product_licenses_modified_licenses_not_edited(self):
305 product, event = self.make_product_event(
306 [License.OTHER_PROPRIETARY], edited_fields='_owner')
307 product_licenses_modified(product, event)
308 notifications = pop_notifications()
309 self.assertEqual(0, len(notifications))
310
311 def test_product_licenses_modified_licenses_common_license(self):
312 product, event = self.make_product_event([License.MIT])
313 product_licenses_modified(product, event)
314 notifications = pop_notifications()
315 self.assertEqual(0, len(notifications))
316 request = get_current_browser_request()
317 self.assertEqual(0, len(request.response.notifications))
318
319 def test_product_licenses_modified_licenses_other_proprietary(self):
320 product, event = self.make_product_event([License.OTHER_PROPRIETARY])
321 product_licenses_modified(product, event)
322 notifications = pop_notifications()
323 self.assertEqual(1, len(notifications))
324 request = get_current_browser_request()
325 self.assertEqual(1, len(request.response.notifications))
326
327 def test_product_licenses_modified_licenses_other_open_source(self):
328 product, event = self.make_product_event([License.OTHER_OPEN_SOURCE])
329 product_licenses_modified(product, event)
330 notifications = pop_notifications()
331 self.assertEqual(1, len(notifications))
332 request = get_current_browser_request()
333 self.assertEqual(0, len(request.response.notifications))
334
335 def test_product_licenses_modified_licenses_other_dont_know(self):
336 product, event = self.make_product_event([License.DONT_KNOW])
337 product_licenses_modified(product, event)
338 notifications = pop_notifications()
339 self.assertEqual(1, len(notifications))
340 request = get_current_browser_request()
341 self.assertEqual(0, len(request.response.notifications))
342
343
344class LicenseNotificationTestCase(TestCaseWithFactory):
345
346 layer = DatabaseFunctionalLayer
347
348 def make_product_user(self, licenses):
349 # Setup an a view that implements ProductLicenseMixin.
350 super(LicenseNotificationTestCase, self).setUp()
351 user = self.factory.makePerson(
352 name='registrant', email='registrant@launchpad.dev')
353 login_person(user)
354 product = self.factory.makeProduct(
355 name='ball', owner=user, licenses=licenses)
356 pop_notifications()
357 return product, user
358
359 def verify_whiteboard(self, product):
360 # Verify that the review whiteboard was updated.
361 naked_product = removeSecurityProxy(product)
362 entries = naked_product.reviewer_whiteboard.split('\n')
363 whiteboard, stamp = entries[-1].rsplit(' ', 1)
364 self.assertEqual(
365 'User notified of license policy on', whiteboard)
366
367 def verify_user_email(self, notification):
368 # Verify that the user was sent an email about the license change.
369 self.assertEqual(
370 'License information for ball in Launchpad',
371 notification['Subject'])
372 self.assertEqual(
373 'Registrant <registrant@launchpad.dev>',
374 notification['To'])
375 self.assertEqual(
376 'Commercial <commercial@launchpad.net>',
377 notification['Reply-To'])
378
379 def test_send_known_license(self):
380 # A known license does not generate an email.
381 product, user = self.make_product_user([License.GNU_GPL_V2])
382 notification = LicenseNotification(product, user)
383 result = notification.send()
384 self.assertIs(False, result)
385 self.assertEqual(0, len(pop_notifications()))
386
387 def test_send_other_dont_know(self):
388 # An Other/I don't know license sends one email.
389 product, user = self.make_product_user([License.DONT_KNOW])
390 notification = LicenseNotification(product, user)
391 result = notification.send()
392 self.assertIs(True, result)
393 self.verify_whiteboard(product)
394 notifications = pop_notifications()
395 self.assertEqual(1, len(notifications))
396 self.verify_user_email(notifications.pop())
397
398 def test_send_other_open_source(self):
399 # An Other/Open Source license sends one email.
400 product, user = self.make_product_user([License.OTHER_OPEN_SOURCE])
401 notification = LicenseNotification(product, user)
402 result = notification.send()
403 self.assertIs(True, result)
404 self.verify_whiteboard(product)
405 notifications = pop_notifications()
406 self.assertEqual(1, len(notifications))
407 self.verify_user_email(notifications.pop())
408
409 def test_send_other_proprietary(self):
410 # An Other/Proprietary license sends one email.
411 product, user = self.make_product_user([License.OTHER_PROPRIETARY])
412 notification = LicenseNotification(product, user)
413 result = notification.send()
414 self.assertIs(True, result)
415 self.verify_whiteboard(product)
416 notifications = pop_notifications()
417 self.assertEqual(1, len(notifications))
418 self.verify_user_email(notifications.pop())
419
420 def test_display_no_request(self):
421 # If there is no request, there is no reason to show a message in
422 # the browser.
423 product, user = self.make_product_user([License.GNU_GPL_V2])
424 notification = LicenseNotification(product, user)
425 logout()
426 result = notification.display()
427 self.assertIs(False, result)
428
429 def test_display_no_message(self):
430 # A notification is not added if there is no message to show.
431 product, user = self.make_product_user([License.GNU_GPL_V2])
432 notification = LicenseNotification(product, user)
433 result = notification.display()
434 self.assertEqual('', notification.getCommercialUseMessage())
435 self.assertIs(False, result)
436
437 def test_display_has_message(self):
438 # A notification is added if there is a message to show.
439 product, user = self.make_product_user([License.OTHER_PROPRIETARY])
440 notification = LicenseNotification(product, user)
441 result = notification.display()
442 message = notification.getCommercialUseMessage()
443 self.assertIs(True, result)
444 request = get_current_browser_request()
445 self.assertEqual(1, len(request.response.notifications))
446 self.assertIn(message, request.response.notifications[0].message)
447 self.assertIn(
448 '<a href="https://help.launchpad.net/CommercialHosting">',
449 request.response.notifications[0].message)
450
451 def test_display_escapee_user_data(self):
452 # A notification is added if there is a message to show.
453 product, user = self.make_product_user([License.OTHER_PROPRIETARY])
454 product.displayname = '<b>Look</b>'
455 notification = LicenseNotification(product, user)
456 result = notification.display()
457 self.assertIs(True, result)
458 request = get_current_browser_request()
459 self.assertEqual(1, len(request.response.notifications))
460 self.assertIn(
461 '&lt;b&gt;Look&lt;/b&gt;',
462 request.response.notifications[0].message)
463
464 def test_formatDate(self):
465 # Verify the date format.
466 now = datetime(2005, 6, 15, 0, 0, 0, 0, pytz.UTC)
467 result = LicenseNotification._formatDate(now)
468 self.assertEqual('2005-06-15', result)
469
470 def test_getTemplateName_other_dont_know(self):
471 product, user = self.make_product_user([License.DONT_KNOW])
472 notification = LicenseNotification(product, user)
473 self.assertEqual(
474 'product-license-dont-know.txt',
475 notification.getTemplateName())
476
477 def test_getTemplateName_propietary(self):
478 product, user = self.make_product_user([License.OTHER_PROPRIETARY])
479 notification = LicenseNotification(product, user)
480 self.assertEqual(
481 'product-license-other-proprietary.txt',
482 notification.getTemplateName())
483
484 def test_getTemplateName_other_open_source(self):
485 product, user = self.make_product_user([License.OTHER_OPEN_SOURCE])
486 notification = LicenseNotification(product, user)
487 self.assertEqual(
488 'product-license-other-open-source.txt',
489 notification.getTemplateName())
490
491 def test_getCommercialUseMessage_without_commercial_subscription(self):
492 product, user = self.make_product_user([License.MIT])
493 notification = LicenseNotification(product, user)
494 self.assertEqual('', notification.getCommercialUseMessage())
495
496 def test_getCommercialUseMessage_with_complimentary_cs(self):
497 product, user = self.make_product_user([License.OTHER_PROPRIETARY])
498 notification = LicenseNotification(product, user)
499 message = (
500 "Ball's complimentary commercial subscription expires on %s." %
501 product.commercial_subscription.date_expires.date().isoformat())
502 self.assertEqual(message, notification.getCommercialUseMessage())
503
504 def test_getCommercialUseMessage_with_commercial_subscription(self):
505 product, user = self.make_product_user([License.MIT])
506 self.factory.makeCommercialSubscription(product)
507 product.licenses = [License.MIT, License.OTHER_PROPRIETARY]
508 notification = LicenseNotification(product, user)
509 message = (
510 "Ball's commercial subscription expires on %s." %
511 product.commercial_subscription.date_expires.date().isoformat())
512 self.assertEqual(message, notification.getCommercialUseMessage())
513
514 def test_getCommercialUseMessage_with_expired_cs(self):
515 product, user = self.make_product_user([License.MIT])
516 self.factory.makeCommercialSubscription(product, expired=True)
517 product.licenses = [License.MIT, License.OTHER_PROPRIETARY]
518 notification = LicenseNotification(product, user)
519 message = (
520 "Ball's commercial subscription expired on %s." %
521 product.commercial_subscription.date_expires.date().isoformat())
522 self.assertEqual(message, notification.getCommercialUseMessage())
523 self.assertEqual(message, notification.getCommercialUseMessage())
524>>>>>>> MERGE-SOURCE
525261
=== modified file 'lib/lp/scripts/garbo.py'
--- lib/lp/scripts/garbo.py 2012-03-29 14:36:36 +0000
+++ lib/lp/scripts/garbo.py 2012-03-26 22:47:47 +0000
@@ -81,7 +81,6 @@
81from lp.services.librarian.model import TimeLimitedToken81from lp.services.librarian.model import TimeLimitedToken
82from lp.services.log.logger import PrefixFilter82from lp.services.log.logger import PrefixFilter
83from lp.services.looptuner import TunableLoop83from lp.services.looptuner import TunableLoop
84from lp.services.memcache.interfaces import IMemcacheClient
85from lp.services.oauth.model import OAuthNonce84from lp.services.oauth.model import OAuthNonce
86from lp.services.openid.model.openidconsumer import OpenIDConsumerNonce85from lp.services.openid.model.openidconsumer import OpenIDConsumerNonce
87from lp.services.propertycache import cachedproperty86from lp.services.propertycache import cachedproperty
@@ -1092,43 +1091,6 @@
1092 self.offset += chunk_size1091 self.offset += chunk_size
10931092
10941093
1095<<<<<<< TREE
1096=======
1097class BugLegacyAccessMirrorer(TunableLoop):
1098 """A `TunableLoop` to populate the access policy schema for all bugs."""
1099
1100 maximum_chunk_size = 5000
1101
1102 def __init__(self, log, abort_time=None):
1103 super(BugLegacyAccessMirrorer, self).__init__(log, abort_time)
1104 watermark = getUtility(IMemcacheClient).get(
1105 '%s:bug-legacy-access-mirrorer' % config.instance_name)
1106 self.start_at = watermark or 0
1107
1108 def findBugIDs(self):
1109 return IMasterStore(Bug).find(
1110 (Bug.id,), Bug.id >= self.start_at).order_by(Bug.id)
1111
1112 def isDone(self):
1113 return self.findBugIDs().is_empty()
1114
1115 def __call__(self, chunk_size):
1116 ids = [row[0] for row in self.findBugIDs()[:chunk_size]]
1117 list(IMasterStore(Bug).using(Bug).find(
1118 SQL('bug_mirror_legacy_access(Bug.id)'),
1119 Bug.id.is_in(ids)))
1120
1121 self.start_at = ids[-1] + 1
1122 result = getUtility(IMemcacheClient).set(
1123 '%s:bug-legacy-access-mirrorer' % config.instance_name,
1124 self.start_at)
1125 if not result:
1126 self.log.warning('Failed to set start_at in memcache.')
1127
1128 transaction.commit()
1129
1130
1131>>>>>>> MERGE-SOURCE
1132class BaseDatabaseGarbageCollector(LaunchpadCronScript):1094class BaseDatabaseGarbageCollector(LaunchpadCronScript):
1133 """Abstract base class to run a collection of TunableLoops."""1095 """Abstract base class to run a collection of TunableLoops."""
1134 script_name = None # Script name for locking and database user. Override.1096 script_name = None # Script name for locking and database user. Override.
@@ -1380,10 +1342,6 @@
1380 UnusedSessionPruner,1342 UnusedSessionPruner,
1381 DuplicateSessionPruner,1343 DuplicateSessionPruner,
1382 BugHeatUpdater,1344 BugHeatUpdater,
1383<<<<<<< TREE
1384=======
1385 BugLegacyAccessMirrorer,
1386>>>>>>> MERGE-SOURCE
1387 ]1345 ]
1388 experimental_tunable_loops = []1346 experimental_tunable_loops = []
13891347
13901348
=== modified file 'lib/lp/scripts/tests/test_garbo.py'
--- lib/lp/scripts/tests/test_garbo.py 2012-03-29 14:36:36 +0000
+++ lib/lp/scripts/tests/test_garbo.py 2012-03-26 22:47:47 +0000
@@ -54,10 +54,6 @@
54 )54 )
55from lp.code.model.codeimportevent import CodeImportEvent55from lp.code.model.codeimportevent import CodeImportEvent
56from lp.code.model.codeimportresult import CodeImportResult56from lp.code.model.codeimportresult import CodeImportResult
57<<<<<<< TREE
58=======
59from lp.registry.interfaces.accesspolicy import IAccessArtifactSource
60>>>>>>> MERGE-SOURCE
61from lp.registry.interfaces.distribution import IDistributionSet57from lp.registry.interfaces.distribution import IDistributionSet
62from lp.registry.interfaces.person import IPersonSet58from lp.registry.interfaces.person import IPersonSet
63from lp.scripts.garbo import (59from lp.scripts.garbo import (
@@ -1108,29 +1104,6 @@
1108 self.assertEqual(whiteboard, spec.whiteboard)1104 self.assertEqual(whiteboard, spec.whiteboard)
1109 self.assertEqual(0, spec.work_items.count())1105 self.assertEqual(0, spec.work_items.count())
11101106
1111<<<<<<< TREE
1112=======
1113 def test_BugLegacyAccessMirrorer(self):
1114 # Private bugs without corresponding data in the access policy
1115 # schema get mirrored.
1116 switch_dbuser('testadmin')
1117 bug = self.factory.makeBug(private=True)
1118 # Remove the existing mirrored data.
1119 getUtility(IAccessArtifactSource).delete([bug])
1120 transaction.commit()
1121 self.runHourly()
1122 # Check that there's an artifact again, and delete it.
1123 switch_dbuser('testadmin')
1124 [artifact] = getUtility(IAccessArtifactSource).find([bug])
1125 getUtility(IAccessArtifactSource).delete([bug])
1126 transaction.commit()
1127 self.runHourly()
1128 # A watermark is kept in memcache, so a second run doesn't
1129 # consider the same bug.
1130 self.assertContentEqual(
1131 [], getUtility(IAccessArtifactSource).find([bug]))
1132
1133>>>>>>> MERGE-SOURCE
11341107
1135class TestGarboTasks(TestCaseWithFactory):1108class TestGarboTasks(TestCaseWithFactory):
1136 layer = LaunchpadZopelessLayer1109 layer = LaunchpadZopelessLayer
11371110
=== modified file 'lib/lp/services/features/flags.py'
--- lib/lp/services/features/flags.py 2012-03-29 14:36:36 +0000
+++ lib/lp/services/features/flags.py 2012-03-26 21:23:40 +0000
@@ -290,7 +290,6 @@
290 ('disclosure.enhanced_sharing.enabled',290 ('disclosure.enhanced_sharing.enabled',
291 'boolean',291 'boolean',
292 ('If true, will allow the use of the new sharing view and apis used '292 ('If true, will allow the use of the new sharing view and apis used '
293<<<<<<< TREE
294 'for the new disclosure data model to view but not write data.'),293 'for the new disclosure data model to view but not write data.'),
295 '',294 '',
296 'Sharing overview',295 'Sharing overview',
@@ -308,18 +307,6 @@
308 'to edit the new disclosure data model.'),307 'to edit the new disclosure data model.'),
309 '',308 '',
310 'Sharing management',309 'Sharing management',
311=======
312 'for the new disclosure data model to view but not write data.'),
313 '',
314 'Sharing overview',
315 ''),
316 ('disclosure.enhanced_sharing.writable',
317 'boolean',
318 ('If true, will allow the use of the new sharing view and apis used '
319 'to edit the new disclosure data model.'),
320 '',
321 'Sharing management',
322>>>>>>> MERGE-SOURCE
323 ''),310 ''),
324 ('garbo.workitem_migrator.enabled',311 ('garbo.workitem_migrator.enabled',
325 'boolean',312 'boolean',
326313
=== added file 'lib/lp/services/job/celeryconfig.py'
--- lib/lp/services/job/celeryconfig.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/job/celeryconfig.py 2012-03-29 14:36:38 +0000
@@ -0,0 +1,4 @@
1from lp.services.config import config
2BROKER_URL = "amqplib://%s" % config.rabbitmq.host
3CELERY_IMPORTS = ("lp.services.job.celeryjob", )
4CELERY_RESULT_BACKEND = "amqp"
05
=== added file 'lib/lp/services/job/celeryjob.py'
--- lib/lp/services/job/celeryjob.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/job/celeryjob.py 2012-03-29 14:36:38 +0000
@@ -0,0 +1,30 @@
1# Copyright 2012 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Celery-specific Job code.
5
6Because celery sets up configuration at import time, code that is not designed
7to use Celery may break if this is used.
8"""
9
10__metaclass__ = type
11
12__all__ = ['CeleryRunJob']
13
14import os
15
16os.environ.setdefault('CELERY_CONFIG_MODULE', 'lp.services.job.celeryconfig')
17from lazr.jobrunner.celerytask import RunJob
18
19from lp.services.job.model.job import UniversalJobSource
20from lp.services.job.runner import BaseJobRunner
21
22
23class CeleryRunJob(RunJob):
24 """The Celery Task that runs a job."""
25
26 job_source = UniversalJobSource
27
28 def getJobRunner(self):
29 """Return a BaseJobRunner, to support customization."""
30 return BaseJobRunner()
031
=== modified file 'lib/lp/services/job/interfaces/job.py'
--- lib/lp/services/job/interfaces/job.py 2012-02-24 21:46:11 +0000
+++ lib/lp/services/job/interfaces/job.py 2012-03-29 14:36:38 +0000
@@ -13,7 +13,6 @@
13 'IRunnableJob',13 'IRunnableJob',
14 'ITwistedJobSource',14 'ITwistedJobSource',
15 'JobStatus',15 'JobStatus',
16 'LeaseHeld',
17 ]16 ]
1817
1918
@@ -38,13 +37,6 @@
38from lp.registry.interfaces.person import IPerson37from lp.registry.interfaces.person import IPerson
3938
4039
41class LeaseHeld(Exception):
42 """Raised when attempting to acquire a list that is already held."""
43
44 def __init__(self):
45 Exception.__init__(self, 'Lease is already held.')
46
47
48class JobStatus(DBEnumeratedType):40class JobStatus(DBEnumeratedType):
49 """Values that ICodeImportJob.state can take."""41 """Values that ICodeImportJob.state can take."""
5042
5143
=== modified file 'lib/lp/services/job/model/job.py'
--- lib/lp/services/job/model/job.py 2012-03-14 16:32:59 +0000
+++ lib/lp/services/job/model/job.py 2012-03-29 14:36:38 +0000
@@ -4,13 +4,21 @@
4"""ORM object representing jobs."""4"""ORM object representing jobs."""
55
6__metaclass__ = type6__metaclass__ = type
7__all__ = ['InvalidTransition', 'Job', 'JobStatus']7__all__ = [
8 'EnumeratedSubclass',
9 'InvalidTransition',
10 'Job',
11 'JobStatus',
12 'UniversalJobSource',
13 ]
814
915
10from calendar import timegm16from calendar import timegm
11import datetime17import datetime
12import time18import time
1319
20from lazr.jobrunner.jobrunner import LeaseHeld
21
14import pytz22import pytz
15from sqlobject import (23from sqlobject import (
16 IntCol,24 IntCol,
@@ -28,16 +36,18 @@
28import transaction36import transaction
29from zope.interface import implements37from zope.interface import implements
3038
39from lp.services.config import dbconfig
31from lp.services.database import bulk40from lp.services.database import bulk
32from lp.services.database.constants import UTC_NOW41from lp.services.database.constants import UTC_NOW
33from lp.services.database.datetimecol import UtcDateTimeCol42from lp.services.database.datetimecol import UtcDateTimeCol
34from lp.services.database.enumcol import EnumCol43from lp.services.database.enumcol import EnumCol
44from lp.services.database.lpstorm import IStore
35from lp.services.database.sqlbase import SQLBase45from lp.services.database.sqlbase import SQLBase
36from lp.services.job.interfaces.job import (46from lp.services.job.interfaces.job import (
37 IJob,47 IJob,
38 JobStatus,48 JobStatus,
39 LeaseHeld,
40 )49 )
50from lp.services import scripts
4151
4252
43UTC = pytz.timezone('UTC')53UTC = pytz.timezone('UTC')
@@ -201,6 +211,29 @@
201 self.lease_expires = None211 self.lease_expires = None
202212
203213
214class EnumeratedSubclass(type):
215 """Metaclass for when subclasses are assigned enums."""
216
217 def __init__(cls, name, bases, dict_):
218 if getattr(cls, '_subclass', None) is None:
219 cls._subclass = {}
220 job_type = dict_.get('class_job_type')
221 if job_type is not None:
222 value = cls._subclass.setdefault(job_type, cls)
223 assert value is cls, (
224 '%s already registered to %s.' % (
225 job_type.name, value.__name__))
226 # Perform any additional set-up requested by class.
227 cls._register_subclass(cls)
228
229 @staticmethod
230 def _register_subclass(cls):
231 pass
232
233 def makeSubclass(cls, job):
234 return cls._subclass[job.job_type](job)
235
236
204Job.ready_jobs = Select(237Job.ready_jobs = Select(
205 Job.id,238 Job.id,
206 And(239 And(
@@ -208,3 +241,29 @@
208 Or(Job.lease_expires == None, Job.lease_expires < UTC_NOW),241 Or(Job.lease_expires == None, Job.lease_expires < UTC_NOW),
209 Or(Job.scheduled_start == None, Job.scheduled_start <= UTC_NOW),242 Or(Job.scheduled_start == None, Job.scheduled_start <= UTC_NOW),
210 ))243 ))
244
245
246class UniversalJobSource:
247 """Returns the RunnableJob associated with a Job.id.
248
249 Only BranchJobs are supported at present.
250 """
251
252 memory_limit = 2 * (1024 ** 3)
253
254 needs_init = True
255
256 @classmethod
257 def get(cls, job_id):
258 if cls.needs_init:
259 scripts.execute_zcml_for_scripts(use_web_security=False)
260 cls.needs_init = False
261
262 dbconfig.override(
263 dbuser='branchscanner', isolation_level='read_committed')
264 from lp.code.model.branchjob import (
265 BranchJob,
266 )
267 store = IStore(BranchJob)
268 branch_job = store.find(BranchJob, BranchJob.job == job_id).one()
269 return branch_job.makeDerived()
211270
=== modified file 'lib/lp/services/job/runner.py'
--- lib/lp/services/job/runner.py 2012-03-22 11:58:42 +0000
+++ lib/lp/services/job/runner.py 2012-03-29 14:36:38 +0000
@@ -6,6 +6,7 @@
6__metaclass__ = type6__metaclass__ = type
77
8__all__ = [8__all__ = [
9 'BaseJobRunner',
9 'BaseRunnableJob',10 'BaseRunnableJob',
10 'BaseRunnableJobSource',11 'BaseRunnableJobSource',
11 'JobCronScript',12 'JobCronScript',
@@ -38,7 +39,10 @@
38 pool,39 pool,
39 )40 )
40from lazr.delegates import delegates41from lazr.delegates import delegates
41from lazr.jobrunner.jobrunner import JobRunner as LazrJobRunner42from lazr.jobrunner.jobrunner import (
43 JobRunner as LazrJobRunner,
44 LeaseHeld,
45 )
42import transaction46import transaction
43from twisted.internet import reactor47from twisted.internet import reactor
44from twisted.internet.defer import (48from twisted.internet.defer import (
@@ -61,7 +65,6 @@
61from lp.services.job.interfaces.job import (65from lp.services.job.interfaces.job import (
62 IJob,66 IJob,
63 IRunnableJob,67 IRunnableJob,
64 LeaseHeld,
65 )68 )
66from lp.services.mail.sendmail import (69from lp.services.mail.sendmail import (
67 MailController,70 MailController,
6871
=== added file 'lib/lp/services/job/tests/celeryconfig.py'
--- lib/lp/services/job/tests/celeryconfig.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/job/tests/celeryconfig.py 2012-03-29 14:36:38 +0000
@@ -0,0 +1,7 @@
1import os
2BROKER_VHOST = "/"
3CELERY_RESULT_BACKEND = "amqp"
4CELERY_IMPORTS = ("lp.services.job.celeryjob", )
5CELERYD_LOG_LEVEL = 'INFO'
6CELERYD_CONCURRENCY = 1
7BROKER_URL = os.environ['BROKER_URL']
08
=== added file 'lib/lp/services/job/tests/test_celeryjob.py'
--- lib/lp/services/job/tests/test_celeryjob.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/job/tests/test_celeryjob.py 2012-03-29 14:36:38 +0000
@@ -0,0 +1,42 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4
5import os
6
7import transaction
8
9from lp.code.model.branchjob import BranchScanJob
10from lp.testing import TestCaseWithFactory
11from lp.testing.layers import ZopelessAppServerLayer
12
13
14class TestCelery(TestCaseWithFactory):
15
16 layer = ZopelessAppServerLayer
17
18 def test_run_scan_job(self):
19 """Running a job via Celery succeeds and emits expected output."""
20 # Delay importing anything that uses Celery until RabbitMQLayer is
21 # running, so that config.rabbitmq.host is defined when
22 # lp.services.job.celeryconfig is loaded.
23 from lp.services.job.celeryjob import CeleryRunJob
24 from celery.exceptions import TimeoutError
25 from lazr.jobrunner.tests.test_celerytask import running
26 cmd_args = ('--config', 'lp.services.job.tests.celeryconfig')
27 env = dict(os.environ)
28 env['BROKER_URL'] = CeleryRunJob.app.conf['BROKER_URL']
29 with running('bin/celeryd', cmd_args, env=env) as proc:
30 self.useBzrBranches()
31 db_branch, bzr_tree = self.create_branch_and_tree()
32 bzr_tree.commit(
33 'First commit', rev_id='rev1', committer='me@example.org')
34 job = BranchScanJob.create(db_branch)
35 transaction.commit()
36 try:
37 CeleryRunJob.delay(job.job_id).wait(30)
38 except TimeoutError:
39 pass
40 self.assertIn(
41 'Updating branch scanner status: 1 revs', proc.stderr.read())
42 self.assertEqual(db_branch.revision_count, 1)
043
=== modified file 'lib/lp/services/job/tests/test_job.py'
--- lib/lp/services/job/tests/test_job.py 2011-12-30 06:14:56 +0000
+++ lib/lp/services/job/tests/test_job.py 2012-03-29 14:36:38 +0000
@@ -7,6 +7,7 @@
7import time7import time
88
9import pytz9import pytz
10from lazr.jobrunner.jobrunner import LeaseHeld
10from storm.locals import Store11from storm.locals import Store
1112
12from lp.services.database.constants import UTC_NOW13from lp.services.database.constants import UTC_NOW
@@ -18,7 +19,6 @@
18from lp.services.job.model.job import (19from lp.services.job.model.job import (
19 InvalidTransition,20 InvalidTransition,
20 Job,21 Job,
21 LeaseHeld,
22 )22 )
23from lp.services.webapp.testing import verifyObject23from lp.services.webapp.testing import verifyObject
24from lp.testing import (24from lp.testing import (
2525
=== modified file 'lib/lp/services/job/tests/test_runner.py'
--- lib/lp/services/job/tests/test_runner.py 2012-03-29 14:36:36 +0000
+++ lib/lp/services/job/tests/test_runner.py 2012-03-29 14:36:38 +0000
@@ -8,7 +8,10 @@
8from textwrap import dedent8from textwrap import dedent
9from time import sleep9from time import sleep
1010
11from lazr.jobrunner.jobrunner import SuspendJobException11from lazr.jobrunner.jobrunner import (
12 LeaseHeld,
13 SuspendJobException,
14 )
12from testtools.matchers import MatchesRegex15from testtools.matchers import MatchesRegex
13from testtools.testcase import ExpectedException16from testtools.testcase import ExpectedException
14import transaction17import transaction
@@ -19,7 +22,6 @@
19from lp.services.job.interfaces.job import (22from lp.services.job.interfaces.job import (
20 IRunnableJob,23 IRunnableJob,
21 JobStatus,24 JobStatus,
22 LeaseHeld,
23 )25 )
24from lp.services.job.model.job import Job26from lp.services.job.model.job import Job
25from lp.services.job.runner import (27from lp.services.job.runner import (
@@ -554,20 +556,11 @@
554 self.assertEqual(expected_exception, (oops['type'], oops['value']))556 self.assertEqual(expected_exception, (oops['type'], oops['value']))
555 self.assertThat(logger.getLogBuffer(), MatchesRegex(557 self.assertThat(logger.getLogBuffer(), MatchesRegex(
556 dedent("""\558 dedent("""\
557<<<<<<< TREE
558 INFO Running through Twisted.559 INFO Running through Twisted.
559 INFO Running <StuckJob.*?> \(ID .*?\).560 INFO Running <StuckJob.*?> \(ID .*?\).
560 INFO Running <StuckJob.*?> \(ID .*?\).561 INFO Running <StuckJob.*?> \(ID .*?\).
561 INFO Job resulted in OOPS: .*562 INFO Job resulted in OOPS: .*
562 """)))563 """)))
563=======
564 INFO Running through Twisted.
565 INFO Running StuckJob \(ID .*\).
566 INFO Running StuckJob \(ID .*\).
567 ERROR OOPS while executing job \d+: \['OOPS-.*\] %s\('%s',\)
568 INFO Job resulted in OOPS: .*
569 """ % (expected_exception))))
570>>>>>>> MERGE-SOURCE
571564
572 def test_timeout_short(self):565 def test_timeout_short(self):
573 """When a job exceeds its lease, an exception is raised.566 """When a job exceeds its lease, an exception is raised.
@@ -590,16 +583,10 @@
590 logger.getLogBuffer(), MatchesRegex(583 logger.getLogBuffer(), MatchesRegex(
591 dedent("""\584 dedent("""\
592 INFO Running through Twisted.585 INFO Running through Twisted.
593<<<<<<< TREE586 INFO Running <ShorterStuckJob.*?> \(ID .*?\).
594 INFO Running <ShorterStuckJob.*?> \(ID .*?\).587 INFO Running <ShorterStuckJob.*?> \(ID .*?\).
595 INFO Running <ShorterStuckJob.*?> \(ID .*?\).
596=======
597 INFO Running ShorterStuckJob \(ID .*\).
598 INFO Running ShorterStuckJob \(ID .*\).
599 ERROR OOPS while executing job \d+: \['OOPS-.*\] %s\('%s',\)
600>>>>>>> MERGE-SOURCE
601 INFO Job resulted in OOPS: %s588 INFO Job resulted in OOPS: %s
602 """) % ('TimeoutError', 'Job ran too long.', oops['id'])))589 """) % oops['id']))
603 self.assertEqual(('TimeoutError', 'Job ran too long.'),590 self.assertEqual(('TimeoutError', 'Job ran too long.'),
604 (oops['type'], oops['value']))591 (oops['type'], oops['value']))
605592
606593
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2012-03-29 14:36:36 +0000
+++ lib/lp/testing/factory.py 2012-03-29 14:36:38 +0000
@@ -1407,7 +1407,7 @@
1407 # We just remove the security proxies to be able to change the objects1407 # We just remove the security proxies to be able to change the objects
1408 # here.1408 # here.
1409 removeSecurityProxy(branch).branchChanged(1409 removeSecurityProxy(branch).branchChanged(
1410 '', 'rev1', None, None, None)1410 '', 'rev1', None, None, None, celery_scan=False)
1411 naked_series = removeSecurityProxy(product.development_focus)1411 naked_series = removeSecurityProxy(product.development_focus)
1412 naked_series.branch = branch1412 naked_series.branch = branch
1413 return branch1413 return branch
@@ -1422,7 +1422,7 @@
1422 # We just remove the security proxies to be able to change the branch1422 # We just remove the security proxies to be able to change the branch
1423 # here.1423 # here.
1424 removeSecurityProxy(branch).branchChanged(1424 removeSecurityProxy(branch).branchChanged(
1425 '', 'rev1', None, None, None)1425 '', 'rev1', None, None, None, celery_scan=False)
1426 with person_logged_in(package.distribution.owner):1426 with person_logged_in(package.distribution.owner):
1427 package.development_version.setBranch(1427 package.development_version.setBranch(
1428 PackagePublishingPocket.RELEASE, branch,1428 PackagePublishingPocket.RELEASE, branch,
@@ -1628,7 +1628,7 @@
1628 if branch.branch_type not in (BranchType.REMOTE, BranchType.HOSTED):1628 if branch.branch_type not in (BranchType.REMOTE, BranchType.HOSTED):
1629 branch.startMirroring()1629 branch.startMirroring()
1630 removeSecurityProxy(branch).branchChanged(1630 removeSecurityProxy(branch).branchChanged(
1631 '', parent.revision_id, None, None, None)1631 '', parent.revision_id, None, None, None, celery_scan=False)
1632 branch.updateScannedDetails(parent, sequence)1632 branch.updateScannedDetails(parent, sequence)
16331633
1634 def makeBranchRevision(self, branch, revision_id=None, sequence=None,1634 def makeBranchRevision(self, branch, revision_id=None, sequence=None,
16351635
=== modified file 'lib/lp/testing/yuixhr.py'
--- lib/lp/testing/yuixhr.py 2012-03-29 14:36:36 +0000
+++ lib/lp/testing/yuixhr.py 2012-03-23 18:07:23 +0000
@@ -235,7 +235,6 @@
235 <!DOCTYPE html>235 <!DOCTYPE html>
236 <html>236 <html>
237 <head>237 <head>
238<<<<<<< TREE
239 <title>Test</title>238 <title>Test</title>
240 %(javascript_block)s239 %(javascript_block)s
241 <script type="text/javascript">240 <script type="text/javascript">
@@ -253,45 +252,6 @@
253 <link rel="stylesheet" href="/+icing/rev%(revno)s/combo.css"/>252 <link rel="stylesheet" href="/+icing/rev%(revno)s/combo.css"/>
254 <script type="text/javascript" src="%(test_module)s"></script>253 <script type="text/javascript" src="%(test_module)s"></script>
255 </head>254 </head>
256=======
257 <title>Test</title>
258 <script type="text/javascript"
259 src="/+icing/rev%(revno)s/build/launchpad.js"></script>
260 <script type="text/javascript">
261 YUI.GlobalConfig = {
262 fetchCSS: false,
263 timeout: 50,
264 ignore: [
265 'yui2-yahoo', 'yui2-event', 'yui2-dom',
266 'yui2-calendar','yui2-dom-event'
267 ]
268 }
269 </script>
270 <link rel="stylesheet"
271 href="/+icing/yui/assets/skins/sam/skin.css"/>
272 <link rel="stylesheet" href="/+icing/rev%(revno)s/combo.css"/>
273 <style>
274 /* Taken and customized from testlogger.css */
275 .yui-console-entry-src { display:none; }
276 .yui-console-entry.yui-console-entry-pass .yui-console-entry-cat {
277 background-color: green;
278 font-weight: bold;
279 color: white;
280 }
281 .yui-console-entry.yui-console-entry-fail .yui-console-entry-cat {
282 background-color: red;
283 font-weight: bold;
284 color: white;
285 }
286 .yui-console-entry.yui-console-entry-ignore .yui-console-entry-cat {
287 background-color: #666;
288 font-weight: bold;
289 color: white;
290 }
291 </style>
292 <script type="text/javascript" src="%(test_module)s"></script>
293 </head>
294>>>>>>> MERGE-SOURCE
295 <body class="yui3-skin-sam">255 <body class="yui3-skin-sam">
296 <div id="log"></div>256 <div id="log"></div>
297 <p>Want to re-run your test?</p>257 <p>Want to re-run your test?</p>
298258
=== modified file 'lib/lp/translations/model/translationsharingjob.py'
--- lib/lp/translations/model/translationsharingjob.py 2011-12-30 06:14:56 +0000
+++ lib/lp/translations/model/translationsharingjob.py 2012-03-29 14:36:38 +0000
@@ -31,7 +31,10 @@
31 IJob,31 IJob,
32 JobStatus,32 JobStatus,
33 )33 )
34from lp.services.job.model.job import Job34from lp.services.job.model.job import (
35 EnumeratedSubclass,
36 Job,
37 )
35from lp.translations.interfaces.translationsharingjob import (38from lp.translations.interfaces.translationsharingjob import (
36 ITranslationSharingJob,39 ITranslationSharingJob,
37 )40 )
@@ -108,21 +111,13 @@
108 self.potemplate = potemplate111 self.potemplate = potemplate
109112
110113
111class RegisteredSubclass(type):
112 """Metaclass for when subclasses should be registered."""
113
114 def __init__(cls, name, bases, dict_):
115 cls._register_subclass(cls)
116
117
118class TranslationSharingJobDerived:114class TranslationSharingJobDerived:
119 """Base class for specialized TranslationTemplate Job types."""115 """Base class for specialized TranslationTemplate Job types."""
120116
121 __metaclass__ = RegisteredSubclass117 __metaclass__ = EnumeratedSubclass
122118
123 delegates(ITranslationSharingJob, 'job')119 delegates(ITranslationSharingJob, 'job')
124120
125 _subclass = {}
126 _event_types = {}121 _event_types = {}
127122
128 @property123 @property
@@ -136,12 +131,6 @@
136 # TranslationPackagingJob) need to be able to override it and call131 # TranslationPackagingJob) need to be able to override it and call
137 # into it, and there's no syntax to call a base class's version of a132 # into it, and there's no syntax to call a base class's version of a
138 # classmethod with the subclass as the first parameter.133 # classmethod with the subclass as the first parameter.
139 job_type = getattr(cls, 'class_job_type', None)
140 if job_type is not None:
141 value = cls._subclass.setdefault(job_type, cls)
142 assert value is cls, (
143 '%s already registered to %s.' % (
144 job_type.name, value.__name__))
145 event_type = getattr(cls, 'create_on_event', None)134 event_type = getattr(cls, 'create_on_event', None)
146 if event_type is not None:135 if event_type is not None:
147 cls._event_types.setdefault(event_type, []).append(cls)136 cls._event_types.setdefault(event_type, []).append(cls)
@@ -215,7 +204,7 @@
215 TranslationSharingJob.job == Job.id,204 TranslationSharingJob.job == Job.id,
216 Job.id.is_in(Job.ready_jobs),205 Job.id.is_in(Job.ready_jobs),
217 *extra_clauses)206 *extra_clauses)
218 return (cls._subclass[job.job_type](job) for job in jobs)207 return (cls.makeSubclass(job) for job in jobs)
219208
220 @classmethod209 @classmethod
221 def getNextJobStatus(cls, packaging):210 def getNextJobStatus(cls, packaging):
222211
=== modified file 'versions.cfg'
--- versions.cfg 2012-03-29 14:36:36 +0000
+++ versions.cfg 2012-03-27 17:29:47 +0000
@@ -42,11 +42,7 @@
42lazr.config = 1.1.342lazr.config = 1.1.3
43lazr.delegates = 1.2.043lazr.delegates = 1.2.0
44lazr.enum = 1.1.344lazr.enum = 1.1.3
45<<<<<<< TREE
46lazr.jobrunner = 0.245lazr.jobrunner = 0.2
47=======
48lazr.jobrunner = 0.1
49>>>>>>> MERGE-SOURCE
50lazr.lifecycle = 1.146lazr.lifecycle = 1.1
51lazr.restful = 0.19.647lazr.restful = 0.19.6
52lazr.restfulclient = 0.12.048lazr.restfulclient = 0.12.0
@@ -87,12 +83,8 @@
87pymongo = 2.1.183pymongo = 2.1.1
88pyOpenSSL = 0.1084pyOpenSSL = 0.10
89pystache = 0.3.185pystache = 0.3.1
90<<<<<<< TREE
91python-dateutil = 1.586python-dateutil = 1.5
92python-memcached = 1.4887python-memcached = 1.48
93=======
94python-memcached = 1.48
95>>>>>>> MERGE-SOURCE
96# 2.2.1 with the one-liner Expect: 100-continue fix from88# 2.2.1 with the one-liner Expect: 100-continue fix from
97# lp:~wgrant/python-openid/python-openid-2.2.1-fix676372.89# lp:~wgrant/python-openid/python-openid-2.2.1-fix676372.
98python-openid = 2.2.1-fix67637290python-openid = 2.2.1-fix676372