← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/product-projectgroup into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/product-projectgroup into lp:launchpad.

Commit message:
Rename Product.project to Product.projectgroup.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #523054 in Launchpad itself: "Rename Product.project to Product.projectgroup"
  https://bugs.launchpad.net/launchpad/+bug/523054

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/product-projectgroup/+merge/248020

Final stage of bug 523054: rename Product.project to Product.projectgroup.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/product-projectgroup into lp:launchpad.
=== modified file 'lib/lp/answers/browser/tests/test_questiontarget.py'
--- lib/lp/answers/browser/tests/test_questiontarget.py	2014-01-30 15:04:06 +0000
+++ lib/lp/answers/browser/tests/test_questiontarget.py	2015-01-29 16:37:55 +0000
@@ -143,14 +143,14 @@
         product = self.factory.makeProduct()
         project_group = self.factory.makeProject(owner=product.owner)
         with person_logged_in(product.owner):
-            product.project = project_group
+            product.projectgroup = project_group
         self.assertViewTemplate(project_group, 'unknown-support.pt')
 
     def test_template_projectgroup_answers_usage_launchpad(self):
         product = self.factory.makeProduct()
         project_group = self.factory.makeProject(owner=product.owner)
         with person_logged_in(product.owner):
-            product.project = project_group
+            product.projectgroup = project_group
             product.answers_usage = ServiceUsage.LAUNCHPAD
         self.assertViewTemplate(project_group, 'question-listing.pt')
 

=== modified file 'lib/lp/app/widgets/product.py'
--- lib/lp/app/widgets/product.py	2013-04-10 08:35:47 +0000
+++ lib/lp/app/widgets/product.py	2015-01-29 16:37:55 +0000
@@ -174,16 +174,18 @@
             value="malone", name=self.name, cssClass=self.cssClass)
 
         # Project or somewhere else.
-        project = product.project
-        if project is None or project.bugtracker is None:
-            project_bugtracker_caption = "Somewhere else"
+        projectgroup = product.projectgroup
+        if projectgroup is None or projectgroup.bugtracker is None:
+            projectgroup_bugtracker_caption = "Somewhere else"
         else:
-            project_bugtracker_caption = structured(
+            projectgroup_bugtracker_caption = structured(
                 'In the %s bug tracker (<a href="%s">%s</a>)</label>',
-                project.displayname, canonical_url(project.bugtracker),
-                project.bugtracker.title).escapedtext
-        project_bugtracker_arguments = dict(
-            index=1, text=self._renderLabel(project_bugtracker_caption, 1),
+                projectgroup.displayname,
+                canonical_url(projectgroup.bugtracker),
+                projectgroup.bugtracker.title).escapedtext
+        projectgroup_bugtracker_arguments = dict(
+            index=1,
+            text=self._renderLabel(projectgroup_bugtracker_caption, 1),
             value="project", name=self.name, cssClass=self.cssClass)
 
         # External bug tracker.
@@ -213,7 +215,7 @@
             'launchpad': malone_item_arguments,
             'external_bugtracker': external_bugtracker_arguments,
             'external_email': external_bugtracker_email_arguments,
-            'unknown': project_bugtracker_arguments,
+            'unknown': projectgroup_bugtracker_arguments,
             }
 
         # Figure out the selected choice.
@@ -229,7 +231,7 @@
             else:
                 selected = external_bugtracker_arguments
         else:
-            selected = project_bugtracker_arguments
+            selected = projectgroup_bugtracker_arguments
 
         # Render.
         for name, arguments in all_arguments.items():

=== modified file 'lib/lp/bugs/browser/bugtarget.py'
--- lib/lp/bugs/browser/bugtarget.py	2014-11-29 01:51:36 +0000
+++ lib/lp/bugs/browser/bugtarget.py	2015-01-29 16:37:55 +0000
@@ -793,9 +793,9 @@
             # we don't need to look at
             # context.product.bug_reported_acknowledgement because a
             # product series inherits this property from the product.
-            next_context = context.product.project
+            next_context = context.product.projectgroup
         elif IProduct.providedBy(context):
-            next_context = context.project
+            next_context = context.projectgroup
         elif IDistributionSourcePackage.providedBy(context):
             next_context = context.distribution
         # IDistroseries and ISourcePackage inherit

=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
--- lib/lp/bugs/browser/tests/test_bugtask.py	2015-01-29 14:14:01 +0000
+++ lib/lp/bugs/browser/tests/test_bugtask.py	2015-01-29 16:37:55 +0000
@@ -1679,7 +1679,7 @@
         """Create a new product and add it to the project group."""
         product = self.factory.makeProduct(official_malone=tracks_bugs_in_lp)
         with person_logged_in(product.owner):
-            product.project = self.target
+            product.projectgroup = self.target
 
     def test_empty_project_group(self):
         # An empty project group does not use Launchpad for bugs.

=== modified file 'lib/lp/bugs/model/bugtasksearch.py'
--- lib/lp/bugs/model/bugtasksearch.py	2015-01-29 10:07:53 +0000
+++ lib/lp/bugs/model/bugtasksearch.py	2015-01-29 16:37:55 +0000
@@ -388,7 +388,7 @@
                         Milestone.id,
                         tables=[Milestone, Product],
                         where=And(
-                            Product.project == params.milestone.target,
+                            Product.projectgroup == params.milestone.target,
                             Milestone.productID == Product.id,
                             Milestone.name == params.milestone.name,
                             ProductSet.getProductPrivacyFilter(params.user)))))
@@ -410,7 +410,7 @@
                     Milestone.id,
                     tables=[Milestone, Product, MilestoneTag],
                     where=And(
-                        Product.project == params.milestone_tag.target,
+                        Product.projectgroup == params.milestone_tag.target,
                         Milestone.productID == Product.id,
                         Milestone.id == MilestoneTag.milestone_id,
                         MilestoneTag.tag.is_in(params.milestone_tag.tags)),
@@ -432,7 +432,7 @@
         extra_clauses.append(And(
             BugTaskFlat.product_id == Product.id,
             search_value_to_storm_where_condition(
-                Product.project, params.projectgroup)))
+                Product.projectgroup, params.projectgroup)))
 
     if params.omit_dupes:
         extra_clauses.append(BugTaskFlat.duplicateof == None)
