launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #06488
[Merge] lp:~wgrant/launchpad/multipolicy-3 into lp:launchpad
William Grant has proposed merging lp:~wgrant/launchpad/multipolicy-3 into lp:launchpad.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~wgrant/launchpad/multipolicy-3/+merge/94499
This is the next part of the new access policy model. It's basically the next generation of <https://code.launchpad.net/~wgrant/launchpad/observer-model/+merge/82852>.
I've basically just got basic operations on AccessPolicies, AccessArtifacts, and their corresponding Grants. All the methods are bulk operations. There's also the required person merge code for the grants.
Two tables in the new schema (AccessPolicyArtifact and AccessPolicyGrantFlat) aren't yet represented, because the branch was too big already.
--
https://code.launchpad.net/~wgrant/launchpad/multipolicy-3/+merge/94499
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~wgrant/launchpad/multipolicy-3 into lp:launchpad.
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg 2012-02-20 11:52:14 +0000
+++ database/schema/security.cfg 2012-02-24 05:05:23 +0000
@@ -1998,6 +1998,7 @@
[person-merge-job]
groups=script
+public.accessartifactgrant = SELECT, UPDATE, DELETE
public.accesspolicygrant = SELECT, UPDATE, DELETE
public.account = SELECT, UPDATE
public.announcement = SELECT, UPDATE
=== modified file 'lib/lp/bugs/model/bugnotification.py'
--- lib/lp/bugs/model/bugnotification.py 2011-12-30 06:14:56 +0000
+++ lib/lp/bugs/model/bugnotification.py 2012-02-24 05:05:23 +0000
@@ -52,13 +52,11 @@
from lp.bugs.model.structuralsubscription import StructuralSubscription
from lp.registry.interfaces.person import IPersonSet
from lp.services.config import config
+from lp.services.database import bulk
from lp.services.database.datetimecol import UtcDateTimeCol
from lp.services.database.enumcol import EnumCol
from lp.services.database.lpstorm import IStore
-from lp.services.database.sqlbase import (
- SQLBase,
- sqlvalues,
- )
+from lp.services.database.sqlbase import SQLBase
from lp.services.database.stormbase import StormBase
from lp.services.messages.model.message import Message
@@ -182,28 +180,19 @@
# XXX jamesh 2008-05-21: these flushes are to fix ordering
# problems in the bugnotification-sending.txt tests.
store.flush()
- sql_values = []
- for recipient in recipients:
- reason_body, reason_header = recipients.getReason(recipient)
- sql_values.append('(%s, %s, %s, %s)' % sqlvalues(
- bug_notification, recipient, reason_header, reason_body))
-
- # We add all the recipients in a single SQL statement to make
- # this a bit more efficient for bugs with many subscribers.
- if len(sql_values) > 0:
- store.execute("""
- INSERT INTO BugNotificationRecipient
- (bug_notification, person, reason_header, reason_body)
- VALUES %s;""" % ', '.join(sql_values))
-
- if len(recipients.subscription_filters) > 0:
- filter_link_sql = [
- "(%s, %s)" % sqlvalues(bug_notification, filter.id)
- for filter in recipients.subscription_filters]
- store.execute("""
- INSERT INTO BugNotificationFilter
- (bug_notification, bug_subscription_filter)
- VALUES %s;""" % ", ".join(filter_link_sql))
+
+ bulk.create(
+ (BugNotificationRecipient.bug_notification,
+ BugNotificationRecipient.person,
+ BugNotificationRecipient.reason_body,
+ BugNotificationRecipient.reason_header),
+ [(bug_notification, recipient) + recipients.getReason(recipient)
+ for recipient in recipients])
+ bulk.create(
+ (BugNotificationFilter.bug_notification,
+ BugNotificationFilter.bug_subscription_filter),
+ [(bug_notification, filter)
+ for filter in recipients.subscription_filters])
return bug_notification
=== modified file 'lib/lp/bugs/model/bugwatch.py'
--- lib/lp/bugs/model/bugwatch.py 2012-02-21 22:46:28 +0000
+++ lib/lp/bugs/model/bugwatch.py 2012-02-24 05:05:23 +0000
@@ -64,16 +64,17 @@
from lp.bugs.model.bugset import BugSetBase
from lp.bugs.model.bugtask import BugTask
from lp.registry.interfaces.person import validate_public_person
+from lp.services.database import bulk
from lp.services.database.constants import UTC_NOW
from lp.services.database.datetimecol import UtcDateTimeCol
from lp.services.database.enumcol import EnumCol
from lp.services.database.lpstorm import IStore
-from lp.services.database.sqlbase import (
- SQLBase,
- sqlvalues,
- )
+from lp.services.database.sqlbase import SQLBase
from lp.services.database.stormbase import StormBase
-from lp.services.helpers import shortlist
+from lp.services.helpers import (
+ ensure_unicode,
+ shortlist,
+ )
from lp.services.messages.model.message import Message
from lp.services.webapp import (
urlappend,
@@ -739,18 +740,13 @@
def bulkAddActivity(self, references,
result=BugWatchActivityStatus.SYNC_SUCCEEDED,
- message=None, oops_id=None):
+ oops_id=None):
"""See `IBugWatchSet`."""
- bug_watch_ids = set(get_bug_watch_ids(references))
- if len(bug_watch_ids) > 0:
- insert_activity_statement = (
- "INSERT INTO BugWatchActivity"
- " (bug_watch, result, message, oops_id) "
- "SELECT BugWatch.id, %s, %s, %s FROM BugWatch"
- " WHERE BugWatch.id IN %s")
- IStore(BugWatch).execute(
- insert_activity_statement % sqlvalues(
- result, message, oops_id, bug_watch_ids))
+ bulk.create(
+ (BugWatchActivity.bug_watch_id, BugWatchActivity.result,
+ BugWatchActivity.oops_id),
+ [(bug_watch_id, result, ensure_unicode(oops_id))
+ for bug_watch_id in set(get_bug_watch_ids(references))])
class BugWatchActivity(StormBase):
=== modified file 'lib/lp/bugs/scripts/bzremotecomponentfinder.py'
--- lib/lp/bugs/scripts/bzremotecomponentfinder.py 2012-01-01 02:58:52 +0000
+++ lib/lp/bugs/scripts/bzremotecomponentfinder.py 2012-02-24 05:05:23 +0000
@@ -16,6 +16,7 @@
)
from BeautifulSoup import BeautifulSoup
+import transaction
from zope.component import getUtility
from lp.bugs.interfaces.bugtracker import (
@@ -23,6 +24,7 @@
IBugTrackerSet,
)
from lp.bugs.model.bugtracker import BugTrackerComponent
+from lp.services.database import bulk
from lp.services.database.lpstorm import IStore
from lp.services.scripts.logger import log as default_log
@@ -199,18 +201,15 @@
# added to launchpad. Record them for now.
for component in product['components'].values():
components_to_add.append(
- "('%s', %d, 'True', 'False')" % (
- component['name'], lp_component_group.id))
+ (component['name'], lp_component_group, True, False))
if len(components_to_add) > 0:
- sqltext = """
- INSERT INTO BugTrackerComponent
- (name, component_group, is_visible, is_custom)
- VALUES %s""" % ",\n ".join(components_to_add)
-
self.logger.debug("...Inserting components into database")
- store = IStore(BugTrackerComponent)
- store.execute(sqltext)
- store.commit()
- store.flush()
+ bulk.create(
+ (BugTrackerComponent.name,
+ BugTrackerComponent.component_group,
+ BugTrackerComponent.is_visible,
+ BugTrackerComponent.is_custom),
+ components_to_add)
+ transaction.commit()
self.logger.debug("...Done")
=== modified file 'lib/lp/bugs/tests/test_bugnotification.py'
--- lib/lp/bugs/tests/test_bugnotification.py 2012-01-20 15:42:44 +0000
+++ lib/lp/bugs/tests/test_bugnotification.py 2012-02-24 05:05:23 +0000
@@ -28,11 +28,11 @@
from lp.bugs.model.bugnotification import (
BugNotification,
BugNotificationFilter,
+ BugNotificationRecipient,
BugNotificationSet,
)
from lp.bugs.model.bugsubscriptionfilter import BugSubscriptionFilterMute
from lp.services.config import config
-from lp.services.database.sqlbase import sqlvalues
from lp.services.messages.interfaces.message import IMessageSet
from lp.services.messages.model.message import MessageSet
from lp.testing import (
@@ -165,14 +165,9 @@
def addNotificationRecipient(self, notification, person):
# Manually insert BugNotificationRecipient for
# construct_email_notifications to work.
- # Not sure why using SQLObject constructor doesn't work (it
- # tries to insert a row with only the ID which fails).
- Store.of(notification).execute("""
- INSERT INTO BugNotificationRecipient
- (bug_notification, person, reason_header, reason_body)
- VALUES (%s, %s, %s, %s)""" % sqlvalues(
- notification, person,
- u'reason header', u'reason body'))
+ BugNotificationRecipient(
+ bug_notification=notification, person=person,
+ reason_header=u'reason header', reason_body=u'reason body')
def addNotification(self, person, bug=None):
# Add a notification along with recipient data.
=== modified file 'lib/lp/bugs/tests/test_bugwatch.py'
--- lib/lp/bugs/tests/test_bugwatch.py 2012-01-20 15:42:44 +0000
+++ lib/lp/bugs/tests/test_bugwatch.py 2012-02-24 05:05:23 +0000
@@ -582,8 +582,8 @@
# the given bug watches.
error = BugWatchActivityStatus.PRIVATE_REMOTE_BUG
getUtility(IBugWatchSet).bulkAddActivity(
- self.bug_watches, error, "Forbidden", "OOPS-1234")
- self._checkActivityForBugWatches(error, "Forbidden", "OOPS-1234")
+ self.bug_watches, error, "OOPS-1234")
+ self._checkActivityForBugWatches(error, None, "OOPS-1234")
def test_bulkAddActivity_with_id_list(self):
# The ids of bug watches can be passed in.
=== modified file 'lib/lp/registry/configure.zcml'
--- lib/lp/registry/configure.zcml 2012-02-23 21:00:22 +0000
+++ lib/lp/registry/configure.zcml 2012-02-24 05:05:23 +0000
@@ -1906,4 +1906,49 @@
for="lp.registry.interfaces.person.IPerson"
provides="lp.registry.interfaces.role.IPersonRoles"
factory="lp.registry.model.personroles.PersonRoles" />
+
+ <class
+ class="lp.registry.model.accesspolicy.AccessPolicy">
+ <allow
+ interface="lp.registry.interfaces.accesspolicy.IAccessPolicy"/>
+ </class>
+ <securedutility
+ component="lp.registry.model.accesspolicy.AccessPolicy"
+ provides="lp.registry.interfaces.accesspolicy.IAccessPolicySource">
+ <allow
+ interface="lp.registry.interfaces.accesspolicy.IAccessPolicySource"/>
+ </securedutility>
+ <class
+ class="lp.registry.model.accesspolicy.AccessArtifact">
+ <allow
+ interface="lp.registry.interfaces.accesspolicy.IAccessArtifact"/>
+ </class>
+ <securedutility
+ component="lp.registry.model.accesspolicy.AccessArtifact"
+ provides="lp.registry.interfaces.accesspolicy.IAccessArtifactSource">
+ <allow
+ interface="lp.registry.interfaces.accesspolicy.IAccessArtifactSource"/>
+ </securedutility>
+ <class
+ class="lp.registry.model.accesspolicy.AccessArtifactGrant">
+ <allow
+ interface="lp.registry.interfaces.accesspolicy.IAccessArtifactGrant"/>
+ </class>
+ <securedutility
+ component="lp.registry.model.accesspolicy.AccessArtifactGrant"
+ provides="lp.registry.interfaces.accesspolicy.IAccessArtifactGrantSource">
+ <allow
+ interface="lp.registry.interfaces.accesspolicy.IAccessArtifactGrantSource"/>
+ </securedutility>
+ <class
+ class="lp.registry.model.accesspolicy.AccessPolicyGrant">
+ <allow
+ interface="lp.registry.interfaces.accesspolicy.IAccessPolicyGrant"/>
+ </class>
+ <securedutility
+ component="lp.registry.model.accesspolicy.AccessPolicyGrant"
+ provides="lp.registry.interfaces.accesspolicy.IAccessPolicyGrantSource">
+ <allow
+ interface="lp.registry.interfaces.accesspolicy.IAccessPolicyGrantSource"/>
+ </securedutility>
</configure>
=== modified file 'lib/lp/registry/enums.py'
--- lib/lp/registry/enums.py 2012-01-17 21:45:24 +0000
+++ lib/lp/registry/enums.py 2012-02-24 05:05:23 +0000
@@ -5,9 +5,10 @@
__metaclass__ = type
__all__ = [
- 'PersonTransferJobType',
+ 'AccessPolicyType',
'DistroSeriesDifferenceStatus',
'DistroSeriesDifferenceType',
+ 'PersonTransferJobType',
]
from lazr.enum import (
@@ -16,6 +17,48 @@
)
+class AccessPolicyType(DBEnumeratedType):
+ """Access policy type.
+
+ The policies used to control which users and teams can see various
+ Launchpad artifacts, including bugs and branches.
+ """
+
+ PUBLIC = DBItem(1, """
+ Public
+
+ Everyone can see this information.
+ """)
+
+ PUBLICSECURITY = DBItem(2, """
+ Public Security
+
+ Everyone can see this information pertaining to a resolved security
+ related bug.
+ """)
+
+ EMBARGOEDSECURITY = DBItem(3, """
+ Embargoed Security
+
+ Only users with permission to see the project's security related
+ artifacts can see this information.
+ """)
+
+ USERDATA = DBItem(4, """
+ User Data
+
+ Only users with permission to see the project's artifacts containing
+ user data can see this information.
+ """)
+
+ PROPRIETARY = DBItem(5, """
+ Proprietary
+
+ Only users with permission to see the project's artifacts containing
+ proprietary data can see this information.
+ """)
+
+
class DistroSeriesDifferenceStatus(DBEnumeratedType):
"""Distribution series difference status.
=== added file 'lib/lp/registry/interfaces/accesspolicy.py'
--- lib/lp/registry/interfaces/accesspolicy.py 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/interfaces/accesspolicy.py 2012-02-24 05:05:23 +0000
@@ -0,0 +1,141 @@
+# Copyright 2011 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Interfaces for pillar and artifact access policies."""
+
+__metaclass__ = type
+
+__all__ = [
+ 'IAccessArtifact',
+ 'IAccessArtifactGrant',
+ 'IAccessArtifactGrantSource',
+ 'IAccessArtifactSource',
+ 'IAccessPolicy',
+ 'IAccessPolicyGrant',
+ 'IAccessPolicyGrantSource',
+ 'IAccessPolicySource',
+ ]
+
+from zope.interface import (
+ Attribute,
+ Interface,
+ )
+
+
+class IAccessArtifact(Interface):
+ id = Attribute("ID")
+ concrete_artifact = Attribute("Concrete artifact")
+
+
+class IAccessArtifactGrant(Interface):
+ grantee = Attribute("Grantee")
+ grantor = Attribute("Grantor")
+ date_created = Attribute("Date created")
+ abstract_artifact = Attribute("Abstract artifact")
+
+ concrete_artifact = Attribute("Concrete artifact")
+
+
+class IAccessPolicy(Interface):
+ id = Attribute("ID")
+ pillar = Attribute("Pillar")
+ type = Attribute("Type")
+
+
+class IAccessPolicyGrant(Interface):
+ grantee = Attribute("Grantee")
+ grantor = Attribute("Grantor")
+ date_created = Attribute("Date created")
+ policy = Attribute("Access policy")
+
+
+class IAccessArtifactSource(Interface):
+
+ def ensure(concrete_artifacts):
+ """Return `IAccessArtifact`s for the concrete artifacts.
+
+ Creates abstract artifacts if they don't already exist.
+ """
+
+ def find(concrete_artifacts):
+ """Return the `IAccessArtifact`s for the artifacts, if they exist.
+
+ Use ensure() if you want to create them if they don't yet exist.
+ """
+
+ def delete(concrete_artifacts):
+ """Delete the `IAccessArtifact`s for the concrete artifact.
+
+ Also revokes any `IAccessArtifactGrant`s for the artifacts.
+ """
+
+
+class IAccessArtifactGrantSource(Interface):
+
+ def grant(grants):
+ """Create `IAccessArtifactGrant`s.
+
+ :param grants: a collection of
+ (`IAccessArtifact`, grantee `IPerson`, grantor `IPerson`) triples
+ to grant.
+ """
+
+ def find(grants):
+ """Return the specified `IAccessArtifactGrant`s if they exist.
+
+ :param grants: a collection of (`IAccessArtifact`, grantee `IPerson`)
+ pairs.
+ """
+
+ def findByArtifact(artifacts):
+ """Return all `IAccessArtifactGrant` objects for the artifacts."""
+
+ def revokeByArtifact(artifacts):
+ """Delete all `IAccessArtifactGrant` objects for the artifacts."""
+
+
+class IAccessPolicySource(Interface):
+
+ def create(pillars_and_types):
+ """Create an `IAccessPolicy` for the given pillars and types.
+
+ :param pillars_and_types: a collection of
+ (`IProduct` or `IDistribution`, `IAccessPolicyType`) pairs to
+ create `IAccessPolicy` objects for.
+ :return: a collection of the created `IAccessPolicy` objects.
+ """
+
+ def find(pillars_and_types):
+ """Return the `IAccessPolicy`s for the given pillars and types.
+
+ :param pillars_and_types: a collection of
+ (`IProduct` or `IDistribution`, `IAccessPolicyType`) pairs to
+ find.
+ """
+
+ def findByID(ids):
+ """Return the `IAccessPolicy`s with the given IDs."""
+
+ def findByPillar(pillars):
+ """Return a `ResultSet` of all `IAccessPolicy`s for the pillars."""
+
+
+class IAccessPolicyGrantSource(Interface):
+
+ def grant(grants):
+ """Create `IAccessPolicyGrant`s.
+
+ :param grants: a collection of
+ (`IAccessPolicy`, grantee `IPerson`, grantor `IPerson`) triples
+ to grant.
+ """
+
+ def find(grants):
+ """Return the specified `IAccessPolicyGrant`s if they exist.
+
+ :param grants: a collection of (`IAccessPolicy`, grantee `IPerson`)
+ pairs.
+ """
+
+ def findByPolicy(policies):
+ """Return all `IAccessPolicyGrant` objects for the artifacts."""
=== added file 'lib/lp/registry/model/accesspolicy.py'
--- lib/lp/registry/model/accesspolicy.py 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/model/accesspolicy.py 2012-02-24 05:05:23 +0000
@@ -0,0 +1,263 @@
+# Copyright 2011 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Model classes for pillar and artifact access policies."""
+
+__metaclass__ = type
+__all__ = [
+ 'AccessArtifact',
+ 'AccessPolicy',
+ 'AccessPolicyGrant',
+ ]
+
+from storm.expr import (
+ And,
+ Or,
+ )
+from storm.properties import (
+ DateTime,
+ Int,
+ )
+from storm.references import Reference
+from zope.component import getUtility
+from zope.interface import implements
+
+from lp.registry.enums import AccessPolicyType
+from lp.registry.interfaces.accesspolicy import (
+ IAccessArtifact,
+ IAccessArtifactGrant,
+ IAccessArtifactGrantSource,
+ IAccessPolicy,
+ IAccessPolicyGrant,
+ )
+from lp.services.database.bulk import create
+from lp.services.database.enumcol import DBEnum
+from lp.services.database.lpstorm import IStore
+from lp.services.database.stormbase import StormBase
+
+
+class AccessArtifact(StormBase):
+ implements(IAccessArtifact)
+
+ __storm_table__ = 'AccessArtifact'
+
+ id = Int(primary=True)
+ bug_id = Int(name='bug')
+ bug = Reference(bug_id, 'Bug.id')
+ branch_id = Int(name='branch')
+ branch = Reference(branch_id, 'Branch.id')
+
+ @property
+ def concrete_artifact(self):
+ artifact = self.bug or self.branch
+ assert artifact is not None
+ return artifact
+
+ @classmethod
+ def _constraintForConcrete(cls, concrete_artifact):
+ from lp.bugs.interfaces.bug import IBug
+ from lp.code.interfaces.branch import IBranch
+ if IBug.providedBy(concrete_artifact):
+ col = cls.bug
+ elif IBranch.providedBy(concrete_artifact):
+ col = cls.branch
+ else:
+ raise ValueError(
+ "%r is not a valid artifact" % concrete_artifact)
+ return col == concrete_artifact
+
+ @classmethod
+ def find(cls, concrete_artifacts):
+ """See `IAccessArtifactSource`."""
+ return IStore(cls).find(
+ cls,
+ Or(*(
+ cls._constraintForConcrete(artifact)
+ for artifact in concrete_artifacts)))
+
+ @classmethod
+ def ensure(cls, concrete_artifacts):
+ """See `IAccessArtifactSource`."""
+ from lp.bugs.interfaces.bug import IBug
+ from lp.code.interfaces.branch import IBranch
+
+ existing = list(cls.find(concrete_artifacts))
+ if len(existing) == len(concrete_artifacts):
+ return existing
+
+ # Not everything exists. Create missing ones.
+ needed = (
+ set(concrete_artifacts) -
+ set(abstract.concrete_artifact for abstract in existing))
+
+ insert_values = []
+ for concrete in needed:
+ if IBug.providedBy(concrete):
+ insert_values.append((concrete, None))
+ elif IBranch.providedBy(concrete):
+ insert_values.append((None, concrete))
+ else:
+ raise ValueError("%r is not a supported artifact" % concrete)
+ new = create((cls.bug, cls.branch), insert_values, get_objects=True)
+ return list(existing) + new
+
+ @classmethod
+ def delete(cls, concrete_artifacts):
+ """See `IAccessPolicyArtifactSource`."""
+ abstracts = list(cls.find(concrete_artifacts))
+ ids = [abstract.id for abstract in abstracts]
+ if len(ids) == 0:
+ return
+ getUtility(IAccessArtifactGrantSource).revokeByArtifact(abstracts)
+ IStore(abstract).find(cls, cls.id.is_in(ids)).remove()
+
+
+class AccessPolicy(StormBase):
+ implements(IAccessPolicy)
+
+ __storm_table__ = 'AccessPolicy'
+
+ id = Int(primary=True)
+ product_id = Int(name='product')
+ product = Reference(product_id, 'Product.id')
+ distribution_id = Int(name='distribution')
+ distribution = Reference(distribution_id, 'Distribution.id')
+ type = DBEnum(allow_none=True, enum=AccessPolicyType)
+
+ @property
+ def pillar(self):
+ return self.product or self.distribution
+
+ @classmethod
+ def create(cls, policies):
+ from lp.registry.interfaces.distribution import IDistribution
+ from lp.registry.interfaces.product import IProduct
+
+ insert_values = []
+ for pillar, type in policies:
+ if IProduct.providedBy(pillar):
+ insert_values.append((pillar, None, type))
+ elif IDistribution.providedBy(pillar):
+ insert_values.append((None, pillar, type))
+ else:
+ raise ValueError("%r is not a supported pillar" % pillar)
+ return create(
+ (cls.product, cls.distribution, cls.type), insert_values,
+ get_objects=True)
+
+ @classmethod
+ def _constraintForPillar(cls, pillar):
+ from lp.registry.interfaces.distribution import IDistribution
+ from lp.registry.interfaces.product import IProduct
+ if IProduct.providedBy(pillar):
+ col = cls.product
+ elif IDistribution.providedBy(pillar):
+ col = cls.distribution
+ else:
+ raise ValueError("%r is not a supported pillar" % pillar)
+ return col == pillar
+
+ @classmethod
+ def find(cls, pillars_and_types):
+ """See `IAccessPolicySource`."""
+ return IStore(cls).find(
+ cls,
+ Or(*(
+ And(cls._constraintForPillar(pillar), cls.type == type)
+ for (pillar, type) in pillars_and_types)))
+
+ @classmethod
+ def findByID(cls, ids):
+ """See `IAccessPolicySource`."""
+ return IStore(cls).find(cls, cls.id.is_in(ids))
+
+ @classmethod
+ def findByPillar(cls, pillars):
+ """See `IAccessPolicySource`."""
+ return IStore(cls).find(
+ cls,
+ Or(*(cls._constraintForPillar(pillar) for pillar in pillars)))
+
+
+class AccessArtifactGrant(StormBase):
+ implements(IAccessArtifactGrant)
+
+ __storm_table__ = 'AccessArtifactGrant'
+ __storm_primary__ = 'abstract_artifact_id', 'grantee_id'
+
+ abstract_artifact_id = Int(name='artifact')
+ abstract_artifact = Reference(
+ abstract_artifact_id, 'AccessArtifact.id')
+ grantee_id = Int(name='grantee')
+ grantee = Reference(grantee_id, 'Person.id')
+ grantor_id = Int(name='grantor')
+ grantor = Reference(grantor_id, 'Person.id')
+ date_created = DateTime()
+
+ @property
+ def concrete_artifact(self):
+ if self.abstract_artifact is not None:
+ return self.abstract_artifact.concrete_artifact
+
+ @classmethod
+ def grant(cls, grants):
+ """See `IAccessArtifactGrantSource`."""
+ return create(
+ (cls.abstract_artifact, cls.grantee, cls.grantor), grants,
+ get_objects=True)
+
+ @classmethod
+ def find(cls, grants):
+ """See `IAccessArtifactGrantSource`."""
+ return IStore(cls).find(
+ cls,
+ Or(*(
+ And(cls.abstract_artifact == artifact, cls.grantee == grantee)
+ for (artifact, grantee) in grants)))
+
+ @classmethod
+ def findByArtifact(cls, artifacts):
+ """See `IAccessArtifactGrantSource`."""
+ ids = [artifact.id for artifact in artifacts]
+ return IStore(cls).find(cls, cls.abstract_artifact_id.is_in(ids))
+
+ @classmethod
+ def revokeByArtifact(cls, artifacts):
+ """See `IAccessPolicyGrantSource`."""
+ cls.findByArtifact(artifacts).remove()
+
+
+class AccessPolicyGrant(StormBase):
+ implements(IAccessPolicyGrant)
+
+ __storm_table__ = 'AccessPolicyGrant'
+ __storm_primary__ = 'policy_id', 'grantee_id'
+
+ policy_id = Int(name='policy')
+ policy = Reference(policy_id, 'AccessPolicy.id')
+ grantee_id = Int(name='grantee')
+ grantee = Reference(grantee_id, 'Person.id')
+ grantor_id = Int(name='grantor')
+ grantor = Reference(grantor_id, 'Person.id')
+ date_created = DateTime()
+
+ @classmethod
+ def grant(cls, grants):
+ """See `IAccessPolicyGrantSource`."""
+ return create(
+ (cls.policy, cls.grantee, cls.grantor), grants, get_objects=True)
+
+ @classmethod
+ def find(cls, grants):
+ """See `IAccessPolicyGrantSource`."""
+ return IStore(cls).find(
+ cls,
+ Or(*(
+ And(cls.policy == policy, cls.grantee == grantee)
+ for (policy, grantee) in grants)))
+
+ @classmethod
+ def findByPolicy(cls, policies):
+ """See `IAccessPolicyGrantSource`."""
+ ids = [policy.id for policy in policies]
+ return IStore(cls).find(cls, cls.policy_id.is_in(ids))
=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2012-02-22 18:51:23 +0000
+++ lib/lp/registry/model/person.py 2012-02-24 05:05:23 +0000
@@ -3526,6 +3526,42 @@
skip.append(
(decorator_table.lower(), person_pointer_column.lower()))
+ def _mergeAccessArtifactGrant(self, cur, from_id, to_id):
+ # Update only the AccessArtifactGrants that will not conflict.
+ cur.execute('''
+ UPDATE AccessArtifactGrant
+ SET grantee=%(to_id)d
+ WHERE
+ grantee = %(from_id)d
+ AND artifact NOT IN (
+ SELECT artifact
+ FROM AccessArtifactGrant
+ WHERE grantee = %(to_id)d
+ )
+ ''' % vars())
+ # and delete those left over.
+ cur.execute('''
+ DELETE FROM AccessArtifactGrant WHERE grantee = %(from_id)d
+ ''' % vars())
+
+ def _mergeAccessPolicyGrant(self, cur, from_id, to_id):
+ # Update only the AccessPolicyGrants that will not conflict.
+ cur.execute('''
+ UPDATE AccessPolicyGrant
+ SET grantee=%(to_id)d
+ WHERE
+ grantee = %(from_id)d
+ AND policy NOT IN (
+ SELECT policy
+ FROM AccessPolicyGrant
+ WHERE grantee = %(to_id)d
+ )
+ ''' % vars())
+ # and delete those left over.
+ cur.execute('''
+ DELETE FROM AccessPolicyGrant WHERE grantee = %(from_id)d
+ ''' % vars())
+
def _mergeBranches(self, from_person, to_person):
# This shouldn't use removeSecurityProxy.
branches = getUtility(IBranchCollection).ownedBy(from_person)
@@ -4079,6 +4115,11 @@
% vars())
skip.append(('gpgkey', 'owner'))
+ self._mergeAccessArtifactGrant(cur, from_id, to_id)
+ self._mergeAccessPolicyGrant(cur, from_id, to_id)
+ skip.append(('accessartifactgrant', 'grantee'))
+ skip.append(('accesspolicygrant', 'grantee'))
+
# Update the Branches that will not conflict, and fudge the names of
# ones that *do* conflict.
self._mergeBranches(from_person, to_person)
=== added file 'lib/lp/registry/tests/test_accesspolicy.py'
--- lib/lp/registry/tests/test_accesspolicy.py 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/tests/test_accesspolicy.py 2012-02-24 05:05:23 +0000
@@ -0,0 +1,307 @@
+# Copyright 2011 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+
+from storm.exceptions import LostObjectError
+from testtools.matchers import (
+ AllMatch,
+ )
+from zope.component import getUtility
+
+from lp.registry.enums import AccessPolicyType
+from lp.registry.interfaces.accesspolicy import (
+ IAccessPolicy,
+ IAccessArtifact,
+ IAccessArtifactGrant,
+ IAccessArtifactGrantSource,
+ IAccessArtifactSource,
+ IAccessPolicyGrant,
+ IAccessPolicyGrantSource,
+ IAccessPolicySource,
+ )
+from lp.services.database.lpstorm import IStore
+from lp.testing import TestCaseWithFactory
+from lp.testing.layers import DatabaseFunctionalLayer
+from lp.testing.matchers import Provides
+
+
+class TestAccessPolicy(TestCaseWithFactory):
+ layer = DatabaseFunctionalLayer
+
+ def test_provides_interface(self):
+ self.assertThat(
+ self.factory.makeAccessPolicy(), Provides(IAccessPolicy))
+
+ def test_pillar(self):
+ product = self.factory.makeProduct()
+ policy = self.factory.makeAccessPolicy(pillar=product)
+ self.assertEqual(product, policy.pillar)
+
+
+class TestAccessPolicySource(TestCaseWithFactory):
+ layer = DatabaseFunctionalLayer
+
+ def test_create(self):
+ wanted = [
+ (self.factory.makeProduct(), AccessPolicyType.PROPRIETARY),
+ (self.factory.makeDistribution(), AccessPolicyType.USERDATA),
+ ]
+ policies = getUtility(IAccessPolicySource).create(wanted)
+ self.assertThat(
+ policies,
+ AllMatch(Provides(IAccessPolicy)))
+ self.assertContentEqual(
+ wanted,
+ [(policy.pillar, policy.type) for policy in policies])
+
+ def test_find(self):
+ # find() finds the right policies.
+ product = self.factory.makeProduct()
+ distribution = self.factory.makeDistribution()
+ other_product = self.factory.makeProduct()
+
+ wanted = [
+ (product, AccessPolicyType.PROPRIETARY),
+ (product, AccessPolicyType.USERDATA),
+ (distribution, AccessPolicyType.PROPRIETARY),
+ (distribution, AccessPolicyType.USERDATA),
+ (other_product, AccessPolicyType.PROPRIETARY),
+ ]
+ getUtility(IAccessPolicySource).create(wanted)
+
+ query = [
+ (product, AccessPolicyType.PROPRIETARY),
+ (product, AccessPolicyType.USERDATA),
+ (distribution, AccessPolicyType.USERDATA),
+ ]
+ self.assertContentEqual(
+ query,
+ [(policy.pillar, policy.type) for policy in
+ getUtility(IAccessPolicySource).find(query)])
+
+ query = [(distribution, AccessPolicyType.PROPRIETARY)]
+ self.assertContentEqual(
+ query,
+ [(policy.pillar, policy.type) for policy in
+ getUtility(IAccessPolicySource).find(query)])
+
+ def test_findByID(self):
+ # findByID finds the right policies.
+ policies = [self.factory.makeAccessPolicy() for i in range(2)]
+ self.factory.makeAccessPolicy()
+ self.assertContentEqual(
+ policies,
+ getUtility(IAccessPolicySource).findByID(
+ [policy.id for policy in policies]))
+
+ def test_findByPillar(self):
+ # findByPillar finds only the relevant policies.
+ product = self.factory.makeProduct()
+ distribution = self.factory.makeProduct()
+ other_product = self.factory.makeProduct()
+ wanted = [
+ (pillar, type)
+ for type in AccessPolicyType.items
+ for pillar in (product, distribution, other_product)]
+ policies = getUtility(IAccessPolicySource).create(wanted)
+ self.assertContentEqual(
+ policies,
+ getUtility(IAccessPolicySource).findByPillar(
+ [product, distribution, other_product]))
+ self.assertContentEqual(
+ [policy for policy in policies if policy.pillar == product],
+ getUtility(IAccessPolicySource).findByPillar([product]))
+
+
+class TestAccessArtifact(TestCaseWithFactory):
+ layer = DatabaseFunctionalLayer
+
+ def test_provides_interface(self):
+ self.assertThat(
+ self.factory.makeAccessArtifact(),
+ Provides(IAccessArtifact))
+
+
+class TestAccessArtifactSourceOnce(TestCaseWithFactory):
+ layer = DatabaseFunctionalLayer
+
+ def test_ensure_other_fails(self):
+ # ensure() rejects unsupported objects.
+ self.assertRaises(
+ ValueError,
+ getUtility(IAccessArtifactSource).ensure,
+ [self.factory.makeProduct()])
+
+
+class BaseAccessArtifactTests:
+ layer = DatabaseFunctionalLayer
+
+ def getConcreteArtifact(self):
+ raise NotImplementedError()
+
+ def test_ensure(self):
+ # ensure() creates abstract artifacts which map to the
+ # concrete ones.
+ concretes = [self.getConcreteArtifact() for i in range(2)]
+ abstracts = getUtility(IAccessArtifactSource).ensure(concretes)
+ self.assertContentEqual(
+ concretes,
+ [abstract.concrete_artifact for abstract in abstracts])
+
+ def test_find(self):
+ # find() finds abstract artifacts which map to the concrete ones.
+ concretes = [self.getConcreteArtifact() for i in range(2)]
+ abstracts = getUtility(IAccessArtifactSource).ensure(concretes)
+ self.assertContentEqual(
+ abstracts, getUtility(IAccessArtifactSource).find(concretes))
+
+ def test_ensure_twice(self):
+ # ensure() will reuse an existing matching abstract artifact if
+ # it exists.
+ concrete1 = self.getConcreteArtifact()
+ concrete2 = self.getConcreteArtifact()
+ [abstract1] = getUtility(IAccessArtifactSource).ensure([concrete1])
+
+ abstracts = getUtility(IAccessArtifactSource).ensure(
+ [concrete1, concrete2])
+ self.assertIn(abstract1, abstracts)
+ self.assertContentEqual(
+ [concrete1, concrete2],
+ [abstract.concrete_artifact for abstract in abstracts])
+
+ def test_delete(self):
+ # delete() removes the abstract artifacts and any associated
+ # grants.
+ concretes = [self.getConcreteArtifact() for i in range(2)]
+ abstracts = getUtility(IAccessArtifactSource).ensure(concretes)
+ grant = self.factory.makeAccessArtifactGrant(artifact=abstracts[0])
+
+ # Make some other grants to ensure they're unaffected.
+ other_grants = [
+ self.factory.makeAccessArtifactGrant(
+ artifact=self.factory.makeAccessArtifact()),
+ self.factory.makeAccessPolicyGrant(
+ policy=self.factory.makeAccessPolicy()),
+ ]
+
+ getUtility(IAccessArtifactSource).delete(concretes)
+ IStore(grant).invalidate()
+ self.assertRaises(LostObjectError, getattr, grant, 'grantor')
+ self.assertRaises(
+ LostObjectError, getattr, abstracts[0], 'concrete_artifact')
+
+ for other_grant in other_grants:
+ self.assertIsNot(None, other_grant.grantor)
+
+ def test_delete_noop(self):
+ # delete() works even if there's no abstract artifact.
+ concrete = self.getConcreteArtifact()
+ getUtility(IAccessArtifactSource).delete([concrete])
+
+
+class TestAccessArtifactBranch(BaseAccessArtifactTests,
+ TestCaseWithFactory):
+
+ def getConcreteArtifact(self):
+ return self.factory.makeBranch()
+
+
+class TestAccessArtifactBug(BaseAccessArtifactTests,
+ TestCaseWithFactory):
+
+ def getConcreteArtifact(self):
+ return self.factory.makeBug()
+
+
+class TestAccessArtifactGrant(TestCaseWithFactory):
+ layer = DatabaseFunctionalLayer
+
+ def test_provides_interface(self):
+ self.assertThat(
+ self.factory.makeAccessArtifactGrant(),
+ Provides(IAccessArtifactGrant))
+
+ def test_concrete_artifact(self):
+ bug = self.factory.makeBug()
+ abstract = self.factory.makeAccessArtifact(bug)
+ grant = self.factory.makeAccessArtifactGrant(artifact=abstract)
+ self.assertEqual(bug, grant.concrete_artifact)
+
+
+class TestAccessArtifactGrantSource(TestCaseWithFactory):
+ layer = DatabaseFunctionalLayer
+
+ def test_grant(self):
+ wanted = [
+ (self.factory.makeAccessArtifact(), self.factory.makePerson(),
+ self.factory.makePerson()),
+ (self.factory.makeAccessArtifact(), self.factory.makePerson(),
+ self.factory.makePerson()),
+ ]
+ grants = getUtility(IAccessArtifactGrantSource).grant(wanted)
+ self.assertContentEqual(
+ wanted,
+ [(g.abstract_artifact, g.grantee, g.grantor) for g in grants])
+
+ def test_find(self):
+ # find() finds the right grants.
+ grants = [self.factory.makeAccessArtifactGrant() for i in range(2)]
+ self.assertContentEqual(
+ grants,
+ getUtility(IAccessArtifactGrantSource).find(
+ [(g.abstract_artifact, g.grantee) for g in grants]))
+
+ def test_findByArtifact(self):
+ # findByArtifact() finds only the relevant grants.
+ artifact = self.factory.makeAccessArtifact()
+ grants = [
+ self.factory.makeAccessArtifactGrant(artifact=artifact)
+ for i in range(3)]
+ self.factory.makeAccessArtifactGrant()
+ self.assertContentEqual(
+ grants,
+ getUtility(IAccessArtifactGrantSource).findByArtifact([artifact]))
+
+
+class TestAccessPolicyGrant(TestCaseWithFactory):
+ layer = DatabaseFunctionalLayer
+
+ def test_provides_interface(self):
+ self.assertThat(
+ self.factory.makeAccessPolicyGrant(),
+ Provides(IAccessPolicyGrant))
+
+
+class TestAccessPolicyGrantSource(TestCaseWithFactory):
+ layer = DatabaseFunctionalLayer
+
+ def test_grant(self):
+ wanted = [
+ (self.factory.makeAccessPolicy(), self.factory.makePerson(),
+ self.factory.makePerson()),
+ (self.factory.makeAccessPolicy(), self.factory.makePerson(),
+ self.factory.makePerson()),
+ ]
+ grants = getUtility(IAccessPolicyGrantSource).grant(wanted)
+ self.assertContentEqual(
+ wanted, [(g.policy, g.grantee, g.grantor) for g in grants])
+
+ def test_find(self):
+ # find() finds the right grants.
+ grants = [self.factory.makeAccessPolicyGrant() for i in range(2)]
+ self.assertContentEqual(
+ grants,
+ getUtility(IAccessPolicyGrantSource).find(
+ [(g.policy, g.grantee) for g in grants]))
+
+ def test_findByPolicy(self):
+ # findByPolicy() finds only the relevant grants.
+ policy = self.factory.makeAccessPolicy()
+ grants = [
+ self.factory.makeAccessPolicyGrant(policy=policy)
+ for i in range(3)]
+ self.factory.makeAccessPolicyGrant()
+ self.assertContentEqual(
+ grants,
+ getUtility(IAccessPolicyGrantSource).findByPolicy([policy]))
=== modified file 'lib/lp/registry/tests/test_personset.py'
--- lib/lp/registry/tests/test_personset.py 2012-02-20 10:32:23 +0000
+++ lib/lp/registry/tests/test_personset.py 2012-02-24 05:05:23 +0000
@@ -13,6 +13,7 @@
from testtools.matchers import (
LessThan,
+ MatchesStructure,
)
from zope.component import getUtility
@@ -23,6 +24,7 @@
InvalidName,
NameAlreadyTaken,
)
+from lp.registry.interfaces.accesspolicy import IAccessPolicyGrantSource
from lp.registry.interfaces.karma import IKarmaCacheManager
from lp.registry.interfaces.mailinglist import MailingListStatus
from lp.registry.interfaces.mailinglistsubscription import (
@@ -527,6 +529,48 @@
dsp = self.factory.makeDistributionSourcePackage()
self.assertConflictingSubscriptionDeletes(dsp)
+ def test_merge_accesspolicygrants(self):
+ # AccessPolicyGrants are transferred from the duplicate.
+ person = self.factory.makePerson()
+ grant = self.factory.makeAccessPolicyGrant()
+ self._do_premerge(grant.grantee, person)
+
+ source = getUtility(IAccessPolicyGrantSource)
+ self.assertEqual(
+ grant.grantee, source.findByPolicy([grant.policy]).one().grantee)
+ with person_logged_in(person):
+ self._do_merge(grant.grantee, person)
+ self.assertEqual(
+ person, source.findByPolicy([grant.policy]).one().grantee)
+
+ def test_merge_accesspolicygrants_conflicts(self):
+ # Conflicting AccessPolicyGrants are deleted.
+ policy = self.factory.makeAccessPolicy()
+
+ person = self.factory.makePerson()
+ person_grantor = self.factory.makePerson()
+ person_grant = self.factory.makeAccessPolicyGrant(
+ grantee=person, grantor=person_grantor, policy=policy)
+ person_grant_date = person_grant.date_created
+
+ duplicate = self.factory.makePerson()
+ duplicate_grantor = self.factory.makePerson()
+ self.factory.makeAccessPolicyGrant(
+ grantee=duplicate, grantor=duplicate_grantor, policy=policy)
+
+ self._do_premerge(duplicate, person)
+ with person_logged_in(person):
+ self._do_merge(duplicate, person)
+
+ # Only one grant for the policy exists: the retained person's.
+ source = getUtility(IAccessPolicyGrantSource)
+ self.assertThat(
+ source.findByPolicy([policy]).one(),
+ MatchesStructure.byEquality(
+ policy=policy,
+ grantee=person,
+ date_created=person_grant_date))
+
def test_mergeAsync(self):
# mergeAsync() creates a new `PersonMergeJob`.
from_person = self.factory.makePerson()
=== modified file 'lib/lp/services/database/bulk.py'
--- lib/lp/services/database/bulk.py 2012-02-22 05:07:08 +0000
+++ lib/lp/services/database/bulk.py 2012-02-24 05:05:23 +0000
@@ -26,6 +26,7 @@
And,
Insert,
Or,
+ SQL,
)
from storm.info import (
get_cls_info,
@@ -168,7 +169,9 @@
def _dbify_value(col, val):
"""Convert a value into a form that Storm can compile directly."""
- if isinstance(col, Reference):
+ if isinstance(val, SQL):
+ return (val,)
+ elif isinstance(col, Reference):
# References are mainly meant to be used as descriptors, so we
# have to perform a bit of evil here to turn the (potentially
# None) value into a sequence of primary key values.
@@ -192,14 +195,16 @@
return (col,)
-def create(columns, values, return_created=True):
+def create(columns, values, get_objects=False,
+ get_primary_keys=False):
"""Create a large number of objects efficiently.
:param cols: The Storm columns to insert values into. Must be from a
single class.
:param values: A list of lists of values for the columns.
- :param return_created: Retrieve the created objects.
- :return: A list of the created objects if return_created, otherwise None.
+ :param get_objects: Return the created objects.
+ :param get_primary_keys: Return the created primary keys.
+ :return: A list of the created objects if get_created, otherwise None.
"""
# Flatten Reference faux-columns into their primary keys.
db_cols = list(chain.from_iterable(map(_dbify_column, columns)))
@@ -208,9 +213,12 @@
raise ValueError(
"The Storm columns to insert values into must be from a single "
"class.")
+ if get_objects and get_primary_keys:
+ raise ValueError(
+ "get_objects and get_primary_keys are mutually exclusive.")
if len(values) == 0:
- return [] if return_created else None
+ return [] if (get_objects or get_primary_keys) else None
[cls] = clses
primary_key = get_cls_info(cls).primary_key
@@ -223,12 +231,15 @@
_dbify_value(col, val) for col, val in zip(columns, value)))
for value in values]
- if not return_created:
- IStore(cls).execute(Insert(db_cols, expr=db_values))
- return None
- else:
+ if get_objects or get_primary_keys:
result = IStore(cls).execute(
Returning(Insert(
db_cols, expr=db_values, primary_columns=primary_key)))
keys = map(itemgetter(0), result) if len(primary_key) == 1 else result
- return load(cls, keys)
+ if get_objects:
+ return load(cls, keys)
+ else:
+ return list(keys)
+ else:
+ IStore(cls).execute(Insert(db_cols, expr=db_values))
+ return None
=== modified file 'lib/lp/services/database/tests/test_bulk.py'
--- lib/lp/services/database/tests/test_bulk.py 2012-02-22 05:07:08 +0000
+++ lib/lp/services/database/tests/test_bulk.py 2012-02-24 05:05:23 +0000
@@ -9,6 +9,7 @@
from pytz import UTC
from storm.exceptions import ClassInfoError
+from storm.expr import SQL
from storm.info import get_obj_info
from storm.store import Store
from testtools.matchers import Equals
@@ -34,6 +35,7 @@
ISlaveStore,
IStore,
)
+from lp.services.database.sqlbase import get_transaction_timestamp
from lp.services.features.model import (
FeatureFlag,
getFeatureStore,
@@ -260,7 +262,7 @@
(BugSubscription.bug, BugSubscription.person,
BugSubscription.subscribed_by, BugSubscription.date_created,
BugSubscription.bug_notification_level),
- wanted)
+ wanted, get_objects=True)
self.assertThat(recorder, HasQueryCount(Equals(2)))
self.assertContentEqual(
@@ -274,7 +276,7 @@
wanted = [(None, job, BranchJobType.RECLAIM_BRANCH_SPACE)]
[branchjob] = bulk.create(
(BranchJob.branch, BranchJob.job, BranchJob.job_type),
- wanted)
+ wanted, get_objects=True)
self.assertEqual(
wanted, [(branchjob.branch, branchjob.job, branchjob.job_type)])
@@ -294,9 +296,24 @@
def test_zero_values_is_noop(self):
# create()ing 0 rows is a no-op.
with StormStatementRecorder() as recorder:
- self.assertEqual([], bulk.create((BugSubscription.bug,), []))
+ self.assertEqual(
+ [],
+ bulk.create((BugSubscription.bug,), [], get_objects=True))
self.assertThat(recorder, HasQueryCount(Equals(0)))
+ def test_can_return_ids(self):
+ # create() can be asked to return the created IDs instead of objects.
+ job = IStore(Job).add(Job())
+ IStore(Job).flush()
+ wanted = [(None, job, BranchJobType.RECLAIM_BRANCH_SPACE)]
+ with StormStatementRecorder() as recorder:
+ [created_id] = bulk.create(
+ (BranchJob.branch, BranchJob.job, BranchJob.job_type),
+ wanted, get_primary_keys=True)
+ self.assertThat(recorder, HasQueryCount(Equals(1)))
+ [reclaimjob] = ReclaimBranchSpaceJob.iterReady()
+ self.assertEqual(created_id, reclaimjob.context.id)
+
def test_load_can_be_skipped(self):
# create() can be told not to load the created rows.
job = IStore(Job).add(Job())
@@ -307,9 +324,23 @@
None,
bulk.create(
(BranchJob.branch, BranchJob.job, BranchJob.job_type),
- wanted, return_created=False))
+ wanted, get_objects=False))
self.assertThat(recorder, HasQueryCount(Equals(1)))
[reclaimjob] = ReclaimBranchSpaceJob.iterReady()
branchjob = reclaimjob.context
self.assertEqual(
wanted, [(branchjob.branch, branchjob.job, branchjob.job_type)])
+
+ def test_sql_passed_through(self):
+ # create() passes SQL() expressions through untouched.
+ bug = self.factory.makeBug()
+ person = self.factory.makePerson()
+
+ [sub] = bulk.create(
+ (BugSubscription.bug, BugSubscription.person,
+ BugSubscription.subscribed_by, BugSubscription.date_created,
+ BugSubscription.bug_notification_level),
+ [(bug, person, person,
+ SQL("CURRENT_TIMESTAMP AT TIME ZONE 'UTC'"),
+ BugNotificationLevel.LIFECYCLE)], get_objects=True)
+ self.assertEqual(get_transaction_timestamp(), sub.date_created)
=== modified file 'lib/lp/services/job/model/job.py'
--- lib/lp/services/job/model/job.py 2011-12-30 06:14:56 +0000
+++ lib/lp/services/job/model/job.py 2012-02-24 05:05:23 +0000
@@ -27,13 +27,11 @@
)
from zope.interface import implements
+from lp.services.database import bulk
from lp.services.database.constants import UTC_NOW
from lp.services.database.datetimecol import UtcDateTimeCol
from lp.services.database.enumcol import EnumCol
-from lp.services.database.sqlbase import (
- quote,
- SQLBase,
- )
+from lp.services.database.sqlbase import SQLBase
from lp.services.job.interfaces.job import (
IJob,
JobStatus,
@@ -123,15 +121,10 @@
:param request: The `IPerson` requesting the jobs.
:return: An iterable of `Job.id` values for the new jobs.
"""
- job_contents = [
- "(%s, %s)" % (
- quote(JobStatus.WAITING), quote(requester))] * num_jobs
- result = store.execute("""
- INSERT INTO Job (status, requester)
- VALUES %s
- RETURNING id
- """ % ", ".join(job_contents))
- return [job_id for job_id, in result]
+ return bulk.create(
+ (Job._status, Job.requester),
+ [(JobStatus.WAITING, requester) for i in range(num_jobs)],
+ get_primary_keys=True)
def acquireLease(self, duration=300):
"""See `IJob`."""
=== modified file 'lib/lp/soyuz/model/distroseriesdifferencejob.py'
--- lib/lp/soyuz/model/distroseriesdifferencejob.py 2011-12-30 06:14:56 +0000
+++ lib/lp/soyuz/model/distroseriesdifferencejob.py 2012-02-24 05:05:23 +0000
@@ -8,7 +8,6 @@
'DistroSeriesDifferenceJob',
]
-import simplejson
from zope.component import getUtility
from zope.interface import (
classProvides,
@@ -22,11 +21,11 @@
from lp.registry.model.distroseries import DistroSeries
from lp.registry.model.distroseriesdifference import DistroSeriesDifference
from lp.registry.model.sourcepackagename import SourcePackageName
+from lp.services.database import bulk
from lp.services.database.lpstorm import (
IMasterStore,
IStore,
)
-from lp.services.database.sqlbase import quote
from lp.services.features import getFeatureFlag
from lp.services.job.model.job import Job
from lp.soyuz.interfaces.distributionjob import (
@@ -74,28 +73,6 @@
return DistroSeriesDifferenceJob(job)
-def compose_job_insertion_tuple(derived_series, parent_series,
- sourcepackagename_id, job_id):
- """Compose tuple for insertion into `DistributionJob`.
-
- :param derived_series: Derived `DistroSeries`.
- :param parent_series: Parent `DistroSeries`.
- :param sourcepackagename_id: ID of `SourcePackageName`.
- :param job_id: associated `Job` id.
- :return: A tuple of: derived distribution id, derived distroseries id,
- job type, job id, JSON data map.
- """
- json = simplejson.dumps(
- make_metadata(sourcepackagename_id, parent_series.id))
- return (
- derived_series.distribution.id,
- derived_series.id,
- DistributionJobType.DISTROSERIESDIFFERENCE,
- job_id,
- json,
- )
-
-
def create_multiple_jobs(derived_series, parent_series):
"""Create `DistroSeriesDifferenceJob`s between parent and derived series.
@@ -120,20 +97,15 @@
sourcepackagenames = source_package_releases.values(
SourcePackageRelease.sourcepackagenameID)
job_ids = Job.createMultiple(store, nb_jobs)
-
- job_tuples = [
- quote(compose_job_insertion_tuple(
- derived_series, parent_series, sourcepackagename, job_id))
- for job_id, sourcepackagename in zip(job_ids, sourcepackagenames)]
-
- store = IStore(DistributionJob)
- result = store.execute("""
- INSERT INTO DistributionJob (
- distribution, distroseries, job_type, job, json_data)
- VALUES %s
- RETURNING id
- """ % ", ".join(job_tuples))
- return [job_id for job_id, in result]
+ return bulk.create(
+ (DistributionJob.distribution, DistributionJob.distroseries,
+ DistributionJob.job_type, DistributionJob.job_id,
+ DistributionJob.metadata),
+ [(derived_series.distribution, derived_series,
+ DistributionJobType.DISTROSERIESDIFFERENCE, job_id,
+ make_metadata(spn_id, parent_series.id))
+ for job_id, spn_id in zip(job_ids, sourcepackagenames)],
+ get_primary_keys=True)
def find_waiting_jobs(derived_series, sourcepackagename, parent_series):
=== modified file 'lib/lp/soyuz/model/packagecopyjob.py'
--- lib/lp/soyuz/model/packagecopyjob.py 2012-01-17 21:45:24 +0000
+++ lib/lp/soyuz/model/packagecopyjob.py 2012-02-24 05:05:23 +0000
@@ -11,7 +11,6 @@
import logging
from lazr.delegates import delegates
-import simplejson
from storm.locals import (
And,
Int,
@@ -40,13 +39,13 @@
from lp.registry.interfaces.pocket import PackagePublishingPocket
from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
from lp.registry.model.distroseries import DistroSeries
+from lp.services.database import bulk
from lp.services.database.decoratedresultset import DecoratedResultSet
from lp.services.database.enumcol import EnumCol
from lp.services.database.lpstorm import (
IMasterStore,
IStore,
)
-from lp.services.database.sqlbase import sqlvalues
from lp.services.database.stormbase import StormBase
from lp.services.job.interfaces.job import (
JobStatus,
@@ -297,9 +296,8 @@
data = (
cls.class_job_type, target_distroseries, copy_policy,
source_archive, target_archive, package_name, job_id,
- simplejson.dumps(metadata, ensure_ascii=False))
- format_string = "(%s)" % ", ".join(["%s"] * len(data))
- return format_string % sqlvalues(*data)
+ metadata)
+ return data
@classmethod
def createMultiple(cls, target_distroseries, copy_tasks, requester,
@@ -313,20 +311,12 @@
target_distroseries, copy_policy, include_binaries, job_id,
task, sponsored)
for job_id, task in zip(job_ids, copy_tasks)]
- result = store.execute("""
- INSERT INTO PackageCopyJob (
- job_type,
- target_distroseries,
- copy_policy,
- source_archive,
- target_archive,
- package_name,
- job,
- json_data)
- VALUES %s
- RETURNING id
- """ % ", ".join(job_contents))
- return [job_id for job_id, in result]
+ return bulk.create(
+ (PackageCopyJob.job_type, PackageCopyJob.target_distroseries,
+ PackageCopyJob.copy_policy, PackageCopyJob.source_archive,
+ PackageCopyJob.target_archive, PackageCopyJob.package_name,
+ PackageCopyJob.job_id, PackageCopyJob.metadata),
+ job_contents, get_primary_keys=True)
@classmethod
def getActiveJobs(cls, target_archive):
=== modified file 'lib/lp/soyuz/model/publishing.py'
--- lib/lp/soyuz/model/publishing.py 2012-01-20 13:23:01 +0000
+++ lib/lp/soyuz/model/publishing.py 2012-02-24 05:05:23 +0000
@@ -48,15 +48,13 @@
from lp.buildmaster.model.packagebuild import PackageBuild
from lp.registry.interfaces.person import validate_public_person
from lp.registry.interfaces.pocket import PackagePublishingPocket
+from lp.services.database import bulk
from lp.services.database.constants import UTC_NOW
from lp.services.database.datetimecol import UtcDateTimeCol
from lp.services.database.decoratedresultset import DecoratedResultSet
from lp.services.database.enumcol import EnumCol
from lp.services.database.lpstorm import IMasterStore
-from lp.services.database.sqlbase import (
- SQLBase,
- sqlvalues,
- )
+from lp.services.database.sqlbase import SQLBase
from lp.services.librarian.browser import ProxiedLibraryFileAlias
from lp.services.librarian.model import (
LibraryFileAlias,
@@ -1462,29 +1460,18 @@
if not needed:
return []
- insert_head = """
- INSERT INTO BinaryPackagePublishingHistory
- (archive, distroarchseries, pocket, binarypackagerelease,
- binarypackagename, component, section, priority, status,
- datecreated)
- VALUES
- """
- insert_pubs = ", ".join(
- "(%s)" % ", ".join(sqlvalues(
- get_archive(archive, bpr).id, das.id, pocket, bpr.id,
- bpr.binarypackagename,
- get_component(archive, das.distroseries, component).id,
- section.id, priority, PackagePublishingStatus.PENDING,
- UTC_NOW))
- for (das, bpr, (component, section, priority)) in needed)
- insert_tail = " RETURNING BinaryPackagePublishingHistory.id"
- new_ids = IMasterStore(BinaryPackagePublishingHistory).execute(
- insert_head + insert_pubs + insert_tail)
-
- publications = IMasterStore(BinaryPackagePublishingHistory).find(
- BinaryPackagePublishingHistory,
- BinaryPackagePublishingHistory.id.is_in(id[0] for id in new_ids))
- return list(publications)
+ BPPH = BinaryPackagePublishingHistory
+ return bulk.create(
+ (BPPH.archive, BPPH.distroarchseries, BPPH.pocket,
+ BPPH.binarypackagerelease, BPPH.binarypackagename,
+ BPPH.component, BPPH.section, BPPH.priority, BPPH.status,
+ BPPH.datecreated),
+ [(get_archive(archive, bpr), das, pocket, bpr,
+ bpr.binarypackagename,
+ get_component(archive, das.distroseries, component),
+ section, priority, PackagePublishingStatus.PENDING, UTC_NOW)
+ for (das, bpr, (component, section, priority)) in needed],
+ get_objects=True)
def publishBinary(self, archive, binarypackagerelease, distroseries,
component, section, priority, pocket):
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2012-02-20 02:07:55 +0000
+++ lib/lp/testing/factory.py 2012-02-24 05:05:23 +0000
@@ -138,9 +138,16 @@
IHWSubmissionSet,
)
from lp.registry.enums import (
+ AccessPolicyType,
DistroSeriesDifferenceStatus,
DistroSeriesDifferenceType,
)
+from lp.registry.interfaces.accesspolicy import (
+ IAccessArtifactGrantSource,
+ IAccessArtifactSource,
+ IAccessPolicyGrantSource,
+ IAccessPolicySource,
+ )
from lp.registry.interfaces.distribution import (
IDistribution,
IDistributionSet,
@@ -4356,6 +4363,42 @@
target_distroseries, target_pocket,
package_version=package_version, requester=requester)
+ def makeAccessPolicy(self, pillar=None,
+ type=AccessPolicyType.PROPRIETARY):
+ if pillar is None:
+ pillar = self.makeProduct()
+ policies = getUtility(IAccessPolicySource).create([(pillar, type)])
+ return policies[0]
+
+ def makeAccessArtifact(self, concrete=None):
+ if concrete is None:
+ concrete = self.makeBranch()
+ artifacts = getUtility(IAccessArtifactSource).ensure([concrete])
+ return artifacts[0]
+
+ def makeAccessArtifactGrant(self, artifact=None, grantee=None,
+ grantor=None):
+ if artifact is None:
+ artifact = self.makeAccessArtifact()
+ if grantee is None:
+ grantee = self.makePerson()
+ if grantor is None:
+ grantor = self.makePerson()
+ [grant] = getUtility(IAccessArtifactGrantSource).grant(
+ [(artifact, grantee, grantor)])
+ return grant
+
+ def makeAccessPolicyGrant(self, policy=None, grantee=None, grantor=None):
+ if policy is None:
+ policy = self.makeAccessPolicy()
+ if grantee is None:
+ grantee = self.makePerson()
+ if grantor is None:
+ grantor = self.makePerson()
+ [grant] = getUtility(IAccessPolicyGrantSource).grant(
+ [(policy, grantee, grantor)])
+ return grant
+
def makeFakeFileUpload(self, filename=None, content=None):
"""Return a zope.publisher.browser.FileUpload like object.