Merge lp:~wgrant/launchpad/improve-branch-edit-type into lp:launchpad

Proposed by William Grant
Status: Merged
Merged at revision: 15617
Proposed branch: lp:~wgrant/launchpad/improve-branch-edit-type
Merge into: lp:launchpad
Diff against target: 531 lines (+185/-228)
5 files modified
lib/lp/code/browser/branch.py (+50/-25)
lib/lp/code/browser/tests/test_branch.py (+80/-58)
lib/lp/code/interfaces/branch.py (+5/-17)
lib/lp/code/model/branch.py (+14/-32)
lib/lp/code/model/tests/test_branch.py (+36/-96)
To merge this branch: bzr merge lp:~wgrant/launchpad/improve-branch-edit-type
Reviewer Review Type Date Requested Status
j.c.sackett (community) Approve
Review via email: mp+114591@code.launchpad.net

Commit message

Rework BranchEditView's information type widget to support the new sharing model.

Description of the change

This branch reworks BranchEditView's information type widget to support the new sharing model a little more easily. It used to choose types to show based on Branch.canBe{Public,Private}, which doesn't work well if we want to use a different set (eg. private projects only support Proprietary).

I adjusted Branch.transitionToInformationType to use the information types allowed by the branch's namespace, or all the information types if the user is a Launchpad admin. BranchEditView.getInformationTypesToShow restricts this further, hiding uninteresting types to avoid confusion, and in a sort of attempt to prevent projects from using private branches for nefarious purposes without paying us. These obscure types are only shown in the UI if there's a linked bug of one of the obscure types, similar to the old behaviour in Branch.canBePrivate.

I've dropped the commercial subscription check from Branch.canBePrivate for now, as it's new, not yet enabled, and will be replaced by a branch configuration option shortly.

There's still some awfulness in BranchEditView around restricting type changes for stacked branches, but I plan to disentangle that in a followup.

To post a comment you must log in.
Revision history for this message
j.c.sackett (jcsackett) wrote :

Thanks William. One minor point.