@@ -520,7 +520,8 @@
             # include products, productseries, and project group subscriptions.
             projectgroup_match = True
             if params.projectgroup is not None:
-                projectgroup_match = Product.project == params.projectgroup
+                projectgroup_match = (
+                    Product.projectgroup == params.projectgroup)
             ss_clauses.append(In(
                 BugTaskFlat.product_id,
                 Select(SS.productID, tables=[SS])))
@@ -531,7 +532,7 @@
                 BugTaskFlat.product_id,
                 Select(Product.id, tables=[SS, Product],
                        where=And(
-                           SS.projectgroupID == Product.projectID,
+                           SS.projectgroupID == Product.projectgroupID,
                            projectgroup_match,
                            Product.active))))
         extra_clauses.append(Or(*ss_clauses))

=== modified file 'lib/lp/bugs/model/structuralsubscription.py'
--- lib/lp/bugs/model/structuralsubscription.py	2015-01-29 10:07:53 +0000
+++ lib/lp/bugs/model/structuralsubscription.py	2015-01-29 16:37:55 +0000
@@ -268,13 +268,13 @@
 
     def __init__(self, target):
         self.target = target
-        self.target_parent = target.project
+        self.target_parent = target.projectgroup
         self.target_arguments = {"product": target}
         self.pillar = target
-        if target.project is not None:
+        if target.projectgroup is not None:
             self.join = Or(
                 StructuralSubscription.product == target,
-                StructuralSubscription.projectgroup == target.project)
+                StructuralSubscription.projectgroup == target.projectgroup)
         else:
             self.join = (
                 StructuralSubscription.product == target)

=== modified file 'lib/lp/bugs/model/tests/test_bugtasksearch.py'
--- lib/lp/bugs/model/tests/test_bugtasksearch.py	2015-01-28 15:35:18 +0000
+++ lib/lp/bugs/model/tests/test_bugtasksearch.py	2015-01-29 16:37:55 +0000
@@ -1072,7 +1072,7 @@
         with person_logged_in(owner):
             product = self.factory.makeProduct(owner=owner)
             self.products.append(product)
-            product.project = self.searchtarget
+            product.projectgroup = self.searchtarget
             bugtasks.append(
                 self.factory.makeBugTask(target=product))
             bugtasks[-1].importance = BugTaskImportance.HIGH
@@ -1081,7 +1081,7 @@
 
             product = self.factory.makeProduct(owner=owner)
             self.products.append(product)
-            product.project = self.searchtarget
+            product.projectgroup = self.searchtarget
             bugtasks.append(
                 self.factory.makeBugTask(target=product))
             bugtasks[-1].importance = BugTaskImportance.LOW
@@ -1090,7 +1090,7 @@
 
             product = self.factory.makeProduct(owner=owner)
             self.products.append(product)
-            product.project = self.searchtarget
+            product.projectgroup = self.searchtarget
             bugtasks.append(
                 self.factory.makeBugTask(target=product))
             bugtasks[-1].importance = BugTaskImportance.CRITICAL
@@ -2390,7 +2390,7 @@
         self.factory.makeBug(target=product)
         with person_logged_in(product.owner):
             project_group = self.factory.makeProject(owner=product.owner)
-            product.project = project_group
+            product.projectgroup = project_group
         with person_logged_in(subscriber):
             project_group.addBugSubscription(subscriber, subscriber)
         params = BugTaskSearchParams(

=== modified file 'lib/lp/code/model/branchcollection.py'
--- lib/lp/code/model/branchcollection.py	2015-01-28 16:38:13 +0000
+++ lib/lp/code/model/branchcollection.py	2015-01-29 16:37:55 +0000
@@ -543,7 +543,7 @@
     def inProjectGroup(self, projectgroup):
         """See `IBranchCollection`."""
         return self._filterBy(
-            [Product.project == projectgroup.id],
+            [Product.projectgroup == projectgroup.id],
             table=Product, join=Join(Product, Branch.product == Product.id))
 
     def inDistribution(self, distribution):

=== modified file 'lib/lp/code/model/revision.py'
--- lib/lp/code/model/revision.py	2013-08-13 06:53:11 +0000
+++ lib/lp/code/model/revision.py	2015-01-29 16:37:55 +0000
@@ -521,7 +521,7 @@
             conditions = And(conditions, Branch.product == obj)
         elif IProjectGroup.providedBy(obj):
             origin.append(Join(Product, Branch.product == Product.id))
-            conditions = And(conditions, Product.project == obj)
+            conditions = And(conditions, Product.projectgroup == obj)
         else:
             raise AssertionError(
                 "Not an IProduct or IProjectGroup: %r" % obj)

=== modified file 'lib/lp/code/model/revisioncache.py'
--- lib/lp/code/model/revisioncache.py	2015-01-28 16:38:13 +0000
+++ lib/lp/code/model/revisioncache.py	2015-01-29 16:37:55 +0000
@@ -109,7 +109,7 @@
         """See `IRevisionCollection`."""
         return self._filterBy(
             [RevisionCache.product == Product.id,
-             Product.project == projectgroup])
+             Product.projectgroup == projectgroup])
 
     def inSourcePackage(self, package):
         """See `IRevisionCollection`."""

=== modified file 'lib/lp/code/model/tests/test_branchcollection.py'
--- lib/lp/code/model/tests/test_branchcollection.py	2015-01-28 16:38:13 +0000
+++ lib/lp/code/model/tests/test_branchcollection.py	2015-01-29 16:37:55 +0000
@@ -292,7 +292,7 @@
         self.factory.makeProductBranch()
         self.factory.makeAnyBranch()
         projectgroup = self.factory.makeProject()
-        removeSecurityProxy(branch.product).project = projectgroup
+        removeSecurityProxy(branch.product).projectgroup = projectgroup
         collection = self.all_branches.inProjectGroup(projectgroup)
         self.assertEqual([branch], list(collection.getBranches()))
 

=== modified file 'lib/lp/code/stories/webservice/xx-branch.txt'
--- lib/lp/code/stories/webservice/xx-branch.txt	2015-01-29 14:14:01 +0000
+++ lib/lp/code/stories/webservice/xx-branch.txt	2015-01-29 16:37:55 +0000
@@ -233,7 +233,7 @@
 
     >>> login('admin@xxxxxxxxxxxxx')
     >>> projectgroup = factory.makeProject(name='widgets')
