Merge lp:~cjwatson/launchpad/git-grantee-widgets into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18853
Proposed branch: lp:~cjwatson/launchpad/git-grantee-widgets
Merge into: lp:launchpad
Diff against target: 783 lines (+708/-3)
6 files modified
lib/lp/code/browser/widgets/gitgrantee.py (+245/-0)
lib/lp/code/browser/widgets/templates/gitgrantee.pt (+27/-0)
lib/lp/code/browser/widgets/tests/test_gitgrantee.py (+305/-0)
lib/lp/code/interfaces/gitrepository.py (+7/-1)
lib/lp/code/model/gitrepository.py (+12/-1)
lib/lp/code/model/tests/test_gitrepository.py (+112/-1)
To merge this branch: bzr merge lp:~cjwatson/launchpad/git-grantee-widgets
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+357600@code.launchpad.net

Commit message

Add widgets to display or select a Git access grantee.

Description of the change

This isn't hugely elegant, but it gets the job done. You can see it in context (with an as-yet-unpushed UI branch) here:

  https://people.canonical.com/~cjwatson/tmp/lp-git-ref-permissions.png

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)
Revision history for this message
Colin Watson (cjwatson) :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'lib/lp/code/browser/widgets/gitgrantee.py'
--- lib/lp/code/browser/widgets/gitgrantee.py 1970-01-01 00:00:00 +0000
+++ lib/lp/code/browser/widgets/gitgrantee.py 2019-01-09 10:45:43 +0000
@@ -0,0 +1,245 @@
1# Copyright 2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4from __future__ import absolute_import, print_function, unicode_literals
5
6__metaclass__ = type
7__all__ = [
8 'GitGranteeDisplayWidget',
9 'GitGranteeField',
10 'GitGranteeWidget',
11 ]
12
13from lazr.enum import DBItem
14from lazr.restful.fields import Reference
15from z3c.ptcompat import ViewPageTemplateFile
16from zope.formlib.interfaces import (
17 ConversionError,
18 IDisplayWidget,
19 IInputWidget,
20 InputErrors,
21 MissingInputError,
22 WidgetInputError,
23 )
24from zope.formlib.utility import setUpWidget
25from zope.formlib.widget import (
26 BrowserWidget,
27 CustomWidgetFactory,
28 DisplayWidget,
29 InputWidget,
30 renderElement,
31 )
32from zope.interface import implementer
33from zope.schema import (
34 Choice,
35 Field,
36 )
37from zope.schema.interfaces import IField
38from zope.schema.vocabulary import getVocabularyRegistry
39from zope.security.proxy import isinstance as zope_isinstance
40
41from lp import _
42from lp.app.browser.tales import PersonFormatterAPI
43from lp.app.errors import UnexpectedFormData
44from lp.app.validators import LaunchpadValidationError
45from lp.app.widgets.popup import PersonPickerWidget
46from lp.code.enums import GitGranteeType
47from lp.code.interfaces.gitrule import IGitRule
48from lp.registry.interfaces.person import IPerson
49from lp.services.webapp.interfaces import (
50 IAlwaysSubmittedWidget,
51 IMultiLineWidgetLayout,
52 )
53
54
55class IGitGranteeField(IField):
56 """An interface for a Git access grantee field."""
57
58 rule = Reference(
59 title=_("Rule"), required=True, readonly=True, schema=IGitRule,
60 description=_("The rule that this grantee is for."))
61
62
63@implementer(IGitGranteeField)
64class GitGranteeField(Field):
65 """A field that holds a Git access grantee."""
66
67 def __init__(self, rule, *args, **kwargs):
68 super(GitGranteeField, self).__init__(*args, **kwargs)
69 self.rule = rule
70
71 def constraint(self, value):
72 """See `IField`."""
73 if zope_isinstance(value, DBItem) and value.enum == GitGranteeType:
74 return value != GitGranteeType.PERSON
75 else:
76 return value in getVocabularyRegistry().get(
77 None, "ValidPersonOrTeam")
78
79
80@implementer(IDisplayWidget)
81class GitGranteePersonDisplayWidget(BrowserWidget):
82
83 def __init__(self, context, vocabulary, request):
84 super(GitGranteePersonDisplayWidget, self).__init__(context, request)
85
86 def __call__(self):
87 if self._renderedValueSet():
88 return PersonFormatterAPI(self._data).link(None)
89 else:
90 return ""
91
92
93@implementer(IMultiLineWidgetLayout)
94class GitGranteeWidgetBase(BrowserWidget):
95
96 template = ViewPageTemplateFile("templates/gitgrantee.pt")
97 default_option = "person"
98 _widgets_set_up = False
99
100 def setUpSubWidgets(self):
101 if self._widgets_set_up:
102 return
103 fields = [
104 Choice(
105 __name__="person", title=u"Person",
106 required=False, vocabulary="ValidPersonOrTeam"),
107 ]
108 if self._read_only:
109 self.person_widget = CustomWidgetFactory(
110 GitGranteePersonDisplayWidget)
111 else:
112 self.person_widget = CustomWidgetFactory(
113 PersonPickerWidget,
114 # XXX cjwatson 2018-10-18: This is a little unfortunate, but
115 # otherwise there's no spacing at all between the
116 # (deliberately unlabelled) radio button and the text box.
117 style="margin-left: 4px;")
118 for field in fields:
119 setUpWidget(
120 self, field.__name__, field, self._sub_widget_interface,
121 prefix=self.name)
122 self._widgets_set_up = True
123
124 def setUpOptions(self):
125 """Set up options to be rendered."""
126 self.options = {}
127 for option in ("repository_owner", "person"):
128 attributes = {
129 "type": "radio", "name": self.name, "value": option,
130 "id": "%s.option.%s" % (self.name, option),
131 # XXX cjwatson 2018-10-18: Ugly, but it's worse without
132 # this, especially in a permissions table where this widget
133 # is normally used.
134 "style": "margin-left: 0;",
135 }
136 if self.request.form_ng.getOne(
137 self.name, self.default_option) == option:
138 attributes["checked"] = "checked"
139 if self._read_only:
140 attributes["disabled"] = "disabled"
141 self.options[option] = renderElement("input", **attributes)
142
143 @property
144 def show_options(self):
145 return {
146 option: not self._read_only or self.default_option == option
147 for option in ("repository_owner", "person")}
148
149 def setRenderedValue(self, value):
150 """See `IWidget`."""
151 self.setUpSubWidgets()
152 if value == GitGranteeType.REPOSITORY_OWNER:
153 self.default_option = "repository_owner"
154 return
155 elif value is None or IPerson.providedBy(value):
156 self.default_option = "person"
157 self.person_widget.setRenderedValue(value)
158 return
159 else:
160 raise AssertionError("Not a valid value: %r" % value)
161
162 def __call__(self):
163 """See `zope.formlib.interfaces.IBrowserWidget`."""
164 self.setUpSubWidgets()
165 self.setUpOptions()
166 return self.template()
167
168
169@implementer(IDisplayWidget)
170class GitGranteeDisplayWidget(GitGranteeWidgetBase, DisplayWidget):
171 """Widget for displaying a Git access grantee."""
172
173 _sub_widget_interface = IDisplayWidget
174 _read_only = True
175
176
177@implementer(IAlwaysSubmittedWidget, IInputWidget)
178class GitGranteeWidget(GitGranteeWidgetBase, InputWidget):
179 """Widget for selecting a Git access grantee."""
180
181 _sub_widget_interface = IInputWidget
182 _read_only = False
183 _widgets_set_up = False
184
185 @property
186 def show_options(self):
187 show_options = super(GitGranteeWidget, self).show_options
188 # Hide options that indicate unique grantee_types (e.g.
189 # repository_owner) if they already exist for the context rule.
190 if (show_options["repository_owner"] and
191 not self.context.rule.repository.findRuleGrantsByGrantee(
192 GitGranteeType.REPOSITORY_OWNER,
193 ref_pattern=self.context.rule.ref_pattern,
194 include_transitive=False).is_empty()):
195 show_options["repository_owner"] = False
196 return show_options
197
198 def hasInput(self):
199 self.setUpSubWidgets()
200 form_value = self.request.form_ng.getOne(self.name)
201 if form_value is None:
202 return False
203 return form_value != "person" or self.person_widget.hasInput()
204
205 def hasValidInput(self):
206 """See `zope.formlib.interfaces.IInputWidget`."""
207 try:
208 self.getInputValue()
209 return True
210 except (InputErrors, UnexpectedFormData):
211 return False
212
213 def getInputValue(self):
214 """See `zope.formlib.interfaces.IInputWidget`."""
215 self.setUpSubWidgets()
216 form_value = self.request.form_ng.getOne(self.name)
217 if form_value == "repository_owner":
218 return GitGranteeType.REPOSITORY_OWNER
219 elif form_value == "person":
220 try:
221 return self.person_widget.getInputValue()
222 except MissingInputError:
223 raise WidgetInputError(
224 self.name, self.label,
225 LaunchpadValidationError(
226 "Please enter a person or team name"))
227 except ConversionError:
228 entered_name = self.request.form_ng.getOne(
229 "%s.person" % self.name)
230 raise WidgetInputError(
231 self.name, self.label,
232 LaunchpadValidationError(
233 "There is no person or team named '%s' registered in "
234 "Launchpad" % entered_name))
235 else:
236 raise UnexpectedFormData("No valid option was selected.")
237
238 def error(self):
239 """See `zope.formlib.interfaces.IBrowserWidget`."""
240 try:
241 if self.hasInput():
242 self.getInputValue()
243 except InputErrors as error:
244 self._error = error
245 return super(GitGranteeWidget, self).error()
0246
=== added file 'lib/lp/code/browser/widgets/templates/gitgrantee.pt'
--- lib/lp/code/browser/widgets/templates/gitgrantee.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/code/browser/widgets/templates/gitgrantee.pt 2019-01-09 10:45:43 +0000
@@ -0,0 +1,27 @@
1<table>
2 <tr tal:condition="view/show_options/repository_owner">
3 <td colspan="2">
4 <label>
5 <input
6 type="radio" value="repository_owner"
7 tal:condition="not: context/readonly"
8 tal:replace="structure view/options/repository_owner" />
9 Repository owner
10 </label>
11 </td>
12 </tr>
13
14 <tr tal:condition="view/show_options/person">
15 <td>
16 <label>
17 <input
18 type="radio" value="person"
19 tal:condition="not: context/readonly"
20 tal:replace="structure view/options/person" />
21 </label>
22 </td>
23 <td>
24 <tal:person replace="structure view/person_widget" />
25 </td>
26 </tr>
27</table>
028
=== added file 'lib/lp/code/browser/widgets/tests/test_gitgrantee.py'
--- lib/lp/code/browser/widgets/tests/test_gitgrantee.py 1970-01-01 00:00:00 +0000
+++ lib/lp/code/browser/widgets/tests/test_gitgrantee.py 2019-01-09 10:45:43 +0000
@@ -0,0 +1,305 @@
1# Copyright 2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4from __future__ import absolute_import, print_function, unicode_literals
5
6__metaclass__ = type
7
8import re
9
10from zope.formlib.interfaces import (
11 IBrowserWidget,
12 IDisplayWidget,
13 IInputWidget,
14 WidgetInputError,
15 )
16
17from lp.app.validators import LaunchpadValidationError
18from lp.code.browser.widgets.gitgrantee import (
19 GitGranteeDisplayWidget,
20 GitGranteeField,
21 GitGranteeWidget,
22 )
23from lp.code.enums import GitGranteeType
24from lp.registry.vocabularies import ValidPersonOrTeamVocabulary
25from lp.services.beautifulsoup import BeautifulSoup
26from lp.services.webapp.escaping import html_escape
27from lp.services.webapp.publisher import canonical_url
28from lp.services.webapp.servers import LaunchpadTestRequest
29from lp.testing import (
30 TestCaseWithFactory,
31 verifyObject,
32 )
33from lp.testing.layers import DatabaseFunctionalLayer
34
35
36class TestGitGranteeWidgetBase:
37
38 layer = DatabaseFunctionalLayer
39
40 def setUp(self):
41 super(TestGitGranteeWidgetBase, self).setUp()
42 [self.ref] = self.factory.makeGitRefs()
43 self.rule = self.factory.makeGitRule(
44 repository=self.ref.repository, ref_pattern=self.ref.path)
45 self.field = GitGranteeField(__name__="grantee", rule=self.rule)
46 self.request = LaunchpadTestRequest()
47 self.widget = self.widget_factory(self.field, self.request)
48
49 def test_implements(self):
50 self.assertTrue(verifyObject(IBrowserWidget, self.widget))
51 self.assertTrue(
52 verifyObject(self.expected_widget_interface, self.widget))
53
54 def test_template(self):
55 # The render template is setup.
56 self.assertTrue(
57 self.widget.template.filename.endswith("gitgrantee.pt"),
58 "Template was not set up.")
59
60 def test_default_option(self):
61 # The person field is the default option.
62 self.assertEqual("person", self.widget.default_option)
63
64 def test_setUpSubWidgets_first_call(self):
65 # The subwidget is set up and a flag is set.
66 self.widget.setUpSubWidgets()
67 self.assertTrue(self.widget._widgets_set_up)
68 self.assertIsInstance(
69 self.widget.person_widget.context.vocabulary,
70 ValidPersonOrTeamVocabulary)
71
72 def test_setUpSubWidgets_second_call(self):
73 # The setUpSubWidgets method exits early if a flag is set to
74 # indicate that the subwidget was set up.
75 self.widget._widgets_set_up = True
76 self.widget.setUpSubWidgets()
77 self.assertIsNone(getattr(self.widget, "person_widget", None))
78
79 def test_setUpOptions_default_person_checked(self):
80 # The radio button options are composed of the setup widgets with
81 # the person widget set as the default.
82 self.widget.setUpSubWidgets()
83 self.widget.setUpOptions()
84 self.assertEqual(
85 '<input class="radioType" style="margin-left: 0;" ' +
86 self.expected_disabled_attr +
87 'id="field.grantee.option.repository_owner" name="field.grantee" '
88 'type="radio" value="repository_owner" />',
89 self.widget.options["repository_owner"])
90 self.assertEqual(
91 '<input class="radioType" style="margin-left: 0;" ' +
92 'checked="checked" ' + self.expected_disabled_attr +
93 'id="field.grantee.option.person" name="field.grantee" '
94 'type="radio" value="person" />',
95 self.widget.options["person"])
96
97 def test_setUpOptions_repository_owner_checked(self):
98 # The repository owner radio button is selected when the form is
99 # submitted when the grantee field's value is 'repository_owner'.
100 form = {"field.grantee": "repository_owner"}
101 self.widget.request = LaunchpadTestRequest(form=form)
102 self.widget.setUpSubWidgets()
103 self.widget.setUpOptions()
104 self.assertEqual(
105 '<input class="radioType" style="margin-left: 0;" '
106 'checked="checked" ' + self.expected_disabled_attr +
107 'id="field.grantee.option.repository_owner" name="field.grantee" '
108 'type="radio" value="repository_owner" />',
109 self.widget.options["repository_owner"])
110 self.assertEqual(
111 '<input class="radioType" style="margin-left: 0;" ' +
112 self.expected_disabled_attr +
113 'id="field.grantee.option.person" name="field.grantee" '
114 'type="radio" value="person" />',
115 self.widget.options["person"])
116
117 def test_setUpOptions_person_checked(self):
118 # The person radio button is selected when the form is submitted
119 # when the grantee field's value is 'person'.
120 form = {"field.grantee": "person"}
121 self.widget.request = LaunchpadTestRequest(form=form)
122 self.widget.setUpSubWidgets()
123 self.widget.setUpOptions()
124 self.assertEqual(
125 '<input class="radioType" style="margin-left: 0;" ' +
126 self.expected_disabled_attr +
127 'id="field.grantee.option.repository_owner" name="field.grantee" '
128 'type="radio" value="repository_owner" />',
129 self.widget.options["repository_owner"])
130 self.assertEqual(
131 '<input class="radioType" style="margin-left: 0;" ' +
132 'checked="checked" ' + self.expected_disabled_attr +
133 'id="field.grantee.option.person" name="field.grantee" '
134 'type="radio" value="person" />',
135 self.widget.options["person"])
136
137 def test_setRenderedValue_repository_owner(self):
138 # Passing GitGranteeType.REPOSITORY_OWNER will set the widget's
139 # render state to "repository_owner".
140 self.widget.setUpSubWidgets()
141 self.widget.setRenderedValue(GitGranteeType.REPOSITORY_OWNER)
142 self.assertEqual("repository_owner", self.widget.default_option)
143
144 def test_setRenderedValue_person(self):
145 # Passing a person will set the widget's render state to "person".
146 self.widget.setUpSubWidgets()
147 person = self.factory.makePerson()
148 self.widget.setRenderedValue(person)
149 self.assertEqual("person", self.widget.default_option)
150 self.assertEqual(person, self.widget.person_widget._data)
151
152 def test_call(self):
153 # The __call__ method sets up the widgets and the options.
154 markup = self.widget()
155 self.assertIsNotNone(self.widget.person_widget)
156 self.assertIn("repository_owner", self.widget.options)
157 self.assertIn("person", self.widget.options)
158 soup = BeautifulSoup(markup)
159 fields = soup.findAll(["input", "select"], {"id": re.compile(".*")})
160 ids = [field["id"] for field in fields]
161 self.assertContentEqual(self.expected_ids, ids)
162
163
164class TestGitGranteeDisplayWidget(
165 TestGitGranteeWidgetBase, TestCaseWithFactory):
166 """Test the GitGranteeDisplayWidget class."""
167
168 widget_factory = GitGranteeDisplayWidget
169 expected_widget_interface = IDisplayWidget
170 expected_disabled_attr = 'disabled="disabled" '
171 expected_ids = ["field.grantee.option.person"]
172
173 def test_setRenderedValue_person_display_widget(self):
174 # If the widget's render state is "person", a customised display
175 # widget is used.
176 self.widget.setUpSubWidgets()
177 person = self.factory.makePerson()
178 self.widget.setRenderedValue(person)
179 person_url = canonical_url(person)
180 self.assertEqual(
181 '<a href="%s">'
182 '<img style="padding-bottom: 2px" alt="" src="/@@/person" /> '
183 '%s</a>' % (person_url, html_escape(person.display_name)),
184 self.widget.person_widget())
185
186
187class TestGitGranteeWidget(TestGitGranteeWidgetBase, TestCaseWithFactory):
188 """Test the GitGranteeWidget class."""
189
190 widget_factory = GitGranteeWidget
191 expected_widget_interface = IInputWidget
192 expected_disabled_attr = ""
193 expected_ids = [
194 "field.grantee.option.repository_owner",
195 "field.grantee.option.person",
196 "field.grantee.person",
197 ]
198
199 def setUp(self):
200 super(TestGitGranteeWidget, self).setUp()
201 self.person = self.factory.makePerson()
202
203 def test_show_options_repository_owner_grant_already_exists(self):
204 # If the rule already has a repository owner grant, the input widget
205 # doesn't offer that option.
206 self.factory.makeGitRuleGrant(
207 rule=self.rule, grantee=GitGranteeType.REPOSITORY_OWNER)
208 self.assertEqual(
209 {"repository_owner": False, "person": True},
210 self.widget.show_options)
211
212 def test_show_options_repository_owner_grant_does_not_exist(self):
213 # If the rule doesn't have a repository owner grant, the input
214 # widget offers that option.
215 self.factory.makeGitRuleGrant(rule=self.rule)
216 self.assertEqual(
217 {"repository_owner": True, "person": True},
218 self.widget.show_options)
219
220 @property
221 def form(self):
222 return {
223 "field.grantee": "person",
224 "field.grantee.person": self.person.name,
225 }
226
227 def test_hasInput_not_in_form(self):
228 # hasInput is false when the widget's name is not in the form data.
229 self.widget.request = LaunchpadTestRequest(form={})
230 self.assertEqual("field.grantee", self.widget.name)
231 self.assertFalse(self.widget.hasInput())
232
233 def test_hasInput_no_person(self):
234 # hasInput is false when the person radio button is selected and the
235 # person widget's name is not in the form data.
236 self.widget.request = LaunchpadTestRequest(
237 form={"field.grantee": "person"})
238 self.assertEqual("field.grantee", self.widget.name)
239 self.assertFalse(self.widget.hasInput())
240
241 def test_hasInput_repository_owner(self):
242 # hasInput is true when the repository owner radio button is selected.
243 self.widget.request = LaunchpadTestRequest(
244 form={"field.grantee": "repository_owner"})
245 self.assertEqual("field.grantee", self.widget.name)
246 self.assertTrue(self.widget.hasInput())
247
248 def test_hasInput_person(self):
249 # hasInput is true when the person radio button is selected and the
250 # person widget's name is in the form data.
251 self.widget.request = LaunchpadTestRequest(form=self.form)
252 self.assertEqual("field.grantee", self.widget.name)
253 self.assertTrue(self.widget.hasInput())
254
255 def test_hasValidInput_true(self):
256 # The field input is valid when all submitted parts are valid.
257 self.widget.request = LaunchpadTestRequest(form=self.form)
258 self.assertTrue(self.widget.hasValidInput())
259
260 def test_hasValidInput_false(self):
261 # The field input is invalid if any of the submitted parts are invalid.
262 form = self.form
263 form["field.grantee.person"] = "non-existent"
264 self.widget.request = LaunchpadTestRequest(form=form)
265 self.assertFalse(self.widget.hasValidInput())
266
267 def test_getInputValue_repository_owner(self):
268 # The field value is GitGranteeType.REPOSITORY_OWNER when the
269 # repository owner radio button is selected.
270 form = self.form
271 form["field.grantee"] = "repository_owner"
272 self.widget.request = LaunchpadTestRequest(form=form)
273 self.assertEqual(
274 GitGranteeType.REPOSITORY_OWNER, self.widget.getInputValue())
275
276 def test_getInputValue_person(self):
277 # The field value is the person when the person radio button is
278 # selected and the person sub field is valid.
279 form = self.form
280 form["field.grantee"] = "person"
281 self.widget.request = LaunchpadTestRequest(form=form)
282 self.assertEqual(self.person, self.widget.getInputValue())
283
284 def test_getInputValue_person_missing(self):
285 # An error is raised when the person field is missing.
286 form = self.form
287 form["field.grantee"] = "person"
288 del form["field.grantee.person"]
289 self.widget.request = LaunchpadTestRequest(form=form)
290 message = "Please enter a person or team name"
291 e = self.assertRaises(WidgetInputError, self.widget.getInputValue)
292 self.assertEqual(LaunchpadValidationError(message), e.errors)
293
294 def test_getInputValue_person_invalid(self):
295 # An error is raised when the person is not valid.
296 form = self.form
297 form["field.grantee"] = "person"
298 form["field.grantee.person"] = "non-existent"
299 self.widget.request = LaunchpadTestRequest(form=form)
300 message = (
301 "There is no person or team named 'non-existent' registered in "
302 "Launchpad")
303 e = self.assertRaises(WidgetInputError, self.widget.getInputValue)
304 self.assertEqual(LaunchpadValidationError(message), e.errors)
305 self.assertEqual(html_escape(message), self.widget.error())
0306
=== modified file 'lib/lp/code/interfaces/gitrepository.py'
--- lib/lp/code/interfaces/gitrepository.py 2018-11-21 00:54:42 +0000
+++ lib/lp/code/interfaces/gitrepository.py 2019-01-09 10:45:43 +0000
@@ -766,12 +766,18 @@
766 :param user: The `IPerson` who is moving the rule.766 :param user: The `IPerson` who is moving the rule.
767 """767 """
768768
769 def findRuleGrantsByGrantee(grantee):769 def findRuleGrantsByGrantee(grantee, include_transitive=True,
770 ref_pattern=None):
770 """Find the grants for a grantee applied to this repository.771 """Find the grants for a grantee applied to this repository.
771772
772 :param grantee: The `IPerson` to search for, or an item of773 :param grantee: The `IPerson` to search for, or an item of
773 `GitGranteeType` other than `GitGranteeType.PERSON` to search774 `GitGranteeType` other than `GitGranteeType.PERSON` to search
774 for some other kind of entity.775 for some other kind of entity.
776 :param include_transitive: If False, match `grantee` exactly; if
777 True (the default), also accept teams of which `grantee` is a
778 member.
779 :param ref_pattern: If not None, only return grants for rules with
780 this ref_pattern.
775 """781 """
776782
777 @export_read_operation()783 @export_read_operation()
778784
=== modified file 'lib/lp/code/model/gitrepository.py'
--- lib/lp/code/model/gitrepository.py 2018-12-10 13:54:34 +0000
+++ lib/lp/code/model/gitrepository.py 2019-01-09 10:45:43 +0000
@@ -1224,7 +1224,8 @@
1224 return Store.of(self).find(1224 return Store.of(self).find(
1225 GitRuleGrant, GitRuleGrant.repository_id == self.id)1225 GitRuleGrant, GitRuleGrant.repository_id == self.id)
12261226
1227 def findRuleGrantsByGrantee(self, grantee):1227 def findRuleGrantsByGrantee(self, grantee, include_transitive=True,
1228 ref_pattern=None):
1228 """See `IGitRepository`."""1229 """See `IGitRepository`."""
1229 if isinstance(grantee, DBItem) and grantee.enum == GitGranteeType:1230 if isinstance(grantee, DBItem) and grantee.enum == GitGranteeType:
1230 if grantee == GitGranteeType.PERSON:1231 if grantee == GitGranteeType.PERSON:
@@ -1232,12 +1233,22 @@
1232 "grantee may not be GitGranteeType.PERSON; pass a person "1233 "grantee may not be GitGranteeType.PERSON; pass a person "
1233 "object instead")1234 "object instead")
1234 clauses = [GitRuleGrant.grantee_type == grantee]1235 clauses = [GitRuleGrant.grantee_type == grantee]
1236 elif not include_transitive:
1237 clauses = [
1238 GitRuleGrant.grantee_type == GitGranteeType.PERSON,
1239 GitRuleGrant.grantee == grantee,
1240 ]
1235 else:1241 else:
1236 clauses = [1242 clauses = [
1237 GitRuleGrant.grantee_type == GitGranteeType.PERSON,1243 GitRuleGrant.grantee_type == GitGranteeType.PERSON,
1238 TeamParticipation.person == grantee,1244 TeamParticipation.person == grantee,
1239 GitRuleGrant.grantee == TeamParticipation.teamID1245 GitRuleGrant.grantee == TeamParticipation.teamID
1240 ]1246 ]
1247 if ref_pattern is not None:
1248 clauses.extend([
1249 GitRuleGrant.rule_id == GitRule.id,
1250 GitRule.ref_pattern == ref_pattern,
1251 ])
1241 return self.grants.find(*clauses).config(distinct=True)1252 return self.grants.find(*clauses).config(distinct=True)
12421253
1243 def getRules(self):1254 def getRules(self):
12441255
=== modified file 'lib/lp/code/model/tests/test_gitrepository.py'
--- lib/lp/code/model/tests/test_gitrepository.py 2018-11-22 16:35:28 +0000
+++ lib/lp/code/model/tests/test_gitrepository.py 2019-01-09 10:45:43 +0000
@@ -265,7 +265,7 @@
265 grant = self.factory.makeGitRuleGrant(265 grant = self.factory.makeGitRuleGrant(
266 rule=rule, grantee=requester, can_push=True, can_create=True)266 rule=rule, grantee=requester, can_push=True, can_create=True)
267267
268 results = repository.findRuleGrantsByGrantee(requester)268 results = repository.findRuleGrantsByGrantee(member)
269 self.assertEqual([grant], list(results))269 self.assertEqual([grant], list(results))
270270
271 def test_findRuleGrantsByGrantee_team_in_team(self):271 def test_findRuleGrantsByGrantee_team_in_team(self):
@@ -357,6 +357,117 @@
357 results = repository.findRuleGrantsByGrantee(requester)357 results = repository.findRuleGrantsByGrantee(requester)
358 self.assertEqual([owner_grant], list(results))358 self.assertEqual([owner_grant], list(results))
359359
360 def test_findRuleGrantsByGrantee_ref_pattern(self):
361 requester = self.factory.makePerson()
362 repository = removeSecurityProxy(
363 self.factory.makeGitRepository(owner=requester))
364 [ref] = self.factory.makeGitRefs(repository=repository)
365
366 exact_grant = self.factory.makeGitRuleGrant(
367 repository=repository, ref_pattern=ref.path, grantee=requester)
368 self.factory.makeGitRuleGrant(
369 repository=repository, ref_pattern="refs/heads/*",
370 grantee=requester)
371
372 results = repository.findRuleGrantsByGrantee(
373 requester, ref_pattern=ref.path)
374 self.assertEqual([exact_grant], list(results))
375
376 def test_findRuleGrantsByGrantee_exclude_transitive_person(self):
377 requester = self.factory.makePerson()
378 repository = removeSecurityProxy(
379 self.factory.makeGitRepository(owner=requester))
380
381 rule = self.factory.makeGitRule(repository)
382 grant = self.factory.makeGitRuleGrant(rule=rule, grantee=requester)
383
384 results = repository.findRuleGrantsByGrantee(
385 requester, include_transitive=False)
386 self.assertEqual([grant], list(results))
387
388 def test_findRuleGrantsByGrantee_exclude_transitive_team(self):
389 team = self.factory.makeTeam()
390 repository = removeSecurityProxy(
391 self.factory.makeGitRepository(owner=team))
392
393 rule = self.factory.makeGitRule(repository)
394 grant = self.factory.makeGitRuleGrant(rule=rule, grantee=team)
395
396 results = repository.findRuleGrantsByGrantee(
397 team, include_transitive=False)
398 self.assertEqual([grant], list(results))
399
400 def test_findRuleGrantsByGrantee_exclude_transitive_member_of_team(self):
401 member = self.factory.makePerson()
402 team = self.factory.makeTeam(members=[member])
403 repository = removeSecurityProxy(
404 self.factory.makeGitRepository(owner=team))
405
406 rule = self.factory.makeGitRule(repository)
407 self.factory.makeGitRuleGrant(rule=rule, grantee=team)
408
409 results = repository.findRuleGrantsByGrantee(
410 member, include_transitive=False)
411 self.assertEqual([], list(results))
412
413 def test_findRuleGrantsByGrantee_no_owner_grant(self):
414 repository = removeSecurityProxy(self.factory.makeGitRepository())
415
416 rule = self.factory.makeGitRule(repository=repository)
417 self.factory.makeGitRuleGrant(rule=rule)
418
419 results = repository.findRuleGrantsByGrantee(
420 GitGranteeType.REPOSITORY_OWNER)
421 self.assertEqual([], list(results))
422
423 def test_findRuleGrantsByGrantee_owner_grant(self):
424 repository = removeSecurityProxy(self.factory.makeGitRepository())
425
426 rule = self.factory.makeGitRule(repository=repository)
427 grant = self.factory.makeGitRuleGrant(
428 rule=rule, grantee=GitGranteeType.REPOSITORY_OWNER)
429 self.factory.makeGitRuleGrant(rule=rule)
430
431 results = repository.findRuleGrantsByGrantee(
432 GitGranteeType.REPOSITORY_OWNER)
433 self.assertEqual([grant], list(results))
434
435 def test_findRuleGrantsByGrantee_owner_ref_pattern(self):
436 repository = removeSecurityProxy(self.factory.makeGitRepository())
437 [ref] = self.factory.makeGitRefs(repository=repository)
438
439 exact_grant = self.factory.makeGitRuleGrant(
440 repository=repository, ref_pattern=ref.path,
441 grantee=GitGranteeType.REPOSITORY_OWNER)
442 self.factory.makeGitRuleGrant(
443 repository=repository, ref_pattern="refs/heads/*",
444 grantee=GitGranteeType.REPOSITORY_OWNER)
445
446 results = ref.repository.findRuleGrantsByGrantee(
447 GitGranteeType.REPOSITORY_OWNER, ref_pattern=ref.path)
448 self.assertEqual([exact_grant], list(results))
449
450 def test_findRuleGrantsByGrantee_owner_exclude_transitive(self):
451 repository = removeSecurityProxy(self.factory.makeGitRepository())
452 [ref] = self.factory.makeGitRefs(repository=repository)
453
454 exact_grant = self.factory.makeGitRuleGrant(
455 repository=repository, ref_pattern=ref.path,
456 grantee=GitGranteeType.REPOSITORY_OWNER)
457 self.factory.makeGitRuleGrant(
458 rule=exact_grant.rule, grantee=repository.owner)
459 wildcard_grant = self.factory.makeGitRuleGrant(
460 repository=repository, ref_pattern="refs/heads/*",
461 grantee=GitGranteeType.REPOSITORY_OWNER)
462
463 results = ref.repository.findRuleGrantsByGrantee(
464 GitGranteeType.REPOSITORY_OWNER, include_transitive=False)
465 self.assertItemsEqual([exact_grant, wildcard_grant], list(results))
466 results = ref.repository.findRuleGrantsByGrantee(
467 GitGranteeType.REPOSITORY_OWNER, ref_pattern=ref.path,
468 include_transitive=False)
469 self.assertEqual([exact_grant], list(results))
470
360471
361class TestGitIdentityMixin(TestCaseWithFactory):472class TestGitIdentityMixin(TestCaseWithFactory):
362 """Test the defaults and identities provided by GitIdentityMixin."""473 """Test the defaults and identities provided by GitIdentityMixin."""