At line #60 you have a comment about using the widget vocabulary in `getInformationTypesToShow`, but you actually do that vocal step on #103, in setUpWidgets. Might be worth moving the comment to the place where the code is actually happening.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/code/browser/branch.py'
2--- lib/lp/code/browser/branch.py 2012-07-10 10:58:27 +0000
3+++ lib/lp/code/browser/branch.py 2012-07-12 21:59:20 +0000
4@@ -91,7 +91,11 @@
5 from lp.blueprints.interfaces.specificationbranch import ISpecificationBranch
6 from lp.bugs.interfaces.bug import IBugSet
7 from lp.bugs.interfaces.bugbranch import IBugBranch
8-from lp.bugs.interfaces.bugtask import UNRESOLVED_BUGTASK_STATUSES
9+from lp.bugs.interfaces.bugtask import (
10+ BugTaskSearchParams,
11+ IBugTaskSet,
12+ UNRESOLVED_BUGTASK_STATUSES,
13+ )
14 from lp.code.browser.branchmergeproposal import (
15 latest_proposals_for_each_branch,
16 )
17@@ -121,6 +125,7 @@
18 from lp.code.interfaces.branchmergeproposal import IBranchMergeProposal
19 from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference
20 from lp.registry.enums import (
21+ InformationType,
22 PRIVATE_INFORMATION_TYPES,
23 PUBLIC_INFORMATION_TYPES,
24 )
25@@ -1095,36 +1100,56 @@
26 if branch.branch_type in (BranchType.HOSTED, BranchType.IMPORTED):
27 self.form_fields = self.form_fields.omit('url')
28
29+ def getInformationTypesToShow(self):
30+ """Get the information types to display on the edit form.
31+
32+ We display a highly customised set of information types:
33+ anything allowed by the namespace, plus the current type,
34+ except some of the obscure types unless there's a linked
35+ bug with an obscure type.
36+ """
37+ allowed_types = self.context.getAllowedInformationTypes(self.user)
38+ shown_types = (
39+ InformationType.PUBLIC,
40+ InformationType.USERDATA,
41+ InformationType.PROPRIETARY,
42+ )
43+
44+ # We only show Embargoed Security and Unembargoed Security
45+ # if the branch is linked to a bug with one of those types,
46+ # as they're confusing and not generally useful otherwise.
47+ # Once Proprietary is fully deployed, User Data should be
48+ # added here.
49+ hidden_types = (
50+ InformationType.UNEMBARGOEDSECURITY,
51+ InformationType.EMBARGOEDSECURITY,
52+ )
53+ if set(allowed_types).intersection(hidden_types):
54+ params = BugTaskSearchParams(
55+ user=self.user, linked_branches=self.context.id,
56+ information_type=hidden_types)
57+ if getUtility(IBugTaskSet).searchBugIds(params).count() > 0:
58+ shown_types += hidden_types
59+
60+ # Now take the intersection of the allowed and shown types.
61+ combined_types = set(allowed_types).intersection(shown_types)
62+ combined_types.add(self.context.information_type)
63+ return combined_types
64+
65 def setUpWidgets(self, context=None):
66 super(BranchEditView, self).setUpWidgets()
67- branch = self.context
68-
69 if self.form_fields.get('information_type') is not None:
70+ # Customise the set of shown types.
71+ types_to_show = self.getInformationTypesToShow()
72+ # Grab the types from the vocab if they exist.
73 # The vocab uses feature flags to control what is displayed so we
74 # need to pull info_types from the vocab to use to make the subset
75- # of what we show the user.
76+ # of what we show the user. This is mostly to hide Proprietary
77+ # while it's disabled.
78 info_type_vocab = self.widgets['information_type'].vocabulary
79- public_types = [
80- info_type
81- for info_type in info_type_vocab
82- if info_type.value in PUBLIC_INFORMATION_TYPES]
83- private_types = [
84- info_type
85- for info_type in info_type_vocab
86- if info_type.value in PRIVATE_INFORMATION_TYPES]
87-
88- allowed_information_types = []
89- if branch.private:
90- if branch.canBePublic(self.user):
91- allowed_information_types.extend(public_types)
92- allowed_information_types.extend(private_types)
93- else:
94- allowed_information_types.extend(public_types)
95- if branch.canBePrivate(self.user):
96- allowed_information_types.extend(private_types)
97-
98- self.widgets['information_type'].vocabulary = (
99- SimpleVocabulary(allowed_information_types))
100+ self.widgets['information_type'].vocabulary = SimpleVocabulary(
101+ [info_type for info_type in info_type_vocab
102+ if info_type.value in types_to_show])
103
104 def validate(self, data):
105 # Check that we're not moving a team branch to the +junk
106
107=== modified file 'lib/lp/code/browser/tests/test_branch.py'
108--- lib/lp/code/browser/tests/test_branch.py 2012-07-09 04:14:09 +0000
109+++ lib/lp/code/browser/tests/test_branch.py 2012-07-12 21:59:20 +0000
110@@ -47,6 +47,7 @@
111 from lp.services.webapp.publisher import canonical_url
112 from lp.services.webapp.servers import LaunchpadTestRequest
113 from lp.testing import (
114+ admin_logged_in,
115 BrowserTestCase,
116 login,
117 login_person,
118@@ -915,32 +916,11 @@
119 admin = admins.teamowner
120 browser = self.getUserBrowser(
121 canonical_url(branch) + '/+edit', user=admin)
122- browser.getControl("Embargoed Security").click()
123+ browser.getControl("User Data").click()
124 browser.getControl("Change Branch").click()
125 with person_logged_in(person):
126 self.assertEqual(
127- InformationType.EMBARGOEDSECURITY, branch.information_type)
128-
129- def test_proprietary_in_ui_vocabulary_commercial_projects(self):
130- # Commercial projects can have information type Proprietary.
131- owner = self.factory.makePerson()
132- product = self.factory.makeProduct()
133- self.factory.makeCommercialSubscription(product)
134- branch = self.factory.makeProductBranch(product=product, owner=owner)
135- with person_logged_in(owner):
136- browser = self.getUserBrowser(
137- canonical_url(branch) + '/+edit', user=owner)
138- self.assertIsNotNone(browser.getControl("Proprietary"))
139-
140- def test_proprietary_not_in_ui_vocabulary_normal_projects(self):
141- # Non-commercial projects can not have information type Proprietary.
142- owner = self.factory.makePerson()
143- product = self.factory.makeProduct()
144- branch = self.factory.makeProductBranch(product=product, owner=owner)
145- with person_logged_in(owner):
146- browser = self.getUserBrowser(
147- canonical_url(branch) + '/+edit', user=owner)
148- self.assertRaises(LookupError, browser.getControl, "Proprietary")
149+ InformationType.USERDATA, branch.information_type)
150
151 def test_can_not_change_privacy_of_stacked_on_private(self):
152 # The privacy field is not shown if the branch is stacked on a
153@@ -958,41 +938,83 @@
154 self.assertRaises(
155 LookupError, browser.getControl, "Information Type")
156
157- def test_authorised_user_can_change_branch_to_private(self):
158- # An authorised user can make the information type private.
159- team_owner = self.factory.makePerson()
160- user = self.factory.makePerson()
161- team = self.factory.makeTeam(
162- owner=team_owner,
163- visibility=PersonVisibility.PRIVATE,
164- subscription_policy=TeamSubscriptionPolicy.RESTRICTED)
165- with person_logged_in(team_owner):
166- team.addMember(user, team_owner)
167- with person_logged_in(user):
168- branch = self.factory.makeBranch(owner=team)
169- browser = self.getUserBrowser(
170- canonical_url(branch) + '/+edit', user=user)
171- self.assertIsNotNone(browser.getControl, "Embargoed Security")
172-
173- def test_unauthorised_user_cannot_change_branch_to_private(self):
174- # An unauthorised user cannot make the information type private.
175- user = self.factory.makePerson()
176- with person_logged_in(user):
177- branch = self.factory.makeBranch(owner=user)
178- browser = self.getUserBrowser(
179- canonical_url(branch) + '/+edit', user=user)
180- self.assertRaises(
181- LookupError, browser.getControl, "Embargoed Security")
182-
183- def test_branch_for_commercial_project(self):
184- # A branch for a commercial project can be private.
185- product = self.factory.makeProduct()
186- self.factory.makeCommercialSubscription(product)
187- branch = self.factory.makeProductBranch(product=product)
188- with person_logged_in(branch.owner):
189- browser = self.getUserBrowser(
190- canonical_url(branch) + '/+edit', user=branch.owner)
191- self.assertIsNotNone(browser.getControl, "Embargoed Security")
192+
193+class TestBranchEditViewInformationTypes(TestCaseWithFactory):
194+ """Tests for BranchEditView.getInformationTypesToShow."""
195+
196+ layer = DatabaseFunctionalLayer
197+
198+ def assertShownTypes(self, types, branch, user=None):
199+ if user is None:
200+ user = removeSecurityProxy(branch).owner
201+ with person_logged_in(user):
202+ view = create_initialized_view(branch, '+edit', user=user)
203+ self.assertContentEqual(types, view.getInformationTypesToShow())
204+
205+ def test_public_branch(self):
206+ # A normal public branch on a public project can only be public.
207+ # We don't show information types like Unembargoed Security
208+ # unless there's a linked branch of that type, as they're not
209+ # useful or unconfusing otherwise.
210+ # The model doesn't enforce this, so it's just a UI thing.
211+ branch = self.factory.makeBranch(
212+ information_type=InformationType.PUBLIC)
213+ self.assertShownTypes([InformationType.PUBLIC], branch)
214+
215+ def test_public_branch_with_security_bug(self):
216+ # A public branch can be set to Unembargoed Security if it has a
217+ # linked Unembargoed Security bug. The project policy doesn't
218+ # allow private branches, so Embargoed Security and User Data
219+ # are unavailable.
220+ branch = self.factory.makeBranch(
221+ information_type=InformationType.PUBLIC)
222+ bug = self.factory.makeBug(
223+ information_type=InformationType.UNEMBARGOEDSECURITY)
224+ with admin_logged_in():
225+ branch.linkBug(bug, branch.owner)
226+ self.assertShownTypes(
227+ [InformationType.PUBLIC, InformationType.UNEMBARGOEDSECURITY],
228+ branch)
229+
230+ def test_branch_with_disallowed_type(self):
231+ # We don't force branches with a disallowed type (eg. Proprietary on a
232+ # non-commercial project) to change, so the current type is
233+ # shown.
234+ branch = self.factory.makeBranch(
235+ information_type=InformationType.PROPRIETARY)
236+ self.assertShownTypes(
237+ [InformationType.PUBLIC, InformationType.PROPRIETARY], branch)
238+
239+ def test_private_branch(self):
240+ # Branches on projects with a private policy can be set to
241+ # User Data (aka. Private)
242+ branch = self.factory.makeBranch(
243+ information_type=InformationType.PUBLIC)
244+ with admin_logged_in():
245+ branch.product.setBranchVisibilityTeamPolicy(
246+ branch.owner, BranchVisibilityRule.PRIVATE)
247+ self.assertShownTypes(
248+ [InformationType.PUBLIC, InformationType.USERDATA,
249+ InformationType.PROPRIETARY], branch)
250+
251+ def test_private_branch_with_security_bug(self):
252+ # Branches on projects that allow private branches can use the
253+ # Embargoed Security information type if they have a security
254+ # bug linked.
255+ branch = self.factory.makeBranch(
256+ information_type=InformationType.PUBLIC)
257+ with admin_logged_in():
258+ branch.product.setBranchVisibilityTeamPolicy(
259+ branch.owner, BranchVisibilityRule.PRIVATE)
260+ bug = self.factory.makeBug(
261+ information_type=InformationType.UNEMBARGOEDSECURITY)
262+ with admin_logged_in():
263+ branch.linkBug(bug, branch.owner)
264+ self.assertShownTypes(
265+ [InformationType.PUBLIC, InformationType.UNEMBARGOEDSECURITY,
266+ InformationType.EMBARGOEDSECURITY, InformationType.USERDATA,
267+ InformationType.PROPRIETARY],
268+ branch)
269
270
271 class TestBranchUpgradeView(TestCaseWithFactory):
272
273=== modified file 'lib/lp/code/interfaces/branch.py'
274--- lib/lp/code/interfaces/branch.py 2012-07-06 03:33:59 +0000
275+++ lib/lp/code/interfaces/branch.py 2012-07-12 21:59:20 +0000
276@@ -968,23 +968,11 @@
277 def visibleByUser(user):
278 """Can the specified user see this branch?"""
279
280- def canBePublic(user):
281- """Can this branch be public?
282-
283- A branch can be made public if:
284- - the branch has a visibility policy which allows it
285- - the user is an admin or bzr expert
286- """
287-
288- def canBePrivate(user):
289- """Can this branch be private?
290-
291- A branch can be made private if:
292- - the branch has a visibility policy which allows it
293- - the user is an admin or bzr expert
294- - the branch is owned by a private team
295- (The branch is already implicitly private)
296- - the branch is linked to a private bug the user can access
297+ def getAllowedInformationTypes(user):
298+ """Get a list of acceptable `InformationType`s for this branch.
299+
300+ If the user is a Launchpad admin, any type is acceptable. Otherwise
301+ the `IBranchNamespace` is consulted.
302 """
303
304
305
306=== modified file 'lib/lp/code/model/branch.py'
307--- lib/lp/code/model/branch.py 2012-07-11 09:43:04 +0000
308+++ lib/lp/code/model/branch.py 2012-07-12 21:59:20 +0000
309@@ -237,6 +237,17 @@
310 information_type = InformationType.PUBLIC
311 return self.transitionToInformationType(information_type, user)
312
313+ def getAllowedInformationTypes(self, who):
314+ """See `IBranch`."""
315+ if user_has_special_branch_access(who):
316+ # Until sharing settles down, admins can set any type.
317+ types = set(PUBLIC_INFORMATION_TYPES + PRIVATE_INFORMATION_TYPES)
318+ else:
319+ # Otherwise the permitted types are defined by the namespace.
320+ policy = IBranchNamespacePolicy(self.namespace)
321+ types = set(policy.getAllowedInformationTypes())
322+ return types
323+
324 def transitionToInformationType(self, information_type, who,
325 verify_policy=True):
326 """See `IBranch`."""
327@@ -246,11 +257,9 @@
328 and self.stacked_on.information_type in PRIVATE_INFORMATION_TYPES
329 and information_type in PUBLIC_INFORMATION_TYPES):
330 raise BranchCannotChangeInformationType()
331- # Only check the privacy policy if the user is not special.
332- if verify_policy and not user_has_special_branch_access(who):
333- policy = IBranchNamespacePolicy(self.namespace)
334- if information_type not in policy.getAllowedInformationTypes():
335- raise BranchCannotChangeInformationType()
336+ if (verify_policy
337+ and information_type not in self.getAllowedInformationTypes(who)):
338+ raise BranchCannotChangeInformationType()
339 self.information_type = information_type
340 self._reconcileAccess()
341 if information_type in PRIVATE_INFORMATION_TYPES:
342@@ -1292,33 +1301,6 @@
343 user, checked_branches)
344 return can_access
345
346- def canBePublic(self, user):
347- """See `IBranch`."""
348- policy = IBranchNamespacePolicy(self.namespace)
349- return InformationType.PUBLIC in policy.getAllowedInformationTypes()
350-
351- def canBePrivate(self, user):
352- """See `IBranch`."""
353- policy = IBranchNamespacePolicy(self.namespace)
354- # Do the easy checks first.
355- policy_allows = (
356- InformationType.USERDATA in policy.getAllowedInformationTypes())
357- if (policy_allows or
358- user_has_special_branch_access(user) or
359- user.visibility == PersonVisibility.PRIVATE):
360- return True
361- # Branches linked to commercial projects can be private.
362- target = self.target.context
363- if (IProduct.providedBy(target) and
364- target.has_current_commercial_subscription):
365- return True
366- # Branches linked to private bugs can be private.
367- params = BugTaskSearchParams(
368- user=user, linked_branches=self.id,
369- information_type=PRIVATE_INFORMATION_TYPES)
370- bug_ids = getUtility(IBugTaskSet).searchBugIds(params)
371- return bug_ids.count() > 0
372-
373 @property
374 def recipes(self):
375 """See `IHasRecipes`."""
376
377=== modified file 'lib/lp/code/model/tests/test_branch.py'
378--- lib/lp/code/model/tests/test_branch.py 2012-07-11 09:36:13 +0000
379+++ lib/lp/code/model/tests/test_branch.py 2012-07-12 21:59:20 +0000
380@@ -76,7 +76,10 @@
381 from lp.code.interfaces.branchmergeproposal import (
382 BRANCH_MERGE_PROPOSAL_FINAL_STATES as FINAL_STATES,
383 )
384-from lp.code.interfaces.branchnamespace import IBranchNamespaceSet
385+from lp.code.interfaces.branchnamespace import (
386+ IBranchNamespacePolicy,
387+ IBranchNamespaceSet,
388+ )
389 from lp.code.interfaces.branchrevision import IBranchRevision
390 from lp.code.interfaces.codehosting import branch_id_alias
391 from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
392@@ -2388,6 +2391,38 @@
393 self.assertEqual([], get_policies_for_artifact(branch))
394
395
396+class TestBranchGetAllowedInformationTypes(TestCaseWithFactory):
397+ """Test Branch.getAllowedInformationTypes."""
398+
399+ layer = DatabaseFunctionalLayer
400+
401+ def test_normal_user_sees_namespace_types(self):
402+ # An unprivileged user sees the types allowed by the namespace.
403+ branch = self.factory.makeBranch()
404+ policy = IBranchNamespacePolicy(branch.namespace)
405+ self.assertContentEqual(
406+ policy.getAllowedInformationTypes(),
407+ branch.getAllowedInformationTypes(branch.owner))
408+ self.assertNotIn(
409+ InformationType.PROPRIETARY,
410+ branch.getAllowedInformationTypes(branch.owner))
411+
412+ def test_admin_sees_namespace_types(self):
413+ # An admin sees all the types, since they occasionally need to
414+ # override the namespace rules. This is hopefully temporary, and
415+ # can go away once the new sharing rules (granting
416+ # non-commercial projects limited use of private branches) are
417+ # deployed.
418+ branch = self.factory.makeBranch()
419+ admin = self.factory.makeAdministrator()
420+ self.assertContentEqual(
421+ PUBLIC_INFORMATION_TYPES + PRIVATE_INFORMATION_TYPES,
422+ branch.getAllowedInformationTypes(admin))
423+ self.assertIn(
424+ InformationType.PROPRIETARY,
425+ branch.getAllowedInformationTypes(admin))
426+
427+
428 class TestBranchSetPrivate(TestCaseWithFactory):
429 """Test IBranch.setPrivate."""
430
431@@ -2507,101 +2542,6 @@
432 get_policies_for_artifact(branch)[0].type)
433
434
435-class TestBranchCanBePrivate(TestCaseWithFactory):
436- """Test IBranch.canBePrivate."""
437-
438- layer = DatabaseFunctionalLayer
439-
440- def setUp(self):
441- # Use an admin user as we aren't checking edit permissions here.
442- TestCaseWithFactory.setUp(self, 'admin@canonical.com')
443-
444- def test_arbitary_branch(self):
445- # By default branches cannot be private.
446- branch = self.factory.makeBranch()
447- self.assertFalse(branch.canBePrivate(branch.owner))
448-
449- def test_admin(self):
450- # Admins can make a branch private.
451- branch = self.factory.makeBranch()
452- admin = getUtility(ILaunchpadCelebrities).admin
453- self.assertTrue(branch.canBePrivate(admin))
454-
455- def test_visibility_policy_private(self):
456- # Users with a suitable visibility policy can make a branch private.
457- team = self.factory.makeTeam(
458- subscription_policy=TeamSubscriptionPolicy.RESTRICTED)
459- product = self.factory.makeProduct()
460- product.setBranchVisibilityTeamPolicy(
461- team, BranchVisibilityRule.PRIVATE)
462- branch = self.factory.makeBranch(product=product, owner=team)
463- self.assertTrue(branch.canBePrivate(team))
464-
465- def test_private_owner(self):
466- # Private team owners can make a branch private.
467- team = self.factory.makeTeam(
468- visibility=PersonVisibility.PRIVATE,
469- subscription_policy=TeamSubscriptionPolicy.RESTRICTED)
470- branch = self.factory.makeBranch(owner=team)
471- self.assertTrue(branch.canBePrivate(team))
472-
473- def test_commercial_project(self):
474- # Branches linked to commercial projects can be private.
475- product = self.factory.makeProduct()
476- self.factory.makeCommercialSubscription(product)
477- branch = self.factory.makeProductBranch(product=product)
478- user = self.factory.makePerson()
479- self.assertTrue(branch.canBePrivate(user))
480-
481- def test_linked_private_bug(self):
482- # Users with access to linked private bugs can make a branch private.
483- for info_type in PRIVATE_INFORMATION_TYPES:
484- user = self.factory.makePerson()
485- bug = self.factory.makeBug(
486- owner=user, information_type=info_type)
487- branch = self.factory.makeBranch()
488- removeSecurityProxy(bug).linkBranch(branch, user)
489- self.assertTrue(branch.canBePrivate(user))
490-
491- def test_linked_public_bug(self):
492- # Users with access to linked public bugs cannot make a branch private.
493- for info_type in PUBLIC_INFORMATION_TYPES:
494- user = self.factory.makePerson()
495- bug = self.factory.makeBug(
496- owner=user, information_type=info_type)
497- branch = self.factory.makeBranch()
498- removeSecurityProxy(bug).linkBranch(branch, user)
499- self.assertFalse(branch.canBePrivate(user))
500-
501-
502-class TestBranchCanBePublic(TestCaseWithFactory):
503- """Test IBranch.canBePublic."""
504-
505- layer = DatabaseFunctionalLayer
506-
507- def setUp(self):
508- # Use an admin user as we aren't checking edit permissions here.
509- TestCaseWithFactory.setUp(self, 'admin@canonical.com')
510-
511- def test_arbitrary_branch(self):
512- # By default branches can be public.
513- branch = self.factory.makeBranch(
514- information_type=InformationType.USERDATA)
515- self.assertTrue(branch.canBePublic(branch.owner))
516-
517- def test_visibility_policy_public_not_allowed(self):
518- # Branches cannot be public if the visibility policy forbids it.
519- team = self.factory.makeTeam(
520- subscription_policy=TeamSubscriptionPolicy.RESTRICTED)
521- product = self.factory.makeProduct()
522- product.setBranchVisibilityTeamPolicy(
523- team, BranchVisibilityRule.PRIVATE_ONLY)
524- branch = self.factory.makeBranch(
525- product=product, owner=team,
526- information_type=InformationType.USERDATA)
527- self.assertFalse(branch.canBePublic(team))
528-
529-
530 class TestBranchCommitsForDays(TestCaseWithFactory):
531 """Tests for `Branch.commitsForDays`."""
532