-    >>> fooix.project = projectgroup
+    >>> fooix.projectgroup = projectgroup
     >>> blob = factory.makeProduct(name='blob', projectgroup=projectgroup)
     >>> branch = factory.makeProductBranch(product=blob, name='bar')
     >>> branch.owner.name = 'mary'

=== modified file 'lib/lp/code/stories/webservice/xx-branchmergeproposal.txt'
--- lib/lp/code/stories/webservice/xx-branchmergeproposal.txt	2015-01-29 14:14:01 +0000
+++ lib/lp/code/stories/webservice/xx-branchmergeproposal.txt	2015-01-29 16:37:55 +0000
@@ -412,7 +412,7 @@
 
     >>> login('admin@xxxxxxxxxxxxx')
     >>> projectgroup = factory.makeProject(name='widgets')
-    >>> fooix.project = projectgroup
+    >>> fooix.projectgroup = projectgroup
     >>> blob = factory.makeProduct(name='blob', projectgroup=projectgroup)
     >>> proposal = factory.makeBranchMergeProposal(
     ...     product=blob, set_state=BranchMergeProposalStatus.NEEDS_REVIEW)

=== modified file 'lib/lp/registry/browser/product.py'
--- lib/lp/registry/browser/product.py	2014-11-29 01:33:59 +0000
+++ lib/lp/registry/browser/product.py	2015-01-29 16:37:55 +0000
@@ -996,12 +996,12 @@
 
     @cachedproperty
     def effective_driver(self):
-        """Return the product driver or the project driver."""
+        """Return the product driver or the project group driver."""
         if self.context.driver is not None:
             driver = self.context.driver
-        elif (self.context.project is not None and
-              self.context.project.driver is not None):
-            driver = self.context.project.driver
+        elif (self.context.projectgroup is not None and
+              self.context.projectgroup.driver is not None):
+            driver = self.context.projectgroup.driver
         else:
             driver = None
         return driver
@@ -1319,7 +1319,7 @@
         "title",
         "summary",
         "description",
-        "project",
+        "projectgroup",
         "homepageurl",
         "information_type",
         "sourceforgeproject",
@@ -2064,7 +2064,7 @@
     def create_product(self, data):
         """Create the product from the user data."""
         # Get optional data.
-        project = data.get('project')
+        projectgroup = data.get('projectgroup')
         description = data.get('description')
         disclaim_maintainer = data.get('disclaim_maintainer', False)
         if disclaim_maintainer:
@@ -2086,7 +2086,7 @@
             licenses=data['licenses'],
             license_info=data['license_info'],
             information_type=data.get('information_type'),
-            project=project)
+            projectgroup=projectgroup)
 
     def link_source_package(self, product, data):
         if (data.get('distroseries') is not None

=== modified file 'lib/lp/registry/browser/project.py'
--- lib/lp/registry/browser/project.py	2014-09-08 08:07:06 +0000
+++ lib/lp/registry/browser/project.py	2015-01-29 16:37:55 +0000
@@ -148,11 +148,11 @@
     usedfor = IProjectGroupSet
 
     def traverse(self, name):
-        # Raise a 404 on an invalid project name
-        project = self.context.getByName(name)
-        if project is None:
+        # Raise a 404 on an invalid project group name
+        projectgroup = self.context.getByName(name)
+        if projectgroup is None:
             raise NotFoundError(name)
-        return self.redirectSubTree(canonical_url(project))
+        return self.redirectSubTree(canonical_url(projectgroup))
 
 
 class ProjectSetBreadcrumb(Breadcrumb):
@@ -499,7 +499,7 @@
             licenses=data['licenses'],
             license_info=data['license_info'],
             information_type=data.get('information_type'),
-            project=self.context,
+            projectgroup=self.context,
             )
 
     @property
@@ -591,12 +591,12 @@
     custom_widget('homepageurl', TextWidget, displayWidth=30)
     label = _('Register a project group with Launchpad')
     page_title = label
-    project = None
+    projectgroup = None
 
     @action(_('Add'), name='add')
     def add_action(self, action, data):
         """Create the new Project from the form details."""
-        self.project = getUtility(IProjectGroupSet).new(
+        self.projectgroup = getUtility(IProjectGroupSet).new(
             name=data['name'].lower().strip(),
             displayname=data['displayname'],
             title=data['title'],
@@ -605,12 +605,13 @@
             description=data['description'],
             owner=data['owner'],
             )
-        notify(ObjectCreatedEvent(self.project))
+        notify(ObjectCreatedEvent(self.projectgroup))
 
     @property
     def next_url(self):
-        assert self.project is not None, 'No project has been created'
-        return canonical_url(self.project)
+        assert self.projectgroup is not None, (
+            'No project group has been created')
+        return canonical_url(self.projectgroup)
 
 
 class ProjectBrandingView(BrandingChangeView):
@@ -655,7 +656,8 @@
             data=self.initial_values, ignore_request=False)
 
     def createProductField(self):
