launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #14633
[Merge] lp:~stub/launchpad/trivial into lp:launchpad
Stuart Bishop has proposed merging lp:~stub/launchpad/trivial into lp:launchpad.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #951401 in Launchpad itself: "parse-ppa-apache-logs failing (missing files)"
https://bugs.launchpad.net/launchpad/+bug/951401
For more details, see:
https://code.launchpad.net/~stub/launchpad/trivial/+merge/137141
= Summary =
allocate-revision-karma.py is broken and causing trouble on production. This is Bug #1050191
== Proposed fix ==
Disable it.
== Pre-implementation notes ==
== LOC Rationale ==
== Implementation details ==
== Tests ==
== Demo and Q/A ==
= Launchpad lint =
Checking for conflicts and issues in changed files.
Linting changed files:
database/schema/patch-2209-39-0.sql
database/schema/security.cfg
cronscripts/nightly.sh
./cronscripts/nightly.sh
30: Line exceeds 80 characters.
34: Line exceeds 80 characters.
37: Line exceeds 80 characters.
46: Line exceeds 80 characters.
--
https://code.launchpad.net/~stub/launchpad/trivial/+merge/137141
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~stub/launchpad/trivial into lp:launchpad.
=== modified file 'cronscripts/nightly.sh'
--- cronscripts/nightly.sh 2012-06-19 14:34:56 +0000
+++ cronscripts/nightly.sh 2012-11-30 10:13:01 +0000
@@ -29,8 +29,9 @@
echo $(date): Expiring memberships >> $LOGFILE
python -S flag-expired-memberships.py -q --log-file=DEBUG:$LOGDIR/flag-expired-memberships.log
-echo $(date): Allocating revision karma >> $LOGFILE
-python -S allocate-revision-karma.py -q --log-file=DEBUG:$LOGDIR/allocate-revision-karma.log
+## Disabled per Bug #1050191, StuartBishop 20121130
+## echo $(date): Allocating revision karma >> $LOGFILE
+## python -S allocate-revision-karma.py -q --log-file=DEBUG:$LOGDIR/allocate-revision-karma.log
echo $(date): Recalculating karma >> $LOGFILE
python -S foaf-update-karma-cache.py -q --log-file=INFO:$LOGDIR/foaf-update-karma-cache.log
=== modified file 'database/schema/Makefile'
=== modified file 'database/schema/comments.sql'
=== modified file 'lib/lp/answers/browser/question.py'
=== modified file 'lib/lp/answers/browser/questiontarget.py'
=== modified file 'lib/lp/app/browser/tales.py'
=== modified file 'lib/lp/app/doc/tales.txt'
=== modified file 'lib/lp/archiveuploader/tests/test_sync_notification.py'
=== modified file 'lib/lp/blueprints/browser/tests/test_specification.py'
=== modified file 'lib/lp/blueprints/interfaces/specification.py'
=== modified file 'lib/lp/blueprints/model/specification.py'
--- lib/lp/blueprints/model/specification.py 2012-11-26 08:33:03 +0000
+++ lib/lp/blueprints/model/specification.py 2012-11-30 10:13:01 +0000
@@ -3,7 +3,12 @@
__metaclass__ = type
__all__ = [
- 'get_specification_filters',
+<<<<<<< TREE
+ 'get_specification_filters',
+=======
+ 'get_specification_filters',
+ 'get_specification_privacy_filter',
+>>>>>>> MERGE-SOURCE
'HasSpecificationsMixin',
'recursive_blocked_query',
'recursive_dependent_query',
@@ -35,6 +40,7 @@
LeftJoin,
Not,
Or,
+ Not,
Select,
)
from storm.locals import (
@@ -112,7 +118,12 @@
SQLBase,
sqlvalues,
)
-from lp.services.database.stormexpr import fti_search
+<<<<<<< TREE
+from lp.services.database.stormexpr import fti_search
+=======
+from lp.services.database.stormexpr import fti_search
+from lp.services.database.lpstorm import IStore
+>>>>>>> MERGE-SOURCE
from lp.services.mail.helpers import get_contact_email_addresses
from lp.services.propertycache import (
cachedproperty,
@@ -988,7 +999,11 @@
elif sort == SpecificationSort.DATE:
return (Desc(Specification.datecreated), Specification.id)
+<<<<<<< TREE
def _preload_specifications_people(self, tables, clauses):
+=======
+ def _preload_specifications_people(self, clauses):
+>>>>>>> MERGE-SOURCE
"""Perform eager loading of people and their validity for query.
:param query: a string query generated in the 'specifications'
@@ -1029,7 +1044,11 @@
index += 1
decorator(person, column)
+<<<<<<< TREE
results = Store.of(self).using(*tables).find(Specification, *clauses)
+=======
+ results = Store.of(self).find(Specification, *clauses)
+>>>>>>> MERGE-SOURCE
return DecoratedResultSet(results, pre_iter_hook=cache_people)
@property
@@ -1272,6 +1291,7 @@
AccessPolicy,
AccessPolicyGrantFlat,
)
+<<<<<<< TREE
tables = [
Specification,
LeftJoin(Product, Specification.productID == Product.id),
@@ -1354,3 +1374,148 @@
Specification.definition_status ==
SpecificationDefinitionStatus.APPROVED
))
+=======
+ public_specification_filter = (
+ Specification.information_type.is_in(PUBLIC_INFORMATION_TYPES))
+ if user is None:
+ return public_specification_filter
+ return Or(
+ public_specification_filter,
+ Specification.id.is_in(
+ Select(
+ Specification.id,
+ tables=(
+ Specification,
+ Join(
+ AccessPolicy,
+ And(
+ Or(
+ Specification.productID ==
+ AccessPolicy.product_id,
+ Specification.distributionID ==
+ AccessPolicy.distribution_id),
+ Specification.information_type ==
+ AccessPolicy.type)),
+ Join(
+ AccessPolicyGrantFlat,
+ AccessPolicy.id == AccessPolicyGrantFlat.policy_id),
+ LeftJoin(
+ AccessArtifact,
+ AccessPolicyGrantFlat.abstract_artifact_id ==
+ AccessArtifact.id),
+ Join(
+ TeamParticipation,
+ And(
+ TeamParticipation.team ==
+ AccessPolicyGrantFlat.grantee_id,
+ TeamParticipation.person == user))),
+ where=Or(
+ AccessPolicyGrantFlat.abstract_artifact_id == None,
+ AccessArtifact.specification_id == Specification.id))))
+
+
+def visible_specification_query(user):
+ """Return a Storm expression and list of tables for filtering
+ specifications by privacy.
+
+ :param user: A Person ID or a column reference.
+ :return: A tuple of tables, clauses to filter out specifications that the
+ user cannot see.
+ """
+ from lp.registry.model.product import Product
+ from lp.registry.model.accesspolicy import (
+ AccessArtifact,
+ AccessPolicy,
+ AccessPolicyGrantFlat,
+ )
+ tables = [
+ Specification,
+ LeftJoin(Product, Specification.productID == Product.id),
+ LeftJoin(AccessPolicy, And(
+ Or(Specification.productID == AccessPolicy.product_id,
+ Specification.distributionID ==
+ AccessPolicy.distribution_id),
+ Specification.information_type == AccessPolicy.type)),
+ LeftJoin(AccessPolicyGrantFlat,
+ AccessPolicy.id == AccessPolicyGrantFlat.policy_id),
+ LeftJoin(
+ TeamParticipation,
+ And(AccessPolicyGrantFlat.grantee == TeamParticipation.teamID,
+ TeamParticipation.person == user)),
+ LeftJoin(AccessArtifact,
+ AccessPolicyGrantFlat.abstract_artifact_id ==
+ AccessArtifact.id)
+ ]
+ clauses = [
+ Or(Specification.information_type.is_in(PUBLIC_INFORMATION_TYPES),
+ And(AccessPolicyGrantFlat.id != None,
+ TeamParticipation.personID != None,
+ Or(AccessPolicyGrantFlat.abstract_artifact == None,
+ AccessArtifact.specification_id == Specification.id))),
+ Or(Specification.product == None, Product.active == True)]
+ return tables, clauses
+
+
+def get_specification_filters(filter, assume_product_active=False):
+ """Return a list of Storm expressions for filtering Specifications.
+
+ :param filters: A collection of SpecificationFilter and/or strings.
+ Strings are used for text searches.
+ :param assume_product_active: If True, assume the Product is active,
+ instead of ensuring it is active.
+ """
+ from lp.registry.model.product import Product
+ clauses = []
+ # If Product is used, it must be active.
+ if not assume_product_active:
+ clauses.extend([Or(Specification.product == None,
+ Not(Specification.productID.is_in(Select(Product.id,
+ Product.active == False))))])
+ # ALL is the trump card.
+ if SpecificationFilter.ALL in filter:
+ return clauses
+ # Look for informational specs.
+ if SpecificationFilter.INFORMATIONAL in filter:
+ clauses.append(Specification.implementation_status ==
+ SpecificationImplementationStatus.INFORMATIONAL)
+ # Filter based on completion. See the implementation of
+ # Specification.is_complete() for more details.
+ if SpecificationFilter.COMPLETE in filter:
+ clauses.append(Specification.storm_completeness())
+ if SpecificationFilter.INCOMPLETE in filter:
+ clauses.append(Not(Specification.storm_completeness()))
+
+ # Filter for validity. If we want valid specs only, then we should exclude
+ # all OBSOLETE or SUPERSEDED specs.
+ if SpecificationFilter.VALID in filter:
+ clauses.append(Not(Specification.definition_status.is_in([
+ SpecificationDefinitionStatus.OBSOLETE,
+ SpecificationDefinitionStatus.SUPERSEDED,
+ ])))
+ # Filter for specification text.
+ for constraint in filter:
+ if isinstance(constraint, basestring):
+ # A string in the filter is a text search filter.
+ clauses.append(fti_search(Specification, constraint))
+ return clauses
+
+
+# NB NB If you change this definition, please update the equivalent
+# DB constraint Specification.specification_start_recorded_chk
+# We choose to define "started" as the set of delivery states NOT
+# in the values we select. Another option would be to say "anything less
+# than a threshold" and to comment the dbschema that "anything not
+# started should be less than the threshold". We'll see how maintainable
+# this is.
+spec_started_clause = Or(Not(Specification.implementation_status.is_in([
+ SpecificationImplementationStatus.UNKNOWN,
+ SpecificationImplementationStatus.NOTSTARTED,
+ SpecificationImplementationStatus.DEFERRED,
+ SpecificationImplementationStatus.INFORMATIONAL,
+ ])),
+ And(Specification.implementation_status ==
+ SpecificationImplementationStatus.INFORMATIONAL,
+ Specification.definition_status ==
+ SpecificationDefinitionStatus.APPROVED
+ ))
+>>>>>>> MERGE-SOURCE
=== modified file 'lib/lp/blueprints/model/sprint.py'
--- lib/lp/blueprints/model/sprint.py 2012-11-15 21:20:16 +0000
+++ lib/lp/blueprints/model/sprint.py 2012-11-30 10:13:01 +0000
@@ -155,8 +155,14 @@
if len(statuses) > 0:
query.append(Or(*statuses))
# Filter for specification text
+<<<<<<< TREE
query.extend(get_specification_filters(filter))
return tables, query
+=======
+ query.extend(
+ get_specification_filters(filter, assume_product_active=True))
+ return tables, query
+>>>>>>> MERGE-SOURCE
def all_specifications(self, user):
return self.specifications(user, filter=[SpecificationFilter.ALL])
=== modified file 'lib/lp/blueprints/tests/test_specification.py'
=== modified file 'lib/lp/blueprints/tests/test_webservice.py'
=== modified file 'lib/lp/bugs/browser/bugalsoaffects.py'
=== modified file 'lib/lp/bugs/browser/bugtarget.py'
=== modified file 'lib/lp/bugs/browser/bugtask.py'
=== modified file 'lib/lp/bugs/browser/bugwatch.py'
=== modified file 'lib/lp/bugs/browser/tests/test_bugtarget_filebug.py'
=== modified file 'lib/lp/bugs/tests/test_bug_notification_recipients.py'
--- lib/lp/bugs/tests/test_bug_notification_recipients.py 2012-11-26 08:33:03 +0000
+++ lib/lp/bugs/tests/test_bug_notification_recipients.py 2012-11-30 10:13:01 +0000
@@ -10,6 +10,10 @@
GreaterThan,
)
from zope.component import getUtility
+from testtools.matchers import (
+ Equals,
+ GreaterThan,
+ )
from lp.app.enums import InformationType
from lp.bugs.enums import BugNotificationLevel
=== modified file 'lib/lp/bugs/tests/test_bugnomination.py'
--- lib/lp/bugs/tests/test_bugnomination.py 2012-11-26 08:33:03 +0000
+++ lib/lp/bugs/tests/test_bugnomination.py 2012-11-30 10:13:01 +0000
@@ -5,15 +5,27 @@
__metaclass__ = type
-from zope.component import getUtility
-
-from lp.app.errors import NotFoundError
-from lp.bugs.interfaces.bugnomination import (
- BugNominationStatus,
- BugNominationStatusError,
- IBugNomination,
- IBugNominationSet,
- )
+<<<<<<< TREE
+from zope.component import getUtility
+
+from lp.app.errors import NotFoundError
+from lp.bugs.interfaces.bugnomination import (
+ BugNominationStatus,
+ BugNominationStatusError,
+ IBugNomination,
+ IBugNominationSet,
+ )
+=======
+from zope.component import getUtility
+
+from lp.app.errors import NotFoundError
+from lp.bugs.interfaces.bugnomination import (
+ BugNominationStatusError,
+ BugNominationStatus,
+ IBugNomination,
+ IBugNominationSet,
+ )
+>>>>>>> MERGE-SOURCE
from lp.soyuz.interfaces.publishing import PackagePublishingStatus
from lp.testing import (
celebrity_logged_in,
=== modified file 'lib/lp/bugs/vocabularies.py'
--- lib/lp/bugs/vocabularies.py 2012-11-26 08:33:03 +0000
+++ lib/lp/bugs/vocabularies.py 2012-11-30 10:13:01 +0000
@@ -32,6 +32,7 @@
)
from zope.component import getUtility
from zope.interface import implements
+from zope.security.proxy import removeSecurityProxy
from zope.schema.interfaces import (
IVocabulary,
IVocabularyTokenized,
=== modified file 'lib/lp/code/browser/codeimport.py'
=== modified file 'lib/lp/code/browser/tests/test_branch.py'
=== modified file 'lib/lp/code/browser/tests/test_branchlisting.py'
=== modified file 'lib/lp/code/xmlrpc/codehosting.py'
--- lib/lp/code/xmlrpc/codehosting.py 2012-11-26 08:33:03 +0000
+++ lib/lp/code/xmlrpc/codehosting.py 2012-11-30 10:13:01 +0000
@@ -65,10 +65,17 @@
IPersonSet,
NoSuchPerson,
)
-from lp.registry.interfaces.product import (
- InvalidProductName,
- NoSuchProduct,
- )
+<<<<<<< TREE
+from lp.registry.interfaces.product import (
+ InvalidProductName,
+ NoSuchProduct,
+ )
+=======
+from lp.registry.interfaces.product import (
+ NoSuchProduct,
+ InvalidProductName,
+ )
+>>>>>>> MERGE-SOURCE
from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
from lp.services.scripts.interfaces.scriptactivity import IScriptActivitySet
from lp.services.webapp import LaunchpadXMLRPCView
=== modified file 'lib/lp/codehosting/codeimport/tests/test_workermonitor.py'
=== modified file 'lib/lp/registry/browser/distroseries.py'
=== modified file 'lib/lp/registry/browser/person.py'
=== modified file 'lib/lp/registry/browser/product.py'
--- lib/lp/registry/browser/product.py 2012-11-27 15:04:39 +0000
+++ lib/lp/registry/browser/product.py 2012-11-30 10:13:01 +0000
@@ -157,6 +157,7 @@
PillarViewMixin,
)
from lp.registry.browser.productseries import get_series_branch_error
+from lp.registry.errors import CannotChangeInformationType
from lp.registry.interfaces.pillar import IPillarNameSet
from lp.registry.interfaces.product import (
IProduct,
@@ -1465,7 +1466,10 @@
@action("Change", name='change')
def change_action(self, action, data):
- self.updateContextFromData(data)
+ try:
+ self.updateContextFromData(data)
+ except CannotChangeInformationType as e:
+ self.setFieldError('information_type', str(e))
class ProductValidationMixin:
=== modified file 'lib/lp/registry/browser/productseries.py'
=== modified file 'lib/lp/registry/browser/sourcepackage.py'
--- lib/lp/registry/browser/sourcepackage.py 2012-11-26 08:40:20 +0000
+++ lib/lp/registry/browser/sourcepackage.py 2012-11-30 10:13:01 +0000
@@ -67,10 +67,14 @@
StepView,
)
from lp.app.browser.tales import CustomizableFormatter
+<<<<<<< TREE
from lp.app.enums import (
InformationType,
ServiceUsage,
)
+=======
+from lp.app.enums import InformationType, ServiceUsage
+>>>>>>> MERGE-SOURCE
from lp.app.widgets.itemswidgets import LaunchpadRadioWidget
from lp.bugs.browser.bugtask import BugTargetTraversalMixin
from lp.registry.browser.product import ProjectAddStepOne
=== modified file 'lib/lp/registry/browser/tests/test_person.py'
--- lib/lp/registry/browser/tests/test_person.py 2012-11-26 08:33:03 +0000
+++ lib/lp/registry/browser/tests/test_person.py 2012-11-30 10:13:01 +0000
@@ -22,6 +22,7 @@
from lp.app.browser.lazrjs import TextAreaEditorWidget
from lp.app.enums import InformationType
from lp.app.errors import NotFoundError
+from lp.app.enums import InformationType
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
from lp.blueprints.enums import SpecificationImplementationStatus
from lp.buildmaster.enums import BuildStatus
@@ -39,6 +40,7 @@
from lp.registry.model.milestone import milestone_sort_key
from lp.scripts.garbo import PopulateLatestPersonSourcePackageReleaseCache
from lp.services.config import config
+from lp.services.features.testing import FeatureFixture
from lp.services.identity.interfaces.account import AccountStatus
from lp.services.identity.interfaces.emailaddress import IEmailAddressSet
from lp.services.log.logger import FakeLogger
@@ -908,6 +910,15 @@
self.assertEqual(self.view.max_results_to_display, count)
+class TestFastPersonRelatedPackagesView(TestPersonRelatedPackagesView):
+ # Re-run TestPersonRelatedPackagesView with feature flag on.
+
+ def setUp(self):
+ super(TestFastPersonRelatedPackagesView, self).setUp()
+ self.useFixture(FeatureFixture({
+ 'registry.fast_related_software.enabled': 'true'}))
+
+
class TestPersonMaintainedPackagesView(TestCaseWithFactory):
"""Test the maintained packages view."""
=== modified file 'lib/lp/registry/browser/tests/test_product.py'
--- lib/lp/registry/browser/tests/test_product.py 2012-11-26 21:01:54 +0000
+++ lib/lp/registry/browser/tests/test_product.py 2012-11-30 10:13:01 +0000
@@ -40,7 +40,9 @@
IProductSet,
License,
)
-from lp.registry.model.product import Product
+from lp.registry.model.product import (
+ Product,
+ )
from lp.services.config import config
from lp.services.database.lpstorm import IStore
from lp.services.features.testing import FeatureFixture
@@ -515,6 +517,7 @@
# the product when the information type is changed.
self.assertIsNotNone(updated_product.commercial_subscription)
+<<<<<<< TREE
def test_change_information_type_proprietary_packaged(self):
# It should be an error to make a Product private if it is packaged.
self.useFixture(FeatureFixture(
@@ -549,6 +552,25 @@
'Some branches are neither proprietary nor embargoed.')
self.assertThat(browser.contents, HTMLContains(tag))
+=======
+ def test_change_information_type_proprietary_packaged(self):
+ # It should be an error to make a Product private if it is packaged.
+ self.useFixture(FeatureFixture(
+ {u'disclosure.private_projects.enabled': u'on'}))
+ product = self.factory.makeProduct()
+ sourcepackage = self.factory.makeSourcePackage()
+ sourcepackage.setPackaging(product.development_focus, product.owner)
+ browser = self.getViewBrowser(product, '+edit', user=product.owner)
+ info_type = browser.getControl(name='field.information_type')
+ info_type.value = ['PROPRIETARY']
+ old_url = browser.url
+ browser.getControl('Change').click()
+ self.assertEqual(old_url, browser.url)
+ tag = Tag('error', 'div', text='Some series are packaged.',
+ attrs={'class': 'message'})
+ self.assertThat(browser.contents, HTMLContains(tag))
+
+>>>>>>> MERGE-SOURCE
def test_change_information_type_public(self):
owner = self.factory.makePerson(name='pting')
product = self.factory.makeProduct(
=== modified file 'lib/lp/registry/browser/tests/test_projectgroup.py'
--- lib/lp/registry/browser/tests/test_projectgroup.py 2012-11-20 20:53:20 +0000
+++ lib/lp/registry/browser/tests/test_projectgroup.py 2012-11-30 10:13:01 +0000
@@ -57,6 +57,7 @@
team_membership_policy_data,
cache.objects['team_membership_policy_data'])
+<<<<<<< TREE
def test_proprietary_product(self):
# Proprietary projects are not listed for people without access to
# them.
@@ -134,6 +135,50 @@
self.assertTrue("This is the public bug" in browser.contents)
self.assertFalse("This is the private bug" in browser.contents)
+=======
+ def test_proprietary_product(self):
+ # Proprietary projects are not listed for people without access to
+ # them.
+ owner = self.factory.makePerson()
+ product = self.factory.makeProduct(
+ information_type=InformationType.PROPRIETARY,
+ project=self.project_group, owner=owner)
+ owner_browser = self.getViewBrowser(self.project_group,
+ user=owner)
+ with person_logged_in(owner):
+ product_name = product.name
+ self.assertIn(product_name, owner_browser.contents)
+ browser = self.getViewBrowser(self.project_group)
+ self.assertNotIn(product_name, browser.contents)
+
+ def test_proprietary_product_milestone(self):
+ # Proprietary projects are not listed for people without access to
+ # them.
+ owner = self.factory.makePerson()
+ public_product = self.factory.makeProduct(
+ information_type=InformationType.PUBLIC,
+ project=self.project_group, owner=owner)
+ public_milestone = self.factory.makeMilestone(product=public_product)
+ product = self.factory.makeProduct(
+ information_type=InformationType.PROPRIETARY,
+ project=self.project_group, owner=owner)
+ milestone = self.factory.makeMilestone(product=product,
+ name=public_milestone.name)
+ (group_milestone,) = self.project_group.milestones
+ self.factory.makeSpecification(milestone=public_milestone)
+ with person_logged_in(owner):
+ self.factory.makeSpecification(milestone=milestone)
+ product_name = product.displayname
+ with person_logged_in(None):
+ owner_browser = self.getViewBrowser(group_milestone, user=owner)
+ browser = self.getViewBrowser(group_milestone)
+
+ self.assertIn(product_name, owner_browser.contents)
+ self.assertIn(public_product.displayname, owner_browser.contents)
+ self.assertNotIn(product_name, browser.contents)
+ self.assertIn(public_product.displayname, browser.contents)
+
+>>>>>>> MERGE-SOURCE
class TestProjectGroupEditView(TestCaseWithFactory):
"""Tests the edit view."""
=== modified file 'lib/lp/registry/configure.zcml'
=== modified file 'lib/lp/registry/doc/person.txt'
=== modified file 'lib/lp/registry/doc/product.txt'
=== modified file 'lib/lp/registry/errors.py'
=== modified file 'lib/lp/registry/interfaces/milestone.py'
--- lib/lp/registry/interfaces/milestone.py 2012-11-26 08:33:03 +0000
+++ lib/lp/registry/interfaces/milestone.py 2012-11-30 10:13:01 +0000
@@ -4,6 +4,7 @@
# pylint: disable-msg=E0211,E0213
"""Milestone interfaces."""
+from lp.bugs.interfaces.bugtasksearch import IBugTaskSearchBase
__metaclass__ = type
=== modified file 'lib/lp/registry/interfaces/person.py'
=== modified file 'lib/lp/registry/interfaces/product.py'
--- lib/lp/registry/interfaces/product.py 2012-11-26 21:01:54 +0000
+++ lib/lp/registry/interfaces/product.py 2012-11-30 10:13:01 +0000
@@ -424,6 +424,7 @@
id = Int(title=_('The Project ID'))
+<<<<<<< TREE
def userCanView(user):
"""True if the given user has access to this product."""
@@ -477,6 +478,51 @@
"who maintains the project information in "
"Launchpad.")))
+=======
+ def userCanView(user):
+ """True if the given user has access to this product."""
+
+
+class IProductLimitedView(IHasLogo, IHasOwner, ILaunchpadUsage):
+ """Attributes that must be visible for person with artifact grants
+ on bugs, branches or specifications for the product.
+ """
+
+ displayname = exported(
+ TextLine(
+ title=_('Display Name'),
+ description=_("""The name of the project as it would appear in a
+ paragraph.""")),
+ exported_as='display_name')
+
+ logo = exported(
+ LogoImageUpload(
+ title=_("Logo"), required=False,
+ default_image_resource='/@@/product-logo',
+ description=_(
+ "An image of exactly 64x64 pixels that will be displayed in "
+ "the heading of all pages related to this project. It should "
+ "be no bigger than 50kb in size.")))
+
+ name = exported(
+ ProductNameField(
+ title=_('Name'),
+ constraint=name_validator,
+ description=_(
+ "At least one lowercase letter or number, followed by "
+ "letters, numbers, dots, hyphens or pluses. "
+ "Keep this name short; it is used in URLs as shown above.")))
+
+ owner = exported(
+ PersonChoice(
+ title=_('Maintainer'),
+ required=True,
+ vocabulary='ValidPillarOwner',
+ description=_("The restricted team, moderated team, or person "
+ "who maintains the project information in "
+ "Launchpad.")))
+
+>>>>>>> MERGE-SOURCE
project = exported(
ReferenceChoice(
title=_('Part of'),
@@ -493,6 +539,7 @@
'and security policy will apply to this project.')),
exported_as='project_group')
+<<<<<<< TREE
title = exported(
Title(
title=_('Title'),
@@ -508,6 +555,23 @@
IOfficialBugTagTargetPublic, IHasOOPSReferences,
IHasRecipes, IHasCodeImports, IServiceUsage):
"""Public IProduct properties."""
+=======
+ title = exported(
+ Title(
+ title=_('Title'),
+ description=_("The project title. Should be just a few words.")))
+
+
+class IProductView(
+ ICanGetMilestonesDirectly, IHasAppointedDriver, IHasBranches,
+ IHasDrivers, IHasExternalBugTracker, IHasIcon,
+ IHasMergeProposals, IHasMilestones, IHasExpirableBugs,
+ IHasMugshot, IHasSprints, IHasTranslationImports,
+ ITranslationPolicy, IKarmaContext, IMakesAnnouncements,
+ IOfficialBugTagTargetPublic, IHasOOPSReferences,
+ ISpecificationTarget, IHasRecipes, IHasCodeImports, IServiceUsage):
+ """Public IProduct properties."""
+>>>>>>> MERGE-SOURCE
registrant = exported(
PublicPersonChoice(
@@ -529,6 +593,14 @@
"than having one project team that does it all."),
required=False, vocabulary='ValidPersonOrTeam'))
+<<<<<<< TREE
+=======
+ drivers = Attribute(
+ "Presents the drivers of this project as a list. A list is "
+ "required because there might be a project driver and also a "
+ "driver appointed in the overarching project group.")
+
+>>>>>>> MERGE-SOURCE
summary = exported(
Summary(
title=_('Summary'),
@@ -618,6 +690,19 @@
"be displayed for all the world to see. It is NOT a wiki "
"so you cannot undo changes."))
+<<<<<<< TREE
+=======
+ icon = exported(
+ IconImageUpload(
+ title=_("Icon"), required=False,
+ default_image_resource='/@@/product',
+ description=_(
+ "A small image of exactly 14x14 pixels and at most 5kb in "
+ "size, that can be used to identify this project. The icon "
+ "will be displayed next to the project name everywhere in "
+ "Launchpad that we refer to the project and link to it.")))
+
+>>>>>>> MERGE-SOURCE
mugshot = exported(
MugshotImageUpload(
title=_("Brand"), required=False,
@@ -920,11 +1005,18 @@
class IProduct(
+<<<<<<< TREE
IBugTarget, IHasBugSupervisor, IHasDrivers, IProductEditRestricted,
IProductModerateRestricted, IProductDriverRestricted, IProductView,
IProductLimitedView, IProductPublic, IQuestionTarget, IRootContext,
ISpecificationTarget, IStructuralSubscriptionTarget, IInformationType,
IPillar):
+=======
+ IBugTarget, IHasBugSupervisor, IProductEditRestricted,
+ IProductModerateRestricted, IProductDriverRestricted, IProductView,
+ IProductLimitedView, IProductPublic, IQuestionTarget, IRootContext,
+ IStructuralSubscriptionTarget, IInformationType, IPillar):
+>>>>>>> MERGE-SOURCE
"""A Product.
The Launchpad Registry describes the open source world as ProjectGroups
@@ -962,6 +1054,13 @@
:return: An iterable of IProduct
"""
+ def get_users_private_products(user):
+ """Get users non-public products.
+
+ :param user: Which user are we searching products for.
+ :return: An iterable of IProduct
+ """
+
def get_all_active(eager_load=True):
"""Get all active products.
=== modified file 'lib/lp/registry/javascript/sharing/tests/test_pillarsharingview.html'
=== modified file 'lib/lp/registry/model/distribution.py'
=== modified file 'lib/lp/registry/model/milestone.py'
--- lib/lp/registry/model/milestone.py 2012-11-26 08:33:03 +0000
+++ lib/lp/registry/model/milestone.py 2012-11-30 10:13:01 +0000
@@ -38,10 +38,17 @@
from zope.interface import implements
from lp.app.errors import NotFoundError
+<<<<<<< TREE
from lp.blueprints.model.specification import (
Specification,
visible_specification_query,
)
+=======
+from lp.blueprints.model.specification import (
+ get_specification_privacy_filter,
+ Specification,
+ )
+>>>>>>> MERGE-SOURCE
from lp.blueprints.model.specificationworkitem import SpecificationWorkItem
from lp.bugs.interfaces.bugsummary import IBugSummaryDimension
from lp.bugs.interfaces.bugtarget import IHasBugs
@@ -158,8 +165,13 @@
origin, clauses = visible_specification_query(user)
origin.extend([
LeftJoin(Person, Specification.assigneeID == Person.id),
+<<<<<<< TREE
])
milestones = self._milestone_ids_expr(user)
+=======
+ ]
+ milestones = self._milestone_ids_expr(user)
+>>>>>>> MERGE-SOURCE
results = store.using(*origin).find(
(Specification, Person),
@@ -175,8 +187,14 @@
SpecificationWorkItem.milestone_id.is_in(
milestones),
SpecificationWorkItem.deleted == False)),
+<<<<<<< TREE
all=True)),
*clauses)
+=======
+ all=True)),
+ get_specification_privacy_filter(user))
+ results.config(distinct=True)
+>>>>>>> MERGE-SOURCE
ordered_results = results.order_by(Desc(Specification.priority),
Specification.definition_status,
Specification.implementation_status,
=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2012-11-28 05:21:33 +0000
+++ lib/lp/registry/model/person.py 2012-11-30 10:13:01 +0000
@@ -130,11 +130,20 @@
SpecificationSort,
)
from lp.blueprints.model.specification import (
- get_specification_filters,
+<<<<<<< TREE
+ get_specification_filters,
+=======
+ get_specification_filters,
+ get_specification_privacy_filter,
+>>>>>>> MERGE-SOURCE
HasSpecificationsMixin,
spec_started_clause,
Specification,
+<<<<<<< TREE
visible_specification_query,
+=======
+ spec_started_clause,
+>>>>>>> MERGE-SOURCE
)
from lp.blueprints.model.specificationworkitem import SpecificationWorkItem
from lp.bugs.interfaces.bugtarget import IBugTarget
@@ -206,10 +215,18 @@
)
from lp.registry.interfaces.personnotification import IPersonNotificationSet
from lp.registry.interfaces.persontransferjob import IPersonMergeJobSource
-from lp.registry.interfaces.product import (
- IProduct,
- IProductSet,
- )
+<<<<<<< TREE
+from lp.registry.interfaces.product import (
+ IProduct,
+ IProductSet,
+ )
+=======
+from lp.registry.interfaces.pillar import IPillarNameSet
+from lp.registry.interfaces.product import (
+ IProduct,
+ IProductSet,
+ )
+>>>>>>> MERGE-SOURCE
from lp.registry.interfaces.projectgroup import IProjectGroup
from lp.registry.interfaces.role import IPersonRoles
from lp.registry.interfaces.ssh import (
@@ -264,6 +281,7 @@
SQLBase,
sqlvalues,
)
+from lp.services.features import getFeatureFlag
from lp.services.helpers import (
ensure_unicode,
shortlist,
@@ -327,6 +345,7 @@
Archive,
validate_ppa,
)
+from lp.soyuz.model.reporting import LatestPersonSourcePackageReleaseCache
from lp.soyuz.model.publishing import SourcePackagePublishingHistory
from lp.soyuz.model.reporting import LatestPersonSourcePackageReleaseCache
from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease
@@ -877,6 +896,7 @@
if SpecificationFilter.APPROVER in filter:
role_clauses.append(Specification.approver == self)
if SpecificationFilter.SUBSCRIBER in filter:
+<<<<<<< TREE
role_clauses.append(
Specification.id.is_in(
Select(SpecificationSubscription.specificationID,
@@ -905,6 +925,34 @@
results.config(distinct=True)
if quantity is not None:
results = results[:quantity]
+=======
+ role_clauses.append(
+ Specification.id.is_in(
+ Select(SpecificationSubscription.specificationID,
+ [SpecificationSubscription.person == self]
+ )))
+ clauses = [Or(*role_clauses), get_specification_privacy_filter(user)]
+ # Defaults for completeness: if nothing is said about completeness
+ # then we want to show INCOMPLETE.
+ if SpecificationFilter.COMPLETE not in filter:
+ if (in_progress and SpecificationFilter.INCOMPLETE not in filter
+ and SpecificationFilter.ALL not in filter):
+ clauses.append(spec_started_clause)
+ filter.add(SpecificationFilter.INCOMPLETE)
+
+ clauses.extend(get_specification_filters(filter))
+ results = Store.of(self).find(Specification, *clauses)
+ # The default sort is priority descending, so only explictly sort for
+ # DATE.
+ if sort == SpecificationSort.DATE:
+ sort = Desc(Specification.datecreated)
+ elif getattr(sort, 'enum', None) is SpecificationSort:
+ sort = None
+ if sort is not None:
+ results = results.order_by(sort)
+ if quantity is not None:
+ results = results[:quantity]
+>>>>>>> MERGE-SOURCE
return results
# XXX: Tom Berger 2008-04-14 bug=191799:
@@ -1036,6 +1084,7 @@
# We want this person's total karma on a given context (that is,
# across all different categories) here; that's why we use a
# "KarmaCache.category IS NULL" clause here.
+<<<<<<< TREE
from lp.registry.model.product import (
Product,
ProductSet,
@@ -1108,6 +1157,77 @@
return query
def getAffiliatedPillars(self, user):
+=======
+ query = """
+ SELECT PillarName.name, KarmaCache.karmavalue
+ FROM KarmaCache
+ JOIN PillarName ON
+ COALESCE(KarmaCache.distribution, -1) =
+ COALESCE(PillarName.distribution, -1)
+ AND
+ COALESCE(KarmaCache.product, -1) =
+ COALESCE(PillarName.product, -1)
+ WHERE person = %(person)s
+ AND KarmaCache.category IS NULL
+ AND KarmaCache.project IS NULL
+ ORDER BY karmavalue DESC, name
+ LIMIT %(limit)s;
+ """ % sqlvalues(person=self, limit=limit)
+ cur = cursor()
+ cur.execute(query)
+ return cur.fetchall()
+
+ def _genAffiliatedProductSql(self, user=None):
+ """Helper to generate the product sql for getAffiliatePillars"""
+ base_query = """
+ SELECT name, 3 as kind, displayname
+ FROM product p
+ WHERE
+ p.active = True
+ AND (
+ p.driver = %(person)s
+ OR p.owner = %(person)s
+ OR p.bug_supervisor = %(person)s
+ )
+ """ % sqlvalues(person=self)
+
+ if user is not None:
+ roles = IPersonRoles(user)
+ if roles.in_admin or roles.in_commercial_admin:
+ return base_query
+
+ # This is the raw sql version of model/product getProductPrivacyFilter
+ granted_products = """
+ SELECT p.id
+ FROM product p,
+ accesspolicygrantflat apflat,
+ teamparticipation part,
+ accesspolicy ap
+ WHERE
+ apflat.grantee = part.team
+ AND part.person = %(user)s
+ AND apflat.policy = ap.id
+ AND ap.product = p.id
+ AND ap.type = p.information_type
+ """ % sqlvalues(user=user)
+
+ # We have to generate the sqlvalues first so that they're properly
+ # setup and escaped. Then we combine the above query which is already
+ # processed.
+ query_values = sqlvalues(information_type=InformationType.PUBLIC)
+ query_values.update(granted_sql=granted_products)
+
+ query = base_query + """
+ AND (
+ p.information_type = %(information_type)s
+ OR p.information_type is NULL
+ OR p.id IN (%(granted_sql)s)
+ )
+ """ % query_values
+ return query
+
+ def getAffiliatedPillars(self, user):
+>>>>>>> MERGE-SOURCE
"""See `IPerson`."""
find_spec = (PillarName, SQL('kind'), SQL('displayname'))
base = """PillarName
@@ -1495,7 +1615,12 @@
Specification.milestoneID) == Milestone.id),
])
today = datetime.today().date()
+<<<<<<< TREE
query.extend([
+=======
+ query = And(
+ get_specification_privacy_filter(user),
+>>>>>>> MERGE-SOURCE
Milestone.dateexpected <= date, Milestone.dateexpected >= today,
WorkItem.deleted == False,
OR(WorkItem.assignee_id.is_in(self.participant_ids),
@@ -2834,6 +2959,7 @@
def getLatestUploadedPPAPackages(self):
"""See `IPerson`."""
+<<<<<<< TREE
return self._latestReleasesQuery(uploader_only=True, ppa_only=True)
def _releasesQueryFilter(self, uploader_only=False, ppa_only=False):
@@ -2978,6 +3104,246 @@
bulk.load_related(Archive, rows, ['archiveID'])
return DecoratedResultSet(rs, pre_iter_hook=load_related_objects)
+=======
+ return self._latestReleasesQuery(uploader_only=True, ppa_only=True)
+
+ def _releasesQueryFilter(self, uploader_only=False, ppa_only=False):
+ """Return the filter used to find latest published source package
+ releases (SPRs) related to this person.
+
+ :param uploader_only: controls if we are interested in SPRs where
+ the person in question is only the uploader (creator) and not the
+ maintainer (debian-syncs) if the `ppa_only` parameter is also
+ False, or, if the flag is False, it returns all SPR maintained
+ by this person.
+
+ :param ppa_only: controls if we are interested only in source
+ package releases targeted to any PPAs or, if False, sources
+ targeted to primary archives.
+
+ Active 'ppa_only' flag is usually associated with active
+ 'uploader_only' because there shouldn't be any sense of maintainership
+ for packages uploaded to PPAs by someone else than the user himself.
+ """
+ clauses = []
+ if uploader_only:
+ clauses.append(
+ LatestPersonSourcePackageReleaseCache.creator_id == self.id)
+ if ppa_only:
+ # Source maintainer is irrelevant for PPA uploads.
+ pass
+ elif uploader_only:
+ lpspr = ClassAlias(LatestPersonSourcePackageReleaseCache, 'lpspr')
+ clauses.append(Not(Exists(Select(1,
+ where=And(
+ lpspr.sourcepackagename_id ==
+ LatestPersonSourcePackageReleaseCache.sourcepackagename_id,
+ lpspr.upload_archive_id ==
+ LatestPersonSourcePackageReleaseCache.upload_archive_id,
+ lpspr.upload_distroseries_id ==
+ LatestPersonSourcePackageReleaseCache.upload_distroseries_id,
+ lpspr.archive_purpose != ArchivePurpose.PPA,
+ lpspr.maintainer_id == self.id),
+ tables=lpspr))))
+ else:
+ clauses.append(
+ LatestPersonSourcePackageReleaseCache.maintainer_id == self.id)
+ if ppa_only:
+ clauses.append(
+ LatestPersonSourcePackageReleaseCache.archive_purpose ==
+ ArchivePurpose.PPA)
+ else:
+ clauses.append(
+ LatestPersonSourcePackageReleaseCache.archive_purpose !=
+ ArchivePurpose.PPA)
+ return clauses
+
+ def _hasReleasesQuery(self, uploader_only=False, ppa_only=False):
+ """Are there sourcepackagereleases (SPRs) related to this person.
+ See `_releasesQueryFilter` for details on the criteria used.
+ """
+ if not getFeatureFlag('registry.fast_related_software.enabled'):
+ return self._legacy_hasReleasesQuery(uploader_only, ppa_only)
+
+ clauses = self._releasesQueryFilter(uploader_only, ppa_only)
+ rs = Store.of(self).using(LatestPersonSourcePackageReleaseCache).find(
+ LatestPersonSourcePackageReleaseCache.publication_id, *clauses)
+ return not rs.is_empty()
+
+ def _latestReleasesQuery(self, uploader_only=False, ppa_only=False):
+ """Return the sourcepackagereleases records related to this person.
+ See `_releasesQueryFilter` for details on the criteria used."""
+
+ if not getFeatureFlag('registry.fast_related_software.enabled'):
+ return self._legacy_latestReleasesQuery(uploader_only, ppa_only)
+
+ clauses = self._releasesQueryFilter(uploader_only, ppa_only)
+ rs = Store.of(self).find(
+ LatestPersonSourcePackageReleaseCache, *clauses).order_by(
+ Desc(LatestPersonSourcePackageReleaseCache.dateuploaded))
+
+ def load_related_objects(rows):
+ if rows and rows[0].maintainer_id:
+ list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
+ set(map(attrgetter("maintainer_id"), rows))))
+ bulk.load_related(
+ SourcePackageName, rows, ['sourcepackagename_id'])
+ bulk.load_related(
+ SourcePackageRelease, rows, ['sourcepackagerelease_id'])
+ bulk.load_related(Archive, rows, ['upload_archive_id'])
+
+ return DecoratedResultSet(rs, pre_iter_hook=load_related_objects)
+
+ def _legacy_releasesQueryFilter(self, uploader_only=False, ppa_only=False):
+ """Return the filter used to find sourcepackagereleases (SPRs)
+ related to this person.
+
+ :param uploader_only: controls if we are interested in SPRs where
+ the person in question is only the uploader (creator) and not the
+ maintainer (debian-syncs) if the `ppa_only` parameter is also
+ False, or, if the flag is False, it returns all SPR maintained
+ by this person.
+
+ :param ppa_only: controls if we are interested only in source
+ package releases targeted to any PPAs or, if False, sources
+ targeted to primary archives.
+
+ Active 'ppa_only' flag is usually associated with active
+ 'uploader_only' because there shouldn't be any sense of maintainership
+ for packages uploaded to PPAs by someone else than the user himself.
+ """
+ clauses = [SourcePackageRelease.upload_archive == Archive.id]
+
+ if uploader_only:
+ clauses.append(SourcePackageRelease.creator == self)
+
+ if ppa_only:
+ # Source maintainer is irrelevant for PPA uploads.
+ pass
+ elif uploader_only:
+ clauses.append(SourcePackageRelease.maintainer != self)
+ else:
+ clauses.append(SourcePackageRelease.maintainer == self)
+
+ if ppa_only:
+ clauses.append(Archive.purpose == ArchivePurpose.PPA)
+ else:
+ clauses.append(Archive.purpose != ArchivePurpose.PPA)
+
+ return clauses
+
+ def _legacy_hasReleasesQuery(self, uploader_only=False, ppa_only=False):
+ """Are there sourcepackagereleases (SPRs) related to this person.
+ See `_legacy_releasesQueryFilter` for details on the criteria used.
+ """
+ clauses = self._legacy_releasesQueryFilter(uploader_only, ppa_only)
+ spph = ClassAlias(SourcePackagePublishingHistory, "spph")
+ tables = (
+ SourcePackageRelease,
+ Join(
+ spph, spph.sourcepackagereleaseID == SourcePackageRelease.id),
+ Join(Archive, Archive.id == spph.archiveID))
+ rs = Store.of(self).using(*tables).find(
+ SourcePackageRelease.id, clauses)
+ return not rs.is_empty()
+
+ def _legacy_latestReleasesQuery(self, uploader_only=False, ppa_only=False):
+ """Return the sourcepackagereleases (SPRs) related to this person.
+ See `_legacy_releasesQueryFilter` for details on the criteria used."""
+ clauses = self._legacy_releasesQueryFilter(uploader_only, ppa_only)
+ spph = ClassAlias(SourcePackagePublishingHistory, "spph")
+ rs = Store.of(self).find(
+ SourcePackageRelease,
+ SourcePackageRelease.id.is_in(
+ Select(
+ SourcePackageRelease.id,
+ tables=[
+ SourcePackageRelease,
+ Join(
+ spph,
+ spph.sourcepackagereleaseID ==
+ SourcePackageRelease.id),
+ Join(Archive, Archive.id == spph.archiveID)],
+ where=And(*clauses),
+ order_by=[SourcePackageRelease.upload_distroseriesID,
+ SourcePackageRelease.sourcepackagenameID,
+ SourcePackageRelease.upload_archiveID,
+ Desc(SourcePackageRelease.dateuploaded)],
+ distinct=(
+ SourcePackageRelease.upload_distroseriesID,
+ SourcePackageRelease.sourcepackagenameID,
+ SourcePackageRelease.upload_archiveID)))
+ ).order_by(
+ Desc(SourcePackageRelease.dateuploaded), SourcePackageRelease.id)
+
+ def load_related_objects(rows):
+ list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
+ set(map(attrgetter("maintainerID"), rows))))
+ bulk.load_related(SourcePackageName, rows, ['sourcepackagenameID'])
+ bulk.load_related(Archive, rows, ['upload_archiveID'])
+
+ return DecoratedResultSet(rs, pre_iter_hook=load_related_objects)
+
+ def hasSynchronisedPublishings(self):
+ """See `IPerson`."""
+ spph = ClassAlias(SourcePackagePublishingHistory, "spph")
+ ancestor_spph = ClassAlias(
+ SourcePackagePublishingHistory, "ancestor_spph")
+ tables = (
+ SourcePackageRelease,
+ Join(
+ spph,
+ spph.sourcepackagereleaseID ==
+ SourcePackageRelease.id),
+ Join(Archive, Archive.id == spph.archiveID),
+ Join(ancestor_spph, ancestor_spph.id == spph.ancestorID))
+ rs = Store.of(self).using(*tables).find(
+ spph.id,
+ spph.creatorID == self.id,
+ ancestor_spph.archiveID != spph.archiveID,
+ Archive.purpose == ArchivePurpose.PRIMARY)
+ return not rs.is_empty()
+
+ def getLatestSynchronisedPublishings(self):
+ """See `IPerson`."""
+ spph = ClassAlias(SourcePackagePublishingHistory, "spph")
+ ancestor_spph = ClassAlias(
+ SourcePackagePublishingHistory, "ancestor_spph")
+ rs = Store.of(self).find(
+ SourcePackagePublishingHistory,
+ SourcePackagePublishingHistory.id.is_in(
+ Select(
+ spph.id,
+ tables=[
+ SourcePackageRelease,
+ Join(
+ spph, spph.sourcepackagereleaseID ==
+ SourcePackageRelease.id),
+ Join(Archive, Archive.id == spph.archiveID),
+ Join(
+ ancestor_spph,
+ ancestor_spph.id == spph.ancestorID)],
+ where=And(
+ spph.creatorID == self.id,
+ ancestor_spph.archiveID != spph.archiveID,
+ Archive.purpose == ArchivePurpose.PRIMARY),
+ order_by=[spph.distroseriesID,
+ SourcePackageRelease.sourcepackagenameID,
+ Desc(spph.datecreated), Desc(spph.id)],
+ distinct=(
+ spph.distroseriesID,
+ SourcePackageRelease.sourcepackagenameID)
+ ))).order_by(
+ Desc(SourcePackagePublishingHistory.datecreated),
+ Desc(SourcePackagePublishingHistory.id))
+
+ def load_related_objects(rows):
+ bulk.load_related(
+ SourcePackageRelease, rows, ['sourcepackagereleaseID'])
+ bulk.load_related(Archive, rows, ['archiveID'])
+
+ return DecoratedResultSet(rs, pre_iter_hook=load_related_objects)
+>>>>>>> MERGE-SOURCE
def createRecipe(self, name, description, recipe_text, distroseries,
registrant, daily_build_archive=None, build_daily=False):
=== modified file 'lib/lp/registry/model/product.py'
--- lib/lp/registry/model/product.py 2012-11-27 13:43:33 +0000
+++ lib/lp/registry/model/product.py 2012-11-30 10:13:01 +0000
@@ -54,6 +54,10 @@
)
from zope.security.proxy import removeSecurityProxy
+from lp.registry.model.accesspolicy import (
+ AccessPolicy,
+ AccessPolicyGrantFlat,
+ )
from lp.answers.enums import QUESTION_STATUS_DEFAULT_SEARCH
from lp.answers.interfaces.faqtarget import IFAQTarget
from lp.answers.model.faq import (
@@ -89,9 +93,20 @@
)
from lp.app.interfaces.services import IService
from lp.app.model.launchpad import InformationTypeMixin
+<<<<<<< TREE
from lp.blueprints.enums import SpecificationFilter
+=======
+from lp.blueprints.enums import (
+ SpecificationFilter,
+ )
+>>>>>>> MERGE-SOURCE
from lp.blueprints.model.specification import (
- get_specification_filters,
+<<<<<<< TREE
+ get_specification_filters,
+=======
+ get_specification_filters,
+ get_specification_privacy_filter,
+>>>>>>> MERGE-SOURCE
HasSpecificationsMixin,
Specification,
SPECIFICATION_POLICY_ALLOWED_TYPES,
@@ -137,8 +152,12 @@
from lp.registry.errors import (
CannotChangeInformationType,
CommercialSubscribersOnly,
+<<<<<<< TREE
ProprietaryProduct,
VoucherAlreadyRedeemed,
+=======
+ VoucherAlreadyRedeemed,
+>>>>>>> MERGE-SOURCE
)
from lp.registry.interfaces.accesspolicy import (
IAccessPolicyArtifactSource,
@@ -161,11 +180,15 @@
LicenseStatus,
)
from lp.registry.interfaces.productrelease import IProductReleaseSet
+<<<<<<< TREE
from lp.registry.interfaces.role import IPersonRoles
from lp.registry.model.accesspolicy import (
AccessPolicy,
AccessPolicyGrantFlat,
)
+=======
+from lp.registry.interfaces.role import IPersonRoles
+>>>>>>> MERGE-SOURCE
from lp.registry.model.announcement import MakesAnnouncements
from lp.registry.model.commercialsubscription import CommercialSubscription
from lp.registry.model.distribution import Distribution
@@ -183,6 +206,7 @@
from lp.registry.model.productlicense import ProductLicense
from lp.registry.model.productrelease import ProductRelease
from lp.registry.model.productseries import ProductSeries
+from lp.registry.model.teammembership import TeamParticipation
from lp.registry.model.series import ACTIVE_STATUSES
from lp.registry.model.sourcepackagename import SourcePackageName
from lp.registry.model.teammembership import TeamParticipation
@@ -524,6 +548,7 @@
# If you have a commercial subscription, but it's not current, you
# cannot set the information type to a PROPRIETARY type.
+<<<<<<< TREE
if not self.has_current_commercial_subscription:
yield CommercialSubscribersOnly(
'A valid commercial subscription is required for private'
@@ -532,6 +557,20 @@
self.bug_supervisor.membership_policy in INCLUSIVE_TEAM_POLICY):
yield CannotChangeInformationType(
'Bug supervisor has inclusive membership.')
+=======
+ if not self._SO_creating and value in PROPRIETARY_INFORMATION_TYPES:
+ if not self.packagings.is_empty():
+ raise CannotChangeInformationType('Some series are packaged.')
+ # Create the complimentary commercial subscription for the product.
+ self._ensure_complimentary_subscription()
+
+ if not self.has_current_commercial_subscription:
+ raise CommercialSubscribersOnly(
+ 'A valid commercial subscription is required for private'
+ ' Projects.')
+
+ return value
+>>>>>>> MERGE-SOURCE
_information_type = EnumCol(
enum=InformationType, default=InformationType.PUBLIC,
@@ -1458,15 +1497,30 @@
# - completeness.
# - informational.
#
+<<<<<<< TREE
tables, clauses = visible_specification_query(user)
clauses.append(Specification.product == self)
clauses.extend(get_specification_filters(filter))
+=======
+ clauses = [Specification.product == self,
+ get_specification_privacy_filter(user)]
+ clauses.extend(get_specification_filters(filter))
+>>>>>>> MERGE-SOURCE
if prejoin_people:
+<<<<<<< TREE
results = self._preload_specifications_people(tables, clauses)
+=======
+ results = self._preload_specifications_people(clauses)
+>>>>>>> MERGE-SOURCE
else:
+<<<<<<< TREE
tableset = Store.of(self).using(*tables)
results = tableset.find(Specification, *clauses)
results.order_by(order).config(distinct=True)
+=======
+ results = Store.of(self).find(Specification, *clauses)
+ results.order_by(order)
+>>>>>>> MERGE-SOURCE
if quantity is not None:
results = results[:quantity]
return results
@@ -1786,6 +1840,7 @@
def people(self):
return getUtility(IPersonSet)
+<<<<<<< TREE
@classmethod
def latest(cls, user, quantity=5):
"""See `IProductSet`."""
@@ -1824,6 +1879,48 @@
clause = cls.getProductPrivacyFilter(user)
result = IStore(Product).find(Product, Product.active,
clause).order_by(Desc(Product.datecreated))
+=======
+ def latest(self, quantity=5):
+ if quantity is None:
+ return self.all_active
+ else:
+ return self.all_active[:quantity]
+
+ @property
+ def all_active(self):
+ return self.get_all_active(None)
+
+ @staticmethod
+ def getProductPrivacyFilter(user):
+ if user is not None:
+ roles = IPersonRoles(user)
+ if roles.in_admin or roles.in_commercial_admin:
+ return True
+ granted_products = And(
+ AccessPolicyGrantFlat.grantee_id == TeamParticipation.teamID,
+ TeamParticipation.person == user,
+ AccessPolicyGrantFlat.policy == AccessPolicy.id,
+ AccessPolicy.product == Product.id,
+ AccessPolicy.type == Product._information_type)
+ return Or(Product._information_type == InformationType.PUBLIC,
+ Product._information_type == None,
+ Product.id.is_in(Select(Product.id, granted_products)))
+
+ @classmethod
+ def get_users_private_products(cls, user):
+ """List the non-public products the user owns."""
+ result = IStore(Product).find(
+ Product,
+ Product._owner == user,
+ Product._information_type.is_in(PROPRIETARY_INFORMATION_TYPES))
+ return result
+
+ @classmethod
+ def get_all_active(cls, user, eager_load=True):
+ clause = cls.getProductPrivacyFilter(user)
+ result = IStore(Product).find(Product, Product.active,
+ clause).order_by(Desc(Product.datecreated))
+>>>>>>> MERGE-SOURCE
if not eager_load:
return result
=== modified file 'lib/lp/registry/model/projectgroup.py'
--- lib/lp/registry/model/projectgroup.py 2012-11-20 20:52:40 +0000
+++ lib/lp/registry/model/projectgroup.py 2012-11-30 10:13:01 +0000
@@ -433,8 +433,13 @@
privacy_filter = ProductSet.getProductPrivacyFilter(user)
conditions = And(Milestone.product == Product.id,
Product.project == self,
+<<<<<<< TREE
Product.active == True,
privacy_filter)
+=======
+ Product.active == True,
+ ProductSet.getProductPrivacyFilter(user))
+>>>>>>> MERGE-SOURCE
result = store.find(columns, conditions)
result.group_by(Milestone.name)
if only_active:
@@ -444,6 +449,7 @@
result.order_by(
'milestone_sort_key(MIN(Milestone.dateexpected), Milestone.name) '
'DESC')
+<<<<<<< TREE
# An extra query is required here in order to get the correct
# products without affecting the group/order of the query above.
products_by_name = {}
@@ -459,6 +465,22 @@
store.find((Product, Milestone.name), product_conditions)):
if name not in products_by_name.keys():
products_by_name[name] = product
+=======
+ # An extra query is required here in order to get the correct
+ # products without affecting the group/order of the query above.
+ products_by_name = {}
+ if result.any() is not None:
+ milestone_names = [data[0] for data in result]
+ product_conditions = And(
+ Product.project == self,
+ Milestone.product == Product.id,
+ Product.active == True,
+ In(Milestone.name, milestone_names))
+ for product, name in (
+ store.find((Product, Milestone.name), product_conditions)):
+ if name not in products_by_name.keys():
+ products_by_name[name] = product
+>>>>>>> MERGE-SOURCE
return shortlist(
[ProjectMilestone(
self, name, dateexpected, active,
=== modified file 'lib/lp/registry/model/sourcepackage.py'
--- lib/lp/registry/model/sourcepackage.py 2012-11-26 08:33:03 +0000
+++ lib/lp/registry/model/sourcepackage.py 2012-11-30 10:13:01 +0000
@@ -54,10 +54,14 @@
ISourcePackageFactory,
)
from lp.registry.model.hasdrivers import HasDriversMixin
+<<<<<<< TREE
from lp.registry.model.packaging import (
Packaging,
PackagingUtil,
)
+=======
+from lp.registry.model.packaging import Packaging, PackagingUtil
+>>>>>>> MERGE-SOURCE
from lp.registry.model.suitesourcepackage import SuiteSourcePackage
from lp.services.database.lpstorm import IStore
from lp.services.database.sqlbase import (
=== modified file 'lib/lp/registry/scripts/productreleasefinder/finder.py'
--- lib/lp/registry/scripts/productreleasefinder/finder.py 2012-11-26 08:33:03 +0000
+++ lib/lp/registry/scripts/productreleasefinder/finder.py 2012-11-30 10:13:01 +0000
@@ -114,6 +114,7 @@
Returns a list of (product_name, filters) for each product in
the database, where the filter keys are series names.
"""
+<<<<<<< TREE
found_globs = IStore(Product).find(
(Product.name, ProductSeries.name, ProductSeries.releasefileglob),
Product.id == ProductSeries.productID,
@@ -132,6 +133,33 @@
filter_pattern = FilterPattern(series_name, glob)
products_with_filters[product_name].append(filter_pattern)
return products_with_filters.items()
+=======
+ todo = []
+
+ self.ztm.begin()
+ products = getUtility(IProductSet)
+ for product in products.get_all_active(None, eager_load=False):
+ filters = []
+
+ for series in product.series:
+ if (series.status == SeriesStatus.OBSOLETE
+ or not series.releasefileglob):
+ continue
+
+ filters.append(FilterPattern(series.name,
+ series.releasefileglob))
+
+ if not len(filters):
+ continue
+
+ self.log.info("%s has %d series with information", product.name,
+ len(filters))
+
+ todo.append((product.name, filters))
+ self.ztm.abort()
+
+ return todo
+>>>>>>> MERGE-SOURCE
def handleProduct(self, product_name, filters):
"""Scan for tarballs and create ProductReleases for the given product.
=== modified file 'lib/lp/registry/tests/test_adapters.py'
--- lib/lp/registry/tests/test_adapters.py 2012-11-26 08:33:03 +0000
+++ lib/lp/registry/tests/test_adapters.py 2012-11-30 10:13:01 +0000
@@ -11,6 +11,7 @@
package_to_sourcepackagename,
productseries_to_product,
sourcepackage_to_distribution,
+ information_type_from_product,
)
from lp.registry.interfaces.distribution import IDistribution
from lp.registry.interfaces.product import IProduct
=== modified file 'lib/lp/registry/tests/test_milestone.py'
--- lib/lp/registry/tests/test_milestone.py 2012-11-26 08:33:03 +0000
+++ lib/lp/registry/tests/test_milestone.py 2012-11-30 10:13:01 +0000
@@ -6,6 +6,7 @@
__metaclass__ = type
from operator import attrgetter
+from storm.exceptions import NoneError
import unittest
from storm.exceptions import NoneError
@@ -474,6 +475,7 @@
spec, target_milestone = self._create_milestones_on_target(
product=product)
milestone = projectgroup.getMilestone(name=target_milestone.name)
+<<<<<<< TREE
self.assertContentEqual([spec], milestone.getSpecifications(None))
def makeMixedMilestone(self):
@@ -519,6 +521,70 @@
specification, list(milestone.getSpecifications(owner)))
self.assertNotIn(
specification, list(milestone.getSpecifications(None)))
+=======
+ self.assertContentEqual([spec], milestone.getSpecifications(None))
+
+ def makeMixedMilestone(self):
+ projectgroup = self.factory.makeProject()
+ owner = self.factory.makePerson()
+ public_product = self.factory.makeProduct(project=projectgroup)
+ public_milestone = self.factory.makeMilestone(product=public_product)
+ product = self.factory.makeProduct(
+ owner=owner, information_type=InformationType.PROPRIETARY,
+ project=projectgroup, bug_sharing_policy=BugSharingPolicy.PUBLIC)
+ target_milestone = self.factory.makeMilestone(
+ product=product, name=public_milestone.name)
+ milestone = projectgroup.getMilestone(name=public_milestone.name)
+ return milestone, target_milestone, owner
+
+ def test_getSpecifications_milestone_privacy(self):
+ # Ensure getSpecifications respects milestone privacy.
+ # This looks wrong, because the specification is actually public, and
+ # we don't normally hide specifications based on the visibility of
+ # their products. But we're not trying to hide the specification.
+ # We're hiding the fact that this specification is associated with
+ # a proprietary Product milestone. We create a proprietary product
+ # because that's the only way to get a proprietary milestone.
+ milestone, target_milestone, owner = self.makeMixedMilestone()
+ with person_logged_in(owner):
+ spec = self.factory.makeSpecification(milestone=target_milestone)
+ self.assertContentEqual([],
+ milestone.getSpecifications(None))
+ self.assertContentEqual([spec],
+ milestone.getSpecifications(owner))
+
+ def test_getSpecifications_specification_privacy(self):
+ # Only specifications visible to the specified user are listed.
+ owner = self.factory.makePerson()
+ enum = SpecificationSharingPolicy
+ product = self.factory.makeProduct(
+ owner=owner, specification_sharing_policy=enum.PROPRIETARY)
+ milestone = self.factory.makeMilestone(product=product)
+ specification = self.factory.makeSpecification(
+ information_type=InformationType.PROPRIETARY,
+ milestone=milestone)
+ self.assertIn(
+ specification, list(milestone.getSpecifications(owner)))
+ self.assertNotIn(
+ specification, list(milestone.getSpecifications(None)))
+
+ def test_bugtasks_milestone_privacy(self):
+ # Ensure bugtasks respects milestone privacy.
+ # This looks wrong, because the bugtask is actually public, and we
+ # don't normally hide bugtasks based on the visibility of their
+ # products. But we're not trying to hide the bugtask. We're hiding
+ # the fact that this bugtask is associated with a proprietary Product
+ # milestone. We create a proprietary product because that's the only
+ # way to get a proprietary milestone.
+ milestone, target_milestone, owner = self.makeMixedMilestone()
+ with person_logged_in(owner):
+ bugtask = self.factory.makeBugTask(
+ target=target_milestone.product)
+ with person_logged_in(bugtask.owner):
+ bugtask.transitionToMilestone(target_milestone, owner)
+ self.assertContentEqual([], milestone.bugtasks(None))
+ self.assertContentEqual([bugtask], milestone.bugtasks(owner))
+>>>>>>> MERGE-SOURCE
def test_milestones_with_deleted_workitems(self):
# Deleted work items do not cause the specification to show up
=== modified file 'lib/lp/registry/tests/test_milestone_vocabularies.py'
--- lib/lp/registry/tests/test_milestone_vocabularies.py 2012-11-16 14:07:39 +0000
+++ lib/lp/registry/tests/test_milestone_vocabularies.py 2012-11-30 10:13:01 +0000
@@ -23,9 +23,14 @@
from lp.testing.layers import DatabaseFunctionalLayer
+<<<<<<< TREE
class TestMilestoneVocabulary(TestCaseWithFactory):
"""Test that the MilestoneVocabulary behaves as expected."""
+=======
+class TestMilestoneVocabulary(TestCase):
+ """Test that the MilestoneVocabulary behaves as expected."""
+>>>>>>> MERGE-SOURCE
layer = DatabaseFunctionalLayer
def setUp(self):
=== modified file 'lib/lp/registry/tests/test_person.py'
--- lib/lp/registry/tests/test_person.py 2012-11-26 08:33:03 +0000
+++ lib/lp/registry/tests/test_person.py 2012-11-30 10:13:01 +0000
@@ -13,6 +13,7 @@
import pytz
from storm.locals import Desc
from storm.store import Store
+from storm.locals import Desc
from testtools.matchers import (
Equals,
LessThan,
@@ -25,14 +26,25 @@
from lp.answers.model.answercontact import AnswerContact
from lp.app.enums import InformationType
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
-from lp.blueprints.enums import (
- NewSpecificationDefinitionStatus,
- SpecificationDefinitionStatus,
- SpecificationFilter,
- SpecificationImplementationStatus,
- SpecificationPriority,
- SpecificationSort,
- )
+<<<<<<< TREE
+from lp.blueprints.enums import (
+ NewSpecificationDefinitionStatus,
+ SpecificationDefinitionStatus,
+ SpecificationFilter,
+ SpecificationImplementationStatus,
+ SpecificationPriority,
+ SpecificationSort,
+ )
+=======
+from lp.blueprints.enums import (
+ NewSpecificationDefinitionStatus,
+ SpecificationDefinitionStatus,
+ SpecificationImplementationStatus,
+ SpecificationPriority,
+ SpecificationFilter,
+ SpecificationSort,
+ )
+>>>>>>> MERGE-SOURCE
from lp.blueprints.model.specification import Specification
from lp.bugs.interfaces.bugtasksearch import (
get_person_bugtasks_search_params,
@@ -745,26 +757,49 @@
self.bzr = product_set.getByName('bzr')
self.now = datetime.now(pytz.UTC)
- def test_canDeactivateAccount_private_projects(self):
- """A user owning non-public products cannot be deactivated."""
- user = self.factory.makePerson()
- self.factory.makeProduct(
- information_type=InformationType.PUBLIC,
- name="public",
- owner=user)
- self.factory.makeProduct(
- information_type=InformationType.PROPRIETARY,
- name="private",
- owner=user)
-
- login(user.preferredemail.email)
- can_deactivate, errors = user.canDeactivateAccountWithErrors()
-
- self.assertFalse(can_deactivate)
- expected_error = ('This account cannot be deactivated because it owns '
- 'the following non-public products: private')
- self.assertIn(expected_error, errors)
-
+<<<<<<< TREE
+ def test_canDeactivateAccount_private_projects(self):
+ """A user owning non-public products cannot be deactivated."""
+ user = self.factory.makePerson()
+ self.factory.makeProduct(
+ information_type=InformationType.PUBLIC,
+ name="public",
+ owner=user)
+ self.factory.makeProduct(
+ information_type=InformationType.PROPRIETARY,
+ name="private",
+ owner=user)
+
+ login(user.preferredemail.email)
+ can_deactivate, errors = user.canDeactivateAccountWithErrors()
+
+ self.assertFalse(can_deactivate)
+ expected_error = ('This account cannot be deactivated because it owns '
+ 'the following non-public products: private')
+ self.assertIn(expected_error, errors)
+
+=======
+ def test_canDeactivateAccount_private_projects(self):
+ """A user owning non-public products cannot be deactivated."""
+ user = self.factory.makePerson()
+ public_product = self.factory.makeProduct(
+ information_type=InformationType.PUBLIC,
+ name="public",
+ owner=user)
+ public_product = self.factory.makeProduct(
+ information_type=InformationType.PROPRIETARY,
+ name="private",
+ owner=user)
+
+ login(user.preferredemail.email)
+ can_deactivate, errors = user.canDeactivateAccountWithErrors()
+
+ self.assertFalse(can_deactivate)
+ expected_error = ('This account cannot be deactivated because it owns '
+ 'the following non-public products: private')
+ self.assertIn(expected_error, errors)
+
+>>>>>>> MERGE-SOURCE
def test_deactivateAccount_copes_with_names_already_in_use(self):
"""When a user deactivates his account, its name is changed.
=== modified file 'lib/lp/registry/tests/test_prf_finder.py'
--- lib/lp/registry/tests/test_prf_finder.py 2012-11-26 08:33:03 +0000
+++ lib/lp/registry/tests/test_prf_finder.py 2012-11-30 10:13:01 +0000
@@ -60,6 +60,7 @@
('product2', ['filter3', 'filter4']))
+<<<<<<< TREE
class FindReleasesDBTestCase(TestCaseWithFactory):
layer = LaunchpadZopelessLayer
@@ -88,6 +89,19 @@
self.assertEqual(expected, found)
+=======
+class FindReleasesDBTestCase(TestCaseWithFactory):
+
+ layer = LaunchpadZopelessLayer
+
+ def test_findReleases_permissions(self):
+ switch_dbuser(config.productreleasefinder.dbuser)
+ prf = ProductReleaseFinder(self.layer.txn, logging.getLogger())
+ # Test that this raises no exceptions.
+ prf.findReleases()
+
+
+>>>>>>> MERGE-SOURCE
class GetFiltersTestCase(TestCaseWithFactory):
layer = LaunchpadZopelessLayer
=== modified file 'lib/lp/registry/tests/test_product.py'
--- lib/lp/registry/tests/test_product.py 2012-11-26 21:09:20 +0000
+++ lib/lp/registry/tests/test_product.py 2012-11-30 10:13:01 +0000
@@ -42,6 +42,7 @@
IServiceUsage,
)
from lp.app.interfaces.services import IService
+<<<<<<< TREE
from lp.blueprints.enums import (
NewSpecificationDefinitionStatus,
SpecificationDefinitionStatus,
@@ -53,6 +54,17 @@
from lp.blueprints.model.specification import (
SPECIFICATION_POLICY_ALLOWED_TYPES,
)
+=======
+from lp.blueprints.enums import (
+ NewSpecificationDefinitionStatus,
+ SpecificationDefinitionStatus,
+ SpecificationFilter,
+ SpecificationImplementationStatus,
+ SpecificationPriority,
+ SpecificationSort,
+ )
+
+>>>>>>> MERGE-SOURCE
from lp.bugs.interfaces.bugsummary import IBugSummaryDimension
from lp.bugs.interfaces.bugsupervisor import IHasBugSupervisor
from lp.bugs.interfaces.bugtarget import BUG_POLICY_ALLOWED_TYPES
@@ -697,6 +709,7 @@
CannotChangeInformationType, 'Answers is enabled.'):
product.information_type = InformationType.PROPRIETARY
+<<<<<<< TREE
def test_no_proprietary_if_packaging(self):
# information_type cannot be set to proprietary while any
# productseries are packaged.
@@ -1138,6 +1151,450 @@
self.assertIsNot(None, getUtility(IAccessPolicySource).find(
[(product, InformationType.EMBARGOED)]).one())
+=======
+ def test_no_proprietary_if_packaging(self):
+ # information_type cannot be set to proprietary while any
+ # productseries are packaged.
+ product = self.factory.makeProduct(
+ licenses=[License.OTHER_PROPRIETARY])
+ series = self.factory.makeProductSeries(product=product)
+ self.factory.makePackagingLink(productseries=series)
+ with person_logged_in(product.owner):
+ with ExpectedException(
+ CannotChangeInformationType, 'Some series are packaged.'):
+ product.information_type = InformationType.PROPRIETARY
+
+ expected_get_permissions = {
+ CheckerPublic: set((
+ 'active', 'id', 'information_type', 'pillar_category', 'private',
+ 'userCanView',)),
+ 'launchpad.LimitedView': set((
+ 'bugtargetdisplayname', 'displayname', 'enable_bug_expiration',
+ 'logo', 'name', 'official_answers', 'official_anything',
+ 'official_blueprints', 'official_codehosting', 'official_malone',
+ 'owner', 'parent_subscription_target', 'project', 'title', )),
+ 'launchpad.View': set((
+ '_getOfficialTagClause', '_all_specifications',
+ '_valid_specifications', 'active_or_packaged_series',
+ 'aliases', 'all_milestones',
+ 'allowsTranslationEdits', 'allowsTranslationSuggestions',
+ 'announce', 'answer_contacts', 'answers_usage', 'autoupdate',
+ 'blueprints_usage', 'branch_sharing_policy',
+ 'bug_reported_acknowledgement', 'bug_reporting_guidelines',
+ 'bug_sharing_policy', 'bug_subscriptions', 'bug_supervisor',
+ 'bug_tracking_usage', 'bugtargetname',
+ 'bugtracker', 'canUserAlterAnswerContact', 'codehosting_usage',
+ 'coming_sprints', 'commercial_subscription',
+ 'commercial_subscription_is_due', 'createBug',
+ 'createCustomLanguageCode', 'custom_language_codes',
+ 'date_next_suggest_packaging', 'datecreated', 'description',
+ 'development_focus', 'development_focusID',
+ 'direct_answer_contacts', 'distrosourcepackages',
+ 'downloadurl', 'driver', 'drivers',
+ 'enable_bugfiling_duplicate_search', 'findReferencedOOPS',
+ 'findSimilarFAQs', 'findSimilarQuestions', 'freshmeatproject',
+ 'getAllowedBugInformationTypes',
+ 'getAllowedSpecificationInformationTypes', 'getAnnouncement',
+ 'getAnnouncements', 'getAnswerContactsForLanguage',
+ 'getAnswerContactRecipients', 'getBranches',
+ 'getBugSummaryContextWhereClause', 'getBugTaskWeightFunction',
+ 'getCustomLanguageCode', 'getDefaultBugInformationType',
+ 'getDefaultSpecificationInformationType',
+ 'getEffectiveTranslationPermission', 'getExternalBugTracker',
+ 'getFAQ', 'getFirstEntryToImport', 'getLinkedBugWatches',
+ 'getMergeProposals', 'getMilestone', 'getMilestonesAndReleases',
+ 'getQuestion', 'getQuestionLanguages', 'getPackage', 'getRelease',
+ 'getSeries', 'getSpecification', 'getSubscription',
+ 'getSubscriptions', 'getSupportedLanguages', 'getTimeline',
+ 'getTopContributors', 'getTopContributorsGroupedByCategory',
+ 'getTranslationGroups', 'getTranslationImportQueueEntries',
+ 'getTranslators', 'getUsedBugTagsWithOpenCounts',
+ 'getVersionSortedSeries',
+ 'has_current_commercial_subscription',
+ 'has_custom_language_codes', 'has_milestones', 'homepage_content',
+ 'homepageurl', 'icon', 'invitesTranslationEdits',
+ 'invitesTranslationSuggestions',
+ 'license_info', 'license_status', 'licenses', 'milestones',
+ 'mugshot', 'name_with_project', 'newCodeImport',
+ 'obsolete_translatable_series', 'official_bug_tags',
+ 'packagedInDistros', 'packagings',
+ 'past_sprints', 'personHasDriverRights', 'pillar',
+ 'primary_translatable', 'private_bugs',
+ 'programminglang', 'qualifies_for_free_hosting',
+ 'recipes', 'redeemSubscriptionVoucher', 'registrant', 'releases',
+ 'remote_product', 'removeCustomLanguageCode',
+ 'screenshotsurl',
+ 'searchFAQs', 'searchQuestions', 'searchTasks', 'security_contact',
+ 'series',
+ 'sharesTranslationsWithOtherSide', 'sourceforgeproject',
+ 'sourcepackages', 'specification_sharing_policy', 'specifications',
+ 'sprints', 'summary', 'target_type_display',
+ 'translatable_packages', 'translatable_series',
+ 'translation_focus', 'translationgroup', 'translationgroups',
+ 'translationpermission', 'translations_usage', 'ubuntu_packages',
+ 'userCanAlterBugSubscription', 'userCanAlterSubscription',
+ 'userCanEdit', 'userHasBugSubscriptions', 'uses_launchpad',
+ 'wikiurl')),
+ 'launchpad.AnyAllowedPerson': set((
+ 'addAnswerContact', 'addBugSubscription',
+ 'addBugSubscriptionFilter', 'addSubscription',
+ 'createQuestionFromBug', 'newQuestion', 'removeAnswerContact',
+ 'removeBugSubscription')),
+ 'launchpad.Append': set(('newFAQ', )),
+ 'launchpad.Driver': set(('newSeries', )),
+ 'launchpad.Edit': set((
+ 'addOfficialBugTag', 'removeOfficialBugTag',
+ 'setBranchSharingPolicy', 'setBugSharingPolicy',
+ 'setSpecificationSharingPolicy')),
+ 'launchpad.Moderate': set((
+ 'is_permitted', 'license_approved', 'project_reviewed',
+ 'reviewer_whiteboard', 'setAliases')),
+ }
+
+ def test_get_permissions(self):
+ product = self.factory.makeProduct()
+ checker = getChecker(product)
+ self.checkPermissions(
+ self.expected_get_permissions, checker.get_permissions, 'get')
+
+ def test_set_permissions(self):
+ expected_set_permissions = {
+ 'launchpad.BugSupervisor': set((
+ 'bug_reported_acknowledgement', 'bug_reporting_guidelines',
+ 'bugtracker', 'enable_bug_expiration',
+ 'enable_bugfiling_duplicate_search', 'official_bug_tags',
+ 'official_malone', 'remote_product')),
+ 'launchpad.Edit': set((
+ 'answers_usage', 'blueprints_usage', 'bug_supervisor',
+ 'bug_tracking_usage', 'codehosting_usage',
+ 'commercial_subscription', 'description', 'development_focus',
+ 'displayname', 'downloadurl', 'driver', 'freshmeatproject',
+ 'homepage_content', 'homepageurl', 'icon', 'information_type',
+ 'license_info', 'licenses', 'logo', 'mugshot',
+ 'official_answers', 'official_blueprints',
+ 'official_codehosting', 'owner', 'private',
+ 'programminglang', 'project', 'redeemSubscriptionVoucher',
+ 'releaseroot', 'screenshotsurl', 'sourceforgeproject',
+ 'summary', 'title', 'uses_launchpad', 'wikiurl')),
+ 'launchpad.Moderate': set((
+ 'active', 'autoupdate', 'license_approved', 'name',
+ 'project_reviewed', 'registrant', 'reviewer_whiteboard')),
+ 'launchpad.TranslationsAdmin': set((
+ 'translation_focus', 'translationgroup',
+ 'translationpermission', 'translations_usage')),
+ 'launchpad.AnyAllowedPerson': set((
+ 'date_next_suggest_packaging', )),
+ }
+ product = self.factory.makeProduct()
+ checker = getChecker(product)
+ self.checkPermissions(
+ expected_set_permissions, checker.set_permissions, 'set')
+
+ def test_access_launchpad_View_public_product(self):
+ # Everybody, including anonymous users, has access to
+ # properties of public products that require the permission
+ # launchpad.View
+ product = self.factory.makeProduct()
+ names = self.expected_get_permissions['launchpad.View']
+ with person_logged_in(None):
+ for attribute_name in names:
+ getattr(product, attribute_name)
+ ordinary_user = self.factory.makePerson()
+ with person_logged_in(ordinary_user):
+ for attribute_name in names:
+ getattr(product, attribute_name)
+ with person_logged_in(product.owner):
+ for attribute_name in names:
+ getattr(product, attribute_name)
+
+ def test_access_launchpad_View_public_inactive_product(self):
+ # Everybody, including anonymous users, has access to
+ # properties of public but inactvie products that require
+ # the permission launchpad.View.
+ product = self.factory.makeProduct()
+ removeSecurityProxy(product).active = False
+ names = self.expected_get_permissions['launchpad.View']
+ with person_logged_in(None):
+ for attribute_name in names:
+ getattr(product, attribute_name)
+ ordinary_user = self.factory.makePerson()
+ with person_logged_in(ordinary_user):
+ for attribute_name in names:
+ getattr(product, attribute_name)
+ with person_logged_in(product.owner):
+ for attribute_name in names:
+ getattr(product, attribute_name)
+
+ def test_access_launchpad_View_proprietary_product(self):
+ # Only people with grants for a private product can access
+ # attributes protected by the permission launchpad.View.
+ product = self.createProduct(
+ information_type=InformationType.PROPRIETARY,
+ license=License.OTHER_PROPRIETARY)
+ owner = removeSecurityProxy(product).owner
+ names = self.expected_get_permissions['launchpad.View']
+ with person_logged_in(None):
+ for attribute_name in names:
+ self.assertRaises(
+ Unauthorized, getattr, product, attribute_name)
+ ordinary_user = self.factory.makePerson()
+ with person_logged_in(ordinary_user):
+ for attribute_name in names:
+ self.assertRaises(
+ Unauthorized, getattr, product, attribute_name)
+ with person_logged_in(owner):
+ for attribute_name in names:
+ getattr(product, attribute_name)
+ # A user with a policy grant for the product can access attributes
+ # of a private product.
+ with person_logged_in(owner):
+ getUtility(IService, 'sharing').sharePillarInformation(
+ product, ordinary_user, owner,
+ {InformationType.PROPRIETARY: SharingPermission.ALL})
+ with person_logged_in(ordinary_user):
+ for attribute_name in names:
+ getattr(product, attribute_name)
+ # Access can be granted to a team too.
+ other_user = self.factory.makePerson()
+ team = self.factory.makeTeam(members=[other_user])
+ with person_logged_in(owner):
+ getUtility(IService, 'sharing').sharePillarInformation(
+ product, team, owner,
+ {InformationType.PROPRIETARY: SharingPermission.ALL})
+ with person_logged_in(other_user):
+ for attribute_name in names:
+ getattr(product, attribute_name)
+ # Admins can access proprietary products.
+ with celebrity_logged_in('admin'):
+ for attribute_name in names:
+ getattr(product, attribute_name)
+ # Commercial admins have access to all products.
+ with celebrity_logged_in('commercial_admin'):
+ for attribute_name in names:
+ getattr(product, attribute_name)
+
+ def test_admin_launchpad_View_proprietary_product(self):
+ # Admins and commercial admins can view proprietary products.
+ product = self.factory.makeProduct(
+ information_type=InformationType.PROPRIETARY)
+ names = self.expected_get_permissions['launchpad.View']
+ with person_logged_in(self.factory.makeAdministrator()):
+ for attribute_name in names:
+ getattr(product, attribute_name)
+ with person_logged_in(self.factory.makeCommercialAdmin()):
+ for attribute_name in names:
+ getattr(product, attribute_name)
+
+ def test_access_LimitedView_public_product(self):
+ # Everybody can access attributes of public products that
+ # require the permission launchpad.LimitedView.
+ product = self.factory.makeProduct()
+ names = self.expected_get_permissions['launchpad.LimitedView']
+ with person_logged_in(None):
+ for attribute_name in names:
+ getattr(product, attribute_name)
+ ordinary_user = self.factory.makePerson()
+ with person_logged_in(ordinary_user):
+ for attribute_name in names:
+ getattr(product, attribute_name)
+
+ def test_access_LimitedView_proprietary_product(self):
+ # Anonymous users and ordinary logged in users cannot access
+ # attributes of private products that require the permission
+ # launchpad.LimitedView.
+ owner = self.factory.makePerson()
+ product = self.factory.makeProduct(
+ owner=owner,
+ information_type=InformationType.PROPRIETARY)
+ names = self.expected_get_permissions['launchpad.LimitedView']
+ with person_logged_in(None):
+ for attribute_name in names:
+ self.assertRaises(
+ Unauthorized, getattr, product, attribute_name)
+ user = self.factory.makePerson()
+ with person_logged_in(user):
+ for attribute_name in names:
+ self.assertRaises(
+ Unauthorized, getattr, product, attribute_name)
+ # Users with a grant on an artifact related to the product
+ # can access the attributes.
+ with person_logged_in(owner):
+ bug = self.factory.makeBug(
+ target=product, information_type=InformationType.PROPRIETARY)
+ getUtility(IService, 'sharing').ensureAccessGrants(
+ [user], owner, bugs=[bug])
+ with person_logged_in(user):
+ for attribute_name in names:
+ getattr(product, attribute_name)
+ # Users with a policy grant for the product also have access.
+ user2 = self.factory.makePerson()
+ with person_logged_in(owner):
+ getUtility(IService, 'sharing').sharePillarInformation(
+ product, user2, owner,
+ {InformationType.PROPRIETARY: SharingPermission.ALL})
+ with person_logged_in(user2):
+ for attribute_name in names:
+ getattr(product, attribute_name)
+
+ def test_access_launchpad_AnyAllowedPerson_public_product(self):
+ # Only logged in persons have access to properties of public products
+ # that require the permission launchpad.AnyAllowedPerson.
+ product = self.factory.makeProduct()
+ names = self.expected_get_permissions['launchpad.AnyAllowedPerson']
+ with person_logged_in(None):
+ for attribute_name in names:
+ self.assertRaises(
+ Unauthorized, getattr, product, attribute_name)
+ ordinary_user = self.factory.makePerson()
+ with person_logged_in(ordinary_user):
+ for attribute_name in names:
+ getattr(product, attribute_name)
+ with person_logged_in(product.owner):
+ for attribute_name in names:
+ getattr(product, attribute_name)
+
+ def test_access_launchpad_AnyAllowedPerson_proprietary_product(self):
+ # Only people with grants for a private product can access
+ # attributes protected by the permission launchpad.AnyAllowedPerson.
+ product = self.createProduct(
+ information_type=InformationType.PROPRIETARY,
+ license=License.OTHER_PROPRIETARY)
+ owner = removeSecurityProxy(product).owner
+ names = self.expected_get_permissions['launchpad.AnyAllowedPerson']
+ with person_logged_in(None):
+ for attribute_name in names:
+ self.assertRaises(
+ Unauthorized, getattr, product, attribute_name)
+ ordinary_user = self.factory.makePerson()
+ with person_logged_in(ordinary_user):
+ for attribute_name in names:
+ self.assertRaises(
+ Unauthorized, getattr, product, attribute_name)
+ with person_logged_in(owner):
+ for attribute_name in names:
+ getattr(product, attribute_name)
+ # A user with a policy grant for the product can access attributes
+ # of a private product.
+ with person_logged_in(owner):
+ getUtility(IService, 'sharing').sharePillarInformation(
+ product, ordinary_user, owner,
+ {InformationType.PROPRIETARY: SharingPermission.ALL})
+ with person_logged_in(ordinary_user):
+ for attribute_name in names:
+ getattr(product, attribute_name)
+
+ def test_set_launchpad_AnyAllowedPerson_public_product(self):
+ # Only logged in users can set attributes protected by the
+ # permission launchpad.AnyAllowedPerson.
+ product = self.factory.makeProduct()
+ with person_logged_in(None):
+ self.assertRaises(
+ Unauthorized, setattr, product, 'date_next_suggest_packaging',
+ 'foo')
+ ordinary_user = self.factory.makePerson()
+ with person_logged_in(ordinary_user):
+ setattr(product, 'date_next_suggest_packaging', 'foo')
+ with person_logged_in(product.owner):
+ setattr(product, 'date_next_suggest_packaging', 'foo')
+
+ def test_set_launchpad_AnyAllowedPerson_proprietary_product(self):
+ # Only people with grants for a private product can set
+ # attributes protected by the permission launchpad.AnyAllowedPerson.
+ product = self.createProduct(
+ information_type=InformationType.PROPRIETARY,
+ license=License.OTHER_PROPRIETARY)
+ owner = removeSecurityProxy(product).owner
+ with person_logged_in(None):
+ self.assertRaises(
+ Unauthorized, setattr, product, 'date_next_suggest_packaging',
+ 'foo')
+ ordinary_user = self.factory.makePerson()
+ with person_logged_in(ordinary_user):
+ self.assertRaises(
+ Unauthorized, setattr, product, 'date_next_suggest_packaging',
+ 'foo')
+ with person_logged_in(owner):
+ setattr(product, 'date_next_suggest_packaging', 'foo')
+ # A user with a policy grant for the product can access attributes
+ # of a private product.
+ with person_logged_in(owner):
+ getUtility(IService, 'sharing').sharePillarInformation(
+ product, ordinary_user, owner,
+ {InformationType.PROPRIETARY: SharingPermission.ALL})
+ with person_logged_in(ordinary_user):
+ setattr(product, 'date_next_suggest_packaging', 'foo')
+
+ def test_userCanView_caches_known_users(self):
+ # userCanView() maintains a cache of users known to have the
+ # permission to access a product.
+ product = self.createProduct(
+ information_type=InformationType.PROPRIETARY,
+ license=License.OTHER_PROPRIETARY)
+ owner = removeSecurityProxy(product).owner
+ user = self.factory.makePerson()
+ with person_logged_in(owner):
+ getUtility(IService, 'sharing').sharePillarInformation(
+ product, user, owner,
+ {InformationType.PROPRIETARY: SharingPermission.ALL})
+ with person_logged_in(user):
+ with StormStatementRecorder() as recorder:
+ # The first access to a property of the product from
+ # a user requires a DB query.
+ product.homepageurl
+ queries_for_first_user_access = len(recorder.queries)
+ # The second access does not require another query.
+ product.description
+ self.assertEqual(
+ queries_for_first_user_access, len(recorder.queries))
+
+ def test_userCanView_works_with_IPersonRoles(self):
+ # userCanView() maintains a cache of users known to have the
+ # permission to access a product.
+ product = self.createProduct(
+ information_type=InformationType.PROPRIETARY,
+ license=License.OTHER_PROPRIETARY)
+ user = self.factory.makePerson()
+ product.userCanView(user)
+ product.userCanView(IPersonRoles(user))
+
+ def test_userCanView_override(self):
+ # userCanView is overridden by the traversal override.
+ product = self.factory.makeProduct(
+ information_type=InformationType.PROPRIETARY)
+ unprivileged = self.factory.makePerson()
+ with person_logged_in(unprivileged):
+ with FeatureFixture(
+ {'disclosure.private_project.traversal_override': 'on'}):
+ self.assertTrue(product.userCanView(unprivileged))
+ self.assertFalse(product.userCanView(unprivileged))
+
+ def test_anonymous_traversal_override(self):
+ # The traversal override affects the permissions granted to anonymous
+ # users.
+ product = self.factory.makeProduct(
+ information_type=InformationType.PROPRIETARY)
+ with person_logged_in(None):
+ with FeatureFixture(
+ {'disclosure.private_project.traversal_override': 'on'}):
+ self.assertTrue(check_permission('launchpad.View', product))
+ self.assertFalse(check_permission('launchpad.View', product))
+
+ def test_information_type_prevents_pruning(self):
+ # Access policies for Product.information_type are not pruned.
+ owner = self.factory.makePerson()
+ for info_type in [
+ InformationType.PROPRIETARY, InformationType.EMBARGOED]:
+ product = self.factory.makeProduct(
+ information_type=info_type, owner=owner)
+ with person_logged_in(owner):
+ product.setBugSharingPolicy(BugSharingPolicy.PUBLIC)
+ product.setSpecificationSharingPolicy(
+ SpecificationSharingPolicy.PUBLIC)
+ product.setBranchSharingPolicy(BranchSharingPolicy.PUBLIC)
+ self.assertIsNot(None, getUtility(IAccessPolicySource).find(
+ [(product, info_type)]).one())
+
+>>>>>>> MERGE-SOURCE
class TestProductBugInformationTypes(TestCaseWithFactory):
@@ -2108,6 +2565,7 @@
self.failUnlessEqual(
[],
ws_product.findReferencedOOPS(start_date=now - day, end_date=now))
+<<<<<<< TREE
class TestProductSet(TestCaseWithFactory):
@@ -2304,3 +2762,161 @@
self.assertIn(name, [p.name for p in productset.search()])
productset = self.wsObject(ProductSet(), self.factory.makePerson())
self.assertNotIn(name, [p.name for p in productset.search()])
+=======
+
+
+class TestProductSet(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def makeAllInformationTypes(self):
+ proprietary = self.factory.makeProduct(
+ information_type=InformationType.PROPRIETARY)
+ embargoed = self.factory.makeProduct(
+ information_type=InformationType.EMBARGOED)
+ public = self.factory.makeProduct(
+ information_type=InformationType.PUBLIC)
+ return proprietary, embargoed, public
+
+ @staticmethod
+ def filterFind(user):
+ clause = ProductSet.getProductPrivacyFilter(user)
+ return IStore(Product).find(Product, clause)
+
+ def test_users_private_products(self):
+ # Ignore any public products the user may own.
+ owner = self.factory.makePerson()
+ self.factory.makeProduct(
+ information_type=InformationType.PUBLIC,
+ owner=owner)
+ proprietary = self.factory.makeProduct(
+ information_type=InformationType.PROPRIETARY,
+ owner=owner)
+ embargoed = self.factory.makeProduct(
+ information_type=InformationType.EMBARGOED,
+ owner=owner)
+ result = ProductSet.get_users_private_products(owner)
+ self.assertIn(proprietary, result)
+ self.assertIn(embargoed, result)
+
+ def test_get_all_active_omits_proprietary(self):
+ # Ignore proprietary products for anonymous users
+ proprietary = self.factory.makeProduct(
+ information_type=InformationType.PROPRIETARY)
+ embargoed = self.factory.makeProduct(
+ information_type=InformationType.EMBARGOED)
+ result = ProductSet.get_all_active(None)
+ self.assertNotIn(proprietary, result)
+ self.assertNotIn(embargoed, result)
+
+ def test_getProductPrivacyFilterAnonymous(self):
+ # Ignore proprietary products for anonymous users
+ proprietary, embargoed, public = self.makeAllInformationTypes()
+ result = self.filterFind(None)
+ self.assertIn(public, result)
+ self.assertNotIn(embargoed, result)
+ self.assertNotIn(proprietary, result)
+
+ def test_getProductPrivacyFilter_excludes_random_users(self):
+ # Exclude proprietary products for anonymous users
+ random = self.factory.makePerson()
+ proprietary, embargoed, public = self.makeAllInformationTypes()
+ result = self.filterFind(random)
+ self.assertIn(public, result)
+ self.assertNotIn(embargoed, result)
+ self.assertNotIn(proprietary, result)
+
+ def grant(self, pillar, information_type, grantee):
+ policy_source = getUtility(IAccessPolicySource)
+ (policy,) = policy_source.find(
+ [(pillar, information_type)])
+ self.factory.makeAccessPolicyGrant(policy, grantee)
+
+ def test_getProductPrivacyFilter_respects_grants(self):
+ # Include proprietary products for users with right grants.
+ grantee = self.factory.makePerson()
+ proprietary, embargoed, public = self.makeAllInformationTypes()
+ self.grant(embargoed, InformationType.EMBARGOED, grantee)
+ self.grant(proprietary, InformationType.PROPRIETARY, grantee)
+ result = self.filterFind(grantee)
+ self.assertIn(public, result)
+ self.assertIn(embargoed, result)
+ self.assertIn(proprietary, result)
+
+ def test_getProductPrivacyFilter_ignores_wrong_product(self):
+ # Exclude proprietary products if grant is on wrong product.
+ grantee = self.factory.makePerson()
+ proprietary, embargoed, public = self.makeAllInformationTypes()
+ self.factory.makeAccessPolicyGrant(grantee=grantee)
+ result = self.filterFind(grantee)
+ self.assertIn(public, result)
+ self.assertNotIn(embargoed, result)
+ self.assertNotIn(proprietary, result)
+
+ def test_getProductPrivacyFilter_ignores_wrong_info_type(self):
+ # Exclude proprietary products if grant is on wrong information type.
+ grantee = self.factory.makePerson()
+ proprietary, embargoed, public = self.makeAllInformationTypes()
+ self.grant(embargoed, InformationType.PROPRIETARY, grantee)
+ self.factory.makeAccessPolicy(proprietary, InformationType.EMBARGOED)
+ self.grant(proprietary, InformationType.EMBARGOED, grantee)
+ result = self.filterFind(grantee)
+ self.assertIn(public, result)
+ self.assertNotIn(embargoed, result)
+ self.assertNotIn(proprietary, result)
+
+ def test_getProductPrivacyFilter_respects_team_grants(self):
+ # Include proprietary products for users in teams with right grants.
+ grantee = self.factory.makeTeam()
+ proprietary, embargoed, public = self.makeAllInformationTypes()
+ self.grant(embargoed, InformationType.EMBARGOED, grantee)
+ self.grant(proprietary, InformationType.PROPRIETARY, grantee)
+ result = self.filterFind(grantee.teamowner)
+ self.assertIn(public, result)
+ self.assertIn(embargoed, result)
+ self.assertIn(proprietary, result)
+
+ def test_getProductPrivacyFilter_includes_admins(self):
+ # Launchpad admins can see everything.
+ proprietary, embargoed, public = self.makeAllInformationTypes()
+ result = self.filterFind(self.factory.makeAdministrator())
+ self.assertIn(public, result)
+ self.assertIn(embargoed, result)
+ self.assertIn(proprietary, result)
+
+ def test_getProductPrivacyFilter_includes_commercial_admins(self):
+ # Commercial admins can see everything.
+ proprietary, embargoed, public = self.makeAllInformationTypes()
+ result = self.filterFind(self.factory.makeCommercialAdmin())
+ self.assertIn(public, result)
+ self.assertIn(embargoed, result)
+ self.assertIn(proprietary, result)
+
+ def test_getTranslatables_filters_private_products(self):
+ # ProductSet.getTranslatables() returns rivate translatable
+ # products only for user that have grants for these products.
+ owner = self.factory.makePerson()
+ product = self.factory.makeProduct(
+ owner=owner, translations_usage=ServiceUsage.LAUNCHPAD)
+ series = self.factory.makeProductSeries(product)
+ self.factory.makePOTemplate(productseries=series)
+ with person_logged_in(owner):
+ product.information_type = InformationType.PROPRIETARY
+ # Anonymous users do not see private products.
+ with person_logged_in(ANONYMOUS):
+ translatables = getUtility(IProductSet).getTranslatables()
+ self.assertNotIn(product, list(translatables))
+ # Ordinary users do not see private products.
+ user = self.factory.makePerson()
+ with person_logged_in(user):
+ translatables = getUtility(IProductSet).getTranslatables()
+ self.assertNotIn(product, list(translatables))
+ # Users with policy grants on private products see them.
+ with person_logged_in(owner):
+ getUtility(IService, 'sharing').sharePillarInformation(
+ product, user, owner,
+ {InformationType.PROPRIETARY: SharingPermission.ALL})
+ with person_logged_in(user):
+ translatables = getUtility(IProductSet).getTranslatables()
+ self.assertIn(product, list(translatables))
+>>>>>>> MERGE-SOURCE
=== modified file 'lib/lp/registry/tests/test_product_webservice.py'
--- lib/lp/registry/tests/test_product_webservice.py 2012-11-26 08:33:03 +0000
+++ lib/lp/registry/tests/test_product_webservice.py 2012-11-30 10:13:01 +0000
@@ -20,6 +20,7 @@
TestCaseWithFactory,
)
from lp.testing.layers import DatabaseFunctionalLayer
+from lp.testing import person_logged_in
from lp.testing.pages import (
LaunchpadWebServiceCaller,
webservice_for_person,
=== modified file 'lib/lp/registry/tests/test_productseries.py'
--- lib/lp/registry/tests/test_productseries.py 2012-11-27 22:02:34 +0000
+++ lib/lp/registry/tests/test_productseries.py 2012-11-30 10:13:01 +0000
@@ -8,12 +8,20 @@
from storm.exceptions import NoneError
from testtools.testcase import ExpectedException
import transaction
+from zope.security.checker import (
+ CheckerPublic,
+ getChecker,
+ )
from zope.component import getUtility
+<<<<<<< TREE
from zope.security.checker import (
CheckerPublic,
getChecker,
)
from zope.security.interfaces import Unauthorized
+=======
+from zope.security.interfaces import Unauthorized
+>>>>>>> MERGE-SOURCE
from zope.security.proxy import removeSecurityProxy
from lp.app.enums import InformationType
@@ -574,6 +582,7 @@
mode = TranslationsBranchImportMode.IMPORT_TRANSLATIONS
ws_series.translations_autoimport_mode = mode.title
ws_series.lp_save()
+<<<<<<< TREE
class ProductSeriesSecurityAdaperTestCase(TestCaseWithFactory):
@@ -926,3 +935,295 @@
# No exception is raised when attributes of timeline
# are accessed.
getattr(timeline, name)
+=======
+
+
+class ProductSeriesSecurityAdaperTestCase(TestCaseWithFactory):
+ """Test for permissions of IProductSeries."""
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(ProductSeriesSecurityAdaperTestCase, self).setUp()
+ self.public_product = self.factory.makeProduct()
+ self.public_series = self.factory.makeProductSeries(
+ product=self.public_product)
+ self.proprietary_product_owner = self.factory.makePerson()
+ self.proprietary_product = self.factory.makeProduct(
+ owner=self.proprietary_product_owner,
+ information_type=InformationType.PROPRIETARY)
+ self.proprietary_series = self.factory.makeProductSeries(
+ product=self.proprietary_product)
+
+ expected_get_permissions = {
+ CheckerPublic: set((
+ 'id', 'userCanView',
+ )),
+ 'launchpad.AnyAllowedPerson': set((
+ 'addBugSubscription', 'addBugSubscriptionFilter',
+ 'addSubscription', 'removeBugSubscription',
+ )),
+ 'launchpad.Edit': set(('newMilestone', )),
+ 'launchpad.View': set((
+ '_all_specifications', '_getOfficialTagClause',
+ '_valid_specifications', 'active', 'all_milestones',
+ 'answers_usage', 'blueprints_usage', 'branch',
+ 'bug_reported_acknowledgement', 'bug_reporting_guidelines',
+ 'bug_subscriptions', 'bug_supervisor', 'bug_tracking_usage',
+ 'bugtargetdisplayname', 'bugtarget_parent', 'bugtargetname',
+ 'codehosting_usage', 'createBug', 'datecreated', 'displayname',
+ 'driver', 'drivers', 'enable_bugfiling_duplicate_search',
+ 'getAllowedSpecificationInformationTypes',
+ 'getBugSummaryContextWhereClause',
+ 'getBugTaskWeightFunction', 'getCachedReleases',
+ 'getCurrentTemplatesCollection', 'getCurrentTranslationFiles',
+ 'getCurrentTranslationTemplates',
+ 'getDefaultSpecificationInformationType',
+ 'getFirstEntryToImport', 'getLatestRelease', 'getPOTemplate',
+ 'getPackage', 'getPackagingInDistribution', 'getRelease',
+ 'getSharingPartner', 'getSpecification', 'getSubscription',
+ 'getSubscriptions', 'getTemplatesAndLanguageCounts',
+ 'getTemplatesCollection', 'getTimeline',
+ 'getTranslationImportQueueEntries',
+ 'getTranslationTemplateByName', 'getTranslationTemplateFormats',
+ 'getTranslationTemplates', 'getUbuntuTranslationFocusPackage',
+ 'getUsedBugTagsWithOpenCounts',
+ 'has_current_translation_templates', 'has_milestones',
+ 'has_obsolete_translation_templates',
+ 'has_sharing_translation_templates', 'has_translation_files',
+ 'has_translation_templates', 'is_development_focus', 'milestones',
+ 'name', 'official_bug_tags', 'owner', 'packagings', 'parent',
+ 'parent_subscription_target',
+ 'personHasDriverRights', 'pillar', 'potemplate_count', 'product',
+ 'productID', 'productserieslanguages', 'release_files',
+ 'releasefileglob', 'releases', 'releaseverstyle', 'searchTasks',
+ 'series', 'setPackaging', 'sourcepackages', 'specifications',
+ 'status', 'summary', 'target_type_display', 'title',
+ 'translations_autoimport_mode', 'userCanAlterBugSubscription',
+ 'userCanAlterSubscription', 'userHasBugSubscriptions',
+ 'translations_branch', 'translations_usage', 'uses_launchpad',
+ )),
+ }
+
+ def test_get_permissions(self):
+ checker = getChecker(self.public_series)
+ self.checkPermissions(
+ self.expected_get_permissions, checker.get_permissions, 'get')
+
+ expected_set_permissions = {
+ 'launchpad.Edit': set((
+ 'answers_usage', 'blueprints_usage', 'branch',
+ 'bug_tracking_usage', 'codehosting_usage', 'driver', 'name',
+ 'owner', 'product', 'releasefileglob', 'status', 'summary',
+ 'translations_autoimport_mode', 'translations_branch',
+ 'translations_usage', 'uses_launchpad',
+ )),
+ 'launchpad.AnyAllowedPerson': set((
+ 'cvsbranch', 'cvsmodule', 'cvsroot', 'cvstarfileurl',
+ 'importstatus', 'rcstype', 'svnrepository',
+ )),
+ }
+
+ def test_set_permissions(self):
+ checker = getChecker(self.public_series)
+ self.checkPermissions(
+ self.expected_set_permissions, checker.set_permissions, 'set')
+
+ def assertAccessAuthorized(self, attribute_names, obj):
+ # Try to access the given attributes of obj. No exception
+ # should be raised.
+ for name in attribute_names:
+ getattr(obj, name)
+
+ def assertAccessUnauthorized(self, attribute_names, obj):
+ # Try to access the given attributes of obj. Unauthorized
+ # should be raised.
+ for name in attribute_names:
+ self.assertRaises(Unauthorized, getattr, obj, name)
+
+ def assertChangeAuthorized(self, attribute_names, obj):
+ # Try to changes the given attributes of obj. Unauthorized
+ # should not be raised.
+ for name in attribute_names:
+ # Not all attributes declared in configure.zcml to be
+ # settable actually exist or are settable. Attempts to set
+ # them raise an AttributeError. Similary, the naive attempt
+ # to set an attribute to None may raise a NoneError
+ #
+ # Both errors can be ignored here: This method intends only
+ # to prove that Unauthorized is not raised.
+ try:
+ setattr(obj, name, None)
+ except (AttributeError, NoneError):
+ pass
+
+ def assertChangeUnauthorized(self, attribute_names, obj):
+ # Try to changes the given attributes of obj. Unauthorized
+ # should be raised.
+ for name in attribute_names:
+ self.assertRaises(Unauthorized, setattr, obj, name, None)
+
+ def test_access_for_anonymous(self):
+ # Anonymous users have access to public attributes of
+ # a series for a private or public product.
+ with person_logged_in(ANONYMOUS):
+ self.assertAccessAuthorized(
+ self.expected_get_permissions[CheckerPublic],
+ self.public_series)
+ self.assertAccessAuthorized(
+ self.expected_get_permissions[CheckerPublic],
+ self.proprietary_series)
+
+ # They have access to attributes requiring the permission
+ # launchpad.View of a series for a public product...
+ self.assertAccessAuthorized(
+ self.expected_get_permissions['launchpad.View'],
+ self.public_series)
+
+ # ...but not to the same attributes of a series for s private
+ # product.
+ self.assertAccessUnauthorized(
+ self.expected_get_permissions['launchpad.View'],
+ self.proprietary_series)
+
+ # The cannot access other attributes of a series for
+ # public and private products.
+ for permission, names in self.expected_get_permissions.items():
+ if permission in (CheckerPublic, 'launchpad.View'):
+ continue
+ self.assertAccessUnauthorized(names, self.public_series)
+ self.assertAccessUnauthorized(names, self.proprietary_series)
+
+ # They cannot change any attributes.
+ for permission, names in self.expected_set_permissions.items():
+ self.assertChangeUnauthorized(names, self.public_series)
+ self.assertChangeUnauthorized(names, self.proprietary_series)
+
+ def test_access_for_regular_user(self):
+ # Regular users have access to public attributes of
+ # a series for a private or public product.
+ user = self.factory.makePerson()
+ with person_logged_in(user):
+ self.assertAccessAuthorized(
+ self.expected_get_permissions[CheckerPublic],
+ self.public_series)
+ self.assertAccessAuthorized(
+ self.expected_get_permissions[CheckerPublic],
+ self.proprietary_series)
+
+ # They have access to attributes requiring the permissions
+ # launchpad.View and launchpadAnyAllowedPerson of a series
+ # for a public product...
+ self.assertAccessAuthorized(
+ self.expected_get_permissions['launchpad.View'],
+ self.public_series)
+ self.assertAccessAuthorized(
+ self.expected_get_permissions['launchpad.AnyAllowedPerson'],
+ self.public_series)
+
+ # ...but not to the same attributes of a series for s private
+ # product.
+ self.assertAccessUnauthorized(
+ self.expected_get_permissions['launchpad.View'],
+ self.proprietary_series)
+ self.assertAccessUnauthorized(
+ self.expected_get_permissions['launchpad.AnyAllowedPerson'],
+ self.proprietary_series)
+
+ # The cannot access other attributes of a series for
+ # public and private products.
+ for permission, names in self.expected_get_permissions.items():
+ if permission in (CheckerPublic, 'launchpad.View',
+ 'launchpad.AnyAllowedPerson'):
+ continue
+ self.assertAccessUnauthorized(names, self.public_series)
+ self.assertAccessUnauthorized(names, self.proprietary_series)
+
+ # They can change attributes requiring the permission
+ # launchpad.AnyAllowedPerson of a series for a public project...
+ self.assertChangeAuthorized(
+ self.expected_set_permissions['launchpad.AnyAllowedPerson'],
+ self.public_series)
+ #... but not for a private project.
+ self.assertChangeUnauthorized(
+ self.expected_set_permissions['launchpad.AnyAllowedPerson'],
+ self.proprietary_series)
+
+ # They cannot change any attributes that require another
+ # permission than launchpad.AnyALlowedPerson.
+ for permission, names in self.expected_set_permissions.items():
+ if permission == 'launchpad.AnyAllowedPerson':
+ continue
+ self.assertChangeUnauthorized(names, self.public_series)
+ self.assertChangeUnauthorized(names, self.proprietary_series)
+
+ def test_access_for_user_with_policy_grant(self):
+ # Users with a policy grant for the parent product can access
+ # properties requring the permission lanchpad.View or
+ # launchpad.ANyALlowedPerson of a series.
+ user = self.factory.makePerson()
+ with person_logged_in(self.proprietary_product_owner):
+ getUtility(IService, 'sharing').sharePillarInformation(
+ self.proprietary_product, user, self.proprietary_product_owner,
+ {InformationType.PROPRIETARY: SharingPermission.ALL})
+ with person_logged_in(user):
+ self.assertAccessAuthorized(
+ self.expected_get_permissions['launchpad.View'],
+ self.proprietary_series)
+ self.assertAccessAuthorized(
+ self.expected_get_permissions['launchpad.AnyAllowedPerson'],
+ self.proprietary_series)
+
+ # The cannot access other attributes of a series for
+ # private products.
+ for permission, names in self.expected_get_permissions.items():
+ if permission in (CheckerPublic, 'launchpad.View',
+ 'launchpad.AnyAllowedPerson'):
+ continue
+ self.assertAccessUnauthorized(names, self.proprietary_series)
+
+ # They can change attributes requiring the permission
+ # launchpad.AnyAllowedPerson of a series for a provate project...
+ self.assertChangeAuthorized(
+ self.expected_set_permissions['launchpad.AnyAllowedPerson'],
+ self.proprietary_series)
+
+ # They cannot change any attributes that require another
+ # permission than launchpad.AnyALlowedPerson.
+ for permission, names in self.expected_set_permissions.items():
+ if permission == 'launchpad.AnyAllowedPerson':
+ continue
+ self.assertChangeUnauthorized(names, self.proprietary_series)
+
+ def test_access_for_product_owner(self):
+ # The owner of a project has access to all attributes of
+ # a product series.
+ with person_logged_in(self.proprietary_product_owner):
+ for permission, names in self.expected_get_permissions.items():
+ self.assertAccessAuthorized(names, self.proprietary_series)
+
+ # They can change all attributes.
+ for permission, names in self.expected_set_permissions.items():
+ self.assertChangeAuthorized(names, self.proprietary_series)
+
+ with person_logged_in(self.public_product.owner):
+ for permission, names in self.expected_get_permissions.items():
+ self.assertAccessAuthorized(names, self.public_series)
+
+ # They can change all attributes.
+ for permission, names in self.expected_set_permissions.items():
+ self.assertChangeAuthorized(names, self.public_series)
+
+ def test_access_for_lp_admins(self):
+ # Launchpad admins can access and change any attribute of a series
+ # of public and private product.
+ with celebrity_logged_in('admin'):
+ for permission, names in self.expected_get_permissions.items():
+ self.assertAccessAuthorized(names, self.public_series)
+ self.assertAccessAuthorized(names, self.proprietary_series)
+
+ # They can change all attributes.
+ for permission, names in self.expected_set_permissions.items():
+ self.assertChangeAuthorized(names, self.public_series)
+ self.assertChangeAuthorized(names, self.proprietary_series)
+>>>>>>> MERGE-SOURCE
=== modified file 'lib/lp/registry/tests/test_sourcepackage.py'
--- lib/lp/registry/tests/test_sourcepackage.py 2012-11-26 08:33:03 +0000
+++ lib/lp/registry/tests/test_sourcepackage.py 2012-11-30 10:13:01 +0000
@@ -12,6 +12,7 @@
from storm.locals import Store
from testtools.testcase import ExpectedException
import transaction
+from testtools.testcase import ExpectedException
from zope.component import getUtility
from zope.interface.verify import verifyObject
from zope.security.checker import canAccess
=== modified file 'lib/lp/registry/tests/test_subscribers.py'
=== modified file 'lib/lp/registry/vocabularies.py'
=== modified file 'lib/lp/scripts/garbo.py'
--- lib/lp/scripts/garbo.py 2012-11-26 08:33:03 +0000
+++ lib/lp/scripts/garbo.py 2012-11-30 10:13:01 +0000
@@ -19,6 +19,7 @@
import logging
import multiprocessing
import os
+import simplejson
import threading
import time
=== modified file 'lib/lp/scripts/tests/test_garbo.py'
--- lib/lp/scripts/tests/test_garbo.py 2012-11-26 08:33:03 +0000
+++ lib/lp/scripts/tests/test_garbo.py 2012-11-30 10:13:01 +0000
@@ -11,6 +11,7 @@
timedelta,
)
import logging
+import pytz
from StringIO import StringIO
import time
=== modified file 'lib/lp/security.py'
--- lib/lp/security.py 2012-11-27 22:02:34 +0000
+++ lib/lp/security.py 2012-11-30 10:13:01 +0000
@@ -38,6 +38,7 @@
AuthorizationBase,
DelegatedAuthorization,
)
+from lp.app.interfaces.services import IService
from lp.archivepublisher.interfaces.publisherconfig import IPublisherConfig
from lp.blueprints.interfaces.specification import (
ISpecification,
=== modified file 'lib/lp/services/features/flags.py'
--- lib/lp/services/features/flags.py 2012-11-19 05:54:50 +0000
+++ lib/lp/services/features/flags.py 2012-11-30 10:13:01 +0000
@@ -238,6 +238,17 @@
'Traversal to private projects requires special access.',
'Override traveral checks.',
'https://dev.launchpad.net/LEP/PrivateProjects'),
+<<<<<<< TREE
+=======
+ ('registry.fast_related_software.enabled',
+ 'boolean',
+ ('If true, use the new reporting cache to load a person\'s related '
+ 'software information.'),
+ '',
+ '',
+ ''),
+
+>>>>>>> MERGE-SOURCE
])
# The set of all flag names that are documented.
=== modified file 'lib/lp/services/job/runner.py'
--- lib/lp/services/job/runner.py 2012-11-26 08:33:03 +0000
+++ lib/lp/services/job/runner.py 2012-11-30 10:13:01 +0000
@@ -35,6 +35,7 @@
SIGHUP,
signal,
)
+from storm.exceptions import LostObjectError
import sys
from textwrap import dedent
from uuid import uuid4
=== modified file 'lib/lp/services/messaging/rabbit.py'
--- lib/lp/services/messaging/rabbit.py 2012-11-26 08:33:03 +0000
+++ lib/lp/services/messaging/rabbit.py 2012-11-30 10:13:01 +0000
@@ -16,6 +16,7 @@
import json
import socket
import sys
+import socket
import threading
import time
=== modified file 'lib/lp/services/webapp/publisher.py'
=== modified file 'lib/lp/soyuz/browser/archive.py'
=== modified file 'lib/lp/soyuz/configure.zcml'
=== modified file 'lib/lp/soyuz/interfaces/reporting.py'
--- lib/lp/soyuz/interfaces/reporting.py 2012-11-26 08:33:03 +0000
+++ lib/lp/soyuz/interfaces/reporting.py 2012-11-30 10:13:01 +0000
@@ -1,31 +1,64 @@
-# Copyright 2012 Canonical Ltd. This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-__metaclass__ = type
-__all__ = [
- 'ILatestPersonSourcePackageReleaseCache',
- ]
-
-
-from zope.interface import Attribute
-
-from lp.soyuz.interfaces.sourcepackagerelease import ISourcePackageRelease
-
-
-class ILatestPersonSourcePackageReleaseCache(ISourcePackageRelease):
- """Published source package release information for a person.
-
- The records represented by this object are the latest published source
- package releases for a given sourcepackage, distroseries, archive, keyed
- on the package creator and maintainer. The table contains a set of data
- records for package creators and a second set of data for package
- maintainers. Queries can be filtered by creator or maintainer as required.
- """
-
- cache_id = Attribute(
- "The id of the associated LatestPersonSourcePackageReleaseCache"
- "record.")
- sourcepackagerelease = Attribute(
- "The SourcePackageRelease which this object represents.")
- publication = Attribute(
- "The publication record for the associated SourcePackageRelease.")
+<<<<<<< TREE
+# Copyright 2012 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+__all__ = [
+ 'ILatestPersonSourcePackageReleaseCache',
+ ]
+
+
+from zope.interface import Attribute
+
+from lp.soyuz.interfaces.sourcepackagerelease import ISourcePackageRelease
+
+
+class ILatestPersonSourcePackageReleaseCache(ISourcePackageRelease):
+ """Published source package release information for a person.
+
+ The records represented by this object are the latest published source
+ package releases for a given sourcepackage, distroseries, archive, keyed
+ on the package creator and maintainer. The table contains a set of data
+ records for package creators and a second set of data for package
+ maintainers. Queries can be filtered by creator or maintainer as required.
+ """
+
+ cache_id = Attribute(
+ "The id of the associated LatestPersonSourcePackageReleaseCache"
+ "record.")
+ sourcepackagerelease = Attribute(
+ "The SourcePackageRelease which this object represents.")
+ publication = Attribute(
+ "The publication record for the associated SourcePackageRelease.")
+=======
+# Copyright 2012 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+__all__ = [
+ 'ILatestPersonSourcePackageReleaseCache',
+ ]
+
+
+from zope.interface import Attribute
+from lp.soyuz.interfaces.sourcepackagerelease import ISourcePackageRelease
+
+
+class ILatestPersonSourcePackageReleaseCache(ISourcePackageRelease):
+ """Published source package release information for a person.
+
+ The records represented by this object are the latest published source
+ package releases for a given sourcepackage, distroseries, archive, keyed
+ on the package creator and maintainer. The table contains a set of data
+ records for package creators and a second set of data for package
+ maintainers. Queries can be filtered by creator or maintainer as required.
+ """
+
+ cache_id = Attribute(
+ "The id of the associated LatestPersonSourcePackageReleaseCache"
+ "record.")
+ sourcepackagerelease = Attribute(
+ "The SourcePackageRelease which this object represents.")
+ publication = Attribute(
+ "The publication record for the associated SourcePackageRelease.")
+>>>>>>> MERGE-SOURCE
=== modified file 'lib/lp/soyuz/model/queue.py'
=== modified file 'lib/lp/soyuz/scripts/packagecopier.py'
=== modified file 'lib/lp/soyuz/stories/soyuz/xx-person-packages.txt'
--- lib/lp/soyuz/stories/soyuz/xx-person-packages.txt 2012-11-19 07:31:45 +0000
+++ lib/lp/soyuz/stories/soyuz/xx-person-packages.txt 2012-11-30 10:13:01 +0000
@@ -160,6 +160,7 @@
... for row in rows:
... print extract_text(row)
+<<<<<<< TREE
Make a function to update the cached latest person source package release
records.
@@ -187,6 +188,33 @@
... job(chunk_size=100)
... switch_dbuser('launchpad')
+=======
+Make a function to update the cached latest person source package release
+records.
+
+ >>> from lp.scripts.garbo import PopulateLatestPersonSourcePackageReleaseCache
+ >>> from lp.services.database.sqlbase import flush_database_updates
+ >>> from lp.services.log.logger import FakeLogger
+ >>> from lp.testing.dbuser import switch_dbuser
+ >>> from lp.services.database.interfaces import (
+ ... IStoreSelector,
+ ... MAIN_STORE,
+ ... MASTER_FLAVOR,
+ ... )
+
+ >>> def update_cached_records(delete_all=False):
+ ... store = getUtility(IStoreSelector).get(MAIN_STORE, MASTER_FLAVOR)
+ ... if delete_all:
+ ... store.execute(
+ ... "delete from latestpersonsourcepackagereleasecache")
+ ... flush_database_updates()
+ ... switch_dbuser('garbo_frequently')
+ ... job = PopulateLatestPersonSourcePackageReleaseCache(FakeLogger())
+ ... while not job.isDone():
+ ... job(chunk_size=100)
+ ... switch_dbuser('launchpad')
+
+>>>>>>> MERGE-SOURCE
Create some new source packages, source1 and source2, both created by cprov
so that they appear in his +packages page.
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2012-11-22 03:57:45 +0000
+++ lib/lp/testing/factory.py 2012-11-30 10:13:01 +0000
@@ -1025,6 +1025,11 @@
if specification_sharing_policy:
naked_product.setSpecificationSharingPolicy(
specification_sharing_policy)
+<<<<<<< TREE
+=======
+ if information_type is not None:
+ naked_product.information_type = information_type
+>>>>>>> MERGE-SOURCE
return product
def makeProductSeries(self, product=None, name=None, owner=None,
=== modified file 'lib/lp/translations/browser/pofile.py'
=== modified file 'lib/lp/translations/browser/productseries.py'
=== modified file 'lib/lp/translations/browser/sourcepackage.py'
=== modified file 'lib/lp/translations/browser/tests/test_translationmessage_view.py'
--- lib/lp/translations/browser/tests/test_translationmessage_view.py 2012-11-26 08:33:03 +0000
+++ lib/lp/translations/browser/tests/test_translationmessage_view.py 2012-11-30 10:13:01 +0000
@@ -33,6 +33,7 @@
CurrentTranslationMessagePageView,
CurrentTranslationMessageView,
revert_unselected_translations,
+ convert_translationmessage_to_submission,
)
from lp.translations.enums import TranslationPermission
from lp.translations.interfaces.side import ITranslationSideTraitsSet
=== modified file 'lib/lp/translations/browser/translationmessage.py'
=== modified file 'lib/lp/translations/doc/translationsoverview.txt'
--- lib/lp/translations/doc/translationsoverview.txt 2012-11-23 22:10:04 +0000
+++ lib/lp/translations/doc/translationsoverview.txt 2012-11-30 10:13:01 +0000
@@ -177,6 +177,7 @@
Evolution: 20
Ubuntu: 24
+<<<<<<< TREE
Private projects are never included.
>>> from lp.app.enums import InformationType
@@ -187,6 +188,18 @@
Evolution: 20
Ubuntu: 24
+=======
+Private projects are never included.
+
+ >>> from lp.app.enums import InformationType
+ >>> removeSecurityProxy(upstart).information_type = (
+ ... InformationType.PROPRIETARY)
+ >>> display_pillars(overview.getMostTranslatedPillars())
+ alsa-utils: 22
+ Evolution: 20
+ Ubuntu: 24
+
+>>>>>>> MERGE-SOURCE
Zero karma
----------
Follow ups