-        """Create a Choice field to select one of the project's products."""
+        """Create a Choice field to select one of the project group's
+        products."""
         return form.Fields(
             Choice(
                 __name__='product', vocabulary='ProjectProducts',
@@ -670,8 +672,8 @@
     @property
     def page_title(self):
         """The current page title."""
-        return _('Ask a question about a project in ${project}',
-                 mapping=dict(project=self.context.displayname))
+        return _('Ask a question about a project in ${projectgroup}',
+                 mapping=dict(projectgroup=self.context.displayname))
 
     @property
     def question_target(self):

=== modified file 'lib/lp/registry/browser/sourcepackage.py'
--- lib/lp/registry/browser/sourcepackage.py	2014-12-06 00:16:55 +0000
+++ lib/lp/registry/browser/sourcepackage.py	2015-01-29 16:37:55 +0000
@@ -657,8 +657,8 @@
             return True
         bugtracker = product.bugtracker
         if bugtracker is None:
-            if product.project is not None:
-                bugtracker = product.project.bugtracker
+            if product.projectgroup is not None:
+                bugtracker = product.projectgroup.bugtracker
         if bugtracker is None:
             return False
         return True

=== modified file 'lib/lp/registry/browser/tests/milestone-views.txt'
--- lib/lp/registry/browser/tests/milestone-views.txt	2014-07-07 03:43:30 +0000
+++ lib/lp/registry/browser/tests/milestone-views.txt	2015-01-29 16:37:55 +0000
@@ -306,7 +306,7 @@
 generates CSS that hides the space occupied by the side portlets.
 
     >>> projectgroup = factory.makeProject(name='flock')
-    >>> product.project = projectgroup
+    >>> product.projectgroup = projectgroup
     >>> project_milestone = projectgroup.getMilestone('kakapo')
     >>> view = create_initialized_view(
     ...     project_milestone, '+index', principal=person)

=== modified file 'lib/lp/registry/browser/tests/pillar-views.txt'
--- lib/lp/registry/browser/tests/pillar-views.txt	2012-10-14 23:42:21 +0000
+++ lib/lp/registry/browser/tests/pillar-views.txt	2015-01-29 16:37:55 +0000
@@ -155,7 +155,7 @@
 applications used by their products.
 
     >>> project_group = factory.makeProject(name='box', owner=product.owner)
-    >>> product.project = project_group
+    >>> product.projectgroup = project_group
 
     >>> view = create_view(project_group, '+get-involved')
     >>> print view.blueprints_usage.name

=== modified file 'lib/lp/registry/browser/tests/projectgroup-views.txt'
--- lib/lp/registry/browser/tests/projectgroup-views.txt	2012-10-14 23:42:21 +0000
+++ lib/lp/registry/browser/tests/projectgroup-views.txt	2015-01-29 16:37:55 +0000
@@ -18,7 +18,7 @@
     ...         name = '%s-%s' % (letter, projectgroup.name)
     ...         product = factory.makeProduct(
     ...             name=name, owner=projectgroup.owner)
-    ...         product.project = projectgroup
+    ...         product.projectgroup = projectgroup
 
     >>> owner = projectgroup.owner
     >>> ignored = login_person(owner)

=== modified file 'lib/lp/registry/browser/tests/sourcepackage-views.txt'
--- lib/lp/registry/browser/tests/sourcepackage-views.txt	2013-04-11 01:33:23 +0000
+++ lib/lp/registry/browser/tests/sourcepackage-views.txt	2015-01-29 16:37:55 +0000
@@ -321,18 +321,18 @@
 bug tracker then the property is false.
 
     >>> product.bugtracker = None
-    >>> project = factory.makeProject()
-    >>> print project.bugtracker
+    >>> projectgroup = factory.makeProject()
+    >>> print projectgroup.bugtracker
     None
-    >>> product.project = project
+    >>> product.projectgroup = projectgroup
     >>> print view.has_bugtracker
     False
 
-If the product's project does have a bug tracker then the product
+If the product's project group does have a bug tracker then the product
 inherits it.
 
-    >>> ignored = login_person(project.owner)
-    >>> project.bugtracker = bugtracker
+    >>> ignored = login_person(projectgroup.owner)
+    >>> projectgroup.bugtracker = bugtracker
     >>> print view.has_bugtracker
     True
 

=== modified file 'lib/lp/registry/browser/tests/test_subscription_links.py'
--- lib/lp/registry/browser/tests/test_subscription_links.py	2015-01-29 15:33:33 +0000
+++ lib/lp/registry/browser/tests/test_subscription_links.py	2015-01-29 16:37:55 +0000
@@ -453,7 +453,7 @@
         # parent has a bugtracker (see bug 770287).
         projectgroup = self.factory.makeProject()
         with person_logged_in(self.target.owner):
-            self.target.project = projectgroup
+            self.target.projectgroup = projectgroup
         self.factory.makeProduct(
             projectgroup=projectgroup, official_malone=True)
         self._create_scenario(self.regular_user)

=== modified file 'lib/lp/registry/configure.zcml'
--- lib/lp/registry/configure.zcml	2015-01-06 12:10:14 +0000
+++ lib/lp/registry/configure.zcml	2015-01-29 16:37:55 +0000
@@ -1320,7 +1320,7 @@
                 official_codehosting
                 owner
                 programminglang
-                project
+                projectgroup
                 redeemSubscriptionVoucher
                 releaseroot
                 screenshotsurl

=== modified file 'lib/lp/registry/doc/productseries.txt'
--- lib/lp/registry/doc/productseries.txt	2015-01-29 14:14:01 +0000
+++ lib/lp/registry/doc/productseries.txt	2015-01-29 16:37:55 +0000
@@ -383,7 +383,7 @@
 If there is a project group on the product, we would expect the project
 group owner:
 
-    >>> print series.product.project.name
+    >>> print series.product.projectgroup.name
     testproj
     >>> for d in series.drivers:
     ...     print d.name
@@ -392,14 +392,14 @@
 If there is NO project group on the product, then we expect the product
 owner:
 
-    >>> product.project = None
+    >>> product.projectgroup = None
     >>> for d in series.drivers:
     ...     print d.name
     mark
 
 Now let's put the project group back:
 
-    >>> product.project = projectgroup.id
+    >>> product.projectgroup = projectgroup.id
     >>> flush_database_updates()
 
 Edgar and cprov will be the drivers.
@@ -434,7 +434,7 @@
 
 Without a project group, the driver role falls back to the product owner.
 
-    >>> product.project = None
+    >>> product.projectgroup = None
     >>> for d in series.drivers:
     ...     print d.name
     cprov

=== modified file 'lib/lp/registry/doc/projectgroup.txt'
--- lib/lp/registry/doc/projectgroup.txt	2013-08-19 06:43:04 +0000
+++ lib/lp/registry/doc/projectgroup.txt	2015-01-29 16:37:55 +0000
@@ -348,7 +348,7 @@
 
     >>> product = factory.makeProduct()
     >>> project_group = factory.makeProject()
-    >>> product.project = project_group
+    >>> product.projectgroup = project_group
     >>> project_group.has_translatable()
     False
 

=== modified file 'lib/lp/registry/interfaces/product.py'
--- lib/lp/registry/interfaces/product.py	2014-11-17 18:36:16 +0000
+++ lib/lp/registry/interfaces/product.py	2015-01-29 16:37:55 +0000
@@ -442,7 +442,7 @@
             description=_("The restricted team, moderated team, or person "
                           "who maintains the project information in "
                           "Launchpad.")))
-    project = exported(
+    projectgroup = exported(
         ReferenceChoice(
             title=_('Part of'),
             required=False,
@@ -960,7 +960,7 @@
 
     @call_with(owner=REQUEST_USER)
     @rename_parameters_as(
-        displayname='display_name', project='project_group',
+        displayname='display_name', projectgroup='project_group',
         homepageurl='home_page_url', screenshotsurl='screenshots_url',
         freshmeatproject='freshmeat_project', wikiurl='wiki_url',
         downloadurl='download_url',
@@ -968,14 +968,14 @@
         programminglang='programming_lang')
     @export_factory_operation(
         IProduct, ['name', 'displayname', 'title', 'summary', 'description',
-                   'project', 'homepageurl', 'screenshotsurl',
+                   'projectgroup', 'homepageurl', 'screenshotsurl',
                    'downloadurl', 'freshmeatproject', 'wikiurl',
                    'sourceforgeproject', 'programminglang',
                    'project_reviewed', 'licenses', 'license_info',
                    'registrant', 'bug_supervisor', 'driver'])
     @export_operation_as('new_project')
     def createProduct(owner, name, displayname, title, summary,
-                      description=None, project=None, homepageurl=None,
+                      description=None, projectgroup=None, homepageurl=None,
                       screenshotsurl=None, wikiurl=None,
                       downloadurl=None, freshmeatproject=None,
                       sourceforgeproject=None, programminglang=None,

=== modified file 'lib/lp/registry/model/announcement.py'
--- lib/lp/registry/model/announcement.py	2015-01-29 12:34:40 +0000
+++ lib/lp/registry/model/announcement.py	2015-01-29 16:37:55 +0000
@@ -161,13 +161,13 @@
                 Announcement.active IS TRUE
                 """
         if IProduct.providedBy(self):
-            if self.project is None:
+            if self.projectgroup is None:
                 query += """ AND
                     Announcement.product = %s""" % sqlvalues(self.id)
             else:
                 query += """ AND
                     (Announcement.product = %s OR Announcement.project = %s)
-                    """ % sqlvalues(self.id, self.project)
+                    """ % sqlvalues(self.id, self.projectgroup)
         elif IProjectGroup.providedBy(self):
             query += """ AND
                 (Announcement.project = %s OR Announcement.product IN

=== modified file 'lib/lp/registry/model/milestone.py'
--- lib/lp/registry/model/milestone.py	2013-06-20 05:50:00 +0000
+++ lib/lp/registry/model/milestone.py	2015-01-29 16:37:55 +0000
@@ -469,7 +469,7 @@
             where=And(
                 Milestone.name == self.name,
                 Milestone.productID == Product.id,
-                Product.project == self.target,
+                Product.projectgroup == self.target,
                 ProductSet.getProductPrivacyFilter(user)))
 
     @property

=== modified file 'lib/lp/registry/model/milestonetag.py'
--- lib/lp/registry/model/milestonetag.py	2013-03-25 05:53:38 +0000
+++ lib/lp/registry/model/milestonetag.py	2015-01-29 16:37:55 +0000
@@ -93,7 +93,7 @@
             tables=[Milestone, Product],
             where=And(
                 Milestone.productID == Product.id,
-                Product.project == self.target,
+                Product.projectgroup == self.target,
                 tag_constraints))
 
 

=== modified file 'lib/lp/registry/model/product.py'
--- lib/lp/registry/model/product.py	2014-09-09 09:57:28 +0000
+++ lib/lp/registry/model/product.py	2015-01-29 16:37:55 +0000
@@ -371,7 +371,7 @@
 
     _table = 'Product'
 
-    project = ForeignKey(
+    projectgroup = ForeignKey(
         foreignKey="ProjectGroup", dbName="project", notNull=False,
         default=None)
     _owner = ForeignKey(
@@ -1113,8 +1113,8 @@
             return None
         elif self.bugtracker is not None:
             return self.bugtracker
-        elif self.project is not None:
-            return self.project.bugtracker
+        elif self.projectgroup is not None:
+            return self.projectgroup.bugtracker
         else:
             return None
 
@@ -1177,12 +1177,12 @@
         """See `IProduct`."""
         drivers = set()
         drivers.add(self.driver)
-        if self.project is not None:
-            drivers.add(self.project.driver)
+        if self.projectgroup is not None:
+            drivers.add(self.projectgroup.driver)
         drivers.discard(None)
         if len(drivers) == 0:
-            if self.project is not None:
-                drivers.add(self.project.owner)
+            if self.projectgroup is not None:
+                drivers.add(self.projectgroup.owner)
             else:
                 drivers.add(self.owner)
         return sorted(drivers, key=lambda driver: driver.displayname)
@@ -1404,9 +1404,9 @@
 
     def getInheritedTranslationPolicy(self):
         """See `ITranslationPolicy`."""
-        # A Product inherits parts of it its effective translation
-        # policy from its ProjectGroup, if any.
-        return self.project
+        # A Product inherits parts of its effective translation policy from
+        # its ProjectGroup, if any.
+        return self.projectgroup
 
     def sharesTranslationsWithOtherSide(self, person, language,
                                         sourcepackage=None,
@@ -1569,14 +1569,15 @@
         return False
 
 
-def get_precached_products(products, need_licences=False, need_projects=False,
-                        need_series=False, need_releases=False,
-                        role_names=None, need_role_validity=False):
+def get_precached_products(products, need_licences=False,
+                           need_projectgroups=False, need_series=False,
+                           need_releases=False, role_names=None,
+                           need_role_validity=False):
     """Load and cache product information.
 
     :param products: the products for which to pre-cache information
     :param need_licences: whether to cache license information
-    :param need_projects: whether to cache project information
+    :param need_projectgroups: whether to cache project group information
     :param need_series: whether to cache series information
     :param need_releases: whether to cache release information
     :param role_names: the role names to cache eg bug_supervisor
@@ -1659,8 +1660,9 @@
             cache = caches[license.productID]
             if not license.license in cache._cached_licenses:
                 cache._cached_licenses.append(license.license)
-    if need_projects:
-        bulk.load_related(ProjectGroup, products_by_id.values(), ['projectID'])
+    if need_projectgroups:
+        bulk.load_related(
+            ProjectGroup, products_by_id.values(), ['projectgroupID'])
     bulk.load_related(ProductSeries, products_by_id.values(),
         ['development_focusID'])
     if role_names is not None:
@@ -1813,7 +1815,7 @@
         return results
 
     def createProduct(self, owner, name, displayname, title, summary,
-                      description=None, project=None, homepageurl=None,
+                      description=None, projectgroup=None, homepageurl=None,
                       screenshotsurl=None, wikiurl=None,
                       downloadurl=None, freshmeatproject=None,
                       sourceforgeproject=None, programminglang=None,
@@ -1839,7 +1841,7 @@
                     ' Projects.')
         product = Product(
             owner=owner, registrant=registrant, name=name,
-            displayname=displayname, title=title, project=project,
+            displayname=displayname, title=title, projectgroup=projectgroup,
             summary=summary, description=description, homepageurl=homepageurl,
             screenshotsurl=screenshotsurl, wikiurl=wikiurl,
             downloadurl=downloadurl, freshmeatproject=None,
@@ -2007,7 +2009,7 @@
 
         def eager_load(products):
             return get_precached_products(
-                products, need_licences=True, need_projects=True,
+                products, need_licences=True, need_projectgroups=True,
                 role_names=[
                     '_owner', 'registrant', 'bug_supervisor', 'driver'])
 

=== modified file 'lib/lp/registry/model/projectgroup.py'
--- lib/lp/registry/model/projectgroup.py	2015-01-29 12:23:27 +0000
+++ lib/lp/registry/model/projectgroup.py	2015-01-29 16:37:55 +0000
@@ -169,7 +169,7 @@
 
     def getProducts(self, user):
         results = Store.of(self).find(
-            Product, Product.project == self, Product.active == True,
+            Product, Product.projectgroup == self, Product.active == True,
             ProductSet.getProductPrivacyFilter(user))
         return results.order_by(Product.displayname)
 
@@ -178,7 +178,7 @@
         return list(self.getProducts(getUtility(ILaunchBag).user))
 
     def getProduct(self, name):
-        return Product.selectOneBy(project=self, name=name)
+        return Product.selectOneBy(projectgroup=self, name=name)
 
     def getConfigurableProducts(self):
         return [product for product in self.products
@@ -205,7 +205,7 @@
             ]
         return store.using(*origin).find(
             Product,
-            Product.project == self.id,
+            Product.projectgroup == self.id,
             Product.translations_usage == ServiceUsage.LAUNCHPAD,
             ).config(distinct=True)
 
@@ -248,7 +248,7 @@
         """See `IHasSpecifications`."""
         base_clauses = [
             Specification.productID == Product.id,
-            Product.projectID == self.id]
+            Product.projectgroupID == self.id]
         tables = [Specification]
         if series:
             base_clauses.append(ProductSeries.name == series)
@@ -266,7 +266,7 @@
 
     def _getOfficialTagClause(self):
         """See `OfficialBugTagTargetMixin`."""
-        And(ProjectGroup.id == Product.projectID,
+        And(ProjectGroup.id == Product.projectgroupID,
             Product.id == OfficialBugTag.productID)
 
     @property
@@ -276,7 +276,7 @@
         result = store.find(
             OfficialBugTag.tag,
             OfficialBugTag.product == Product.id,
-            Product.project == self.id).order_by(OfficialBugTag.tag)
+            Product.projectgroup == self.id).order_by(OfficialBugTag.tag)
         result.config(distinct=True)
         return result
 
@@ -339,8 +339,8 @@
             projectgroup=self).getResults()
 
     def hasProducts(self):
-        """Returns True if a project has products associated with it, False
-        otherwise.
+        """Returns True if a project group has products associated with it,
+        False otherwise.
 
         If the project group has < 1 product, selected links will be disabled.
         This is to avoid situations where users try to file bugs against
@@ -354,7 +354,7 @@
         privacy_filter = ProductSet.getProductPrivacyFilter(user)
         return And(
             Milestone.productID == Product.id,
-            Product.projectID == self.id,
+            Product.projectgroupID == self.id,
             privacy_filter)
 
     def _getMilestones(self, user, only_active):
@@ -375,7 +375,7 @@
             )
         privacy_filter = ProductSet.getProductPrivacyFilter(user)
         conditions = And(Milestone.product == Product.id,
-                         Product.project == self,
+                         Product.projectgroup == self,
                          Product.active == True,
                          privacy_filter)
         result = store.find(columns, conditions)
@@ -393,7 +393,7 @@
         if result.any() is not None:
             milestone_names = [data[0] for data in result]
             product_conditions = And(
-                Product.project == self,
+                Product.projectgroup == self,
                 Milestone.product == Product.id,
                 Product.active == True,
                 privacy_filter,
@@ -415,7 +415,7 @@
         result = store.find(
             Milestone.id,
             And(Milestone.product == Product.id,
-                Product.project == self,
+                Product.projectgroup == self,
                 Product.active == True))
         return result.any() is not None
 
@@ -450,7 +450,7 @@
         has_series = ProductSeries.selectFirst(
             AND(ProductSeries.q.productID == Product.q.id,
                 ProductSeries.q.name == series_name,
-                Product.q.projectID == self.id), orderBy='id')
+                Product.q.projectgroupID == self.id), orderBy='id')
 
         if has_series is None:
             return None
@@ -534,10 +534,10 @@
         NotFoundError: -1
         """
         try:
-            project = ProjectGroup.get(projectgroupid)
+            projectgroup = ProjectGroup.get(projectgroupid)
         except SQLObjectNotFound:
             raise NotFoundError(projectgroupid)
-        return project
+        return projectgroup
 
     def getByName(self, name, ignore_inactive=False):
         """See `IProjectGroupSet`."""
@@ -592,8 +592,8 @@
                 product_query = "Product.fti @@ ftq(%s)" % sqlvalues(text)
                 queries.append(product_query)
             else:
-                project_query = "Project.fti @@ ftq(%s)" % sqlvalues(text)
-                queries.append(project_query)
+                projectgroup_query = "Project.fti @@ ftq(%s)" % sqlvalues(text)
+                queries.append(projectgroup_query)
 
         if 'Product' in clauseTables:
             queries.append('Product.project=Project.id')

=== modified file 'lib/lp/registry/publisher.py'
--- lib/lp/registry/publisher.py	2011-12-24 17:49:30 +0000
+++ lib/lp/registry/publisher.py	2015-01-29 16:37:55 +0000
@@ -18,10 +18,10 @@
     def isWithin(self, scope):
         """Is this product within the given scope?
 
-        A product is within itself or its project.
+        A product is within itself or its project group.
         """
 
-        return scope == self.context or scope == self.context.project
+        return scope == self.context or scope == self.context.projectgroup
 
 
 class LaunchpadDistributionSourcePackageContainer(LaunchpadContainer):

=== modified file 'lib/lp/registry/stories/webservice/xx-project-registry.txt'
--- lib/lp/registry/stories/webservice/xx-project-registry.txt	2013-02-07 01:24:48 +0000
+++ lib/lp/registry/stories/webservice/xx-project-registry.txt	2015-01-29 16:37:55 +0000
@@ -568,20 +568,20 @@
 The project collection has a method for creating a new project.
 
     >>> def create_project(name, display_name, title, summary,
-    ...                    description=None, project=None, homepage_url=None,
-    ...                    screenshots_url=None, wiki_url=None,
-    ...                    download_url=None, freshmeat_project=None,
-    ...                    sourceforge_project=None, programming_lang=None,
-    ...                    licenses=(), license_info=None,
-    ...                    project_reviewed=False,
+    ...                    description=None, project_group=None,
+    ...                    homepage_url=None, screenshots_url=None,
+    ...                    wiki_url=None, download_url=None,
+    ...                    freshmeat_project=None, sourceforge_project=None,
+    ...                    programming_lang=None, licenses=(),
+    ...                    license_info=None, project_reviewed=False,
     ...                    registrant=None):
     ...     return webservice.named_post(
     ...         "/projects", "new_project",
     ...         name=name, display_name=display_name,
     ...         title=title, summary=summary, description=description,
-    ...         homepage_url=homepage_url, screenshots_url=screenshots_url,
-    ...         wiki_url=wiki_url, download_url=download_url,
-    ...         freshmeat_project=freshmeat_project,
+    ...         project_group=project_group, homepage_url=homepage_url,
+    ...         screenshots_url=screenshots_url, wiki_url=wiki_url,
+    ...         download_url=download_url, freshmeat_project=freshmeat_project,
     ...         sourceforge_project=sourceforge_project,
     ...         programming_lang=programming_lang,
     ...         licenses=licenses, license_info=license_info,

=== modified file 'lib/lp/registry/templates/object-milestones.pt'
--- lib/lp/registry/templates/object-milestones.pt	2011-01-19 21:06:27 +0000
+++ lib/lp/registry/templates/object-milestones.pt	2015-01-29 16:37:55 +0000
@@ -16,11 +16,11 @@
     </p>
 
     <ul class="horizontal"
-      tal:condition="context/project|nothing">
+      tal:condition="context/projectgroup|nothing">
       <li>
         <a class="sprite info"
-          tal:attributes="href context/project/menu:overview/milestones/fmt:url">View milestones for
-          <tal:project replace="context/project/displayname" /></a>
+          tal:attributes="href context/projectgroup/menu:overview/milestones/fmt:url">View milestones for
+          <tal:projectgroup replace="context/projectgroup/displayname" /></a>
       </li>
     </ul>
   </div>

=== modified file 'lib/lp/registry/templates/product-index.pt'
--- lib/lp/registry/templates/product-index.pt	2014-11-14 10:31:36 +0000
+++ lib/lp/registry/templates/product-index.pt	2015-01-29 16:37:55 +0000
@@ -81,9 +81,9 @@
             <h2>Project information</h2>
 
             <div class="two-column-list">
-              <dl id="partof" tal:condition="context/project">
+              <dl id="partof" tal:condition="context/projectgroup">
                 <dt>Part of:</dt>
-                <dd><a tal:replace="structure context/project/fmt:link" /></dd>
+                <dd><a tal:replace="structure context/projectgroup/fmt:link" /></dd>
               </dl>
 
               <dl id="owner" tal:condition="context/owner">

=== modified file 'lib/lp/registry/templates/product-rdf.pt'
--- lib/lp/registry/templates/product-rdf.pt	2014-09-08 08:07:06 +0000
+++ lib/lp/registry/templates/product-rdf.pt	2015-01-29 16:37:55 +0000
@@ -37,10 +37,10 @@
                        tal:content="context/screenshotsurl">
             http://www.foo.org/screenshots/
         </lp:screenshot>
-        <lp:inProject tal:condition="context/project">
+        <lp:inProject tal:condition="context/projectgroup">
             <lp:Project>
                 <lp:specifiedAt tal:attributes="rdf:resource
-                    string:${context/project/fmt:url}/+rdf" />
+                    string:${context/projectgroup/fmt:url}/+rdf" />
             </lp:Project>
         </lp:inProject>
         <lp:series tal:repeat="series context/series">

=== modified file 'lib/lp/registry/tests/test_product.py'
--- lib/lp/registry/tests/test_product.py	2014-06-19 10:04:55 +0000
+++ lib/lp/registry/tests/test_product.py	2015-01-29 16:37:55 +0000
@@ -828,7 +828,7 @@
             'getSpecification',
             'icon', 'logo', 'name', 'official_answers', 'official_anything',
             'official_blueprints', 'official_codehosting', 'official_malone',
-            'owner', 'parent_subscription_target', 'pillar', 'project',
+            'owner', 'parent_subscription_target', 'pillar', 'projectgroup',
             'searchTasks', 'title')),
         'launchpad.View': set((
             '_getOfficialTagClause', 'visible_specifications',
@@ -930,7 +930,7 @@
                 'license_info', 'licenses', 'logo', 'mugshot',
                 'official_answers', 'official_blueprints',
                 'official_codehosting', 'owner', 'private',
-                'programminglang', 'project', 'redeemSubscriptionVoucher',
+                'programminglang', 'projectgroup', 'redeemSubscriptionVoucher',
                 'releaseroot', 'screenshotsurl', 'sourceforgeproject',
                 'summary', 'title', 'uses_launchpad', 'wikiurl')),
             'launchpad.Moderate': set((

=== modified file 'lib/lp/registry/tests/test_project_milestone.py'
--- lib/lp/registry/tests/test_project_milestone.py	2014-06-19 06:38:53 +0000
+++ lib/lp/registry/tests/test_project_milestone.py	2015-01-29 16:37:55 +0000
@@ -182,7 +182,7 @@
         """
         # firefox does not belong to the Gnome project.
         firefox = getUtility(IProductSet)['firefox']
-        self.assertNotEqual(firefox.project.name, 'gnome')
+        self.assertNotEqual(firefox.projectgroup.name, 'gnome')
 
         self.createProductMilestone('1.1', 'firefox', None)
         gnome = getUtility(IProjectGroupSet)['gnome']

=== modified file 'lib/lp/registry/tests/test_projectgroup.py'
--- lib/lp/registry/tests/test_projectgroup.py	2015-01-29 15:33:33 +0000
+++ lib/lp/registry/tests/test_projectgroup.py	2015-01-29 16:37:55 +0000
@@ -111,7 +111,7 @@
             name="zazzle-product",
             title="Hoozah",
             owner=self.person)
-        product.project = self.projectgroup1
+        product.projectgroup = self.projectgroup1
         results = self.projectgroupset.search(
             text="Hoozah", search_products=False)
         self.assertEqual(0, results.count())
@@ -123,7 +123,7 @@
             name="zazzle-product",
             title="Hoozah",
             owner=self.person)
-        product.project = self.projectgroup1
+        product.projectgroup = self.projectgroup1
         results = self.projectgroupset.search(
             text="Hoozah", search_products=True)
         self.assertEqual(1, results.count())

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2015-01-29 14:14:01 +0000
+++ lib/lp/testing/factory.py	2015-01-29 16:37:55 +0000
@@ -984,7 +984,7 @@
                 summary,
                 self.getUniqueString('description'),
                 licenses=licenses,
-                project=projectgroup,
+                projectgroup=projectgroup,
                 registrant=registrant,
                 icon=icon,
                 information_type=information_type)

=== modified file 'lib/lp/translations/model/translationsperson.py'
--- lib/lp/translations/model/translationsperson.py	2013-01-14 02:33:31 +0000
+++ lib/lp/translations/model/translationsperson.py	2015-01-29 16:37:55 +0000
@@ -294,7 +294,8 @@
                                 ServiceUsage.LAUNCHPAD,
                             Product.active == True)),
                     LeftJoin(
-                        ProjectGroup, ProjectGroup.id == Product.projectID),
+                        ProjectGroup,
+                        ProjectGroup.id == Product.projectgroupID),
                     Join(
                         SQL('reviewable_groups'),
                         SQL('reviewable_groups.id') ==
@@ -348,7 +349,7 @@
             Product.active == True))
 
         ProjectJoin = LeftJoin(
-            ProjectGroup, ProjectGroup.id == Product.projectID)
+            ProjectGroup, ProjectGroup.id == Product.projectgroupID)
 
         # Look up translation group.
         groupjoin_conditions = Or(

=== modified file 'lib/lp/translations/tests/test_translationpermission.py'
--- lib/lp/translations/tests/test_translationpermission.py	2015-01-29 14:14:01 +0000
+++ lib/lp/translations/tests/test_translationpermission.py	2015-01-29 16:37:55 +0000
@@ -62,10 +62,11 @@
     """Return the effective translation permission for `product`.
 
     This combines the translation permissions for `product` and
-    `product.project`.
+    `product.projectgroup`.
     """
     return max(
-        product.project.translationpermission, product.translationpermission)
+        product.projectgroup.translationpermission,
+        product.translationpermission)
 
 
 class TestTranslationPermission(TestCaseWithFactory):
@@ -87,8 +88,8 @@
         closed to the public.
         """
         product.translationpermission = TranslationPermission.CLOSED
-        if product.project is not None:
-            product.project.translationpermission = (
+        if product.projectgroup is not None:
+            product.projectgroup.translationpermission = (
                 TranslationPermission.CLOSED)
 
     def makePOTemplateForProduct(self, product):
@@ -179,7 +180,7 @@
         self.closeTranslations(product)
         user = self.factory.makePerson()
         group = self.factory.makeTranslationGroup()
-        product.project.translationgroup = group
+        product.projectgroup.translationgroup = group
         pofile = self.makePOFileForProduct(product)
         getUtility(ITranslatorSet).new(group, pofile.language, user)
 
@@ -194,19 +195,20 @@
         self.closeTranslations(product)
         pofile = self.makePOFileForProduct(product)
         product_translator = self.factory.makePerson()
-        project_translator = self.factory.makePerson()
-        product.project.translationgroup = self.factory.makeTranslationGroup()
+        projectgroup_translator = self.factory.makePerson()
+        product.projectgroup.translationgroup = (
+            self.factory.makeTranslationGroup())
         product.translationgroup = self.factory.makeTranslationGroup()
         self.makeTranslationTeam(
-            product.project.translationgroup, pofile.language,
-            [project_translator])
+            product.projectgroup.translationgroup, pofile.language,
+            [projectgroup_translator])
         self.makeTranslationTeam(
             product.translationgroup, pofile.language, [product_translator])
 
         # Both the translator from the project group's translation team
         # and the one from the product's translation team have edit
         # privileges on the translation.
-        self.assertTrue(pofile.canEditTranslations(project_translator))
+        self.assertTrue(pofile.canEditTranslations(projectgroup_translator))
         self.assertTrue(pofile.canEditTranslations(product_translator))
 
     def test_projectgroup_and_product_permissions_combine(self):
@@ -216,8 +218,9 @@
         product = self.makeProductInProjectGroup()
         user = self.factory.makePerson()
         pofiles = self.makePOFilesForCoverageLevels(product, user)
-        for project_permission in TranslationPermission.items:
-            product.project.translationpermission = project_permission
+        for projectgroup_permission in TranslationPermission.items:
+            product.projectgroup.translationpermission = (
+                projectgroup_permission)
             for product_permission in TranslationPermission.items:
                 product.translationpermission = product_permission
                 effective_permission = combine_permissions(product)
@@ -272,11 +275,12 @@
 
         # The strictest of Open and something else is always the
         # something else.
-        for project_permission in TranslationPermission.items:
-            product.project.translationpermission = project_permission
+        for projectgroup_permission in TranslationPermission.items:
+            product.projectgroup.translationpermission = (
+                projectgroup_permission)
             for product_permission in TranslationPermission.items:
                 product.translationpermission = product_permission
                 expected_permission = (
-                    combinations[project_permission][product_permission])
+                    combinations[projectgroup_permission][product_permission])
                 self.assertEqual(
                     expected_permission, combine_permissions(product))


Follow ups